Skip to content

mazda: pass the stock camera's LKAS and HUD frames through when disengaged - #8

Merged
zephleggett merged 8 commits into
zoompilot:mazda-devfrom
mzdnick:mazda-lka-relay
Aug 31, 2026
Merged

mazda: pass the stock camera's LKAS and HUD frames through when disengaged#8
zephleggett merged 8 commits into
zoompilot:mazda-devfrom
mzdnick:mazda-lka-relay

Conversation

@mzdnick

@mzdnick mzdnick commented Aug 30, 2026

Copy link
Copy Markdown

Problem

With the device powered but openpilot disengaged, the car lost its stock lane keep. The Mazda safety mode lists CAM_LKAS (0x243) and CAM_LANEINFO (0x440) in its TX allowlists with check_relay = true, so the generic static-forwarding block dropped the camera's copies of those frames from bus 2 to bus 0 at all times, while the controller substituted a zero-torque 0x243 at 100 Hz and a curated 0x440 with LDW_WARN_LL/RL hard-zeroed. The EPS never saw the camera's LKAS_REQUEST, and the dash never saw a departure warning.

Fix

Three coordinated pieces in opendbc/safety/modes/mazda.h, all keyed off controls_allowed || controls_allowed_lateral (the lateral term covers MADS), now one named predicate both hooks read (mazda_openpilot_controlling(), 947e6cc5):

  1. mazda_fwd_hook blocks the camera's 0x243/0x440 only while openpilot is controlling. Otherwise they reach the car whole, so disengaged behavior is stock by construction: thresholds, torque, dash state, LDW, and boot-time FSC health belong to the camera, with nothing decoded, curated, or re-encoded.
  2. Tx-hook veto keeps openpilot idle 0x243/0x440 off the main bus while not controlling. Both decisions read the same panda state, so there is no state in which both senders are active: bus 0 has one sender per state, at frame granularity (the same granularity as the existing tesla pass-through). The veto sits after the steer checks on purpose: steer_torque_cmd_checks resets its rate-limit state on every disengaged frame, keeping limits clean at the next engage edge.
  3. .disable_static_blocking = true on the MAZDA_LKAS / MAZDA_LKAS_HUD entries in both TX lists, the same opt-out tesla uses for selective AEB forwarding. check_relay stays true, so the relay-malfunction watchdog is unchanged.

In op-long disengaged this also enforces one-sender-per-state, removing a latent duplicate-0x243 condition.

Note on 0x440 TX validation: 0x440 remains whitelist-only (no payload checks), unchanged from before this commit, which only narrows 0x440 TX by adding the disengaged veto. That matches the cross-brand pattern for display-only HUD frames (Toyota PCS_HUD, Honda 0x33D LKAS HUD are whitelist-only; messages carrying control bits are validated: Toyota 0x1D2 PCM cancel, Honda nidec ACC_HUD, Mazda CRZ_INFO/CRZ_CTRL/CRZ_BTNS). All documented 0x440 signals are display/warning state, with no actuation content.

Testing

  • test_stock_passthrough (8ec52e0a: moved into common.py as an opt-in, STOCK_PASSTHROUGH_ADDRS with per-mode states and probe): matrix over controls_allowed × controls_allowed_lateral: disengaged forwards camera frames and drops openpilot frames; engaged and MADS lateral-only block the camera. Inherited by the longitudinal safety class.
  • ALLOW_DISENGAGED_STEER_TX knob on the torque test base; FWD_BLACKLISTED_ADDRS = {2: []} for the generic forward test.
  • Full safety suite: 7624 passed; Mazda car tests: 174 passed; 0 failures; no new MISRA findings.
  • On-car 2026-08-29 (CX-5): stock LKA confirmed restored while disengaged: activation, corrective torque, dash indicators.
  • The alpha-long disengaged coexistence drive is the one remaining on-car check.

Remaining CI failures

