Match Dom hands-free cluster scope across Ford CAN-FD

This commit is contained in:
firestar5683
2026-10-21 12:36:48 -05:00
parent 43eea2e977
commit b3c26b5824
7 changed files with 109 additions and 10 deletions
@@ -77,6 +77,7 @@ class CarController(CarControllerBase):
self.mache_extended_announced = False
self.mache_ui_active = False
self.hands_free_cluster_input = None
self.cluster_extended_announced = False
self.generic_canfd = generic_canfd_qualified(CP)
self.classic_lateral = GenericCanfdLateralController(CP) if self.generic_canfd else create_classic_controller(CP)
self.new_port_curvature = create_new_port_curvature(CP)
@@ -329,6 +330,10 @@ class CarController(CarControllerBase):
self.accel = accel
self.gas = gas
# Track the actual outgoing LKA announcement for every CAN-FD display owner.
if any(address == 0x3CA for address, _, _ in can_sends):
self.cluster_extended_announced = True
### ui ###
# AOL safety forwards stock UI until an active lateral replacement owns it.
# Avoid competing cluster messages while stock remains the display owner.
@@ -347,7 +352,7 @@ class CarController(CarControllerBase):
if (self.frame % CarControllerParams.ACC_UI_STEP) == 0 or send_ui:
show_distance_bars = self.frame - self.distance_bar_frame < 400
hands_free_cluster = bool(self.mache_lateral is not None and self.mache_extended_announced and
hands_free_cluster = bool(self.cluster_extended_announced and
self.hands_free_cluster_input is not None and self.hands_free_cluster_input.enabled)
can_sends.append(fordcan.create_acc_ui_msg(self.packer, self.CAN, self.CP, main_on, ui_enabled,
fcw_alert, CS.out.cruiseState.standstill, show_distance_bars,
@@ -0,0 +1,83 @@
"""Dom CAN-FD cluster scope through actual factory, setting UI and controller."""
from pathlib import Path
from types import SimpleNamespace
import pytest
from opendbc.can import CANParser
from opendbc.car import structs
from opendbc.car.ford.tests.test_mache_ui_owner import setup, UI
from opendbc.car.ford.values import CAR, FordFlags
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.car.ford.cluster_preferences import KEY, qualified
from openpilot.starpilot.controller_extensions import configure_controller
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import row_change
DOM_CANFD = (
CAR.FORD_MUSTANG_MACH_E_MK1, CAR.FORD_ESCAPE_MK4_5, CAR.FORD_EXPEDITION_MK4,
CAR.FORD_F_150_MK14, CAR.FORD_F_150_LIGHTNING_MK1, CAR.FORD_RANGER_MK2, CAR.FORD_MONDEO_MK5,
)
def test_scope_covers_every_dom_canfd_profile():
assert set(DOM_CANFD) == {car for car in CAR if car.config.flags & FordFlags.CANFD and not car.config.flags & FordFlags.LKA_STEERING}
@pytest.mark.parametrize('identity', DOM_CANFD)
@pytest.mark.parametrize('aol', [False, True])
@pytest.mark.parametrize('enabled', [False, True])
def test_all_dom_profiles_have_shared_setting_and_correct_active_status(identity, aol, enabled):
with OpenpilotPrefix():
saved = Params()
controller, state = setup(aol=aol, identity=identity)
assert qualified(controller.CP)
owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP,
vehicle_fingerprint=lambda: str(identity))
row = next(r for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows if r.key == KEY)
assert row.available and row.value == 'Off'
if enabled:
assert owner.apply(row_change(row, 1))
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
assert controller.hands_free_cluster_input.enabled == enabled
command = structs.CarControl()
for _ in range(10):
_, frames = controller.update(command.as_reader(), state, controller.frame * 10_000_000)
if identity == CAR.FORD_MUSTANG_MACH_E_MK1 and aol:
assert not UI.intersection(address for address, _, _ in frames)
command.latActive = True
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
statuses = []
for _ in range(110):
_, frames = controller.update(command.as_reader(), state, controller.frame * 10_000_000)
for frame in frames:
if frame[0] == 0x18A:
parser.update([(controller.frame * 10_000_000, [frame])])
statuses.append(parser.vl['ACCDATA_3']['Tja_D_Stat'])
assert statuses and set(statuses) == {7 if enabled else 2}
assert controller.cluster_extended_announced
@pytest.mark.parametrize('identity', [car for car in CAR if car not in DOM_CANFD])
def test_non_canfd_fords_do_not_show_cluster_setting(identity):
with OpenpilotPrefix():
saved = Params()
controller, _ = setup(identity=identity)
assert not qualified(controller.CP)
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
assert controller.hands_free_cluster_input is None
owner = FeatureSettingsOwner(saved, lambda group: True, vehicle_params=lambda: controller.CP,
vehicle_fingerprint=lambda: str(identity))
assert KEY not in {r.key for r in owner.snapshot('torque', parked=True, system_long=True, lateral_context=True, metric=False).rows}
def test_invalid_profile_does_not_enable_display_preference():
with OpenpilotPrefix():
saved = Params()
Path(saved.get_param_path(KEY)).write_bytes(b'1')
controller, _ = setup()
controller.CP.safetyConfigs[0].safetyParam = 0
assert not qualified(controller.CP)
configure_controller(SimpleNamespace(CP=controller.CP, CC=controller), saved)
assert controller.hands_free_cluster_input is None
@@ -12,7 +12,7 @@ from opendbc.car.ford.tests.test_mache_ui_owner import setup, UI
from opendbc.car.ford.values import CAR
from openpilot.common.params import Params
from openpilot.common.prefix import OpenpilotPrefix
from openpilot.starpilot.car.ford.cluster_preferences import KEY, MachEClusterPreference, read_choice
from openpilot.starpilot.car.ford.cluster_preferences import KEY, FordClusterPreference, read_choice
from openpilot.starpilot.controller_extensions import configure_controller
from openpilot.starpilot.ui.feature_settings_owner import FeatureSettingsOwner
from openpilot.starpilot.ui.feature_settings_state import row_change
@@ -33,7 +33,7 @@ def test_saved_opt_in_through_actual_controller_configuration_and_warning_priori
saved = Params()
owner, state = setup()
configure_controller(SimpleNamespace(CP=owner.CP, CC=owner), saved)
assert isinstance(owner.hands_free_cluster_input, MachEClusterPreference)
assert isinstance(owner.hands_free_cluster_input, FordClusterPreference)
assert not owner.hands_free_cluster_input.enabled
parser = CANParser('ford_lincoln_base_pt', [('ACCDATA_3', 5)], 0)
command = structs.CarControl(latActive=True)
@@ -5,13 +5,24 @@ KEY = "FordHandsFreeCluster"
CHOICES = {0: "Off", 1: "On"}
def qualified(cp):
"""Dom's CAN-FD/non-LKA scope, constrained to existing admitted owners."""
from opendbc.car.ford.values import FordFlags
from opendbc.car.ford.mache_lateral import qualified as mache
from opendbc.car.ford.generic_canfd_lateral import qualified as generic
from opendbc.car.ford.new_port_curvature import qualified as new_port
from opendbc.bluepilot_lateral.hosts.starpilot import qualified as angle
return bool(cp.brand == "ford" and cp.flags & FordFlags.CANFD and not cp.flags & FordFlags.LKA_STEERING and
(mache(cp) or generic(cp) or new_port(cp) or angle(cp)))
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 MachEClusterPreference:
class FordClusterPreference:
def __init__(self, params):
self.params = params
self.enabled = bool(read_choice(params)[0])
+3 -3
View File
@@ -139,9 +139,9 @@ class ResumePlanInputs:
def configure_controller(CI, params):
cp = CI.CP
controller = CI.CC
if controller is not None and getattr(controller, "mache_lateral", None) is not None:
from openpilot.starpilot.car.ford.cluster_preferences import MachEClusterPreference
controller.hands_free_cluster_input = MachEClusterPreference(params)
from openpilot.starpilot.car.ford.cluster_preferences import FordClusterPreference, qualified as cluster_qualified
if controller is not None and cluster_qualified(cp):
controller.hands_free_cluster_input = FordClusterPreference(params)
if controller is not None and cp.brand == 'gm':
from opendbc.car.gm.values import (is_bolt_euv_longitudinal, is_volt_longitudinal,
camera_acc_pedal_profile, volt_cc_pedal_profile)
@@ -62,12 +62,12 @@ class FordLateralFeature:
class FordClusterFeature:
"""Shared native/Galaxy saved display preference for the Mach-E."""
"""Shared native/Galaxy saved display preference for Ford CAN-FD profiles."""
def __init__(self, owner):
self.owner = owner
def capability(self):
from opendbc.car.ford.mache_lateral import qualified
from openpilot.starpilot.car.ford.cluster_preferences import qualified
cp = self.owner.vehicle_params()
try:
if cp is None or not qualified(cp):
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "accbfb22312328a90cb22906acefde416d9eb24121e049a21d5283529531ed68", "schema_version": 1}
{"protected_inventory_sha256": "4026fd67ff54d43b1eda7fd9b8a732fbf8bcbc9cf820498a83ce2521715935ef", "schema_version": 1}