Skip to content

Ford: keep the angle-mode shadow curvature truthful during overrides#144

Closed
jrneulight wants to merge 2 commits into
BluePilotDev:bp-devfrom
jrneulight:bp-7.0-shadow-race
Closed

Ford: keep the angle-mode shadow curvature truthful during overrides#144
jrneulight wants to merge 2 commits into
BluePilotDev:bp-devfrom
jrneulight:bp-7.0-shadow-race

Conversation

@jrneulight

@jrneulight jrneulight commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Ford: keep the angle-mode shadow curvature truthful during overrides

Angle mode publishes a "shadow curvature" — the kappa its path_angle was derived from —
in Lane_Assist_Data1's unused bits, and ford.h judges it against measured curvature
(angle_meas). That is angle mode's only commanded-vs-measured deviation check. This PR
makes the published value honest in the situations where the planner kappa cannot
describe what the car is actually doing, and ships the replay tool that found (and now
guards) the underlying behavior.

The evidence that motivated this

We built a frame-exact replay of ford.h's LKA/LMC TX-hook semantics
(tools/ford_lmc_safety_replay.py, first commit of this PR) and ran it against ~4.5 h of
real Explorer rlogs (routes 00000001--be********, 00000002--71********,
00000003--a1********, 00000004--f0********; 72,782 LMC frames). Two properties make
its attributions trustworthy rather than guesswork:

  • Ground truth independent of the simulation: a sendcan frame with no TX loopback
    echo in the can stream was actually blocked by the panda, and
    pandaStates.safetyTxBlocked counter deltas cross-check the totals frame-exactly.
  • The reset-bypass latch is modeled. An earlier analysis omitted it and
    mis-attributed blocks; this tool exists so firmware semantics can never silently drift
    from the analysis again.

Findings:

  1. Exactly two real lateral safety blocks occurred in 4.5 h — same mechanism both
    times.
    A driver correcting mid-curve with the mode still enabled: the
    deviation-clipped planner kappa can't follow the wheel, so the shadow exits the error
    band while a sustained curve has drained the latch.

    Route t shadow (CAN) angle_meas window speed pressed counter
    00000003 455.0 s −630 [−795, −782] 11.2 m/s yes 0→2 at 455.0
    00000004 1301.7 s −766 [−942, −927] 10.0 m/s yes 2→4 at 1301.7
  2. 35 latent "re-engage race" frames, none of which ever blocked. On release of a
    human-turn override or stall blip, the old code zeroed the shadow, so the first
    re-enabled LMC frame (20 Hz) races LKA's 33 Hz shadow latch and can present a stale
    zero against real measured curvature. Every one of these frames was bypassed by the
    reset-bypass latch (all 72,782 LMC frames echoed to the bus). The race is real at the
    check-semantics level and would surface under any future latch tightening — but it
    has never blocked anything in current firmware.

  3. The safetyTxBlocked folklore dissolved under attribution. The counter resets
    when the safety model is set, so the 5–9 counts seen at every ignition are
    pre-safety-mode init TX, not drive events; the only other in-drive increments were
    cancel-button frames gated at cruise-state edges (Steering_Data_FD1 passthrough,
    unrelated to lateral).

  4. The latch itself does more than expected: in angle mode the wire curvature is
    always 0, so any straight-driving frame recharges the 60-frame full bypass, and the
    shadow check is only live during >3 s sustained curves (latch empty 23–60% of frames
    depending on route). Documented here honestly; narrowing it is future work this tool
    can validate.

Why the fix is shaped this way

  • All measurement reads are routed through one accessor,
    LateralCurvExt.get_current_curvature(), so the shadow judged by the check and the
    check's own reference can never come from different measurement families.
  • The shadow is published from the measured curvature whenever the planner kappa cannot
    honestly describe the car's steering: while the driver presses (fixes the two real
    blocks), and during human-turn/blip mode-0 pulses and inactive frames (kills the
    re-engage race at the source — the panda-latched shadow tracks reality continuously,
    so the first re-engaged frame compares measured-vs-measured).
  • We deliberately did not add a firmware engage-edge grace: replay shows it would be
    dead code under the current latch. No dead code in safety files.
  • Caveat stated plainly: while the driver presses, the deviation check becomes vacuous
    (shadow ≡ measurement). We consider that correct — during an override the honest
    "command" is the driver's curvature, and the driver has control — but it is a
    reviewer-visible trade-off, not a hidden one.

