From 089ff96671ae1a28dbd7cf0c1a66e136b5171de6 Mon Sep 17 00:00:00 2001 From: Max Date: Tue, 25 Aug 2026 12:53:49 +1200 Subject: [PATCH] mazda: fix CX-9 cruise speed for PXM7 PCM --- opendbc/car/mazda/carstate.py | 9 ++- opendbc/car/mazda/interface.py | 7 ++- .../car/mazda/tests/test_mazda_carstate.py | 57 +++++++++++++++++-- opendbc/car/mazda/values.py | 2 + 4 files changed, 68 insertions(+), 7 deletions(-) diff --git a/opendbc/car/mazda/carstate.py b/opendbc/car/mazda/carstate.py index 5b5e7cfba0c..0d38e07a6c0 100644 --- a/opendbc/car/mazda/carstate.py +++ b/opendbc/car/mazda/carstate.py @@ -2,7 +2,7 @@ from opendbc.car import Bus, DT_CTRL, create_button_events, structs from opendbc.car.common.conversions import Conversions as CV from opendbc.car.interfaces import CarStateBase -from opendbc.car.mazda.values import DBC, LKAS_LIMITS, CarControllerParams +from opendbc.car.mazda.values import DBC, LKAS_LIMITS, CarControllerParams, MazdaFlags from opendbc.sunnypilot.car.mazda.carstate_ext import CarStateExt ButtonType = structs.CarState.ButtonEvent.Type @@ -181,7 +181,12 @@ def update(self, can_parsers) -> tuple[structs.CarState, structs.CarStateSP]: ret.cruiseState.enabled = cp.vl["CRZ_CTRL"]["CRZ_ACTIVE"] == 1 self.brake_pressed_prev = ret.brakePressed ret.cruiseState.standstill = cp.vl["PEDALS"]["STANDSTILL"] == 1 - ret.cruiseState.speed = cp.vl["CRZ_EVENTS"]["CRZ_SPEED"] * CV.KPH_TO_MS + cruise_speed_kph = cp.vl["CRZ_EVENTS"]["CRZ_SPEED"] + if self.CP.flags & MazdaFlags.PXM7_CRUISE_SPEED and cruise_speed_kph > 0: + # The shared DBC decodes CRZ_SPEED as raw / 200 - 0.5, which matches other + # Mazdas. Export CX-9s with PXM7 PCMs use (raw + 96) / 196 instead. + cruise_speed_kph = cruise_speed_kph * 50 / 49 + 1 + ret.cruiseState.speed = cruise_speed_kph * CV.KPH_TO_MS # stock lkas should be on # TODO: is this needed? diff --git a/opendbc/car/mazda/interface.py b/opendbc/car/mazda/interface.py index c0a94e38d2f..5ac616f8a8c 100755 --- a/opendbc/car/mazda/interface.py +++ b/opendbc/car/mazda/interface.py @@ -5,7 +5,7 @@ from opendbc.car.mazda.carcontroller import CarController from opendbc.car.mazda.carstate import CarState from opendbc.car.mazda.radar_interface import RadarInterface -from opendbc.car.mazda.values import CAR, DBC, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaSafetyFlags +from opendbc.car.mazda.values import CAR, DBC, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, MazdaFlags, MazdaSafetyFlags class CarInterface(CarInterfaceBase): @@ -20,6 +20,11 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.radarUnavailable = Bus.radar not in DBC[candidate] + if candidate == CAR.MAZDA_CX9_2021 and any( + fw.ecu == 'engine' and fw.fwVersion.startswith(b'PXM7-188K2-') for fw in car_fw + ): + ret.flags |= MazdaFlags.PXM7_CRUISE_SPEED.value + # 2022+ CX-5 EPS can steer to zero and has no hands-off lockout. Detected by EPS firmware # rather than by model, so an EPS swapped into an older Mazda is recognized as what it is. steer_to_zero = candidate == CAR.MAZDA_CX5_2022 or \ diff --git a/opendbc/car/mazda/tests/test_mazda_carstate.py b/opendbc/car/mazda/tests/test_mazda_carstate.py index d1f2ff75b7e..061cac5fe2b 100644 --- a/opendbc/car/mazda/tests/test_mazda_carstate.py +++ b/opendbc/car/mazda/tests/test_mazda_carstate.py @@ -1,6 +1,6 @@ import pytest -from opendbc.car import DT_CTRL, gen_empty_fingerprint +from opendbc.car import DT_CTRL, gen_empty_fingerprint, structs from opendbc.car.common.conversions import Conversions as CV from opendbc.car.mazda.interface import CarInterface from opendbc.car.mazda.values import CAR, CarControllerParams @@ -15,11 +15,24 @@ FAULTED = bytes([0x42, 0b00000001, 0, 0, 0, 0x01, 0, 0]) # ERR_BIT (bit 40) set -def _interface(alpha_long=True): +Ecu = structs.CarParams.Ecu + + +def _engine_fw(version): + fw = structs.CarParams.CarFw() + fw.ecu = Ecu.engine + fw.address = 0x7e0 + fw.subAddress = 0 + fw.fwVersion = version + return [fw] + + +def _interface(alpha_long=True, candidate=CAR.MAZDA_CX5_2022, car_fw=None): fingerprint = gen_empty_fingerprint() - CP = CarInterface.get_params(CAR.MAZDA_CX5_2022, fingerprint, [], alpha_long=alpha_long, + car_fw = car_fw or [] + CP = CarInterface.get_params(candidate, fingerprint, car_fw, alpha_long=alpha_long, is_release=False, docs=False) - CP_SP = CarInterface.get_params_sp(CP, CAR.MAZDA_CX5_2022, fingerprint, [], + CP_SP = CarInterface.get_params_sp(CP, candidate, fingerprint, car_fw, alpha_long=alpha_long, is_release_sp=False, docs=False) return CarInterface(CP, CP_SP) @@ -193,6 +206,42 @@ def test_implausible_frames_read_as_no_limit(self, sign_on, speed_sign): assert ret_sp.speedLimit == 0.0 +class TestCruiseSetSpeed: + @staticmethod + def _decode(candidate, raw, engine_fw=None): + car_fw = _engine_fw(engine_fw) if engine_fw is not None else [] + CI = _interface(alpha_long=False, candidate=candidate, car_fw=car_fw) + payload = raw.to_bytes(2, "big") + bytes(6) + ret = None + for i in range(2): + ret, _ = CI.update([(int(i * DT_CTRL * 1e9), [(0x21F, payload, 0)])]) + return ret.cruiseState.speed / CV.KPH_TO_MS + + @pytest.mark.parametrize(("raw", "cluster_kph"), [ + (6176, 32), + (7352, 38), + (10098, 52), + (19504, 100), + ]) + @pytest.mark.parametrize("engine_fw", [b'PXM7-188K2-D', b'PXM7-188K2-E', b'PXM7-188K2-F']) + def test_pxm7_cx9_uses_cluster_scale(self, raw, cluster_kph, engine_fw): + # Real CRZ_EVENTS samples from the reporter's CX-9 route. + decoded_kph = self._decode(CAR.MAZDA_CX9_2021, raw, engine_fw=engine_fw) + assert decoded_kph == pytest.approx((raw + 96) / 196) + assert round(decoded_kph) == cluster_kph + + def test_us_pxm4_cx9_uses_shared_dbc_scale(self): + raw = 19504 + dbc_kph = raw * 0.005 - 0.5 + assert self._decode(CAR.MAZDA_CX9_2021, raw, engine_fw=b'PXM4-188K2-D') == pytest.approx(dbc_kph) + + @pytest.mark.parametrize("candidate", [CAR.MAZDA_CX5_2022, CAR.MAZDA_CX9]) + def test_shared_dbc_decode_is_unchanged_for_other_platforms(self, candidate): + raw = 19504 + dbc_kph = raw * 0.005 - 0.5 + assert self._decode(candidate, raw) == pytest.approx(dbc_kph) + + class TestCancelUnderBraking: """The availability brake-hold exists for brake-only PEDALS samples that arrive with both bits low mid-press. A wheel CANCEL turns the MRCC main state off for real and must land diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py index e7dfcce8b38..84e33c5a7db 100644 --- a/opendbc/car/mazda/values.py +++ b/opendbc/car/mazda/values.py @@ -99,6 +99,8 @@ class MazdaFlags(IntFlag): # Static flags # Gen 1 hardware: same CAN messages and same camera GEN1 = 1 + # Export CX-9 PCM family whose CRZ_SPEED scale differs from the shared Mazda DBC. + PXM7_CRUISE_SPEED = 2 class MazdaSafetyFlags(IntFlag):