#!/usr/bin/env python3 import unittest from typing import Dict, Iterable, Optional, Tuple import capnp from parameterized import parameterized from hypothesis import settings, given, strategies as st from cereal import car from openpilot.selfdrive.car.ford.values import FW_QUERY_CONFIG, 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) } ECU_FW_CORE = { Ecu.eps: [ b"14D003", ], Ecu.abs: [ b"2D053", ], Ecu.fwdRadar: [ b"14D049", ], Ecu.fwdCamera: [ b"14F397", # Ford Q3 b"14H102", # Ford Q4 ], Ecu.engine: [ b"14C204", ], } class TestFordFW(unittest.TestCase): def test_fw_query_config(self): for (ecu, addr, subaddr) in FW_QUERY_CONFIG.extra_ecus: self.assertIn(ecu, ECU_ADDRESSES, "Unknown ECU") self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch") self.assertIsNone(subaddr, "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, Optional[int]], Iterable[bytes]]): for (ecu, addr, subaddr), fws in fw_versions.items(): self.assertIn(ecu, ECU_ADDRESSES, "Unknown ECU") self.assertEqual(addr, ECU_ADDRESSES[ecu], "ECU address mismatch") self.assertIsNone(subaddr, "Unexpected ECU subaddress") # Software part number takes the form: PREFIX-CORE-SUFFIX # Prefix changes based on the family of part. It includes the model year # and likely the platform. # Core identifies the type of the item (e.g. 14D003 = PSCM, 14C204 = PCM). # Suffix specifies the version of the part. -AA would be followed by -AB. # Small increments in the suffix are usually compatible. # Details: https://forscan.org/forum/viewtopic.php?p=70008#p70008 for fw in fws: self.assertEqual(len(fw), 24, "Expected ECU response to be 24 bytes") # TODO: parse with regex, don't need detailed error message fw_parts = fw.rstrip(b'\x00').split(b'-') self.assertEqual(len(fw_parts), 3, "Expected FW to be in format: prefix-core-suffix") prefix, core, suffix = fw_parts self.assertEqual(len(prefix), 4, "Expected FW prefix to be 4 characters") self.assertIn(len(core), (5, 6), "Expected FW core to be 5-6 characters") self.assertIn(core, ECU_FW_CORE[ecu], f"Unexpected FW core for {ecu}") self.assertIn(len(suffix), (2, 3), "Expected FW suffix to be 2-3 characters") @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) if __name__ == "__main__": unittest.main()