The evidence afterwards

Re-running the same tool with --truthful-shadow (the fix modeled at real LKA cadence,
so latch-age timing skew is included):

Route shadow-check violations before after predicted real blocks before → after
00000001 2 0 0 → 0
00000002 2 0 0 → 0
00000003 10 (+1 real) 0 1 → 0
00000004 21 (+1 real) 0 1 → 0

No new violations are possible by construction: the published value sits inside
[meas_min − band, meas_max + band] at latch time. On-road confirmation: a ~27 min
acceptance drive on this code (demanding roads, driver corrections included) shows zero
lateral blocks and zero pre-latch shadow signatures — the latent class is empty in
live driving, not just in counterfactual replay. The only residual entries are six
engagement-transition controls_allowed sampling artifacts, unrelated to the shadow
system and all latch-masked. Hands-off behavior is unchanged (unit-tested: the clipped
planner kappa is still published when hands are off).

What each change is

  1. tools/ford_lmc_safety_replay.py (first commit): the full CAN-path TX-hook port —
    value limits, steer_angle_cmd_checks including its controls-not-allowed gate and
    desired_angle_last reset semantics, the shadow check, ROC checks, and the
    reset-bypass latch — plus the echo-based ground truth and the --truthful-shadow
    counterfactual.
  2. lateral_curv_ext.py: get_current_curvature(CS) accessor (yaw-rate
    implementation, identical math to the expressions it replaces).
  3. lateral_angle_ext.py: the four publishing sites (inactive, human-turn override,
    stall blip, driver-pressed) switched from 0.0/clipped-kappa to the accessor.
  4. tests/test_lateral_angle_ext.py: pins the truthfulness contract at all four
    sites and the unchanged hands-off path, with a harness mirroring CarController's
    mixin composition.

Test it yourself

# unit tests (no hardware)
pytest opendbc_repo/opendbc/sunnypilot/car/ford/tests/test_lateral_angle_ext.py

# safety suite: untouched by this PR (control-code only); not-worse-than-baseline
pytest opendbc_repo/opendbc/safety/tests/test_ford.py

# replay any of your own Ford angle-mode routes (rlogs required)
FORD_REPLAY_DONGLE_ID=<your_dongle> tools/ford_lmc_safety_replay.py <route> [--truthful-shadow]

The replay prints, per route: sim violations pre-latch (what would block with no latch),
post-latch (predicted real blocks), the per-check breakdown, the echo ground truth
(sent/echoed/no-echo per address), and every safetyTxBlocked counter increment with
route time — so you can reproduce the attribution table above on our routes or check
your own fleet's behavior before/after this change.

ghbarker added a commit to ghbarker/openpilot that referenced this pull request Jul 20, 2026
…luePilotDev#144 (truthful shadow)

Folds the two open upstream PRs that bear on the Mach-E angle-mode
lateral failures into this branch, adapted to compose with the drift
branch and low-speed boost already here:

- PR BluePilotDev#148: the classic stall branch now arms on a fractional delivery
  deficit (measured < 0.65x desired) instead of desired-leads-measured
  plus the absolute gap alone. Upstream's on-road evidence: the absolute
  condition misfired twice mid-curve on honest deep-curve entry
  transients (0.7-0.85x delivered), releasing steering exactly when the
  car was behind; true stalls measure 0.28-0.59x. The drift branch keeps
  its own near-straight guard and is unaffected.

- PR BluePilotDev#144: bp_kappa_cmd (the shadow curvature ford.h's angle-mode
  deviation check judges against angle_meas) is now published from
  measured curvature whenever the planner kappa cannot honestly describe
  the car's steering: inactive frames, human-turn override, stall-blip
  pulses, and while the driver presses. Kills the stale-zero re-engage
  race at the source -- which matters more on this branch, since the
  drift branch fires mode-0 pulses in situations the old code never did
  -- and fixes the driver-press shadow excursions behind the only two
  real lateral safety blocks in upstream's 4.5h replay. All measurement
  reads route through the new LateralCurvExt.get_current_curvature().

