mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-08-20 07:43:48 +08:00
Ford Stuff
Huge thanks to BluePilot guys!
This commit is contained in:
@@ -335,6 +335,16 @@ inline static std::unordered_map<std::string, ParamKeyAttributes> keys = {
|
||||
{"ForceStopDistanceOffset", {PERSISTENT, INT, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"ForceStandstill", {PERSISTENT, BOOL, "0", "0", 2, SETTINGS_SIMPLE}},
|
||||
{"ForceTorqueController", {PERSISTENT, BOOL, "0", "0", 3}},
|
||||
{"FordAngleBlend", {PERSISTENT, FLOAT, "0.5", "0.5", 2}},
|
||||
{"FordAngleHighSpeedDamping", {PERSISTENT, FLOAT, "1.0", "1.0", 2}},
|
||||
{"FordAngleHighSpeedFactor", {PERSISTENT, FLOAT, "1.0", "1.0", 2}},
|
||||
{"FordAngleLaneChangeFactor", {PERSISTENT, FLOAT, "1.0", "1.0", 2}},
|
||||
{"FordAngleLowSpeedFactor", {PERSISTENT, FLOAT, "1.0", "1.0", 2}},
|
||||
{"FordCurvatureBlendHigh", {PERSISTENT, FLOAT, "0.4", "0.4", 2}},
|
||||
{"FordCurvatureBlendLow", {PERSISTENT, FLOAT, "0.4", "0.4", 2}},
|
||||
{"FordCurvatureLaneChangeFactor", {PERSISTENT, FLOAT, "0.85", "0.85", 2}},
|
||||
{"FordHumanTurnDetection", {PERSISTENT, BOOL, "1", "1", 2}},
|
||||
{"FordLateralMode", {PERSISTENT, INT, "1", "1", 2}},
|
||||
{"FLMActiveOverrides", {PERSISTENT, JSON, "{}", "{}", 2}},
|
||||
{"FLMActiveProfileId", {PERSISTENT, STRING, "", "", 2}},
|
||||
{"FLMSubmittedTune", {CLEAR_ON_MANAGER_START, JSON, "{}", "{}"}},
|
||||
|
||||
@@ -6,6 +6,8 @@ from opendbc.car.lateral import ISO_LATERAL_ACCEL, apply_std_steer_angle_limits
|
||||
from opendbc.car.ford import fordcan
|
||||
from opendbc.car.ford.values import CarControllerParams, FordFlags, CAR
|
||||
from opendbc.car.interfaces import CarControllerBase, V_CRUISE_MAX
|
||||
from openpilot.starpilot.car.ford import fordcan as starpilot_fordcan
|
||||
from openpilot.starpilot.car.ford.lateral import FordLateralController, FordLateralMode, FordLateralResult
|
||||
|
||||
LongCtrlState = structs.CarControl.Actuators.LongControlState
|
||||
VisualAlert = structs.CarControl.HUDControl.VisualAlert
|
||||
@@ -80,6 +82,9 @@ class CarController(CarControllerBase):
|
||||
self.steer_alert_last = False
|
||||
self.lead_distance_bars_last = None
|
||||
self.distance_bar_frame = 0
|
||||
self.ford_lateral = None if CP.flags & FordFlags.LKA_STEERING else FordLateralController(CP)
|
||||
self.ford_shadow_curvature = 0.0
|
||||
self.ford_lateral_announced_mode = FordLateralMode.native
|
||||
|
||||
def update(self, CC, CS, now_nanos, starpilot_toggles):
|
||||
can_sends = []
|
||||
@@ -91,6 +96,9 @@ class CarController(CarControllerBase):
|
||||
steer_alert = hud_control.visualAlert in (VisualAlert.steerRequired, VisualAlert.ldw)
|
||||
fcw_alert = hud_control.visualAlert == VisualAlert.fcw
|
||||
|
||||
if self.ford_lateral is not None:
|
||||
self.ford_lateral.update_inputs()
|
||||
|
||||
### acc buttons ###
|
||||
if CC.cruiseControl.cancel:
|
||||
can_sends.append(fordcan.create_button_msg(self.packer, self.CAN.camera, CS.buttons_stock_values, cancel=True))
|
||||
@@ -128,38 +136,70 @@ class CarController(CarControllerBase):
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN, active=lka_active, apply_angle=self.apply_angle_last,
|
||||
direction=direction, ramp_type=ramp_type, curvature=-self.apply_curvature_last))
|
||||
else:
|
||||
# send steer msg at 20Hz
|
||||
if (self.frame % CarControllerParams.STEER_STEP) == 0:
|
||||
# Bronco and some other cars consistently overshoot curv requests
|
||||
# Apply some deadzone + smoothing convergence to avoid oscillations
|
||||
if self.CP.carFingerprint in (CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_F_150_MK14):
|
||||
self.anti_overshoot_curvature_last = anti_overshoot(actuators.curvature, self.anti_overshoot_curvature_last, CS.out.vEgoRaw)
|
||||
apply_curvature = self.anti_overshoot_curvature_last
|
||||
lateral_mode = self.ford_lateral.mode
|
||||
lateral_mode_ready = lateral_mode == self.ford_lateral_announced_mode
|
||||
|
||||
# Keep the original Ford path available without changing its command behavior.
|
||||
if lateral_mode == FordLateralMode.native:
|
||||
if (self.frame % CarControllerParams.STEER_STEP) == 0:
|
||||
if not lateral_mode_ready:
|
||||
self.apply_curvature_last = 0.0
|
||||
apply_curvature = 0.0
|
||||
elif self.CP.carFingerprint in (CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_F_150_MK14):
|
||||
self.anti_overshoot_curvature_last = anti_overshoot(
|
||||
actuators.curvature, self.anti_overshoot_curvature_last, CS.out.vEgoRaw)
|
||||
apply_curvature = self.anti_overshoot_curvature_last
|
||||
else:
|
||||
apply_curvature = actuators.curvature
|
||||
|
||||
current_curvature = -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
||||
self.apply_curvature_last = apply_ford_curvature_limits(
|
||||
apply_curvature, self.apply_curvature_last, current_curvature,
|
||||
CS.out.vEgoRaw, 0., CC.latActive and lateral_mode_ready, self.CP)
|
||||
|
||||
if self.CP.flags & FordFlags.CANFD:
|
||||
mode = 1 if CC.latActive and lateral_mode_ready else 0
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(fordcan.create_lat_ctl2_msg(
|
||||
self.packer, self.CAN, mode, 0., 0., -self.apply_curvature_last, 0., counter))
|
||||
else:
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(
|
||||
self.packer, self.CAN, CC.latActive and lateral_mode_ready, 0., 0., -self.apply_curvature_last, 0.))
|
||||
elif (self.frame % CarControllerParams.STEER_STEP) == 0:
|
||||
if not lateral_mode_ready:
|
||||
lateral = FordLateralResult(shadow_curvature=self.ford_lateral._current_curvature(CS))
|
||||
elif lateral_mode == FordLateralMode.angle:
|
||||
lateral = self.ford_lateral.update_angle(CC, CS, actuators)
|
||||
else:
|
||||
apply_curvature = actuators.curvature
|
||||
|
||||
# apply rate limits, curvature error limit, and clip to signal range
|
||||
current_curvature = -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
||||
|
||||
self.apply_curvature_last = apply_ford_curvature_limits(apply_curvature, self.apply_curvature_last, current_curvature,
|
||||
CS.out.vEgoRaw, 0., CC.latActive, self.CP)
|
||||
lateral = self.ford_lateral.update_curvature(CC, CS, actuators)
|
||||
|
||||
self.apply_curvature_last = lateral.curvature
|
||||
self.ford_shadow_curvature = lateral.shadow_curvature
|
||||
if self.CP.flags & FordFlags.CANFD:
|
||||
# TODO: extended mode
|
||||
# Ford uses four individual signals to dictate how to drive to the car. Curvature alone (limited to 0.02m/s^2)
|
||||
# can actuate the steering for a large portion of any lateral movements. However, in order to get further control on
|
||||
# steer actuation, the other three signals are necessary. Ford controls vehicles differently than most other makes.
|
||||
# A detailed explanation on ford control can be found here:
|
||||
# https://www.f150gen14.com/forum/threads/introducing-bluepilot-a-ford-specific-fork-for-comma3x-openpilot.24241/#post-457706
|
||||
mode = 1 if CC.latActive else 0
|
||||
counter = (self.frame // CarControllerParams.STEER_STEP) % 0x10
|
||||
can_sends.append(fordcan.create_lat_ctl2_msg(self.packer, self.CAN, mode, 0., 0., -self.apply_curvature_last, 0., counter))
|
||||
can_sends.append(starpilot_fordcan.create_lat_ctl2_msg(
|
||||
self.packer, self.CAN, 1 if lateral.active else 0,
|
||||
lateral.ramp_type, lateral.precision_type,
|
||||
-lateral.path_offset, -lateral.path_angle,
|
||||
-lateral.curvature, -lateral.curvature_rate, counter))
|
||||
else:
|
||||
can_sends.append(fordcan.create_lat_ctl_msg(self.packer, self.CAN, CC.latActive, 0., 0., -self.apply_curvature_last, 0.))
|
||||
can_sends.append(starpilot_fordcan.create_lat_ctl_msg(
|
||||
self.packer, self.CAN, lateral.active,
|
||||
lateral.ramp_type, lateral.precision_type,
|
||||
-lateral.path_offset, -lateral.path_angle,
|
||||
-lateral.curvature, -lateral.curvature_rate))
|
||||
|
||||
# send lka msg at 33Hz
|
||||
if (self.frame % CarControllerParams.LKA_STEP) == 0:
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
if lateral_mode == FordLateralMode.native:
|
||||
can_sends.append(fordcan.create_lka_msg(self.packer, self.CAN))
|
||||
else:
|
||||
angle_mode = lateral_mode == FordLateralMode.angle
|
||||
shadow_curvature = -self.ford_lateral._current_curvature(CS)
|
||||
if angle_mode:
|
||||
shadow_curvature = -self.ford_shadow_curvature
|
||||
can_sends.append(starpilot_fordcan.create_lka_msg(
|
||||
self.packer, self.CAN, angle_mode=angle_mode, shadow_curvature=shadow_curvature))
|
||||
self.ford_lateral_announced_mode = lateral_mode
|
||||
|
||||
### longitudinal control ###
|
||||
# send acc msg at 50Hz
|
||||
|
||||
@@ -16,12 +16,18 @@ class CarState(CarStateBase):
|
||||
super().__init__(CP, FPCP)
|
||||
can_define = CANDefine(DBC[CP.carFingerprint][Bus.pt])
|
||||
if CP.transmissionType == TransmissionType.automatic:
|
||||
self.shifter_values = can_define.dv["PowertrainData_10"]["TrnRng_D_Rq"]
|
||||
if CP.flags & FordFlags.CANFD:
|
||||
self.shifter_values = can_define.dv["Gear_Shift_by_Wire_FD1"]["TrnRng_D_RqGsm"]
|
||||
elif CP.flags & FordFlags.ALT_STEER_ANGLE:
|
||||
self.shifter_values = can_define.dv["TransGearData"]["GearLvrPos_D_Actl"]
|
||||
else:
|
||||
self.shifter_values = can_define.dv["PowertrainData_10"]["TrnRng_D_Rq"]
|
||||
|
||||
self.distance_button = 0
|
||||
self.lc_button = 0
|
||||
self.lkas_available = False
|
||||
self.lateral_motion_control = None
|
||||
self.steering_angle_offset_deg = 0.0
|
||||
|
||||
def update(self, can_parsers, starpilot_toggles) -> structs.CarState:
|
||||
cp = can_parsers[Bus.pt]
|
||||
@@ -29,13 +35,24 @@ class CarState(CarStateBase):
|
||||
|
||||
ret = structs.CarState()
|
||||
|
||||
# Occasionally on startup, the ABS module recalibrates the steering pinion offset, so we need to block engagement
|
||||
# The vehicle usually recovers out of this state within a minute of normal driving
|
||||
ret.vehicleSensorsInvalid = cp.vl["SteeringPinion_Data"]["StePinCompAnEst_D_Qf"] != 3
|
||||
if self.CP.flags & FordFlags.ALT_STEER_ANGLE:
|
||||
sensors_valid = (
|
||||
int((cp.vl["ParkAid_Data"]["ExtSteeringAngleReq2"] + 1000) * 10) not in (32766, 32767)
|
||||
and cp.vl["ParkAid_Data"]["EPASExtAngleStatReq"] == 0
|
||||
and cp.vl["ParkAid_Data"]["ApaSys_D_Stat"] in (0, 1)
|
||||
)
|
||||
ret.vehicleSensorsInvalid = not sensors_valid
|
||||
else:
|
||||
# The ABS can recalibrate the steering pinion offset briefly after startup.
|
||||
ret.vehicleSensorsInvalid = cp.vl["SteeringPinion_Data"]["StePinCompAnEst_D_Qf"] != 3
|
||||
|
||||
# car speed
|
||||
ret.vEgoRaw = cp.vl["BrakeSysFeatures"]["Veh_V_ActlBrk"] * CV.KPH_TO_MS
|
||||
ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw)
|
||||
if self.CP.flags & FordFlags.CANFD:
|
||||
ret.vEgoCluster = ((cp.vl["Cluster_Info_3_FD1"]["DISPLAY_SPEED_SCALING"] / 100) *
|
||||
cp.vl["EngVehicleSpThrottle2"]["Veh_V_ActlEng"] +
|
||||
cp.vl["Cluster_Info_3_FD1"]["DISPLAY_SPEED_OFFSET"]) * CV.KPH_TO_MS
|
||||
ret.yawRate = cp.vl["Yaw_Data_FD1"]["VehYaw_W_Actl"]
|
||||
ret.standstill = cp.vl["DesiredTorqBrk"]["VehStop_D_Stat"] == 1
|
||||
|
||||
@@ -48,7 +65,14 @@ class CarState(CarStateBase):
|
||||
ret.parkingBrake = cp.vl["DesiredTorqBrk"]["PrkBrkStatus"] in (1, 2)
|
||||
|
||||
# steering wheel
|
||||
ret.steeringAngleDeg = cp.vl["SteeringPinion_Data"]["StePinComp_An_Est"]
|
||||
if self.CP.flags & FordFlags.ALT_STEER_ANGLE:
|
||||
steering_angle_init = cp.vl["SteeringPinion_Data_Alt"]["StePinRelInit_An_Sns"]
|
||||
if not ret.vehicleSensorsInvalid:
|
||||
steering_angle_est = cp.vl["ParkAid_Data"]["ExtSteeringAngleReq2"]
|
||||
self.steering_angle_offset_deg = steering_angle_est - steering_angle_init
|
||||
ret.steeringAngleDeg = steering_angle_init + self.steering_angle_offset_deg
|
||||
else:
|
||||
ret.steeringAngleDeg = cp.vl["SteeringPinion_Data"]["StePinComp_An_Est"]
|
||||
ret.steeringTorque = cp.vl["EPAS_INFO"]["SteeringColumnTorque"]
|
||||
ret.steeringPressed = self.update_steering_pressed(abs(ret.steeringTorque) > CarControllerParams.STEER_DRIVER_ALLOWANCE, 5)
|
||||
ret.steerFaultTemporary = cp.vl["EPAS_INFO"]["EPAS_Failure"] == 1
|
||||
@@ -60,7 +84,8 @@ class CarState(CarStateBase):
|
||||
ret.steerFaultTemporary |= cp.vl["Lane_Assist_Data3_FD1"]["LatCtlSte_D_Stat"] not in (1, 2, 3)
|
||||
|
||||
# cruise state
|
||||
is_metric = cp.vl["INSTRUMENT_PANEL"]["METRIC_UNITS"] == 1 if not self.CP.flags & FordFlags.CANFD else False
|
||||
is_metric = cp.vl["INSTRUMENT_PANEL"]["METRIC_UNITS"] == 1 if not self.CP.flags & FordFlags.CANFD else \
|
||||
cp_cam.vl["IPMA_Data2"]["IsaVLimUnit_D_Rq"] == 1
|
||||
ret.cruiseState.speed = cp.vl["EngBrakeData"]["Veh_V_DsplyCcSet"] * (CV.KPH_TO_MS if is_metric else CV.MPH_TO_MS)
|
||||
ret.cruiseState.enabled = cp.vl["EngBrakeData"]["CcStat_D_Actl"] in (4, 5)
|
||||
ret.cruiseState.available = cp.vl["EngBrakeData"]["CcStat_D_Actl"] in (3, 4, 5)
|
||||
@@ -72,7 +97,12 @@ class CarState(CarStateBase):
|
||||
|
||||
# gear
|
||||
if self.CP.transmissionType == TransmissionType.automatic:
|
||||
gear = self.shifter_values.get(cp.vl["PowertrainData_10"]["TrnRng_D_Rq"])
|
||||
if self.CP.flags & FordFlags.CANFD:
|
||||
gear = self.shifter_values.get(cp.vl["Gear_Shift_by_Wire_FD1"]["TrnRng_D_RqGsm"])
|
||||
elif self.CP.flags & FordFlags.ALT_STEER_ANGLE:
|
||||
gear = self.shifter_values.get(cp.vl["TransGearData"]["GearLvrPos_D_Actl"])
|
||||
else:
|
||||
gear = self.shifter_values.get(cp.vl["PowertrainData_10"]["TrnRng_D_Rq"])
|
||||
ret.gearShifter = self.parse_gear_shifter(gear)
|
||||
elif self.CP.transmissionType == TransmissionType.manual:
|
||||
if bool(cp.vl["BCM_Lamp_Stat_FD1"]["RvrseLghtOn_B_Stat"]):
|
||||
@@ -126,6 +156,22 @@ class CarState(CarStateBase):
|
||||
]
|
||||
|
||||
fp_ret = custom.StarPilotCarState.new_message()
|
||||
fp_ret.brakeLights = ret.brakePressed
|
||||
try:
|
||||
fp_ret.brakeLights = bool(cp.vl["BCM_Lamp_Stat_FD1"]["StopLghtOn_B_Stat"])
|
||||
except (KeyError, AttributeError):
|
||||
try:
|
||||
fp_ret.brakeLights = cp.vl["BrakeSysFeatures_2"]["BrkLamp_B_Rq"] == 1
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
try:
|
||||
speed_limit = cp_cam.vl["Traffic_RecognitnData"]["TsrVLim1MsgTxt_D_Rq"]
|
||||
speed_limit_unit = cp_cam.vl["Traffic_RecognitnData"]["TsrVlUnitMsgTxt_D_Rq"]
|
||||
speed_factor = CV.MPH_TO_MS if speed_limit_unit == 2 else CV.KPH_TO_MS if speed_limit_unit == 1 else 0.0
|
||||
fp_ret.dashboardSpeedLimit = speed_limit * speed_factor if speed_limit not in (0, 255) else 0.0
|
||||
except (KeyError, AttributeError):
|
||||
fp_ret.dashboardSpeedLimit = 0.0
|
||||
|
||||
return ret, fp_ret
|
||||
|
||||
|
||||
@@ -12,6 +12,8 @@ FW_VERSIONS = {
|
||||
b'LX6C-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.abs, 0x760, None): [
|
||||
b'LX6C-2D053-KF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'LX6C-2D053-KG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'LX6C-2D053-RD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'LX6C-2D053-RE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'LX6C-2D053-RF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
@@ -24,6 +26,21 @@ FW_VERSIONS = {
|
||||
b'M1PT-14F397-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
},
|
||||
CAR.FORD_EDGE_MK2: {
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'M2GC-14D003-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.abs, 0x760, None): [
|
||||
b'M2GC-2D053-CB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'M2GC-2D053-EA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x764, None): [
|
||||
b'JX7T-14D049-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdCamera, 0x706, None): [
|
||||
b'KT4T-14F397-AF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
},
|
||||
CAR.FORD_ESCAPE_MK4: {
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'LX6C-14D003-AF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
@@ -49,7 +66,9 @@ FW_VERSIONS = {
|
||||
},
|
||||
CAR.FORD_ESCAPE_MK4_5: {
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'PZ11-14D003-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PZ11-14D003-EA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PZ11-14D003-FA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.abs, 0x760, None): [
|
||||
b'PZ1C-2D053-EJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
@@ -58,11 +77,20 @@ FW_VERSIONS = {
|
||||
b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdCamera, 0x706, None): [
|
||||
b'PJ6T-14H102-ABE\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PJ6T-14H102-ABL\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PJ6T-14H102-MDC\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PJ6T-14H102-MDF\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PJ6T-14H102-SCD\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PJ6T-14H102-SCG\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
},
|
||||
CAR.FORD_EXPLORER_MK6: {
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'R1MC-14D003-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'R1MC-14D003-AF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'R1MC-14D003-AG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'R1MC-14D003-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'L1MC-14D003-AJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'L1MC-14D003-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'L1MC-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
@@ -96,6 +124,7 @@ FW_VERSIONS = {
|
||||
],
|
||||
(Ecu.abs, 0x760, None): [
|
||||
b'RL14-2D053-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PL14-2D053-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x764, None): [
|
||||
b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
@@ -106,6 +135,7 @@ FW_VERSIONS = {
|
||||
},
|
||||
CAR.FORD_F_150_MK14: {
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'MB3C-14D003-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'ML3V-14D003-BA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'ML3V-14D003-BC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'ML3V-14D003-BD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
@@ -136,8 +166,11 @@ FW_VERSIONS = {
|
||||
},
|
||||
CAR.FORD_F_150_LIGHTNING_MK1: {
|
||||
(Ecu.abs, 0x760, None): [
|
||||
b'NL38-2D053-AF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PL38-2D053-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'RL38-2D053-BC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'RL38-2D053-BD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'TL38-2D053-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdCamera, 0x706, None): [
|
||||
b'ML3T-14H102-ABT\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
@@ -145,9 +178,13 @@ FW_VERSIONS = {
|
||||
b'RJ6T-14H102-BBC\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x764, None): [
|
||||
b'ML3T-14D049-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'RB5T-14D049-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'NL38-14D003-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'NL38-14D003-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'RL38-14D003-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
},
|
||||
@@ -164,6 +201,8 @@ FW_VERSIONS = {
|
||||
b'LK9C-2D053-CN\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x764, None): [
|
||||
b'ML3T-14D049-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'ML3T-14D049-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdCamera, 0x706, None): [
|
||||
@@ -174,15 +213,42 @@ FW_VERSIONS = {
|
||||
CAR.FORD_FOCUS_MK4: {
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'JX6C-14D003-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'JX6C-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'JX6C-14D003-BB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'NX6C-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.abs, 0x760, None): [
|
||||
b'JX61-2D053-CJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'LX61-2D053-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'NX61-2D053-MD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x764, None): [
|
||||
b'JX7T-14D049-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'JX7T-14D049-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdCamera, 0x706, None): [
|
||||
b'JX7T-14F397-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'N1BT-14F397-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
},
|
||||
CAR.FORD_MONDEO_MK5: {
|
||||
(Ecu.fwdCamera, 0x706, None): [
|
||||
b'KT4T-14F397-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.abs, 0x760, None): [
|
||||
b'KG9C-2D053-DF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'K2GC-14D003-AJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x764, None): [
|
||||
b'JX7T-14D049-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.engine, 0x7e0, None): [
|
||||
b'HS7A-14C204-CJD\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.debug, 0x7d0, None): [
|
||||
b'1U5T-14G374-DA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
},
|
||||
CAR.FORD_MAVERICK_MK1: {
|
||||
@@ -207,17 +273,25 @@ FW_VERSIONS = {
|
||||
},
|
||||
CAR.FORD_RANGER_MK2: {
|
||||
(Ecu.eps, 0x730, None): [
|
||||
b'JR3C-14D003-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'NB3C-14D003-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'NL14-14D003-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'NL14-14D003-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'RB3C-14D003-AA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.abs, 0x760, None): [
|
||||
b'MB3C-2D053-AE\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'MB3C-2D053-ZJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PB3C-2D053-ZB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PB3C-2D053-ZC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PB3C-2D053-ZD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PB3C-2D053-ZG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PB3C-2D053-ZJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'PB9C-2D053-ZG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'RB3C-2D053-AK\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdRadar, 0x764, None): [
|
||||
b'JX7T-14D049-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
b'ML3T-14D049-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00',
|
||||
],
|
||||
(Ecu.fwdCamera, 0x706, None): [
|
||||
|
||||
@@ -31,12 +31,13 @@ class CarInterface(CarInterfaceBase):
|
||||
|
||||
ret.radarUnavailable = Bus.radar not in DBC[candidate]
|
||||
ret.steerControlType = structs.CarParams.SteerControlType.angle
|
||||
ret.steerActuatorDelay = 0.05 if ret.flags & FordFlags.LKA_STEERING else 0.2
|
||||
ret.steerActuatorDelay = 0.05 if ret.flags & FordFlags.LKA_STEERING else 0.22
|
||||
ret.steerLimitTimer = 1.0
|
||||
ret.steerAtStandstill = True
|
||||
|
||||
ret.longitudinalTuning.kiBP = [0.]
|
||||
ret.longitudinalTuning.kiV = [0.5]
|
||||
ret.longitudinalTuning.kpV = [0.]
|
||||
|
||||
if not ret.radarUnavailable and DBC[candidate][Bus.radar] == RADAR.DELPHI_MRR:
|
||||
# average of 33.3 Hz radar timestep / 4 scan modes = 60 ms
|
||||
@@ -44,6 +45,10 @@ class CarInterface(CarInterfaceBase):
|
||||
ret.radarDelay = 0.06
|
||||
|
||||
CAN = CanBus(fingerprint=fingerprint)
|
||||
if 0x365 in fingerprint[CAN.main]:
|
||||
ret.flags |= int(FordFlags.HEV_CLUSTER_DATA)
|
||||
if all(address in fingerprint[CAN.main] for address in (0x07A, 0x24B, 0x24C)):
|
||||
ret.flags |= int(FordFlags.HEV_BATTERY_DATA)
|
||||
cfgs = [get_safety_config(structs.CarParams.SafetyModel.ford)]
|
||||
if CAN.main >= 4:
|
||||
cfgs.insert(0, get_safety_config(structs.CarParams.SafetyModel.noOutput))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import numpy as np
|
||||
from collections import deque
|
||||
from typing import cast
|
||||
from collections import defaultdict
|
||||
from math import cos, sin
|
||||
@@ -19,6 +20,7 @@ DELPHI_MRR_RADAR_MSG_COUNT = 64
|
||||
DELPHI_MRR_RADAR_RANGE_COVERAGE = {0: 42, 1: 164, 2: 45, 3: 175} # scan index to detection range (m)
|
||||
DELPHI_MRR_MIN_LONG_RANGE_DIST = 30 # meters
|
||||
DELPHI_MRR_CLUSTER_THRESHOLD = 5 # meters, lateral distance and relative velocity are weighted
|
||||
STEER_ASSIST_DATA_ADDR = 0x3D7
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -89,12 +91,17 @@ def _create_delphi_mrr_radar_can_parser(CP) -> CANParser:
|
||||
return CANParser(RADAR.DELPHI_MRR, messages, CanBus(CP).radar)
|
||||
|
||||
|
||||
def _create_steer_assist_radar_can_parser(CP) -> CANParser:
|
||||
return CANParser(RADAR.STEER_ASSIST_DATA, [("Steer_Assist_Data", 20)], CanBus(CP).camera)
|
||||
|
||||
|
||||
class RadarInterface(RadarInterfaceBase):
|
||||
def __init__(self, CP):
|
||||
super().__init__(CP)
|
||||
|
||||
self.points: list[list[float]] = []
|
||||
self.clusters: list[Cluster] = []
|
||||
self.v_rel_history = deque(maxlen=20)
|
||||
|
||||
self.updated_messages = set()
|
||||
self.track_id = 0
|
||||
@@ -111,6 +118,9 @@ class RadarInterface(RadarInterfaceBase):
|
||||
elif self.radar == RADAR.DELPHI_MRR:
|
||||
self.rcp = _create_delphi_mrr_radar_can_parser(CP)
|
||||
self.trigger_msg = DELPHI_MRR_RADAR_HEADER_ADDR
|
||||
elif self.radar == RADAR.STEER_ASSIST_DATA:
|
||||
self.rcp = _create_steer_assist_radar_can_parser(CP)
|
||||
self.trigger_msg = STEER_ASSIST_DATA_ADDR
|
||||
else:
|
||||
raise ValueError(f"Unsupported radar: {self.radar}")
|
||||
|
||||
@@ -135,10 +145,45 @@ class RadarInterface(RadarInterfaceBase):
|
||||
_update = self._update_delphi_mrr(ret)
|
||||
if not _update:
|
||||
return None
|
||||
elif self.radar == RADAR.STEER_ASSIST_DATA:
|
||||
self._update_steer_assist()
|
||||
|
||||
ret.points = list(self.pts.values())
|
||||
return ret
|
||||
|
||||
def _update_steer_assist(self):
|
||||
msg = self.rcp.vl["Steer_Assist_Data"]
|
||||
confidence = msg["CmbbObjConfdnc_D_Stat"]
|
||||
if confidence <= 0:
|
||||
self.pts.pop(0, None)
|
||||
self.v_rel_history.clear()
|
||||
return
|
||||
|
||||
d_rel = msg["CmbbObjDistLong_L_Actl"]
|
||||
v_rel = msg["CmbbObjRelLong_V_Actl"]
|
||||
new_track = 0 not in self.pts
|
||||
if new_track:
|
||||
self.pts[0] = structs.RadarData.RadarPoint()
|
||||
self.pts[0].trackId = self.track_id
|
||||
self.track_id += 1
|
||||
elif abs(v_rel) < 1e-2:
|
||||
self.v_rel_history.append(d_rel - self.pts[0].dRel)
|
||||
v_rel = sum(self.v_rel_history)
|
||||
else:
|
||||
self.v_rel_history.clear()
|
||||
|
||||
if not new_track and (abs(self.pts[0].vRel - v_rel) > 2.0 or abs(self.pts[0].dRel - d_rel) > 5.0):
|
||||
self.pts[0].trackId = self.track_id
|
||||
self.track_id += 1
|
||||
self.v_rel_history.clear()
|
||||
|
||||
self.pts[0].dRel = d_rel
|
||||
self.pts[0].yRel = msg["CmbbObjDistLat_L_Actl"]
|
||||
self.pts[0].vRel = v_rel
|
||||
self.pts[0].aRel = float('nan')
|
||||
self.pts[0].yvRel = msg["CmbbObjRelLat_V_Actl"]
|
||||
self.pts[0].measured = True
|
||||
|
||||
def _update_delphi_esr(self):
|
||||
for ii in sorted(self.updated_messages):
|
||||
cpt = self.rcp.vl[ii]
|
||||
|
||||
@@ -6,7 +6,7 @@ from parameterized import parameterized
|
||||
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.fw_versions import build_fw_dict
|
||||
from opendbc.car.ford.values import CAR, FW_QUERY_CONFIG, FW_PATTERN, get_platform_codes
|
||||
from opendbc.car.ford.values import CAR, FW_QUERY_CONFIG, FW_PATTERN, get_platform_codes, match_vin_to_car
|
||||
from opendbc.car.ford.fingerprints import FW_VERSIONS
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
@@ -20,6 +20,7 @@ ECU_ADDRESSES = {
|
||||
Ecu.engine: 0x7E0, # Powertrain Control Module (PCM)
|
||||
Ecu.shiftByWire: 0x732, # Gear Shift Module (GSM)
|
||||
Ecu.debug: 0x7D0, # Accessory Protocol Interface Module (APIM)
|
||||
Ecu.hud: 0x720, # Instrument Cluster Module (ICM)
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +42,16 @@ ECU_PART_NUMBER = {
|
||||
|
||||
|
||||
class TestFordFW:
|
||||
def test_vin_fallback(self):
|
||||
def vin(wmi, vds, powertrain, year):
|
||||
return f"{wmi}{vds}{powertrain}0{year}1234567"
|
||||
|
||||
assert match_vin_to_car(vin("2FM", "PK4A", "A", "N")) == {str(CAR.FORD_EDGE_MK2)}
|
||||
assert match_vin_to_car(vin("3FM", "K1RA", "A", "M")) == {str(CAR.FORD_MUSTANG_MACH_E_MK1)}
|
||||
assert match_vin_to_car(vin("1FT", "F1CA", "A", "M")) == {str(CAR.FORD_F_150_MK14)}
|
||||
assert match_vin_to_car(vin("1FT", "F1CA", "L", "N")) == {str(CAR.FORD_F_150_LIGHTNING_MK1)}
|
||||
assert match_vin_to_car("0" * 17) == set()
|
||||
|
||||
def test_fw_query_config(self):
|
||||
for (ecu, addr, subaddr) in FW_QUERY_CONFIG.extra_ecus:
|
||||
assert ecu in ECU_ADDRESSES, "Unknown ECU"
|
||||
@@ -50,10 +61,13 @@ class TestFordFW:
|
||||
@parameterized.expand(FW_VERSIONS.items())
|
||||
def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[int, int, int | None], Iterable[bytes]]):
|
||||
for (ecu, addr, subaddr), fws in fw_versions.items():
|
||||
assert ecu in ECU_PART_NUMBER, "Unexpected ECU"
|
||||
assert ecu in ECU_ADDRESSES, "Unknown ECU"
|
||||
assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"
|
||||
assert subaddr is None, "Unexpected ECU subaddress"
|
||||
|
||||
if ecu not in ECU_PART_NUMBER:
|
||||
continue
|
||||
|
||||
for fw in fws:
|
||||
assert len(fw) == 24, "Expected ECU response to be 24 bytes"
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from opendbc.car.lateral import AngleSteeringLimits
|
||||
from opendbc.car.structs import CarParams
|
||||
from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column
|
||||
from opendbc.car.fw_query_definitions import FwQueryConfig, LiveFwVersions, OfflineFwVersions, Request, StdQueries, p16
|
||||
from opendbc.car.vin import Vin, is_valid_vin
|
||||
|
||||
Ecu = CarParams.Ecu
|
||||
|
||||
@@ -53,11 +54,15 @@ class FordFlags(IntFlag):
|
||||
# Static flags
|
||||
CANFD = 1
|
||||
LKA_STEERING = 2
|
||||
ALT_STEER_ANGLE = 4
|
||||
HEV_CLUSTER_DATA = 8
|
||||
HEV_BATTERY_DATA = 16
|
||||
|
||||
|
||||
class RADAR:
|
||||
DELPHI_ESR = 'ford_fusion_2018_adas'
|
||||
DELPHI_MRR = 'FORD_CADS'
|
||||
STEER_ASSIST_DATA = 'ford_lincoln_base_pt'
|
||||
|
||||
|
||||
class Footnote(Enum):
|
||||
@@ -87,6 +92,10 @@ class FordCarDocs(CarDocs):
|
||||
|
||||
@dataclass
|
||||
class FordPlatformConfig(PlatformConfig):
|
||||
wmis: set[str] = field(default_factory=set)
|
||||
vds_codes: set[str] = field(default_factory=set)
|
||||
years: set[str] = field(default_factory=set)
|
||||
|
||||
dbc_dict: DbcDict = field(default_factory=lambda: {
|
||||
Bus.pt: 'ford_lincoln_base_pt',
|
||||
Bus.radar: RADAR.DELPHI_MRR,
|
||||
@@ -106,6 +115,7 @@ class FordPlatformConfig(PlatformConfig):
|
||||
class FordCANFDPlatformConfig(FordPlatformConfig):
|
||||
dbc_dict: DbcDict = field(default_factory=lambda: {
|
||||
Bus.pt: 'ford_lincoln_base_pt',
|
||||
Bus.radar: RADAR.STEER_ASSIST_DATA,
|
||||
})
|
||||
|
||||
def init(self):
|
||||
@@ -129,10 +139,23 @@ class FordF150LightningPlatform(FordCANFDPlatformConfig):
|
||||
self.car_docs = []
|
||||
|
||||
|
||||
MY_2020, MY_2021, MY_2022, MY_2023, MY_2024, MY_2025 = 'L', 'M', 'N', 'P', 'R', 'S'
|
||||
F150_VDS_CODES = {'F1C', 'F1E', 'W1C', 'W1E', 'X1C', 'X1E', 'W1R', 'W1P', 'W1S', 'W1T'}
|
||||
F150_ELECTRIC_CODES = {'L', 'V'}
|
||||
MACH_E_VDS_CODES = {'K1R', 'K1S', 'K2S', 'K3R', 'K3S', 'K4S'}
|
||||
|
||||
|
||||
class CAR(Platforms):
|
||||
FORD_BRONCO_SPORT_MK1 = FordPlatformConfig(
|
||||
[FordCarDocs("Ford Bronco Sport 2021-24")],
|
||||
CarSpecs(mass=1625, wheelbase=2.67, steerRatio=17.7),
|
||||
wmis={'3FM'}, vds_codes={'CR9'}, years={MY_2021, MY_2022, MY_2023, MY_2024},
|
||||
)
|
||||
FORD_EDGE_MK2 = FordPlatformConfig(
|
||||
[FordCarDocs("Ford Edge 2022")],
|
||||
CarSpecs(mass=1933, wheelbase=2.824, steerRatio=15.3),
|
||||
flags=FordFlags.ALT_STEER_ANGLE,
|
||||
wmis={'2FM'}, vds_codes={'PK4'}, years={MY_2022},
|
||||
)
|
||||
FORD_ESCAPE_MK4 = FordPlatformConfig(
|
||||
[
|
||||
@@ -140,6 +163,7 @@ class CAR(Platforms):
|
||||
FordCarDocs("Ford Kuga 2020-23", "Adaptive Cruise Control with Lane Centering", hybrid=True, plug_in_hybrid=True),
|
||||
],
|
||||
CarSpecs(mass=1750, wheelbase=2.71, steerRatio=16.7),
|
||||
wmis={'1FM'}, vds_codes={'CU0', 'CU9'}, years={MY_2020, MY_2021, MY_2022},
|
||||
)
|
||||
FORD_ESCAPE_MK4_5 = FordCANFDPlatformConfig(
|
||||
[
|
||||
@@ -148,6 +172,7 @@ class CAR(Platforms):
|
||||
FordCarDocs("Ford Kuga Plug-in Hybrid 2024", "All"),
|
||||
],
|
||||
CarSpecs(mass=1750, wheelbase=2.71, steerRatio=16.7),
|
||||
wmis={'1FM'}, vds_codes={'CU0', 'CU9'}, years={MY_2023, MY_2024},
|
||||
)
|
||||
FORD_EXPLORER_MK6 = FordPlatformConfig(
|
||||
[
|
||||
@@ -155,37 +180,49 @@ class CAR(Platforms):
|
||||
FordCarDocs("Lincoln Aviator 2020-24", "Co-Pilot360 Plus", plug_in_hybrid=True), # Hybrid: Grand Touring only
|
||||
],
|
||||
CarSpecs(mass=2050, wheelbase=3.025, steerRatio=16.8),
|
||||
wmis={'1FM', '5LM'}, vds_codes={'5K7', '5K8', '5J7'},
|
||||
years={MY_2020, MY_2021, MY_2022, MY_2023, MY_2024},
|
||||
)
|
||||
FORD_EXPEDITION_MK4 = FordCANFDPlatformConfig(
|
||||
[FordCarDocs("Ford Expedition 2022-24", "Co-Pilot360 Assist 2.0", hybrid=False)],
|
||||
CarSpecs(mass=2000, wheelbase=3.69, steerRatio=17.0),
|
||||
wmis={'1FM'}, vds_codes={'JU1', 'JU2', 'JK1'}, years={MY_2022, MY_2023, MY_2024},
|
||||
)
|
||||
FORD_F_150_MK14 = FordCANFDPlatformConfig(
|
||||
[FordCarDocs("Ford F-150 2021-23", "Co-Pilot360 Assist 2.0", hybrid=True)],
|
||||
CarSpecs(mass=2000, wheelbase=3.69, steerRatio=17.0),
|
||||
CarSpecs(mass=3334, wheelbase=3.99, steerRatio=17.0),
|
||||
wmis={'1FT'}, vds_codes=F150_VDS_CODES, years={MY_2021, MY_2022, MY_2023},
|
||||
)
|
||||
FORD_F_150_LIGHTNING_MK1 = FordF150LightningPlatform(
|
||||
[FordCarDocs("Ford F-150 Lightning 2022-23", "Co-Pilot360 Assist 2.0")],
|
||||
[FordCarDocs("Ford F-150 Lightning 2022-25", "Co-Pilot360 Assist 2.0")],
|
||||
CarSpecs(mass=2948, wheelbase=3.70, steerRatio=16.9),
|
||||
wmis={'1FT'}, vds_codes=F150_VDS_CODES, years={MY_2022, MY_2023, MY_2024, MY_2025},
|
||||
)
|
||||
FORD_FOCUS_MK4 = FordPlatformConfig(
|
||||
[FordCarDocs("Ford Focus 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS], hybrid=True)], # mHEV only
|
||||
[FordCarDocs("Ford Focus 2018-22", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS], hybrid=True)], # mHEV only
|
||||
CarSpecs(mass=1350, wheelbase=2.7, steerRatio=15.0),
|
||||
)
|
||||
FORD_MONDEO_MK5 = FordCANFDPlatformConfig(
|
||||
[FordCarDocs("Ford Mondeo 2014-22", "Adaptive Cruise Control with Lane Centering")],
|
||||
CarSpecs(mass=1550, wheelbase=2.85, steerRatio=14.8),
|
||||
)
|
||||
FORD_MAVERICK_MK1 = FordPlatformConfig(
|
||||
[
|
||||
FordCarDocs("Ford Maverick 2022", "LARIAT Luxury", hybrid=True),
|
||||
FordCarDocs("Ford Maverick 2023-24", "Co-Pilot360 Assist", hybrid=True),
|
||||
],
|
||||
CarSpecs(mass=1650, wheelbase=3.076, steerRatio=17.0),
|
||||
wmis={'3FT'}, vds_codes={'TW8'}, years={MY_2022, MY_2023, MY_2024},
|
||||
)
|
||||
FORD_MUSTANG_MACH_E_MK1 = FordCANFDPlatformConfig(
|
||||
[FordCarDocs("Ford Mustang Mach-E 2021-24", "All", setup_video="https://www.youtube.com/watch?v=AR4_eTF3b_A")],
|
||||
CarSpecs(mass=2200, wheelbase=2.984, steerRatio=17.0), # TODO: check steer ratio
|
||||
wmis={'3FM'}, vds_codes=MACH_E_VDS_CODES, years={MY_2021, MY_2022, MY_2023, MY_2024},
|
||||
)
|
||||
FORD_RANGER_MK2 = FordCANFDPlatformConfig(
|
||||
[FordCarDocs("Ford Ranger 2024", "Adaptive Cruise Control with Lane Centering", setup_video="https://www.youtube.com/watch?v=2oJlXCKYOy0")],
|
||||
CarSpecs(mass=2000, wheelbase=3.27, steerRatio=17.0),
|
||||
wmis={'1FT'}, vds_codes={'ER4'}, years={MY_2024},
|
||||
)
|
||||
FORD_TRANSIT_MK5 = FordLKASteeringPlatformConfig(
|
||||
[FordCarDocs("Ford Transit 2025", "Co-Pilot360 Assist+")],
|
||||
@@ -257,7 +294,25 @@ def match_fw_to_car_fuzzy(live_fw_versions: LiveFwVersions, vin: str, offline_fw
|
||||
if valid_expected_ecus.issubset(valid_found_ecus):
|
||||
candidates.add(candidate)
|
||||
|
||||
return candidates
|
||||
return candidates or match_vin_to_car(vin)
|
||||
|
||||
|
||||
def match_vin_to_car(vin: str) -> set[str]:
|
||||
if not is_valid_vin(vin):
|
||||
return set()
|
||||
|
||||
vin_obj = Vin(vin)
|
||||
vds = vin[3:7]
|
||||
model_year = vin[9]
|
||||
candidates = {platform for platform in CAR if vin_obj.wmi in platform.config.wmis and
|
||||
model_year in platform.config.years and
|
||||
any(code in vds for code in platform.config.vds_codes)}
|
||||
|
||||
if {CAR.FORD_F_150_MK14, CAR.FORD_F_150_LIGHTNING_MK1} & candidates:
|
||||
electric = vin[7] in F150_ELECTRIC_CODES
|
||||
candidates.discard(CAR.FORD_F_150_MK14 if electric else CAR.FORD_F_150_LIGHTNING_MK1)
|
||||
|
||||
return {str(candidate) for candidate in candidates}
|
||||
|
||||
|
||||
# All of these ECUs must be present and are expected to have platform codes we can match
|
||||
@@ -266,10 +321,10 @@ PLATFORM_CODE_ECUS = (Ecu.abs, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.eps)
|
||||
DATA_IDENTIFIER_FORD_ASBUILT = 0xDE00
|
||||
|
||||
ASBUILT_BLOCKS: list[tuple[int, list]] = [
|
||||
(1, [Ecu.debug, Ecu.fwdCamera, Ecu.eps]),
|
||||
(2, [Ecu.abs, Ecu.debug, Ecu.eps]),
|
||||
(3, [Ecu.abs, Ecu.debug, Ecu.eps]),
|
||||
(4, [Ecu.debug, Ecu.fwdCamera]),
|
||||
(1, [Ecu.debug, Ecu.fwdCamera, Ecu.eps, Ecu.hud]),
|
||||
(2, [Ecu.abs, Ecu.debug, Ecu.eps, Ecu.hud]),
|
||||
(3, [Ecu.abs, Ecu.debug, Ecu.eps, Ecu.hud]),
|
||||
(4, [Ecu.debug, Ecu.fwdCamera, Ecu.hud]),
|
||||
(5, [Ecu.debug]),
|
||||
(6, [Ecu.debug]),
|
||||
(7, [Ecu.debug]),
|
||||
@@ -297,13 +352,13 @@ FW_QUERY_CONFIG = FwQueryConfig(
|
||||
Request(
|
||||
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
|
||||
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
|
||||
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
|
||||
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire, Ecu.hud],
|
||||
logging=True,
|
||||
),
|
||||
Request(
|
||||
[StdQueries.TESTER_PRESENT_REQUEST, StdQueries.MANUFACTURER_SOFTWARE_VERSION_REQUEST],
|
||||
[StdQueries.TESTER_PRESENT_RESPONSE, StdQueries.MANUFACTURER_SOFTWARE_VERSION_RESPONSE],
|
||||
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire],
|
||||
whitelist_ecus=[Ecu.abs, Ecu.debug, Ecu.engine, Ecu.eps, Ecu.fwdCamera, Ecu.fwdRadar, Ecu.shiftByWire, Ecu.hud],
|
||||
bus=0,
|
||||
auxiliary=True,
|
||||
),
|
||||
@@ -320,7 +375,9 @@ FW_QUERY_CONFIG = FwQueryConfig(
|
||||
# Note: We are unlikely to get a response from behind the gateway
|
||||
(Ecu.shiftByWire, 0x732, None), # Gear Shift Module
|
||||
(Ecu.debug, 0x7d0, None), # Accessory Protocol Interface Module
|
||||
(Ecu.hud, 0x720, None), # Instrument Cluster Module
|
||||
],
|
||||
non_essential_ecus={Ecu.eps: [CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_EXPEDITION_MK4]},
|
||||
# Custom fuzzy fingerprinting function using platform and model year hints
|
||||
match_fw_to_car_fuzzy=match_fw_to_car_fuzzy,
|
||||
)
|
||||
|
||||
@@ -31,6 +31,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
"FORD_BRONCO_SPORT_MK1" = [nan, 1.5, nan]
|
||||
"FORD_ESCAPE_MK4" = [nan, 1.5, nan]
|
||||
"FORD_ESCAPE_MK4_5" = [nan, 1.5, nan]
|
||||
"FORD_EDGE_MK2" = [nan, 1.5, nan]
|
||||
"FORD_EXPLORER_MK6" = [nan, 1.5, nan]
|
||||
"FORD_EXPEDITION_MK4" = [nan, 1.5, nan]
|
||||
"FORD_F_150_MK14" = [nan, 1.5, nan]
|
||||
@@ -39,6 +40,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"]
|
||||
"FORD_F_150_LIGHTNING_MK1" = [nan, 1.5, nan]
|
||||
"FORD_MUSTANG_MACH_E_MK1" = [nan, 1.5, nan]
|
||||
"FORD_RANGER_MK2" = [nan, 1.5, nan]
|
||||
"FORD_MONDEO_MK5" = [nan, 1.5, nan]
|
||||
"FORD_TRANSIT_MK5" = [nan, 1.5, nan]
|
||||
###
|
||||
|
||||
|
||||
@@ -87,6 +87,9 @@ static bool ford_get_quality_flag_valid(const CANPacket_t *msg) {
|
||||
#define FORD_CANFD_INACTIVE_CURVATURE_RATE 1024U
|
||||
|
||||
static bool ford_lka_steering = false;
|
||||
static bool ford_extended_lateral = false;
|
||||
static bool ford_angle_mode = false;
|
||||
static int16_t ford_shadow_curvature = 0;
|
||||
|
||||
// Curvature rate limits
|
||||
#define FORD_LIMITS(limit_lateral_acceleration) { \
|
||||
@@ -112,6 +115,59 @@ static bool ford_lka_steering = false;
|
||||
|
||||
static const AngleSteeringLimits FORD_STEERING_LIMITS = FORD_LIMITS(false);
|
||||
|
||||
#define FORD_EXTENDED_LIMITS(limit_lateral_acceleration) { \
|
||||
.max_angle = 1000, \
|
||||
.angle_deg_to_can = 50000, \
|
||||
.max_angle_error = 100, \
|
||||
.angle_rate_up_lookup = { \
|
||||
{5., 16., 25.}, \
|
||||
{0.0025, 0.0014, 0.00018} \
|
||||
}, \
|
||||
.angle_rate_down_lookup = { \
|
||||
{5., 16., 25.}, \
|
||||
{0.0025, 0.0014, 0.00018} \
|
||||
}, \
|
||||
.angle_error_min_speed = 10.0, \
|
||||
.frequency = 20U, \
|
||||
.angle_is_curvature = (limit_lateral_acceleration), \
|
||||
.enforce_angle_error = true, \
|
||||
.inactive_angle_is_zero = true, \
|
||||
}
|
||||
|
||||
static const AngleSteeringLimits FORD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(false);
|
||||
|
||||
static int ford_desired_path_angle_last = 0;
|
||||
|
||||
static bool ford_path_angle_checks(int desired_path_angle, bool steer_control_enabled) {
|
||||
bool violation = false;
|
||||
if (steer_control_enabled) {
|
||||
float speed = ((float)vehicle_speed.min / VEHICLE_SPEED_FACTOR) - 1.0;
|
||||
const struct lookup_t path_angle_rate = {
|
||||
.x = {10., 15., 25.},
|
||||
.y = {0.0561, 0.04335, 0.00918},
|
||||
};
|
||||
int max_delta = (safety_interpolate(path_angle_rate, speed) * 2000.0) + 1.0;
|
||||
violation |= safety_max_limit_check(desired_path_angle,
|
||||
ford_desired_path_angle_last + max_delta,
|
||||
ford_desired_path_angle_last - max_delta);
|
||||
} else {
|
||||
violation |= desired_path_angle != 0;
|
||||
}
|
||||
ford_desired_path_angle_last = violation ? 0 : desired_path_angle;
|
||||
return violation;
|
||||
}
|
||||
|
||||
static bool ford_shadow_curvature_check(int desired_curvature, bool steer_control_enabled,
|
||||
const AngleSteeringLimits limits) {
|
||||
if (steer_control_enabled && limits.enforce_angle_error &&
|
||||
((vehicle_speed.values[0] / VEHICLE_SPEED_FACTOR) > limits.angle_error_min_speed)) {
|
||||
int lowest_allowed = angle_meas.min - limits.max_angle_error - 1;
|
||||
int highest_allowed = angle_meas.max + limits.max_angle_error + 1;
|
||||
return safety_max_limit_check(desired_curvature, highest_allowed, lowest_allowed);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void ford_rx_hook(const CANPacket_t *msg) {
|
||||
if (msg->bus == FORD_MAIN_BUS) {
|
||||
// Update in motion state from standstill signal
|
||||
@@ -235,6 +291,15 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
if (!valid_lka_action) {
|
||||
tx = false;
|
||||
}
|
||||
|
||||
if (!ford_lka_steering) {
|
||||
ford_angle_mode = (msg->data[4] & 0x1U) != 0U;
|
||||
ford_extended_lateral = (msg->data[4] & 0x2U) != 0U;
|
||||
ford_shadow_curvature = (int16_t)((msg->data[5] << 8) | msg->data[6]);
|
||||
if (ford_angle_mode && !ford_extended_lateral) {
|
||||
tx = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Safety check for LateralMotionControl action
|
||||
@@ -246,12 +311,38 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
unsigned int raw_path_angle = (msg->data[3] << 3) | (msg->data[4] >> 5);
|
||||
unsigned int raw_path_offset = (msg->data[5] << 2) | (msg->data[6] >> 6);
|
||||
|
||||
// These signals are not yet tested with the current safety limits
|
||||
bool violation = (raw_curvature_rate != FORD_INACTIVE_CURVATURE_RATE) || (raw_path_angle != FORD_INACTIVE_PATH_ANGLE) || (raw_path_offset != FORD_INACTIVE_PATH_OFFSET);
|
||||
|
||||
// Check angle error and steer_control_enabled
|
||||
int desired_curvature = raw_curvature - FORD_INACTIVE_CURVATURE; // /FORD_STEERING_LIMITS.angle_deg_to_can to get real curvature
|
||||
violation |= steer_angle_cmd_checks(desired_curvature, steer_control_enabled, FORD_STEERING_LIMITS);
|
||||
int desired_curvature_rate = raw_curvature_rate - FORD_INACTIVE_CURVATURE_RATE;
|
||||
int desired_path_angle = raw_path_angle - FORD_INACTIVE_PATH_ANGLE;
|
||||
int desired_path_offset = raw_path_offset - FORD_INACTIVE_PATH_OFFSET;
|
||||
|
||||
bool violation = false;
|
||||
if (ford_extended_lateral) {
|
||||
violation |= desired_path_offset != 0;
|
||||
violation |= (desired_curvature_rate < -4096) || (desired_curvature_rate > 4095);
|
||||
violation |= ford_path_angle_checks(desired_path_angle, steer_control_enabled);
|
||||
if (ford_angle_mode) {
|
||||
violation |= (desired_path_angle < -1000) || (desired_path_angle > 1047);
|
||||
violation |= desired_curvature != 0;
|
||||
violation |= steer_control_enabled && !(aol_allowed || controls_allowed);
|
||||
int shadow_curvature_can = ROUND((float)ford_shadow_curvature * 0.05);
|
||||
violation |= ford_shadow_curvature_check(shadow_curvature_can, steer_control_enabled,
|
||||
FORD_EXTENDED_STEERING_LIMITS);
|
||||
desired_angle_last = 0;
|
||||
} else {
|
||||
violation |= desired_path_angle != 0;
|
||||
violation |= steer_angle_cmd_checks(desired_curvature, steer_control_enabled,
|
||||
FORD_EXTENDED_STEERING_LIMITS);
|
||||
}
|
||||
if (!steer_control_enabled) {
|
||||
violation |= (desired_curvature != 0) || (desired_curvature_rate != 0);
|
||||
}
|
||||
} else {
|
||||
violation |= (raw_curvature_rate != FORD_INACTIVE_CURVATURE_RATE) ||
|
||||
(raw_path_angle != FORD_INACTIVE_PATH_ANGLE) ||
|
||||
(raw_path_offset != FORD_INACTIVE_PATH_OFFSET);
|
||||
violation |= steer_angle_cmd_checks(desired_curvature, steer_control_enabled, FORD_STEERING_LIMITS);
|
||||
}
|
||||
|
||||
if (violation) {
|
||||
tx = false;
|
||||
@@ -261,6 +352,7 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
// Safety check for LateralMotionControl2 action
|
||||
if (msg->addr == FORD_LateralMotionControl2) {
|
||||
static const AngleSteeringLimits FORD_CANFD_STEERING_LIMITS = FORD_LIMITS(true);
|
||||
static const AngleSteeringLimits FORD_CANFD_EXTENDED_STEERING_LIMITS = FORD_EXTENDED_LIMITS(true);
|
||||
|
||||
// Signal: LatCtl_D2_Rq
|
||||
bool steer_control_enabled = ((msg->data[0] >> 4) & 0x7U) != 0U;
|
||||
@@ -269,12 +361,39 @@ static bool ford_tx_hook(const CANPacket_t *msg) {
|
||||
unsigned int raw_path_angle = ((msg->data[3] & 0x1FU) << 6) | (msg->data[4] >> 2);
|
||||
unsigned int raw_path_offset = ((msg->data[4] & 0x3U) << 8) | msg->data[5];
|
||||
|
||||
// These signals are not yet tested with the current safety limits
|
||||
bool violation = (raw_curvature_rate != FORD_CANFD_INACTIVE_CURVATURE_RATE) || (raw_path_angle != FORD_INACTIVE_PATH_ANGLE) || (raw_path_offset != FORD_INACTIVE_PATH_OFFSET);
|
||||
|
||||
// Check angle error and steer_control_enabled
|
||||
int desired_curvature = raw_curvature - FORD_INACTIVE_CURVATURE; // /FORD_STEERING_LIMITS.angle_deg_to_can to get real curvature
|
||||
violation |= steer_angle_cmd_checks(desired_curvature, steer_control_enabled, FORD_CANFD_STEERING_LIMITS);
|
||||
int desired_curvature_rate = raw_curvature_rate - FORD_CANFD_INACTIVE_CURVATURE_RATE;
|
||||
int desired_path_angle = raw_path_angle - FORD_INACTIVE_PATH_ANGLE;
|
||||
int desired_path_offset = raw_path_offset - FORD_INACTIVE_PATH_OFFSET;
|
||||
|
||||
bool violation = false;
|
||||
if (ford_extended_lateral) {
|
||||
violation |= desired_path_offset != 0;
|
||||
violation |= (desired_curvature_rate < -1024) || (desired_curvature_rate > 1023);
|
||||
violation |= ford_path_angle_checks(desired_path_angle, steer_control_enabled);
|
||||
if (ford_angle_mode) {
|
||||
violation |= (desired_path_angle < -1000) || (desired_path_angle > 1047);
|
||||
violation |= desired_curvature != 0;
|
||||
violation |= steer_control_enabled && !(aol_allowed || controls_allowed);
|
||||
int shadow_curvature_can = ROUND((float)ford_shadow_curvature * 0.05);
|
||||
violation |= ford_shadow_curvature_check(shadow_curvature_can, steer_control_enabled,
|
||||
FORD_CANFD_EXTENDED_STEERING_LIMITS);
|
||||
desired_angle_last = 0;
|
||||
} else {
|
||||
violation |= desired_path_angle != 0;
|
||||
violation |= steer_angle_cmd_checks(desired_curvature, steer_control_enabled,
|
||||
FORD_CANFD_EXTENDED_STEERING_LIMITS);
|
||||
}
|
||||
if (!steer_control_enabled) {
|
||||
violation |= (desired_curvature != 0) || (desired_curvature_rate != 0);
|
||||
}
|
||||
} else {
|
||||
violation |= (raw_curvature_rate != FORD_CANFD_INACTIVE_CURVATURE_RATE) ||
|
||||
(raw_path_angle != FORD_INACTIVE_PATH_ANGLE) ||
|
||||
(raw_path_offset != FORD_INACTIVE_PATH_OFFSET);
|
||||
violation |= steer_angle_cmd_checks(desired_curvature, steer_control_enabled,
|
||||
FORD_CANFD_STEERING_LIMITS);
|
||||
}
|
||||
|
||||
if (violation) {
|
||||
tx = false;
|
||||
@@ -328,6 +447,10 @@ static safety_config ford_init(uint16_t param) {
|
||||
const uint16_t FORD_PARAM_LKA_STEERING = 4;
|
||||
const bool ford_canfd = GET_FLAG(param, FORD_PARAM_CANFD);
|
||||
ford_lka_steering = GET_FLAG(param, FORD_PARAM_LKA_STEERING);
|
||||
ford_extended_lateral = false;
|
||||
ford_angle_mode = false;
|
||||
ford_shadow_curvature = 0;
|
||||
ford_desired_path_angle_last = 0;
|
||||
|
||||
bool ford_longitudinal = false;
|
||||
|
||||
|
||||
@@ -410,6 +410,38 @@ class TestFordCANFDStockSafety(TestFordSafetyBase):
|
||||
self.safety.set_safety_hooks(CarParams.SafetyModel.ford, FordSafetyFlags.CANFD)
|
||||
self.safety.init_tests()
|
||||
|
||||
def _extended_lka_msg(self, angle_mode=False, shadow_curvature=0.0):
|
||||
msg = self._lkas_command_msg(0)
|
||||
raw_shadow = int(round(shadow_curvature / 1e-6)) & 0xFFFF
|
||||
msg[0].data[4] |= 0x2 | int(angle_mode)
|
||||
msg[0].data[5] = raw_shadow >> 8
|
||||
msg[0].data[6] = raw_shadow & 0xFF
|
||||
return msg
|
||||
|
||||
def test_extended_curvature_signals(self):
|
||||
speed = 15.0
|
||||
self.safety.set_controls_allowed(True)
|
||||
self._reset_curvature_measurement(0.0, speed)
|
||||
self.assertTrue(self._tx(self._extended_lka_msg()))
|
||||
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, 0.0, 0.001, 0.0005)))
|
||||
|
||||
self.assertTrue(self._tx(self._extended_lka_msg()))
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.1, 0.0, 0.001, 0.0005)))
|
||||
|
||||
def test_extended_angle_signals(self):
|
||||
speed = 15.0
|
||||
curvature = 0.005
|
||||
self.safety.set_controls_allowed(True)
|
||||
self._reset_curvature_measurement(curvature, speed)
|
||||
self.assertTrue(self._tx(self._extended_lka_msg(angle_mode=True, shadow_curvature=curvature)))
|
||||
self.assertTrue(self._tx(self._lat_ctl_msg(True, 0.0, 0.02, 0.0, 0.0)))
|
||||
|
||||
self.assertTrue(self._tx(self._extended_lka_msg(angle_mode=True, shadow_curvature=curvature)))
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.2, 0.0, 0.0)))
|
||||
|
||||
self.assertTrue(self._tx(self._extended_lka_msg(angle_mode=True, shadow_curvature=-curvature)))
|
||||
self.assertFalse(self._tx(self._lat_ctl_msg(True, 0.0, 0.01, 0.0, 0.0)))
|
||||
|
||||
|
||||
class TestFordLongitudinalSafetyBase(TestFordSafetyBase):
|
||||
MAX_ACCEL = 2.0 # accel is used for brakes, but openpilot can set positive values
|
||||
|
||||
@@ -5,6 +5,7 @@ from openpilot.selfdrive.ui.lib.starpilot_state import starpilot_state
|
||||
from openpilot.system.ui.lib.application import gui_app, FontWeight
|
||||
from openpilot.system.ui.lib.multilang import tr, tr_noop
|
||||
from openpilot.system.ui.widgets import DialogResult
|
||||
from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog
|
||||
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.panel import _SettingsPage
|
||||
from openpilot.selfdrive.ui.layouts.settings.starpilot.aethergrid import (
|
||||
@@ -52,7 +53,7 @@ class SteeringManagerView(CardHubManagerView):
|
||||
super().__init__(controller, [], **kwargs)
|
||||
|
||||
def _build_cards(self):
|
||||
return [
|
||||
cards = [
|
||||
{
|
||||
"title": tr("Steering Behavior"),
|
||||
"desc": tr("Configure Always On Lateral (AOL), pause speed thresholds, and turn signal behaviors."),
|
||||
@@ -72,6 +73,14 @@ class SteeringManagerView(CardHubManagerView):
|
||||
"on_click": lambda: self._controller._navigate_to("advanced"),
|
||||
},
|
||||
]
|
||||
if starpilot_state.car_state.isFord:
|
||||
cards.append({
|
||||
"title": tr("Ford Lateral Tuning"),
|
||||
"desc": tr("Select the Ford steering strategy and tune prediction, lane-change, and speed response."),
|
||||
"icon": "steering",
|
||||
"on_click": lambda: self._controller._navigate_to("ford"),
|
||||
})
|
||||
return cards
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
@@ -319,6 +328,88 @@ class StarPilotLateralLayout(_SettingsPage):
|
||||
),
|
||||
]
|
||||
|
||||
# ── 4. Ford Lateral Tuning ──
|
||||
def ford_curvature_mode():
|
||||
return p.get_int("FordLateralMode") == 1
|
||||
|
||||
def ford_angle_mode():
|
||||
return p.get_int("FordLateralMode") == 2
|
||||
|
||||
def ford_enhanced_mode():
|
||||
return p.get_int("FordLateralMode") != 0
|
||||
|
||||
self._ford_rows = [
|
||||
SettingRow(
|
||||
"FordLateralMode", "value", tr_noop("Steering Strategy"),
|
||||
subtitle=tr_noop("Native keeps the existing Ford controls. Curvature and Angle enable the enhanced Ford strategies."),
|
||||
get_value=self._get_ford_lateral_mode,
|
||||
on_click=self._show_ford_lateral_mode,
|
||||
),
|
||||
SettingRow(
|
||||
"FordHumanTurnDetection", "toggle", tr_noop("Manual Turn Release"),
|
||||
subtitle=tr_noop("Release lateral control during a sustained hands-on turn, then ramp back in smoothly."),
|
||||
get_state=lambda: p.get_bool("FordHumanTurnDetection"),
|
||||
set_state=lambda s: p.put_bool("FordHumanTurnDetection", s),
|
||||
visible=ford_enhanced_mode,
|
||||
),
|
||||
SettingRow(
|
||||
"FordCurvatureBlendLow", "value", tr_noop("Small-Curve Prediction"),
|
||||
subtitle=tr_noop("Blend model-predicted curvature into gentle turns."),
|
||||
get_value=lambda: f"{p.get_float('FordCurvatureBlendLow') * 100:.0f}%",
|
||||
on_click=lambda: self._show_slider("FordCurvatureBlendLow", 0.0, 1.0, step=0.05, unit="", value_type="float"),
|
||||
visible=ford_curvature_mode,
|
||||
),
|
||||
SettingRow(
|
||||
"FordCurvatureBlendHigh", "value", tr_noop("Large-Curve Prediction"),
|
||||
subtitle=tr_noop("Blend model-predicted curvature into tighter turns."),
|
||||
get_value=lambda: f"{p.get_float('FordCurvatureBlendHigh') * 100:.0f}%",
|
||||
on_click=lambda: self._show_slider("FordCurvatureBlendHigh", 0.0, 1.0, step=0.05, unit="", value_type="float"),
|
||||
visible=ford_curvature_mode,
|
||||
),
|
||||
SettingRow(
|
||||
"FordCurvatureLaneChangeFactor", "value", tr_noop("Curvature Lane-Change Factor"),
|
||||
subtitle=tr_noop("Scale steering during high-speed lane changes in Curvature mode."),
|
||||
get_value=lambda: f"{p.get_float('FordCurvatureLaneChangeFactor'):.2f}x",
|
||||
on_click=lambda: self._show_slider("FordCurvatureLaneChangeFactor", 0.5, 1.25, step=0.05, unit="x", value_type="float"),
|
||||
visible=ford_curvature_mode,
|
||||
),
|
||||
SettingRow(
|
||||
"FordAngleBlend", "value", tr_noop("Angle Prediction Blend"),
|
||||
subtitle=tr_noop("Blend model prediction into the path-angle command."),
|
||||
get_value=lambda: f"{p.get_float('FordAngleBlend') * 100:.0f}%",
|
||||
on_click=lambda: self._show_slider("FordAngleBlend", 0.0, 1.0, step=0.05, unit="", value_type="float"),
|
||||
visible=ford_angle_mode,
|
||||
),
|
||||
SettingRow(
|
||||
"FordAngleLowSpeedFactor", "value", tr_noop("Low-Speed Angle Response"),
|
||||
subtitle=tr_noop("Adjust path-angle strength at lower speeds and higher curvature."),
|
||||
get_value=lambda: f"{p.get_float('FordAngleLowSpeedFactor'):.2f}x",
|
||||
on_click=lambda: self._show_slider("FordAngleLowSpeedFactor", 0.5, 1.5, step=0.05, unit="x", value_type="float"),
|
||||
visible=ford_angle_mode,
|
||||
),
|
||||
SettingRow(
|
||||
"FordAngleHighSpeedFactor", "value", tr_noop("High-Speed Angle Response"),
|
||||
subtitle=tr_noop("Adjust path-angle strength through larger highway curves."),
|
||||
get_value=lambda: f"{p.get_float('FordAngleHighSpeedFactor'):.2f}x",
|
||||
on_click=lambda: self._show_slider("FordAngleHighSpeedFactor", 0.5, 1.5, step=0.05, unit="x", value_type="float"),
|
||||
visible=ford_angle_mode,
|
||||
),
|
||||
SettingRow(
|
||||
"FordAngleHighSpeedDamping", "value", tr_noop("High-Speed Damping"),
|
||||
subtitle=tr_noop("Dampen small steering corrections at highway speed."),
|
||||
get_value=lambda: f"{p.get_float('FordAngleHighSpeedDamping'):.2f}x",
|
||||
on_click=lambda: self._show_slider("FordAngleHighSpeedDamping", 0.25, 1.25, step=0.05, unit="x", value_type="float"),
|
||||
visible=ford_angle_mode,
|
||||
),
|
||||
SettingRow(
|
||||
"FordAngleLaneChangeFactor", "value", tr_noop("Angle Lane-Change Factor"),
|
||||
subtitle=tr_noop("Scale steering during high-speed lane changes in Angle mode."),
|
||||
get_value=lambda: f"{p.get_float('FordAngleLaneChangeFactor'):.2f}x",
|
||||
on_click=lambda: self._show_slider("FordAngleLaneChangeFactor", 0.5, 1.5, step=0.05, unit="x", value_type="float"),
|
||||
visible=ford_angle_mode,
|
||||
),
|
||||
]
|
||||
|
||||
self._manager_view = SteeringManagerView(
|
||||
self,
|
||||
header_title=tr_noop("Steering"),
|
||||
@@ -362,6 +453,13 @@ class StarPilotLateralLayout(_SettingsPage):
|
||||
parent_toggle=pt_advanced,
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._sub_panels["ford"] = AetherSettingsView(
|
||||
self,
|
||||
[SettingSection(title="", rows=self._ford_rows)],
|
||||
header_title=tr_noop("Ford Lateral Tuning"),
|
||||
header_subtitle=tr_noop("Tune Ford-specific polynomial steering while retaining the native strategy as a fallback."),
|
||||
panel_style=PANEL_STYLE,
|
||||
)
|
||||
self._wire_sub_panels()
|
||||
|
||||
def _on_pause_lateral_speed_clicked(self):
|
||||
@@ -402,3 +500,17 @@ class StarPilotLateralLayout(_SettingsPage):
|
||||
current = self._params.get_int("LaneChangeSmoothing") if self._params.get_int("LaneChangeSmoothing") > 0 else 5
|
||||
gui_app.push_widget(AetherSliderDialog(tr("Lane Change Smoothing"), 1, 10, 1, current, on_close,
|
||||
color=self.SLIDER_COLOR))
|
||||
|
||||
def _get_ford_lateral_mode(self) -> str:
|
||||
return tr(("Native", "Curvature", "Angle")[max(0, min(2, self._params.get_int("FordLateralMode")))])
|
||||
|
||||
def _show_ford_lateral_mode(self):
|
||||
options = [tr("Native"), tr("Curvature"), tr("Angle")]
|
||||
current = options[max(0, min(2, self._params.get_int("FordLateralMode")))]
|
||||
|
||||
def on_select(res):
|
||||
if res == DialogResult.CONFIRM and dialog.selection in options:
|
||||
self._params.put_int("FordLateralMode", options.index(dialog.selection))
|
||||
|
||||
dialog = MultiOptionDialog(tr("Ford Steering Strategy"), options, current, callback=on_select)
|
||||
gui_app.push_widget(dialog)
|
||||
|
||||
@@ -15,6 +15,7 @@ from openpilot.starpilot.common.lateral_delay import full_lateral_delay
|
||||
class StarPilotCarState:
|
||||
# ========== Car Type Detection ==========
|
||||
isGM: bool = False
|
||||
isFord: bool = False
|
||||
isHKG: bool = False
|
||||
isJeep: bool = False
|
||||
isToyota: bool = False
|
||||
@@ -91,6 +92,7 @@ class StarPilotState:
|
||||
brand = FINGERPRINT_MAKE_TO_VALUES_DIR.get(fallback_make_lower, fallback_make_lower)
|
||||
fallback_model_str = fallback_model or ""
|
||||
self.car_state.isGM = brand == "gm"
|
||||
self.car_state.isFord = brand == "ford"
|
||||
self.car_state.isHKG = brand == "hyundai"
|
||||
self.car_state.isJeep = brand == "chrysler" and fallback_model_str.startswith("JEEP_")
|
||||
self.car_state.isSubaru = brand == "subaru"
|
||||
@@ -164,6 +166,7 @@ class StarPilotState:
|
||||
self.car_state.isAngleCar = self._safe_get(CP, "steerControlType", None) == car.CarParams.SteerControlType.angle
|
||||
self.car_state.isBolt = car_fingerprint.startswith("CHEVROLET_BOLT")
|
||||
self.car_state.isGM = car_make == "gm"
|
||||
self.car_state.isFord = car_make == "ford"
|
||||
self.car_state.isHKG = car_make == "hyundai"
|
||||
self.car_state.isHKGCanFd = self.car_state.isHKG and safety_model == car.CarParams.SafetyModel.hyundaiCanfd
|
||||
self.car_state.isJeep = car_make == "chrysler" and car_fingerprint.startswith("JEEP_")
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Vehicle-specific StarPilot extensions."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Ford-specific control extensions."""
|
||||
@@ -0,0 +1,53 @@
|
||||
from opendbc.car.ford.fordcan import CanBus, calculate_lat_ctl2_checksum
|
||||
|
||||
|
||||
SHADOW_CURVATURE_SCALE = 1e-6
|
||||
|
||||
|
||||
def create_lka_msg(packer, CAN: CanBus, angle_mode: bool = False, shadow_curvature: float = 0.0,
|
||||
extended_mode: bool = True):
|
||||
addr, dat, bus = packer.make_can_msg("Lane_Assist_Data1", CAN.main, {})
|
||||
dat = bytearray(dat)
|
||||
|
||||
shadow_raw = int(round(shadow_curvature / SHADOW_CURVATURE_SCALE))
|
||||
shadow_raw = max(-32768, min(32767, shadow_raw)) & 0xFFFF
|
||||
dat[4] |= int(angle_mode) | (int(extended_mode) << 1)
|
||||
dat[5] = shadow_raw >> 8
|
||||
dat[6] = shadow_raw & 0xFF
|
||||
return addr, bytes(dat), bus
|
||||
|
||||
|
||||
def create_lat_ctl_msg(packer, CAN: CanBus, active: bool, ramp_type: int, precision_type: int,
|
||||
path_offset: float, path_angle: float, curvature: float, curvature_rate: float):
|
||||
values = {
|
||||
"LatCtlRng_L_Max": 0,
|
||||
"HandsOffCnfm_B_Rq": 0,
|
||||
"LatCtl_D_Rq": 1 if active else 0,
|
||||
"LatCtlRampType_D_Rq": ramp_type,
|
||||
"LatCtlPrecision_D_Rq": precision_type,
|
||||
"LatCtlPathOffst_L_Actl": path_offset,
|
||||
"LatCtlPath_An_Actl": path_angle,
|
||||
"LatCtlCurv_NoRate_Actl": curvature_rate,
|
||||
"LatCtlCurv_No_Actl": curvature,
|
||||
}
|
||||
return packer.make_can_msg("LateralMotionControl", CAN.main, values)
|
||||
|
||||
|
||||
def create_lat_ctl2_msg(packer, CAN: CanBus, mode: int, ramp_type: int, precision_type: int,
|
||||
path_offset: float, path_angle: float, curvature: float,
|
||||
curvature_rate: float, counter: int):
|
||||
values = {
|
||||
"LatCtl_D2_Rq": mode,
|
||||
"LatCtlRampType_D_Rq": ramp_type,
|
||||
"LatCtlPrecision_D_Rq": precision_type,
|
||||
"LatCtlPathOffst_L_Actl": path_offset,
|
||||
"LatCtlPath_An_Actl": path_angle,
|
||||
"LatCtlCurv_No_Actl": curvature,
|
||||
"LatCtlCrv_NoRate2_Actl": curvature_rate,
|
||||
"HandsOffCnfm_B_Rq": 0,
|
||||
"LatCtlPath_No_Cnt": counter,
|
||||
"LatCtlPath_No_Cs": 0,
|
||||
}
|
||||
dat = packer.make_can_msg("LateralMotionControl2", 0, values)[1]
|
||||
values["LatCtlPath_No_Cs"] = calculate_lat_ctl2_checksum(mode, counter, dat)
|
||||
return packer.make_can_msg("LateralMotionControl2", CAN.main, values)
|
||||
@@ -0,0 +1,275 @@
|
||||
from collections import deque
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
|
||||
import numpy as np
|
||||
|
||||
from opendbc.car import ACCELERATION_DUE_TO_GRAVITY, DT_CTRL
|
||||
from opendbc.car.ford.values import CAR, CarControllerParams, FordFlags
|
||||
from opendbc.car.lateral import AngleSteeringLimits, ISO_LATERAL_ACCEL, apply_std_steer_angle_limits
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
|
||||
class FordLateralMode(IntEnum):
|
||||
native = 0
|
||||
curvature = 1
|
||||
angle = 2
|
||||
|
||||
|
||||
FORD_ANGLE_LIMITS = AngleSteeringLimits(
|
||||
0.02,
|
||||
([5, 16, 25], [0.0025, 0.0012, 0.00008]),
|
||||
([5, 16, 25], [0.0025, 0.0014, 0.00018]),
|
||||
)
|
||||
|
||||
MAX_LATERAL_ACCEL = ISO_LATERAL_ACCEL - ACCELERATION_DUE_TO_GRAVITY * 0.06
|
||||
PATH_ANGLE_MIN = -0.5
|
||||
PATH_ANGLE_MAX = 0.5235
|
||||
STEER_DT = CarControllerParams.STEER_STEP * DT_CTRL
|
||||
|
||||
CANFD_BODY_ON_FRAME = frozenset({
|
||||
CAR.FORD_F_150_MK14,
|
||||
CAR.FORD_F_150_LIGHTNING_MK1,
|
||||
CAR.FORD_EXPEDITION_MK4,
|
||||
CAR.FORD_RANGER_MK2,
|
||||
})
|
||||
CANFD_UNIBODY = frozenset({
|
||||
CAR.FORD_MUSTANG_MACH_E_MK1,
|
||||
CAR.FORD_ESCAPE_MK4_5,
|
||||
})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FordLateralResult:
|
||||
curvature: float = 0.0
|
||||
curvature_rate: float = 0.0
|
||||
path_offset: float = 0.0
|
||||
path_angle: float = 0.0
|
||||
ramp_type: int = 0
|
||||
precision_type: int = 1
|
||||
active: bool = False
|
||||
shadow_curvature: float = 0.0
|
||||
|
||||
|
||||
class HumanTurnDetector:
|
||||
ANGLE_DEG = 45.0
|
||||
HOLD_SECONDS = 1.5
|
||||
PRETURNED_HOLD_SECONDS = 3.0
|
||||
|
||||
def __init__(self):
|
||||
self.timer = 0.0
|
||||
self.active = False
|
||||
self._pressed_last = False
|
||||
self._press_started_preturned = False
|
||||
|
||||
def update(self, enabled: bool, steering_pressed: bool, steering_angle_deg: float) -> bool:
|
||||
if steering_pressed and not self._pressed_last:
|
||||
self._press_started_preturned = abs(steering_angle_deg) > self.ANGLE_DEG
|
||||
self._pressed_last = steering_pressed
|
||||
|
||||
if enabled and steering_pressed and abs(steering_angle_deg) > self.ANGLE_DEG:
|
||||
self.timer += STEER_DT
|
||||
else:
|
||||
self.timer = 0.0
|
||||
|
||||
hold_time = self.PRETURNED_HOLD_SECONDS if self._press_started_preturned else self.HOLD_SECONDS
|
||||
self.active = self.timer + 1e-9 >= hold_time
|
||||
return self.active
|
||||
|
||||
def reset(self):
|
||||
self.timer = 0.0
|
||||
self.active = False
|
||||
self._pressed_last = False
|
||||
self._press_started_preturned = False
|
||||
|
||||
|
||||
class FordLateralController:
|
||||
"""Ford polynomial lateral strategies kept outside the native car implementation."""
|
||||
|
||||
def __init__(self, CP):
|
||||
self.CP = CP
|
||||
self.params = Params(return_defaults=True)
|
||||
try:
|
||||
import cereal.messaging as messaging
|
||||
self.sm = messaging.SubMaster(["modelV2", "liveDelay"])
|
||||
except ImportError:
|
||||
# The host interface tests don't load the device messaging extension.
|
||||
self.sm = None
|
||||
self.model = None
|
||||
|
||||
self.mode = FordLateralMode.curvature
|
||||
self.human_turn_enabled = True
|
||||
self.curvature_blend_low = 0.4
|
||||
self.curvature_blend_high = 0.4
|
||||
self.angle_blend = 0.5
|
||||
self.curvature_lane_change_factor = 0.85
|
||||
self.angle_lane_change_factor = 1.0
|
||||
self.angle_low_speed_factor = 1.0
|
||||
self.angle_high_speed_factor = 1.0
|
||||
self.angle_high_speed_damping = 1.0
|
||||
|
||||
self.human_turn = HumanTurnDetector()
|
||||
self.curvature_samples = deque(maxlen=max(2, round(0.3 / STEER_DT)))
|
||||
self.path_angle_last = 0.0
|
||||
self.curvature_last = 0.0
|
||||
self._frame = 0
|
||||
self._update_params()
|
||||
|
||||
def _update_params(self):
|
||||
try:
|
||||
self.mode = FordLateralMode(int(np.clip(self.params.get_int("FordLateralMode", return_default=True), 0, 2)))
|
||||
except ValueError:
|
||||
self.mode = FordLateralMode.native
|
||||
|
||||
self.human_turn_enabled = self.params.get_bool("FordHumanTurnDetection")
|
||||
self.curvature_blend_low = float(np.clip(self.params.get_float("FordCurvatureBlendLow", return_default=True), 0.0, 1.0))
|
||||
self.curvature_blend_high = float(np.clip(self.params.get_float("FordCurvatureBlendHigh", return_default=True), 0.0, 1.0))
|
||||
self.angle_blend = float(np.clip(self.params.get_float("FordAngleBlend", return_default=True), 0.0, 1.0))
|
||||
self.curvature_lane_change_factor = float(np.clip(
|
||||
self.params.get_float("FordCurvatureLaneChangeFactor", return_default=True), 0.5, 1.25))
|
||||
self.angle_lane_change_factor = float(np.clip(
|
||||
self.params.get_float("FordAngleLaneChangeFactor", return_default=True), 0.5, 1.5))
|
||||
self.angle_low_speed_factor = float(np.clip(
|
||||
self.params.get_float("FordAngleLowSpeedFactor", return_default=True), 0.5, 1.5))
|
||||
self.angle_high_speed_factor = float(np.clip(
|
||||
self.params.get_float("FordAngleHighSpeedFactor", return_default=True), 0.5, 1.5))
|
||||
self.angle_high_speed_damping = float(np.clip(
|
||||
self.params.get_float("FordAngleHighSpeedDamping", return_default=True), 0.25, 1.25))
|
||||
|
||||
def update_inputs(self):
|
||||
if self.sm is not None:
|
||||
self.sm.update(0)
|
||||
if self.sm.updated["modelV2"]:
|
||||
self.model = self.sm["modelV2"]
|
||||
if self._frame % 100 == 0:
|
||||
self._update_params()
|
||||
self._frame += 1
|
||||
|
||||
def _predicted_curvature(self, v_ego: float, lookup_time: float) -> float:
|
||||
if self.model is None or len(self.model.orientationRate.z) < 17:
|
||||
return 0.0
|
||||
curvatures = np.asarray(self.model.orientationRate.z) / max(v_ego, 0.01)
|
||||
return float(np.interp(lookup_time, ModelConstants.T_IDXS, curvatures))
|
||||
|
||||
def _lane_change(self) -> tuple[bool, int]:
|
||||
if self.model is None:
|
||||
return False, 0
|
||||
state = int(self.model.meta.laneChangeState)
|
||||
return state in (1, 2, 3), int(self.model.meta.laneChangeDirection)
|
||||
|
||||
@staticmethod
|
||||
def _current_curvature(CS) -> float:
|
||||
return -CS.out.yawRate / max(CS.out.vEgoRaw, 0.1)
|
||||
|
||||
def _blend_and_scale(self, desired: float, predicted: float, v_ego: float, angle_mode: bool) -> tuple[float, int]:
|
||||
if angle_mode:
|
||||
blend = self.angle_blend
|
||||
high_factor = self.angle_lane_change_factor
|
||||
else:
|
||||
blend = float(np.interp(abs(desired), [0.0, 0.001], [self.curvature_blend_low, self.curvature_blend_high]))
|
||||
high_factor = self.curvature_lane_change_factor
|
||||
|
||||
requested = predicted * blend + desired * (1.0 - blend)
|
||||
lane_change, direction = self._lane_change()
|
||||
precision = 1
|
||||
if lane_change:
|
||||
factor = float(np.interp(v_ego, [4.4, 40.23], [0.95, high_factor]))
|
||||
if (direction == 1 and requested < 0.0) or (direction == 2 and requested > 0.0):
|
||||
requested *= factor
|
||||
precision = 0
|
||||
return requested, precision
|
||||
|
||||
def _manual_turn(self, CC, CS) -> bool:
|
||||
if not CC.latActive:
|
||||
self.human_turn.reset()
|
||||
return False
|
||||
return self.human_turn.update(
|
||||
self.human_turn_enabled, CS.out.steeringPressed, CS.out.steeringAngleDeg)
|
||||
|
||||
def update_curvature(self, CC, CS, actuators) -> FordLateralResult:
|
||||
if not CC.latActive or self._manual_turn(CC, CS) or CS.out.vEgoRaw < 0.1:
|
||||
self.curvature_samples.clear()
|
||||
self.curvature_last = 0.0
|
||||
return FordLateralResult(shadow_curvature=self._current_curvature(CS))
|
||||
|
||||
v_ego = float(CS.out.vEgoRaw)
|
||||
predicted = self._predicted_curvature(v_ego, 0.2)
|
||||
requested, precision = self._blend_and_scale(float(actuators.curvature), predicted, v_ego, False)
|
||||
current = self._current_curvature(CS)
|
||||
|
||||
if v_ego > 9.0:
|
||||
requested = float(np.clip(requested, current - CarControllerParams.CURVATURE_ERROR,
|
||||
current + CarControllerParams.CURVATURE_ERROR))
|
||||
applied = float(apply_std_steer_angle_limits(
|
||||
requested, self.curvature_last, v_ego, CS.out.steeringAngleDeg, True, FORD_ANGLE_LIMITS))
|
||||
if self.CP.flags & FordFlags.CANFD:
|
||||
max_curvature = MAX_LATERAL_ACCEL / max(v_ego, 1.0) ** 2
|
||||
applied = float(np.clip(applied, -max_curvature, max_curvature))
|
||||
|
||||
self.curvature_samples.append(predicted)
|
||||
curvature_rate = 0.0
|
||||
if len(self.curvature_samples) > 1:
|
||||
sample_time = (len(self.curvature_samples) - 1) * STEER_DT
|
||||
curvature_rate = (self.curvature_samples[-1] - self.curvature_samples[0]) / max(sample_time * v_ego, 0.01)
|
||||
curvature_rate *= float(np.interp(abs(predicted), [0.0, 0.008, 0.01], [0.0, 0.0, 1.0]))
|
||||
curvature_rate *= float(np.interp(v_ego, [0.0, 14.5, 15.5], [1.0, 1.0, 0.0]))
|
||||
if self._lane_change()[0]:
|
||||
curvature_rate = 0.0
|
||||
|
||||
self.curvature_last = float(np.clip(applied, -0.02, 0.02))
|
||||
curvature_rate = float(np.clip(curvature_rate, -0.001024, 0.001023))
|
||||
return FordLateralResult(
|
||||
curvature=self.curvature_last,
|
||||
curvature_rate=curvature_rate,
|
||||
ramp_type=2,
|
||||
precision_type=precision,
|
||||
active=True,
|
||||
)
|
||||
|
||||
def _platform_angle_gains(self) -> tuple[float, float]:
|
||||
if self.CP.carFingerprint in CANFD_BODY_ON_FRAME:
|
||||
return 0.95, 0.95
|
||||
if self.CP.carFingerprint in CANFD_UNIBODY:
|
||||
return 1.0, 1.05
|
||||
return 1.0, 1.15
|
||||
|
||||
def update_angle(self, CC, CS, actuators) -> FordLateralResult:
|
||||
current = self._current_curvature(CS)
|
||||
if not CC.latActive or self._manual_turn(CC, CS):
|
||||
self.path_angle_last = 0.0
|
||||
return FordLateralResult(shadow_curvature=current)
|
||||
|
||||
v_ego = float(CS.out.vEgoRaw)
|
||||
live_delay = 0.12 if self.sm is None else float(np.clip(self.sm["liveDelay"].lateralDelay, 0.1, 0.15))
|
||||
speed_factor = float(np.interp(v_ego, [11.176, 24.587], [1.0, 0.0]))
|
||||
curvature_factor = float(np.interp(abs(actuators.curvature), [0.005, 0.02], [1.0, 0.0]))
|
||||
lookup_time = live_delay + 0.05 + 0.10 * speed_factor * curvature_factor
|
||||
predicted = self._predicted_curvature(v_ego, lookup_time)
|
||||
requested, precision = self._blend_and_scale(float(actuators.curvature), predicted, v_ego, True)
|
||||
|
||||
if v_ego > 9.0:
|
||||
requested = float(np.clip(requested, current - CarControllerParams.CURVATURE_ERROR,
|
||||
current + CarControllerParams.CURVATURE_ERROR))
|
||||
|
||||
low_gain_high_speed, high_gain_high_speed = self._platform_angle_gains()
|
||||
low_gain = float(np.interp(v_ego, [13.5, 26.82],
|
||||
[1.0, low_gain_high_speed * self.angle_high_speed_damping]))
|
||||
high_gain = float(np.interp(v_ego, [13.5, 26.82],
|
||||
[1.30 * self.angle_low_speed_factor,
|
||||
high_gain_high_speed * self.angle_high_speed_factor]))
|
||||
gain = float(np.interp(abs(requested), [0.0007, 0.001], [low_gain, high_gain]))
|
||||
path_angle = float(np.clip(requested * v_ego * gain, PATH_ANGLE_MIN, PATH_ANGLE_MAX))
|
||||
|
||||
max_delta = float(np.interp(v_ego, [9.0, 10.0, 15.0, 25.0], [0.055, 0.055, 0.0425, 0.009]))
|
||||
path_angle = float(np.clip(path_angle, self.path_angle_last - max_delta, self.path_angle_last + max_delta))
|
||||
self.path_angle_last = path_angle
|
||||
|
||||
shadow = current if CS.out.steeringPressed else requested
|
||||
return FordLateralResult(
|
||||
path_angle=path_angle,
|
||||
ramp_type=2,
|
||||
precision_type=precision,
|
||||
active=True,
|
||||
shadow_curvature=shadow,
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
import sys
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
from ..lateral import FordLateralController, HumanTurnDetector
|
||||
|
||||
|
||||
class FakeSubMaster(dict):
|
||||
def __init__(self, services):
|
||||
super().__init__({"liveDelay": SimpleNamespace(lateralDelay=0.12)})
|
||||
self.updated = dict.fromkeys(services, False)
|
||||
|
||||
def update(self, timeout):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def controller(monkeypatch):
|
||||
messaging = SimpleNamespace(SubMaster=FakeSubMaster)
|
||||
monkeypatch.setitem(sys.modules, "cereal.messaging", messaging)
|
||||
CP = SimpleNamespace(flags=0, carFingerprint="FORD_EDGE_MK2")
|
||||
return FordLateralController(CP)
|
||||
|
||||
|
||||
def car_state(speed=15.0, curvature=0.0, steering_pressed=False, steering_angle=0.0):
|
||||
return SimpleNamespace(out=SimpleNamespace(
|
||||
vEgoRaw=speed,
|
||||
yawRate=-curvature * speed,
|
||||
steeringPressed=steering_pressed,
|
||||
steeringAngleDeg=steering_angle,
|
||||
))
|
||||
|
||||
|
||||
def test_human_turn_requires_sustained_input():
|
||||
detector = HumanTurnDetector()
|
||||
assert not detector.update(True, True, 0.0)
|
||||
for _ in range(29):
|
||||
assert not detector.update(True, True, 50.0)
|
||||
assert detector.update(True, True, 50.0)
|
||||
assert not detector.update(True, False, 50.0)
|
||||
|
||||
|
||||
def test_curvature_strategy_uses_polynomial_signals(controller):
|
||||
result = controller.update_curvature(
|
||||
SimpleNamespace(latActive=True), car_state(), SimpleNamespace(curvature=0.001))
|
||||
assert result.active
|
||||
assert 0.0 < result.curvature <= 0.001
|
||||
assert result.ramp_type == 2
|
||||
|
||||
|
||||
def test_angle_strategy_uses_path_angle_and_shadow(controller):
|
||||
result = controller.update_angle(
|
||||
SimpleNamespace(latActive=True), car_state(curvature=0.001), SimpleNamespace(curvature=0.001))
|
||||
assert result.active
|
||||
assert result.curvature == 0.0
|
||||
assert result.path_angle > 0.0
|
||||
assert result.shadow_curvature == pytest.approx(0.001)
|
||||
|
||||
|
||||
def test_manual_turn_releases_lateral(controller):
|
||||
controller.human_turn_enabled = True
|
||||
CC = SimpleNamespace(latActive=True)
|
||||
CS = car_state(steering_pressed=True, steering_angle=50.0)
|
||||
actuators = SimpleNamespace(curvature=0.001)
|
||||
for _ in range(61):
|
||||
result = controller.update_angle(CC, CS, actuators)
|
||||
assert not result.active
|
||||
assert result.path_angle == 0.0
|
||||
Reference in New Issue
Block a user