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.
This commit is contained in:
Dom
2026-10-27 17:14:17 -05:00
parent 4b94f415ad
commit 4cbffe53b3
22 changed files with 575 additions and 33 deletions
+4
View File
@@ -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.
@@ -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,
@@ -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);
+7
View File
@@ -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:
@@ -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:
@@ -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)
+84
View File
@@ -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)
@@ -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)
+67 -12
View File
@@ -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;
}
+3 -2
View File
@@ -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,
@@ -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;
}
@@ -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];
@@ -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; }
+1
View File
@@ -42,6 +42,7 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> 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"}},
@@ -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<uint16_t>(word);
+2 -1
View File
@@ -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:
+14 -6
View File
@@ -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)
@@ -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()
@@ -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":
@@ -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
+6 -1
View File
@@ -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:
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "13911302060b8abd2039f8860f69bfe5e78a40a9b05d68eeb09b9d47254a4450", "schema_version": 1}
{"protected_inventory_sha256": "e586cac372850cf9e03c7a677d3477ae1e3b685d47cd1767faf424579acb0631", "schema_version": 1}