From 3c8689504faa985dc64307c5edc3f96abc58636b Mon Sep 17 00:00:00 2001 From: MazdaNick Date: Sun, 20 Sep 2026 12:30:17 -0400 Subject: [PATCH] mazda: read invalidLkasSetting from the camera's intervention bits Issue: a settings-menu or camera state can leave the EPS taking our steer request and applying none of it, with no block or fault. Fix: the lane-assist-off state clears both CAM_SETTINGS intervention bits, LKAS_INERVENTION_ON1 and ILKAS_NTERVENTION_ON2, together; either bit alone is a live setting, and cameras assert them unevenly, so CarState raises invalidLkasSetting only when both are clear, replacing the LANE_LINES term. Latch that the camera sent CAM_SETTINGS once, so a car that never sends it cannot read as off, and the state holds through the message's silent cycles. Validation: mazda car suite 546 passed; corpus replay: a single-bit camera no longer reads as off, the lane-assist-off drive stays blocked. --- opendbc/car/mazda/carstate.py | 10 +++++-- .../car/mazda/tests/test_mazda_carstate.py | 30 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/opendbc/car/mazda/carstate.py b/opendbc/car/mazda/carstate.py index ec515fc8f51..817ca018c89 100644 --- a/opendbc/car/mazda/carstate.py +++ b/opendbc/car/mazda/carstate.py @@ -83,6 +83,7 @@ def __init__(self, CP, CP_SP): self.cam_laneinfo_seen = False self.cam_laneinfo_silent_frames = 0 self.cam_empty_seen = False + self.cam_settings_seen = False self.radar_session_refused = False self.radar_session_response = 0 self.fsc_settled_frames = 0 @@ -305,9 +306,11 @@ def update(self, can_parsers) -> tuple[structs.CarState, structs.CarStateSP]: ret.cruiseState.standstill = cp.vl["PEDALS"]["STANDSTILL"] == 1 and not self.CP.openpilotLongitudinalControl ret.cruiseState.speed = cp.vl["CRZ_EVENTS"]["CRZ_SPEED"] * CV.KPH_TO_MS - # Stock LKAS must be active. - # TODO: is this needed? - ret.invalidLkasSetting = cam_laneinfo_fresh and cp_cam.vl["CAM_LANEINFO"]["LANE_LINES"] == 0 + self.cam_settings_seen |= len(cp_cam.vl_all["CAM_SETTINGS"]["LKAS_INERVENTION_ON1"]) > 0 + # Either intervention bit is a live setting: cameras assert them unevenly (a + # TJA-fitted camera holds ON1 clear for a whole drive); only the settings-off + # state clears both. + ret.invalidLkasSetting = self.cam_settings_seen and not any(cp_cam.vl["CAM_SETTINGS"][s] for s in ("LKAS_INERVENTION_ON1", "ILKAS_NTERVENTION_ON2")) if ret.cruiseState.enabled: if not self.lkas_allowed_speed and self.acc_active_last: @@ -387,6 +390,7 @@ def get_can_parsers(CP, CP_SP): cam_messages = [ # Read these optional camera messages without making them part of canValid. ("CAM_LANEINFO", float("nan")), + ("CAM_SETTINGS", float("nan")), ("CAM_TRAFFIC_SIGNS", float("nan")), ("CAM_EMPTY", float("nan")), ("CAM_PEDESTRIAN", float("nan")), diff --git a/opendbc/car/mazda/tests/test_mazda_carstate.py b/opendbc/car/mazda/tests/test_mazda_carstate.py index 0d13dfe641a..e015833c125 100644 --- a/opendbc/car/mazda/tests/test_mazda_carstate.py +++ b/opendbc/car/mazda/tests/test_mazda_carstate.py @@ -734,3 +734,33 @@ def test_rolling_or_leaving_standby_releases(self, release): kw.update(release) rig.step(100, 0, kw.pop('blocked'), **kw) assert not rig.CS.steer_first_engage_hold + + +def test_intervention_bits_become_an_invalid_lkas_setting(): + CI, pk = car_interface(alpha_long=False), packer() + healthy = pk.make_can_msg("CAM_SETTINGS", 2, {"LKAS_INERVENTION_ON1": 1, "ILKAS_NTERVENTION_ON2": 1}) + assert not feed(CI, 0, healthy)[0].invalidLkasSetting + off = pk.make_can_msg("CAM_SETTINGS", 2, {"LKAS_INERVENTION_ON1": 0, "ILKAS_NTERVENTION_ON2": 0}) + assert feed(CI, 1, off)[0].invalidLkasSetting + # the flag holds through the silent cycles between the message's arrivals + for i in range(2, 12): + assert feed(CI, i)[0].invalidLkasSetting + # either bit alone is a live setting: cameras assert them unevenly, so the + # single-bit states must not read as off + on1_only = pk.make_can_msg("CAM_SETTINGS", 2, {"LKAS_INERVENTION_ON1": 1, "ILKAS_NTERVENTION_ON2": 0}) + assert not feed(CI, 12, on1_only)[0].invalidLkasSetting + on2_only = pk.make_can_msg("CAM_SETTINGS", 2, {"LKAS_INERVENTION_ON1": 0, "ILKAS_NTERVENTION_ON2": 1}) + assert not feed(CI, 13, on2_only)[0].invalidLkasSetting + + +def test_cam_settings_absence_never_reads_as_off(): + # the parser's default zeros must not read as off on a car that never sends CAM_SETTINGS + CI = car_interface(alpha_long=False) + for i in range(10): + assert not feed(CI, i)[0].invalidLkasSetting + + +def test_lane_lines_zero_alone_is_not_an_invalid_lkas_setting(): + CI, pk = car_interface(alpha_long=False), packer() + lanes = pk.make_can_msg("CAM_LANEINFO", 2, {"LANE_LINES": 0}) + assert not feed(CI, 0, lanes)[0].invalidLkasSetting