tests.yml has never run on mazda-dev (prior PRs sat action_required), and the remaining red jobs are pre-existing on base:

  • test models (1): test_car_interface/test_radar_interface on MAZDA_CX9 replay the 2020 route through the python car interface only, with no safety involvement; CarState never reaches canValid under the current carstate code.
  • ./test.sh (both OSes): 5 MISRA findings at mazda.h 254/87/146 that pre-date this PR, plus a ModuleNotFoundError: pytest in modules this PR does not touch.
  • Safety mutation tests (ubuntu): survivor longitudinal.h:26 [comparison] !=->==, a core file this PR does not touch; it survives identically with this PR's files reverted (repro: python opendbc/safety/tests/mutation.py --max-mutants 286), and earlier green runs were the harness counting worker crashes as kills (mutation.py:553/561). macOS passes the same head.

The failures this change initially introduced are fixed: 7aac334d (model tx passes expect the disengaged veto) and bcb670de (cancel-press rx test restores the 100% safety coverage gate). safety and test models (0)/(2)/(3) are green.

AI Usage

Disclaimer: GLM-5.3 by Z.ai was used to help develop, debug, and document this submission. All changes were reviewed and validated by a human.

…gaged

The static forwarding block dropped the camera's CAM_LKAS (0x243) and
CAM_LANEINFO (0x440) from bus 2 to bus 0 whenever the safety mode was
loaded, engaged or not, while the controller substituted its own
zero-torque 0x243 and a curated 0x440. With the device powered but not
engaged that defeated the stock lane keep outright and muted the dash's
departure warnings -- the car behaved worse than stock for every mile
driven disengaged.

Give the mode a fwd hook: while openpilot is not controlling (no cruise
engagement, no MADS lateral) the camera's frames reach the car whole,
so disengaged behavior is stock by construction -- thresholds, torque,
dash icons, boot-time FSC health all belong to the camera again. The
tx hook drops our own idle 0x243/0x440 in that state so the EPS and the
dash see one sender per state; the handover is atomic because both
decisions read the same controls_allowed/controls_allowed_lateral the
rx hook updates. The steer-check block still runs before the veto so
its disengaged-frame state resets keep happening every frame.

The two TX entries keep check_relay, so the harness relay-malfunction
watchdog is unchanged; they only opt out of static blocking, the same
escape hatch tesla uses for selective AEB forwarding.
The design rationale lives on mazda_fwd_hook; the tx-veto comment
carries only the ordering constraint (after the steer checks, which
reset rate-limit state on every disengaged frame), and the passthrough
test section comments shrink to state labels. Names follow their
identifiers: frames by DBC name (CAM_LKAS, CAM_LANEINFO), the test
helper after the DBC message, and the torque-test knob becomes
ALLOW_DISENGAGED_STEER_TX to state the contract directly. No behavior
change.
test_panda_safety_tx_cases asserts every controller emission passes
safety_tx_hook in its disengaged passes. Mazda now drops the idle
CAM_LKAS/CAM_LANEINFO frames at the panda while disengaged
(mazda_fwd_hook forwards the stock camera instead), so those two
addresses are asserted rejected there and allowed in the engaged pass,
pinning the one-sender-per-state contract at the model level.
A cancel press on CRZ_BTNS must always drop controls. No test received
one, which also left the rx hook's cancel branch uncovered and failed
the safety job's 100% line coverage gate.
@github-actions github-actions Bot added the car label Aug 30, 2026
@mzdnick
mzdnick marked this pull request as ready for review August 30, 2026 03:39
@zephleggett

Copy link
Copy Markdown

Reviewed and safe to merge.

The two gates are exact complements on the same globals, evaluated per frame, so bus 0 keeps exactly one sender of 0x243/0x440: the tx hook drops openpilot frames while !(controls_allowed || controls_allowed_lateral), and the fwd hook blocks the camera frames when that same expression is true.

