|
|
|
#!/usr/bin/env python3
|
|
|
|
import random
|
|
|
|
from collections.abc import Iterable
|
|
|
|
|
|
|
|
import capnp
|
|
|
|
from hypothesis import settings, given, strategies as st
|
|
|
|
from parameterized import parameterized
|
|
|
|
|
|
|
|
from cereal import car
|
|
|
|
from openpilot.selfdrive.car.fw_versions import build_fw_dict
|
|
|
|
from openpilot.selfdrive.car.ford.values import CAR, FW_QUERY_CONFIG, FW_PATTERN, get_platform_codes
|
|
|
|
from openpilot.selfdrive.car.ford.fingerprints import FW_VERSIONS
|
|
|
|
|
|
|
|
Ecu = car.CarParams.Ecu
|
|
|
|
|
|
|
|
|
|
|
|
ECU_ADDRESSES = {
|
|
|
|
Ecu.eps: 0x730, # Power Steering Control Module (PSCM)
|
|
|
|
Ecu.abs: 0x760, # Anti-Lock Brake System (ABS)
|
|
|
|
Ecu.fwdRadar: 0x764, # Cruise Control Module (CCM)
|
|
|
|
Ecu.fwdCamera: 0x706, # Image Processing Module A (IPMA)
|
|
|
|
Ecu.engine: 0x7E0, # Powertrain Control Module (PCM)
|
|
|
|
Ecu.shiftByWire: 0x732, # Gear Shift Module (GSM)
|
Ford: log interesting module configuration data (#31569)
* Ford: log interesting module configuration data
Ford ECUs have what is called "As-Built Data" which is configured at the
factory/workshop to set what packages/features are enabled on the car.
But they also contain vehicle specific information (VIN, make, model,
weight, wheel base...), DTC information and driver preferences.
I dumped the CAN traffic for the FORScan diagnostic tool to see how it
requests this information from the ECUs.
<details>
<summary>FORScan communication with IPMA (camera)</summary>
<pre>
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': '0200'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'TESTER_PRESENT', 'hex': '00'}
{'addr': 1806, 'type': 'positive_response', 'service': 'TESTER_PRESENT', 'hex': '00'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f190'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de00'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de00020799dbaa10296516a440000000000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f113'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'data': b'\xf1\x13JX7T-19H406-CH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'hex': 'f1134a5837542d3139483430362d434800000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f188'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'data': b'\xf1\x88JX7T-14F397-AH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'hex': 'f1884a5837542d3134463339372d414800000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f120'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'data': b'\xf1 JX7T-14F397-BF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'hex': 'f1204a5837542d3134463339372d424600000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f121'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f124'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'data': b'\xf1$JX7T-14F398-AG\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'hex': 'f1244a5837542d3134463339382d414700000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f125'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'data': b'\xf1%JX7T-14F398-BF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'hex': 'f1254a5837542d3134463339382d424600000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f126'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f10a'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f111'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'data': b'\xf1\x11JX7T-14F403-CA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'hex': 'f1114a5837542d3134463430332d434100000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f18c'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'data': b'\xf1\x8c182762191\x00\x00\x00\x00\x00\x00\x00', 'hex': 'f18c31383237363231393100000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f162'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f110'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'data': b'\xf1\x10DS-JX7T-19H406-AD\x00\x00\x00\x00\x00\x00\x00', 'hex': 'f11044532d4a5837542d3139483430362d414400000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': '0202'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'd100'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'd10001'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'd700'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'd70001010101'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'd701'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'd70101020000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'dd01'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'dd010102f8'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'f113'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'data': b'\xf1\x13JX7T-19H406-CH\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', 'hex': 'f1134a5837542d3139483430362d434800000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'fd08'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'fd0800000500500100300000000000000000000000300000000000000000200100400300100200001200f00300500000000000000300c00b00400200000000000000000000000000000000200f01201e01501400a00200200400700d02501d01700700e06405005e05503401100a000000002002002001000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'fd09'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'fd09ffec0001fef60002'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de00'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de00020799dbaa10296516a440000000000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DTC_INFORMATION', 'hex': '028f'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DTC_INFORMATION', 'hex': '02ff50019768c253002cc401862cc418862c'}
... skip DTC requests ...
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de00'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de00020799dbaa10296516a440000000000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de01'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de01fd5616db5fffff557fe1f842080000000800000008000000080000000819bfe00f7c00000000000000000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de02'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de02800000008000000080000000800000008337fc00800000008000000080000000800000008337fc0000000000000000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de03'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de03fffc26c3800000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de04'}
{'addr': 1806, 'type': 'positive_response', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de04546a8c0000000000'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de05'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de06'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de07'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de08'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de09'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de0a'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de0b'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de0c'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de0d'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
{'addr': 1798, 'type': 'request', 'service': 'READ_DATA_BY_IDENTIFIER', 'hex': 'de0e'}
{'addr': 1806, 'type': 'negative_response', 'hex': '2231'}
</pre>
</details>
Using UDS service `READ_DATA_BY_IDENTIFIER`, we can read the As-Built
blocks from `0xDExx` with no diagnostic session/security access necessary.
I used [Online As-Built databases](https://cyanlabs.net/asbuilt-db/) and
various coding spreadsheets shared online to find values we might be
interested in using for fingerprinting (both vehicle parameters and
identifying the platform).
ABS:
- Payload tier (Base, Mid Payload Upgrade, Heavy Duty Payload
Upgrade...)
- Wheelbase
- Steering ratio
- Cruise Control Mode (Normal, Adaptive)
- Enable Stop and Go
PSCM:
- Enable Lane Keeping Aid
- Enable Traffic Jam Assist
- Enable Lane Centering Assist
IPMA (Q4):
- Steering ratio
- Wheelbase
APIM (Sync 3 and Sync 4):
- Steering ratio
- Vehicle weight
- Wheelbase
There are more potentially useful signals which I haven't included
although they might not be necessary:
- Vehicle (Ford platform code, like "C344" or "C519" - although the
source of the mapping from index to code is FORScan and not Ford
themselves unless we can find a better source).
- Fuel type
- Vehicle length/height/front track/rear track
- Tire circumference (could be useful for converting wheel speed rad/s
to m/s)
- Steering angle source (Pinion, Wheel)
- Country code (letters, e.g. US, CA or UK)
- Transmission type
- CAN network architecture
- More feature flags (the APIM also stores settings for ACC, LCA, BLIS)
The full list of settings I have found is
[here](https://github.com/incognitojam/op-notebooks/blob/main/ford/settings.py).
* FwQueryConfig: add data_requests
* add car_data to CarInterface get_params
* Revert "add car_data to CarInterface get_params"
This reverts commit aa161a6b82082705db97bea2c4317e1888a74845.
* test_ford: add APIM ecu address
* Revert "FwQueryConfig: add data_requests"
This reverts commit dc5484a9b80be5bc61a7fdf55560b8813bc43ef7.
* fix block numbers and add extra queries
* bump test_fw_query_timing
* add missing query whitelists
* simplify asbuilt requests
* use forscan block ids
* formatting
---------
Co-authored-by: Shane Smiskol <shane@smiskol.com>
1 year ago
|
|
|
Ecu.debug: 0x7D0, # Accessory Protocol Interface Module (APIM)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ECU_PART_NUMBER = {
|
|
|
|
Ecu.eps: [
|
|
|
|
b"14D003",
|
|
|
|
],
|
|
|
|
Ecu.abs: [
|
|
|
|
b"2D053",
|
|
|
|
],
|
|
|
|
Ecu.fwdRadar: [
|
|
|
|
b"14D049",
|
|
|
|
],
|
|
|
|
Ecu.fwdCamera: [
|
|
|
|
b"14F397", # Ford Q3
|
|
|
|
b"14H102", # Ford Q4
|
|
|
|
],
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class TestFordFW:
|
|
|
|
def test_fw_query_config(self):
|
|
|
|
for (ecu, addr, subaddr) in FW_QUERY_CONFIG.extra_ecus:
|
|
|
|
assert ecu in ECU_ADDRESSES, "Unknown ECU"
|
|
|
|
assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"
|
|
|
|
assert subaddr is None, "Unexpected ECU subaddress"
|
|
|
|
|
|
|
|
@parameterized.expand(FW_VERSIONS.items())
|
|
|
|
def test_fw_versions(self, car_model: str, fw_versions: dict[tuple[capnp.lib.capnp._EnumModule, int, int | None], Iterable[bytes]]):
|
|
|
|
for (ecu, addr, subaddr), fws in fw_versions.items():
|
|
|
|
assert ecu in ECU_PART_NUMBER, "Unexpected ECU"
|
|
|
|
assert addr == ECU_ADDRESSES[ecu], "ECU address mismatch"
|
|
|
|
assert subaddr is None, "Unexpected ECU subaddress"
|
|
|
|
|
|
|
|
for fw in fws:
|
|
|
|
assert len(fw) == 24, "Expected ECU response to be 24 bytes"
|
|
|
|
|
|
|
|
match = FW_PATTERN.match(fw)
|
|
|
|
assert match is not None, f"Unable to parse FW: {fw!r}"
|
|
|
|
if match:
|
|
|
|
part_number = match.group("part_number")
|
|
|
|
assert part_number in ECU_PART_NUMBER[ecu], f"Unexpected part number for {fw!r}"
|
|
|
|
|
|
|
|
codes = get_platform_codes([fw])
|
|
|
|
assert 1 == len(codes), f"Unable to parse FW: {fw!r}"
|
|
|
|
|
|
|
|
@settings(max_examples=100)
|
|
|
|
@given(data=st.data())
|
|
|
|
def test_platform_codes_fuzzy_fw(self, data):
|
|
|
|
"""Ensure function doesn't raise an exception"""
|
|
|
|
fw_strategy = st.lists(st.binary())
|
|
|
|
fws = data.draw(fw_strategy)
|
|
|
|
get_platform_codes(fws)
|
|
|
|
|
|
|
|
def test_platform_codes_spot_check(self):
|
|
|
|
# Asserts basic platform code parsing behavior for a few cases
|
|
|
|
results = get_platform_codes([
|
|
|
|
b"JX6A-14C204-BPL\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
b"NZ6T-14F397-AC\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
b"PJ6T-14H102-ABJ\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
b"LB5A-14C204-EAC\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
])
|
|
|
|
assert results == {(b"X6A", b"J"), (b"Z6T", b"N"), (b"J6T", b"P"), (b"B5A", b"L")}
|
|
|
|
|
|
|
|
def test_fuzzy_match(self):
|
|
|
|
for platform, fw_by_addr in FW_VERSIONS.items():
|
|
|
|
# Ensure there's no overlaps in platform codes
|
|
|
|
for _ in range(20):
|
|
|
|
car_fw = []
|
|
|
|
for ecu, fw_versions in fw_by_addr.items():
|
|
|
|
ecu_name, addr, sub_addr = ecu
|
|
|
|
fw = random.choice(fw_versions)
|
|
|
|
car_fw.append({"ecu": ecu_name, "fwVersion": fw, "address": addr,
|
|
|
|
"subAddress": 0 if sub_addr is None else sub_addr})
|
|
|
|
|
|
|
|
CP = car.CarParams.new_message(carFw=car_fw)
|
|
|
|
matches = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(build_fw_dict(CP.carFw), CP.carVin, FW_VERSIONS)
|
|
|
|
assert matches == {platform}
|
|
|
|
|
|
|
|
def test_match_fw_fuzzy(self):
|
|
|
|
offline_fw = {
|
|
|
|
(Ecu.eps, 0x730, None): [
|
|
|
|
b"L1MC-14D003-AJ\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
b"L1MC-14D003-AL\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
],
|
|
|
|
(Ecu.abs, 0x760, None): [
|
|
|
|
b"L1MC-2D053-BA\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
b"L1MC-2D053-BD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
],
|
|
|
|
(Ecu.fwdRadar, 0x764, None): [
|
|
|
|
b"LB5T-14D049-AB\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
b"LB5T-14D049-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
],
|
|
|
|
# We consider all model year hints for ECU, even with different platform codes
|
|
|
|
(Ecu.fwdCamera, 0x706, None): [
|
|
|
|
b"LB5T-14F397-AD\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
b"NC5T-14F397-AF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
|
|
],
|
|
|
|
}
|
|
|
|
expected_fingerprint = CAR.FORD_EXPLORER_MK6
|
|
|
|
|
|
|
|
# ensure that we fuzzy match on all non-exact FW with changed revisions
|
|
|
|
live_fw = {
|
|
|
|
(0x730, None): {b"L1MC-14D003-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
|
|
|
|
(0x760, None): {b"L1MC-2D053-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
|
|
|
|
(0x764, None): {b"LB5T-14D049-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
|
|
|
|
(0x706, None): {b"LB5T-14F397-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"},
|
|
|
|
}
|
|
|
|
candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw})
|
|
|
|
assert candidates == {expected_fingerprint}
|
|
|
|
|
|
|
|
# model year hint in between the range should match
|
|
|
|
live_fw[(0x706, None)] = {b"MB5T-14F397-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"}
|
|
|
|
candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw,})
|
|
|
|
assert candidates == {expected_fingerprint}
|
|
|
|
|
|
|
|
# unseen model year hint should not match
|
|
|
|
live_fw[(0x760, None)] = {b"M1MC-2D053-XX\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"}
|
|
|
|
candidates = FW_QUERY_CONFIG.match_fw_to_car_fuzzy(live_fw, '', {expected_fingerprint: offline_fw})
|
|
|
|
assert len(candidates) == 0, "Should not match new model year hint"
|