From 4cbffe53b39655af1dd5c5be0ccb85489b00309d Mon Sep 17 00:00:00 2001 From: Dom <168790843+firestar5683@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:14:17 -0500 Subject: [PATCH] Ford: add opt-in pinion curvature measurement Adapt BluePilot PR #145 by jrneulight (fe5b019568c8c13fb894dc6d8d779d60985051a8) to the existing exact native profile protocol. Default off; qualified parked setting applies at next startup. Preserve existing larger Mach-E envelope, all yaw/heartbeat/relay health, command limits and shadow freshness checks. Cloud qualification: 171 host tests and 170 subtests; 80 new native/geometry tests; 153 existing native tests and 9131 subtests; release native tests, pandad protocol inventory and full ARM debug/release builds passed. Rebuilt source-stamped firmware follows before branch publication. --- CREDITS.md | 4 + .../bluepilot_lateral/hosts/starpilot.py | 13 +- .../bluepilot_lateral/safety/ford_bp.h | 4 +- opendbc_repo/opendbc/car/ford/aol.py | 7 + .../opendbc/car/ford/carcontroller.py | 4 +- .../opendbc/car/ford/lateral_strategy.py | 7 +- opendbc_repo/opendbc/car/ford/pinion.py | 84 ++++++++++ .../car/ford/tests/test_pinion_measurement.py | 157 ++++++++++++++++++ opendbc_repo/opendbc/safety/modes/ford.h | 79 +++++++-- opendbc_repo/opendbc/safety/modes/ford_aol.h | 5 +- .../opendbc/safety/modes/ford_pinion.h | 31 ++++ .../safety/modes/ford_pinion_measurement.h | 21 +++ .../opendbc/safety/tests/libsafety/safety.c | 8 + openpilot/common/params_keys.h | 1 + .../pandad/tests/test_aol_protocol.cc | 12 ++ openpilot/starpilot/car/ford/aol_policy.h | 3 +- openpilot/starpilot/controller_extensions.py | 20 ++- .../galaxy/tests/test_ford_pinion_settings.py | 61 +++++++ .../starpilot/ui/feature_settings_owner.py | 4 + openpilot/starpilot/ui/ford_pinion_feature.py | 74 +++++++++ openpilot/starpilot/vehicle_preferences.py | 7 +- prebuilt.json | 2 +- 22 files changed, 575 insertions(+), 33 deletions(-) create mode 100644 opendbc_repo/opendbc/car/ford/pinion.py create mode 100644 opendbc_repo/opendbc/car/ford/tests/test_pinion_measurement.py create mode 100644 opendbc_repo/opendbc/safety/modes/ford_pinion.h create mode 100644 opendbc_repo/opendbc/safety/modes/ford_pinion_measurement.h create mode 100644 openpilot/starpilot/galaxy/tests/test_ford_pinion_settings.py create mode 100644 openpilot/starpilot/ui/ford_pinion_feature.py diff --git a/CREDITS.md b/CREDITS.md index 0db2eb3c0e..c537ec0729 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -33,3 +33,7 @@ Spotify's Bluetooth output binds the comma's BlueALSA A2DP source to the selecte ## Tesla Pre-AP pedal control The Pre-AP configuration, pedal codec/feedback, VirtualDAS feedforward table, grade compensation, torque-zero estimator and vehicle-specific gains preserve firestar5683's intentional Dom work, including `7837a3c1aab`, `aed0b7fdb9e9` and pinned `b220a37f4bd119e402225a7aa16a7cd84878d0af`. Original author attribution: firestar5683 <168790843+firestar5683@users.noreply.github.com>. Startup/calibration ownership, current upstream button/PID interfaces and native command bounds are adapted to the current StarPilot architecture; the old permissive native gateway is not transplanted. Existing source/license notices remain intact. + +## Ford optional pinion measurement + +The default-off Use Pinion Yaw Sensor measurement option is adapted from jrneulight’s BluePilot PR #145 (https://github.com/BluePilotDev/bluepilot/pull/145), merged as `fe5b019568c8c13fb894dc6d8d779d60985051a8`. Its per-platform geometry, native pinion measurement, integrity requirements and host vehicle-model compensation are retained. StarPilot adapts the option to its existing exact safety-profile word, modern curvature checks, AOL authority and Galaxy/native settings; the upstream maintainers do not maintain this integration. Existing upstream copyright and license notices remain in THIRD_PARTY_NOTICES.md. diff --git a/opendbc_repo/opendbc/bluepilot_lateral/hosts/starpilot.py b/opendbc_repo/opendbc/bluepilot_lateral/hosts/starpilot.py index 02e2e7b45c..dd9419eaf0 100644 --- a/opendbc_repo/opendbc/bluepilot_lateral/hosts/starpilot.py +++ b/opendbc_repo/opendbc/bluepilot_lateral/hosts/starpilot.py @@ -4,6 +4,7 @@ Only exact final CP words derived from the existing five classic/six CANFD extended profiles can select angle. Combined AOL retains its original Ford profile identity. """ from opendbc.car import structs +from opendbc.car.ford.pinion import base_word, configured, MASK as PINION_MASK from opendbc.car.ford.values import CAR, FordFlags from opendbc.bluepilot_lateral.core.angle import AngleController, Inputs from opendbc.bluepilot_lateral.core.trim import LaneGeometry @@ -39,7 +40,9 @@ def mode_word(cp, *, selected): expected = original | (256 if manual else 0) | (ANGLE_FLAG if selected else 0) else: expected = ANGLE_FLAG + (2 if fd else 0) + long if selected else original - return expected if safety.safetyModel == structs.CarParams.SafetyModel.ford and safety.safetyParam == expected else None + valid = (safety.safetyModel == structs.CarParams.SafetyModel.ford and base_word(safety.safetyParam) == expected and + (safety.safetyParam == expected or configured(cp))) + return expected if valid else None def supported(cp): @@ -58,7 +61,8 @@ def select(cp, raw): if cp.alternativeExperience == 32: cp.safetyConfigs[-1].safetyParam |= ANGLE_FLAG else: - cp.safetyConfigs[-1].safetyParam = ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl) + metadata = cp.safetyConfigs[-1].safetyParam & PINION_MASK + cp.safetyConfigs[-1].safetyParam = (ANGLE_FLAG + (2 if cp.flags & FordFlags.CANFD else 0) + int(cp.openpilotLongitudinalControl)) | metadata cp.steerActuatorDelay = 0.22 return True @@ -86,6 +90,8 @@ class Host: raise ValueError("BluePilot angle requires an exact admitted Ford safety word") low, high = (0.95, 0.95) if cp.carFingerprint in TRUCKS else ((1.0, 1.05) if cp.carFingerprint in CANFD else (1.0, 1.15)) self.cp = cp + from opendbc.car.ford.pinion import Measurement + self.measurement = Measurement(cp) self.controller = AngleController(low_gain=low, high_gain=high) self.inputs = None self.steering_rejected = False @@ -93,6 +99,7 @@ class Host: def update(self, CC, CS): if self.inputs is not None: self.inputs.update() + self.measurement.update_parameters(getattr(self.inputs, "vehicle_parameters", None)) snapshot = self.inputs.lateral_snapshot(CS.out.vEgoRaw, angle=True) if self.inputs is not None else None model, times, delay, _ = snapshot if snapshot is not None else (None, (), 0.15, True) aol = self.cp.alternativeExperience == 32 @@ -121,7 +128,7 @@ class Host: speed_max = max(high, CS.out.vEgoRaw, 0.1) + margin meta = model.meta if model is not None else None return self.controller.step(Inputs( - active=ready, speed=CS.out.vEgoRaw, measured_curvature=-CS.out.yawRate / max(CS.out.vEgoRaw, 0.1), + active=ready, speed=CS.out.vEgoRaw, measured_curvature=self.measurement.curvature(CS.out), desired_curvature=CC.actuators.curvature, steering_pressed=CS.out.steeringPressed, steering_angle=CS.out.steeringAngleDeg, yaw_predictions=tuple(model.orientationRate.z) if model is not None else (), time_indices=tuple(times), lateral_delay=delay, diff --git a/opendbc_repo/opendbc/bluepilot_lateral/safety/ford_bp.h b/opendbc_repo/opendbc/bluepilot_lateral/safety/ford_bp.h index cbebed36c1..7c46d54890 100644 --- a/opendbc_repo/opendbc/bluepilot_lateral/safety/ford_bp.h +++ b/opendbc_repo/opendbc/bluepilot_lateral/safety/ford_bp.h @@ -213,8 +213,8 @@ static bool ford_bp_lateral(const CANPacket_t *msg) { (SAFETY_ABS(path) >= (relation_low - 2.0F)) && (SAFETY_ABS(path) <= (relation_high + 2.0F)) && (SAFETY_ABS(shadow) <= (physical_cap + FORD_BP_SHADOW_ROUNDING_MARGIN)) && (path_curvature_high <= (physical_cap + FORD_BP_SHADOW_ROUNDING_MARGIN)); if (speed_max > 10.0F) { - const int measured_min = curvature_state.meas.min - 100; - const int measured_max = curvature_state.meas.max + 100; + const int measured_min = curvature_state.meas.min - (ford_pinion_enabled ? 150 : 100); + const int measured_max = curvature_state.meas.max + (ford_pinion_enabled ? 150 : 100); const float measured_min_float = (float)measured_min; const float measured_max_float = (float)measured_max; valid &= (shadow_can >= measured_min_float) && (shadow_can <= measured_max_float); diff --git a/opendbc_repo/opendbc/car/ford/aol.py b/opendbc_repo/opendbc/car/ford/aol.py index 6e0f9c19b5..dd894938e6 100644 --- a/opendbc_repo/opendbc/car/ford/aol.py +++ b/opendbc_repo/opendbc/car/ford/aol.py @@ -12,6 +12,9 @@ ANGLE = 128 ANGLE_BASE_WORDS = frozenset((18, 19, 32, 33, 66, 67)) AOL_WORDS = (BASE_WORDS | frozenset(word | ANGLE for word in ANGLE_BASE_WORDS)) AOL_WORDS |= frozenset(word | MANUAL for word in AOL_WORDS) +from opendbc.car.ford.pinion import ENABLE, SHIFT, PROFILES, base_word, configured +AOL_WORDS |= frozenset(word | ENABLE | index << SHIFT for index, words in PROFILES.items() + for word in tuple(AOL_WORDS) if (word & ~MANUAL) in words) def aol_safety_config(cp): @@ -33,6 +36,10 @@ def base_safety_param(cp): if cp.transmissionType not in (structs.CarParams.TransmissionType.automatic, structs.CarParams.TransmissionType.manual): return -1 word = int(owner.safetyParam) + normalized = base_word(word) + if normalized != word and not configured(cp): + return -1 + word = normalized if (word & MANUAL and not manual) or (manual and cp.alternativeExperience == AOL_EXPERIENCE and not word & MANUAL): return -1 if word & ANGLE: diff --git a/opendbc_repo/opendbc/car/ford/carcontroller.py b/opendbc_repo/opendbc/car/ford/carcontroller.py index 1ceb28781f..5f63af9ebf 100644 --- a/opendbc_repo/opendbc/car/ford/carcontroller.py +++ b/opendbc_repo/opendbc/car/ford/carcontroller.py @@ -183,7 +183,7 @@ class CarController(CarControllerBase): demanded = self.classic_lateral.update(CC, CS, actuators) if self.classic_extended_announced or self.generic_canfd else FordLateralResult() bound = generic_canfd_bounded_command if self.generic_canfd else classic_bounded_command lateral = bound(self.classic_lateral, demanded, previous, CS.out.vEgoRaw, - -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)) + self.classic_lateral._current_curvature(CS)) self.classic_lateral_demand = demanded self.apply_curvature_last = lateral.curvature if self.generic_canfd: @@ -209,7 +209,7 @@ class CarController(CarControllerBase): previous = self.new_port_curvature.curvature_last demanded = self.new_port_curvature.update(CC, CS, actuators) lateral = new_port_bounded_command(self.new_port_curvature, demanded, previous, CS.out.vEgoRaw, - -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)) + self.new_port_curvature._current_curvature(CS)) self.new_port_curvature_demand = demanded self.apply_curvature_last = lateral.curvature if self.CP.flags & FordFlags.CANFD: diff --git a/opendbc_repo/opendbc/car/ford/lateral_strategy.py b/opendbc_repo/opendbc/car/ford/lateral_strategy.py index 5f3160eee4..336018eb87 100644 --- a/opendbc_repo/opendbc/car/ford/lateral_strategy.py +++ b/opendbc_repo/opendbc/car/ford/lateral_strategy.py @@ -162,6 +162,8 @@ class FordLateralController: def __init__(self, CP): self.CP = CP + from opendbc.car.ford.pinion import Measurement + self.measurement = Measurement(CP) self.model = None self.time_indices = () self.lookahead = CURVATURE_LOOKAHEAD_MIN @@ -231,9 +233,8 @@ class FordLateralController: return MACH_E_HIGH_SPEED_LOOKAHEAD_EXTRA * float(np.interp( v_ego, [MACH_E_HIGH_SPEED_LOOKAHEAD_START_SPEED, MACH_E_HIGH_SPEED_LOOKAHEAD_FULL_SPEED], [0.0, 1.0])) - @staticmethod - def _current_curvature(CS) -> float: - return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1) + def _current_curvature(self, CS) -> float: + return self.measurement.curvature(CS.out) def _driver_assisting_curve(self, CS, desired: float) -> bool: v_ego = float(CS.out.vEgoRaw) diff --git a/opendbc_repo/opendbc/car/ford/pinion.py b/opendbc_repo/opendbc/car/ford/pinion.py new file mode 100644 index 0000000000..f6cac968ed --- /dev/null +++ b/opendbc_repo/opendbc/car/ford/pinion.py @@ -0,0 +1,84 @@ +"""Init-only pinion measurement selection adapted from BluePilot PR #145. + +See CREDITS.md and THIRD_PARTY_NOTICES.md for upstream attribution and license. +The geometry is carried in the existing exact Panda safety word, not live Params. +""" +from opendbc.car.ford.values import CAR + +KEY = "FordPrefSteerAngleCurvature" +ENABLE = 0x200 +SHIFT = 10 +MASK = 0x3E00 +GEOMETRY = { + CAR.FORD_BRONCO_SPORT_MK1: 1, CAR.FORD_ESCAPE_MK4: 2, CAR.FORD_ESCAPE_MK4_5: 3, + CAR.FORD_EXPEDITION_MK4: 4, CAR.FORD_EXPLORER_MK6: 5, CAR.FORD_FOCUS_MK4: 6, + CAR.FORD_F_150_LIGHTNING_MK1: 7, CAR.FORD_F_150_MK14: 8, CAR.FORD_MAVERICK_MK1: 9, + CAR.FORD_MONDEO_MK5: 10, CAR.FORD_MUSTANG_MACH_E_MK1: 11, CAR.FORD_RANGER_MK2: 12, +} +CLASSIC = frozenset((32, 33, 128, 129, 160, 161)) +GENERIC = frozenset((66, 67, 130, 131, 194, 195)) +MACH_E = frozenset((18, 19, 130, 131, 146, 147)) +PROFILES = {i: CLASSIC if i in (1, 2, 5, 6, 9) else MACH_E if i == 11 else frozenset((10, 11)) if i == 10 else GENERIC + for i in range(1, 13)} + + +def decode(word): + """Return (original profile, geometry), or None for malformed metadata.""" + if not word & MASK: + return word, 0 + index = (word >> SHIFT) & 15 + base = word & ~MASK + if not word & ENABLE or index not in PROFILES or (base & ~256) not in PROFILES[index]: + return None + return base, index + + +def base_word(word): + result = decode(int(word)) + return result[0] if result is not None else -1 + + +def configured(cp): + if cp.brand != "ford" or not cp.safetyConfigs: + return False + result = decode(int(cp.safetyConfigs[-1].safetyParam)) + return bool(result is not None and result[1] and result[1] == GEOMETRY.get(cp.carFingerprint)) + + +def select(cp, enabled): + if not enabled or cp.brand != "ford" or cp.passive or cp.dashcamOnly or cp.notCar: + return False + from opendbc.car.ford.aol import qualified + if not qualified(cp): + return False + index = GEOMETRY.get(cp.carFingerprint) + if index is None: + return False + cp.safetyConfigs[-1].safetyParam |= ENABLE | index << SHIFT + return True + + +class Measurement: + """Same init-time family as native; live model correction never changes the source.""" + def __init__(self, cp): + self.enabled = configured(cp) + self.offset = self.roll = 0.0 + self.model = None + if self.enabled: + from opendbc.car.vehicle_model import VehicleModel + self.model = VehicleModel(cp) + + def update_parameters(self, parameters): + import math + if self.model is None or parameters is None: + return + values = (parameters.angleOffsetDeg, parameters.roll, parameters.stiffnessFactor, parameters.steerRatio) + if parameters.valid and all(math.isfinite(v) for v in values): + self.offset, self.roll = values[:2] + self.model.update_params(max(values[2], 0.1), max(values[3], 0.1)) + + def curvature(self, state): + if not self.enabled: + return -state.yawRate / max(state.vEgoRaw, 0.1) + import math + return -self.model.calc_curvature(math.radians(state.steeringAngleDeg - self.offset), state.vEgoRaw, self.roll) diff --git a/opendbc_repo/opendbc/car/ford/tests/test_pinion_measurement.py b/opendbc_repo/opendbc/car/ford/tests/test_pinion_measurement.py new file mode 100644 index 0000000000..7e07fa4dbb --- /dev/null +++ b/opendbc_repo/opendbc/car/ford/tests/test_pinion_measurement.py @@ -0,0 +1,157 @@ +"""Closed, init-only measurement selection; disabled behavior stays unchanged.""" +from types import SimpleNamespace + +import pytest +from opendbc.car import gen_empty_fingerprint, structs +from opendbc.car.ford.interface import CarInterface +from opendbc.car.ford.pinion import GEOMETRY, ENABLE, SHIFT, MASK, decode, select, configured, Measurement +from opendbc.car.ford.aol import qualified +from opendbc.car.vehicle_model import VehicleModel, calc_slip_factor +from opendbc.can.packer import CANPacker +from opendbc.safety.tests.libsafety import libsafety_py + + +def params(car, long=False): + fp = gen_empty_fingerprint() + fp[0][0x5A] = 8 + fp[2].update({0x3D6: 8, 0x186: 8}) + return CarInterface.get_params(car, fp, [], long, False, False) + + +def test_closed_decoder_matches_native_exhaustively(): + n = libsafety_py.libsafety + for word in range(65536): + decoded = decode(word) + assert n.get_ford_pinion_base(word) == (decoded[0] if decoded is not None else 65535) + + +@pytest.mark.parametrize('car', list(GEOMETRY)) +@pytest.mark.parametrize('long', [False, True]) +def test_default_off_and_exact_geometry_selection(car, long): + cp = params(car, long) + before = cp.to_dict() + assert not select(cp, False) + assert cp.to_dict() == before + assert select(cp, True) + assert configured(cp) and qualified(cp) + assert decode(cp.safetyConfigs[-1].safetyParam)[1] == GEOMETRY[car] + # Cross-family/identity corruption is not accepted by the host. + cp.safetyConfigs[-1].safetyParam = (cp.safetyConfigs[-1].safetyParam & ~MASK) | ENABLE | ((GEOMETRY[car] % 12 + 1) << SHIFT) + assert not configured(cp) and not qualified(cp) + + +@pytest.mark.parametrize('car', list(GEOMETRY)) +def test_geometry_matches_actual_vehicle_model_and_native_sign(car): + cp = params(car) + index = GEOMETRY[car] + n = libsafety_py.libsafety + vm = VehicleModel(cp) + assert n.get_ford_pinion_slip(index) == pytest.approx(calc_slip_factor(vm), abs=2e-10) + assert n.get_ford_pinion_ratio(index) == pytest.approx(cp.steerRatio) + assert n.get_ford_pinion_wheelbase(index) == pytest.approx(cp.wheelbase) + assert select(cp, True) + host = Measurement(cp) + packer = CANPacker('ford_lincoln_base_pt') + for speed in (0., 5., 15., 30.): + for angle in (-100., -10., 0., 10., 100.): + n.set_alternative_experience(0) + n.set_safety_hooks(structs.CarParams.SafetyModel.ford, cp.safetyConfigs[-1].safetyParam) + assert n.get_ford_pinion_enabled() + for counter in range(6): + a, d, b = packer.make_can_msg('BrakeSysFeatures', 0, {'Veh_V_ActlBrk': speed * 3.6, 'VehVActlBrk_D_Qf': 3}) + assert n.safety_rx_hook(libsafety_py.make_CANPacket(a, b, d)) + a, d, b = packer.make_can_msg('SteeringPinion_Data', 0, {'StePinComp_An_Est': angle, 'StePinCompAnEst_D_Qf': 3, 'StePinAn_No_Cnt': counter}) + assert n.safety_rx_hook(libsafety_py.make_CANPacket(a, b, d)) + state = SimpleNamespace(vEgoRaw=max(speed, .1), yawRate=99., steeringAngleDeg=angle) + expected = -host.curvature(state) * 50000 + assert n.get_curvature_meas_min() == pytest.approx(expected, abs=1.) + assert n.get_curvature_meas_max() == pytest.approx(expected, abs=1.) + # A valid but contradictory yaw packet never changes selected pinion measurement. + a, d, b = packer.make_can_msg('Yaw_Data_FD1', 0, {'VehYaw_W_Actl': 1., 'VehYawWActl_D_Qf': 3}) + assert n.safety_rx_hook(libsafety_py.make_CANPacket(a, b, d)) + assert n.get_curvature_meas_min() == pytest.approx(expected, abs=1.) + + +def test_default_measurement_is_yaw_even_with_conflicting_pinion(): + cp = params(next(iter(GEOMETRY))) + m = Measurement(cp) + state = SimpleNamespace(vEgoRaw=15., yawRate=.3, steeringAngleDeg=100.) + assert m.curvature(state) == -.3 / 15. + assert not m.enabled + + +@pytest.mark.parametrize('index,base', [(1, 32), (3, 66), (10, 10), (11, 18)]) +@pytest.mark.parametrize('long', [False, True]) +@pytest.mark.parametrize('manual', [False, True]) +def test_pinion_inventory_and_invalid_quality_withdraw_authority(index, base, long, manual): + from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent + n = TestFordAolDriverIntent() + n.setUp() + base |= int(long) | (256 if manual else 0) + n.reset(base) + count = n.safety.get_ford_pinion_rx_count() + n.safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, base | ENABLE | index << SHIFT) + n.safety.set_aol_test_heartbeat(True) + assert n.safety.get_ford_pinion_rx_count() == count + 1 + original_rx = n.rx + + def rx(name, values): + if name == 'Yaw_Data_FD1': + original_rx('SteeringPinion_Data', {'StePinComp_An_Est': 0., 'StePinCompAnEst_D_Qf': 3, 'StePinAn_No_Cnt': n.tick % 16}) + original_rx(name, values) + n.rx = rx + try: + n.arm() + assert n.safety.aol_get_permission_mask() == 1 + a, d, b = n.packer.make_can_msg('SteeringPinion_Data', 0, + {'StePinComp_An_Est': 100., 'StePinCompAnEst_D_Qf': 0, 'StePinAn_No_Cnt': (n.tick + 1) % 16}) + assert not n.safety.safety_rx_hook(libsafety_py.make_CANPacket(a, b, d)) + assert n.safety.aol_get_permission_mask() == 0 + assert n.safety.get_curvature_meas_max() == 0 + finally: + n.tearDown() + + +@pytest.mark.parametrize('failure', ['missing', 'wrong_bus', 'short', 'counter']) +def test_pinion_integrity_is_required_not_substituted_by_healthy_yaw(failure): + from opendbc.safety.tests.test_ford_aol import TestFordAolDriverIntent + n = TestFordAolDriverIntent() + n.setUp() + n.reset(66) + n.safety.set_safety_hooks(structs.CarParams.SafetyModel.ford, 66 | ENABLE | 7 << SHIFT) + n.safety.set_aol_test_heartbeat(True) + original_rx = n.rx + inject = True + + def rx(name, values): + if name == 'Yaw_Data_FD1': + if inject: + original_rx('SteeringPinion_Data', {'StePinComp_An_Est': 0., 'StePinCompAnEst_D_Qf': 3, 'StePinAn_No_Cnt': n.tick % 16}) + elif failure != 'missing': + a, d, b = n.packer.make_can_msg('SteeringPinion_Data', 0, + {'StePinComp_An_Est': 100., 'StePinCompAnEst_D_Qf': 3, 'StePinAn_No_Cnt': 2}) + n.safety.safety_rx_hook(libsafety_py.make_CANPacket(a, 1 if failure == 'wrong_bus' else b, d[:7] if failure == 'short' else d)) + original_rx(name, values) + n.rx = rx + try: + n.arm() + inject = False + n.pump(120, mask=1) + assert n.safety.aol_get_permission_mask() == 0 + finally: + n.tearDown() + + +@pytest.mark.parametrize('car', [c for c in GEOMETRY if 'MONDEO' not in c]) +@pytest.mark.parametrize('long', [False, True]) +def test_angle_selection_preserves_pinion_and_aol_exact_profile(car, long): + from opendbc.bluepilot_lateral.hosts.starpilot import select as angle_select, qualified as angle_qualified + from opendbc.car.ford.aol import base_safety_param, AOL_WORDS + cp = params(car, long) + assert select(cp, True) + assert angle_select(cp, 1) + assert configured(cp) and angle_qualified(cp) + cp.safetyConfigs[-1].safetyParam |= base_safety_param(cp) + cp.alternativeExperience = 32 + assert cp.safetyConfigs[-1].safetyParam in AOL_WORDS + assert configured(cp) and angle_qualified(cp) and qualified(cp, marked_only=True) diff --git a/opendbc_repo/opendbc/safety/modes/ford.h b/opendbc_repo/opendbc/safety/modes/ford.h index bd1c2ca9da..7bc38c332c 100644 --- a/opendbc_repo/opendbc/safety/modes/ford.h +++ b/opendbc_repo/opendbc/safety/modes/ford.h @@ -2,6 +2,7 @@ #include "opendbc/safety/declarations.h" #include "opendbc/safety/modes/ford_aol.h" +#include "opendbc/safety/modes/ford_pinion_measurement.h" #include "opendbc/bluepilot_lateral/safety/ford_bp.h" // StarPilot's extended Ford curvature enforcement below is substantially adapted from @@ -16,6 +17,7 @@ #define FORD_DesiredTorqBrk 0x213U // RX from ABS, for standstill state #define FORD_BrakeSysFeatures 0x415U // RX from ABS, for vehicle speed #define FORD_EngVehicleSpThrottle2 0x202U // RX from PCM, for second vehicle speed +#define FORD_SteeringPinion_Data 0x7EU #define FORD_Yaw_Data_FD1 0x91U // RX from RCM, for yaw rate #define FORD_Steering_Data_FD1 0x083U // TX by OP, various driver switches and LKAS/CC buttons #define FORD_ACCDATA 0x186U // TX by OP, ACC controls @@ -40,6 +42,9 @@ static uint8_t ford_get_counter(const CANPacket_t *msg) { // Signal: VehRollYaw_No_Cnt cnt = msg->data[5]; } + if (msg->addr == FORD_SteeringPinion_Data) { + cnt = (msg->data[5] >> 4) & 0xFU; + } return cnt; } @@ -86,6 +91,9 @@ static bool ford_get_quality_flag_valid(const CANPacket_t *msg) { if (msg->addr == FORD_Yaw_Data_FD1) { valid = ((msg->data[6] >> 4) & 0x3U) == 0x3U; // VehYawWActl_D_Qf } + if (msg->addr == FORD_SteeringPinion_Data) { + valid = ((msg->data[5] >> 2) & 0x3U) == 0x3U; + } return valid; } @@ -105,6 +113,17 @@ static const CurvatureSteeringLimits FORD_STEERING_LIMITS = { .max_steer_power = 0, // disabled, Ford has no steed power signal }; +static CurvatureSteeringLimits ford_measurement_limits(CurvatureSteeringLimits limits) { + return (CurvatureSteeringLimits){ + .max_curvature = limits.max_curvature, + .curvature_to_can = limits.curvature_to_can, + .frequency = limits.frequency, + .max_curvature_error = ford_pinion_enabled ? SAFETY_MAX(limits.max_curvature_error, 150) : limits.max_curvature_error, + .curvature_error_min_speed = limits.curvature_error_min_speed, + .max_steer_power = limits.max_steer_power, + }; +} + // Transit 0x3CA has 0.000005 rad/m per raw step and runs at 33 Hz. Keep its // command history separate from the inactive 0x3D3 heartbeat at 20 Hz. static const CurvatureSteeringLimits FORD_LKA_STEERING_LIMITS = { @@ -178,7 +197,7 @@ static void ford_rx_hook(const CANPacket_t *msg) { } // Update vehicle yaw rate - if (msg_matches(msg, FORD_Yaw_Data_FD1, FORD_MAIN_BUS)) { + if (msg_matches(msg, FORD_Yaw_Data_FD1, FORD_MAIN_BUS) && !ford_pinion_enabled) { // FIXME: safety can receive yaw before new vehicle speed, it should recompute meas on either received // Signal: VehYaw_W_Actl // TODO: we should use the speed which results in the closest angle measurement to the desired angle @@ -193,6 +212,14 @@ static void ford_rx_hook(const CANPacket_t *msg) { } } + if (ford_pinion_enabled && msg_matches(msg, FORD_SteeringPinion_Data, FORD_MAIN_BUS)) { + const int raw = ((msg->data[2] & 0x7FU) << 8) | msg->data[3]; + const float radians = (((float)raw * 0.1F) - 1600.0F) * 0.017453292519943295F; + const float speed = SAFETY_MAX(vehicle_speed.values[0] / VEHICLE_SPEED_FACTOR, 0.1F); + const float measured = radians * get_curvature_factor(speed, *ford_pinion_params) / ford_pinion_params->steer_ratio; + update_sample(&curvature_state.meas, ROUND(measured * FORD_STEERING_LIMITS.curvature_to_can)); + } + // Update gas pedal if (msg_matches(msg, FORD_EngVehicleSpThrottle, FORD_MAIN_BUS)) { // Pedal position: (0.1 * val) in percent @@ -383,7 +410,7 @@ static bool ford_tx_hook(const CANPacket_t *msg) { } else { violation |= raw_curvature_rate != FORD_INACTIVE_CURVATURE_RATE; } - violation |= steer_curvature_cmd_checks(desired_curvature, 0, steer_control_enabled, FORD_STEERING_LIMITS); + violation |= steer_curvature_cmd_checks(desired_curvature, 0, steer_control_enabled, ford_measurement_limits(FORD_STEERING_LIMITS)); violation |= ford_lka_steering && steer_control_enabled; if (violation) { @@ -443,7 +470,7 @@ static bool ford_tx_hook(const CANPacket_t *msg) { violation |= ((desired_curvature * desired_path_angle) <= 0) || (combined_accel > 2.5F); violation |= SAFETY_ABS(desired_path_angle - ford_mach_e_path_angle_last) > 110; } - violation |= steer_curvature_cmd_checks(desired_curvature, 0, steer_control_enabled, FORD_MACH_E_STEERING_LIMITS); + violation |= steer_curvature_cmd_checks(desired_curvature, 0, steer_control_enabled, ford_measurement_limits(FORD_MACH_E_STEERING_LIMITS)); } else if (ford_generic_canfd_extended) { // One generic CAN FD owner; Mach-E's wider error/path assist stays separate. static const struct lookup_t source_rate = {{5.0F, 16.0F, 25.0F}, {0.0025F, 0.0014F, 0.00018F}}; @@ -460,10 +487,10 @@ static bool ford_tx_hook(const CANPacket_t *msg) { } else { violation |= (desired_curvature != 0) || (raw_curvature_rate != FORD_CANFD_INACTIVE_CURVATURE_RATE); } - violation |= steer_curvature_cmd_checks(desired_curvature, 0, steer_control_enabled, FORD_STEERING_LIMITS); + violation |= steer_curvature_cmd_checks(desired_curvature, 0, steer_control_enabled, ford_measurement_limits(FORD_STEERING_LIMITS)); } else { violation |= (raw_curvature_rate != FORD_CANFD_INACTIVE_CURVATURE_RATE) || (raw_path_angle != FORD_INACTIVE_PATH_ANGLE); - violation |= steer_curvature_cmd_checks(desired_curvature, 0, steer_control_enabled, FORD_STEERING_LIMITS); + violation |= steer_curvature_cmd_checks(desired_curvature, 0, steer_control_enabled, ford_measurement_limits(FORD_STEERING_LIMITS)); } tx &= !violation; if (ford_generic_canfd_extended && !tx) { @@ -505,8 +532,15 @@ static void ford_aol_bind_tx(safety_config *ret, CanMsg workspace[7]) { } } +static RxCheck ford_rx_workspace[11]; + static safety_config ford_init(uint16_t configured_param) { - const uint16_t bp_param = configured_param & ~256U; + const bool pinion_valid = ford_pinion_word_valid(configured_param); + const uint16_t profile_param = ford_pinion_base_word(configured_param); + const uint16_t geometry = (configured_param >> 10U) & 15U; + ford_pinion_enabled = pinion_valid && ((configured_param & FORD_PINION_ENABLE) != 0U); + ford_pinion_params = ford_pinion_enabled ? &ford_pinion_geometry[geometry] : &ford_pinion_geometry[0]; + const uint16_t bp_param = profile_param & ~256U; const bool joint_angle = ((unsigned int)alternative_experience == 32U) && ((bp_param == 146U) || (bp_param == 147U) || (bp_param == 160U) || (bp_param == 161U) || (bp_param == 194U) || (bp_param == 195U)); const uint16_t param = joint_angle ? (bp_param & ~128U) : bp_param; @@ -672,7 +706,6 @@ static safety_config ford_init(uint16_t configured_param) { {.msg = {{0x176, 0, 8, 10U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, {.msg = {{FORD_Lane_Assist_Data3, 0, 8, 30U, .ignore_checksum = true, .ignore_counter = true, .ignore_quality_flag = true}, {0}, {0}}}, }; - static RxCheck ford_aol_rx_workspace[10]; _Static_assert(sizeof(ford_aol_default_rx) == (9U * sizeof(RxCheck)), "Ford default RX count"); _Static_assert(sizeof(ford_aol_edge_rx) == (9U * sizeof(RxCheck)), "Ford Edge RX count"); _Static_assert(sizeof(ford_aol_mondeo_rx) == (10U * sizeof(RxCheck)), "Ford Mondeo RX count"); @@ -692,16 +725,16 @@ static safety_config ford_init(uint16_t configured_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; + ford_rx_workspace[i] = selected_rx[i]; + const unsigned int addr = ford_rx_workspace[i].msg[0].addr; if (manual && ((addr == 0x176U) || (addr == 0x230U) || (addr == 0x5AU))) { - ford_aol_rx_workspace[i].msg[0].addr = 0x3C3U; + ford_rx_workspace[i].msg[0].addr = 0x3C3U; } } else { - ford_aol_rx_workspace[i] = (RxCheck){0}; + ford_rx_workspace[i] = (RxCheck){0}; } } - ret.rx_checks = ford_aol_rx_workspace; + ret.rx_checks = ford_rx_workspace; ret.rx_checks_len = selected_len; ret.disable_forwarding = false; static CanMsg ford_aol_tx_msgs[7]; @@ -711,6 +744,28 @@ static safety_config ford_init(uint16_t configured_param) { _Static_assert(sizeof(FORD_CANFD_LONG_TX_MSGS) <= sizeof(ford_aol_tx_msgs), "Ford CAN FD long TX capacity"); ford_aol_bind_tx(&ret, ford_aol_tx_msgs); } + if (ford_pinion_enabled) { + // Append after profile-specific RX selection so AOL retains every existing check. + if (ret.rx_checks_len <= 10) { + const int count = ret.rx_checks_len; + for (int i = 0; i < count; i++) { + ford_rx_workspace[i] = ret.rx_checks[i]; + } + ford_rx_workspace[count] = (RxCheck){.msg = {{FORD_SteeringPinion_Data, 0, 8, 100U, + .max_counter = 15U, .ignore_checksum = true}, {0}, {0}}}; + ret.rx_checks = ford_rx_workspace; + ret.rx_checks_len = count + 1; + } else { + ret.tx_msgs = NULL; + ret.tx_msgs_len = 0; + ford_aol_reset(); + } + } + if (!pinion_valid) { + ret.tx_msgs = NULL; + ret.tx_msgs_len = 0; + ford_aol_reset(); + } return ret; } diff --git a/opendbc_repo/opendbc/safety/modes/ford_aol.h b/opendbc_repo/opendbc/safety/modes/ford_aol.h index 24dec5b0d3..6a866c5ba6 100644 --- a/opendbc_repo/opendbc/safety/modes/ford_aol.h +++ b/opendbc_repo/opendbc/safety/modes/ford_aol.h @@ -1,4 +1,5 @@ #pragma once +#include "opendbc/safety/modes/ford_pinion.h" static bool ford_aol_manual = false; static bool ford_aol_enabled = false; @@ -202,7 +203,7 @@ static void ford_aol_full_engagement(bool cruise_rising) { static bool ford_aol_param_valid(uint16_t param) { bool result = false; - switch (param & ~256U) { + switch (ford_pinion_base_word(param) & ~256U) { case 8U: case 9U: case 10U: case 12U: case 13U: case 18U: case 32U: case 33U: case 66U: result = true; break; @@ -220,7 +221,7 @@ 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_manual = (param & 256U) != 0U; - ford_aol_param = param & ~(256U | 128U); + ford_aol_param = ford_pinion_base_word(param) & ~(256U | 128U); if (ford_aol_enabled) { static const AolSafetyPolicy policy = { .reset = ford_aol_reset, diff --git a/opendbc_repo/opendbc/safety/modes/ford_pinion.h b/opendbc_repo/opendbc/safety/modes/ford_pinion.h new file mode 100644 index 0000000000..d3f6f868d5 --- /dev/null +++ b/opendbc_repo/opendbc/safety/modes/ford_pinion.h @@ -0,0 +1,31 @@ +#pragma once +// Pinion measurement selection adapted from BluePilot PR #145 (fe5b0195). +// See CREDITS.md and THIRD_PARTY_NOTICES.md. No live mode switching. +#define FORD_PINION_ENABLE 0x200U +#define FORD_PINION_MASK 0x3E00U + +static bool ford_pinion_word_valid(uint16_t word) { + bool valid = true; + if ((word & FORD_PINION_MASK) != 0U) { + const uint16_t index = (word >> 10U) & 15U; + const uint16_t base = word & ~(FORD_PINION_MASK | 256U); + valid = (word & FORD_PINION_ENABLE) != 0U; + if ((index == 1U) || (index == 2U) || (index == 5U) || (index == 6U) || (index == 9U)) { + valid &= (base == 32U) || (base == 33U) || (base == 128U) || (base == 129U) || (base == 160U) || (base == 161U); + } else if ((index == 3U) || (index == 4U) || (index == 7U) || (index == 8U) || (index == 12U)) { + valid &= (base == 66U) || (base == 67U) || (base == 130U) || (base == 131U) || (base == 194U) || (base == 195U); + } else if (index == 11U) { + valid &= (base == 18U) || (base == 19U) || (base == 130U) || (base == 131U) || (base == 146U) || (base == 147U); + } else if (index == 10U) { + valid &= (base == 10U) || (base == 11U); + } else { + valid = false; + } + } + return valid; +} + +static uint16_t ford_pinion_base_word(uint16_t word) { + return ford_pinion_word_valid(word) ? (word & ~FORD_PINION_MASK) : 0xFFFFU; +} + diff --git a/opendbc_repo/opendbc/safety/modes/ford_pinion_measurement.h b/opendbc_repo/opendbc/safety/modes/ford_pinion_measurement.h new file mode 100644 index 0000000000..376a24fd60 --- /dev/null +++ b/opendbc_repo/opendbc/safety/modes/ford_pinion_measurement.h @@ -0,0 +1,21 @@ +#pragma once +#define FORD_PINION_GEOMETRY_COUNT 12U +static const AngleSteeringParams ford_pinion_geometry[FORD_PINION_GEOMETRY_COUNT + 1U] = { + {.slip_factor = 0.0f, .steer_ratio = 1.0f, .wheelbase = 1.0f}, // 0: invalid + {.slip_factor = -0.00062819555f, .steer_ratio = 17.7f, .wheelbase = 2.670f}, // 1: FORD_BRONCO_SPORT_MK1 + {.slip_factor = -0.00061892325f, .steer_ratio = 16.7f, .wheelbase = 2.710f}, // 2: FORD_ESCAPE_MK4 + {.slip_factor = -0.00061892325f, .steer_ratio = 16.7f, .wheelbase = 2.710f}, // 3: FORD_ESCAPE_MK4_5 + {.slip_factor = -0.00045454798f, .steer_ratio = 17.0f, .wheelbase = 3.690f}, // 4: FORD_EXPEDITION_MK4 + {.slip_factor = -0.00055447339f, .steer_ratio = 16.8f, .wheelbase = 3.025f}, // 5: FORD_EXPLORER_MK6 + {.slip_factor = -0.00062121569f, .steer_ratio = 15.0f, .wheelbase = 2.700f}, // 6: FORD_FOCUS_MK4 + {.slip_factor = -0.00045331952f, .steer_ratio = 16.9f, .wheelbase = 3.700f}, // 7: FORD_F_150_LIGHTNING_MK1 + {.slip_factor = -0.00042037149f, .steer_ratio = 17.0f, .wheelbase = 3.990f}, // 8: FORD_F_150_MK14 + {.slip_factor = -0.00054528036f, .steer_ratio = 17.0f, .wheelbase = 3.076f}, // 9: FORD_MAVERICK_MK1 + {.slip_factor = -0.00058852001f, .steer_ratio = 14.8f, .wheelbase = 2.850f}, // 10: FORD_MONDEO_MK5 + {.slip_factor = -0.00056209187f, .steer_ratio = 17.0f, .wheelbase = 2.984f}, // 11: FORD_MUSTANG_MACH_E_MK1 + {.slip_factor = -0.00051293030f, .steer_ratio = 17.0f, .wheelbase = 3.270f}, // 12: FORD_RANGER_MK2 +}; + + +static bool ford_pinion_enabled = false; +static const AngleSteeringParams *ford_pinion_params = &ford_pinion_geometry[0]; diff --git a/opendbc_repo/opendbc/safety/tests/libsafety/safety.c b/opendbc_repo/opendbc/safety/tests/libsafety/safety.c index d24cfefd66..c01a2c55f3 100644 --- a/opendbc_repo/opendbc/safety/tests/libsafety/safety.c +++ b/opendbc_repo/opendbc/safety/tests/libsafety/safety.c @@ -456,3 +456,11 @@ bool safety_test_hyundai_gv60_catalog_rejection(bool alternate) { #endif return rejected; } + +// Pinion protocol introspection for exact host/native geometry and namespace tests. +uint16_t get_ford_pinion_base(uint16_t word) { return ford_pinion_base_word(word); } +bool get_ford_pinion_enabled(void) { return ford_pinion_enabled; } +float get_ford_pinion_slip(int index) { return ford_pinion_geometry[index].slip_factor; } +float get_ford_pinion_ratio(int index) { return ford_pinion_geometry[index].steer_ratio; } +float get_ford_pinion_wheelbase(int index) { return ford_pinion_geometry[index].wheelbase; } +int get_ford_pinion_rx_count(void) { return current_safety_config.rx_checks_len; } diff --git a/openpilot/common/params_keys.h b/openpilot/common/params_keys.h index 69cd8827c7..df7320ba31 100644 --- a/openpilot/common/params_keys.h +++ b/openpilot/common/params_keys.h @@ -42,6 +42,7 @@ inline static std::unordered_map keys = { {"HondaBoschARadar", {PERSISTENT, BOOL, "1"}}, {"HondaGasFactorParams", {PERSISTENT, FLOAT, "1.0"}}, {"HondaWindFactorParams", {PERSISTENT, FLOAT, "1.0"}}, + {"FordPrefSteerAngleCurvature", {PERSISTENT, BOOL, "0"}}, {"FordLateralMode", {PERSISTENT, INT, "0"}}, {"FordHandsFreeCluster", {PERSISTENT, BOOL, "0"}}, {"FordHumanTurnDetection", {PERSISTENT, BOOL, "1"}}, diff --git a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc index 49e4c06e6c..9881274a0c 100644 --- a/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc +++ b/openpilot/selfdrive/pandad/tests/test_aol_protocol.cc @@ -252,6 +252,18 @@ int main() { for (uint32_t word = 0U; word <= 65535U; ++word) { bool expected = false; for (const auto exact : ford_words) expected |= (word == exact) || (word == (exact | 256U)); + // Exact opt-in geometry families, independently enumerated from the native decoder. + for (uint16_t geometry = 1U; geometry <= 12U; geometry++) { + const bool classic = (geometry == 1U) || (geometry == 2U) || (geometry == 5U) || (geometry == 6U) || (geometry == 9U); + const uint16_t curvature = classic ? 32U : ((geometry == 11U) ? 18U : ((geometry == 10U) ? 10U : 66U)); + for (uint16_t longitudinal = 0U; longitudinal <= 1U; longitudinal++) { + for (uint16_t angle = 0U; angle <= 1U; angle++) { + if ((geometry == 10U) && angle) continue; + const uint16_t exact = 512U | (geometry << 10U) | curvature | longitudinal | (angle ? 128U : 0U); + expected |= (word == exact) || (word == (exact | 256U)); + } + } + } auto ford = status(); ford.safety_mode = 6U; ford.safety_param = static_cast(word); diff --git a/openpilot/starpilot/car/ford/aol_policy.h b/openpilot/starpilot/car/ford/aol_policy.h index 028fb5d2a2..5ac9947947 100644 --- a/openpilot/starpilot/car/ford/aol_policy.h +++ b/openpilot/starpilot/car/ford/aol_policy.h @@ -1,10 +1,11 @@ #pragma once #include "openpilot/selfdrive/pandad/aol_protocol.h" +#include "opendbc/safety/modes/ford_pinion.h" static inline bool ford_aol_param(uint16_t param) { bool result = false; - switch (param & ~256U) { + switch (ford_pinion_base_word(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: case 146U: case 147U: case 160U: case 161U: case 194U: case 195U: diff --git a/openpilot/starpilot/controller_extensions.py b/openpilot/starpilot/controller_extensions.py index 461a223b8b..7ddd5d0699 100644 --- a/openpilot/starpilot/controller_extensions.py +++ b/openpilot/starpilot/controller_extensions.py @@ -10,13 +10,17 @@ from openpilot.common.swaglog import cloudlog from openpilot.starpilot.controllers.toyota_cruise import ToyotaCruisePreference, capability as toyota_cruise_capability from opendbc.car.ford.values import CAR, FordFlags +from opendbc.car.ford.pinion import configured as pinion_configured class ManualTurnInputs: - def __init__(self, params, *, track_assist_permission: bool = False): + def __init__(self, params, *, track_assist_permission: bool = False, pinion: bool = False): self.params = params + self.pinion = pinion + self.vehicle_parameters = None self.track_assist_permission = track_assist_permission - self.sm = messaging.SubMaster(["modelV2", "lateralDelay"] + (["pandaStates", "aolSafetyWire"] if track_assist_permission else [])) + services = ["modelV2", "lateralDelay"] + (["pandaStates", "aolSafetyWire"] if track_assist_permission else []) + self.sm = messaging.SubMaster(services + (["vehicleParameters"] if pinion else [])) self.enabled = bool(self.params.get("FordHumanTurnDetection", return_default=True)) self.frames = 0 self.blend_settings = self._read_blend_settings() @@ -39,12 +43,16 @@ class ManualTurnInputs: def apply_blend_settings(self, controller): for name in ("mache_lateral", "classic_lateral", "new_port_curvature"): owner = getattr(controller, name, None) + if owner is not None and self.pinion: + owner.measurement.update_parameters(self.vehicle_parameters) if owner is not None and self._applied_blends.get(owner) != self.blend_settings: owner.set_blend_settings(*self.blend_settings) self._applied_blends[owner] = self.blend_settings def update(self): self.sm.update(0) + if self.pinion and self.sm.valid["vehicleParameters"] and self.sm.alive["vehicleParameters"]: + self.vehicle_parameters = self.sm["vehicleParameters"] if self.frames % 100 == 0: self.enabled = bool(self.params.get("FordHumanTurnDetection", return_default=True)) self.blend_settings = self._read_blend_settings() @@ -249,20 +257,20 @@ def configure_controller(CI, params): if (controller is not None and cp.brand == "ford" and cp.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1 and not cp.flags & FordFlags.LKA_STEERING and not cp.passive and not cp.dashcamOnly and not cp.notCar and getattr(controller, "manual_turn", None) is not None): - controller.manual_turn_inputs = ManualTurnInputs(params, track_assist_permission=bool(cp.flags & FordFlags.CANFD)) + controller.manual_turn_inputs = ManualTurnInputs(params, track_assist_permission=bool(cp.flags & FordFlags.CANFD), pinion=pinion_configured(cp)) from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle_qualified if controller is not None and angle_qualified(cp) and getattr(controller, "bp_lat", None) is not None: - controller.bp_lat.host.inputs = ManualTurnInputs(params, track_assist_permission=True) + controller.bp_lat.host.inputs = ManualTurnInputs(params, track_assist_permission=True, pinion=pinion_configured(cp)) from opendbc.car.ford.classic_lateral import qualified as classic_qualified from opendbc.car.ford.generic_canfd_lateral import qualified as generic_canfd_qualified if controller is not None and (classic_qualified(cp) or generic_canfd_qualified(cp)) and getattr(controller, "classic_lateral", None) is not None: - controller.manual_turn_inputs = ManualTurnInputs(params) + controller.manual_turn_inputs = ManualTurnInputs(params, pinion=pinion_configured(cp)) from opendbc.car.ford.new_port_curvature import qualified as new_port_curvature_qualified if controller is not None and new_port_curvature_qualified(cp) and getattr(controller, "new_port_curvature", None) is not None: - controller.manual_turn_inputs = ManualTurnInputs(params) + controller.manual_turn_inputs = ManualTurnInputs(params, pinion=pinion_configured(cp)) if controller is not None and cp.brand == "ford" and getattr(controller, "manual_turn_inputs", None) is not None: controller.manual_turn_inputs.apply_blend_settings(controller) diff --git a/openpilot/starpilot/galaxy/tests/test_ford_pinion_settings.py b/openpilot/starpilot/galaxy/tests/test_ford_pinion_settings.py new file mode 100644 index 0000000000..3d581ee785 --- /dev/null +++ b/openpilot/starpilot/galaxy/tests/test_ford_pinion_settings.py @@ -0,0 +1,61 @@ +"""Actual typed Params, Galaxy parked write, and startup configuration for pinion.""" +from pathlib import Path +import tempfile +from types import SimpleNamespace + +import pytest +from opendbc.car import gen_empty_fingerprint +from opendbc.car.ford.pinion import KEY, GEOMETRY, configured +from opendbc.car.ford.values import CAR +from opendbc.car.ford.tests.test_three_ports import params as ford_params +from openpilot.common.params import Params +from openpilot.starpilot.galaxy.settings import AuthorityContext, SettingsGateway + + +def test_real_galaxy_typed_parked_selection_and_stale_dependency(): + with tempfile.TemporaryDirectory() as directory: + params = Params(directory) + assert params.get_default_value(KEY) is False + params.put_bool('SafeMode', False, block=True) + params.put_bool('AlwaysOnLateral', False, block=True) + cp = ford_params(CAR.FORD_EXPLORER_MK6) + parked = True + context = SimpleNamespace(sample=lambda: AuthorityContext(parked, cp, cp.as_reader().as_builder().to_bytes())) + gateway = SettingsGateway(params, context) + try: + page = gateway.page('torque', 'pinion-test', b'generation') + index, row = next((i, row) for i, row in enumerate(page['rows']) if row['label'] == 'Use Pinion Yaw Sensor') + assert row['choices'] == ['Off', 'On'] and row['available'] + intent = gateway.preview(page['view'], index, 0, 'pinion-test', b'generation', value='On') + assert gateway.confirm(intent['intent'], 'pinion-test', b'generation') + assert params.get(KEY) is True + assert Path(params.get_param_path(KEY)).read_bytes() == b'1' + assert not configured(cp) # Saved preference cannot mutate the current driving configuration. + parked = False + page = gateway.page('torque', 'pinion-test', b'generation') + row = next(row for row in page['rows'] if row['label'] == 'Use Pinion Yaw Sensor') + assert not row['available'] + finally: + gateway.close() + + +@pytest.mark.parametrize('identity', list(GEOMETRY)) +def test_actual_factory_applies_before_controller_and_published_cp(identity): + from openpilot.starpilot.tests.test_vehicle_preferences import TestVehicleStartupPreferences + helper = TestVehicleStartupPreferences() + helper.setUp() + try: + helper.raw('SafeMode', b'0') + helper.raw('AlwaysOnLateral', b'0') + helper.params.put_bool('AlphaLongitudinalEnabled', False, block=True) + helper.params.put_bool('IsReleaseBranch', False, block=True) + fp = gen_empty_fingerprint() + fp[0][0x5A] = 8 + fp[2].update({0x3D6: 8, 0x186: 8}) + host, constructed, published = helper.start(identity, key=KEY, requested=True, observed=fp, + capture=lambda ci: configured(ci.CP)) + assert constructed == [True] + assert configured(published) + assert host.CI.CP.to_dict() == published.to_dict() + finally: + helper.doCleanups() diff --git a/openpilot/starpilot/ui/feature_settings_owner.py b/openpilot/starpilot/ui/feature_settings_owner.py index 413073f9b4..797665e35e 100644 --- a/openpilot/starpilot/ui/feature_settings_owner.py +++ b/openpilot/starpilot/ui/feature_settings_owner.py @@ -53,6 +53,7 @@ from openpilot.starpilot.ui.wheel_feature import WheelFeature, PREFIX as WHEEL_P from openpilot.starpilot.longitudinal.output_max import KEY as OUTPUT_MAX_KEY from openpilot.starpilot.ui.tesla_screen_feature import TeslaScreenFeature, KEYS as TESLA_SCREEN_KEYS from openpilot.starpilot.ui.ford_lateral_feature import FordLateralFeature, FordClusterFeature +from openpilot.starpilot.ui.ford_pinion_feature import FordPinionFeature from openpilot.starpilot.ui.preap_radar_feature import PreAPRadarFeature, KEYS as PREAP_RADAR_KEYS, capability as preap_radar_capability from openpilot.starpilot.ui.gm_tune_feature import GmTuneFeature, GmTruckFeature, GmEvPresetFeature from openpilot.starpilot.car.gm.tune_preferences import KEY as GM_TUNE_KEY @@ -992,6 +993,7 @@ class FeatureSettingsOwner: elif page == FeaturePage.TORQUE: title = "Steering and Torque" rows.extend(FordLateralFeature(self).rows(parked)) + rows.extend(FordPinionFeature(self).rows(parked)) rows.extend(FordClusterFeature(self).rows(parked)) controller_row = self.controller.row() if controller_row is not None: @@ -1309,6 +1311,8 @@ class FeatureSettingsOwner: return self._apply_lateral_pause(request) if key == "FordHandsFreeCluster": return FordClusterFeature(self).apply(request) + if key == "FordPrefSteerAngleCurvature": + return FordPinionFeature(self).apply(request) if key == "FordLateralMode": return FordLateralFeature(self).apply(request) if key == "TurnAssist": diff --git a/openpilot/starpilot/ui/ford_pinion_feature.py b/openpilot/starpilot/ui/ford_pinion_feature.py new file mode 100644 index 0000000000..6c8419c431 --- /dev/null +++ b/openpilot/starpilot/ui/ford_pinion_feature.py @@ -0,0 +1,74 @@ +"""Parked, init-only pinion measurement preference shared by native UI and Galaxy.""" +from opendbc.car.ford.aol import qualified as aol_qualified +from opendbc.car.ford.pinion import KEY +from openpilot.starpilot.saved_source import read_saved +from openpilot.starpilot.saved_document import commit_exact +from openpilot.starpilot.ui.feature_settings_state import FeatureRow + +CHOICES = {0: "Off", 1: "On"} + + +def read_choice(params): + raw, readable = read_saved(params, KEY, 8) + valid = readable and raw in (None, b"0", b"1") + return int(valid and raw == b"1"), raw, readable, valid + + +class FordPinionFeature: + def __init__(self, owner): + self.owner = owner + + def capability(self): + cp = self.owner.vehicle_params() + try: + if cp is None: + return None + from opendbc.car.ford.pinion import GEOMETRY + if cp.carFingerprint not in GEOMETRY or not aol_qualified(cp): + return None + return (str(cp.carFingerprint), int(cp.flags), int(cp.alternativeExperience), bool(cp.openpilotLongitudinalControl), + bool(cp.pcmCruise), str(cp.steerControlType), bool(cp.passive), bool(cp.dashcamOnly), bool(cp.notCar), + tuple((str(c.safetyModel), int(c.safetyParam)) for c in cp.safetyConfigs)) + except (AttributeError, TypeError, ValueError, OverflowError): + return None + + def dependencies(self): + values = [] + for key in ("SafeMode", "AlwaysOnLateral"): + raw, readable = read_saved(self.owner.params, key, 8) + if not readable or raw not in ((None, b"0") if key == "SafeMode" else (None, b"0", b"1")): + return None + values.append((key, raw)) + return tuple(values) + + def rows(self, parked): + capability = self.capability() + if capability is None: + return () + value, raw, readable, valid = read_choice(self.owner.params) + dependencies = self.dependencies() + return (FeatureRow(KEY, "Use Pinion Yaw Sensor", CHOICES[value] if valid else "Invalid saved choice", raw, + choices=tuple(CHOICES.values()), available=parked and self.owner.authority("parked_preferences") and + readable and dependencies is not None, capability=capability, + dependencies=dependencies or (), repair_value="" if valid else CHOICES[0], default_value=CHOICES[0], + reason="Off by default. Uses steering-pinion curvature instead of RCM yaw in control and Panda safety. " + + "Applies next vehicle startup; Safe Mode must be off."),) + + def apply(self, request): + value = next((code for code, label in CHOICES.items() if label == request.value), None) + if (request.key != KEY or value is None or request.related_source is not None or request.display_unit or request.direction): + return False + + def authorized(): + capability = self.capability() + _, raw, readable, valid = read_choice(self.owner.params) + dependencies = self.dependencies() + return (self.owner.authority("parked_preferences") and bool(request.vehicle_fingerprint) and + self.owner.vehicle_fingerprint() == request.vehicle_fingerprint and capability is not None and + capability == request.capability and dependencies is not None and dependencies == request.dependencies and + readable and raw == request.expected and (valid or value == 0)) + + result = commit_exact(self.owner.params, key=KEY, max_bytes=8, raw=str(value).encode(), expected=request.expected, + authorized=authorized, temp_prefix=".ford-pinion-") + return result.committed and result.verified + diff --git a/openpilot/starpilot/vehicle_preferences.py b/openpilot/starpilot/vehicle_preferences.py index fa8b50d2e8..7d1c44a86a 100644 --- a/openpilot/starpilot/vehicle_preferences.py +++ b/openpilot/starpilot/vehicle_preferences.py @@ -42,6 +42,7 @@ class VehicleStartupPreferences: toyota_filter: bool = True gm_longitudinal_tune: int = 0 ford_lateral_mode: int = 0 + ford_pinion: bool = field(default=False, kw_only=True) toyota_lock_doors: bool = False toyota_unlock_doors: bool = False toyota_radar_mode: int = 0 @@ -112,7 +113,9 @@ class VehicleStartupPreferences: subaru_sng=bool(enabled and readable and safe in (None, b"0") and read_saved(params, "SubaruSNG", 8) == (b"1", True)), subaru_software_cruise=bool(enabled and readable and safe in (None, b"0") and read_saved(params, "SubaruRedneckCruise", 8) == (b"1", True)), toyota_radar_mode=toyota_radar_mode(params, enabled=enabled), - ford_lateral_mode=ford_mode(params, enabled=enabled), tesla_screen=screen, + ford_lateral_mode=ford_mode(params, enabled=enabled), + ford_pinion=bool(enabled and read_saved(params, "SafeMode", 8) in ((None, True), (b"0", True)) and + read_saved(params, "FordPrefSteerAngleCurvature", 8) == (b"1", True)), tesla_screen=screen, tesla_screen_brake=screen_brake, toyota_auto_hold=toyota, jeep_brake_hold=jeep_hold, gm_longitudinal_tune=selected_tune(params, enabled=enabled), toyota_lock_doors=bool(enabled and readable and safe in (None, b"0") and @@ -221,6 +224,8 @@ class VehicleStartupPreferences: apply_radar_mode(cp, self.toyota_radar_mode) from opendbc.bluepilot_lateral.hosts.starpilot import select as select_ford_lateral select_ford_lateral(cp, self.ford_lateral_mode) + from opendbc.car.ford.pinion import select as select_pinion + select_pinion(cp, self.ford_pinion) return cp def finalize(self, cp) -> None: diff --git a/prebuilt.json b/prebuilt.json index c20db3c16c..c658315eed 100644 --- a/prebuilt.json +++ b/prebuilt.json @@ -1 +1 @@ -{"protected_inventory_sha256": "13911302060b8abd2039f8860f69bfe5e78a40a9b05d68eeb09b9d47254a4450", "schema_version": 1} +{"protected_inventory_sha256": "e586cac372850cf9e03c7a677d3477ae1e3b685d47cd1767faf424579acb0631", "schema_version": 1}