Complete Ford release long manual and multi-Panda support

This commit is contained in:
firestar5683
2026-10-22 22:49:49 -05:00
parent e24c677109
commit aaa2f687ea
54 changed files with 979 additions and 204 deletions
+31 -7
View File
@@ -1,18 +1,43 @@
from opendbc.car import structs
from opendbc.car.ford.values import CAR, FordFlags
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
AOL_EXPERIENCE = 32
CLASSIC = frozenset((CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_ESCAPE_MK4, CAR.FORD_FOCUS_MK4,
CAR.FORD_MAVERICK_MK1, CAR.FORD_EXPLORER_MK6))
GENERIC_CANFD = frozenset((CAR.FORD_ESCAPE_MK4_5, CAR.FORD_EXPEDITION_MK4, CAR.FORD_F_150_MK14,
CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_RANGER_MK2))
AOL_WORDS = frozenset((8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67))
MANUAL = int(FordSafetyFlags.MANUAL)
BASE_WORDS = frozenset((8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67))
AOL_WORDS = BASE_WORDS | frozenset(word | MANUAL for word in BASE_WORDS)
def aol_safety_config(cp):
if len(cp.safetyConfigs) not in (1, 2):
return None
if len(cp.safetyConfigs) == 2:
passive = cp.safetyConfigs[0]
if passive.safetyModel != structs.CarParams.SafetyModel.noOutput or passive.safetyParam != 0:
return None
owner = cp.safetyConfigs[-1]
return owner if owner.safetyModel == structs.CarParams.SafetyModel.ford else None
def base_safety_param(cp):
owner = aol_safety_config(cp)
if owner is None:
return -1
manual = cp.transmissionType == structs.CarParams.TransmissionType.manual
if cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual):
return -1
word = int(owner.safetyParam)
if (word & MANUAL and not manual) or (manual and cp.alternativeExperience == AOL_EXPERIENCE and not word & MANUAL):
return -1
return word & ~MANUAL
def qualified(cp, *, marked_only=False):
if (cp.brand != 'ford' or cp.carFingerprint not in tuple(CAR) or cp.passive or cp.notCar or cp.dashcamOnly or
cp.steerControlType != structs.CarParams.SteerControlType.angle or
cp.transmissionType != structs.CarParams.TransmissionType.automatic or len(cp.safetyConfigs) != 1 or
cp.alternativeExperience not in ((32,) if marked_only else (0, 32))):
return False
flags = int(cp.flags) & ~int(FordFlags.HAS_BSM)
@@ -30,10 +55,9 @@ def qualified(cp, *, marked_only=False):
base = 12
else:
return False
safety = cp.safetyConfigs[0]
return (cp.pcmCruise and
safety.safetyModel == structs.CarParams.SafetyModel.ford and
safety.safetyParam == (base | int(cp.openpilotLongitudinalControl)))
safety = aol_safety_config(cp)
return bool(cp.pcmCruise and safety is not None and
base_safety_param(cp) == (base | int(cp.openpilotLongitudinalControl)))
def temporary_restriction(cp, state):
@@ -215,6 +215,8 @@ class CarState(CarStateBase):
gear = ("Gear_Shift_by_Wire_FD1" if CP.flags & FordFlags.CANFD else
"TransGearData" if CP.flags & FordFlags.ALT_STEER_ANGLE else "PowertrainData_10")
pt_messages.append((gear, 10))
if CP.transmissionType == TransmissionType.manual:
pt_messages.append(("BCM_Lamp_Stat_FD1", 10))
if CP.flags & FordFlags.LKA_STEERING:
pt_messages.append(("Lane_Assist_Data3_FD1", 30))
cam_messages.append(("LateralMotionControl", 20))
@@ -6,6 +6,7 @@ CREDITS.md and THIRD_PARTY_NOTICES.md; this module only selects admitted callers
"""
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.explorer_lateral import ExplorerLateralController, bounded_command as bounded_command, qualified as explorer_qualified
from opendbc.car.ford.lateral_strategy import FordLateralController
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
@@ -16,8 +17,6 @@ CLASSIC_EXTENDED_CARS = GENERIC_CLASSIC_CARS | {CAR.FORD_EXPLORER_MK6}
def qualified(cp):
if cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1:
return False
if cp.carFingerprint == CAR.FORD_EXPLORER_MK6:
return explorer_qualified(cp)
if (cp.brand != "ford" or cp.carFingerprint not in GENERIC_CLASSIC_CARS or
@@ -29,7 +28,7 @@ def qualified(cp):
return False
safety = cp.safetyConfigs[-1]
expected = int(FordSafetyFlags.CLASSIC_EXTENDED | (FordSafetyFlags.LONG_CONTROL if cp.openpilotLongitudinalControl else 0))
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == expected
class GenericClassicLateralController(FordLateralController):
+13 -4
View File
@@ -4,8 +4,8 @@ from opendbc.car.ford.values import CAR, FordFlags
def normal_bus0_queries():
# Verify both configured and effective requests before omitting the OBD sweep.
# All essential and as-built requests remain on bus 0. The bus-1 logging
# request must be an exact duplicate of a bus-0 query, not unique evidence.
# Essential queries use normal bus0 and its exact auxiliary Panda bus4 copy.
# As-built remains bus0; bus1 logging cannot provide unique evidence.
from opendbc.car import fw_versions
configured = fw_versions.FW_QUERY_CONFIGS['ford'].requests
effective = [request for brand, _, request in fw_versions.REQUESTS if brand == 'ford']
@@ -16,8 +16,17 @@ def normal_bus0_queries():
return (tuple(request.request), tuple(request.response), request.rx_offset, frozenset(request.whitelist_ecus))
for requests in (configured, effective):
normal = {signature(request) for request in requests if request.bus == 0}
if not normal or any(request.bus != 0 and (not request.logging or signature(request) not in normal)
for request in requests):
essential = {signature(request) for request in requests if request.bus == 0 and not request.logging}
auxiliary = {signature(request) for request in requests if request.bus == 4 and not request.logging}
if not normal or not essential or auxiliary != essential:
return False
for request in requests:
if request.bus == 0:
continue
if request.bus == 4 and not request.logging and signature(request) in essential:
continue
if request.bus == 1 and request.logging and signature(request) in normal:
continue
return False
return True
@@ -11,6 +11,7 @@ from typing import Protocol
import numpy as np
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.lateral_strategy import FordLateralController, FordLateralResult, FORD_CURVATURE_LIMITS, STEER_DT
from opendbc.car.ford.values import CAR, CarControllerParams, FordFlags, FordSafetyFlags
@@ -34,8 +35,6 @@ class ExplorerLateralController(FordLateralController):
def qualified(CP) -> bool:
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
return False
if (CP.brand != "ford" or CP.carFingerprint != CAR.FORD_EXPLORER_MK6 or
CP.flags & ~int(FordFlags.HAS_BSM) or CP.passive or CP.dashcamOnly or CP.notCar or
CP.alternativeExperience not in (0, 32) or len(CP.safetyConfigs) not in (1, 2)):
@@ -45,7 +44,7 @@ def qualified(CP) -> bool:
return False
safety = CP.safetyConfigs[-1]
expected = int(FordSafetyFlags.EXPLORER_EXTENDED | (FordSafetyFlags.LONG_CONTROL if CP.openpilotLongitudinalControl else 0))
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(CP) == expected
class CurvatureOwner(Protocol):
@@ -4,6 +4,7 @@ The reused BluePilot-derived strategy retains its original attribution in
lateral_strategy.py, CREDITS.md and THIRD_PARTY_NOTICES.md.
"""
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.explorer_lateral import bounded_command as shared_bound
from opendbc.car.ford.lateral_strategy import FordLateralController
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
@@ -14,8 +15,6 @@ SOURCE_ACCEL_CAP = 3.0 - 9.81 * .06
def qualified(cp):
if cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1:
return False
if (cp.brand != "ford" or cp.carFingerprint not in GENERIC_CANFD_CARS or
not cp.flags & FordFlags.CANFD or cp.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) or
cp.passive or cp.dashcamOnly or cp.notCar or cp.alternativeExperience not in (0, 32) or
@@ -27,7 +26,7 @@ def qualified(cp):
s = cp.safetyConfigs[-1]
expected = int(FordSafetyFlags.GENERIC_CANFD_EXTENDED | FordSafetyFlags.CANFD |
(FordSafetyFlags.LONG_CONTROL if cp.openpilotLongitudinalControl else 0))
return s.safetyModel == structs.CarParams.SafetyModel.ford and s.safetyParam == expected
return s.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == expected
class GenericCanfdLateralController(FordLateralController):
+5 -10
View File
@@ -55,12 +55,12 @@ class CarInterface(CarInterfaceBase):
new_port = bool(ret.flags & FordFlags.NEW_PORT)
if candidate in GENERIC_CANFD_CARS:
ret.alphaLongitudinalAvailable = not is_release
ret.alphaLongitudinalAvailable = True
ret.steerActuatorDelay = 0.22
longitudinal_requested = bool(alpha_long and not is_release)
longitudinal_requested = bool(alpha_long)
elif candidate == CAR.FORD_MUSTANG_MACH_E_MK1:
ret.alphaLongitudinalAvailable = not is_release
longitudinal_requested = bool(alpha_long and not is_release)
ret.alphaLongitudinalAvailable = True
longitudinal_requested = bool(alpha_long)
elif candidate in CLASSIC_EXTENDED_CARS:
# Original shared classic ownership is explicit stock/OP-long selection.
# Actuator delay is live; modern LongControl already supplies the original P=0.
@@ -68,7 +68,7 @@ class CarInterface(CarInterfaceBase):
ret.steerActuatorDelay = 0.22
longitudinal_requested = bool(alpha_long)
else:
ret.alphaLongitudinalAvailable = (not is_release if ret.flags & FordFlags.CANFD else True) if new_port else ret.radarUnavailable
ret.alphaLongitudinalAvailable = True if new_port else ret.radarUnavailable
longitudinal_requested = bool(alpha_long and ret.alphaLongitudinalAvailable) if new_port else alpha_long or not ret.radarUnavailable
if longitudinal_requested:
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.LONG_CONTROL.value
@@ -139,11 +139,6 @@ class CarInterface(CarInterfaceBase):
and ret.safetyConfigs[-1].safetyParam in (2, 3)
)
if mach_e_extended:
if is_release:
# The native CANFD LONG profile is DEBUG-only; RELEASE stays stock.
ret.alphaLongitudinalAvailable = False
ret.openpilotLongitudinalControl = False
ret.safetyConfigs[-1].safetyParam = FordSafetyFlags.CANFD.value
ret.safetyConfigs[-1].safetyParam |= FordSafetyFlags.MACH_E_EXTENDED.value
if (candidate in GENERIC_CANFD_CARS and ret.flags & FordFlags.CANFD and
not ret.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) and
@@ -17,6 +17,7 @@ from dataclasses import replace
import numpy as np
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
from opendbc.car.lateral import MAX_LATERAL_JERK
from opendbc.car.ford.lateral_strategy import (
@@ -99,8 +100,6 @@ class MachELateralController(FordLateralController):
def qualified(CP) -> bool:
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
return False
if (CP.brand != "ford" or CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or
not CP.flags & FordFlags.CANFD or CP.flags & ~int(FordFlags.CANFD | FordFlags.HAS_BSM) or
CP.passive or CP.dashcamOnly or CP.notCar or CP.alternativeExperience not in (0, 32) or
@@ -111,7 +110,7 @@ def qualified(CP) -> bool:
return False
safety = CP.safetyConfigs[-1]
return (safety.safetyModel == structs.CarParams.SafetyModel.ford and
safety.safetyParam == int(FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_EXTENDED |
base_safety_param(CP) == int(FordSafetyFlags.CANFD | FordSafetyFlags.MACH_E_EXTENDED |
(FordSafetyFlags.LONG_CONTROL if CP.openpilotLongitudinalControl else 0)))
@@ -7,6 +7,7 @@ from dataclasses import replace
import math
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.explorer_lateral import bounded_command as shared_bound
from opendbc.car.ford.lateral_strategy import FordLateralController, FordLateralResult
from opendbc.car.ford.values import CAR, FordFlags
@@ -22,7 +23,7 @@ def qualified(cp):
if (profile is None or cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar or
cp.alternativeExperience not in (0, 32) or not cp.pcmCruise or
cp.steerControlType != structs.CarParams.SteerControlType.angle or
len(cp.safetyConfigs) not in (1, 2) or (cp.alternativeExperience == 32 and len(cp.safetyConfigs) != 1)):
len(cp.safetyConfigs) not in (1, 2)):
return False
flags, word = profile
if (int(cp.flags) & ~int(FordFlags.HAS_BSM)) != flags:
@@ -31,7 +32,7 @@ def qualified(cp):
cp.safetyConfigs[0].safetyParam != 0):
return False
safety = cp.safetyConfigs[-1]
return safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == word + int(cp.openpilotLongitudinalControl)
return safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(cp) == word + int(cp.openpilotLongitudinalControl)
class NewPortCurvatureController(FordLateralController):
@@ -1,6 +1,7 @@
"""Ford physical cancel/resume switch context; no new cruise authority."""
from opendbc.car import structs
from opendbc.car.ford.aol import base_safety_param
from opendbc.car.ford.values import CAR, FordFlags, FordSafetyFlags
from opendbc.car.ford.classic_lateral import CLASSIC_EXTENDED_CARS
from opendbc.car.ford.generic_canfd_lateral import qualified as generic_canfd_qualified
@@ -30,8 +31,6 @@ def qualified(CP) -> bool:
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified
if angle_qualified(CP):
return not CP.openpilotLongitudinalControl
if CP.alternativeExperience == 32 and len(CP.safetyConfigs) != 1:
return False
"""Existing stock profiles with the paired physical-driver button contract."""
if (CP.brand != "ford" or CP.carFingerprint not in tuple(CAR) or CP.openpilotLongitudinalControl or CP.alternativeExperience not in (0, 32) or
CP.passive or CP.dashcamOnly or CP.notCar or len(CP.safetyConfigs) not in (1, 2) or
@@ -41,17 +40,17 @@ def qualified(CP) -> bool:
CP.safetyConfigs[0].safetyParam != 0):
return False
safety = CP.safetyConfigs[-1]
if safety.safetyModel != structs.CarParams.SafetyModel.ford or safety.safetyParam not in (2, 8, 10, 12, 18, 32, 66):
if safety.safetyModel != structs.CarParams.SafetyModel.ford or base_safety_param(CP) not in (2, 8, 10, 12, 18, 32, 66):
return False
if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == 32:
if safety.safetyModel == structs.CarParams.SafetyModel.ford and base_safety_param(CP) == 32:
return CP.carFingerprint in CLASSIC_EXTENDED_CARS and not CP.flags & ~int(FordFlags.HAS_BSM)
if safety.safetyParam == 66:
if base_safety_param(CP) == 66:
return generic_canfd_qualified(CP)
expected = (int(FordSafetyFlags.CANFD) if CP.flags & FordFlags.CANFD else 0)
expected |= int(FordSafetyFlags.NEW_PORT) if CP.flags & FordFlags.NEW_PORT else 0
expected |= int(FordSafetyFlags.LKA_STEERING) if CP.flags & FordFlags.LKA_STEERING else 0
if safety.safetyParam == 18:
if base_safety_param(CP) == 18:
if CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1 or expected != int(FordSafetyFlags.CANFD):
return False
expected |= int(FordSafetyFlags.MACH_E_EXTENDED)
return safety.safetyParam == expected
return base_safety_param(CP) == expected
@@ -20,18 +20,24 @@ PROFILES = (
@pytest.mark.parametrize('identity,base_word', PROFILES)
@pytest.mark.parametrize('alpha', (False, True))
def test_unmarked_profiles_keep_actual_ordinary_controls(identity, base_word, alpha, monkeypatch):
@pytest.mark.parametrize('topology', ('automatic', 'manual', 'multi-panda', 'manual-multi'))
def test_unmarked_profiles_keep_actual_ordinary_controls(identity, base_word, alpha, topology, monkeypatch):
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
from opendbc.car.ford.tests.test_three_ports import params
monkeypatch.setenv('REPLAY', '1')
monkeypatch.setenv('AOL_REPLAY_RUNTIME', '0')
cp = params(identity, alpha, False)
offset = 4 if topology in ('multi-panda', 'manual-multi') else 0
manual = topology in ('manual', 'manual-multi')
fp = gen_empty_fingerprint()
if not manual:
fp[offset][0x5A] = 8
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
cp = CarInterface.get_params(identity, fp, [], alpha, True, False)
assert cp.alternativeExperience == 0
assert cp.safetyConfigs[0].safetyParam == base_word | int(alpha)
assert cp.safetyConfigs[-1].safetyParam == base_word | int(alpha)
with OpenpilotPrefix():
saved = Params()
saved.put_bool('AlwaysOnLateral', False, block=True)
@@ -67,15 +73,17 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
if topology != "manual":
fingerprint[offset][0x5A] = 8
fingerprint[offset + 2].update({0x3D6: 8, 0x186: 8})
expected_long = alpha and not release
expected_word = base_word | int(expected_long)
expected_long = alpha
initial_word = base_word | int(expected_long)
expected_word = initial_word | (256 if topology == "manual" else 0)
initial = CarInterface.get_params(identity, fingerprint, [], expected_long, release, False)
initial.carVin = "0" * 17
initial.carFw = []
admitted = topology == "automatic"
admitted = True
expected = initial.to_dict()
expected["alternativeExperience"] = 32 if admitted else 0
assert initial.safetyConfigs[-1].safetyParam == expected_word
expected["safetyConfigs"][-1]["safetyParam"] = expected_word
assert initial.safetyConfigs[-1].safetyParam == initial_word
assert initial.openpilotLongitudinalControl == expected_long
assert not initial.passive and not initial.dashcamOnly
with OpenpilotPrefix():
@@ -145,7 +153,7 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
controls.publish(command, lateral_log)
ci.apply(command.as_reader(), 1_000_000_000)
if admitted:
from openpilot.starpilot.aol.wire import SAFETY_SERVICE, SafetyState, encode_safety
from openpilot.starpilot.aol.wire import SAFETY_SERVICE, PandaSlot, SafetyState, encode_safety
for tick, (acknowledged, stale) in enumerate(((False, False), (True, False), (True, True)), 1):
now = 1_000_000_000 + tick * 10_000_000
@@ -160,8 +168,11 @@ def run_startup(identity, base_word, alpha, release, topology, monkeypatch):
axis.aolAxisState.nativeAcknowledged = acknowledged
native = messaging.new_message(SAFETY_SERVICE, 0, valid=True, logMonoTime=now)
native.aolSafetyWire = encode_safety(SafetyState(
1, True, now, now + 200_000_000, int(card.CP.safetyConfigs[0].safetyModel.raw),
card.CP.safetyConfigs[0].safetyParam, acknowledged, False, True, False, 'fixture-panda', 'ford-current'))
1, True, now, now + 200_000_000, int(card.CP.safetyConfigs[-1].safetyModel.raw),
card.CP.safetyConfigs[-1].safetyParam, acknowledged, False, True, False, 'fixture-panda', 'ford-current', sourcePandaStatesMonoTime=now,
pandaInventory=tuple(PandaSlot(index, 'fixture-panda' if index == len(card.CP.safetyConfigs) - 1 else 'passive-panda',
int(config.safetyModel.raw), config.safetyParam, 32, False, False)
for index, config in enumerate(card.CP.safetyConfigs))))
controls.sm.update_msgs(now / 1e9, [axis.as_reader(), native.as_reader()])
active_command, _ = controls.state_control()
assert active_command.enabled
@@ -182,7 +193,7 @@ def test_actual_card_aol_publication_and_disabled_controls(identity, word, alpha
@pytest.mark.parametrize("identity,word", PROFILES)
@pytest.mark.parametrize("topology", ("manual", "multi-panda"))
def test_aol_denied_topology_preserves_actual_ordinary_startup(identity, word, topology, monkeypatch):
def test_aol_manual_and_multi_panda_keep_actual_ordinary_startup(identity, word, topology, monkeypatch):
run_startup(identity, word, False, False, topology, monkeypatch)
@@ -17,7 +17,7 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.card import Car, alpha_long_requested
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.controller_extensions import ManualTurnInputs
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
@@ -34,7 +34,7 @@ def test_generic_actual_card_publication_reaches_blend_settings(identity, monkey
saved.put_bool('FordHumanTurnDetection', False, block=True)
for key, value in (("FordCurvatureBlendLow", 0.25), ("FordCurvatureBlendHigh", 0.65), ("FordCurvatureLaneChangeFactor", 1.1)):
saved.put(key, value, block=True)
cp = params(identity, alpha_long_requested(saved, is_release=release), release)
cp = params(identity, alpha, release)
cp.carVin = '0' * 17
cp.carFw = []
expected = cp.to_dict()
@@ -10,14 +10,14 @@ from opendbc.car.ford.values import CAR, FordFlags
@pytest.mark.parametrize("car", sorted(GENERIC_CANFD_CARS))
@pytest.mark.parametrize("alpha,release", ((False,False),(True,False),(False,True),(True,True)))
def test_actual_profile_and_release_long_denial(car, alpha, release):
def test_actual_profile_and_release_long_support(car, alpha, release):
fp = gen_empty_fingerprint()
fp[0][0x5A] = 8
fp[2].update({0x3D6:8,0x186:8})
cp = CarInterface.get_params(car, fp, [], alpha, release, False)
assert cp.alphaLongitudinalAvailable == (not release)
assert cp.openpilotLongitudinalControl == (alpha and not release)
assert cp.safetyConfigs[-1].safetyParam == (67 if alpha and not release else 66)
assert cp.alphaLongitudinalAvailable
assert cp.openpilotLongitudinalControl == alpha
assert cp.safetyConfigs[-1].safetyParam == (67 if alpha else 66)
assert cp.steerActuatorDelay == pytest.approx(.22)
assert qualified(cp)
@@ -212,8 +212,8 @@ def test_mache_camera_source_preserves_exact_factory_words(alpha, release, bsm):
if bsm:
fp[0].update({0x3A6: 8, 0x3A7: 8})
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fp, [], alpha, release, False)
assert cp.safetyConfigs[-1].safetyParam == (19 if alpha and not release else 18)
assert cp.openpilotLongitudinalControl == (alpha and not release)
assert cp.safetyConfigs[-1].safetyParam == (19 if alpha else 18)
assert cp.openpilotLongitudinalControl == alpha
assert not cp.radarUnavailable
radar = RadarInterface(cp)
assert radar.rcp.bus == 2 and radar.generic_canfd_lead
@@ -14,15 +14,15 @@ def fingerprint(offset=0, secoc=False):
class TestMachEProfile(unittest.TestCase):
def test_actual_params_stock_debug_long_and_release_stock(self):
for alpha, release, word in ((False, False, 18), (True, False, 19), (False, True, 18), (True, True, 18)):
def test_actual_params_stock_debug_long_and_release_long(self):
for alpha, release, word in ((False, False, 18), (True, False, 19), (False, True, 18), (True, True, 19)):
with self.subTest(alpha=alpha, release=release):
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint(), [], alpha, release, False)
self.assertEqual(cp.safetyConfigs[-1].safetyParam, word)
self.assertEqual(cp.openpilotLongitudinalControl, word == 19)
self.assertEqual(cp.alternativeExperience, 0)
if release:
self.assertFalse(cp.alphaLongitudinalAvailable)
self.assertTrue(cp.alphaLongitudinalAvailable)
def test_secoc_unrelated_flags_and_ae_do_not_select_extension(self):
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fingerprint(secoc=True), [], False, False, False)
@@ -28,7 +28,7 @@ class TestNewPortCurvature(unittest.TestCase):
for identity in PROFILES:
for alpha, release in ((False, False), (True, False), (False, True), (True, True)):
cp = factory(identity, alpha, release)
expected_long = bool(alpha and (identity == CAR.FORD_EDGE_MK2 or not release))
expected_long = bool(alpha)
self.assertTrue(cp.pcmCruise)
self.assertEqual(cp.openpilotLongitudinalControl, expected_long)
self.assertEqual(cp.safetyConfigs[-1].safetyParam, (8 if identity == CAR.FORD_EDGE_MK2 else 10) + int(expected_long))
@@ -156,6 +156,7 @@ class TestNewPortCurvature(unittest.TestCase):
command, controller_log = controls.state_control()
self.assertTrue(command.enabled)
control_events = []
def capture_control(service, event, collector=control_events):
if service == "carControl":
collector.append(messaging.log_from_bytes(event.to_bytes()))
@@ -20,7 +20,7 @@ def test_actual_card_publication_reaches_ford_extensions(identity, alpha, releas
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car.card import Car, alpha_long_requested
from openpilot.selfdrive.car.card import Car
from openpilot.selfdrive.controls.controlsd import Controls
from openpilot.starpilot.controller_extensions import ManualTurnInputs
from openpilot.starpilot.lateral.tests.test_lane_runtime import feed
@@ -36,7 +36,7 @@ def test_actual_card_publication_reaches_ford_extensions(identity, alpha, releas
saved.put_bool('IsReleaseBranch', release, block=True)
saved.put_bool('DisableOpenpilotLongitudinal', disable, block=True)
saved.put_bool('FordHumanTurnDetection', False, block=True)
cp = params(identity, alpha_long_requested(saved, is_release=release), release)
cp = params(identity, alpha, release)
cp.carVin = '0' * 17
cp.carFw = []
expected = cp.to_dict()
@@ -62,7 +62,7 @@ class TestFordThreePorts(unittest.TestCase):
self.assertTrue(requested.openpilotLongitudinalControl)
self.assertTrue(requested.safetyConfigs[-1].safetyParam & FordSafetyFlags.LONG_CONTROL)
if candidate == CAR.FORD_MONDEO_MK5:
self.assertFalse(params(candidate, alpha=True, release=True).openpilotLongitudinalControl)
self.assertTrue(params(candidate, alpha=True, release=True).openpilotLongitudinalControl)
self.assertAlmostEqual(params(CAR.FORD_MUSTANG_MACH_E_MK1).steerActuatorDelay, 0.22, places=7)
self.assertFalse(params(CAR.FORD_BRONCO_SPORT_MK1).openpilotLongitudinalControl)
self.assertTrue(params(CAR.FORD_BRONCO_SPORT_MK1, alpha=True).openpilotLongitudinalControl)
+4 -3
View File
@@ -48,6 +48,7 @@ class FordSafetyFlags(IntFlag):
MACH_E_EXTENDED = 16
CLASSIC_EXTENDED = 32
EXPLORER_EXTENDED = CLASSIC_EXTENDED # Existing public profile spelling.
MANUAL = 256
BLUEPILOT_ANGLE = 128
GENERIC_CANFD_EXTENDED = 64
@@ -328,12 +329,12 @@ FW_QUERY_CONFIG = FwQueryConfig(
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
logging=True,
),
Request(
*[Request(
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
bus=0,
),
bus=bus,
) for bus in (0, 4)],
*[Request(
[StdQueries.TESTER_PRESENT_REQUEST, ford_asbuilt_block_request(block_id)],
[StdQueries.TESTER_PRESENT_RESPONSE, ford_asbuilt_block_response(block_id)],
+15 -14
View File
@@ -501,7 +501,11 @@ static void ford_aol_bind_tx(safety_config *ret, CanMsg workspace[7]) {
}
}
static safety_config ford_init(uint16_t param) {
static safety_config ford_init(uint16_t configured_param) {
const uint16_t param = configured_param & ~256U;
const bool manual = (configured_param & 256U) != 0U;
// Manual variants are only defined for the exact registered Ford profiles.
const bool manual_valid = !manual || ford_aol_param_valid(configured_param);
// warning: quality flags are not yet checked in openpilot's CAN parser,
// this may be the cause of blocked messages
// Shared entries preserve all current checksum/counter/quality contracts.
@@ -534,13 +538,11 @@ static safety_config ford_init(uint16_t param) {
{FORD_Lane_Assist_Data1, 0, 8, .check_relay = true}, \
{FORD_IPMA_Data, 0, 8, .check_relay = true}, \
#ifdef ALLOW_DEBUG
static const CanMsg FORD_CANFD_LONG_TX_MSGS[] = {
FORD_COMMON_TX_MSGS
{FORD_ACCDATA, 0, 8, .check_relay = true},
{FORD_LateralMotionControl2, 0, 8, .check_relay = true},
};
#endif
static const CanMsg FORD_CANFD_STOCK_TX_MSGS[] = {
FORD_COMMON_TX_MSGS
@@ -565,9 +567,7 @@ static safety_config ford_init(uint16_t param) {
const uint16_t FORD_PARAM_GENERIC_CANFD_EXTENDED = 64U;
const bool generic_canfd_namespace = GET_FLAG(param, FORD_PARAM_GENERIC_CANFD_EXTENDED);
ford_generic_canfd_extended = (param == 66U) && (((unsigned int)alternative_experience == 0U) || (((unsigned int)alternative_experience == 32U) && ford_aol_param_valid(param)));
#ifdef ALLOW_DEBUG
ford_generic_canfd_extended |= (param == 67U) && (((unsigned int)alternative_experience == 0U) || (((unsigned int)alternative_experience == 32U) && ford_aol_param_valid(param)));
#endif
ford_generic_canfd_announced = false;
const bool explorer_namespace = GET_FLAG(param, FORD_PARAM_EXPLORER_EXTENDED);
ford_explorer_extended = ((param == 32U) || (param == 33U)) &&
@@ -575,9 +575,7 @@ static safety_config ford_init(uint16_t param) {
ford_explorer_announced = false;
const bool mach_e_namespace = GET_FLAG(param, FORD_PARAM_MACH_E_EXTENDED);
ford_mach_e_extended = (param == 18U) && (((unsigned int)alternative_experience == 0U) || (((unsigned int)alternative_experience == 32U) && ford_aol_param_valid(param)));
#ifdef ALLOW_DEBUG
ford_mach_e_extended |= (param == 19U) && (((unsigned int)alternative_experience == 0U) || (((unsigned int)alternative_experience == 32U) && ford_aol_param_valid(param)));
#endif
const bool bp_config_valid = ford_bp_configure(param);
ford_stock_switch = ((((unsigned int)alternative_experience == 0U) || (((unsigned int)alternative_experience == 32U) && ford_aol_param_valid(param))) &&
((param == 2U) || (param == 8U) || (param == 10U) || (param == 12U) ||
@@ -605,12 +603,13 @@ static safety_config ford_init(uint16_t param) {
safety_config ret;
if (ford_canfd) {
ret = BUILD_SAFETY_CFG(ford_rx_checks, FORD_CANFD_STOCK_TX_MSGS);
bool long_profile = (param == 11U) || (param == 19U) || (param == 67U);
#ifdef ALLOW_DEBUG
const uint16_t FORD_PARAM_LONGITUDINAL = 1;
if (GET_FLAG(param, FORD_PARAM_LONGITUDINAL)) {
long_profile |= GET_FLAG(param, 1U);
#endif
if (long_profile) {
ret = BUILD_SAFETY_CFG(ford_rx_checks, FORD_CANFD_LONG_TX_MSGS);
}
#endif
} else {
ret = ford_new_port ? BUILD_SAFETY_CFG(ford_rx_checks, FORD_STOCK_TX_MSGS) :
BUILD_SAFETY_CFG(ford_rx_checks, FORD_LONG_TX_MSGS);
@@ -632,13 +631,13 @@ static safety_config ford_init(uint16_t param) {
SET_RX_CHECKS(ford_stock_rx_checks, ret);
}
}
if (!bp_config_valid || (mach_e_namespace && !ford_mach_e_extended) ||
if (!manual_valid || !bp_config_valid || (mach_e_namespace && !ford_mach_e_extended) ||
(explorer_namespace && !ford_explorer_extended) ||
(generic_canfd_namespace && !ford_generic_canfd_extended)) {
ret.tx_msgs = NULL;
ret.tx_msgs_len = 0;
}
ford_aol_configure(param);
ford_aol_configure(configured_param);
if (ford_aol_enabled) {
static const RxCheck ford_aol_default_rx[] = {
FORD_COMMON_RX_CHECKS
@@ -687,6 +686,10 @@ static safety_config ford_init(uint16_t param) {
for (int i = 0; i < 10; i++) {
if (i < selected_len) {
ford_aol_rx_workspace[i] = selected_rx[i];
const unsigned int addr = ford_aol_rx_workspace[i].msg[0].addr;
if (manual && ((addr == 0x176U) || (addr == 0x230U) || (addr == 0x5AU))) {
ford_aol_rx_workspace[i].msg[0].addr = 0x3C3U;
}
} else {
ford_aol_rx_workspace[i] = (RxCheck){0};
}
@@ -698,9 +701,7 @@ static safety_config ford_init(uint16_t param) {
_Static_assert(sizeof(FORD_CANFD_STOCK_TX_MSGS) <= sizeof(ford_aol_tx_msgs), "Ford CAN FD stock TX capacity");
_Static_assert(sizeof(FORD_STOCK_TX_MSGS) <= sizeof(ford_aol_tx_msgs), "Ford stock TX capacity");
_Static_assert(sizeof(FORD_LONG_TX_MSGS) <= sizeof(ford_aol_tx_msgs), "Ford long TX capacity");
#ifdef ALLOW_DEBUG
_Static_assert(sizeof(FORD_CANFD_LONG_TX_MSGS) <= sizeof(ford_aol_tx_msgs), "Ford CAN FD long TX capacity");
#endif
ford_aol_bind_tx(&ret, ford_aol_tx_msgs);
}
return ret;
+9 -5
View File
@@ -1,5 +1,6 @@
#pragma once
static bool ford_aol_manual = false;
static bool ford_aol_enabled = false;
static bool ford_aol_token = false;
static bool ford_aol_main_available = false;
@@ -151,7 +152,9 @@ static void ford_aol_rx(const CANPacket_t *msg) {
ford_aol_lka_ready = ((ford_aol_param & 0xFFFEU) == 12U) ?
((((msg->data[0] >> 4U) & 3U) == 3U) && ((msg->data[0] & 0x40U) == 0U)) :
((state == 1U) || (state == 2U) || (state == 3U));
} else {
} else if (ford_aol_manual && msg_matches(msg, 0x3C3U, 0U, 8U)) {
ford_aol_drive = !GET_BIT(msg, 9U);
} else if (!ford_aol_manual) {
const uint16_t base = ford_aol_param & 0xFFFEU;
const uint16_t gear_addr = (base == 8U) ? 0x230U : ((base == 10U) ? 0x5AU : 0x176U);
if (msg_matches(msg, gear_addr, 0U, 8U)) {
@@ -159,6 +162,8 @@ static void ford_aol_rx(const CANPacket_t *msg) {
((base == 10U) ? (msg->data[6] & 15U) : (msg->data[3] & 15U));
ford_aol_drive = (gear == 3U) || (gear == 4U) || (gear == 5U);
}
} else {
// Manual profiles consume only the dedicated reverse lamp source.
}
}
}
@@ -177,15 +182,13 @@ static void ford_aol_full_engagement(bool cruise_rising) {
static bool ford_aol_param_valid(uint16_t param) {
bool result = false;
switch (param) {
switch (param & ~256U) {
case 8U: case 9U: case 10U: case 12U: case 13U: case 18U: case 32U: case 33U: case 66U:
result = true;
break;
#ifdef ALLOW_DEBUG
case 11U: case 19U: case 67U:
result = true;
break;
#endif
default:
break;
}
@@ -195,7 +198,8 @@ static bool ford_aol_param_valid(uint16_t param) {
static void ford_aol_configure(uint16_t param) {
ford_aol_reset();
ford_aol_enabled = ford_aol_param_valid(param) && ((unsigned int)alternative_experience == 32U);
ford_aol_param = param;
ford_aol_manual = (param & 256U) != 0U;
ford_aol_param = param & ~256U;
if (ford_aol_enabled) {
static const AolSafetyPolicy policy = {
.reset = ford_aol_reset,
@@ -8,11 +8,11 @@ from opendbc.safety.tests.libsafety import libsafety_py
libsafety_py.ffi.cdef('extern safety_config current_safety_config;')
STOCK_CANFD_TX = frozenset(((0x83, 0, 8), (0x83, 2, 8), (0x18A, 0, 8), (0x3CA, 0, 8), (0x3D8, 0, 8), (0x3D6, 0, 8)))
RELEASE_LEDGER = {3: (6, STOCK_CANFD_TX), 11: (6, STOCK_CANFD_TX), 19: (6, frozenset()), 67: (6, frozenset())}
RELEASE_LEDGER = {3: (6, STOCK_CANFD_TX)}
def assert_release_denial(native, word):
"""Prove absent AOL/LONG, preserving the existing stock fallback of3/11.
"""Prove absent AOL/LONG, preserving the existing stock fallback of legacy parameter3.
A physical PCM edge can still set the ordinary boolean even with an empty TX
table. No assertion equates that boolean or hook return code with authority.
@@ -16,6 +16,17 @@ class TestFordAolAdmission(unittest.TestCase):
self.safety.set_alternative_experience(0)
self.safety.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0)
def test_angle_profiles_reject_malformed_lateral_frames_without_healthy_sources(self):
for word in (128, 129, 130, 131):
with self.subTest(word=word):
self.safety.init_tests()
self.safety.set_alternative_experience(0)
self.assertEqual(self.safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, word), 0)
self.safety.set_controls_allowed(False)
for address in (0x3CA, 0x3D3 if word & 2 else 0x3D6):
self.assertFalse(self.safety.safety_tx_hook(libsafety_py.make_CANPacket(address, 0, bytes(8))))
self.assertFalse(self.safety.get_controls_allowed())
def test_no_frames_no_lateral_or_longitudinal_authority(self):
for word in (8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67):
for ae in (0, 32, 33):
@@ -31,7 +42,8 @@ class TestFordAolAdmission(unittest.TestCase):
class TestFordAolDriverIntent(unittest.TestCase):
PROFILES = (32, 8, 10, 12, 18, 66)
BASE_PROFILES = (8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67)
PROFILES = BASE_PROFILES + tuple(word | 256 for word in BASE_PROFILES)
def setUp(self):
from opendbc.can import CANPacker
@@ -56,8 +68,12 @@ class TestFordAolDriverIntent(unittest.TestCase):
self.assertTrue(self.safety.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data)))
def pump(self, count, *, main=3, tja=False, gear=3, eps=0, mask=0, heartbeat=True, speed=10., curvature=0.):
gear_name, gear_signal = ('TransGearData', 'GearLvrPos_D_Actl') if self.word == 8 else (
('Gear_Shift_by_Wire_FD1', 'TrnRng_D_RqGsm') if self.word == 10 else ('PowertrainData_10', 'TrnRng_D_Rq'))
base = self.word & ~257
gear_name, gear_signal = ('TransGearData', 'GearLvrPos_D_Actl') if base == 8 else (
('Gear_Shift_by_Wire_FD1', 'TrnRng_D_RqGsm') if base == 10 else ('PowertrainData_10', 'TrnRng_D_Rq'))
if self.word & 256:
gear_name, gear_signal = 'BCM_Lamp_Stat_FD1', 'RvrseLghtOn_B_Stat'
gear = int(gear == 1)
for _ in range(count):
self.tick += 1
self.safety.set_timer(1_000_000 + self.tick * 10_000)
@@ -76,7 +92,7 @@ class TestFordAolDriverIntent(unittest.TestCase):
for name, hz, fields in values:
if self.tick * hz // 100 != (self.tick - 1) * hz // 100:
self.rx(name, fields)
if (self.word & ~1) in (10, 12, 18, 66) and self.tick * 30 // 100 != (self.tick - 1) * 30 // 100:
if base in (10, 12, 18, 66) and self.tick * 30 // 100 != (self.tick - 1) * 30 // 100:
self.rx('Lane_Assist_Data3_FD1', {'LatCtlSte_D_Stat': 1, 'LaActAvail_D_Actl': 3, 'LaActDeny_B_Actl': 0})
self.safety.aol_set_host_request(mask)
self.safety.aol_get_permission_mask()
@@ -0,0 +1,103 @@
"""Exact manual profiles consume a fresh physical reverse signal on local bus0."""
import pytest
from opendbc.car import structs
from opendbc.safety.tests.libsafety import libsafety_py
from opendbc.safety.tests.test_ford_full_engagement import ford_native as ford_native, select
from opendbc.safety.tests.test_ford_alpha_aol import tx
WORDS = (8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67)
@pytest.mark.parametrize('base', WORDS)
def test_manual_activation_brake_reverse_fault_recovery_and_long_bounds(ford_native, base):
n = ford_native
select(n, base | 256)
n.pump(40, main=3)
n.pump(10, main=4, mask=3)
assert n.safety.aol_get_permission_mask() == (3 if base & 1 else 1)
assert tx(n, active=True) == bool(base & 1)
n.rx('EngBrakeData', {'BpedDrvAppl_D_Actl': 2, 'CcStat_D_Actl': 3})
assert n.safety.aol_get_permission_mask() == 1
assert not tx(n, active=True)
n.pump(20, gear=1, mask=3)
assert n.safety.aol_get_permission_mask() == 0
n.pump(20, gear=3, mask=3)
assert n.safety.aol_get_permission_mask() == 1
n.pump(20, eps=1, mask=3)
assert n.safety.aol_get_permission_mask() == 0
n.pump(20, eps=0, mask=3)
assert n.safety.aol_get_permission_mask() == 1
n.pump(20, eps=2, mask=3)
assert n.safety.aol_get_permission_mask() == 0
n.pump(20, eps=0, mask=3)
assert n.safety.aol_get_permission_mask() == 0
assert not n.safety.get_controls_allowed() and not tx(n, active=True)
n.pump(20, main=0, mask=3)
assert n.safety.aol_get_permission_mask() == 0
@pytest.mark.parametrize('base', WORDS)
@pytest.mark.parametrize('failure', ['missing', 'wrong-bus', 'wrong-length', 'automatic-gear'])
def test_manual_reverse_source_cannot_be_substituted(ford_native, base, failure):
n = ford_native
select(n, base | 256)
real_rx = n.rx
def filtered(name, values):
if name != 'BCM_Lamp_Stat_FD1':
return real_rx(name, values)
if failure == 'missing':
return None
if failure == 'automatic-gear':
return real_rx('PowertrainData_10', {'TrnRng_D_Rq': 3})
addr, data, _ = n.packer.make_can_msg(name, 0, values)
n.safety.safety_rx_hook(libsafety_py.make_CANPacket(addr, 1 if failure == 'wrong-bus' else 0, data if failure == 'wrong-bus' else data[:-1]))
n.rx = filtered
n.pump(40, main=3)
n.pump(10, main=4, mask=3)
assert not n.safety.safety_config_valid()
assert n.safety.aol_get_permission_mask() == 0
assert not tx(n, active=True)
@pytest.mark.parametrize('base', WORDS)
def test_manual_reverse_source_expiry_withdraws_axes(ford_native, base):
n = ford_native
select(n, base | 256)
n.pump(40, main=3)
n.pump(10, main=4, mask=3)
assert n.safety.aol_get_permission_mask() & 1
original = n.rx
n.rx = lambda name, fields: None if name == 'BCM_Lamp_Stat_FD1' else original(name, fields)
for _ in range(120):
n.pump(1, main=4, mask=3)
n.safety.safety_tick()
assert not n.safety.safety_config_valid()
assert n.safety.aol_get_permission_mask() == 0
assert not tx(n, active=True)
@pytest.mark.parametrize('base', WORDS)
def test_manual_unmarked_profile_preserves_ordinary_physical_long_and_brake(ford_native, base):
n = ford_native
select(n, base, ae=0)
n.word |= 256 # Emit real manual sources; ordinary safety keeps its original namespace.
n.pump(40, main=3)
assert not n.safety.get_controls_allowed()
assert n.safety.aol_get_permission_mask() == 0
assert not tx(n, active=True)
n.pump(10, main=4)
assert n.safety.get_controls_allowed() and n.safety.safety_config_valid()
assert n.safety.aol_get_permission_mask() == 0
assert tx(n, active=True) == bool(base & 1)
n.rx('EngBrakeData', {'BpedDrvAppl_D_Actl': 2, 'CcStat_D_Actl': 3})
assert not n.safety.get_controls_allowed() and not tx(n, active=True)
def test_release_keeps_unregistered_legacy_canfd_long_parameter_denied(ford_native):
n = ford_native
if n.safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) == 0:
pytest.skip('Legacy parameter3 remains DEBUG-only; RELEASE exercises the denial')
from opendbc.safety.tests.ford_release_contract import assert_release_denial
select(n, 3)
assert_release_denial(n, 3)
@@ -3,7 +3,6 @@
import pytest
from opendbc.car import structs
from opendbc.safety.tests.ford_release_contract import assert_release_denial
from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent as _FordNative
WORDS = (8, 9, 10, 11, 12, 13, 18, 19, 32, 33, 66, 67)
@@ -21,14 +20,10 @@ def ford_native():
def select(native, word, *, ae=32):
safety = native.safety
debug = safety.set_safety_hooks(structs.CarParams.SafetyModel.allOutput, 0) == 0
safety.init_tests()
safety.set_alternative_experience(ae)
result = safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, word)
assert result == 0
if not debug and word in (11, 19, 67):
assert_release_denial(native, word)
pytest.skip('RELEASE configuration/physical RX/AOL and TX denial proved; positive vector requires DEBUG')
native.word, native.tick = word & ~1, 0
safety.set_aol_test_heartbeat(True)
@@ -41,14 +41,14 @@ class TestFordGenericCanfdExtended(unittest.TestCase):
self._set_prev_desired_angle(0.)
self.safety.set_controls_allowed(True)
def test_exact_namespace_and_release_long_denial(self):
def test_exact_namespace_and_release_long_support(self):
for word in (66,67):
self.select(word)
admitted = word == 66 or self.debug
admitted = True
self.assertEqual(admitted, self.announce())
self.prepare()
self.assertEqual(admitted, self._tx(self._lat_ctl_msg(True,0,0,0,.0002)))
self.assertEqual(word == 67 and self.debug,
self.assertEqual(word == 67,
self._tx(self._acc_command_msg(self.INACTIVE_GAS,self.INACTIVE_ACCEL,False)))
for word, ae in ((64,0),(65,0),(68,0),(70,0),(74,0),(82,0),(98,0),(194,0),(66,33),(67,33)):
self.select(word,ae)
@@ -56,7 +56,7 @@ class TestFordGenericCanfdExtended(unittest.TestCase):
self.assertFalse(self._tx(self._lat_ctl_msg(False,0,0,0,0)))
for word in (66, 67):
self.select(word, 32)
self.assertEqual(word == 66 or self.debug, self.announce())
self.assertTrue(self.announce())
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0, 0, .0002, 0)))
def test_announcement_path_and_inactive_contract(self):
@@ -169,27 +169,11 @@ class TestFordMachEDebugLongExtended(TestFordMachEExtended):
REQUIRES_DEBUG = True
class TestFordMachELongReleaseDenied(unittest.TestCase):
class TestFordMachELongReleaseExtended(TestFordMachEExtended):
SAFETY_PARAM = 19
REQUIRES_DEBUG = False
def setUp(self):
safety = libsafety_py.libsafety
debug = safety.set_safety_hooks(CarParams.SafetyModel.allOutput, 0) == 0
safety.set_safety_hooks(CarParams.SafetyModel.noOutput, 0)
safety.set_alternative_experience(0)
if self.REQUIRES_DEBUG is not None and debug != self.REQUIRES_DEBUG:
self.skipTest("This case requires " + ("DEBUG" if self.REQUIRES_DEBUG else "RELEASE") + " safety")
self._message_fixture = ford.TestFordCANFDStockSafety()
self._message_fixture.SAFETY_PARAM = self.SAFETY_PARAM
self._message_fixture.setUp()
def __getattr__(self, name):
fixture = self.__dict__.get("_message_fixture")
if fixture is None:
raise AttributeError(name)
return getattr(fixture, name)
def test_long_profile_no_transmit_authority_in_release(self):
self.safety.set_controls_allowed(True)
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, 0.0, 0.0)))
# Compatibility for existing CI selector names: this runs the promoted profile's
# complete positive RELEASE contract, including the unchanged actuation bounds.
TestFordMachELongReleaseDenied = TestFordMachELongReleaseExtended
+8 -2
View File
@@ -295,6 +295,8 @@ class Car:
cp, long_cp, enabled=eligible, is_release=is_release)
self.ioniq6_long_selected = self.ioniq6_long_pending
cp = startup_preferences.prepare(cp, fingerprints=fingerprints)
from openpilot.starpilot.car.ford.longitudinal_preferences import prepare_release_long as prepare_ford_release_long
cp = prepare_ford_release_long(cp, self.params, is_release=is_release, enabled=openpilot_enabled_toggle)
return self.vehicle_startup.prepare(cp, interfaces[candidate], self.can_callbacks,
requested=alpha_long_allowed and not is_release and openpilot_enabled_toggle,
admission=self.startup_diagnostic_admission)
@@ -387,8 +389,12 @@ class Car:
self.aol_sequence = 0
self.aol_process_fault_context = AolProcessFaultContext() if self.aol_replay else None
if self.aol_qualified:
config = (volkswagen_aol_safety_config(self.CP) if self.CP.brand == 'volkswagen' else
(hyundai_aol_safety_config(self.CP) if self.CP.brand == 'hyundai' else self.CP.safetyConfigs[0]))
if self.CP.brand == 'ford':
from opendbc.car.ford.aol import aol_safety_config as ford_aol_safety_config
config = ford_aol_safety_config(self.CP)
else:
config = (volkswagen_aol_safety_config(self.CP) if self.CP.brand == 'volkswagen' else
(hyundai_aol_safety_config(self.CP) if self.CP.brand == 'hyundai' else self.CP.safetyConfigs[0]))
if config is None:
raise RuntimeError('AOL requires the exact vehicle safety configuration')
config.safetyParam |= aol_policy.safety_param_addition
@@ -19,14 +19,15 @@ VIN = '3FMTK1SS0MMA00001'
class FordEcus:
"""Transport only: the real ISO-TP layer assembles VIN and firmware replies."""
def __init__(self, *, firmware=None, vin=VIN, background=None):
def __init__(self, *, firmware=None, vin=VIN, background=None, offset=0):
self.offset = offset
self.clock = 100.
self.vin = vin
self.versions = firmware if firmware is not None else {(0x706, b'\x22\xf1\x88'): b'RJ6T-14H102-BAE',
(0x730, b'\x22\xf1\x88'): b'LJ9C-14D003-GA', (0x760, b'\x22\xf1\x88'): b'LK9C-2D053-CN',
(0x7E0, b'\x22\xf1\x88'): b'MJ98-14C204-BYG'}
self.background = background or [CanData(0x5A, bytes(8), 0), CanData(0x3D6, bytes(8), 2),
CanData(0x186, bytes(8), 2)]
self.background = background or [CanData(0x5A, bytes(8), offset), CanData(0x3D6, bytes(8), offset + 2),
CanData(0x186, bytes(8), offset + 2)]
self.queued = []
self.pending = {}
self.mux_calls = []
@@ -39,30 +40,29 @@ class FordEcus:
self.mux_calls.append(enabled)
assert not enabled, 'Ford discovery switched CAN2 onto the OBD network'
def reply(self, address, payload):
def reply(self, address, payload, *, bus=None):
bus = self.offset if bus is None else bus
if len(payload) < 8:
frames = [bytes([len(payload)]) + payload]
else:
frames = [bytes([0x10 | (len(payload) >> 8), len(payload) & 0xFF]) + payload[:6]]
frames += [bytes([0x20 | (index & 15)]) + payload[offset:offset + 7]
for index, offset in enumerate(range(6, len(payload), 7), 1)]
self.queued.append(CanData(address + 8, frames[0].ljust(8, b'\0'), 0))
self.pending[address] = [CanData(address + 8, data.ljust(8, b'\0'), 0) for data in frames[1:]]
self.queued.append(CanData(address + 8, frames[0].ljust(8, b'\0'), bus))
self.pending[address, bus] = [CanData(address + 8, data.ljust(8, b'\0'), bus) for data in frames[1:]]
def send(self, frames):
self.sent.extend(frames)
for address, data, bus in frames:
if bus != 0:
continue
if data[0] >> 4 == 3:
self.queued.extend(self.pending.pop(address, []))
self.queued.extend(self.pending.pop((address, bus), []))
elif data[0] >> 4 == 0:
request = bytes(data[1:1 + data[0]])
if address == 0x7DF and request == b'\x22\xf1\x90':
self.reply(0x7E0, b'\x62\xf1\x90' + self.vin.encode())
elif request == b'\x3e\x00' and any(tx == address for tx, _ in self.versions):
if bus == 0 and address == 0x7DF and request == b'\x22\xf1\x90':
self.reply(0x7E0, b'\x62\xf1\x90' + self.vin.encode(), bus=0)
elif bus == self.offset and request == b'\x3e\x00' and any(tx == address for tx, _ in self.versions):
self.reply(address, b'\x7e\x00')
elif (address, request) in self.versions:
elif bus == self.offset and (address, request) in self.versions:
self.reply(address, bytes([request[0] + 0x40]) + request[1:] + self.versions[address, request])
def recv(self, wait_for_one=False):
@@ -85,8 +85,9 @@ def discover(transport, candidate, *, cached=None):
@pytest.mark.parametrize('candidate', CANFD_CARS)
def test_registered_ford_uses_real_normal_bus_vin_fw_can_and_factory(candidate):
transport = FordEcus()
@pytest.mark.parametrize('offset', (0, 4))
def test_registered_ford_uses_real_normal_bus_vin_fw_can_and_factory(candidate, offset):
transport = FordEcus(offset=offset)
ci = discover(transport, candidate)
assert transport.mux_calls and set(transport.mux_calls) == {False}
assert ci.CP.carFingerprint == candidate and ci.CP.carVin == VIN
@@ -95,9 +96,13 @@ def test_registered_ford_uses_real_normal_bus_vin_fw_can_and_factory(candidate):
assert ci.CP.safetyConfigs[-1].safetyModel == structs.CarParams.SafetyModel.ford
assert len(ci.CP.carFw) == len(transport.versions)
for fw in ci.CP.carFw:
assert fw.brand == 'ford' and fw.bus == 0
assert fw.brand == 'ford' and fw.bus == offset
assert fw.fwVersion == transport.versions[fw.address, b'\x22\xf1\x88']
assert all(frame.src == 0 for frame in transport.sent)
assert all(frame.src in (0, 4) for frame in transport.sent)
assert len(ci.CP.safetyConfigs) == (2 if offset else 1)
if offset:
assert ci.CP.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput
assert ci.CP.safetyConfigs[0].safetyParam == 0
assert any(frame.dat[0] >> 4 == 3 for frame in transport.sent), 'Real ISO-TP flow control was not exercised'
@@ -118,7 +123,10 @@ def test_wrong_brand_cache_cannot_replace_live_ford_responses():
assert transport.sent and ci.CP.carFw and all(f.brand == 'ford' for f in ci.CP.carFw)
@pytest.mark.parametrize('drift', ['essential_bus1', 'unique_logging_bus1', 'empty', 'effective_only'])
@pytest.mark.parametrize('drift', [
'essential_bus1', 'unique_logging_bus1', 'empty', 'effective_only', 'foreign_auxiliary',
'unique_auxiliary', 'missing_auxiliary', 'effective_missing_auxiliary',
])
def test_query_topology_drift_refuses_direct_scoped_discovery_before_io(drift):
original = fw_versions.FW_QUERY_CONFIGS['ford']
changed = list(original.requests)
@@ -128,7 +136,15 @@ def test_query_topology_drift_refuses_direct_scoped_discovery_before_io(drift):
changed.append(replace(changed[0], request=[changed[0].request[0], b'\x22\xf1\xff']))
elif drift == 'empty':
changed = []
elif drift == 'missing_auxiliary':
changed = [request for request in changed if request.bus != 4]
elif drift == 'foreign_auxiliary':
changed.append(replace(changed[1], bus=8))
elif drift == 'unique_auxiliary':
changed.append(replace(changed[1], bus=4, request=[changed[1].request[0], b'\x22\xf1\xff']))
effective = [('ford', original, replace(original.requests[1], bus=1))] if drift == 'effective_only' else fw_versions.REQUESTS
if drift == 'effective_missing_auxiliary':
effective = [(brand, owner, request) for brand, owner, request in effective if brand != 'ford' or request.bus != 4]
config = replace(original, requests=changed, extra_ecus=[])
with mock.patch.dict(fw_versions.FW_QUERY_CONFIGS, {'ford': config}), mock.patch.object(fw_versions, 'REQUESTS', effective):
assert not normal_bus0_queries()
@@ -251,7 +251,7 @@ int main() {
constexpr uint16_t ford_words[] = {8U, 9U, 10U, 11U, 12U, 13U, 18U, 19U, 32U, 33U, 66U, 67U};
for (uint32_t word = 0U; word <= 65535U; ++word) {
bool expected = false;
for (const auto exact : ford_words) expected |= word == exact;
for (const auto exact : ford_words) expected |= (word == exact) || (word == (exact | 256U));
auto ford = status();
ford.safety_mode = 6U;
ford.safety_param = static_cast<uint16_t>(word);
@@ -1,4 +1,5 @@
#include <cstring>
#include "openpilot/starpilot/car/ford/aol_policy.h"
#include <unistd.h>
#include <cstdlib>
#include <filesystem>
@@ -44,7 +45,7 @@ public:
OwnerPanda(std::unique_ptr<OwnerHandle> h, uint32_t offset) : Panda(std::move(h), cereal::PandaState::PandaType::RED_PANDA, offset) {}
};
void test_actual_config_slots_and_owner_handle() {
void test_actual_config_slots_and_owner_handle(cereal::CarParams::SafetyModel mode, uint16_t word) {
using Mode = cereal::CarParams::SafetyModel;
const std::string prefix = "test-panda-owner-" + std::to_string(getpid());
const char *old = getenv("OPENPILOT_PREFIX");
@@ -72,15 +73,15 @@ void test_actual_config_slots_and_owner_handle() {
cp.setAlternativeExperience(32);
auto slots = cp.initSafetyConfigs(2);
slots[0].setSafetyModel(Mode::NO_OUTPUT); slots[0].setSafetyParam(0);
slots[1].setSafetyModel(Mode::HYUNDAI_CANFD); slots[1].setSafetyParam(0x809);
slots[1].setSafetyModel(mode); slots[1].setSafetyParam(word);
auto words = capnp::messageToFlatArray(message);
auto bytes = words.asBytes();
params.put("CarParams", reinterpret_cast<const char *>(bytes.begin()), bytes.size());
params.putBool("FirmwareQueryDone", true); params.putBool("ControlsReady", true);
safety.configureSafetyMode(true);
CHECK(aux->health.safety_mode_pkt == (uint8_t)Mode::NO_OUTPUT);
CHECK(owner->health.safety_mode_pkt == (uint8_t)Mode::HYUNDAI_CANFD);
CHECK(owner->health.safety_param_pkt == 0x809);
CHECK(owner->health.safety_mode_pkt == (uint8_t)mode);
CHECK(owner->health.safety_param_pkt == word);
CHECK(extra->health.safety_mode_pkt == (uint8_t)Mode::SILENT);
CHECK(aux->health.alternative_experience_pkt == 32 && owner->health.alternative_experience_pkt == 32);
CHECK(safety.aolOwner() == nullptr);
@@ -96,7 +97,7 @@ void test_actual_config_slots_and_owner_handle() {
peer->health.flags_pkt |= HEALTH_FLAG_CONTROLS_ALLOWED;
CHECK(owner_safety.aolOwner() == nullptr);
peer->health.flags_pkt = 0;
peer->health.safety_param_pkt = 0x809;
peer->health.safety_param_pkt = word;
CHECK(owner_safety.aolOwner() == nullptr);
peer->health.safety_param_pkt = 0;
peer->comms_healthy = false;
@@ -106,8 +107,8 @@ void test_actual_config_slots_and_owner_handle() {
CHECK(owner_safety.aolOwner() == nullptr);
peer->connected = true;
}
owner->health.safety_param_pkt = 0x808; CHECK(owner_safety.aolOwner() == nullptr);
owner->health.safety_param_pkt = 0x809;
owner->health.safety_param_pkt = (word ^ 1U); CHECK(owner_safety.aolOwner() == nullptr);
owner->health.safety_param_pkt = word;
owner->health.alternative_experience_pkt = 0; CHECK(owner_safety.aolOwner() == nullptr);
owner->health.alternative_experience_pkt = 32;
slots[0].setSafetyParam(1);
@@ -121,8 +122,8 @@ void test_actual_config_slots_and_owner_handle() {
params.put("CarParams", reinterpret_cast<const char *>(data.begin()), data.size()); }
owner_safety.configureSafetyMode(false); owner_safety.configureSafetyMode(true);
CHECK(owner_safety.aolOwner() == &primary);
const AolSafetyProfile profiles[] = {{(uint8_t)Mode::HYUNDAI_CANFD, [](uint16_t p) { return p == 0x809; }, true}};
AolProfileRegistry registry{profiles, 1}; AolAxisNegotiator negotiator(registry);
const AolSafetyProfile profiles[] = {{(uint8_t)Mode::HYUNDAI_CANFD, [](uint16_t p) { return p == 0x809; }, true}, FORD_AOL_PROFILE};
AolProfileRegistry registry{profiles, 2}; AolAxisNegotiator negotiator(registry);
owner->aol.capability_flags = 1;
aux->aol = owner->aol; aux->aol.permission_mask = 3; aux->aol.request_mask = 3;
AolAxisInput axis{true, true, "session", 1, 200000001, 1, true, false};
@@ -142,4 +143,12 @@ void test_actual_config_slots_and_owner_handle() {
PandaSafety single({&primary}); CHECK(single.aolOwner() == &primary);
}
int main() { return run_native_test(test_actual_config_slots_and_owner_handle); }
void test_registered_owners() {
test_actual_config_slots_and_owner_handle(cereal::CarParams::SafetyModel::HYUNDAI_CANFD, 0x809);
for (uint16_t word : {8U, 9U, 10U, 11U, 12U, 13U, 18U, 19U, 32U, 33U, 66U, 67U}) {
test_actual_config_slots_and_owner_handle(cereal::CarParams::SafetyModel::FORD, word);
test_actual_config_slots_and_owner_handle(cereal::CarParams::SafetyModel::FORD, word | 256U);
}
}
int main() { return run_native_test(test_registered_owners); }
@@ -2,6 +2,7 @@ from collections.abc import Callable
from openpilot.common.params import Params
from openpilot.starpilot.car.hyundai.aol import ioniq6_settings_capable
from openpilot.starpilot.car.ford.longitudinal_preferences import release_long_available as ford_release_long_available
from opendbc.car.volkswagen.meb_modes import MEB_CARS, settings_capable as meb_settings_capable
from openpilot.starpilot.car.volkswagen.preferences import stock_long_requested
from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item
@@ -144,7 +145,7 @@ class DeveloperLayout(Widget):
if ui_state.CP is not None:
alpha_avail = (meb_settings_capable(ui_state.CP) if ui_state.CP.brand == 'volkswagen' and ui_state.CP.carFingerprint in MEB_CARS
else ui_state.CP.alphaLongitudinalAvailable or ioniq6_settings_capable(ui_state.CP))
if not alpha_avail or (self._is_release and not meb_settings_capable(ui_state.CP)):
if not alpha_avail or (self._is_release and not (meb_settings_capable(ui_state.CP) or ford_release_long_available(ui_state.CP))):
self._alpha_long_toggle.set_visible(False)
else:
self._alpha_long_toggle.set_visible(True)
@@ -215,7 +216,8 @@ class DeveloperLayout(Widget):
if result == DialogResult.CONFIRM:
if (not ui_state.is_offroad() or ui_state.CP is None or
not (meb_settings_capable(ui_state.CP) if ui_state.CP.brand == 'volkswagen' and ui_state.CP.carFingerprint in MEB_CARS
else ui_state.CP.alphaLongitudinalAvailable or ioniq6_settings_capable(ui_state.CP))):
else ui_state.CP.alphaLongitudinalAvailable or ioniq6_settings_capable(ui_state.CP)) or
self._is_release and not (meb_settings_capable(ui_state.CP) or ford_release_long_available(ui_state.CP))):
self._alpha_long_toggle.action_item.set_state(False)
return
self._params.put_bool("AlphaLongitudinalEnabled", True, block=True)
@@ -1,3 +1,4 @@
from openpilot.starpilot.car.ford.longitudinal_preferences import release_long_available as ford_release_long_available
from openpilot.cereal import log
from openpilot.common.params import Params, UnknownKeyName
from openpilot.system.ui.widgets import Widget
@@ -188,7 +189,7 @@ class TogglesLayout(Widget):
long_desc = unavailable + " " + tr("openpilot longitudinal control may come in a future update.")
if ui_state.CP.alphaLongitudinalAvailable:
if self._is_release:
if self._is_release and not ford_release_long_available(ui_state.CP):
long_desc = unavailable + " " + tr("An alpha version of openpilot longitudinal control can be tested, along with " +
"Experimental mode, on non-release branches.")
else:
@@ -8,6 +8,7 @@ from openpilot.system.ui.lib.application import gui_app
from openpilot.selfdrive.ui.layouts.settings.common import restart_needed_callback
from openpilot.selfdrive.ui.ui_state import ui_state
from openpilot.starpilot.car.hyundai.aol import ioniq6_settings_capable
from openpilot.starpilot.car.ford.longitudinal_preferences import release_long_available as ford_release_long_available
from opendbc.car.volkswagen.meb_modes import MEB_CARS, settings_capable as meb_settings_capable
from openpilot.starpilot.car.volkswagen.preferences import stock_long_requested
from openpilot.selfdrive.ui.widgets.ssh_key import SshKeyFetcher
@@ -171,7 +172,7 @@ class DeveloperLayoutMici(NavScroller):
if ui_state.CP is not None:
alpha_avail = (meb_settings_capable(ui_state.CP) if ui_state.CP.brand == 'volkswagen' and ui_state.CP.carFingerprint in MEB_CARS
else ui_state.CP.alphaLongitudinalAvailable or ioniq6_settings_capable(ui_state.CP))
if not alpha_avail or (ui_state.is_release and not meb_settings_capable(ui_state.CP)):
if not alpha_avail or (ui_state.is_release and not (meb_settings_capable(ui_state.CP) or ford_release_long_available(ui_state.CP))):
self._alpha_long_toggle.set_visible(False)
else:
self._alpha_long_toggle.set_visible(True)
@@ -194,7 +195,8 @@ class DeveloperLayoutMici(NavScroller):
def do_toggle(_state: bool):
if (not ui_state.is_offroad() or (_state and (ui_state.CP is None or
not (meb_settings_capable(ui_state.CP) if ui_state.CP.brand == 'volkswagen' and ui_state.CP.carFingerprint in MEB_CARS
else ui_state.CP.alphaLongitudinalAvailable or ioniq6_settings_capable(ui_state.CP))))):
else ui_state.CP.alphaLongitudinalAvailable or ioniq6_settings_capable(ui_state.CP)) or
ui_state.is_release and not (meb_settings_capable(ui_state.CP) or ford_release_long_available(ui_state.CP))))):
self._update_toggles()
return
ui_state.params.put_bool("AlphaLongitudinalEnabled", _state, block=True)
+4
View File
@@ -82,6 +82,10 @@ def native_matches_cp(CP, model: int, param: int) -> bool:
config = hyundai.aol_safety_config(CP)
return bool(config is not None and hyundai.native_accepts_cp(CP, model, param) and
model == int(config.safetyModel.raw) and param == int(config.safetyParam))
if CP.brand == 'ford':
config = ford.aol_safety_config(CP)
return bool(config is not None and ford.native_accepts_cp(CP, model, param) and
model == int(config.safetyModel.raw) and param == int(config.safetyParam))
if CP.brand == 'honda' and len(CP.safetyConfigs) == 2:
config = honda.aol_safety_config(CP)
return bool(config is not None and honda.native_accepts_cp(CP, model, param) and
+8 -7
View File
@@ -1,6 +1,6 @@
from dataclasses import replace
from opendbc.car import structs
from opendbc.car.ford.aol import qualified, AOL_WORDS, temporary_restriction
from opendbc.car.ford.aol import qualified, AOL_WORDS, MANUAL, temporary_restriction, aol_safety_config
from openpilot.starpilot.aol.intent import AolCardIntent, AOL_TOGGLE
from openpilot.starpilot.aol.policy import AolVehiclePolicy
@@ -9,7 +9,8 @@ def policy_for(cp):
if qualified(cp):
return AolVehiclePolicy(intent_supported=True, settings_supported=True, runtime_supported=True,
normal_runtime_supported=True, ordinary_axis_ack_required=qualified(cp, marked_only=True),
explicit_latch=True, alternative_experience_addition=32)
explicit_latch=True, alternative_experience_addition=32,
safety_param_addition=MANUAL if cp.transmissionType == structs.CarParams.TransmissionType.manual else 0)
return AolVehiclePolicy()
@@ -18,7 +19,7 @@ def native_profile_supported(model, param):
def native_accepts_cp(cp, model, param):
return qualified(cp, marked_only=True) and native_profile_supported(model, param) and param == cp.safetyConfigs[0].safetyParam
return qualified(cp, marked_only=True) and native_profile_supported(model, param) and param == aol_safety_config(cp).safetyParam
def native_latch_rejected(cp, native):
@@ -134,9 +135,9 @@ def create_intent(cp, settings):
def temporary_eps_restriction(cp, state):
"""Exact stock Ford physical EPS pause; intent only, no native permission."""
"""Ford physical EPS pause after LONG is off; intent only, no native permission."""
try:
return bool(qualified(cp, marked_only=True) and not cp.openpilotLongitudinalControl and
return bool(qualified(cp, marked_only=True) 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
@@ -148,9 +149,9 @@ def temporary_eps_restriction(cp, state):
def temporary_eps_brake_recovery(cp, state):
"""Stock LAT intent may outlive EPS loss while the brake remains held."""
"""Lateral-only intent may outlive EPS loss while the brake remains held."""
try:
return bool(qualified(cp, marked_only=True) and not cp.openpilotLongitudinalControl and
return bool(qualified(cp, marked_only=True) and
state.brakePressed and not 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.regenBraking and not state.gasPressed and
+1 -1
View File
@@ -4,7 +4,7 @@
static inline bool ford_aol_param(uint16_t param) {
bool result = false;
switch (param) {
switch (param & ~256U) {
case 8U: case 9U: case 10U: case 11U: case 12U: case 13U:
case 18U: case 19U: case 32U: case 33U: case 66U: case 67U:
result = true;
@@ -1,4 +1,4 @@
"""Keep stock-Ford intent during a short command lease withdrawal; no grant."""
"""Keep Ford lateral-only intent during a short command lease withdrawal; no grant."""
from opendbc.car import structs
from opendbc.car.ford.aol import qualified
from openpilot.cereal import log
@@ -9,7 +9,7 @@ from openpilot.starpilot.aol.transport_pause import cp_identity
def retain_command_pause_intent(feedback, sm, cp, cs, axis, now_ns, *, latched, producer):
"""Retain only the original fresh LAT-only proof, without renewing any lease."""
try:
if (not qualified(cp, marked_only=True) or cp.openpilotLongitudinalControl or not latched or
if (not qualified(cp, marked_only=True) or not latched or
feedback.cp != cp_identity(cp) or feedback.failed or feedback.paused or feedback.pending_event or
feedback.awaiting or feedback.retention_started_ns or feedback.retention_pending or feedback.retention_qualified or
feedback.source_retention_started_ns or not feedback.lateral_only_baseline or not feedback.lateral_inventory or
+4 -2
View File
@@ -113,8 +113,10 @@ def retained_gas_lateral_native(feedback, sm, cp, cs, axis, now_ns, *, latched,
or native.longitudinalAllowed
or native.observedMonoTime < feedback.lateral_native_ns
or (native.pandaSerial, tuple(p.hardwareSerial for p in native.pandaInventory)) != feedback.lateral_inventory
or not all(p.controlsAllowed for p in native.pandaInventory)
or not all(p.controlsAllowed for p in sm['pandaStates'])
or not all(bool(p.controlsAllowed) == (p.safetyModel == int(structs.CarParams.SafetyModel.ford))
for p in native.pandaInventory)
or not all(bool(p.controlsAllowed) == (c.safetyModel == structs.CarParams.SafetyModel.ford)
for p, c in zip(sm['pandaStates'], cp.safetyConfigs, strict=True))
):
return None
return native
@@ -0,0 +1,26 @@
"""Exact Ford release opt-in before parser/controller construction."""
from opendbc.car.ford.aol import qualified, aol_safety_config
from openpilot.starpilot.saved_source import read_saved
def prepare_release_long(cp, params, *, is_release, enabled):
if not is_release or not enabled or cp.brand != 'ford':
return cp
try:
if (cp.alternativeExperience != 0 or not qualified(cp) or not cp.alphaLongitudinalAvailable or
cp.openpilotLongitudinalControl or read_saved(params, 'AlphaLongitudinalEnabled', 8) != (b'1', True) or
read_saved(params, 'SafeMode', 8) not in ((None, True), (b'0', True))):
return cp
candidate = cp.as_reader().as_builder()
aol_safety_config(candidate).safetyParam |= 1
candidate.openpilotLongitudinalControl = True
return candidate if qualified(candidate) else cp
except (AttributeError, OSError, TypeError, ValueError, OverflowError, IndexError):
return cp
def release_long_available(cp):
try:
return bool(cp is not None and cp.brand == 'ford' and cp.alphaLongitudinalAvailable and qualified(cp))
except (AttributeError, TypeError, ValueError, OverflowError, IndexError):
return False
@@ -113,7 +113,8 @@ def retain_physical_pause_intent(feedback, sm, cp, cs, axis, now_ns, *, latched,
or bool(native.longitudinalAllowed) != (eps and long_active)
or native.observedMonoTime < feedback.healthy_native_ns
or (native.pandaSerial, tuple(p.hardwareSerial for p in native.pandaInventory)) != feedback.healthy_inventory
or any(bool(p.controlsAllowed) != ordinary for p in native.pandaInventory)
or any(bool(p.controlsAllowed) != (ordinary and p.safetyModel == int(structs.CarParams.SafetyModel.ford))
for p in native.pandaInventory)
or not sm.seen['pandaStates']
or not sm.valid['pandaStates']
or not sm.alive['pandaStates']
@@ -125,7 +126,7 @@ def retain_physical_pause_intent(feedback, sm, cp, cs, axis, now_ns, *, latched,
int(p.safetyModel.raw) == int(c.safetyModel.raw)
and p.safetyParam == c.safetyParam
and p.alternativeExperience == cp.alternativeExperience
and bool(p.controlsAllowed) == ordinary
and bool(p.controlsAllowed) == (ordinary and c.safetyModel == structs.CarParams.SafetyModel.ford)
and not p.safetyRxChecksInvalid
and not p.heartbeatLost
and not p.faults
@@ -1,6 +1,7 @@
"""Ford qualification for the shared physical Reverse/native join."""
from opendbc.car.ford.aol import qualified
from openpilot.cereal import log
from openpilot.starpilot.aol.reverse_intent import retain_reverse_intent as retain_qualified_reverse_intent
@@ -10,4 +11,7 @@ def retain_reverse_intent(feedback, sm, cp, cs, axis, now_ns, *, latched, produc
except (AttributeError, KeyError, IndexError, TypeError, ValueError, OverflowError):
return False
return retain_qualified_reverse_intent(feedback, sm, cp, cs, axis, now_ns, latched=latched, producer=producer,
qualified_cp=qualified_cp)
qualified_cp=qualified_cp,
# The preceding brake USER_DISABLE can arrive with the first Reverse frame.
# Retain only the existing fresh LAT-only proof; Reverse still denies axes.
extra_user_events=(log.OnroadEvent.EventName.pedalPressed,))
@@ -0,0 +1,92 @@
"""Ford release opt-in reaches Galaxy's real capability and confirmation owner."""
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.car_helpers import interfaces
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.aol.vehicle import policy_for
from openpilot.starpilot.galaxy.settings import AuthorityContext, ContextSource, SettingsChanged, SettingsGateway
from openpilot.starpilot.tests.ford_lifecycle_helpers import FORD_FACTORIES
def fixture(identity, *, manual, offset, marked=False):
params = Params()
params.put_bool('IsReleaseBranch', True, block=True)
fp = gen_empty_fingerprint()
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
if not manual:
fp[offset][0x5A] = 8
cp = interfaces[identity].get_params(identity, fp, [], False, False, True)
if marked:
policy = policy_for(cp)
cp.alternativeExperience |= policy.alternative_experience_addition
cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition
state = SimpleNamespace(parked=True, cp=cp, raw=cp.to_bytes())
gateway = SettingsGateway(params, Mock(spec=ContextSource, sample=lambda: AuthorityContext(state.parked, state.cp, state.raw)), clock=lambda: 10)
return params, state, gateway
def row(gateway):
page = gateway.page('vehicle', 'session', b'generation')
index = next(i for i, item in enumerate(page['rows']) if item['label'] == 'Alpha Longitudinal')
return page, index
@pytest.mark.parametrize('identity', [identity for identity, _ in FORD_FACTORIES])
@pytest.mark.parametrize('manual,offset,marked', [(False, 0, False), (True, 0, True), (False, 4, True), (True, 4, False)])
def test_release_ford_exact_configs_reach_real_galaxy_capability(identity, manual, offset, marked):
with OpenpilotPrefix():
params, state, gateway = fixture(identity, manual=manual, offset=offset, marked=marked)
try:
original = state.cp.to_dict()
page, index = row(gateway)
assert page['rows'][index]['available'] and not params.get_bool('AlphaLongitudinalEnabled')
assert state.cp.to_dict() == original
finally:
gateway.close()
@pytest.mark.parametrize('manual,offset', [(False, 0), (True, 0), (False, 4), (True, 4)])
def test_release_ford_real_preview_confirm_on_off_and_parked_revocation(manual, offset):
with OpenpilotPrefix():
params, state, gateway = fixture('FORD_MUSTANG_MACH_E_MK1', manual=manual, offset=offset)
try:
original = state.cp.to_dict()
for choice in ('On', 'Off'):
page, index = row(gateway)
intent = gateway.preview(page['view'], index, 0, 'session', b'generation', value=choice)
assert gateway.confirm(intent['intent'], 'session', b'generation')
assert params.get_bool('AlphaLongitudinalEnabled') == (choice == 'On')
assert state.cp.to_dict() == original
page, index = row(gateway)
intent = gateway.preview(page['view'], index, 0, 'session', b'generation', value='On')
state.parked = False
with pytest.raises(SettingsChanged):
gateway.confirm(intent['intent'], 'session', b'generation')
assert not params.get_bool('AlphaLongitudinalEnabled')
finally:
gateway.close()
@pytest.mark.parametrize('failure', ['legacy', 'wrong-owner', 'automatic-manual-marker', 'passive', 'dashcam', 'not-car'])
def test_release_ford_capability_rejects_invalid_native_owner_and_passive_config(failure):
with OpenpilotPrefix():
params, state, gateway = fixture('FORD_MUSTANG_MACH_E_MK1', manual=False, offset=0)
try:
if failure == 'legacy':
state.cp.safetyConfigs[-1].safetyParam = 3
elif failure == 'wrong-owner':
state.cp.safetyConfigs[-1].safetyParam = 66
elif failure == 'automatic-manual-marker':
state.cp.safetyConfigs[-1].safetyParam |= 256
else:
setattr(state.cp, {'not-car': 'notCar', 'dashcam': 'dashcamOnly'}.get(failure, failure), True)
state.raw = state.cp.to_bytes()
page = gateway.page('vehicle', 'session', b'generation')
assert not any(item['label'] == 'Alpha Longitudinal' for item in page['rows'])
assert not params.get_bool('AlphaLongitudinalEnabled')
finally:
gateway.close()
@@ -95,13 +95,17 @@ def ford_joined(identity, alpha, hands_free, monkeypatch, record_property):
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})
topology = getattr(monkeypatch, 'ford_topology', 'automatic')
offset = 4 if topology == 'multi-panda' else 0
manual = topology == 'manual' or topology == 'manual-multi'
if topology == 'manual-multi':
offset = 4
if not manual:
fp[offset][0x5A] = 8
fp[offset].update({0x3A6: 8, 0x3A7: 8})
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
name, base = identity
observed = (name, fp, '0' * 17, [], structs.CarParams.FingerprintSource.can, True)
try:
@@ -112,15 +116,15 @@ def ford_joined(identity, alpha, hands_free, monkeypatch, record_property):
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)
expected_long = bool(alpha)
assert host.CP.openpilotLongitudinalControl == expected_long
assert host.CP.safetyConfigs[0].safetyParam == base | int(expected_long)
assert host.CP.safetyConfigs[-1].safetyParam == base | int(expected_long) | (256 if manual else 0)
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)
record_property('ford_safety_word', int(host.CP.safetyConfigs[-1].safetyParam))
native.reset(host.CP.safetyConfigs[-1].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.
@@ -152,12 +156,14 @@ def ford_joined(identity, alpha, hands_free, monkeypatch, record_property):
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, {})]
extras += [('Side_Detect_L_Stat', 2 if host.CP.flags & FordFlags.CANFD else 0, {}),
('Side_Detect_R_Stat', 2 if host.CP.flags & FordFlags.CANFD else 0, {})]
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)])
state = host.CI.update([(10_000_000_000 + native.tick * 10_000_000, [(addr, data, bus + offset) for addr, data, bus in 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)
yield SimpleNamespace(host=host, native=native, safety=native.safety, initial_cp=host.CP.to_dict(),
expected_long=expected_long, hands_free=hands_free, offset=offset)
finally:
native.tearDown()
if host is not None:
@@ -176,6 +182,7 @@ def run_actual_producer(ford_joined, monkeypatch, *, scenario='reverse', reengag
steps = start_tick = native.tick
warmup_waits = qualified_samples = 0
active_lat_frames = active_long_frames = 0
transition_trace = []
long_parser = CANParser(DBC[host.CP.carFingerprint][Bus.pt], [('ACCDATA', 50)], 0)
class ClockMappedSafety:
@@ -292,14 +299,17 @@ def run_actual_producer(ford_joined, monkeypatch, *, scenario='reverse', reengag
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, {})]
extras += [('Side_Detect_L_Stat', 2 if host.CP.flags & FordFlags.CANFD else 0, {}),
('Side_Detect_R_Stat', 2 if host.CP.flags & FordFlags.CANFD else 0, {})]
frames += [native.packer.make_can_msg(name, bus, fields) for name, bus, fields in extras]
frames = [(addr, data, bus + joined.offset) for addr, data, bus in frames]
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]
config = host.CP.safetyConfigs[-1]
slot = PandaSlot(
0, 'authority-panda', config.safetyModel.raw, config.safetyParam, 32, bool(safety.get_controls_allowed()), not bool(safety.safety_config_valid())
len(host.CP.safetyConfigs) - 1, 'authority-panda', config.safetyModel.raw, config.safetyParam, 32,
bool(safety.get_controls_allowed()), not bool(safety.safety_config_valid())
)
native_state = SafetyState(
1,
@@ -315,16 +325,20 @@ def run_actual_producer(ford_joined, monkeypatch, *, scenario='reverse', reengag
'authority-panda',
str(prior_axis.aolAxisState.sessionId) if prior_axis is not None else host.aol_axis_session(),
sourcePandaStatesMonoTime=now[0],
pandaInventory=(slot,),
pandaInventory=(slot,) if len(host.CP.safetyConfigs) == 1 else (
PandaSlot(0, 'silent-panda-0', host.CP.safetyConfigs[0].safetyModel.raw, 0, 32, False, False), 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
panda = messaging.new_message('pandaStates', len(host.CP.safetyConfigs), valid=True, logMonoTime=now[0])
panda.pandaStates[-1].safetyModel = config.safetyModel
panda.pandaStates[-1].safetyParam = config.safetyParam
panda.pandaStates[-1].alternativeExperience = 32
panda.pandaStates[-1].controlsAllowed = slot.controlsAllowed
panda.pandaStates[-1].safetyRxChecksInvalid = slot.safetyRxChecksInvalid
if len(host.CP.safetyConfigs) == 2:
panda.pandaStates[0].safetyModel = host.CP.safetyConfigs[0].safetyModel
panda.pandaStates[0].alternativeExperience = 32
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()]
@@ -364,6 +378,8 @@ def run_actual_producer(ford_joined, monkeypatch, *, scenario='reverse', reengag
native_request_long=bool(native_request & 2),
panda_controls_allowed=bool(safety.get_controls_allowed()),
panda_fault=slot.safetyRxChecksInvalid,
panda_state_message=panda,
safety_message=wire,
)
assert state.to_dict() == parsed_before_producer, 'Authority harness changed actual parsed CarState'
previous_sd.update(sd.pm.messages)
@@ -380,7 +396,7 @@ def run_actual_producer(ford_joined, monkeypatch, *, scenario='reverse', reengag
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))
assert safety.safety_tx_hook(libsafety_py.make_CANPacket(frame.address, frame.src - joined.offset, 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)
@@ -388,7 +404,7 @@ def run_actual_producer(ford_joined, monkeypatch, *, scenario='reverse', reengag
assert command.latActive and permission & 1
active_lat_frames += 1
elif address == 0x186:
long_parser.update([(source_ns, [(address, bytes(data), frame.src)])])
long_parser.update([(source_ns, [(address, bytes(data), frame.src - joined.offset)])])
active = bool(long_parser.vl['ACCDATA']['Cmbb_B_Enbl'])
assert active == bool(command.longActive)
active_long_frames += int(active)
@@ -417,6 +433,11 @@ def run_actual_producer(ford_joined, monkeypatch, *, scenario='reverse', reengag
assert not axis.longitudinalActive and not command.longActive
elif command.longActive:
assert permission & 2 and safety.get_controls_allowed()
transition_trace.append((steps, str(state.gearShifter), bool(state.brakePressed), bool(host.aol_card_intent.allowed_latch),
bool(command.latActive), bool(command.longActive), permission, native_request,
str(axis.faultReason), bool(host.aol_transport_feedback.failed),
str(host.aol_transport_feedback.pending_event),
tuple(str(event.name) for event in host.sm['onroadEvents'])))
return state, axis, sd_state, command
def joined_packet(frame):
@@ -429,6 +450,38 @@ def run_actual_producer(ford_joined, monkeypatch, *, scenario='reverse', reengag
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
if scenario == 'configuration-parity':
for _ in range(40):
state, axis, sd_state, command = step(engaged=True)
assert command.latActive and host.aol_card_intent.allowed_latch
assert bool(command.longActive) == joined.expected_long
for _ in range(30):
state, axis, sd_state, command = step(engaged=True, fault='gas')
assert command.latActive and not command.longActive and host.aol_card_intent.allowed_latch
for _ in range(30):
state, axis, sd_state, command = step(engaged=True)
assert command.latActive and bool(command.longActive) == joined.expected_long
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(30):
state, axis, sd_state, command = step(reverse=True)
assert host.aol_card_intent.allowed_latch and not command.latActive and not command.longActive, transition_trace[-40:]
for _ in range(40):
state, axis, sd_state, command = step()
assert command.latActive and not command.longActive
for _ in range(40):
state, axis, sd_state, command = step(fault='temporary-eps')
assert host.aol_card_intent.allowed_latch and not command.latActive, transition_trace[-40:]
for _ in range(40):
state, axis, sd_state, command = step()
assert command.latActive and not command.longActive
for _ in range(30):
state, axis, sd_state, command = step(main_off=True)
assert not host.aol_card_intent.allowed_latch and not command.latActive and not command.longActive
assert active_lat_frames > 0
assert bool(active_long_frames) == joined.expected_long
return
assert scenario in ('route24', 'route24-mixed-eps', 'route2a', 'route2a-main', 'route2a-main-fault')
if scenario in ('route24', 'route24-mixed-eps', 'route2a', 'route2a-main', 'route2a-main-fault'):
mixed_eps = scenario in ('route24-mixed-eps', 'route2a', 'route2a-main', 'route2a-main-fault')
+1 -1
View File
@@ -117,7 +117,7 @@ class TestFordAngleStartup(unittest.TestCase):
observed[0][0x5A] = 8
observed[2].update({0x3D6: 8, 0x186: 8})
cp = CarInterface.get_params(identity, observed, [], alpha, release, False)
long = alpha and (identity not in CANFD or not release)
long = alpha
self.assertEqual(cp.openpilotLongitudinalControl, long)
self.assertTrue(cp.pcmCruise)
self.assertEqual(cp.alternativeExperience, 0)
@@ -0,0 +1,37 @@
"""Qualified Ford configurations remain collected by both CI lanes."""
import ast
from tools.ci import run_vehicle_tests as runner
FAMILY_TARGETS = (
'openpilot/starpilot/tests/test_ford_configuration_parity.py',
'openpilot/starpilot/tests/test_ford_configuration_admission.py',
'openpilot/starpilot/tests/test_ford_release_long_preferences.py',
'openpilot/selfdrive/car/tests/test_registered_ford_discovery.py',
'opendbc_repo/opendbc/safety/tests/test_ford_configuration_parity.py',
'opendbc_repo/opendbc/safety/tests/test_ford_alpha_aol.py',
'opendbc_repo/opendbc/safety/tests/test_ford_full_engagement.py',
)
def test_both_actual_ci_selectors_collect_all_qualified_ford_configuration_contracts():
for suite in ('aol-lifecycle', 'safety-release'):
selected = runner.targets_for(suite)
for path in FAMILY_TARGETS:
assert path in selected
assert (runner.ROOT / path).is_file()
release = runner.targets_for('safety-release')
prefix = 'opendbc_repo/opendbc/safety/tests/test_ford_mach_e_extended.py::'
assert prefix + 'TestFordMachELongReleaseExtended' in release
assert prefix + 'TestFordMachELongReleaseDenied' not in release
source = ast.parse((runner.ROOT / prefix.split('::', 1)[0]).read_text())
assert any(isinstance(node, ast.ClassDef) and node.name == 'TestFordMachELongReleaseExtended' for node in source.body)
def test_actual_host_collector_discovers_release_ui_guards(tmp_path):
from tools.ci.run_host_tests import pytest_files
name = 'openpilot/starpilot/ui/tests/test_ford_release_alpha.py'
target = tmp_path / name
target.parent.mkdir(parents=True)
target.write_bytes((runner.ROOT / name).read_bytes())
assert name in pytest_files(tmp_path)
@@ -0,0 +1,65 @@
"""Ford dual-Panda and transmission admission remains exact and fail-closed."""
from dataclasses import replace
import pytest
from opendbc.car import gen_empty_fingerprint, structs
from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.aol import qualified
from openpilot.starpilot.aol.vehicle import native_matches_cp, policy_for
from openpilot.starpilot.aol.runtime import native_inventory_matches_cp
from openpilot.starpilot.aol.wire import PandaSlot, SafetyState
from openpilot.starpilot.tests.ford_lifecycle_helpers import FORD_FACTORIES
@pytest.mark.parametrize('identity,base', FORD_FACTORIES)
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('manual', [False, True])
def test_exact_dual_owner_and_manual_identity(identity, base, alpha, manual):
fp = gen_empty_fingerprint()
if not manual:
fp[4][0x5A] = 8
fp[6].update({0x3D6: 8, 0x186: 8})
cp = CarInterface.get_params(identity, fp, [], alpha, True, False)
policy = policy_for(cp)
cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition
cp.alternativeExperience |= policy.alternative_experience_addition
word = base | int(alpha) | (256 if manual else 0)
assert cp.safetyConfigs[-1].safetyParam == word
assert qualified(cp, marked_only=True) and native_matches_cp(cp, 6, word)
for field, value in [('safetyParam', word ^ 256), ('safetyParam', word | 512), ('safetyModel', structs.CarParams.SafetyModel.noOutput)]:
bad = cp.as_reader().as_builder()
setattr(bad.safetyConfigs[-1], field, value)
assert not qualified(bad) and not native_matches_cp(bad, 6, word)
for field, value in [('safetyParam', 1), ('safetyModel', structs.CarParams.SafetyModel.ford)]:
bad = cp.as_reader().as_builder()
setattr(bad.safetyConfigs[0], field, value)
assert not qualified(bad) and not native_matches_cp(bad, 6, word)
bad = cp.as_reader().as_builder()
bad.safetyConfigs = [cp.safetyConfigs[0], cp.safetyConfigs[0], cp.safetyConfigs[-1]]
assert not qualified(bad)
slots = (PandaSlot(0, 'passive', int(cp.safetyConfigs[0].safetyModel.raw), 0, 32, False, False), PandaSlot(1, 'ford', 6, word, 32, True, False))
state = SafetyState(1, True, 100, 200, 6, word, True, bool(alpha), True, bool(alpha), 'ford', 'session',
sourcePandaStatesMonoTime=100, pandaInventory=slots)
assert native_inventory_matches_cp(state, cp)
assert not native_inventory_matches_cp(replace(state, sourcePandaStatesMonoTime=0, pandaInventory=()), cp)
for index, changes in [(0, {'controlsAllowed': True}), (0, {'safetyParam': 1}), (0, {'hardwareSerial': 'ford'}),
(0, {'heartbeatLost': True}), (1, {'safetyRxChecksInvalid': True}),
(1, {'slotIndex': 0}), (1, {'safetyParam': word ^ 1}), (1, {'alternativeExperience': 0})]:
invalid = list(slots)
invalid[index] = replace(invalid[index], **changes)
assert not native_inventory_matches_cp(replace(state, pandaInventory=tuple(invalid)), cp)
def test_marked_manual_policy_survives_cards_alternative_experience_reset():
fp = gen_empty_fingerprint()
cp = CarInterface.get_params(FORD_FACTORIES[0][0], fp, [], False, False, False)
initial = policy_for(cp)
assert cp.safetyConfigs[-1].safetyParam == 32 and initial.safety_param_addition == 256
cp.safetyConfigs[-1].safetyParam |= initial.safety_param_addition
cp.alternativeExperience |= initial.alternative_experience_addition
assert native_matches_cp(cp, 6, 288)
cp.alternativeExperience = 0 # Car resets this before reapplying saved settings.
recovered = policy_for(cp)
assert recovered.intent_supported and recovered.safety_param_addition == 256
cp.safetyConfigs[-1].safetyParam |= recovered.safety_param_addition
cp.alternativeExperience |= recovered.alternative_experience_addition
assert native_matches_cp(cp, 6, 288)
@@ -0,0 +1,19 @@
"""Actual Ford factory, parser, Card, SelfdriveD, Controls and native matrix.
CAN, perception, USB and clocks are synthetic; no physical acceptance claim.
"""
import pytest
from openpilot.starpilot.tests.ford_lifecycle_helpers import FORD_FACTORIES, ford_joined as ford_joined, run_actual_producer
@pytest.fixture(autouse=True)
def configuration_topology(monkeypatch, topology):
monkeypatch.ford_topology = topology
@pytest.mark.parametrize('identity', FORD_FACTORIES)
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('hands_free', [False])
@pytest.mark.parametrize('topology', ['automatic', 'manual', 'multi-panda', 'manual-multi'])
def test_actual_configuration_lifecycle(ford_joined, monkeypatch):
run_actual_producer(ford_joined, monkeypatch, scenario='configuration-parity')
@@ -0,0 +1,46 @@
"""Manual AOL marking cannot replace existing ordinary/angle configuration words."""
from pathlib import Path
from unittest.mock import patch
import pytest
from opendbc.car import car_helpers, gen_empty_fingerprint, structs
from opendbc.car.ford.values import CAR
from openpilot.cereal import messaging
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.selfdrive.car import card as card_module
@pytest.mark.parametrize('identity,fd', [(CAR.FORD_ESCAPE_MK4, False), (CAR.FORD_MUSTANG_MACH_E_MK1, True)])
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('release', [False, True])
@pytest.mark.parametrize('offset', [0, 4])
def test_actual_manual_angle_startup_preserves_original_unmarked_owner(identity, fd, alpha, release, offset, monkeypatch):
monkeypatch.setenv('SIMULATION', '1')
monkeypatch.setenv('REPLAY', '1')
monkeypatch.setenv('AOL_REPLAY_RUNTIME', '0')
fp = gen_empty_fingerprint()
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
observed = (identity, fp, '0' * 17, [], structs.CarParams.FingerprintSource.can, True)
with OpenpilotPrefix():
saved = Params()
for key, value in [('OpenpilotEnabledToggle', True), ('AlwaysOnLateral', False), ('SafeMode', False),
('AlphaLongitudinalEnabled', alpha), ('IsReleaseBranch', release), ('FordHumanTurnDetection', False)]:
saved.put_bool(key, value, block=True)
Path(saved.get_param_path('FordLateralMode')).write_bytes(b'1')
with patch.object(car_helpers, 'fingerprint', return_value=observed), \
patch.object(card_module, 'can_comm_callbacks', return_value=(lambda *args: [], lambda frames: None)), \
patch.object(card_module.messaging, 'recv_one_retry', return_value=messaging.new_message('can', 1)):
host = card_module.Car()
try:
assert host.CP.transmissionType == structs.CarParams.TransmissionType.manual
assert host.CP.alternativeExperience == 0 and host.aol_card_intent is None
assert host.CP.minEnableSpeed == pytest.approx(20 * .44704)
assert host.CP.safetyConfigs[-1].safetyParam == 128 + (2 if fd else 0) + int(alpha and not release)
assert host.CP.openpilotLongitudinalControl == bool(alpha and not release)
assert host.CI.CC.bp_lat is not None
if offset:
assert host.CP.safetyConfigs[0].safetyModel == structs.CarParams.SafetyModel.noOutput
assert host.CP.safetyConfigs[0].safetyParam == 0
assert saved.get_bool('AlphaLongitudinalEnabled') == alpha
finally:
host.vehicle_startup.close()
@@ -0,0 +1,62 @@
"""Saved Ford release intent is consumed before construction, with exact admission."""
from types import SimpleNamespace
import pytest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.values import CAR
from openpilot.starpilot.car.ford.longitudinal_preferences import prepare_release_long
from openpilot.starpilot.tests.ford_lifecycle_helpers import FORD_FACTORIES
def saved(tmp_path, alpha=b'1', safe=b'0'):
for key, value in [('AlphaLongitudinalEnabled', alpha), ('SafeMode', safe)]:
if value is not None:
(tmp_path / key).write_bytes(value)
return SimpleNamespace(get_param_path=lambda key: str(tmp_path / key))
@pytest.mark.parametrize('identity,base', FORD_FACTORIES)
@pytest.mark.parametrize('manual', [False, True])
@pytest.mark.parametrize('offset', [0, 4])
def test_release_long_changes_only_selected_owner_and_long_selection(identity, base, manual, offset, tmp_path):
fp = gen_empty_fingerprint()
if not manual:
fp[offset][0x5A] = 8
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
stock = CarInterface.get_params(identity, fp, [], False, True, False)
stock.carVin = '0' * 17
stock.fuzzyFingerprint = True
before = stock.to_dict()
selected = prepare_release_long(stock, saved(tmp_path), is_release=True, enabled=True)
expected = stock.as_reader().as_builder()
expected.openpilotLongitudinalControl = True
expected.safetyConfigs[-1].safetyParam = base | 1
assert stock.to_dict() == before
assert selected.to_dict() == expected.to_dict()
@pytest.mark.parametrize('alpha,safe,release,enabled', [
(None, b'0', True, True), (b'0', b'0', True, True), (b'1junk', b'0', True, True),
(b'1', b'1', True, True), (b'1', b'bad', True, True), (b'1', b'0', False, True),
(b'1', b'0', True, False),
])
def test_release_opt_in_off_invalid_safe_mode_debug_and_disabled_stay_unchanged(alpha, safe, release, enabled, tmp_path):
fp = gen_empty_fingerprint()
fp[0][0x5A] = 8
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fp, [], False, True, False)
assert prepare_release_long(cp, saved(tmp_path, alpha, safe), is_release=release, enabled=enabled) is cp
@pytest.mark.parametrize('restriction', ['secoc', 'extra-panda', 'angle', 'foreign-brand'])
def test_release_never_promotes_unowned_configuration(restriction, tmp_path):
fp = gen_empty_fingerprint()
fp[0][0x5A] = 8
fp[2].update({0x3D6: 16 if restriction == 'secoc' else 8, 0x186: 16 if restriction == 'secoc' else 8})
cp = CarInterface.get_params(CAR.FORD_MUSTANG_MACH_E_MK1, fp, [], False, True, False)
if restriction == 'extra-panda':
cp.safetyConfigs = [cp.safetyConfigs[0], cp.safetyConfigs[0]]
elif restriction == 'angle':
cp.safetyConfigs[-1].safetyParam = 130
elif restriction == 'foreign-brand':
cp.brand = 'hyundai'
assert prepare_release_long(cp, saved(tmp_path), is_release=True, enabled=True) is cp
@@ -340,3 +340,24 @@ def test_newer_active_proof_cannot_extend_original_healthy_source_lease():
def test_reverse_qualification_keeps_malformed_configuration_fail_closed():
assert not retain_reverse_intent(None, None, None, None, None, NOW, latched=True, producer=PRODUCER)
@pytest.mark.parametrize('event,immediate,soft,expected', [
('pedalPressed', False, False, True), ('pedalPressed', True, False, False),
('pedalPressed', False, True, False), ('controlsMismatch', True, False, False),
('steerUnavailable', True, False, False),
])
def test_brake_user_disable_at_reverse_join_retains_only_existing_lateral_intent(event, immediate, soft, expected):
cp, cs, feedback = baseline()
cs.gearShifter = structs.CarState.GearShifter.reverse
sm, native = sample(cp, NOW + 10_000_000, NOW + 20_000_000, 2, allowed=False)
change(sm, 'onroadEvents', lambda events: setattr(events[0], 'name', event))
change(sm, 'onroadEvents', lambda events: setattr(events[0], 'immediateDisable', immediate))
change(sm, 'onroadEvents', lambda events: setattr(events[0], 'softDisable', soft))
before = (feedback.healthy_feedback_ns, feedback.healthy_native_ns, feedback.lateral_baseline_ns,
feedback.lateral_source_ns, feedback.lateral_native_ns)
assert retain_reverse_intent(feedback, sm, cp, cs, sm['aolAxisState'], NOW + 20_000_000,
latched=True, producer=PRODUCER) == expected
assert before == (feedback.healthy_feedback_ns, feedback.healthy_native_ns, feedback.lateral_baseline_ns,
feedback.lateral_source_ns, feedback.lateral_native_ns)
assert not native.lateralAllowed and not native.longitudinalAllowed
@@ -0,0 +1,51 @@
"""The HKG healthy-stop exception cannot widen a Ford native denial."""
from types import SimpleNamespace
from unittest.mock import Mock
import pytest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.ford.interface import CarInterface
from openpilot.starpilot.aol.vehicle import policy_for
from openpilot.starpilot.car.hyundai.aol import expected_stopped_lateral_denial
from openpilot.starpilot.tests.ford_lifecycle_helpers import FORD_FACTORIES
@pytest.mark.parametrize('identity,base', FORD_FACTORIES)
@pytest.mark.parametrize('alpha', [False, True])
@pytest.mark.parametrize('manual,offset', [(False, 0), (True, 0), (False, 4), (True, 4)])
def test_hkg_stop_exception_never_qualifies_ford(identity, base, alpha, manual, offset, monkeypatch):
fp = gen_empty_fingerprint()
if not manual:
fp[offset][0x5A] = 8
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
cp = CarInterface.get_params(identity, fp, [], alpha, True, False)
policy = policy_for(cp)
cp.safetyConfigs[-1].safetyParam |= policy.safety_param_addition
cp.alternativeExperience |= policy.alternative_experience_addition
assert cp.safetyConfigs[-1].safetyParam == (base | int(alpha) | (256 if manual else 0))
transport = Mock(side_effect=AssertionError('Ford must reject the HKG-only exception before native transport'))
monkeypatch.setattr('openpilot.starpilot.aol.transport_pause._configured_transport_native', transport)
assert not expected_stopped_lateral_denial(cp, SimpleNamespace(), SimpleNamespace(), sm=None, axis_session_id='ford', now_ns=100)
transport.assert_not_called()
from openpilot.starpilot.car.hyundai.aol import source_deadline_supported
from opendbc.car.hyundai.canfd_stock_aol import qualified as hkg_stock_qualified
assert not source_deadline_supported(cp)
assert not hkg_stock_qualified(cp, marked_only=True)
from openpilot.starpilot.car.volkswagen.aol import qualified as vw_qualified, temporary_eps_restriction
assert not vw_qualified(cp, marked_only=True)
assert not temporary_eps_restriction(cp, SimpleNamespace())
from openpilot.starpilot.car.subaru.smoothing import prepare, install, read_frozen, control_maximum, seconds, filter_action
before = cp.to_dict()
params = Mock()
assert prepare(cp, object()).to_dict() == before
assert install(cp, b'ford', object(), params) is None
assert read_frozen(cp, params) == 0. and control_maximum(SimpleNamespace(CP=cp)) == 0.
assert not params.mock_calls
action = SimpleNamespace(desiredCurvature=.001, desiredAcceleration=.5)
previous = SimpleNamespace(desiredCurvature=.002)
for speed in (0., .3, 5., 20.):
tau = seconds(speed, 0.)
assert tau == 0.
filter_action(action, previous, speed, tau)
assert vars(action) == {'desiredCurvature': .001, 'desiredAcceleration': .5}
@@ -198,11 +198,12 @@ class FeatureSettingsOwner:
try:
from opendbc.car.values import PLATFORMS
from openpilot.starpilot.car.hyundai.aol import ioniq6_settings_capable
from openpilot.starpilot.car.ford.longitudinal_preferences import release_long_available as ford_release_long_available
platform = PLATFORMS.get(str(cp.carFingerprint))
if (not self._galaxy() or platform is None or platform.__class__.__module__.split('.')[-2] != str(cp.brand) or
not readable or release not in (None, b'0', b'1') or cp.notCar or cp.passive or cp.dashcamOnly or
not (cp.alphaLongitudinalAvailable or ioniq6_settings_capable(cp)) or
release == b'1'):
(release == b'1' and not ford_release_long_available(cp))):
return None
return (str(cp.carFingerprint), str(cp.brand), int(cp.flags), int(cp.alternativeExperience),
bool(cp.alphaLongitudinalAvailable), bool(cp.openpilotLongitudinalControl), bool(cp.pcmCruise),
@@ -0,0 +1,95 @@
"""Both actual Developer panels expose only qualified Ford release opt-in."""
from tempfile import TemporaryDirectory
from types import SimpleNamespace
from unittest.mock import patch
import pytest
from opendbc.car import gen_empty_fingerprint
from opendbc.car.ford.interface import CarInterface
from opendbc.car.ford.values import CAR
from openpilot.common.params import Params
from openpilot.starpilot.tests.ford_lifecycle_helpers import FORD_FACTORIES
from openpilot.starpilot.ui.tests.test_ioniq6_alpha_request import Toggle
from openpilot.selfdrive.ui.layouts.settings import developer as large
from openpilot.selfdrive.ui.mici.layouts.settings import developer as compact
def panel_for(module, cp, params):
ui = SimpleNamespace(CP=cp.as_reader(), params=params, is_release=True, engaged=False, has_longitudinal_control=False,
is_offroad=lambda: True, update_params=lambda: None)
layout = large.DeveloperLayout if module is large else compact.DeveloperLayoutMici
panel = layout.__new__(layout)
panel._alpha_long_toggle = Toggle()
panel._alpha_long_toggle.title, panel._alpha_long_toggle.description = 'Alpha Long', 'Existing alpha confirmation'
panel._long_maneuver_toggle = Toggle()
panel._lat_maneuver_toggle = Toggle()
if module is large:
panel._params, panel._is_release = params, True
for name in ('_joystick_toggle', '_adb_toggle', '_ssh_toggle', '_ui_debug_toggle', '_cellular_disabled_toggle'):
setattr(panel, name, Toggle())
else:
panel._refresh_toggles = (('AlphaLongitudinalEnabled', panel._alpha_long_toggle),)
return panel, ui
def ford(identity, manual, offset):
fp = gen_empty_fingerprint()
if not manual:
fp[offset][0x5A] = 8
fp[offset + 2].update({0x3D6: 8, 0x186: 8})
return CarInterface.get_params(identity, fp, [], False, True, False)
@pytest.mark.parametrize('module', [large, compact])
@pytest.mark.parametrize('identity,base', FORD_FACTORIES)
@pytest.mark.parametrize('manual,offset', [(False, 0), (True, 0), (False, 4), (True, 4)])
def test_actual_release_panels_show_registered_ford_and_preserve_request(module, identity, base, manual, offset):
cp = ford(identity, manual, offset)
before = cp.to_dict()
with TemporaryDirectory() as directory:
params = Params(directory)
params.put_bool('AlphaLongitudinalEnabled', True, block=True)
panel, ui = panel_for(module, cp, params)
with patch.object(module, 'ui_state', ui):
panel._update_toggles()
assert panel._alpha_long_toggle.visible is True
cp.passive = True
panel._update_toggles()
assert panel._alpha_long_toggle.visible is False
assert params.get_bool('AlphaLongitudinalEnabled')
cp.passive = False
ui.CP = cp.as_reader().as_builder()
ui.CP.brand = 'hyundai'
panel._update_toggles()
assert panel._alpha_long_toggle.visible is False
assert cp.to_dict() == before
@pytest.mark.parametrize('module', [large, compact])
@pytest.mark.parametrize('confirmation_state', ['valid', 'onroad', 'foreign'])
def test_actual_release_confirmation_rechecks_offroad_and_exact_ford_before_saving(module, confirmation_state):
cp = ford(CAR.FORD_MUSTANG_MACH_E_MK1, False, 4)
before = cp.to_dict()
with TemporaryDirectory() as directory:
params = Params(directory)
panel, ui = panel_for(module, cp, params)
callbacks = []
def dialog(*args, **kwargs):
callbacks.append(kwargs['callback'] if module is large else args[0])
return object()
confirmation = 'ConfirmDialog' if module is large else 'AlphaLongConfirmPage'
with patch.object(module, 'ui_state', ui), patch.object(module, confirmation, side_effect=dialog), \
patch.object(module.gui_app, 'push_widget'), patch.object(panel, '_update_toggles'):
panel._on_alpha_long_enabled(True)
assert len(callbacks) == 1 and not params.get_bool('AlphaLongitudinalEnabled')
if confirmation_state == 'onroad':
ui.is_offroad = lambda: False
elif confirmation_state == 'foreign':
ui.CP = cp.as_reader().as_builder()
ui.CP.brand = 'hyundai'
if module is large:
callbacks[0](module.DialogResult.CONFIRM)
else:
with patch.object(module, 'restart_needed_callback'):
callbacks[0]()
assert params.get_bool('AlphaLongitudinalEnabled') == (confirmation_state == 'valid')
assert cp.to_dict() == before
+18 -1
View File
@@ -24,6 +24,14 @@ from tools.ci import vehicle_pytest
# Representative production callers plus their source/native refusal cases.
# The emitted selected_test_ids record exact parametrized configurations.
AOL_LIFECYCLE_TARGETS = (
'openpilot/starpilot/tests/test_ford_configuration_parity.py',
'openpilot/starpilot/tests/test_ford_configuration_admission.py',
'openpilot/starpilot/tests/test_ford_release_long_preferences.py',
'openpilot/selfdrive/car/tests/test_registered_ford_discovery.py',
'openpilot/starpilot/tests/test_ford_ci_registration.py',
'opendbc_repo/opendbc/safety/tests/test_ford_configuration_parity.py',
'opendbc_repo/opendbc/safety/tests/test_ford_alpha_aol.py',
'opendbc_repo/opendbc/safety/tests/test_ford_full_engagement.py',
'opendbc_repo/opendbc/safety/tests/test_hkg_mode_parity.py',
'opendbc_repo/opendbc/safety/tests/test_pe_stopped_callers.py',
'opendbc_repo/opendbc/safety/tests/test_pe_combined_callers.py',
@@ -122,6 +130,7 @@ AOL_LIFECYCLE_TARGETS = (
)
AOL_LIFECYCLE_SCOPE = {
"ford": "14 stock/Alpha identities, RELEASE CAN-FD Alpha, auto/manual, one/two Pandas; actual acquisition and caller/native lifecycle guards",
"rivian": ("GEN1 base stock/DEBUG Alpha: actual factory/Card/SD/Controls/native engagement, brake LAT-only, gas, standstill zero output, " +
"Reverse/Drive, temporary ToI recovery and physical UP_1 cancel; GEN2/unknown profiles refused"),
"mazda": ("CX5 2022/CX9 2021 GEN1 stock AE32: engagement/brake/Cancel pause/toggle/Reverse/standstill/EPS recovery; " +
@@ -158,6 +167,14 @@ DEBUG_SAFETY_CLASS_TARGETS = {
RELEASE_TARGETS = (
'openpilot/starpilot/tests/test_ford_configuration_parity.py',
'openpilot/starpilot/tests/test_ford_configuration_admission.py',
'openpilot/starpilot/tests/test_ford_release_long_preferences.py',
'openpilot/selfdrive/car/tests/test_registered_ford_discovery.py',
'openpilot/starpilot/tests/test_ford_ci_registration.py',
'opendbc_repo/opendbc/safety/tests/test_ford_configuration_parity.py',
'opendbc_repo/opendbc/safety/tests/test_ford_alpha_aol.py',
'opendbc_repo/opendbc/safety/tests/test_ford_full_engagement.py',
'opendbc_repo/opendbc/safety/tests/test_pe_combined_aol.py::TestPECombinedReleaseDenied',
'openpilot/starpilot/aol/tests/test_pe_combined_profile_contract.py',
'opendbc_repo/opendbc/safety/tests/test_palisade_stock_aol_status.py',
@@ -267,7 +284,7 @@ RELEASE_TARGETS = (
"openpilot/starpilot/tests/test_ford_aol_intent.py",
"opendbc_repo/opendbc/safety/tests/test_ford_aol.py",
"opendbc_repo/opendbc/safety/tests/test_ford_mach_e_extended.py::TestFordMachEExtended",
"opendbc_repo/opendbc/safety/tests/test_ford_mach_e_extended.py::TestFordMachELongReleaseDenied",
"opendbc_repo/opendbc/safety/tests/test_ford_mach_e_extended.py::TestFordMachELongReleaseExtended",
"opendbc_repo/opendbc/car/ford/tests/test_added_firmware_identity.py",
"opendbc_repo/opendbc/car/ford/tests/test_required_source_health.py",
"opendbc_repo/opendbc/car/ford/tests/test_blend_settings.py",