Keep automatic Ford discovery on the normal CAN harness

This commit is contained in:
firestar5683
2026-10-25 22:55:54 -05:00
parent eb2b6c2d23
commit 822dbe692f
4 changed files with 167 additions and 5 deletions
+6 -4
View File
@@ -11,7 +11,8 @@ from opendbc.car.fw_versions import (ObdCallback, get_fw_versions, get_fw_versio
from opendbc.car.mock.values import CAR as MOCK
from opendbc.car.gm.pedal_capability import automatic_bolt_candidate
from opendbc.car.gm.discovery import automatic_volt_candidate
from opendbc.car.ford.discovery import normal_bus0_selection as ford_normal_bus0_selection, normal_bus0_queries as ford_normal_bus0_queries
from opendbc.car.ford.discovery import (normal_bus0_selection as ford_normal_bus0_selection,
normal_bus0_queries as ford_normal_bus0_queries, normal_bus_query_brand)
from opendbc.car.hyundai.discovery import normal_bus_topology
from opendbc.car.values import BRANDS
from opendbc.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN
@@ -143,15 +144,16 @@ def fingerprint(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_mu
car_fw = get_fw_versions(can_recv, can_send, set_obd_multiplexing,
query_brand='ford', normal_bus_only=True)
ecu_rx_addrs = {(fw.responseAddress, fw.subAddress or None, fw.bus) for fw in car_fw if fw.responseAddress > 0}
elif normal_bus_topology(can_recv):
elif (normal_brand := normal_bus_query_brand(can_recv, normal_bus_topology)) is not None:
set_obd_multiplexing(False)
warmup_remaining = 0.1 - (time.monotonic() - start_time)
if warmup_remaining > 0:
time.sleep(warmup_remaining)
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, (0, 1))
vin_buses = (0,) if normal_brand == 'ford' else (0, 1)
vin_rx_addr, vin_rx_bus, vin = get_vin(can_recv, can_send, vin_buses)
car_fw = get_fw_versions(can_recv, can_send, set_obd_multiplexing,
query_brand='hyundai', normal_bus_only=True)
query_brand=normal_brand, normal_bus_only=True)
ecu_rx_addrs = {(fw.responseAddress, fw.subAddress or None, fw.bus) for fw in car_fw if fw.responseAddress > 0}
else:
# enable OBD multiplexing for VIN query
@@ -1,4 +1,6 @@
"""Normal-bus discovery for explicitly selected Ford CAN-FD vehicles."""
import time
from opendbc.car.ford.values import CAR, FordFlags
@@ -34,3 +36,42 @@ def normal_bus0_queries():
def normal_bus0_selection(candidate):
return bool(candidate in CAR and CAR(candidate).config.flags & FordFlags.CANFD and
not CAR(candidate).config.flags & FordFlags.LKA_STEERING and normal_bus0_queries())
# Normal harness PT/chassis and stock-camera messages. This selects only a
# diagnostic query family; live VIN, ECU responses and fingerprinting still
# determine the vehicle and its control configuration.
NORMAL_BUS0_LAYOUT = {(0, 0x82): 8, (0, 0x165): 8, (0, 0x204): 8,
(0, 0x217): 8, (2, 0x3CA): 8, (2, 0x3D6): 8}
def normal_bus_query_brand(can_recv, hyundai_probe, *, clock=time.monotonic):
# Share the existing bounded passive HKG read, rather than consuming a second
# startup window or enabling the OBD mux to find out whether this is a Ford.
received = {}
malformed = False
reads = 0
deadline = clock() + .12
def observe(**kwargs):
nonlocal malformed, reads
reads += 1
packets = can_recv(**kwargs)
for packet in packets:
for frame in packet:
key = (frame.src, frame.address)
if key in NORMAL_BUS0_LAYOUT:
length = len(frame.dat)
malformed |= length != NORMAL_BUS0_LAYOUT[key]
received[key] = length
return packets
if hyundai_probe(observe):
return 'hyundai'
# A Ford CAN1 frame can invalidate an HKG-only layout on the first read.
# Complete Ford evidence within the original time/read budget, not a new probe.
while not malformed and received != NORMAL_BUS0_LAYOUT and reads < 24 and clock() < deadline:
observe(wait_for_one=True)
if not malformed and received == NORMAL_BUS0_LAYOUT and normal_bus0_queries():
return 'ford'
return None
@@ -0,0 +1,119 @@
"""Cold automatic Ford discovery must not put CAN2 on the unrelated OBD bus."""
from unittest import mock
import pytest
from opendbc.car import car_helpers, structs
from opendbc.car.can_definitions import CanData
from opendbc.car.ford.discovery import normal_bus_query_brand
from opendbc.car.ford.values import CAR
from opendbc.car.hyundai.discovery import NORMAL_BUS_LAYOUT, normal_bus_topology
from openpilot.selfdrive.car.tests.test_registered_ford_discovery import FordEcus, VIN
# Six original bus/address/DLC/payload samples from bfef.../00000003.
RECORDED = (
(0, 130, '770814008b004000'),
(0, 357, '50c0000000000000'),
(0, 516, 'd1267d0000d10000'),
(0, 535, '268f268b26a226aa'),
(2, 970, '1e7d17f200000000'),
(2, 982, '051f7b8f9a067ffc'),
)
def recorded_frames():
return [CanData(address, bytes.fromhex(data), bus) for bus, address, data in RECORDED]
def brand(frames):
return normal_bus_query_brand(lambda **kwargs: [frames], normal_bus_topology)
def test_recorded_normal_harness_selects_ford_queries_only():
assert brand(recorded_frames()) == 'ford'
@pytest.mark.parametrize('missing', range(6))
def test_partial_layout_does_not_choose_ford(missing):
frames = recorded_frames()
del frames[missing]
assert brand(frames) is None
@pytest.mark.parametrize('bad', range(6))
def test_wrong_bus_dlc_and_returned_frames_do_not_choose_ford(bad):
for bus_delta, size in ((1, 8), (128, 8), (0, 7), (0, 12)):
frames = recorded_frames()
original = frames[bad]
frames[bad] = CanData(original.address, bytes(size), original.src + bus_delta)
assert brand(frames) is None
def test_conflicting_dlc_cannot_be_overwritten_by_a_good_copy():
frames = recorded_frames()
assert brand([CanData(frames[0].address, bytes(7), frames[0].src), *frames]) is None
def test_existing_hyundai_and_unknown_layouts_keep_their_paths():
frames = [CanData(address, bytes(length), bus) for (bus, address), length in NORMAL_BUS_LAYOUT.items()]
assert brand(frames) == 'hyundai'
assert brand([]) is None
assert brand([*frames, *recorded_frames()]) == 'hyundai'
def test_query_configuration_must_still_allow_normal_ford_bus():
with mock.patch('opendbc.car.ford.discovery.normal_bus0_queries', return_value=False):
assert brand(recorded_frames()) is None
def test_shared_probe_uses_existing_read_bound():
calls = []
def recv(**kwargs):
calls.append(kwargs)
return []
assert normal_bus_query_brand(recv, lambda recv: normal_bus_topology(recv, clock=lambda: 1.)) is None
assert len(calls) == 24
assert all(call == {'wait_for_one': True} for call in calls)
@pytest.mark.parametrize('firmware_known', [False, True])
def test_cold_automatic_uses_real_vin_isotp_fw_and_identity_matching(firmware_known):
firmware = {(0x706, b'\x22\xf1\x88'): b'RJ6T-14H102-BAE'.ljust(24, b'\0'),
(0x730, b'\x22\xf1\x88'): b'LJ9C-14D003-FA'.ljust(24, b'\0'),
(0x760, b'\x22\xf1\x88'): b'LK9C-2D053-CN'.ljust(24, b'\0'),
(0x764, b'\x22\xf1\x88'): b'ML3T-14D049-AL'.ljust(24, b'\0')}
if not firmware_known:
firmware = dict.fromkeys(firmware, b'unknown')
transport = FordEcus(firmware=firmware, background=recorded_frames())
with mock.patch.dict(car_helpers.os.environ, {'FINGERPRINT': '', 'SKIP_FW_QUERY': '', 'DISABLE_FW_CACHE': ''}), \
mock.patch.object(car_helpers.time, 'monotonic', side_effect=lambda: transport.clock), \
mock.patch.object(car_helpers.time, 'sleep', side_effect=transport.sleep):
ci = car_helpers.get_car(transport.recv, transport.send, transport.mux, False, False)
assert transport.mux_calls and set(transport.mux_calls) == {False}
assert transport.sent and all(frame.src in (0, 4) for frame in transport.sent)
assert any(frame.dat[0] >> 4 == 3 for frame in transport.sent)
if firmware_known:
assert ci.CP.carFingerprint == CAR.FORD_MUSTANG_MACH_E_MK1
assert ci.CP.fingerprintSource == structs.CarParams.FingerprintSource.fw
assert ci.CP.carVin == VIN
assert {(fw.address, fw.fwVersion) for fw in ci.CP.carFw} == {(address, data) for (address, _), data in firmware.items()}
else:
assert ci.CP.carFingerprint != CAR.FORD_MUSTANG_MACH_E_MK1
assert not ci.CP.safetyConfigs or ci.CP.safetyConfigs[-1].safetyModel != structs.CarParams.SafetyModel.ford
def test_hyundai_mismatch_does_not_abort_remaining_ford_window():
packets = iter([[CanData(0x1A0, bytes(8), 1)], recorded_frames()])
def recv(**kwargs):
return [next(packets, [])]
assert normal_bus_query_brand(recv, normal_bus_topology) == 'ford'
def test_hyundai_early_exit_does_not_extend_outer_deadline():
calls = []
times = iter((1., 1.13))
def recv(**kwargs):
calls.append(kwargs)
return []
assert normal_bus_query_brand(recv, lambda recv: False, clock=lambda: next(times)) is None
assert not calls
+1 -1
View File
@@ -1 +1 @@
{"protected_inventory_sha256": "d97d73289304727972ae534f32902cf5d207ea61af92426673df8a2fd4b6ed60", "schema_version": 1}
{"protected_inventory_sha256": "10eb839743b1c431389b7e65d78621ddc6794ba3d1b158b77d542b571423727f", "schema_version": 1}