disable_static_blocking does not weaken relay-malfunction protection. safety_fwd_hook short-circuits on relay_malfunction before the static loop, safety_tx_hook returns false on it, and detection in stock_ecu_check keys on check_relay alone, which stays true. This is the same pattern tesla.h uses on 0x488/0x27D for selective pass-through.

CI red is pre-existing: MISRA flags untouched lines (87, 146, 254), test.sh also hits a missing pytest and codespell, and test models is the CX-9 car-interface/radar failure. None come from these hunks.

Follow-up worth doing separately, neither blocking:

  • Factor the shared condition into one helper used by both hooks. The complement is the safety invariant and it is currently written twice, 85 lines apart, at opposite polarity.
  • Confirm the panda tx echo of forwarded 0x243/0x440 does not re-enter the rx hook as bus 0, which would latch a permanent relay_malfunction via safety.h:215. Tesla has the same exposure, so this is likely fine, but it is cheap to check on car.

The tx veto and the fwd block spelled the same engagement predicate in
opposite polarity, in two places; a future flag added to one site would
silently break the complement. Both hooks now read
mazda_openpilot_controlling(), and both address pairs read
mazda_is_lka_addr(). No behavior change.
test_stock_passthrough moves to SafetyTest behind a STOCK_PASSTHROUGH_ADDRS
opt-in, with the gating states and the tx probe supplied by the mode. Mazda
is the only consumer (three engagement states, both camera addrs, both
directions); other brands can adopt it later, e.g. Tesla's
test_stock_lkas_passthrough TODO.
@mzdnick

mzdnick commented Aug 30, 2026

Copy link
Copy Markdown
Author

Helper (947e6cc5): both hooks now read mazda_is_lka_addr() and mazda_openpilot_controlling(), invariant comment at the helper; the tx site keeps its ordering note (veto after the steer checks, which reset rate-limit state on disengaged frames). Pure extract-function, no behavior change; the (unsigned int) casts in the addr helper are MISRA 10.4 load-bearing. 8ec52e0a pins it: test_stock_passthrough is now a common.py opt-in (STOCK_PASSTHROUGH_ADDRS) asserting, per engagement state, that forwarding and the openpilot tx veto are exact complements on both addrs.

Worth noting for future changes - Longitudinal-only engagement (controls_allowed with lateral off: op-long engaged, or stock MRCC while lateral off) counts as openpilot controlling, so stock LKA stays dark there. Deliberate: the controller sends an idle 0x243 every frame (and a 0x440 every 50th) regardless of lateral state, so a lateral-only gate would put two senders with independent counter/checksum domains (frame % 16) on 0x243, the EPS-faulting case. The middle test state is exactly controls_allowed=1, lateral=0, so the mapping changes only as a conscious edit; restoring stock LKA there is possible later but is engaged-state behavior needing its own validation.

The call sites read themselves through the helper names, so the comment keeps
only the invariant, the purpose, and why the predicate is either-axis rather
than lateral-only.
@mzdnick

mzdnick commented Aug 30, 2026

Copy link
Copy Markdown
Author

Three of the four reds are the known pre-existing set; the new one (ubuntu mutation) is too:

  • The survivor is longitudinal.h:26 [comparison] !=->==, a file this PR never touches. It survives on base as well: the harness gives identical survivor sets with and without this PR's changes (repro: python opendbc/safety/tests/mutation.py --max-mutants 286).
  • Earlier greens were luck: mutation.py counts worker crashes as kills (lines 553, 561), so results vary run to run.

Durable fix is upstream: allowlist the survivor or add killing brake tests; happy to send that separately. A rerun may also flake green.

@zephleggett
zephleggett merged commit dcda06d into zoompilot:mazda-dev Aug 31, 2026
10 of 14 checks passed
@zephleggett

Copy link
Copy Markdown

merged, ty!
I'm looking at the failing tests now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants