mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 15:33:45 +08:00
228 lines
12 KiB
Python
228 lines
12 KiB
Python
"""Current-cycle StarPilot metadata precedes steering without changing CAN traffic."""
|
|
|
|
from collections import Counter
|
|
from types import SimpleNamespace as NS
|
|
from typing import Any, cast
|
|
|
|
import pytest
|
|
|
|
from opendbc.can import CANPacker
|
|
from opendbc.car import gen_empty_fingerprint, structs
|
|
from opendbc.car.ford.carcontroller import CarController
|
|
from opendbc.car.ford.carstate import CarState
|
|
from opendbc.car.ford.interface import CarInterface
|
|
from opendbc.car.ford.tests.test_bluepilot_lateral import STARTUP
|
|
from opendbc.car.ford.values import CAR, DBC, FordFlags
|
|
from opendbc.bluepilot_lateral.hosts.starpilot.safety_param import init_ford_safety_param_starpilot
|
|
from opendbc.bluepilot_lateral.core.lateral_curv_ext import PrimaryLateralControl
|
|
from opendbc.safety.tests.libsafety import libsafety_py
|
|
from openpilot.starpilot.car.ford.bluepilot import FordBluePilotController
|
|
from openpilot.starpilot.car.ford.controller_selection import FordController
|
|
from openpilot.starpilot.controller_extensions import order_controller_can
|
|
from openpilot.starpilot.tests.test_ford_native_pinion_guard import LearnedGeometryInputs
|
|
|
|
|
|
def native_run(identity, direction, *, ordered, invalid_shadow=False, withdraw=False):
|
|
fp = gen_empty_fingerprint()
|
|
fp[0][0x5A] = 8
|
|
fp[2].update({0x3D6: 8, 0x186: 8})
|
|
cp = CarInterface.get_params(identity, fp, [], False, False, False)
|
|
init_ford_safety_param_starpilot(cp, STARTUP['on+pinion'])
|
|
controller = CarController(DBC[cp.carFingerprint], cp)
|
|
inputs = LearnedGeometryInputs(cp, .85, -direction * .04)
|
|
controller.bluepilot_inputs = inputs
|
|
controller.bp = FordBluePilotController(cp, controller.packer, controller.CAN, inputs)
|
|
ci = NS(CP=cp, CC=controller)
|
|
state = CarState(cp)
|
|
state.update(state.get_can_parsers(cp))
|
|
safety, packer = libsafety_py.libsafety, CANPacker('ford_lincoln_base_pt')
|
|
safety.init_tests()
|
|
safety.set_alternative_experience(0)
|
|
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, cp.safetyConfigs[-1].safetyParam) == 0
|
|
# Standalone native diagnostic; the actual factory/authority lifecycle is tested separately.
|
|
safety.set_controls_allowed(True)
|
|
blocked, traffic, changes = [], [], []
|
|
try:
|
|
for frame in range(45):
|
|
safety.set_timer(1_000_000 + frame * 10_000)
|
|
received = []
|
|
|
|
def rx(name, values, received=received):
|
|
address, data, bus = packer.make_can_msg(name, 0, values)
|
|
assert safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data))
|
|
received.append((address, data, bus))
|
|
|
|
if frame % 2 == 0:
|
|
rx('BrakeSysFeatures', {'Veh_V_ActlBrk': 108., 'VehVActlBrk_D_Qf': 3, 'VehVActlBrk_No_Cnt': frame // 2 % 16})
|
|
rx('EngVehicleSpThrottle2', {'Veh_V_ActlEng': 108., 'VehVActlEng_D_Qf': 3})
|
|
rx('DesiredTorqBrk', {'VehStop_D_Stat': 0})
|
|
rx('Yaw_Data_FD1', {'VehYaw_W_Actl': 0., 'VehYawWActl_D_Qf': 3, 'VehRollYaw_No_Cnt': frame % 256})
|
|
rx('EngVehicleSpThrottle', {'ApedPos_Pc_ActlArb': 0})
|
|
if frame % 10 == 0:
|
|
rx('EngBrakeData', {'BpedDrvAppl_D_Actl': 1, 'CcStat_D_Actl': 5})
|
|
rx('Steering_Data_FD1', {})
|
|
angle = direction * (30. - .5 * max(0, min(frame - 24, 6)))
|
|
rx('SteeringPinion_Data', {'StePinComp_An_Est': angle, 'StePinCompAnEst_D_Qf': 3, 'StePinAn_No_Cnt': frame % 16})
|
|
controller.bp.observe_can([(0, received)])
|
|
cs = structs.CarState(vEgo=30., vEgoRaw=30., steeringAngleDeg=angle, canValid=True,
|
|
gearShifter=structs.CarState.GearShifter.drive)
|
|
cs.cruiseState.available = cs.cruiseState.enabled = True
|
|
state.out = cs.as_reader()
|
|
cc = structs.CarControl(enabled=True, latActive=True)
|
|
cc.actuators.curvature = -direction * .02 if 25 <= frame < 30 else 0.
|
|
_, original = controller.update(cc.as_reader(), state, (1_000_000 + frame * 10_000) * 1000)
|
|
packets = order_controller_can(ci, original) if ordered else original
|
|
assert Counter(packets) == Counter(original)
|
|
if packets != original:
|
|
changes.append(frame)
|
|
traffic.extend((frame, address, bus) for address, _, bus in packets)
|
|
if frame == 30 and invalid_shadow:
|
|
packets = [(a, d[:5] + b'\x7f\xff' + d[7:] if a == 0x3CA else d, b) for a, d, b in packets]
|
|
if frame == 30 and withdraw:
|
|
safety.set_controls_allowed(False)
|
|
for address, data, bus in packets:
|
|
if address in (0x3CA, 0x3D3, 0x3D6) and not safety.safety_tx_hook(libsafety_py.make_CANPacket(address, bus, data)):
|
|
blocked.append((frame, address))
|
|
if frame == 30 and withdraw:
|
|
assert not safety.get_controls_allowed()
|
|
return blocked, traffic, changes, 0x3D6 if cp.flags & FordFlags.CANFD else 0x3D3
|
|
finally:
|
|
safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0)
|
|
|
|
|
|
@pytest.mark.parametrize('message,signal,value', [
|
|
('ACCDATA_3', 'FcwVisblWarn_B_Rq', 1), ('ACCDATA_3', 'FcwAudioWarn_B_Rq', 1),
|
|
('ACCDATA_3', 'TjaWarn_D_Rq', 3), ('ACCDATA_3', 'TjaMsgTxt_D_Dsply', 1),
|
|
('ACCDATA_3', 'AccMsgTxt_D2_Rq', 10), ('ACCDATA_3', 'AccWarn_D_Dsply', 1),
|
|
('ACCDATA_3', 'FcwMsgTxt_D_Rq', 2), ('ACCDATA_3', 'Tja_D_Stat', 5),
|
|
('IPMA_Data', 'LaHandsOff_D_Dsply', 1), ('IPMA_Data', 'LaDenyStats_B_Dsply', 1),
|
|
('IPMA_Data', 'CamraStats_D_Dsply', 3), ('IPMA_Data', 'DasWarn_D_Dsply', 2),
|
|
('IPMA_Data', 'LaActvStats_D_Dsply', 3),
|
|
])
|
|
def test_classic_pause_preserves_dashboard_warnings(message, signal, value):
|
|
fp = gen_empty_fingerprint()
|
|
fp[0][0x5A] = 8
|
|
cp = CarInterface.get_params(CAR.FORD_EXPLORER_MK6, fp, [], True, False, False)
|
|
cp.alternativeExperience = 32
|
|
controller = cast(Any, FordBluePilotController.__new__(FordBluePilotController))
|
|
controller.CP, controller.CAN = cp, NS(main=0)
|
|
controller.lateral_active = controller.starpilot_selected = False
|
|
packet = CANPacker('ford_lincoln_base_pt').make_can_msg(message, 0, {signal: value})
|
|
assert controller.order_can_sends([packet]) == [packet]
|
|
|
|
|
|
@pytest.mark.parametrize('identity', [CAR.FORD_MUSTANG_MACH_E_MK1, CAR.FORD_EXPLORER_MK6, CAR.FORD_F_150_MK14])
|
|
@pytest.mark.parametrize('direction', [-1., 1.])
|
|
def test_current_shadow_prevents_coincident_native_refusal_without_extra_packets(identity, direction):
|
|
previous, old_traffic, _, address = native_run(identity, direction, ordered=False)
|
|
current, traffic, changes, current_address = native_run(identity, direction, ordered=True)
|
|
assert address == current_address
|
|
assert previous == [(30, address)]
|
|
assert not current, current
|
|
assert Counter(traffic) == Counter(old_traffic)
|
|
assert changes and all(frame % 15 == 0 for frame in changes)
|
|
|
|
|
|
@pytest.mark.parametrize('negative', ['shadow', 'permission'])
|
|
def test_ordering_cannot_make_invalid_metadata_or_denied_authority_steer(negative):
|
|
blocked, _, _, address = native_run(CAR.FORD_MUSTANG_MACH_E_MK1, 1., ordered=True,
|
|
invalid_shadow=negative == 'shadow', withdraw=negative == 'permission')
|
|
assert (30, address) in blocked
|
|
|
|
|
|
@pytest.mark.parametrize('main', [0, 4])
|
|
@pytest.mark.parametrize('canfd', [False, True])
|
|
def test_ordering_preserves_buses_payloads_and_noncoincident_cadences(main, canfd):
|
|
controller = cast(Any, FordBluePilotController.__new__(FordBluePilotController))
|
|
controller.starpilot_selected = True
|
|
controller.curv = NS(primary_lateral_control=PrimaryLateralControl.angle)
|
|
controller.CAN = NS(main=main)
|
|
controller.CP = structs.CarParams(flags=int(FordFlags.CANFD if canfd else 0)).as_reader()
|
|
lmc = (0x3D6 if canfd else 0x3D3, bytes(8), main)
|
|
lka = (0x3CA, bytes(8), main)
|
|
unrelated = (0x165, b'12345678', main + 2)
|
|
for frame in range(15):
|
|
controller.frame = frame
|
|
packets = [unrelated]
|
|
if frame % 5 == 0:
|
|
packets.append(lmc)
|
|
if frame % 3 == 0:
|
|
packets.append(lka)
|
|
result = controller.order_can_sends(packets)
|
|
assert Counter(result) == Counter(packets)
|
|
assert result[0] == unrelated
|
|
if frame == 0:
|
|
assert result == [unrelated, lka, lmc]
|
|
else:
|
|
assert result is packets
|
|
controller.frame = 0
|
|
for packets in ([lmc, (lka[0], lka[1], main + 2)], [lmc, lka, lka], [lmc, lmc, lka], [lka, lmc]):
|
|
assert controller.order_can_sends(packets) is packets
|
|
controller.starpilot_selected = False
|
|
assert controller.order_can_sends([lmc, lka]) == [lmc, lka]
|
|
controller.starpilot_selected = True
|
|
controller.curv.primary_lateral_control = PrimaryLateralControl.curvature
|
|
assert controller.order_can_sends([lmc, lka]) == [lmc, lka]
|
|
|
|
|
|
def test_steering_ordering_is_required_in_both_native_ci_builds():
|
|
from tools.ci.run_vehicle_tests import targets_for
|
|
path = 'openpilot/starpilot/tests/test_ford_starpilot_ordering.py'
|
|
assert targets_for('safety-debug').count(path) == 1
|
|
assert targets_for('safety-release').count(path) == 1
|
|
|
|
|
|
@pytest.mark.parametrize('identity', [CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_ESCAPE_MK4, CAR.FORD_EXPLORER_MK6,
|
|
CAR.FORD_FOCUS_MK4, CAR.FORD_MAVERICK_MK1,
|
|
CAR.FORD_MUSTANG_MACH_E_MK1, CAR.FORD_F_150_MK14])
|
|
@pytest.mark.parametrize('alpha', [False, True])
|
|
@pytest.mark.parametrize('choice', list(FordController))
|
|
@pytest.mark.parametrize('aol', [False, True])
|
|
def test_classic_aol_pause_returns_dashboard_to_camera_without_changing_actuators(identity, alpha, choice, aol):
|
|
fp = gen_empty_fingerprint()
|
|
fp[0][0x5A] = 8
|
|
fp[2].update({0x3D3: 8, 0x186: 8})
|
|
cp = CarInterface.get_params(identity, fp, [], alpha, False, False)
|
|
cp.alternativeExperience = 32 if aol else 0
|
|
init_ford_safety_param_starpilot(cp, STARTUP['on+pinion'])
|
|
controller = CarController(DBC[cp.carFingerprint], cp)
|
|
inputs = LearnedGeometryInputs(cp, 1., 0.)
|
|
inputs.controller_choice = choice
|
|
inputs.settings['FordPrefLateralControl'] = b'0' if choice == FordController.BLUEPILOT_CURVATURE else b'1'
|
|
controller.bluepilot_inputs = inputs
|
|
controller.bp = FordBluePilotController(cp, controller.packer, controller.CAN, inputs)
|
|
ci = NS(CP=cp, CC=controller)
|
|
state = CarState(cp)
|
|
state.update(state.get_can_parsers(cp))
|
|
safety = libsafety_py.libsafety
|
|
safety.init_tests()
|
|
safety.set_alternative_experience(cp.alternativeExperience)
|
|
assert safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, cp.safetyConfigs[-1].safetyParam) == 0
|
|
try:
|
|
for frame in range(11):
|
|
active = frame < 5 or frame >= 10
|
|
cs = structs.CarState(vEgo=10. if active else 0., vEgoRaw=10. if active else 0., standstill=not active,
|
|
canValid=True, gearShifter=structs.CarState.GearShifter.drive)
|
|
cs.cruiseState.available = cs.cruiseState.enabled = True
|
|
cs.cruiseState.standstill = not active
|
|
state.out = cs.as_reader()
|
|
cc = structs.CarControl(enabled=True, latActive=active, longActive=alpha)
|
|
cc.actuators.longControlState = structs.CarControl.Actuators.LongControlState.stopping
|
|
_, original = controller.update(cc.as_reader(), state, (1_000_000 + frame * 10_000) * 1000)
|
|
actual = order_controller_can(ci, original)
|
|
ui = [(a, d, b) for a, d, b in original if a in (0x18A, 0x3D8) and b == controller.CAN.main]
|
|
if active or not aol or cp.flags & FordFlags.CANFD:
|
|
assert Counter(actual) == Counter(original)
|
|
else:
|
|
assert Counter(actual) == Counter(original) - Counter(ui)
|
|
if frame == 5:
|
|
assert {a for a, _, _ in ui} == {0x18A, 0x3D8}
|
|
assert any(a == 0x3D3 for a, _, _ in actual)
|
|
# The native camera path stays available; no authority is seeded by this routing test.
|
|
for address in (0x18A, 0x3D8):
|
|
assert safety.safety_fwd_hook(2, address) == 0
|
|
if frame == 6:
|
|
assert any(a == 0x186 for a, _, _ in actual) == alpha
|
|
finally:
|
|
safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0)
|