From 0672caa7cf58d701a804fc2b05a5976113aae07c Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Mon, 7 Sep 2026 14:07:36 -0400 Subject: [PATCH 01/13] mazda: add the 2012-16 CX-5 KE platform The KE chassis (2012-16 CX-5, incl. the 2016.5 mid-year refresh) with its stock G46L radar. The platform claims no radar bus: the G46L does not put the 0x361-0x366 tracks on bus 0, so the lead comes from vision and the radar DBC stays unset. EPS swaps are common on these bodies (the 2022 CX-5 EPS is what unlocks steer-to-zero), so the FW_VERSIONS entry carries no EPS firmware on purpose: swap firmware belongs to MAZDA_CX5_2022 and STEER_TO_ZERO_EPS_FW, not the chassis. --- opendbc/car/mazda/fingerprints.py | 17 +++++++++++++++++ opendbc/car/mazda/values.py | 7 +++++++ opendbc/car/tests/routes.py | 3 +++ opendbc/car/torque_data/substitute.toml | 1 + 4 files changed, 28 insertions(+) diff --git a/opendbc/car/mazda/fingerprints.py b/opendbc/car/mazda/fingerprints.py index d2af0dda7ec..cf55dcde383 100644 --- a/opendbc/car/mazda/fingerprints.py +++ b/opendbc/car/mazda/fingerprints.py @@ -67,6 +67,23 @@ b'SH51-21PS1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', ], }, + CAR.MAZDA_CX5_KE: { + (Ecu.engine, 0x7e0, None): [ + b'PYAS-188K2-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdRadar, 0x764, None): [ + b'G46L-67XA1-C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.abs, 0x760, None): [ + b'KA0G-437AS-0-03\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.fwdCamera, 0x706, None): [ + b'GMG6-67XK2-G\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + (Ecu.transmission, 0x7e1, None): [ + b'PYAS-21PS1-D\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00', + ], + }, CAR.MAZDA_CX5: { (Ecu.eps, 0x730, None): [ b'K319-3210X-A-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py index b38cf56dcd8..af79eb67405 100644 --- a/opendbc/car/mazda/values.py +++ b/opendbc/car/mazda/values.py @@ -197,6 +197,13 @@ class MazdaPlatformConfig(PlatformConfig): class CAR(Platforms): + MAZDA_CX5_KE = MazdaPlatformConfig( + [MazdaCarDocs("Mazda CX-5 2012-16")], + MazdaCarSpecs(mass=3561 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=15.5), + # This radar does not publish 0x361-0x366 tracks on bus 0. + dbc_dict={Bus.pt: 'mazda_2017'}, + wmis={WMI.JAPAN_CROSSOVER}, chassis_codes={'KE'}, years={'C', 'D', 'E', 'F', 'G'}, # 2012-16 + ) MAZDA_CX5 = MazdaPlatformConfig( [MazdaCarDocs("Mazda CX-5 2017-21")], MazdaCarSpecs(mass=3655 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=15.5), diff --git a/opendbc/car/tests/routes.py b/opendbc/car/tests/routes.py index 91d19a10f23..9ec9f012c49 100644 --- a/opendbc/car/tests/routes.py +++ b/opendbc/car/tests/routes.py @@ -54,6 +54,9 @@ GM.CHEVROLET_TRAILBLAZER_NON_ACC_2ND_GEN, GM.CHEVROLET_MALIBU_NON_ACC_9TH_GEN, GM.CADILLAC_XT5_NON_ACC_1ST_GEN, + + # no public route yet, the 2016.5 swap car is the only known device-validated KE + MAZDA.MAZDA_CX5_KE, ] diff --git a/opendbc/car/torque_data/substitute.toml b/opendbc/car/torque_data/substitute.toml index 945ab359688..1b217568e52 100644 --- a/opendbc/car/torque_data/substitute.toml +++ b/opendbc/car/torque_data/substitute.toml @@ -3,6 +3,7 @@ legend = ["LAT_ACCEL_FACTOR", "MAX_LAT_ACCEL_MEASURED", "FRICTION"] "MAZDA_6" = "MAZDA_CX9_2021" "MAZDA_CX5" = "MAZDA_CX9_2021" "MAZDA_CX5_2022" = "MAZDA_CX9_2021" +"MAZDA_CX5_KE" = "MAZDA_CX9_2021" "MAZDA_CX9" = "MAZDA_CX9_2021" "DODGE_DURANGO" = "CHRYSLER_PACIFICA_2020" From a8b54937beb9c72f50a492cec2aa0e44ba523d12 Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Sun, 6 Sep 2026 00:33:42 -0400 Subject: [PATCH 02/13] mazda: tests for the KE platform VIN years C-G name the platform, the 2016.5 firmware set exact-matches on the body ECUs alone, and dealer-updated ECUs still name by VIN through the donor EPS. The platform runs vision-only under a swap and never offers alpha long; docs build without firmware. --- .../car/mazda/tests/test_mazda_interface.py | 17 +++++++++- opendbc/car/mazda/tests/test_mazda_vin.py | 33 ++++++++++++++++++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/opendbc/car/mazda/tests/test_mazda_interface.py b/opendbc/car/mazda/tests/test_mazda_interface.py index 8fce8512365..2d4abefede1 100644 --- a/opendbc/car/mazda/tests/test_mazda_interface.py +++ b/opendbc/car/mazda/tests/test_mazda_interface.py @@ -103,6 +103,21 @@ def test_alpha_long_follows_the_eps(self, candidate, swapped): def test_stock_long_still_reads_the_radar_tracks(self): assert not car_params(CAR.MAZDA_CX9_2021, alpha_long=True).radarUnavailable + def test_ke_runs_vision_only_under_a_swapped_eps(self): + # the first-generation radar speaks no track dialect, so the platform promises no + # radar bus: the lead comes from the model and no teardown is offered, while the + # EPS swap still lifts the steering lockouts + stock = car_params(CAR.MAZDA_CX5_KE) + assert stock.radarUnavailable + assert stock.dashcamOnly + + swapped = car_params(CAR.MAZDA_CX5_KE, car_fw=eps_fw(SWAPPED_EPS_FW)) + assert swapped.radarUnavailable + assert not swapped.dashcamOnly + assert swapped.minSteerSpeed == 0 + assert swapped.steerActuatorDelay == pytest.approx(0.14, abs=5e-8) + assert not swapped.alphaLongitudinalAvailable + @pytest.mark.parametrize("candidate, car_fw, alpha_long, expected", [ (CAR.MAZDA_CX5_2022, None, False, True), (CAR.MAZDA_CX5_2022, None, True, True), @@ -124,7 +139,7 @@ def test_safety_param_follows_the_eps(self, candidate, car_fw, alpha_long, expec # longitudinal keeps its own bit assert bool(CP.safetyConfigs[0].safetyParam & MazdaSafetyFlags.LONG.value) == CP.openpilotLongitudinalControl - @pytest.mark.parametrize("candidate", [CAR.MAZDA_CX5, CAR.MAZDA_CX9, CAR.MAZDA_3, CAR.MAZDA_6]) + @pytest.mark.parametrize("candidate", [CAR.MAZDA_CX5_KE, CAR.MAZDA_CX5, CAR.MAZDA_CX9, CAR.MAZDA_3, CAR.MAZDA_6]) def test_docs_are_generated_without_firmware(self, candidate): # car_fw is empty when building CARS.md, so the docs must keep advertising dashcam mode from opendbc.car import gen_empty_fingerprint diff --git a/opendbc/car/mazda/tests/test_mazda_vin.py b/opendbc/car/mazda/tests/test_mazda_vin.py index e36fa3459ed..d4cfee735cd 100644 --- a/opendbc/car/mazda/tests/test_mazda_vin.py +++ b/opendbc/car/mazda/tests/test_mazda_vin.py @@ -72,7 +72,7 @@ def test_wrong_wmi_does_not_match(self): @pytest.mark.parametrize("wmi, chassis_code, year_code", [ ('JM1', 'BP', 'K'), # Mazda 3 2019+ ('JM3', 'DM', 'N'), # CX-30 - ('JM3', 'KE', 'H'), # pre-2017 CX-5 + ('JM3', 'KE', 'H'), # a CX-5 KE past the last supported model year ('7MM', 'VA', 'P'), # CX-50 ('JM3', 'TC', 'T'), # a CX-9 past the last supported model year ]) @@ -96,6 +96,11 @@ def test_every_vin_field_is_required(self): assert match_fw_to_car_fuzzy({}, make_vin('JM3', 'KF', 'G'), FW_VERSIONS) == set() # 2016 KF predates the port assert match_fw_to_car_fuzzy({}, make_vin('JM3', 'KF', 'N'), FW_VERSIONS) == {str(CAR.MAZDA_CX5_2022)} + @pytest.mark.parametrize("year_code", ['C', 'D', 'E', 'F', 'G']) + def test_ke_model_years_name_the_ke_platform(self, year_code): + # the first-generation CX-5, one platform across its whole 2012-16 run + assert match_fw_to_car_fuzzy({}, make_vin('JM3', 'KE', year_code), FW_VERSIONS) == {str(CAR.MAZDA_CX5_KE)} + def test_engine_firmware_alone_is_not_evidence_without_a_decodable_vin(self): # an Oceania export VIN (real report): no model year, no known WMI; the # engine is the only responding firmware in the database. One recognised @@ -217,6 +222,18 @@ def test_stock_fw_set_still_exact_matches(self): assert exact_match assert matches == {str(CAR.MAZDA_CX9_2021)} + def test_reported_ke_exact_matches_on_its_body_ecus(self): + # the 2016.5 report: a 2022 CX-5 EPS swap riding on first-generation body ECUs. + # The swap firmware stays under MAZDA_CX5_2022; the body ECUs alone exact-match + # the chassis, and no EPS entry exists to contradict the swap + donor_eps = FW_VERSIONS[CAR.MAZDA_CX5_2022][(Ecu.eps, 0x730, None)][1] # KSD5, the reported swap + car_fw = [_car_fw(ecu, addr, versions[0]) + for (ecu, addr, _), versions in FW_VERSIONS[CAR.MAZDA_CX5_KE].items()] + car_fw.append(_car_fw(Ecu.eps, 0x730, donor_eps)) + exact_match, matches = match_fw_to_car(car_fw, make_vin('JM3', 'KE', 'G')) + assert exact_match + assert matches == {str(CAR.MAZDA_CX5_KE)} + def test_oceania_eps_swap_matches_on_the_engine_behind_the_donor_eps(self): # the reported car: Oceania VIN (never decodes), donor EPS, and chassis ECUs # unknown to the North American database. Two recognised ECUs: the engine @@ -291,3 +308,17 @@ def test_known_vins_still_match_through_the_real_matcher(self, vin, expected): else: assert not exact_match assert matches == {str(expected)}, vin + + def test_ke_names_by_vin_through_a_donor_eps(self): + # a KE with dealer-updated body ECUs still names by VIN through the swap; the + # donor EPS is the one ECU the database knows + donor_eps = FW_VERSIONS[CAR.MAZDA_CX5_2022][(Ecu.eps, 0x730, None)][0] + car_fw = [ + _car_fw(Ecu.eps, 0x730, donor_eps), + _car_fw(Ecu.engine, 0x7e0, UNKNOWN_ENGINE_FW), + _car_fw(Ecu.abs, 0x760, UNKNOWN_ABS_FW), + _car_fw(Ecu.transmission, 0x7e1, UNKNOWN_TRANS_FW), + ] + exact_match, matches = match_fw_to_car(car_fw, make_vin('JM3', 'KE', 'G')) + assert not exact_match + assert matches == {str(CAR.MAZDA_CX5_KE)} From c237496e56dfc09e0505a9ca5e3310dfc95e4e53 Mon Sep 17 00:00:00 2001 From: mzdnick Date: Sun, 6 Sep 2026 01:16:11 -0400 Subject: [PATCH 03/13] mazda: correct the CX-5 steer ratio and KE mass The pre-2022 CX-5 racks run the 2022's 18.1 ratio (same hardware), and the KE platform mass is 3433 lb from the platform-table research. --- opendbc/car/mazda/values.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py index af79eb67405..58c33561629 100644 --- a/opendbc/car/mazda/values.py +++ b/opendbc/car/mazda/values.py @@ -199,14 +199,14 @@ class MazdaPlatformConfig(PlatformConfig): class CAR(Platforms): MAZDA_CX5_KE = MazdaPlatformConfig( [MazdaCarDocs("Mazda CX-5 2012-16")], - MazdaCarSpecs(mass=3561 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=15.5), + MazdaCarSpecs(mass=3433 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=18.1), #steerRatio copied from 22 CX-5, same hardware # This radar does not publish 0x361-0x366 tracks on bus 0. dbc_dict={Bus.pt: 'mazda_2017'}, wmis={WMI.JAPAN_CROSSOVER}, chassis_codes={'KE'}, years={'C', 'D', 'E', 'F', 'G'}, # 2012-16 ) MAZDA_CX5 = MazdaPlatformConfig( [MazdaCarDocs("Mazda CX-5 2017-21")], - MazdaCarSpecs(mass=3655 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=15.5), + MazdaCarSpecs(mass=3655 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=18.1), #steerRatio copied from 22 CX-5, same hardware wmis={WMI.JAPAN_CROSSOVER}, chassis_codes={'KF'}, years={'H', 'J', 'K', 'L', 'M'}, # 2017-21 ) MAZDA_CX9 = MazdaPlatformConfig( From b85698b5f61d79c205e4a28d36ea45798867621c Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Sun, 6 Sep 2026 01:20:21 -0400 Subject: [PATCH 04/13] mazda: add CX-5 KE speed-dep torque seeds from the 2022 table The CX-9 2021 entry is the pattern: seeds learned under a donor EPS apply only when the interface detects it, and requires_steer_to_zero withholds them on the stock EPS's flat 800-count scale. The KE table copies the CX-5 2022 values unchanged, since the donor EPS is the EPS those bins were learned under and no KE has its own fit yet; seed_version mirrors the source table. --- opendbc/car/torque_data/speed_dependent.toml | 11 ++++++++ .../car/tests/test_speed_dep_config.py | 27 ++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/opendbc/car/torque_data/speed_dependent.toml b/opendbc/car/torque_data/speed_dependent.toml index 22086546c7a..e4ad46799b5 100644 --- a/opendbc/car/torque_data/speed_dependent.toml +++ b/opendbc/car/torque_data/speed_dependent.toml @@ -22,3 +22,14 @@ seed_version = 1 speed_bp = [6.5, 9.5, 12.0, 16.4, 21.0, 28.0, 35.0] laf_bp = [2.67, 2.70, 2.56, 1.53, 1.28, 1.72, 1.97] friction_bp = [0.161, 0.154, 0.116, 0.163, 0.136, 0.128, 0.108] + +[MAZDA_CX5_KE] +# Apply these values only to a KE with a detected CX-5 2022 donor EPS. The stock EPS +# uses a flat 800-count scale and a 45 kph steering floor. +requires_steer_to_zero = true +# The CX-5 2022 table copied unchanged: the donor EPS is the EPS those bins were learned +# under, and no KE has its own fit yet. seed_version mirrors the source table. +speed_bp = [6.5, 9.5, 12.0, 16.4, 21.0, 28.0, 35.0] +laf_bp = [2.42, 2.86, 2.29, 1.21, 1.22, 1.53, 1.76] +friction_bp = [0.160, 0.143, 0.124, 0.145, 0.133, 0.122, 0.108] +seed_version = 1 diff --git a/opendbc/sunnypilot/car/tests/test_speed_dep_config.py b/opendbc/sunnypilot/car/tests/test_speed_dep_config.py index a50b5712e99..2421499fe64 100644 --- a/opendbc/sunnypilot/car/tests/test_speed_dep_config.py +++ b/opendbc/sunnypilot/car/tests/test_speed_dep_config.py @@ -8,7 +8,7 @@ from opendbc.car import gen_empty_fingerprint from opendbc.car.mazda.interface import CarInterface -from opendbc.car.mazda.values import CAR, MazdaFlags +from opendbc.car.mazda.values import CAR, STEER_TO_ZERO_EPS_FW, MazdaFlags from opendbc.car.structs import CarParams from opendbc.sunnypilot.car.interfaces import (get_speed_dep_config, get_speed_dep_config_for_car, get_steer_max_schedule, get_steer_rail_schedule, get_steer_slew_schedule) @@ -23,6 +23,18 @@ def cx5_2022_cp() -> CarParams: return cp +def ke_swapped_cp() -> CarParams: + # the 2016.5 report: a KE body with the 2022 CX-5 EPS the seed bins were learned under + fw = CarParams.CarFw() + fw.ecu = CarParams.Ecu.eps + fw.address = 0x730 + fw.subAddress = 0 + fw.fwVersion = sorted(STEER_TO_ZERO_EPS_FW)[0] + cp = CarInterface.get_params(CAR.MAZDA_CX5_KE, gen_empty_fingerprint(), [fw], alpha_long=False, is_release=False, docs=False) + assert cp.flags & MazdaFlags.STEER_TO_ZERO_EPS and cp.minSteerSpeed == 0.0 + return cp + + def brand_cp(brand: str, fingerprint: str = "", min_steer_speed: float = 0.0) -> CarParams: # a minimal CarParams for the brands the schedule lookup has to decline cp = CarParams() @@ -84,6 +96,19 @@ def test_flagged_entry_stays_empty_behind_a_floor(self, monkeypatch): monkeypatch.setattr(mod, 'get_speed_dep_config', lambda: {'MAZDA_CX9_2021': {'requires_steer_to_zero': True, 'speed_bp': [30.0]}}) assert get_speed_dep_config_for_car(brand_cp(**STOCK_MAZDA)) == {} + def test_ke_seeds_apply_under_the_donor_eps(self): + # the tables start identical: the donor EPS is the EPS the CX-5 2022 bins were + # learned under, and the swap carries the same STEER_MAX schedule with them + ke = get_speed_dep_config_for_car(ke_swapped_cp()) + cx5_2022 = get_speed_dep_config_for_car(cx5_2022_cp()) + for key in ('speed_bp', 'laf_bp', 'friction_bp', 'steer_max_schedule'): + assert ke[key] == cx5_2022[key], key + + def test_ke_seeds_withheld_on_the_stock_eps(self): + # a stock-EPS KE keeps the flat 800-count scale, so the entry must not apply + cp = brand_cp(brand="mazda", fingerprint=str(CAR.MAZDA_CX5_KE), min_steer_speed=20.0) + assert get_speed_dep_config_for_car(cp) == {} + def test_config_copy_not_cached_dict(self): a = get_speed_dep_config_for_car(cx5_2022_cp()) a['steer_max_schedule'] = 'mutated' From 8d60cd3715a422968abb62e943c12741a52f1252 Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Sun, 6 Sep 2026 01:23:49 -0400 Subject: [PATCH 05/13] mazda: keep the spec test on mass and stiffness The pre-2022 CX-5 racks now run the 2022's 18.1 steerRatio, so the ratio assertion no longer separates the platforms; mass and tireStiffnessFactor do. --- opendbc/car/mazda/tests/test_mazda_interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc/car/mazda/tests/test_mazda_interface.py b/opendbc/car/mazda/tests/test_mazda_interface.py index 2d4abefede1..e6ec651f7a1 100644 --- a/opendbc/car/mazda/tests/test_mazda_interface.py +++ b/opendbc/car/mazda/tests/test_mazda_interface.py @@ -64,11 +64,11 @@ def test_swapped_eps_unlocks_longitudinal(self): assert not car_params(CAR.MAZDA_CX5, alpha_long=True).alphaLongitudinalAvailable def test_swapped_eps_keeps_the_real_vehicle_specs(self): - # EPS detection must not replace the chassis-specific physical parameters. + # EPS detection must not replace the chassis-specific physical parameters. steerRatio + # no longer separates the platforms: the pre-2022 racks run the 2022's 18.1 too. swapped = car_params(CAR.MAZDA_CX5, car_fw=eps_fw(SWAPPED_EPS_FW)) cx5_2022 = car_params(CAR.MAZDA_CX5_2022) assert swapped.mass != cx5_2022.mass - assert swapped.steerRatio != cx5_2022.steerRatio assert swapped.tireStiffnessFactor != cx5_2022.tireStiffnessFactor def test_supported_platforms_are_unchanged(self): From 2fe5bbf116014243950fac63474a951bed5030a9 Mon Sep 17 00:00:00 2001 From: Nick Date: Sun, 6 Sep 2026 17:54:40 -0400 Subject: [PATCH 06/13] mazda: foreign-radar vision-only, G46L teardown dialect, VIN platform check A talking radar whose firmware no platform lists sends no tracks we can parse. Behind a platform bundle that claims a radar bus (an EPS-swapped older body carrying a forward bundle), radard starved on radarTracks and selfdrived raised commIssue for the whole drive. - radarUnavailable: platform claim OR foreign radar firmware; a silent radar keeps the claim - alpha availability decoupled from radarUnavailable: offered where the platform radar speaks the 2022 family dialect or the detected radar is the G46L whose replay exists - port the G46L teardown dialect from mazda-alpha-long-ke: flag, firmware set, static-only radar frames, safety capture - warn when a fixed platform bundle contradicts the VIN's platform (platform_from_vin over platform VIN metadata) --- opendbc/car/car_helpers.py | 12 ++- opendbc/car/mazda/carcontroller.py | 4 +- opendbc/car/mazda/interface.py | 37 ++++++++- opendbc/car/mazda/mazdacan.py | 9 ++- .../car/mazda/tests/test_mazda_interface.py | 81 ++++++++++++++++++- .../car/mazda/tests/test_mazda_mazdacan.py | 7 ++ opendbc/car/mazda/tests/test_mazda_vin.py | 14 ++++ opendbc/car/mazda/values.py | 14 ++++ opendbc/car/values.py | 30 +++++++ opendbc/safety/modes/mazda.h | 14 +++- opendbc/safety/tests/test_mazda.py | 12 +++ 11 files changed, 222 insertions(+), 12 deletions(-) diff --git a/opendbc/car/car_helpers.py b/opendbc/car/car_helpers.py index c1e8054e35a..dcb12ecea44 100644 --- a/opendbc/car/car_helpers.py +++ b/opendbc/car/car_helpers.py @@ -8,7 +8,7 @@ from opendbc.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars from opendbc.car.fw_versions import ObdCallback, get_fw_versions_ordered, get_present_ecus, match_fw_to_car from opendbc.car.mock.values import CAR as MOCK -from opendbc.car.values import BRANDS +from opendbc.car.values import BRANDS, platform_from_vin from opendbc.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN from opendbc.sunnypilot.car.interfaces import setup_interfaces as sunnypilot_interfaces @@ -167,6 +167,16 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip CP.carFw = car_fw CP.fingerprintSource = source CP.fuzzyFingerprint = not exact_match + + if fixed_fingerprint and vin != VIN_UNKNOWN: + # A carried-forward CarPlatformBundle can disagree with the physical car after a + # hardware swap or a branch switch without reinstall. A swap cannot change the VIN, + # so a VIN that identifies exactly one other platform is worth a loud line in the log. + vin_platform = platform_from_vin(vin) + if vin_platform is not None and vin_platform != candidate: + carlog.warning({"event": "platformBundleVinMismatch", "bundle": str(candidate), "vin_platform": vin_platform, + "hint": "the selected platform bundle does not match the VIN's platform"}) + CP_SP = CarInterface.get_params_sp(CP, candidate, fingerprints, car_fw, alpha_long_allowed, is_release_sp, docs=False) sunnypilot_interfaces(CarInterface, CP, CP_SP, init_params_list_sp, can_recv, can_send) diff --git a/opendbc/car/mazda/carcontroller.py b/opendbc/car/mazda/carcontroller.py index 259ad87dd30..fb89324ec73 100644 --- a/opendbc/car/mazda/carcontroller.py +++ b/opendbc/car/mazda/carcontroller.py @@ -32,6 +32,8 @@ def __init__(self, dbc_names, CP, CP_SP): # scale and the non-delivery latch belong to the steer-to-zero firmware alone. self.eps_2022 = bool(CP.flags & MazdaFlags.EPS_HW) self.steer_to_zero = bool(CP.flags & MazdaFlags.STEER_TO_ZERO_EPS) + # The teardown replays this radar's own dialect, not the 2022 captures. + self.g46l = bool(CP.flags & MazdaFlags.G46L_RADAR) self.apply_torque_last = 0 self.driver_torque_samples: deque[float] = deque(maxlen=self.params.STEER_DRIVER_SAMPLES if self.eps_2022 else 1) self.sent_torque: deque[int] = deque(maxlen=self.params.STEER_ECHO_HISTORY if self.eps_2022 else 1) @@ -253,7 +255,7 @@ def update_longitudinal(self, CC, CC_SP, CS): if radar_master and self.frame % CarControllerParams.RADAR_STEP == 0: for bus in LONG_BUSES: - can_sends.extend(mazdacan.create_radar_frames(bus, self.radar_counter, self.lead_adv.lead)) + can_sends.extend(mazdacan.create_radar_frames(bus, self.radar_counter, self.lead_adv.lead, g46l=self.g46l)) self.radar_counter += 1 if radar_master and self.frame % CarControllerParams.LONG_STEP == 0: diff --git a/opendbc/car/mazda/interface.py b/opendbc/car/mazda/interface.py index 00e2cf39d14..d276ce1b488 100755 --- a/opendbc/car/mazda/interface.py +++ b/opendbc/car/mazda/interface.py @@ -1,11 +1,23 @@ #!/usr/bin/env python3 from opendbc.car import Bus, get_safety_config, structs +from opendbc.car.carlog import carlog from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.mazda.carcontroller import CarController from opendbc.car.mazda.carstate import CarState +from opendbc.car.mazda.fingerprints import FW_VERSIONS from opendbc.car.mazda.radar_interface import RadarInterface -from opendbc.car.mazda.values import CAR, DBC, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags +from opendbc.car.mazda.values import CAR, DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags + +# Radar firmware whose bus publishes the 0x361-0x366 track dialect: every radar the +# database lists except the G46L, which it lists for fingerprinting even though that +# radar never sends tracks on bus 0. Stored null-stripped so UDS response padding of any +# length compares equal. A talking radar outside this set behind a claiming platform (an +# EPS-swapped older body with a carried-forward bundle) gets the vision-only path; a +# silent one (empty fw query) keeps the platform's word. +TRACK_RADAR_FW = {fw.rstrip(b'\x00') for fw in set().union( + *(fw.get((structs.CarParams.Ecu.fwdRadar, 0x764, None), []) for fw in FW_VERSIONS.values()) +)} - G46L_RADAR_FW class CarInterface(CarInterfaceBase): @@ -18,7 +30,10 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.brand = "mazda" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.mazda)] - ret.radarUnavailable = Bus.radar not in DBC[candidate] + # A talking radar outside the track dialects sends no tracks we can parse: run + # vision-only instead of starving radarTracks behind a parser that never goes valid. + foreign_radar = any(fw.ecu == 'fwdRadar' and fw.fwVersion.rstrip(b'\x00') not in TRACK_RADAR_FW for fw in car_fw) + ret.radarUnavailable = Bus.radar not in DBC[candidate] or foreign_radar # Every gen1 Mazda EPS is the same hardware; only the firmware differs. Steer-to-zero follows # the EPS firmware, so a donor-EPS swap carries it and older firmware in a 2022 body loses it. @@ -36,8 +51,18 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.flags |= MazdaFlags.LEGACY_FW_EPS.value ret.safetyConfigs[0].safetyParam |= MazdaSafetyFlags.LEGACY_FW_EPS.value - # Offer alpha longitudinal only with the EPS that retains lateral control through a stop. - ret.alphaLongitudinalAvailable = steer_to_zero and not ret.radarUnavailable + # The G46L is the one foreign radar whose dialect alpha-long can replay (mazdacan.py). + # Detected with nulls stripped so UDS response padding cannot break the match. + g46l_radar = any(fw.ecu == 'fwdRadar' and fw.fwVersion.rstrip(b'\x00') in G46L_RADAR_FW for fw in car_fw) + if g46l_radar: + ret.flags |= MazdaFlags.G46L_RADAR.value + + # Alpha-long silences the radar and stands in for it, so it needs the radar's dialect, + # not its tracks: offer it wherever the platform's radar speaks the 2022 family dialect + # (its DBC claims a radar bus) or the detected radar is the G46L whose own replay exists. + # The EPS gate stays: a stock older EPS cuts lateral below 45 kph, so stop-and-go would + # run unsteered. + ret.alphaLongitudinalAvailable = steer_to_zero and (Bus.radar in DBC[candidate] or g46l_radar) ret.openpilotLongitudinalControl = alpha_long and ret.alphaLongitudinalAvailable if ret.openpilotLongitudinalControl: ret.safetyConfigs[0].safetyParam |= MazdaSafetyFlags.LONG.value @@ -50,6 +75,10 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo # Older EPS firmware enforces hands-off and low-speed steering lockouts. ret.dashcamOnly = candidate not in (CAR.MAZDA_CX5_2022, CAR.MAZDA_CX9_2021) and not steer_to_zero + carlog.info({"event": "mazdaRadarVerdict", "radarUnavailable": ret.radarUnavailable, + "platformClaim": Bus.radar in DBC[candidate], "foreignRadarFw": foreign_radar, + "g46lRadar": g46l_radar, "steerToZeroEps": steer_to_zero}) + ret.enableBsm = 0x477 in fingerprint[0] # Command-to-torque lag measured on the EPS hardware; lagd learns the remaining delay. diff --git a/opendbc/car/mazda/mazdacan.py b/opendbc/car/mazda/mazdacan.py index f780ade1a59..e98c996ff60 100644 --- a/opendbc/car/mazda/mazdacan.py +++ b/opendbc/car/mazda/mazdacan.py @@ -18,6 +18,10 @@ DIST_OBJ_SCALE = 0.0625 # m per bit, DIST_OBJ and RELV_OBJ share it DIST_OBJ_MAX = 255.875 # m, the full-scale DIST_OBJ reading a track can carry +# The G46L radar (2016.5 bodies) sends only this static frame and no track messages at all; +# fully static — no counter, no checksum. +G46L_RADAR_STATIC_MSG = (0x499, bytes.fromhex("0098400000000000")) + def crz_info_checksum(dat: bytes) -> int: # Invert the sum of the first seven bytes, excluding STOPPING and RESUME_UNLATCHING. @@ -82,8 +86,11 @@ def create_lead_track(d_rel: float, v_rel: float) -> bytes: return bytes(dat) -def create_radar_frames(bus, counter, lead): +def create_radar_frames(bus, counter, lead, g46l=False): """lead is the (dRel, vRel) of the object to advertise on 0x364, or None for an empty slot.""" + if g46l: + # The G46L never sends track messages; the lead rides CRZ_CTRL alone. + return [CanData(G46L_RADAR_STATIC_MSG[0], G46L_RADAR_STATIC_MSG[1], bus)] frames = [CanData(RADAR_STATIC_MSG[0], RADAR_STATIC_MSG[1], bus)] for addr, dat in RADAR_TRACK_MSGS.items(): if lead is not None and addr == LEAD_TRACK_ADDR: diff --git a/opendbc/car/mazda/tests/test_mazda_interface.py b/opendbc/car/mazda/tests/test_mazda_interface.py index e6ec651f7a1..7e0d9cf89b0 100644 --- a/opendbc/car/mazda/tests/test_mazda_interface.py +++ b/opendbc/car/mazda/tests/test_mazda_interface.py @@ -12,8 +12,9 @@ from opendbc.car import Bus, structs from opendbc.car.common.conversions import Conversions as CV from opendbc.car.mazda.carcontroller import CarController +from opendbc.car.mazda.fingerprints import FW_VERSIONS from opendbc.car.mazda.tests.conftest import DBC_NAME, car_params, car_params_sp -from opendbc.car.mazda.values import CAR, DBC, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags +from opendbc.car.mazda.values import CAR, DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags Ecu = structs.CarParams.Ecu @@ -36,6 +37,21 @@ def eps_fw(version: bytes) -> list[structs.CarParams.CarFw]: return [fw] +def radar_fw(version: bytes) -> structs.CarParams.CarFw: + fw = structs.CarParams.CarFw() + fw.ecu = Ecu.fwdRadar + fw.address = 0x764 + fw.subAddress = 0 + fw.fwVersion = version + return fw + + +# The 2016.5-era radar a first-gen body keeps through an EPS swap, padded to the 24-byte +# fw field the UDS query returns (the padding length is load-bearing: the G46L is listed +# in fingerprints.py, and a longer test padding once masked an exact-match miss) +G46L_FW = sorted(G46L_RADAR_FW)[0] + b'\x00' * (24 - len(sorted(G46L_RADAR_FW)[0])) + + class TestMazdaEpsSwap: """A 2022+ CX-5 EPS swapped into an older Mazda brings the EPS-derived behavior with it. @@ -224,6 +240,69 @@ def test_legacy_firmware_is_listed_for_the_2022_body_only(self): assert LEGACY_FW_EPS not in STEER_TO_ZERO_EPS_FW assert set(FW_VERSIONS[CAR.MAZDA_CX5_2022][(Ecu.eps, 0x730, None)]) == STEER_TO_ZERO_EPS_FW | {LEGACY_FW_EPS} +class TestForeignRadar: + """A talking radar whose firmware no platform lists, behind any platform the bundle picks. + + An EPS-swapped older body keeps its first-gen radar, and a carried-forward platform + bundle can claim a radar bus the physical car cannot fill. Parsing must not starve + behind that claim (radard waits on a parser that never goes valid), and alpha-long must + key on the radar's dialect, not its tracks: the G46L is the one foreign radar whose + replay exists (mazdacan.py). + """ + + def test_foreign_radar_runs_vision_only_behind_a_radar_claim(self): + # the support-ticket car: a 2016 KE body with the swapped 2022 EPS, forced to the + # CX-5 2022 platform by a carried-forward bundle. The G46L answers the fw query but + # never sends 0x361-0x366, so parsing its bus would starve radarTracks forever + CP = car_params(CAR.MAZDA_CX5_2022, car_fw=[radar_fw(G46L_FW)]) + assert CP.radarUnavailable + assert CP.alphaLongitudinalAvailable + assert CP.flags & MazdaFlags.G46L_RADAR + + def test_g46l_unlocks_alpha_long_on_a_platform_without_a_radar_bus(self): + # the same car on its own platform: no radar bus claimed, but the G46L is reachable + # and its dialect can be replayed, so the port is offered with the swapped EPS + fw = eps_fw(SWAPPED_EPS_FW) + [radar_fw(G46L_FW)] + CP = car_params(CAR.MAZDA_CX5_KE, car_fw=fw, alpha_long=True) + assert CP.radarUnavailable + assert not CP.dashcamOnly + assert CP.alphaLongitudinalAvailable + assert CP.openpilotLongitudinalControl + assert bool(CP.safetyConfigs[0].safetyParam & MazdaSafetyFlags.LONG.value) + + # a stock EPS keeps the offer off even with the G46L present + stock = car_params(CAR.MAZDA_CX5_KE, car_fw=[radar_fw(G46L_FW)], alpha_long=True) + assert not stock.alphaLongitudinalAvailable + + def test_unknown_foreign_radar_offers_no_new_dialect(self): + # vision-only applies whatever the platform claims, but only the G46L adds an offer + # beyond the claim: an unknown radar behind a platform without a radar bus has no + # replay alpha-long could use + unknown = [radar_fw(b'KK00-67X00-A' + b'\x00' * 16)] + claiming = car_params(CAR.MAZDA_CX5_2022, car_fw=unknown, alpha_long=True) + assert claiming.radarUnavailable + assert claiming.alphaLongitudinalAvailable + + non_claiming = car_params(CAR.MAZDA_CX5_KE, car_fw=eps_fw(SWAPPED_EPS_FW) + unknown, alpha_long=True) + assert non_claiming.radarUnavailable + assert not non_claiming.alphaLongitudinalAvailable + assert not non_claiming.openpilotLongitudinalControl + + def test_silent_radar_keeps_the_platform_claim(self): + # only a talking foreign radar degrades: a fw query that never reached the radar + # must not flip a claiming platform into vision-only + CP = car_params(CAR.MAZDA_CX5_2022, car_fw=eps_fw(SWAPPED_EPS_FW)) + assert not CP.radarUnavailable + + def test_the_platforms_own_radar_fw_stays_parsed(self): + # TRACK_RADAR_FW is derived from the fingerprint database; the CX-5 2022's own radar + # firmware must keep the track-parsing path + own_fw = sorted(FW_VERSIONS[CAR.MAZDA_CX5_2022][(Ecu.fwdRadar, 0x764, None)])[0] + CP = car_params(CAR.MAZDA_CX5_2022, car_fw=[radar_fw(own_fw)]) + assert not CP.radarUnavailable + assert CP.alphaLongitudinalAvailable + assert not CP.flags & MazdaFlags.G46L_RADAR + def test_non_gen1_platform_refused_at_admission(): # one init-time check instead of per-frame guards in the message builders, which every diff --git a/opendbc/car/mazda/tests/test_mazda_mazdacan.py b/opendbc/car/mazda/tests/test_mazda_mazdacan.py index c9ffd1f27bc..a7bb0c813d2 100644 --- a/opendbc/car/mazda/tests/test_mazda_mazdacan.py +++ b/opendbc/car/mazda/tests/test_mazda_mazdacan.py @@ -128,6 +128,13 @@ def test_radar_frames_counter_and_lead_track(): assert tracks[0x364] == "0a4e00001c00000f" +def test_g46l_radar_frames_are_the_static_capture_alone(): + # the G46L never sends track messages, lead or not: the lead rides CRZ_CTRL alone + for lead in (None, (10.25, 0.)): + frames = mazdacan.create_radar_frames(0, 15, lead, g46l=True) + assert [(f.address, f.dat.hex(), f.src) for f in frames] == [(0x499, "0098400000000000", 0)] + + def test_lead_track_constant_bytes_match_the_stock_release_capture(): # the template's measurement fields are zeroed, so a zero-range lead reproduces it exactly assert mazdacan.create_lead_track(0., 0.) == mazdacan.LEAD_TRACK_TEMPLATE diff --git a/opendbc/car/mazda/tests/test_mazda_vin.py b/opendbc/car/mazda/tests/test_mazda_vin.py index d4cfee735cd..7a67db2d95b 100644 --- a/opendbc/car/mazda/tests/test_mazda_vin.py +++ b/opendbc/car/mazda/tests/test_mazda_vin.py @@ -10,6 +10,7 @@ from opendbc.car.fw_versions import match_fw_to_car from opendbc.car.mazda.fingerprints import FW_VERSIONS from opendbc.car.mazda.values import CAR, STEER_TO_ZERO_EPS_FW, match_fw_to_car_fuzzy +from opendbc.car.values import platform_from_vin from opendbc.car.vin import VIN_UNKNOWN # a steer-to-zero EPS a swap donates; the CX-5 2022 list also carries legacy firmware now @@ -66,6 +67,19 @@ def test_real_listing_vins(self, vin, expected): expected_platforms = {str(expected)} if expected is not None else set() assert match_fw_to_car_fuzzy({}, vin, FW_VERSIONS) == expected_platforms + def test_the_support_log_vin_resolves(self): + # the KE body from the alpha-long support log, running behind a carried-forward + # CX-5 2022 platform bundle: the resolver must name its own platform + assert platform_from_vin('JM3KE4DYXG0877243') == str(CAR.MAZDA_CX5_KE) + + @pytest.mark.parametrize("vin, expected", REAL_VINS) + def test_platform_from_vin_matches_the_fuzzy_matcher(self, vin, expected): + assert platform_from_vin(vin) == (str(expected) if expected is not None else None) + + def test_platform_from_vin_rejects_unknown(self): + assert platform_from_vin(VIN_UNKNOWN) is None + assert platform_from_vin('JM3KE4DYXG08772') is None # short + def test_wrong_wmi_does_not_match(self): assert match_fw_to_car_fuzzy({}, make_vin('JM6', 'TC', 'M'), FW_VERSIONS) == set() diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py index 58c33561629..4c6742e76d1 100644 --- a/opendbc/car/mazda/values.py +++ b/opendbc/car/mazda/values.py @@ -170,6 +170,11 @@ class MazdaFlags(IntFlag): # Everything keyed on the measured hardware rather than on what the firmware permits. EPS_HW = STEER_TO_ZERO_EPS | LEGACY_FW_EPS + # The 2016.5-era radar kept by an EPS-swapped older body. fingerprints.py lists it for + # fingerprinting, but it never sends tracks on bus 0; alpha-long replays its own dialect + # instead of the 2022 captures (mazdacan.py). + G46L_RADAR = 4 + class MazdaSafetyFlags(IntFlag): LONG = 1 @@ -250,6 +255,15 @@ class LKAS_LIMITS: b'KSD5-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', } +# The 2016.5-era radar kept by an EPS-swapped older body. fingerprints.py lists it for +# fingerprinting, but it never publishes 0x361-0x366 on bus 0: the interface keeps it off +# the track dialects (vision-only under stock longitudinal), and alpha-long replays this +# radar's own dialect instead of the 2022 templates (mazdacan.py). Stored unpadded; the +# interface matches with nulls stripped so response padding cannot break it. +G46L_RADAR_FW = { + b'G46L-67XA1-C', +} + class Buttons: NONE = 0 diff --git a/opendbc/car/values.py b/opendbc/car/values.py index 1101fb4e700..083913bad36 100644 --- a/opendbc/car/values.py +++ b/opendbc/car/values.py @@ -20,3 +20,33 @@ BRANDS = get_args(Platform) PLATFORMS: dict[str, Platform] = {str(platform): platform for brand in BRANDS for platform in brand} + + +def platform_from_vin(vin: str) -> str | None: + """The one platform the VIN's fields identify, or None when the VIN is unknown to every + platform or ambiguous. + + Platforms advertise VIN metadata (wmis, chassis_codes, years) on their config; those + without any are skipped. A hardware swap cannot change the VIN, so this is the strongest + signal to check a user-selected platform bundle against. + """ + from opendbc.car.vin import Vin, is_valid_vin + + if not is_valid_vin(vin): + return None + + vin_obj = Vin(vin) + chassis_code = vin_obj.vds[0:2] + year = vin_obj.vis[0] + + candidates = set() + for platform in PLATFORMS.values(): + config = platform.config + wmis = getattr(config, 'wmis', None) + if not wmis: + continue + if vin_obj.wmi in wmis and chassis_code in getattr(config, 'chassis_codes', set()) \ + and year in getattr(config, 'years', set()): + candidates.add(str(platform)) + + return next(iter(candidates)) if len(candidates) == 1 else None diff --git a/opendbc/safety/modes/mazda.h b/opendbc/safety/modes/mazda.h index f977fe5f7c1..f7659079c2e 100644 --- a/opendbc/safety/modes/mazda.h +++ b/opendbc/safety/modes/mazda.h @@ -59,11 +59,17 @@ static bool mazda_radar_was_silenced = false; // Pin replaced-radar traffic to captured stock patterns where possible. +// Each radar generation sends its own static capture; the controller picks the dialect (mazdacan.py) static bool mazda_radar_static_msg_valid(const CANPacket_t *msg) { - return (msg->data[0] == 0x00U) && (msg->data[1] == 0x08U) && - (msg->data[2] == 0xc0U) && (msg->data[3] == 0x00U) && - (msg->data[4] == 0x00U) && (msg->data[5] == 0x00U) && - (msg->data[6] == 0x00U) && (msg->data[7] == 0x00U); + bool capture_2022 = (msg->data[0] == 0x00U) && (msg->data[1] == 0x08U) && + (msg->data[2] == 0xc0U) && (msg->data[3] == 0x00U) && + (msg->data[4] == 0x00U) && (msg->data[5] == 0x00U) && + (msg->data[6] == 0x00U) && (msg->data[7] == 0x00U); + bool capture_g46l = (msg->data[0] == 0x00U) && (msg->data[1] == 0x98U) && + (msg->data[2] == 0x40U) && (msg->data[3] == 0x00U) && + (msg->data[4] == 0x00U) && (msg->data[5] == 0x00U) && + (msg->data[6] == 0x00U) && (msg->data[7] == 0x00U); + return capture_2022 || capture_g46l; } static bool mazda_empty_radar_track_msg_valid(const CANPacket_t *msg) { diff --git a/opendbc/safety/tests/test_mazda.py b/opendbc/safety/tests/test_mazda.py index 42948438c41..6802ca90fae 100755 --- a/opendbc/safety/tests/test_mazda.py +++ b/opendbc/safety/tests/test_mazda.py @@ -435,6 +435,18 @@ def test_empty_radar_tracks_allowed(self): for addr, dat in radar_messages.items(): self.assertTrue(self._tx(common.make_msg(bus, addr, 8, dat))) + def test_g46l_radar_static_allowed(self): + # the 2016.5 G46L body's own static capture; the 2022 one above is not its frame, and + # the G46L never sends track messages at all + for controls_allowed in (False, True): + self.safety.set_controls_allowed(controls_allowed) + for bus in (0, 2): + self.assertTrue(self._tx(common.make_msg(bus, 0x499, 8, bytes.fromhex("0098400000000000")))) + + self.safety.set_controls_allowed(True) + for bus in (0, 2): + self.assertFalse(self._tx(common.make_msg(bus, 0x499, 8, bytes.fromhex("0098400100000000")))) + def test_synthetic_lead_radar_track_allowed_disengaged(self): # Permit the measurement fields while requiring the occupied-track template. The slot is # perception, not actuation, so it remains valid with controls_allowed low like stock radar From ef7eb54686792f0f00d6afdf368e0eef5cd41208 Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Mon, 7 Sep 2026 12:58:18 -0400 Subject: [PATCH 07/13] mazda: keep firmware-gated platforms in the car picker Docs mode runs with empty car_fw, so every Mazda outside the factory steer-to-zero pair resolved dashcamOnly, and the car-list generator skips dashcamOnly platforms: CX-5 KE, CX-5 2017-21, CX-9 2016-20 (and Mazda 3/6) were unselectable. dashcamOnly is a measured-hardware call, so docs mode now keeps the default and the on-device EPS check keeps the gate; regenerate car_list.json (380 -> 385 platforms). --- opendbc/car/mazda/interface.py | 4 +- .../car/mazda/tests/test_mazda_interface.py | 6 +- opendbc/sunnypilot/car/car_list.json | 66 +++++++++++++++++++ 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/opendbc/car/mazda/interface.py b/opendbc/car/mazda/interface.py index d276ce1b488..ae2c255f0df 100755 --- a/opendbc/car/mazda/interface.py +++ b/opendbc/car/mazda/interface.py @@ -73,7 +73,9 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.longitudinalActuatorDelay = 0.36 # measured ~0.3 s dead time + ~0.3 s first-order lag # Older EPS firmware enforces hands-off and low-speed steering lockouts. - ret.dashcamOnly = candidate not in (CAR.MAZDA_CX5_2022, CAR.MAZDA_CX9_2021) and not steer_to_zero + # Docs mode carries no real EPS firmware, so leave dashcamOnly at the default. + if not docs: + ret.dashcamOnly = candidate not in (CAR.MAZDA_CX5_2022, CAR.MAZDA_CX9_2021) and not steer_to_zero carlog.info({"event": "mazdaRadarVerdict", "radarUnavailable": ret.radarUnavailable, "platformClaim": Bus.radar in DBC[candidate], "foreignRadarFw": foreign_radar, diff --git a/opendbc/car/mazda/tests/test_mazda_interface.py b/opendbc/car/mazda/tests/test_mazda_interface.py index 7e0d9cf89b0..20b927b83a5 100644 --- a/opendbc/car/mazda/tests/test_mazda_interface.py +++ b/opendbc/car/mazda/tests/test_mazda_interface.py @@ -157,11 +157,13 @@ def test_safety_param_follows_the_eps(self, candidate, car_fw, alpha_long, expec @pytest.mark.parametrize("candidate", [CAR.MAZDA_CX5_KE, CAR.MAZDA_CX5, CAR.MAZDA_CX9, CAR.MAZDA_3, CAR.MAZDA_6]) def test_docs_are_generated_without_firmware(self, candidate): - # car_fw is empty when building CARS.md, so the docs must keep advertising dashcam mode + # car_fw is empty in docs mode, and the car picker consumes docs mode: a firmware-gated + # platform must stay selectable there. dashcamOnly is a measured-hardware call, so the + # on-device EPS check keeps the gate; docs describe the stock car from the platform table. from opendbc.car import gen_empty_fingerprint from opendbc.car.mazda.interface import CarInterface CP = CarInterface.get_params(candidate, gen_empty_fingerprint(), [], alpha_long=False, is_release=False, docs=True) - assert CP.dashcamOnly + assert not CP.dashcamOnly class TestMazdaLegacyFwEps: diff --git a/opendbc/sunnypilot/car/car_list.json b/opendbc/sunnypilot/car/car_list.json index 536c4258280..f92d1b3582c 100644 --- a/opendbc/sunnypilot/car/car_list.json +++ b/opendbc/sunnypilot/car/car_list.json @@ -2800,6 +2800,58 @@ ], "package": "Adaptive Cruise Control (ACC) & Lane Assist" }, + "Mazda 3 2017-18": { + "platform": "MAZDA_3", + "make": "Mazda", + "brand": "mazda", + "model": "3", + "year": [ + "2017", + "2018" + ], + "package": "All" + }, + "Mazda 6 2017-20": { + "platform": "MAZDA_6", + "make": "Mazda", + "brand": "mazda", + "model": "6", + "year": [ + "2017", + "2018", + "2019", + "2020" + ], + "package": "All" + }, + "Mazda CX-5 2012-16": { + "platform": "MAZDA_CX5_KE", + "make": "Mazda", + "brand": "mazda", + "model": "CX-5", + "year": [ + "2012", + "2013", + "2014", + "2015", + "2016" + ], + "package": "All" + }, + "Mazda CX-5 2017-21": { + "platform": "MAZDA_CX5", + "make": "Mazda", + "brand": "mazda", + "model": "CX-5", + "year": [ + "2017", + "2018", + "2019", + "2020", + "2021" + ], + "package": "All" + }, "Mazda CX-5 2022-25": { "platform": "MAZDA_CX5_2022", "make": "Mazda", @@ -2813,6 +2865,20 @@ ], "package": "All" }, + "Mazda CX-9 2016-20": { + "platform": "MAZDA_CX9", + "make": "Mazda", + "brand": "mazda", + "model": "CX-9", + "year": [ + "2016", + "2017", + "2018", + "2019", + "2020" + ], + "package": "All" + }, "Mazda CX-9 2021-23": { "platform": "MAZDA_CX9_2021", "make": "Mazda", From 91c8c8e43ecb70d6b7851d33bd8b985016faf037 Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Mon, 7 Sep 2026 14:48:39 -0400 Subject: [PATCH 08/13] mazda: tighten the G46L and VIN comment story The G46L facts were told at six production sites. The fw-set definition now carries the identity story and the null-strip matching, the detection and availability sites keep only their local why, the controller note drops, and the mazdacan constant's comment absorbs the lead-on-CRZ_CTRL clause so the branch note can go. The VIN swap rationale lives in platform_from_vin's docstring alone. No behavior change. --- opendbc/car/car_helpers.py | 3 +-- opendbc/car/mazda/carcontroller.py | 1 - opendbc/car/mazda/interface.py | 5 +---- opendbc/car/mazda/mazdacan.py | 5 ++--- opendbc/car/mazda/values.py | 16 ++++++---------- 5 files changed, 10 insertions(+), 20 deletions(-) diff --git a/opendbc/car/car_helpers.py b/opendbc/car/car_helpers.py index dcb12ecea44..865c511dbd5 100644 --- a/opendbc/car/car_helpers.py +++ b/opendbc/car/car_helpers.py @@ -170,8 +170,7 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip if fixed_fingerprint and vin != VIN_UNKNOWN: # A carried-forward CarPlatformBundle can disagree with the physical car after a - # hardware swap or a branch switch without reinstall. A swap cannot change the VIN, - # so a VIN that identifies exactly one other platform is worth a loud line in the log. + # hardware swap or a branch switch without reinstall. vin_platform = platform_from_vin(vin) if vin_platform is not None and vin_platform != candidate: carlog.warning({"event": "platformBundleVinMismatch", "bundle": str(candidate), "vin_platform": vin_platform, diff --git a/opendbc/car/mazda/carcontroller.py b/opendbc/car/mazda/carcontroller.py index fb89324ec73..76f974d5ab6 100644 --- a/opendbc/car/mazda/carcontroller.py +++ b/opendbc/car/mazda/carcontroller.py @@ -32,7 +32,6 @@ def __init__(self, dbc_names, CP, CP_SP): # scale and the non-delivery latch belong to the steer-to-zero firmware alone. self.eps_2022 = bool(CP.flags & MazdaFlags.EPS_HW) self.steer_to_zero = bool(CP.flags & MazdaFlags.STEER_TO_ZERO_EPS) - # The teardown replays this radar's own dialect, not the 2022 captures. self.g46l = bool(CP.flags & MazdaFlags.G46L_RADAR) self.apply_torque_last = 0 self.driver_torque_samples: deque[float] = deque(maxlen=self.params.STEER_DRIVER_SAMPLES if self.eps_2022 else 1) diff --git a/opendbc/car/mazda/interface.py b/opendbc/car/mazda/interface.py index ae2c255f0df..48f4331ca65 100755 --- a/opendbc/car/mazda/interface.py +++ b/opendbc/car/mazda/interface.py @@ -12,9 +12,7 @@ # Radar firmware whose bus publishes the 0x361-0x366 track dialect: every radar the # database lists except the G46L, which it lists for fingerprinting even though that # radar never sends tracks on bus 0. Stored null-stripped so UDS response padding of any -# length compares equal. A talking radar outside this set behind a claiming platform (an -# EPS-swapped older body with a carried-forward bundle) gets the vision-only path; a -# silent one (empty fw query) keeps the platform's word. +# length compares equal. TRACK_RADAR_FW = {fw.rstrip(b'\x00') for fw in set().union( *(fw.get((structs.CarParams.Ecu.fwdRadar, 0x764, None), []) for fw in FW_VERSIONS.values()) )} - G46L_RADAR_FW @@ -52,7 +50,6 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.safetyConfigs[0].safetyParam |= MazdaSafetyFlags.LEGACY_FW_EPS.value # The G46L is the one foreign radar whose dialect alpha-long can replay (mazdacan.py). - # Detected with nulls stripped so UDS response padding cannot break the match. g46l_radar = any(fw.ecu == 'fwdRadar' and fw.fwVersion.rstrip(b'\x00') in G46L_RADAR_FW for fw in car_fw) if g46l_radar: ret.flags |= MazdaFlags.G46L_RADAR.value diff --git a/opendbc/car/mazda/mazdacan.py b/opendbc/car/mazda/mazdacan.py index e98c996ff60..103b94e3ce0 100644 --- a/opendbc/car/mazda/mazdacan.py +++ b/opendbc/car/mazda/mazdacan.py @@ -18,8 +18,8 @@ DIST_OBJ_SCALE = 0.0625 # m per bit, DIST_OBJ and RELV_OBJ share it DIST_OBJ_MAX = 255.875 # m, the full-scale DIST_OBJ reading a track can carry -# The G46L radar (2016.5 bodies) sends only this static frame and no track messages at all; -# fully static — no counter, no checksum. +# The G46L radar (2016.5 bodies) sends only this static frame and no track messages at +# all, so the lead rides CRZ_CTRL alone; fully static — no counter, no checksum. G46L_RADAR_STATIC_MSG = (0x499, bytes.fromhex("0098400000000000")) @@ -89,7 +89,6 @@ def create_lead_track(d_rel: float, v_rel: float) -> bytes: def create_radar_frames(bus, counter, lead, g46l=False): """lead is the (dRel, vRel) of the object to advertise on 0x364, or None for an empty slot.""" if g46l: - # The G46L never sends track messages; the lead rides CRZ_CTRL alone. return [CanData(G46L_RADAR_STATIC_MSG[0], G46L_RADAR_STATIC_MSG[1], bus)] frames = [CanData(RADAR_STATIC_MSG[0], RADAR_STATIC_MSG[1], bus)] for addr, dat in RADAR_TRACK_MSGS.items(): diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py index 4c6742e76d1..dfd443bef49 100644 --- a/opendbc/car/mazda/values.py +++ b/opendbc/car/mazda/values.py @@ -170,9 +170,7 @@ class MazdaFlags(IntFlag): # Everything keyed on the measured hardware rather than on what the firmware permits. EPS_HW = STEER_TO_ZERO_EPS | LEGACY_FW_EPS - # The 2016.5-era radar kept by an EPS-swapped older body. fingerprints.py lists it for - # fingerprinting, but it never sends tracks on bus 0; alpha-long replays its own dialect - # instead of the 2022 captures (mazdacan.py). + # The 2016.5-era radar; see G46L_RADAR_FW below for what makes it special. G46L_RADAR = 4 @@ -204,14 +202,14 @@ class MazdaPlatformConfig(PlatformConfig): class CAR(Platforms): MAZDA_CX5_KE = MazdaPlatformConfig( [MazdaCarDocs("Mazda CX-5 2012-16")], - MazdaCarSpecs(mass=3433 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=18.1), #steerRatio copied from 22 CX-5, same hardware + MazdaCarSpecs(mass=3433 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=18.1), # steer ratio from the 2022 CX-5: same rack hardware # This radar does not publish 0x361-0x366 tracks on bus 0. dbc_dict={Bus.pt: 'mazda_2017'}, wmis={WMI.JAPAN_CROSSOVER}, chassis_codes={'KE'}, years={'C', 'D', 'E', 'F', 'G'}, # 2012-16 ) MAZDA_CX5 = MazdaPlatformConfig( [MazdaCarDocs("Mazda CX-5 2017-21")], - MazdaCarSpecs(mass=3655 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=18.1), #steerRatio copied from 22 CX-5, same hardware + MazdaCarSpecs(mass=3655 * CV.LB_TO_KG, wheelbase=2.7, steerRatio=18.1), # steer ratio from the 2022 CX-5: same rack hardware wmis={WMI.JAPAN_CROSSOVER}, chassis_codes={'KF'}, years={'H', 'J', 'K', 'L', 'M'}, # 2017-21 ) MAZDA_CX9 = MazdaPlatformConfig( @@ -255,11 +253,9 @@ class LKAS_LIMITS: b'KSD5-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', } -# The 2016.5-era radar kept by an EPS-swapped older body. fingerprints.py lists it for -# fingerprinting, but it never publishes 0x361-0x366 on bus 0: the interface keeps it off -# the track dialects (vision-only under stock longitudinal), and alpha-long replays this -# radar's own dialect instead of the 2022 templates (mazdacan.py). Stored unpadded; the -# interface matches with nulls stripped so response padding cannot break it. +# The 2016.5-era radar kept by an EPS-swapped older body. Listed for fingerprinting, but +# it never publishes 0x361-0x366 on bus 0. Stored unpadded; matched with nulls stripped so +# UDS response padding cannot break it. G46L_RADAR_FW = { b'G46L-67XA1-C', } From 1a6431d09e1ea8e78e777445a98bde72446ac407 Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Mon, 7 Sep 2026 15:29:18 -0400 Subject: [PATCH 09/13] mazda: a radar-dialect registry for the replay path The G46L replay was a special case threaded through four files: a bare fw set, a flag bit, a hardcoded capture, and a g46l= boolean plumbed through the controller. REPLAY_RADAR_DIALECTS now registers each replayable radar in one place (name, firmware, flag, sends_tracks), mazdacan's capture table keys off the dialect object, and the interface resolves the detected radar through the registry. Adding a dialect is data plus captures; a consistency test pins that every dialect claims an unclaimed single bit, stays off the track set, and has its capture registered. That test forced a flag fix: rebasing onto the danger-unstable promotion had left LEGACY_FW_EPS and G46L_RADAR both on bit 4. Latent (the dialect bit is only read behind the steer-to-zero precondition), but every legacy-firmware car read as carrying the G46L dialect. G46L_RADAR moves to bit 8, and mazdaRadarVerdict names the resolved dialect instead of a boolean. --- opendbc/car/mazda/carcontroller.py | 6 ++--- opendbc/car/mazda/interface.py | 26 +++++++++--------- opendbc/car/mazda/mazdacan.py | 18 ++++++++----- .../car/mazda/tests/test_mazda_interface.py | 27 +++++++++++++++++-- .../car/mazda/tests/test_mazda_mazdacan.py | 3 ++- opendbc/car/mazda/values.py | 26 ++++++++++++++---- 6 files changed, 76 insertions(+), 30 deletions(-) diff --git a/opendbc/car/mazda/carcontroller.py b/opendbc/car/mazda/carcontroller.py index 76f974d5ab6..2ffb196fc37 100644 --- a/opendbc/car/mazda/carcontroller.py +++ b/opendbc/car/mazda/carcontroller.py @@ -9,7 +9,7 @@ from opendbc.car.mazda import mazdacan from opendbc.car.mazda.longitudinal import (BREAKAWAY_FRAMES, RADAR_ADDR, AdvertisedLead, RadarSessionManager, RadarSessionState, StandstillHold, create_radar_session_msg) -from opendbc.car.mazda.values import CarControllerParams, Buttons, MazdaFlags +from opendbc.car.mazda.values import CarControllerParams, Buttons, MazdaFlags, REPLAY_RADAR_DIALECTS from opendbc.sunnypilot.car.mazda.icbm import IntelligentCruiseButtonManagementInterface @@ -32,7 +32,7 @@ def __init__(self, dbc_names, CP, CP_SP): # scale and the non-delivery latch belong to the steer-to-zero firmware alone. self.eps_2022 = bool(CP.flags & MazdaFlags.EPS_HW) self.steer_to_zero = bool(CP.flags & MazdaFlags.STEER_TO_ZERO_EPS) - self.g46l = bool(CP.flags & MazdaFlags.G46L_RADAR) + self.radar_dialect = next((d for d in REPLAY_RADAR_DIALECTS if CP.flags & d.flag), None) self.apply_torque_last = 0 self.driver_torque_samples: deque[float] = deque(maxlen=self.params.STEER_DRIVER_SAMPLES if self.eps_2022 else 1) self.sent_torque: deque[int] = deque(maxlen=self.params.STEER_ECHO_HISTORY if self.eps_2022 else 1) @@ -254,7 +254,7 @@ def update_longitudinal(self, CC, CC_SP, CS): if radar_master and self.frame % CarControllerParams.RADAR_STEP == 0: for bus in LONG_BUSES: - can_sends.extend(mazdacan.create_radar_frames(bus, self.radar_counter, self.lead_adv.lead, g46l=self.g46l)) + can_sends.extend(mazdacan.create_radar_frames(bus, self.radar_counter, self.lead_adv.lead, dialect=self.radar_dialect)) self.radar_counter += 1 if radar_master and self.frame % CarControllerParams.LONG_STEP == 0: diff --git a/opendbc/car/mazda/interface.py b/opendbc/car/mazda/interface.py index 48f4331ca65..316bddc50a3 100755 --- a/opendbc/car/mazda/interface.py +++ b/opendbc/car/mazda/interface.py @@ -7,15 +7,15 @@ from opendbc.car.mazda.carstate import CarState from opendbc.car.mazda.fingerprints import FW_VERSIONS from opendbc.car.mazda.radar_interface import RadarInterface -from opendbc.car.mazda.values import CAR, DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags +from opendbc.car.mazda.values import CAR, DBC, LKAS_LIMITS, REPLAY_RADAR_DIALECTS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags # Radar firmware whose bus publishes the 0x361-0x366 track dialect: every radar the -# database lists except the G46L, which it lists for fingerprinting even though that -# radar never sends tracks on bus 0. Stored null-stripped so UDS response padding of any -# length compares equal. +# database lists except the registered replay dialects, which it lists for fingerprinting +# even though those radars never send tracks on bus 0. Stored null-stripped so UDS +# response padding of any length compares equal. TRACK_RADAR_FW = {fw.rstrip(b'\x00') for fw in set().union( *(fw.get((structs.CarParams.Ecu.fwdRadar, 0x764, None), []) for fw in FW_VERSIONS.values()) -)} - G46L_RADAR_FW +)} - frozenset().union(*(d.fw for d in REPLAY_RADAR_DIALECTS)) class CarInterface(CarInterfaceBase): @@ -49,17 +49,19 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.flags |= MazdaFlags.LEGACY_FW_EPS.value ret.safetyConfigs[0].safetyParam |= MazdaSafetyFlags.LEGACY_FW_EPS.value - # The G46L is the one foreign radar whose dialect alpha-long can replay (mazdacan.py). - g46l_radar = any(fw.ecu == 'fwdRadar' and fw.fwVersion.rstrip(b'\x00') in G46L_RADAR_FW for fw in car_fw) - if g46l_radar: - ret.flags |= MazdaFlags.G46L_RADAR.value + # Resolve the detected radar to a registered replay dialect (mazdacan.py replays + # its own wire behavior, not the 2022 captures). + radar_fw = {fw.fwVersion.rstrip(b'\x00') for fw in car_fw if fw.ecu == 'fwdRadar'} + dialect = next((d for d in REPLAY_RADAR_DIALECTS if radar_fw & d.fw), None) + if dialect is not None: + ret.flags |= int(dialect.flag) # Alpha-long silences the radar and stands in for it, so it needs the radar's dialect, # not its tracks: offer it wherever the platform's radar speaks the 2022 family dialect - # (its DBC claims a radar bus) or the detected radar is the G46L whose own replay exists. + # (its DBC claims a radar bus) or the detected radar has a registered replay dialect. # The EPS gate stays: a stock older EPS cuts lateral below 45 kph, so stop-and-go would # run unsteered. - ret.alphaLongitudinalAvailable = steer_to_zero and (Bus.radar in DBC[candidate] or g46l_radar) + ret.alphaLongitudinalAvailable = steer_to_zero and (Bus.radar in DBC[candidate] or dialect is not None) ret.openpilotLongitudinalControl = alpha_long and ret.alphaLongitudinalAvailable if ret.openpilotLongitudinalControl: ret.safetyConfigs[0].safetyParam |= MazdaSafetyFlags.LONG.value @@ -76,7 +78,7 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo carlog.info({"event": "mazdaRadarVerdict", "radarUnavailable": ret.radarUnavailable, "platformClaim": Bus.radar in DBC[candidate], "foreignRadarFw": foreign_radar, - "g46lRadar": g46l_radar, "steerToZeroEps": steer_to_zero}) + "replayDialect": dialect.name if dialect is not None else None, "steerToZeroEps": steer_to_zero}) ret.enableBsm = 0x477 in fingerprint[0] diff --git a/opendbc/car/mazda/mazdacan.py b/opendbc/car/mazda/mazdacan.py index 103b94e3ce0..c9502cc0dd9 100644 --- a/opendbc/car/mazda/mazdacan.py +++ b/opendbc/car/mazda/mazdacan.py @@ -1,5 +1,5 @@ from opendbc.car.can_definitions import CanData -from opendbc.car.mazda.values import Buttons +from opendbc.car.mazda.values import Buttons, G46L # Captured empty radar tracks required by the body ECU for stop-and-go. Only the counter # nibble changes; 0x364 carries the advertised lead when present. @@ -18,9 +18,12 @@ DIST_OBJ_SCALE = 0.0625 # m per bit, DIST_OBJ and RELV_OBJ share it DIST_OBJ_MAX = 255.875 # m, the full-scale DIST_OBJ reading a track can carry -# The G46L radar (2016.5 bodies) sends only this static frame and no track messages at -# all, so the lead rides CRZ_CTRL alone; fully static — no counter, no checksum. -G46L_RADAR_STATIC_MSG = (0x499, bytes.fromhex("0098400000000000")) +# Static capture per replay dialect (REPLAY_RADAR_DIALECTS in values.py). The G46L +# (2016.5 bodies) sends only this static frame and no track messages at all, so the lead +# rides CRZ_CTRL alone; fully static — no counter, no checksum. +RADAR_STATIC_CAPTURES = { + G46L: (0x499, bytes.fromhex("0098400000000000")), +} def crz_info_checksum(dat: bytes) -> int: @@ -86,10 +89,11 @@ def create_lead_track(d_rel: float, v_rel: float) -> bytes: return bytes(dat) -def create_radar_frames(bus, counter, lead, g46l=False): +def create_radar_frames(bus, counter, lead, dialect=None): """lead is the (dRel, vRel) of the object to advertise on 0x364, or None for an empty slot.""" - if g46l: - return [CanData(G46L_RADAR_STATIC_MSG[0], G46L_RADAR_STATIC_MSG[1], bus)] + if dialect is not None: + addr, dat = RADAR_STATIC_CAPTURES[dialect] + return [CanData(addr, dat, bus)] frames = [CanData(RADAR_STATIC_MSG[0], RADAR_STATIC_MSG[1], bus)] for addr, dat in RADAR_TRACK_MSGS.items(): if lead is not None and addr == LEAD_TRACK_ADDR: diff --git a/opendbc/car/mazda/tests/test_mazda_interface.py b/opendbc/car/mazda/tests/test_mazda_interface.py index 20b927b83a5..940082345bd 100644 --- a/opendbc/car/mazda/tests/test_mazda_interface.py +++ b/opendbc/car/mazda/tests/test_mazda_interface.py @@ -14,7 +14,7 @@ from opendbc.car.mazda.carcontroller import CarController from opendbc.car.mazda.fingerprints import FW_VERSIONS from opendbc.car.mazda.tests.conftest import DBC_NAME, car_params, car_params_sp -from opendbc.car.mazda.values import CAR, DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags +from opendbc.car.mazda.values import CAR, DBC, G46L, LKAS_LIMITS, REPLAY_RADAR_DIALECTS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags Ecu = structs.CarParams.Ecu @@ -49,7 +49,7 @@ def radar_fw(version: bytes) -> structs.CarParams.CarFw: # The 2016.5-era radar a first-gen body keeps through an EPS swap, padded to the 24-byte # fw field the UDS query returns (the padding length is load-bearing: the G46L is listed # in fingerprints.py, and a longer test padding once masked an exact-match miss) -G46L_FW = sorted(G46L_RADAR_FW)[0] + b'\x00' * (24 - len(sorted(G46L_RADAR_FW)[0])) +G46L_FW = sorted(G46L.fw)[0] + b'\x00' * (24 - len(sorted(G46L.fw)[0])) class TestMazdaEpsSwap: @@ -296,6 +296,29 @@ def test_silent_radar_keeps_the_platform_claim(self): CP = car_params(CAR.MAZDA_CX5_2022, car_fw=eps_fw(SWAPPED_EPS_FW)) assert not CP.radarUnavailable +class TestReplayRadarRegistry: + """One home per dialect: the registry, its captures, and a flag bit nothing else claims.""" + + def test_dialect_bits_claim_exactly_one_flag(self): + # single-bit members sharing a dialect's bit must be the dialect's own flag alone: + # the rebase onto the danger-unstable promotion once left LEGACY_FW_EPS and + # G46L_RADAR both on bit 4, and every legacy-firmware car read as carrying the dialect + assert len({d.flag for d in REPLAY_RADAR_DIALECTS}) == len(REPLAY_RADAR_DIALECTS) + for d in REPLAY_RADAR_DIALECTS: + sharers = [m.name for m in MazdaFlags if m.value & d.flag and bin(m.value).count('1') == 1] + assert len(sharers) == 1, (d.name, sharers) + + def test_dialect_firmware_stays_off_the_track_set(self): + from opendbc.car.mazda.interface import TRACK_RADAR_FW + for d in REPLAY_RADAR_DIALECTS: + assert not d.fw & TRACK_RADAR_FW + + def test_every_dialect_has_a_registered_capture(self): + from opendbc.car.mazda import mazdacan + for d in REPLAY_RADAR_DIALECTS: + assert d in mazdacan.RADAR_STATIC_CAPTURES + + def test_the_platforms_own_radar_fw_stays_parsed(self): # TRACK_RADAR_FW is derived from the fingerprint database; the CX-5 2022's own radar # firmware must keep the track-parsing path diff --git a/opendbc/car/mazda/tests/test_mazda_mazdacan.py b/opendbc/car/mazda/tests/test_mazda_mazdacan.py index a7bb0c813d2..26a9ce2b8e6 100644 --- a/opendbc/car/mazda/tests/test_mazda_mazdacan.py +++ b/opendbc/car/mazda/tests/test_mazda_mazdacan.py @@ -10,6 +10,7 @@ import pytest from opendbc.car.mazda import mazdacan +from opendbc.car.mazda.values import G46L from opendbc.car.mazda.tests.conftest import CAM_LANEINFO, LEAD_TRACK, parse_frame @@ -131,7 +132,7 @@ def test_radar_frames_counter_and_lead_track(): def test_g46l_radar_frames_are_the_static_capture_alone(): # the G46L never sends track messages, lead or not: the lead rides CRZ_CTRL alone for lead in (None, (10.25, 0.)): - frames = mazdacan.create_radar_frames(0, 15, lead, g46l=True) + frames = mazdacan.create_radar_frames(0, 15, lead, dialect=G46L) assert [(f.address, f.dat.hex(), f.src) for f in frames] == [(0x499, "0098400000000000", 0)] diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py index dfd443bef49..e85cd58f58e 100644 --- a/opendbc/car/mazda/values.py +++ b/opendbc/car/mazda/values.py @@ -170,8 +170,8 @@ class MazdaFlags(IntFlag): # Everything keyed on the measured hardware rather than on what the firmware permits. EPS_HW = STEER_TO_ZERO_EPS | LEGACY_FW_EPS - # The 2016.5-era radar; see G46L_RADAR_FW below for what makes it special. - G46L_RADAR = 4 + # The G46L replay dialect's bit; see REPLAY_RADAR_DIALECTS below. + G46L_RADAR = 8 class MazdaSafetyFlags(IntFlag): @@ -253,12 +253,28 @@ class LKAS_LIMITS: b'KSD5-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', } +@dataclass(frozen=True) +class RadarDialect: + """A radar dialect the teardown can stand in for, beyond the 2022 track family. + + A talking radar outside TRACK_RADAR_FW gets the vision-only path; the dialects + registered here are the ones whose own wire behavior alpha-long can replay. Adding + one: register it, claim its MazdaFlags bit, add its static capture to mazdacan.py's + RADAR_STATIC_CAPTURES and to mazda_radar_static_msg_valid in panda safety, and mirror + the capture in the safety tests. + """ + name: str + fw: frozenset[bytes] # null-stripped firmware strings that speak this dialect + flag: int # the MazdaFlags bit that marks it in CarParams.flags + sends_tracks: bool # False = static-only; the lead rides CRZ_CTRL alone + + # The 2016.5-era radar kept by an EPS-swapped older body. Listed for fingerprinting, but # it never publishes 0x361-0x366 on bus 0. Stored unpadded; matched with nulls stripped so # UDS response padding cannot break it. -G46L_RADAR_FW = { - b'G46L-67XA1-C', -} +G46L = RadarDialect(name='G46L', fw=frozenset((b'G46L-67XA1-C',)), + flag=MazdaFlags.G46L_RADAR, sends_tracks=False) +REPLAY_RADAR_DIALECTS = (G46L,) class Buttons: From 8cf1a8d5a238da3d2160c081b47c94060b38abe8 Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Mon, 7 Sep 2026 15:45:04 -0400 Subject: [PATCH 10/13] mazda: pin the registry's replay and safety invariants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sends_tracks was decorative; the replay-shape test makes it the promise it names: every static-only dialect replays exactly its registered capture, whatever the lead. The consistency test also pins that dialect firmware sets stay pairwise disjoint — two dialects listing the same string would make resolution silently pick the first. And the safety capture test now iterates RADAR_STATIC_CAPTURES through mazda_rx_hook, so a dialect registered without its mazda.h mirror goes red by name. --- opendbc/car/mazda/tests/test_mazda_interface.py | 10 +++++++++- opendbc/car/mazda/tests/test_mazda_mazdacan.py | 14 +++++++++++++- opendbc/safety/tests/test_mazda.py | 10 ++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/opendbc/car/mazda/tests/test_mazda_interface.py b/opendbc/car/mazda/tests/test_mazda_interface.py index 940082345bd..4b1fd676922 100644 --- a/opendbc/car/mazda/tests/test_mazda_interface.py +++ b/opendbc/car/mazda/tests/test_mazda_interface.py @@ -49,7 +49,8 @@ def radar_fw(version: bytes) -> structs.CarParams.CarFw: # The 2016.5-era radar a first-gen body keeps through an EPS swap, padded to the 24-byte # fw field the UDS query returns (the padding length is load-bearing: the G46L is listed # in fingerprints.py, and a longer test padding once masked an exact-match miss) -G46L_FW = sorted(G46L.fw)[0] + b'\x00' * (24 - len(sorted(G46L.fw)[0])) +_g46l_stem = sorted(G46L.fw)[0] +G46L_FW = _g46l_stem + b'\x00' * (24 - len(_g46l_stem)) class TestMazdaEpsSwap: @@ -313,6 +314,13 @@ def test_dialect_firmware_stays_off_the_track_set(self): for d in REPLAY_RADAR_DIALECTS: assert not d.fw & TRACK_RADAR_FW + def test_dialect_firmware_sets_are_pairwise_disjoint(self): + # two dialects listing the same string would make resolution silently pick the first + seen: set[bytes] = set() + for d in REPLAY_RADAR_DIALECTS: + assert not d.fw & seen, d.name + seen |= d.fw + def test_every_dialect_has_a_registered_capture(self): from opendbc.car.mazda import mazdacan for d in REPLAY_RADAR_DIALECTS: diff --git a/opendbc/car/mazda/tests/test_mazda_mazdacan.py b/opendbc/car/mazda/tests/test_mazda_mazdacan.py index 26a9ce2b8e6..75686807ef7 100644 --- a/opendbc/car/mazda/tests/test_mazda_mazdacan.py +++ b/opendbc/car/mazda/tests/test_mazda_mazdacan.py @@ -10,7 +10,7 @@ import pytest from opendbc.car.mazda import mazdacan -from opendbc.car.mazda.values import G46L +from opendbc.car.mazda.values import G46L, REPLAY_RADAR_DIALECTS from opendbc.car.mazda.tests.conftest import CAM_LANEINFO, LEAD_TRACK, parse_frame @@ -136,6 +136,18 @@ def test_g46l_radar_frames_are_the_static_capture_alone(): assert [(f.address, f.dat.hex(), f.src) for f in frames] == [(0x499, "0098400000000000", 0)] +def test_registered_static_only_dialects_replay_the_capture_alone(): + # the registry's promise for a static-only dialect: no track frames whatever the lead, + # exactly the registered capture (the concrete G46L case above, said once for all) + for dialect in REPLAY_RADAR_DIALECTS: + if dialect.sends_tracks: + continue + addr, dat = mazdacan.RADAR_STATIC_CAPTURES[dialect] + for lead in (None, (10.25, 0.)): + frames = mazdacan.create_radar_frames(0, 15, lead, dialect=dialect) + assert [(f.address, f.dat, f.src) for f in frames] == [(addr, dat, 0)], dialect.name + + def test_lead_track_constant_bytes_match_the_stock_release_capture(): # the template's measurement fields are zeroed, so a zero-range lead reproduces it exactly assert mazdacan.create_lead_track(0., 0.) == mazdacan.LEAD_TRACK_TEMPLATE diff --git a/opendbc/safety/tests/test_mazda.py b/opendbc/safety/tests/test_mazda.py index 6802ca90fae..fd64853879f 100755 --- a/opendbc/safety/tests/test_mazda.py +++ b/opendbc/safety/tests/test_mazda.py @@ -447,6 +447,16 @@ def test_g46l_radar_static_allowed(self): for bus in (0, 2): self.assertFalse(self._tx(common.make_msg(bus, 0x499, 8, bytes.fromhex("0098400100000000")))) + def test_registered_dialect_captures_allowed(self): + # every capture the python registry registers must pass the mirrored whitelist in + # mazda.h: registering a dialect without its safety capture goes red here + from opendbc.car.mazda import mazdacan + for addr, dat in mazdacan.RADAR_STATIC_CAPTURES.values(): + for controls_allowed in (False, True): + self.safety.set_controls_allowed(controls_allowed) + for bus in (0, 2): + self.assertTrue(self._tx(common.make_msg(bus, addr, 8, dat))) + def test_synthetic_lead_radar_track_allowed_disengaged(self): # Permit the measurement fields while requiring the occupied-track template. The slot is # perception, not actuation, so it remains valid with controls_allowed low like stock radar From 5723af521121ff4823f94496b0b8ee1d7e4925f6 Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Mon, 7 Sep 2026 15:57:00 -0400 Subject: [PATCH 11/13] mazda: final comment sweep One stale clause fixed: the registry recipe still told the reader to mirror captures in the safety tests, which the registry-driven test now does by itself. The G46L identity facts consolidate into the registry entry (the no-counter, no-checksum nature of its frame moves there), and the per-site comments shrink to their local why. Test names carry what test names can. --- opendbc/car/mazda/interface.py | 8 +++----- opendbc/car/mazda/mazdacan.py | 4 +--- opendbc/car/mazda/tests/test_mazda_interface.py | 5 ++--- opendbc/car/mazda/tests/test_mazda_mazdacan.py | 3 +-- opendbc/car/mazda/values.py | 10 +++++----- 5 files changed, 12 insertions(+), 18 deletions(-) diff --git a/opendbc/car/mazda/interface.py b/opendbc/car/mazda/interface.py index 316bddc50a3..7735e2ddf69 100755 --- a/opendbc/car/mazda/interface.py +++ b/opendbc/car/mazda/interface.py @@ -10,9 +10,8 @@ from opendbc.car.mazda.values import CAR, DBC, LKAS_LIMITS, REPLAY_RADAR_DIALECTS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags # Radar firmware whose bus publishes the 0x361-0x366 track dialect: every radar the -# database lists except the registered replay dialects, which it lists for fingerprinting -# even though those radars never send tracks on bus 0. Stored null-stripped so UDS -# response padding of any length compares equal. +# database lists except the registered replay dialects — listed for fingerprinting, but +# they never send tracks on bus 0. Stored null-stripped so UDS padding cannot break it. TRACK_RADAR_FW = {fw.rstrip(b'\x00') for fw in set().union( *(fw.get((structs.CarParams.Ecu.fwdRadar, 0x764, None), []) for fw in FW_VERSIONS.values()) )} - frozenset().union(*(d.fw for d in REPLAY_RADAR_DIALECTS)) @@ -49,8 +48,7 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.flags |= MazdaFlags.LEGACY_FW_EPS.value ret.safetyConfigs[0].safetyParam |= MazdaSafetyFlags.LEGACY_FW_EPS.value - # Resolve the detected radar to a registered replay dialect (mazdacan.py replays - # its own wire behavior, not the 2022 captures). + # The registry's dialects are the ones mazdacan can replay as-is, not the 2022 captures. radar_fw = {fw.fwVersion.rstrip(b'\x00') for fw in car_fw if fw.ecu == 'fwdRadar'} dialect = next((d for d in REPLAY_RADAR_DIALECTS if radar_fw & d.fw), None) if dialect is not None: diff --git a/opendbc/car/mazda/mazdacan.py b/opendbc/car/mazda/mazdacan.py index c9502cc0dd9..7c1771f619c 100644 --- a/opendbc/car/mazda/mazdacan.py +++ b/opendbc/car/mazda/mazdacan.py @@ -18,9 +18,7 @@ DIST_OBJ_SCALE = 0.0625 # m per bit, DIST_OBJ and RELV_OBJ share it DIST_OBJ_MAX = 255.875 # m, the full-scale DIST_OBJ reading a track can carry -# Static capture per replay dialect (REPLAY_RADAR_DIALECTS in values.py). The G46L -# (2016.5 bodies) sends only this static frame and no track messages at all, so the lead -# rides CRZ_CTRL alone; fully static — no counter, no checksum. +# Static capture per replay dialect (REPLAY_RADAR_DIALECTS in values.py). RADAR_STATIC_CAPTURES = { G46L: (0x499, bytes.fromhex("0098400000000000")), } diff --git a/opendbc/car/mazda/tests/test_mazda_interface.py b/opendbc/car/mazda/tests/test_mazda_interface.py index 4b1fd676922..5c681ae6bc1 100644 --- a/opendbc/car/mazda/tests/test_mazda_interface.py +++ b/opendbc/car/mazda/tests/test_mazda_interface.py @@ -46,9 +46,8 @@ def radar_fw(version: bytes) -> structs.CarParams.CarFw: return fw -# The 2016.5-era radar a first-gen body keeps through an EPS swap, padded to the 24-byte -# fw field the UDS query returns (the padding length is load-bearing: the G46L is listed -# in fingerprints.py, and a longer test padding once masked an exact-match miss) +# padded to the 24-byte fw field the UDS query returns; the padding length is +# load-bearing — a longer test padding once masked an exact-match miss _g46l_stem = sorted(G46L.fw)[0] G46L_FW = _g46l_stem + b'\x00' * (24 - len(_g46l_stem)) diff --git a/opendbc/car/mazda/tests/test_mazda_mazdacan.py b/opendbc/car/mazda/tests/test_mazda_mazdacan.py index 75686807ef7..064f252ae02 100644 --- a/opendbc/car/mazda/tests/test_mazda_mazdacan.py +++ b/opendbc/car/mazda/tests/test_mazda_mazdacan.py @@ -137,8 +137,7 @@ def test_g46l_radar_frames_are_the_static_capture_alone(): def test_registered_static_only_dialects_replay_the_capture_alone(): - # the registry's promise for a static-only dialect: no track frames whatever the lead, - # exactly the registered capture (the concrete G46L case above, said once for all) + # static-only: no track frames whatever the lead, exactly the registered capture for dialect in REPLAY_RADAR_DIALECTS: if dialect.sends_tracks: continue diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py index e85cd58f58e..6b13613f9d9 100644 --- a/opendbc/car/mazda/values.py +++ b/opendbc/car/mazda/values.py @@ -259,9 +259,9 @@ class RadarDialect: A talking radar outside TRACK_RADAR_FW gets the vision-only path; the dialects registered here are the ones whose own wire behavior alpha-long can replay. Adding - one: register it, claim its MazdaFlags bit, add its static capture to mazdacan.py's - RADAR_STATIC_CAPTURES and to mazda_radar_static_msg_valid in panda safety, and mirror - the capture in the safety tests. + one: register it, claim its MazdaFlags bit, and add its static capture to mazdacan.py's + RADAR_STATIC_CAPTURES and to mazda_radar_static_msg_valid in panda safety — the safety + test picks registered captures up by itself. """ name: str fw: frozenset[bytes] # null-stripped firmware strings that speak this dialect @@ -270,8 +270,8 @@ class RadarDialect: # The 2016.5-era radar kept by an EPS-swapped older body. Listed for fingerprinting, but -# it never publishes 0x361-0x366 on bus 0. Stored unpadded; matched with nulls stripped so -# UDS response padding cannot break it. +# it never publishes 0x361-0x366 on bus 0; its one frame is fully static — no counter, no +# checksum. Stored unpadded; matched with nulls stripped so UDS padding cannot break it. G46L = RadarDialect(name='G46L', fw=frozenset((b'G46L-67XA1-C',)), flag=MazdaFlags.G46L_RADAR, sends_tracks=False) REPLAY_RADAR_DIALECTS = (G46L,) From 5426622133d3aa353617eeebeeb34ad4668d7aed Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Mon, 7 Sep 2026 17:09:07 -0400 Subject: [PATCH 12/13] =?UTF-8?q?mazda:=20review=20revision=20=E2=80=94=20?= =?UTF-8?q?invert=20the=20foreign-radar=20rule,=20simplify,=20VIN=20in=20m?= =?UTF-8?q?azda?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per review: an unlisted newer revision of a working radar must not silently lose its tracks, so radarUnavailable now keys on the platform's bus claim or the G46L alone, and TRACK_RADAR_FW is gone. The registry reverts to the simple G46L_RADAR_FW set, flag and g46l kwarg (bit 8 kept; a one-line test pins that no other single-bit flag shares it — that assertion caught the LEGACY_FW_EPS/bit-4 collision). VIN matching consolidates into mazda/values.py: platform_from_vin serves match_fw_to_car_fuzzy and the mismatch warning, which moves to _get_params_sp off carVin and fingerprintSource, covering the FINGERPRINT env path; the generic helper and the car_helpers hook are deleted. mazdaRadarVerdict downgraded to debug. --- opendbc/car/car_helpers.py | 10 +-- opendbc/car/mazda/carcontroller.py | 6 +- opendbc/car/mazda/interface.py | 44 ++++++------ opendbc/car/mazda/mazdacan.py | 16 ++--- .../car/mazda/tests/test_mazda_interface.py | 72 +++++++------------ .../car/mazda/tests/test_mazda_mazdacan.py | 14 +--- opendbc/car/mazda/tests/test_mazda_vin.py | 3 +- opendbc/car/mazda/values.py | 55 ++++++-------- opendbc/car/values.py | 29 -------- opendbc/safety/tests/test_mazda.py | 10 --- 10 files changed, 80 insertions(+), 179 deletions(-) diff --git a/opendbc/car/car_helpers.py b/opendbc/car/car_helpers.py index 865c511dbd5..8747263752a 100644 --- a/opendbc/car/car_helpers.py +++ b/opendbc/car/car_helpers.py @@ -8,7 +8,7 @@ from opendbc.car.fingerprints import eliminate_incompatible_cars, all_legacy_fingerprint_cars from opendbc.car.fw_versions import ObdCallback, get_fw_versions_ordered, get_present_ecus, match_fw_to_car from opendbc.car.mock.values import CAR as MOCK -from opendbc.car.values import BRANDS, platform_from_vin +from opendbc.car.values import BRANDS from opendbc.car.vin import get_vin, is_valid_vin, VIN_UNKNOWN from opendbc.sunnypilot.car.interfaces import setup_interfaces as sunnypilot_interfaces @@ -168,14 +168,6 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip CP.fingerprintSource = source CP.fuzzyFingerprint = not exact_match - if fixed_fingerprint and vin != VIN_UNKNOWN: - # A carried-forward CarPlatformBundle can disagree with the physical car after a - # hardware swap or a branch switch without reinstall. - vin_platform = platform_from_vin(vin) - if vin_platform is not None and vin_platform != candidate: - carlog.warning({"event": "platformBundleVinMismatch", "bundle": str(candidate), "vin_platform": vin_platform, - "hint": "the selected platform bundle does not match the VIN's platform"}) - CP_SP = CarInterface.get_params_sp(CP, candidate, fingerprints, car_fw, alpha_long_allowed, is_release_sp, docs=False) sunnypilot_interfaces(CarInterface, CP, CP_SP, init_params_list_sp, can_recv, can_send) diff --git a/opendbc/car/mazda/carcontroller.py b/opendbc/car/mazda/carcontroller.py index 2ffb196fc37..76f974d5ab6 100644 --- a/opendbc/car/mazda/carcontroller.py +++ b/opendbc/car/mazda/carcontroller.py @@ -9,7 +9,7 @@ from opendbc.car.mazda import mazdacan from opendbc.car.mazda.longitudinal import (BREAKAWAY_FRAMES, RADAR_ADDR, AdvertisedLead, RadarSessionManager, RadarSessionState, StandstillHold, create_radar_session_msg) -from opendbc.car.mazda.values import CarControllerParams, Buttons, MazdaFlags, REPLAY_RADAR_DIALECTS +from opendbc.car.mazda.values import CarControllerParams, Buttons, MazdaFlags from opendbc.sunnypilot.car.mazda.icbm import IntelligentCruiseButtonManagementInterface @@ -32,7 +32,7 @@ def __init__(self, dbc_names, CP, CP_SP): # scale and the non-delivery latch belong to the steer-to-zero firmware alone. self.eps_2022 = bool(CP.flags & MazdaFlags.EPS_HW) self.steer_to_zero = bool(CP.flags & MazdaFlags.STEER_TO_ZERO_EPS) - self.radar_dialect = next((d for d in REPLAY_RADAR_DIALECTS if CP.flags & d.flag), None) + self.g46l = bool(CP.flags & MazdaFlags.G46L_RADAR) self.apply_torque_last = 0 self.driver_torque_samples: deque[float] = deque(maxlen=self.params.STEER_DRIVER_SAMPLES if self.eps_2022 else 1) self.sent_torque: deque[int] = deque(maxlen=self.params.STEER_ECHO_HISTORY if self.eps_2022 else 1) @@ -254,7 +254,7 @@ def update_longitudinal(self, CC, CC_SP, CS): if radar_master and self.frame % CarControllerParams.RADAR_STEP == 0: for bus in LONG_BUSES: - can_sends.extend(mazdacan.create_radar_frames(bus, self.radar_counter, self.lead_adv.lead, dialect=self.radar_dialect)) + can_sends.extend(mazdacan.create_radar_frames(bus, self.radar_counter, self.lead_adv.lead, g46l=self.g46l)) self.radar_counter += 1 if radar_master and self.frame % CarControllerParams.LONG_STEP == 0: diff --git a/opendbc/car/mazda/interface.py b/opendbc/car/mazda/interface.py index 7735e2ddf69..eb78500062c 100755 --- a/opendbc/car/mazda/interface.py +++ b/opendbc/car/mazda/interface.py @@ -5,16 +5,8 @@ from opendbc.car.interfaces import CarInterfaceBase from opendbc.car.mazda.carcontroller import CarController from opendbc.car.mazda.carstate import CarState -from opendbc.car.mazda.fingerprints import FW_VERSIONS from opendbc.car.mazda.radar_interface import RadarInterface -from opendbc.car.mazda.values import CAR, DBC, LKAS_LIMITS, REPLAY_RADAR_DIALECTS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags - -# Radar firmware whose bus publishes the 0x361-0x366 track dialect: every radar the -# database lists except the registered replay dialects — listed for fingerprinting, but -# they never send tracks on bus 0. Stored null-stripped so UDS padding cannot break it. -TRACK_RADAR_FW = {fw.rstrip(b'\x00') for fw in set().union( - *(fw.get((structs.CarParams.Ecu.fwdRadar, 0x764, None), []) for fw in FW_VERSIONS.values()) -)} - frozenset().union(*(d.fw for d in REPLAY_RADAR_DIALECTS)) +from opendbc.car.mazda.values import CAR, DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags, platform_from_vin class CarInterface(CarInterfaceBase): @@ -27,10 +19,14 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.brand = "mazda" ret.safetyConfigs = [get_safety_config(structs.CarParams.SafetyModel.mazda)] - # A talking radar outside the track dialects sends no tracks we can parse: run - # vision-only instead of starving radarTracks behind a parser that never goes valid. - foreign_radar = any(fw.ecu == 'fwdRadar' and fw.fwVersion.rstrip(b'\x00') not in TRACK_RADAR_FW for fw in car_fw) - ret.radarUnavailable = Bus.radar not in DBC[candidate] or foreign_radar + # The G46L is the one radar known never to publish 0x361-0x366 on bus 0: parsing its + # claimed bus would starve radarTracks behind a parser that never goes valid, so it runs + # vision-only. Any other radar keeps the platform's word — an unlisted newer revision of + # a working radar must not silently lose its tracks. + g46l_radar = any(fw.ecu == 'fwdRadar' and fw.fwVersion.rstrip(b'\x00') in G46L_RADAR_FW for fw in car_fw) + if g46l_radar: + ret.flags |= MazdaFlags.G46L_RADAR.value + ret.radarUnavailable = Bus.radar not in DBC[candidate] or g46l_radar # Every gen1 Mazda EPS is the same hardware; only the firmware differs. Steer-to-zero follows # the EPS firmware, so a donor-EPS swap carries it and older firmware in a 2022 body loses it. @@ -48,18 +44,12 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.flags |= MazdaFlags.LEGACY_FW_EPS.value ret.safetyConfigs[0].safetyParam |= MazdaSafetyFlags.LEGACY_FW_EPS.value - # The registry's dialects are the ones mazdacan can replay as-is, not the 2022 captures. - radar_fw = {fw.fwVersion.rstrip(b'\x00') for fw in car_fw if fw.ecu == 'fwdRadar'} - dialect = next((d for d in REPLAY_RADAR_DIALECTS if radar_fw & d.fw), None) - if dialect is not None: - ret.flags |= int(dialect.flag) - # Alpha-long silences the radar and stands in for it, so it needs the radar's dialect, # not its tracks: offer it wherever the platform's radar speaks the 2022 family dialect - # (its DBC claims a radar bus) or the detected radar has a registered replay dialect. + # (its DBC claims a radar bus) or the detected radar is the G46L whose own replay exists. # The EPS gate stays: a stock older EPS cuts lateral below 45 kph, so stop-and-go would # run unsteered. - ret.alphaLongitudinalAvailable = steer_to_zero and (Bus.radar in DBC[candidate] or dialect is not None) + ret.alphaLongitudinalAvailable = steer_to_zero and (Bus.radar in DBC[candidate] or g46l_radar) ret.openpilotLongitudinalControl = alpha_long and ret.alphaLongitudinalAvailable if ret.openpilotLongitudinalControl: ret.safetyConfigs[0].safetyParam |= MazdaSafetyFlags.LONG.value @@ -74,9 +64,8 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo if not docs: ret.dashcamOnly = candidate not in (CAR.MAZDA_CX5_2022, CAR.MAZDA_CX9_2021) and not steer_to_zero - carlog.info({"event": "mazdaRadarVerdict", "radarUnavailable": ret.radarUnavailable, - "platformClaim": Bus.radar in DBC[candidate], "foreignRadarFw": foreign_radar, - "replayDialect": dialect.name if dialect is not None else None, "steerToZeroEps": steer_to_zero}) + carlog.debug({"event": "mazdaRadarVerdict", "radarUnavailable": ret.radarUnavailable, + "platformClaim": Bus.radar in DBC[candidate], "g46lRadar": g46l_radar, "steerToZeroEps": steer_to_zero}) ret.enableBsm = 0x477 in fingerprint[0] @@ -95,4 +84,11 @@ def _get_params_sp(stock_cp: structs.CarParams, ret: structs.CarParamsSP, candid car_fw: list[structs.CarParams.CarFw], alpha_long: bool, is_release_sp: bool, docs: bool) -> structs.CarParamsSP: ret.intelligentCruiseButtonManagementAvailable = True + # A carried-forward CarPlatformBundle can disagree with the physical car after a + # hardware swap or a branch switch without reinstall. + vin_platform = platform_from_vin(stock_cp.carVin) + if vin_platform is not None and vin_platform != str(candidate): + carlog.warning({"event": "platformBundleVinMismatch", "bundle": str(candidate), "vin_platform": vin_platform, + "hint": "the selected platform bundle does not match the VIN's platform"}) + return ret diff --git a/opendbc/car/mazda/mazdacan.py b/opendbc/car/mazda/mazdacan.py index 7c1771f619c..103b94e3ce0 100644 --- a/opendbc/car/mazda/mazdacan.py +++ b/opendbc/car/mazda/mazdacan.py @@ -1,5 +1,5 @@ from opendbc.car.can_definitions import CanData -from opendbc.car.mazda.values import Buttons, G46L +from opendbc.car.mazda.values import Buttons # Captured empty radar tracks required by the body ECU for stop-and-go. Only the counter # nibble changes; 0x364 carries the advertised lead when present. @@ -18,10 +18,9 @@ DIST_OBJ_SCALE = 0.0625 # m per bit, DIST_OBJ and RELV_OBJ share it DIST_OBJ_MAX = 255.875 # m, the full-scale DIST_OBJ reading a track can carry -# Static capture per replay dialect (REPLAY_RADAR_DIALECTS in values.py). -RADAR_STATIC_CAPTURES = { - G46L: (0x499, bytes.fromhex("0098400000000000")), -} +# The G46L radar (2016.5 bodies) sends only this static frame and no track messages at +# all, so the lead rides CRZ_CTRL alone; fully static — no counter, no checksum. +G46L_RADAR_STATIC_MSG = (0x499, bytes.fromhex("0098400000000000")) def crz_info_checksum(dat: bytes) -> int: @@ -87,11 +86,10 @@ def create_lead_track(d_rel: float, v_rel: float) -> bytes: return bytes(dat) -def create_radar_frames(bus, counter, lead, dialect=None): +def create_radar_frames(bus, counter, lead, g46l=False): """lead is the (dRel, vRel) of the object to advertise on 0x364, or None for an empty slot.""" - if dialect is not None: - addr, dat = RADAR_STATIC_CAPTURES[dialect] - return [CanData(addr, dat, bus)] + if g46l: + return [CanData(G46L_RADAR_STATIC_MSG[0], G46L_RADAR_STATIC_MSG[1], bus)] frames = [CanData(RADAR_STATIC_MSG[0], RADAR_STATIC_MSG[1], bus)] for addr, dat in RADAR_TRACK_MSGS.items(): if lead is not None and addr == LEAD_TRACK_ADDR: diff --git a/opendbc/car/mazda/tests/test_mazda_interface.py b/opendbc/car/mazda/tests/test_mazda_interface.py index 5c681ae6bc1..1775687ea11 100644 --- a/opendbc/car/mazda/tests/test_mazda_interface.py +++ b/opendbc/car/mazda/tests/test_mazda_interface.py @@ -14,7 +14,7 @@ from opendbc.car.mazda.carcontroller import CarController from opendbc.car.mazda.fingerprints import FW_VERSIONS from opendbc.car.mazda.tests.conftest import DBC_NAME, car_params, car_params_sp -from opendbc.car.mazda.values import CAR, DBC, G46L, LKAS_LIMITS, REPLAY_RADAR_DIALECTS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags +from opendbc.car.mazda.values import CAR, DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags Ecu = structs.CarParams.Ecu @@ -48,7 +48,7 @@ def radar_fw(version: bytes) -> structs.CarParams.CarFw: # padded to the 24-byte fw field the UDS query returns; the padding length is # load-bearing — a longer test padding once masked an exact-match miss -_g46l_stem = sorted(G46L.fw)[0] +_g46l_stem = sorted(G46L_RADAR_FW)[0] G46L_FW = _g46l_stem + b'\x00' * (24 - len(_g46l_stem)) @@ -243,16 +243,16 @@ def test_legacy_firmware_is_listed_for_the_2022_body_only(self): assert set(FW_VERSIONS[CAR.MAZDA_CX5_2022][(Ecu.eps, 0x730, None)]) == STEER_TO_ZERO_EPS_FW | {LEGACY_FW_EPS} class TestForeignRadar: - """A talking radar whose firmware no platform lists, behind any platform the bundle picks. + """The G46L is the one radar known never to publish 0x361-0x366 on bus 0. - An EPS-swapped older body keeps its first-gen radar, and a carried-forward platform - bundle can claim a radar bus the physical car cannot fill. Parsing must not starve - behind that claim (radard waits on a parser that never goes valid), and alpha-long must - key on the radar's dialect, not its tracks: the G46L is the one foreign radar whose - replay exists (mazdacan.py). + A carried-forward platform bundle can claim a radar bus the physical car cannot fill; + parsing that bus would starve radarTracks behind a parser that never goes valid. The + G46L gets the vision-only path, and alpha-long keys on the radar's dialect, not its + tracks (mazdacan.py replays the G46L's own). Every other radar keeps the platform's + word, so an unlisted newer revision of a working radar loses nothing. """ - def test_foreign_radar_runs_vision_only_behind_a_radar_claim(self): + def test_g46l_runs_vision_only_behind_a_radar_claim(self): # the support-ticket car: a 2016 KE body with the swapped 2022 EPS, forced to the # CX-5 2022 platform by a carried-forward bundle. The G46L answers the fw query but # never sends 0x361-0x366, so parsing its bus would starve radarTracks forever @@ -276,13 +276,12 @@ def test_g46l_unlocks_alpha_long_on_a_platform_without_a_radar_bus(self): stock = car_params(CAR.MAZDA_CX5_KE, car_fw=[radar_fw(G46L_FW)], alpha_long=True) assert not stock.alphaLongitudinalAvailable - def test_unknown_foreign_radar_offers_no_new_dialect(self): - # vision-only applies whatever the platform claims, but only the G46L adds an offer - # beyond the claim: an unknown radar behind a platform without a radar bus has no - # replay alpha-long could use + def test_an_unknown_radar_keeps_the_stock_parse_path(self): + # an unlisted newer revision of a working radar must not silently lose its tracks: + # only the G46L is known not to publish them, so everything else keeps the claim unknown = [radar_fw(b'KK00-67X00-A' + b'\x00' * 16)] - claiming = car_params(CAR.MAZDA_CX5_2022, car_fw=unknown, alpha_long=True) - assert claiming.radarUnavailable + claiming = car_params(CAR.MAZDA_CX5_2022, car_fw=unknown) + assert not claiming.radarUnavailable assert claiming.alphaLongitudinalAvailable non_claiming = car_params(CAR.MAZDA_CX5_KE, car_fw=eps_fw(SWAPPED_EPS_FW) + unknown, alpha_long=True) @@ -291,44 +290,21 @@ def test_unknown_foreign_radar_offers_no_new_dialect(self): assert not non_claiming.openpilotLongitudinalControl def test_silent_radar_keeps_the_platform_claim(self): - # only a talking foreign radar degrades: a fw query that never reached the radar - # must not flip a claiming platform into vision-only + # a fw query that never reached the radar must not flip a claiming platform into + # vision-only CP = car_params(CAR.MAZDA_CX5_2022, car_fw=eps_fw(SWAPPED_EPS_FW)) assert not CP.radarUnavailable -class TestReplayRadarRegistry: - """One home per dialect: the registry, its captures, and a flag bit nothing else claims.""" - - def test_dialect_bits_claim_exactly_one_flag(self): - # single-bit members sharing a dialect's bit must be the dialect's own flag alone: - # the rebase onto the danger-unstable promotion once left LEGACY_FW_EPS and - # G46L_RADAR both on bit 4, and every legacy-firmware car read as carrying the dialect - assert len({d.flag for d in REPLAY_RADAR_DIALECTS}) == len(REPLAY_RADAR_DIALECTS) - for d in REPLAY_RADAR_DIALECTS: - sharers = [m.name for m in MazdaFlags if m.value & d.flag and bin(m.value).count('1') == 1] - assert len(sharers) == 1, (d.name, sharers) - - def test_dialect_firmware_stays_off_the_track_set(self): - from opendbc.car.mazda.interface import TRACK_RADAR_FW - for d in REPLAY_RADAR_DIALECTS: - assert not d.fw & TRACK_RADAR_FW - - def test_dialect_firmware_sets_are_pairwise_disjoint(self): - # two dialects listing the same string would make resolution silently pick the first - seen: set[bytes] = set() - for d in REPLAY_RADAR_DIALECTS: - assert not d.fw & seen, d.name - seen |= d.fw - - def test_every_dialect_has_a_registered_capture(self): - from opendbc.car.mazda import mazdacan - for d in REPLAY_RADAR_DIALECTS: - assert d in mazdacan.RADAR_STATIC_CAPTURES - + def test_g46l_flag_bit_is_unshared(self): + # single-bit members sharing the G46L bit must be its own flag alone: the rebase onto + # the danger-unstable promotion once left LEGACY_FW_EPS and G46L_RADAR both on bit 4, + # and every legacy-firmware car read as carrying the G46L dialect + sharers = [m.name for m in MazdaFlags if m.value & MazdaFlags.G46L_RADAR and bin(m.value).count('1') == 1] + assert sharers == ['G46L_RADAR'] def test_the_platforms_own_radar_fw_stays_parsed(self): - # TRACK_RADAR_FW is derived from the fingerprint database; the CX-5 2022's own radar - # firmware must keep the track-parsing path + # only the G46L set disables parsing: the CX-5 2022's own radar firmware must keep + # the track path own_fw = sorted(FW_VERSIONS[CAR.MAZDA_CX5_2022][(Ecu.fwdRadar, 0x764, None)])[0] CP = car_params(CAR.MAZDA_CX5_2022, car_fw=[radar_fw(own_fw)]) assert not CP.radarUnavailable diff --git a/opendbc/car/mazda/tests/test_mazda_mazdacan.py b/opendbc/car/mazda/tests/test_mazda_mazdacan.py index 064f252ae02..a7bb0c813d2 100644 --- a/opendbc/car/mazda/tests/test_mazda_mazdacan.py +++ b/opendbc/car/mazda/tests/test_mazda_mazdacan.py @@ -10,7 +10,6 @@ import pytest from opendbc.car.mazda import mazdacan -from opendbc.car.mazda.values import G46L, REPLAY_RADAR_DIALECTS from opendbc.car.mazda.tests.conftest import CAM_LANEINFO, LEAD_TRACK, parse_frame @@ -132,21 +131,10 @@ def test_radar_frames_counter_and_lead_track(): def test_g46l_radar_frames_are_the_static_capture_alone(): # the G46L never sends track messages, lead or not: the lead rides CRZ_CTRL alone for lead in (None, (10.25, 0.)): - frames = mazdacan.create_radar_frames(0, 15, lead, dialect=G46L) + frames = mazdacan.create_radar_frames(0, 15, lead, g46l=True) assert [(f.address, f.dat.hex(), f.src) for f in frames] == [(0x499, "0098400000000000", 0)] -def test_registered_static_only_dialects_replay_the_capture_alone(): - # static-only: no track frames whatever the lead, exactly the registered capture - for dialect in REPLAY_RADAR_DIALECTS: - if dialect.sends_tracks: - continue - addr, dat = mazdacan.RADAR_STATIC_CAPTURES[dialect] - for lead in (None, (10.25, 0.)): - frames = mazdacan.create_radar_frames(0, 15, lead, dialect=dialect) - assert [(f.address, f.dat, f.src) for f in frames] == [(addr, dat, 0)], dialect.name - - def test_lead_track_constant_bytes_match_the_stock_release_capture(): # the template's measurement fields are zeroed, so a zero-range lead reproduces it exactly assert mazdacan.create_lead_track(0., 0.) == mazdacan.LEAD_TRACK_TEMPLATE diff --git a/opendbc/car/mazda/tests/test_mazda_vin.py b/opendbc/car/mazda/tests/test_mazda_vin.py index 7a67db2d95b..4388ed3a81e 100644 --- a/opendbc/car/mazda/tests/test_mazda_vin.py +++ b/opendbc/car/mazda/tests/test_mazda_vin.py @@ -9,8 +9,7 @@ from opendbc.car import structs from opendbc.car.fw_versions import match_fw_to_car from opendbc.car.mazda.fingerprints import FW_VERSIONS -from opendbc.car.mazda.values import CAR, STEER_TO_ZERO_EPS_FW, match_fw_to_car_fuzzy -from opendbc.car.values import platform_from_vin +from opendbc.car.mazda.values import CAR, STEER_TO_ZERO_EPS_FW, match_fw_to_car_fuzzy, platform_from_vin from opendbc.car.vin import VIN_UNKNOWN # a steer-to-zero EPS a swap donates; the CX-5 2022 list also carries legacy firmware now diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py index 6b13613f9d9..5aca6ddfb65 100644 --- a/opendbc/car/mazda/values.py +++ b/opendbc/car/mazda/values.py @@ -170,7 +170,7 @@ class MazdaFlags(IntFlag): # Everything keyed on the measured hardware rather than on what the firmware permits. EPS_HW = STEER_TO_ZERO_EPS | LEGACY_FW_EPS - # The G46L replay dialect's bit; see REPLAY_RADAR_DIALECTS below. + # The G46L radar's dialect bit; see G46L_RADAR_FW below. G46L_RADAR = 8 @@ -253,28 +253,12 @@ class LKAS_LIMITS: b'KSD5-3210X-C-00\x00\x00\x00\x00\x00\x00\x00\x00\x00', } -@dataclass(frozen=True) -class RadarDialect: - """A radar dialect the teardown can stand in for, beyond the 2022 track family. - - A talking radar outside TRACK_RADAR_FW gets the vision-only path; the dialects - registered here are the ones whose own wire behavior alpha-long can replay. Adding - one: register it, claim its MazdaFlags bit, and add its static capture to mazdacan.py's - RADAR_STATIC_CAPTURES and to mazda_radar_static_msg_valid in panda safety — the safety - test picks registered captures up by itself. - """ - name: str - fw: frozenset[bytes] # null-stripped firmware strings that speak this dialect - flag: int # the MazdaFlags bit that marks it in CarParams.flags - sends_tracks: bool # False = static-only; the lead rides CRZ_CTRL alone - - # The 2016.5-era radar kept by an EPS-swapped older body. Listed for fingerprinting, but # it never publishes 0x361-0x366 on bus 0; its one frame is fully static — no counter, no # checksum. Stored unpadded; matched with nulls stripped so UDS padding cannot break it. -G46L = RadarDialect(name='G46L', fw=frozenset((b'G46L-67XA1-C',)), - flag=MazdaFlags.G46L_RADAR, sends_tracks=False) -REPLAY_RADAR_DIALECTS = (G46L,) +G46L_RADAR_FW = { + b'G46L-67XA1-C', +} class Buttons: @@ -285,27 +269,34 @@ class Buttons: CANCEL = 4 -def match_fw_to_car_fuzzy(live_fw_versions, vin, offline_fw_versions) -> set[str]: - # After firmware matching fails, require VIN fields to identify one chassis platform. +def platform_from_vin(vin: str) -> str | None: + """The one platform the VIN's fields identify, or None when the VIN is unknown to + every platform or ambiguous. + + Shared by the fuzzy firmware fallback and the selected-car/VIN mismatch warning. + """ if not is_valid_vin(vin): - return set() + return None vin_obj = Vin(vin) chassis_code = vin_obj.vds[0:2] year = vin_obj.vis[0] - candidates = set() - for platform in CAR: - platform_config = platform.config - if vin_obj.wmi in platform_config.wmis and chassis_code in platform_config.chassis_codes and year in platform_config.years: - candidates.add(platform) + candidates = {platform for platform in CAR + if vin_obj.wmi in platform.config.wmis and chassis_code in platform.config.chassis_codes + and year in platform.config.years} + return str(next(iter(candidates))) if len(candidates) == 1 else None - if len(candidates) == 1: - carlog.error(f"Fingerprinted {next(iter(candidates))} by VIN") - return {str(c) for c in candidates} + +def match_fw_to_car_fuzzy(live_fw_versions, vin, offline_fw_versions) -> set[str]: + # After firmware matching fails, require VIN fields to identify one chassis platform. + platform = platform_from_vin(vin) + if platform is not None: + carlog.error(f"Fingerprinted {platform} by VIN") + return {platform} # Only export VINs without model-year data continue to the EPS-swap fallback. - if vin_obj.wmi != WMI.OCEANIA_EXPORT: + if not is_valid_vin(vin) or Vin(vin).wmi != WMI.OCEANIA_EXPORT: return set() # Export-car swaps require a recognized EPS and an engine that identifies one platform. diff --git a/opendbc/car/values.py b/opendbc/car/values.py index 083913bad36..073d161cdd3 100644 --- a/opendbc/car/values.py +++ b/opendbc/car/values.py @@ -21,32 +21,3 @@ PLATFORMS: dict[str, Platform] = {str(platform): platform for brand in BRANDS for platform in brand} - -def platform_from_vin(vin: str) -> str | None: - """The one platform the VIN's fields identify, or None when the VIN is unknown to every - platform or ambiguous. - - Platforms advertise VIN metadata (wmis, chassis_codes, years) on their config; those - without any are skipped. A hardware swap cannot change the VIN, so this is the strongest - signal to check a user-selected platform bundle against. - """ - from opendbc.car.vin import Vin, is_valid_vin - - if not is_valid_vin(vin): - return None - - vin_obj = Vin(vin) - chassis_code = vin_obj.vds[0:2] - year = vin_obj.vis[0] - - candidates = set() - for platform in PLATFORMS.values(): - config = platform.config - wmis = getattr(config, 'wmis', None) - if not wmis: - continue - if vin_obj.wmi in wmis and chassis_code in getattr(config, 'chassis_codes', set()) \ - and year in getattr(config, 'years', set()): - candidates.add(str(platform)) - - return next(iter(candidates)) if len(candidates) == 1 else None diff --git a/opendbc/safety/tests/test_mazda.py b/opendbc/safety/tests/test_mazda.py index fd64853879f..6802ca90fae 100755 --- a/opendbc/safety/tests/test_mazda.py +++ b/opendbc/safety/tests/test_mazda.py @@ -447,16 +447,6 @@ def test_g46l_radar_static_allowed(self): for bus in (0, 2): self.assertFalse(self._tx(common.make_msg(bus, 0x499, 8, bytes.fromhex("0098400100000000")))) - def test_registered_dialect_captures_allowed(self): - # every capture the python registry registers must pass the mirrored whitelist in - # mazda.h: registering a dialect without its safety capture goes red here - from opendbc.car.mazda import mazdacan - for addr, dat in mazdacan.RADAR_STATIC_CAPTURES.values(): - for controls_allowed in (False, True): - self.safety.set_controls_allowed(controls_allowed) - for bus in (0, 2): - self.assertTrue(self._tx(common.make_msg(bus, addr, 8, dat))) - def test_synthetic_lead_radar_track_allowed_disengaged(self): # Permit the measurement fields while requiring the occupied-track template. The slot is # perception, not actuation, so it remains valid with controls_allowed low like stock radar From e360b2ae97f4b14f63f717745ed7b4babfd95f87 Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Mon, 7 Sep 2026 18:38:09 -0400 Subject: [PATCH 13/13] car: drop leftover blank lines from the VIN revert Both files are byte-identical to develop again; they leave the PR diff. --- opendbc/car/car_helpers.py | 1 - opendbc/car/values.py | 1 - 2 files changed, 2 deletions(-) diff --git a/opendbc/car/car_helpers.py b/opendbc/car/car_helpers.py index 8747263752a..c1e8054e35a 100644 --- a/opendbc/car/car_helpers.py +++ b/opendbc/car/car_helpers.py @@ -167,7 +167,6 @@ def get_car(can_recv: CanRecvCallable, can_send: CanSendCallable, set_obd_multip CP.carFw = car_fw CP.fingerprintSource = source CP.fuzzyFingerprint = not exact_match - CP_SP = CarInterface.get_params_sp(CP, candidate, fingerprints, car_fw, alpha_long_allowed, is_release_sp, docs=False) sunnypilot_interfaces(CarInterface, CP, CP_SP, init_params_list_sp, can_recv, can_send) diff --git a/opendbc/car/values.py b/opendbc/car/values.py index 073d161cdd3..1101fb4e700 100644 --- a/opendbc/car/values.py +++ b/opendbc/car/values.py @@ -20,4 +20,3 @@ BRANDS = get_args(Platform) PLATFORMS: dict[str, Platform] = {str(platform): platform for brand in BRANDS for platform in brand} -