diff --git a/opendbc/car/mazda/interface.py b/opendbc/car/mazda/interface.py index 81c9a12f4e8..f1129767a74 100755 --- a/opendbc/car/mazda/interface.py +++ b/opendbc/car/mazda/interface.py @@ -6,8 +6,8 @@ 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 DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, STEER_TO_ZERO_PLATFORMS, SUPPORTED_PLATFORMS, MazdaFlags, \ - MazdaSafetyFlags, WMI, platform_from_vin +from opendbc.car.mazda.values import DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, STEER_TO_ZERO_PLATFORMS, SUPPORTED_PLATFORMS, \ + TRACKLESS_DIALECT_PLATFORMS, MazdaFlags, MazdaSafetyFlags, WMI, platform_from_vin from opendbc.car.vin import Vin, is_valid_vin from opendbc.sunnypilot.car.mazda.values import MazdaFlagsSP @@ -47,12 +47,11 @@ 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 - # 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) + # Alpha-long silences the radar and stands in for it, so it needs the radar's dialect, not + # its tracks (bus claim, G46L fw, or the trackless registry). 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 + or candidate in TRACKLESS_DIALECT_PLATFORMS) ret.openpilotLongitudinalControl = alpha_long and ret.alphaLongitudinalAvailable if ret.openpilotLongitudinalControl: ret.safetyConfigs[0].safetyParam |= MazdaSafetyFlags.LONG.value @@ -62,10 +61,8 @@ def _get_params(ret: structs.CarParams, candidate, fingerprint, car_fw, alpha_lo ret.stopAccel = -1.024 # stock MRCC standstill command 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. - # Docs mode carries no real EPS firmware, so leave dashcamOnly at the default. if not docs: - ret.dashcamOnly = candidate not in SUPPORTED_PLATFORMS and not steer_to_zero + ret.dashcamOnly = candidate not in SUPPORTED_PLATFORMS carlog.debug({"event": "mazdaRadarVerdict", "radarUnavailable": ret.radarUnavailable, "platformClaim": Bus.radar in DBC[candidate], "g46lRadar": g46l_radar, "steerToZeroEps": steer_to_zero}) diff --git a/opendbc/car/mazda/tests/test_mazda_interface.py b/opendbc/car/mazda/tests/test_mazda_interface.py index 3f3ceb3b158..d4d40c1ba35 100644 --- a/opendbc/car/mazda/tests/test_mazda_interface.py +++ b/opendbc/car/mazda/tests/test_mazda_interface.py @@ -16,7 +16,8 @@ from opendbc.car.mazda.fingerprints import FW_VERSIONS from opendbc.car.mazda.interface import CarInterface from opendbc.car.mazda.tests.conftest import CAM_LKAS, CAM_LANEINFO, DBC_NAME, car_interface, car_params, car_params_sp -from opendbc.car.mazda.values import CAR, DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, STEER_TO_ZERO_PLATFORMS, MazdaFlags, MazdaSafetyFlags +from opendbc.car.mazda.values import CAR, DBC, G46L_RADAR_FW, LKAS_LIMITS, STEER_TO_ZERO_EPS_FW, STEER_TO_ZERO_PLATFORMS, \ + TRACKLESS_DIALECT_PLATFORMS, MazdaFlags, MazdaSafetyFlags Ecu = structs.CarParams.Ecu @@ -57,18 +58,17 @@ def radar_fw(version: bytes) -> structs.CarParams.CarFw: class TestMazdaEpsSwap: """A 2022+ CX-5 EPS swapped into an older Mazda brings the EPS-derived behavior with it. - Pre-2022 Mazdas are dashcam only because their EPS locks steering out after ~5 s hands-off - and below 45 kph. That lockout lives in the EPS, so the swap lifts it. Everything keyed on - the radar, camera or vehicle dynamics must stay keyed on the model. + The swap lifts what lives in the EPS (floor, tune profile, alpha-long offer); the model + keeps everything else. """ - def test_stock_older_mazda_is_dashcam_only(self): + def test_stock_older_mazda_drives_above_its_floor(self): CP = car_params(CAR.MAZDA_CX5, car_fw=eps_fw(STOCK_CX5_EPS_FW)) - assert CP.dashcamOnly + assert not CP.dashcamOnly assert CP.minSteerSpeed == pytest.approx(MIN_STEER_SPEED_STOCK_EPS, abs=5e-8) assert CP.steerActuatorDelay == pytest.approx(0.14, abs=5e-8) - def test_swapped_eps_lifts_dashcam_and_the_speed_floor(self): + def test_swapped_eps_lifts_the_speed_floor(self): CP = car_params(CAR.MAZDA_CX5, car_fw=eps_fw(SWAPPED_EPS_FW)) assert not CP.dashcamOnly assert CP.minSteerSpeed == 0 @@ -106,14 +106,11 @@ def test_supported_platforms_are_unchanged(self): @pytest.mark.parametrize("candidate", list(CAR)) @pytest.mark.parametrize("swapped", [False, True], ids=["stock", "swapped_eps"]) def test_alpha_long_follows_the_eps(self, candidate, swapped): - # alpha long is offered wherever the steer-to-zero EPS is: the platforms that ship it and any - # Mazda with that EPS swapped in. A stock older EPS cuts lateral below 45 kph, so - # stop-and-go would run unsteered; those cars are not offered it. Neither is a platform - # whose DBC has no radar bus (the pre-2021 CX-9), since the port has never seen its radar + # the steer-to-zero gate keeps stop-and-go off an EPS that floors lateral below 45 kph car_fw = eps_fw(SWAPPED_EPS_FW) if swapped else None CP = car_params(candidate, car_fw=car_fw, alpha_long=True) - has_radar_dbc = Bus.radar in DBC[candidate] - expected = (candidate in STEER_TO_ZERO_PLATFORMS or swapped) and has_radar_dbc + has_dialect = Bus.radar in DBC[candidate] or candidate in TRACKLESS_DIALECT_PLATFORMS + expected = (candidate in STEER_TO_ZERO_PLATFORMS or swapped) and has_dialect assert CP.alphaLongitudinalAvailable == expected assert CP.openpilotLongitudinalControl == expected assert bool(CP.safetyConfigs[0].safetyParam & MazdaSafetyFlags.LONG.value) == expected @@ -124,10 +121,10 @@ def test_stock_long_still_reads_the_radar_tracks(self): 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 + # EPS swap still lifts the speed floor stock = car_params(CAR.MAZDA_CX5_KE) assert stock.radarUnavailable - assert stock.dashcamOnly + assert not stock.dashcamOnly swapped = car_params(CAR.MAZDA_CX5_KE, car_fw=eps_fw(SWAPPED_EPS_FW)) assert swapped.radarUnavailable @@ -159,15 +156,18 @@ 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 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. + # docs feed the car picker, which drops dashcamOnly platforms; every platform stays selectable 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 not CP.dashcamOnly +def test_every_gen1_platform_is_admitted(): + for candidate in CAR: + assert not car_params(candidate).dashcamOnly, candidate + + class TestMazdaLegacyFwEps: """The 2022 EPS hardware behind firmware that keeps the 45 kph floor (the THACO-built CX-5 2023, EPS K319-3210X-B-00). The measured slew, driver window and road-speed ceiling follow @@ -209,9 +209,9 @@ def test_2022_body_with_an_unlisted_eps_gets_the_floor_not_a_silent_latch(self, assert CP.minSteerSpeed == pytest.approx(MIN_STEER_SPEED_STOCK_EPS, abs=5e-8) assert not CP.dashcamOnly - def test_legacy_firmware_in_an_older_body_stays_dashcam(self): + def test_legacy_firmware_in_an_older_body_keeps_the_floor(self): CP = car_params(CAR.MAZDA_CX5, car_fw=eps_fw(LEGACY_FW_EPS)) - assert CP.dashcamOnly + assert not CP.dashcamOnly assert CP.flags & MazdaFlags.LEGACY_FW_EPS @pytest.mark.parametrize("candidate, car_fw", [ @@ -231,8 +231,8 @@ def test_every_non_steer_to_zero_eps_is_on_the_hardware_envelope(self, candidate assert CP.safetyConfigs[0].safetyParam & MazdaSafetyFlags.LEGACY_FW_EPS.value assert CP.minSteerSpeed == pytest.approx(MIN_STEER_SPEED_STOCK_EPS, abs=5e-8) assert CP.steerActuatorDelay == pytest.approx(0.14, abs=5e-8) - # support itself is still the platform's call: only the CX-9 2021 drives among these - assert CP.dashcamOnly == (candidate != CAR.MAZDA_CX9_2021) + # every platform is admitted; the legacy EPS keeps its floor + assert not CP.dashcamOnly def test_no_mazda_is_left_on_the_upstream_envelope(self): for candidate in CAR: @@ -267,7 +267,7 @@ def test_g46l_runs_vision_only_behind_a_radar_claim(self): 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 + # and its dialect can be replayed, so alpha long is offered behind 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 @@ -280,6 +280,16 @@ 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_trackless_cx9_unlocks_alpha_long_behind_a_swap(self): + assert not car_params(CAR.MAZDA_CX9, alpha_long=True).alphaLongitudinalAvailable + + CP = car_params(CAR.MAZDA_CX9, car_fw=eps_fw(SWAPPED_EPS_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) + 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 diff --git a/opendbc/car/mazda/values.py b/opendbc/car/mazda/values.py index 267d3aaf618..835d3b895df 100644 --- a/opendbc/car/mazda/values.py +++ b/opendbc/car/mazda/values.py @@ -261,8 +261,11 @@ class LKAS_LIMITS: # Platforms that ship the steer-to-zero EPS from the factory: what an unread EPS falls back to. STEER_TO_ZERO_PLATFORMS = frozenset({CAR.MAZDA_CX5_2022, CAR.MAZDA_CX8_2023}) -# Bodies supported on their stock EPS; any other Mazda needs a steer-to-zero EPS swapped in. -SUPPORTED_PLATFORMS = STEER_TO_ZERO_PLATFORMS | {CAR.MAZDA_CX9_2021} +SUPPORTED_PLATFORMS = frozenset(CAR) + +# The pre-2021 CX-9's radar isn't known to publish 0x361-0x366, so its platform claims no radar bus; +# its dialect is assumed 2022-family (shared K123/K131 fw, no capture) and replays standard. +TRACKLESS_DIALECT_PLATFORMS = frozenset({CAR.MAZDA_CX9}) # 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