Tests: merged both PRs' unit suites into one file (shared harness),
plus two new drift-branch cases (hands-off drift fires; curve exit with
a genuine command never fires). 9/9 pass locally. Replay of the three
incident segments (4/6/8) is unchanged except one classic fire shifting
0.2s later under the stricter gate -- every fire on this route is a true
stall (0.2-0.3x delivered) or a guarded drift event.

Not folded: PR BluePilotDev#145 (pinion yaw sensor) -- changes panda safety firmware
(ford.h) and is opt-in; deferred until after this branch's road test.
PR BluePilotDev#146 (Expedition FW), BluePilotDev#147 (audio), BluePilotDev#132 (comma-four UI refactor) are
unrelated to this issue.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Frame-exact port of ford.h's Lane_Assist_Data1/LateralMotionControl TX-hook semantics
(value limits, steer_angle_cmd_checks, shadow-curvature check, ROC checks, and -- the
piece an earlier analysis omitted -- the reset-bypass latch) replayed against real
rlogs. Ground truth is independent of the sim: a sendcan frame with no TX loopback echo
in the can stream was actually blocked, and pandaStates.safetyTxBlocked counter deltas
cross-check the totals, so mis-attribution of block causes can't recur.

Includes a --truthful-shadow counterfactual that latches the shadow from measured
curvature on the frames the truthful-shadow control fix republishes, at real LKA
cadence, to validate that fix against recorded routes.
…ing overrides

The shadow curvature (bp_kappa_cmd, carried in Lane_Assist_Data1 bytes 5-6 and judged
against angle_meas by ford.h's angle-mode deviation check) was zeroed during human-turn
overrides, stall blips, and inactive frames, and kept publishing the clipped planner
kappa while the driver pressed. Two problems:

1. Driver pressing mid-curve with the mode still enabled: the clipped planner kappa
   cannot follow the wheel, so the shadow exits the deviation band. Frame-exact replay
   of ~3h of road-test routes (tools/ford_lmc_safety_replay.py) attributed the ONLY
   in-drive lateral safety block to exactly this scenario (sustained curve, latch
   drained, shadow -630 vs measured [-795,-782] CAN units).

2. Zeroed shadow during mode-0/inactive frames: the panda latches the shadow from LKA
   (33 Hz) and enable from LMC (20 Hz), so the first re-engaged frame can compare a
   stale zero against real measured curvature. Today this race is masked by ford.h's
   reset-bypass latch (replay: 14 such frames across three routes, every one bypassed,
   zero actually blocked) -- but the published value was still dishonest, and any future
   tightening of that latch would surface the race.

Fix: route every measurement read through LateralCurvExt.get_current_curvature() and
publish the shadow from it whenever the planner kappa cannot honestly describe the
car's steering -- while pressed, during human-turn/blip mode-0 pulses, and while
inactive. The latched shadow then tracks reality continuously and the check compares
measured-vs-measured at every re-engage edge.

Replay validation (same tool, truthful-shadow counterfactual): shadow-check violations
2/2/14 -> 0/0/0 across the three routes; the one real block is eliminated; no new
violations introduced (the value is inside the band by construction at latch time).
@jrneulight
jrneulight force-pushed the bp-7.0-shadow-race branch from 699c17d to b6c45bc Compare July 20, 2026 14:47
@jrneulight

Copy link
Copy Markdown
Contributor Author

Closing: this already landed in bp-dev as part of #145.

Both commits are ancestors of bp-dev — ee1ff983e9 (LMC safety-check replay tool) and 699c17d9fd (truthful shadow curvature) rode in inside #145's merge, since this branch was #145's base.

Verified in bp-dev rather than assumed:

GitHub couldn't auto-close this because I'd force-pushed a rebased copy of the branch, so the PR head SHAs no longer matched the ones that merged. That's on me — the content is in, nothing is lost.

No action needed; the follow-up work continues in #152 (restores the pinion toggle that the #145 merge dropped from settings_ui.json), #148, and #153.

@jrneulight jrneulight closed this Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant