import copy import re from dataclasses import dataclass, field, replace from enum import Enum, IntFlag from opendbc.car import AngleSteeringLimits, Bus, CarSpecs, DbcDict, PlatformConfig, Platforms, uds from opendbc.car.structs import CarParams from opendbc.car.docs_definitions import CarFootnote, CarHarness, CarDocs, CarParts, Column, \ Device from opendbc.car.fw_query_definitions import FwQueryConfig, LiveFwVersions, OfflineFwVersions, Request, StdQueries, p16 Ecu = CarParams.Ecu class CarControllerParams: STEER_STEP = 5 # LateralMotionControl, 20Hz LKA_STEP = 3 # Lane_Assist_Data1, 33Hz ACC_CONTROL_STEP = 2 # ACCDATA, 50Hz LKAS_UI_STEP = 100 # IPMA_Data, 1Hz ACC_UI_STEP = 20 # ACCDATA_3, 5Hz BUTTONS_STEP = 5 # Steering_Data_FD1, 10Hz, but send twice as fast STEER_DRIVER_ALLOWANCE = 1.0 # Driver intervention threshold, Nm ANGLE_LIMITS: AngleSteeringLimits = AngleSteeringLimits( 0.02, # Max curvature for steering command, m^-1 # Curvature rate limits # Max curvature is limited by the EPS to an equivalent of ~2.0 m/s^2 at all speeds, # however max curvature rate linearly decreases as speed increases: # ~0.009 m^-1/sec at 7 m/s, ~0.002 m^-1/sec at 35 m/s # Limit to ~2 m/s^3 up, ~3.3 m/s^3 down at 75 mph and match EPS limit at low speed ([5, 25], [0.00045, 0.0001]), ([5, 25], [0.00045, 0.00015]) ) CURVATURE_ERROR = 0.002 # ~6 degrees at 10 m/s, ~10 degrees at 35 m/s ACCEL_MAX = 2.0 # m/s^2 max acceleration ACCEL_MIN = -3.5 # m/s^2 max deceleration MIN_GAS = -0.5 INACTIVE_GAS = -5.0 def __init__(self, CP): pass class FordSafetyFlags(IntFlag): LONG_CONTROL = 1 CANFD = 2 class FordFlags(IntFlag): # Static flags CANFD = 1 class RADAR: DELPHI_ESR = 'ford_fusion_2018_adas' DELPHI_MRR = 'FORD_CADS' class Footnote(Enum): FOCUS = CarFootnote( "Refers only to the Focus Mk4 (C519) available in Europe/China/Taiwan/Australasia, not the Focus Mk3 (C346) in " + "North and South America/Southeast Asia.", Column.MODEL, ) @dataclass class FordCarDocs(CarDocs): package: str = "Co-Pilot360 Assist+" hybrid: bool = False plug_in_hybrid: bool = False def init_make(self, CP: CarParams): harness = CarHarness.ford_q4 if CP.flags & FordFlags.CANFD else CarHarness.ford_q3 if CP.carFingerprint in (CAR.FORD_BRONCO_SPORT_MK1, CAR.FORD_MAVERICK_MK1, CAR.FORD_F_150_MK14, CAR.FORD_F_150_LIGHTNING_MK1): self.car_parts = CarParts([Device.threex_angled_mount, harness]) else: self.car_parts = CarParts([Device.threex, harness]) if harness == CarHarness.ford_q4: self.setup_video_link = "https://www.youtube.com/watch?v=uUGkH6C_EQU" @dataclass class FordPlatformConfig(PlatformConfig): dbc_dict: DbcDict = field(default_factory=lambda: { Bus.pt: 'ford_lincoln_base_pt', Bus.radar: RADAR.DELPHI_MRR, }) def init(self): for car_docs in list(self.car_docs): if car_docs.hybrid: name = f"{car_docs.make} {car_docs.model} Hybrid {car_docs.years}" self.car_docs.append(replace(copy.deepcopy(car_docs), name=name)) if car_docs.plug_in_hybrid: name = f"{car_docs.make} {car_docs.model} Plug-in Hybrid {car_docs.years}" self.car_docs.append(replace(copy.deepcopy(car_docs), name=name)) @dataclass class FordCANFDPlatformConfig(FordPlatformConfig): dbc_dict: DbcDict = field(default_factory=lambda: { Bus.pt: 'ford_lincoln_base_pt', }) def init(self): super().init() self.flags |= FordFlags.CANFD @dataclass class FordF150LightningPlatform(FordCANFDPlatformConfig): def init(self): super().init() # Don't show in docs until this issue is resolved. See https://github.com/commaai/openpilot/issues/30302 self.car_docs = [] class CAR(Platforms): FORD_BRONCO_SPORT_MK1 = FordPlatformConfig( [FordCarDocs("Ford Bronco Sport 2021-24")], CarSpecs(mass=1625, wheelbase=2.67, steerRatio=17.7), ) FORD_ESCAPE_MK4 = FordPlatformConfig( [ FordCarDocs("Ford Escape 2020-22", hybrid=True, plug_in_hybrid=True), 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), ) FORD_ESCAPE_MK4_5 = FordCANFDPlatformConfig( [ FordCarDocs("Ford Escape 2023-24", hybrid=True, plug_in_hybrid=True), FordCarDocs("Ford Kuga Hybrid 2024", "All"), FordCarDocs("Ford Kuga Plug-in Hybrid 2024", "All"), ], CarSpecs(mass=1750, wheelbase=2.71, steerRatio=16.7), ) FORD_EXPLORER_MK6 = FordPlatformConfig( [ FordCarDocs("Ford Explorer 2020-24", hybrid=True), # Hybrid: Limited and Platinum only 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), ) 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), ) FORD_F_150_LIGHTNING_MK1 = FordF150LightningPlatform( [FordCarDocs("Ford F-150 Lightning 2022-23", "Co-Pilot360 Assist 2.0")], CarSpecs(mass=2948, wheelbase=3.70, steerRatio=16.9), ) FORD_FOCUS_MK4 = FordPlatformConfig( [FordCarDocs("Ford Focus 2018", "Adaptive Cruise Control with Lane Centering", footnotes=[Footnote.FOCUS], hybrid=True)], # mHEV only CarSpecs(mass=1350, wheelbase=2.7, steerRatio=15.0), ) 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), ) FORD_MUSTANG_MACH_E_MK1 = FordCANFDPlatformConfig( [FordCarDocs("Ford Mustang Mach-E 2021-23", "All")], CarSpecs(mass=2200, wheelbase=2.984, steerRatio=17.0), # TODO: check steer ratio ) FORD_RANGER_MK2 = FordCANFDPlatformConfig( [FordCarDocs("Ford Ranger 2024", "Adaptive Cruise Control with Lane Centering")], CarSpecs(mass=2000, wheelbase=3.27, steerRatio=17.0), ) # FW response contains a combined software and part number # A-Z except no I, O or W # e.g. NZ6A-14C204-AAA # 1222-333333-444 # 1 = Model year hint (approximates model year/generation) # 2 = Platform hint # 3 = Part number # 4 = Software version FW_ALPHABET = b'A-HJ-NP-VX-Z' FW_PATTERN = re.compile(b'^(?P[' + FW_ALPHABET + b'])' + b'(?P[0-9' + FW_ALPHABET + b']{3})-' + b'(?P[0-9' + FW_ALPHABET + b']{5,6})-' + b'(?P[' + FW_ALPHABET + b']{2,})\x00*$') def get_platform_codes(fw_versions: list[bytes] | set[bytes]) -> set[tuple[bytes, bytes]]: codes = set() for fw in fw_versions: match = FW_PATTERN.match(fw) if match is not None: codes.add((match.group('platform_hint'), match.group('model_year_hint'))) return codes def match_fw_to_car_fuzzy(live_fw_versions: LiveFwVersions, vin: str, offline_fw_versions: OfflineFwVersions) -> set[str]: candidates: set[str] = set() for candidate, fws in offline_fw_versions.items(): # Keep track of ECUs which pass all checks (platform hint, within model year hint range) valid_found_ecus = set() valid_expected_ecus = {ecu[1:] for ecu in fws if ecu[0] in PLATFORM_CODE_ECUS} for ecu, expected_versions in fws.items(): addr = ecu[1:] # Only check ECUs expected to have platform codes if ecu[0] not in PLATFORM_CODE_ECUS: continue # Expected platform codes & model year hints codes = get_platform_codes(expected_versions) expected_platform_codes = {code for code, _ in codes} expected_model_year_hints = {model_year_hint for _, model_year_hint in codes} # Found platform codes & model year hints codes = get_platform_codes(live_fw_versions.get(addr, set())) found_platform_codes = {code for code, _ in codes} found_model_year_hints = {model_year_hint for _, model_year_hint in codes} # Check platform code matches for any found versions if not any(found_platform_code in expected_platform_codes for found_platform_code in found_platform_codes): break # Check any model year hint within range in the database. Note that some models have more than one # platform code per ECU which we don't consider as separate ranges if not any(min(expected_model_year_hints) <= found_model_year_hint <= max(expected_model_year_hints) for found_model_year_hint in found_model_year_hints): break valid_found_ecus.add(addr) # If all live ECUs pass all checks for candidate, add it as a match if valid_expected_ecus.issubset(valid_found_ecus): candidates.add(candidate) return candidates # All of these ECUs must be present and are expected to have platform codes we can match 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]), (5, [Ecu.debug]), (6, [Ecu.debug]), (7, [Ecu.debug]), (8, [Ecu.debug]), (9, [Ecu.debug]), (16, [Ecu.debug, Ecu.fwdCamera]), (18, [Ecu.fwdCamera]), (20, [Ecu.fwdCamera]), (21, [Ecu.fwdCamera]), ] def ford_asbuilt_block_request(block_id: int): return bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER]) + p16(DATA_IDENTIFIER_FORD_ASBUILT + block_id - 1) def ford_asbuilt_block_response(block_id: int): return bytes([uds.SERVICE_TYPE.READ_DATA_BY_IDENTIFIER + 0x40]) + p16(DATA_IDENTIFIER_FORD_ASBUILT + block_id - 1) FW_QUERY_CONFIG = FwQueryConfig( requests=[ # CAN and CAN FD queries are combined. # FIXME: For CAN FD, ECUs respond with frames larger than 8 bytes on the powertrain bus 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], 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], bus=0, auxiliary=True, ), *[Request( [StdQueries.TESTER_PRESENT_REQUEST, ford_asbuilt_block_request(block_id)], [StdQueries.TESTER_PRESENT_RESPONSE, ford_asbuilt_block_response(block_id)], whitelist_ecus=ecus, bus=0, logging=True, ) for block_id, ecus in ASBUILT_BLOCKS], ], extra_ecus=[ (Ecu.engine, 0x7e0, None), # Powertrain Control Module # 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 ], # Custom fuzzy fingerprinting function using platform and model year hints match_fw_to_car_fuzzy=match_fw_to_car_fuzzy, ) DBC = CAR.create_dbc_map()