Ford: opt-in pinion curvature measurement (Use Pinion Yaw Sensor)#145
Merged
Conversation
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. Claude-Session: https://claude.ai/code/session_01VaqLvbbgSL67xLkFUp9sRU
…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). Claude-Session: https://claude.ai/code/session_01VaqLvbbgSL67xLkFUp9sRU
Some Ford RCMs broadcast implausible yaw rate (sign-inverted vs the IMU and steering geometry, corr -0.98 on a 2021 Explorer across three datasets over five months) while the signal's quality flag still reads OK. Since current_curvature is measured from that yaw signal, an affected car sees phantom "steering limit exceeded" blocks at 20-35 mph and weak curve tracking, with a healthy EPS. This adds an opt-in alternative measurement: the steering pinion angle (SteeringPinion_Data, PSCM) converted through the vehicle model -- the same measurement family Tesla/Toyota/Nissan/PSA safety code uses, independently validated against the comma IMU (corr +0.99). Selection is init-time state on CP_SP.safetyParam (FordSafetyFlagsSP.STEER_ANGLE_CURVATURE, set from the FordPrefSteerAngleCurvature toggle in a later commit), never a live Params read: the safety firmware mirrors the same flag from current_safety_param_sp, and the two layers must always agree. Default OFF is a zero-delta: get_current_curvature() returns the yaw-rate measurement exactly as before. FORD_PINION_GEOMETRY_INDEX maps each supported platform to the firmware geometry-table row (Edge excluded: ALT_STEER_ANGLE reads a relative pinion angle and lacks the absolute measurement this needs; the toggle no-ops there).
…RVATURE SP flag
Adds an opt-in alternative angle_meas source for vehicles whose RCM broadcasts
implausible yaw (sign-inverted vs IMU/steering geometry) while its quality flag reads
OK: the steering pinion angle (SteeringPinion_Data, PSCM) converted through the vehicle
model, replay-validated on real routes (bit extraction verified against CANParser on
3000/3000 frames; historical phantom "steering limit exceeded" fault rate 55% -> 0.018%
with this source).
Configuration comes from the sunnypilot SP safety param (current_safety_param_sp,
delivered via USB 0xdf -- the subaru_common.h pattern): bit 0 enables, bits 1-4 select a
row in a per-platform geometry table (slip factor / steer ratio / wheelbase for all 12
supported platforms; FORD_EDGE_MK2 is unsupported -- ALT_STEER_ANGLE reads a relative
pinion angle). A zero or out-of-range index disables the feature outright, so a
half-configured param can never select the wrong geometry.
Default off is a zero-delta by construction:
- the yaw rx block, the 0.002 error band, and the rx-check list are selected exactly
as stock when the flag is clear (the pinion RxCheck is only enforced when the
message is actually consumed, via SET_RX_CHECKS -- a pinion hiccup can never
disable controls for non-users);
- the widened 0.003 band (raw pinion angle has no roll/offset compensation in
firmware) applies only on the pinion path, via a second FORD_LIMITS instantiation.
- StePinAn_No_Cs checksum algorithm is unknown (Ford sum-invert patterns don't match
real frames) -> ignore_checksum; integrity via counter + quality flag + 100Hz check.
Tests: the pinion classes rerun the entire stock matrix with the SP param set
(SAFETY_PARAM_SP, Toyota convention) across three geometries (Explorer primary,
Bronco Sport smallest wheelbase, F-150 largest), plus pinion-specific tests (sign
convention -- the broken-yaw failure mode must block; QF gating; inertness below the
gate speed). The stock classes never set the SP param and pass unchanged (zero-delta
proof). A geometry-consistency test reads the firmware table through new ALLOW_DEBUG
libsafety getters and checks every row against CarSpecs + calc_slip_factor, so the
table cannot rot as platforms change.
…surement FordPrefSteerAngleCurvature (bool, default off, PERSISTENT+BACKUP; UI title "Use Pinion Yaw Sensor"): read once at car init via the sunnypilot params path (initialize_params -> _initialize_ford), which sets FordSafetyFlagsSP.STEER_ANGLE_CURVATURE plus the platform's geometry-table index on CP_SP.safetyParam -- the same value the safety firmware receives as current_safety_param_sp and the control side mirrors, so the two layers can never disagree. Init-time by design (no live Params read, no ccProp): a live flip against stale firmware would fight the panda. card re-reads the param at every car start, so a flip simply applies on the next drive; the sunnylink entry is offroad-only and flagged needs_onroad_cycle, and the BluePilot-menu descriptions say so in plain words. The toggle lives in the BluePilot menu's Vehicle section (plus the mici variant and the sunnylink remote settings). The param is declared in BOTH common/params_keys.h and bluepilot/params/params.json per fork convention -- the params_keys.h registry entry is what card validates keys against. CP_SP.safetyParam stays a plain int (FordSafetyFlagsSP uses plain int constants like its SP siblings): an IntFlag-typed value crashes card's capnp serialization, observed on-device and regression-tested. Platforms without a geometry row (FORD_EDGE_MK2: ALT_STEER_ANGLE reads a relative pinion angle) silently keep stock yaw behavior -- the toggle no-ops rather than half-configuring, matching the firmware's invalid-index handling. The settings descriptions lead with the symptoms of the sensor fault this works around and point at tools/ford_yaw_health_check.py as the "do I need this?" diagnostic.
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>
ford_yaw_health_check.py answers "do I need the steering-angle curvature measurement toggle?" from any drive's logs by comparing the RCM yaw rate against the yaw implied by the steering angle through the vehicle model (with the comma IMU as a mount sanity check). The sign convention is anchored by the on-road-validated pinion measurement (wire-shadow corr +0.989), so both fault classes are caught: sign inversion (negative correlation) and the gain fault actually measured on a faulty 2021 Explorer RCM -- high correlation but a median gain of +2.11x on one drive and +0.45x on another the same day, either of which corrupts the curvature measurement far beyond the safety check's whole error band. ford_pinion_replay.py replays the pinion-sourced firmware check semantics and the Python-layer clip against real rlogs (validation: historical phantom fault rate 55% -> 0.018% with the pinion source on the diagnosis routes). Reads the dongle ID from FORD_REPLAY_DONGLE_ID.
jrneulight
force-pushed
the
bp-7.0-pinion-rework
branch
from
July 20, 2026 14:26
c8214ef to
e0bb63e
Compare
This was referenced Jul 20, 2026
alan-polk
pushed a commit
that referenced
this pull request
Jul 20, 2026
The bp-dev merge in #145 resolved the settings_ui.json conflict by hand and fused two adjacent toggle objects into one: FordPrefSteerAngleCurvature kept its opening brace, then BPUseCustomSounds' keys were appended into the same object instead of starting a new one. JSON allows duplicate keys and the last wins, so the file still parsed and still grepped as containing both toggle names -- but once parsed, the pinion toggle was gone from the sunnylink settings UI along with its offroad_only enablement. The feature itself merged fine; only its remote settings entry disappeared. settings_ui.json is generated from settings_ui_src/ by tools/compile_settings_ui.py. The YAML source is correct and was never affected, so this simply regenerates the committed artifact from it. Verified: tools/compile_settings_ui.py --check now matches (it did not before), and sunnypilot/sunnylink/tests/ goes from 2 failed / 75 passed to 77 passed / 1 skipped.
alan-polk
added a commit
that referenced
this pull request
Jul 21, 2026
…'s shown PR #145 correctly no-ops this toggle in the safety/control layers for Edge MK2 (its pinion sensor only reports a relative angle -- see FORD_PINION_GEOMETRY_INDEX in values_ext.py), but never actually disabled the toggle itself: TICI settings, MICI settings, and the SunnyLink remote config all left it live-clickable with only a description-text mention ("Not available on the Edge") as the safeguard. An Edge owner could flip it, have it silently do nothing, and reasonably conclude some unrelated fix (in one report, a car restart clearing a stale dashcam-mode fingerprint -- CarParamsCache is CLEAR_ON_MANAGER_START, so any reboot alone forces a fresh re-fingerprint) was actually caused by the toggle. - TICI/MICI: grey the toggle via ui_state.CP.carFingerprint. - SunnyLink: added a "platform" capability (mirrors the existing "brand" bundle-first/CP-fallback pattern exactly) since no per-model capability existed to gate on, then a not_ford_edge_mk2 macro referenced from vehicle.yaml. Regenerated settings_ui.json via compile_settings_ui.py (never hand-edited) -- diff is exactly the new enablement rule. Verified: capabilities/settings-schema/ford test suites all pass (same pre-existing, unrelated failures as before this change), ruff clean (same pre-existing issues as before, nothing new), and an end-to-end check that generate_capabilities() resolves platform="FORD_EDGE_MK2" from a synthetic CarParams. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ford: opt-in pinion curvature measurement — "Use Pinion Yaw Sensor"
Opt-in toggle (default OFF; zero behavior change unless enabled) that measures curvature
from the steering pinion angle instead of the RCM yaw rate, in both the control
software and the panda safety firmware, for vehicles whose yaw sensor is faulty.
Depends on #144 (this branch includes it): the shadow published to the angle-mode
deviation check must come from the same measurement family the check consumes.
Scope: this PR is measurement only. The low-speed stall-rescue extension that
previously rode along here has been split out to a follow-up so this can be reviewed and
merged on its own; nothing in this branch changes stall-detector behaviour.
The evidence: what a faulty Ford yaw sensor actually looks like
Measured with the shipped diagnostic (
tools/ford_yaw_health_check.py) across fivemonths of one 2021 Explorer's drives, with the sign convention anchored by the
on-road-validated pinion measurement:
The failure mode is drive-to-drive unstable gain with high positive correlation —
never sign inversion, and invisible to the CAN quality flag: the message transport is
perfect throughout (100.0 Hz, QF=3 on 100% of frames, zero gaps >50 ms). Only the value
is wrong. A 2× gain error at 0.005 curvature is 2.5× the safety check's entire error
band, so the system-level damage on this car was: replayed phantom
"steering limit exceeded" fault rate 55% in the 20–35 mph band, hands-off tracking
ratio 0.88, and a year of "Service AdvanceTrac"-style complaints with a healthy EPS.
Fleet precedent: Tesla, Toyota, Nissan and PSA safety code all measure from the steering
angle sensor. Ford was the outlier in trusting yaw rate.
Why the changes are shaped this way
init and lands on
CP_SP.safetyParam→current_safety_param_sp(the sunnypilot SPsafety param, USB 0xdf — the Subaru STOP_AND_GO pattern), which
ford_initreads. Thecontrol side mirrors the same value. A live flip is deliberately impossible: control
and firmware measuring from different families would make the safety layer fight the
control layer.
(slip factor / steer ratio / wheelbase) selected by a 4-bit index in the SP param.
A zero or out-of-range index disables the feature outright — a half-configured param
can never select the wrong geometry. The table cannot rot: a test reads it back
through ALLOW_DEBUG libsafety getters and checks every row against CarSpecs +
calc_slip_factor(VehicleModel(CP)).relative pinion angle, which openpilot centers with a runtime-learned offset from a
park-assist signal. Learned calibration inside safety firmware — where a few degrees
of unlearned offset equals the whole error band — is not acceptable; the toggle no-ops
there and every description says so.
SteeringPinion_Datacheck(100 Hz, counter, quality flag) is added via
SET_RX_CHECKSonly when the feature ison, so a pinion hiccup can never disable controls for non-users. The OEM checksum
algorithm is unknown (Ford sum-invert patterns don't match real frames) → checksum
ignored, integrity via counter + QF + frequency. Bit extraction was verified against
CANParser on 3000/3000 real frames.
FORD_LIMITSinstantiation):the raw pinion angle carries no roll/alignment-offset compensation in firmware; the
stock path keeps 100 untouched.
The evidence afterwards
tools/ford_pinion_replay.py, diagnosis routes00000001--25********,000000d9--89********): phantom fault rate at 20–35 mph55% → 0.018% (curvature mode), 0.28% overall on the angle-mode shadow check;
Python deviation-clip bite 14.9–17.7% → 4.3–6.0%.
tracking ratio 0.82 → 0.90 (measured against the live learned steerRatio
17.8–18.0; the factory 16.8 reads ~7% higher), under-tracking 26.6% → 15.4%; firmware
shadow-vs-measured wire correlation +0.989 over 11,733 angle-mode frames; a
counterfactual sign flip yields 52% violations — the sign convention is proven, not
assumed.
pinion-measured tracking delivery stayed in a ±2% band (~0.93–0.97 at live steer
ratio) — steering no longer follows the broken sensor.
their per-test outcomes are bit-identical to bp-dev (verified per-method, including
the pre-existing failures); firmware stock path expands to the upstream arrays and
limits exactly.
branch): ~27 min on demanding roads (curvy sections at 100–200 m radii, driver
corrections included). Zero in-drive lateral safety blocks — all 25,003 steering frames
and 41,672 LKA frames reached the bus (echo-verified;
safetyTxBlockedmoved only atignition init) — zero latent shadow-check signatures, and hands-off tracking 0.85 at live
steer ratio. That build also carried the low-speed stall-rescue extension that has
since been split out of this PR (see the scope note above); it acts only below 9 m/s,
so the measurement and tracking figures quoted here are unaffected by it.
What each commit is
get_current_curvature()gains the pinion/vehicle-model source,selected by
FordSafetyFlagsSP.STEER_ANGLE_CURVATUREon CP_SP (plain int constants,sibling convention);
FORD_PINION_GEOMETRY_INDEXplatform map.angle_measblock, geometry table + index handling inford_init, conditional RX checks, 100/150 limit variants; pinion safety-test mixinre-running the entire stock matrix across three geometries (Explorer, Bronco Sport,
F-150) plus pinion-specific tests (the sign-flipped command that a broken sensor
would demand must block; QF gating; inertness below the gate speed) and the
geometry-consistency test.
FordPrefSteerAngleCurvature(params_keys.h + params.json),_initialize_ford, BluePilot menu Vehicle-section toggle + mici variant + sunnylinkentry (
needs_onroad_cycle, offroad-only).ford_yaw_health_check.py(the "do I need this?" diagnostic) andford_pinion_replay.py(the firmware-check replay used for the numbers above).Test it yourself
On a car: BluePilot menu → Vehicle → "Use Pinion Yaw Sensor" → start the car (the
param is read once at car start; the descriptions and the sunnylink
needs_onroad_cycleflag say so). Expect: no phantom steering-limit alerts at 20–35 mph and visibly better curve tracking
on an affected car. Post-drive, the two replay tools reproduce every number in this PR
from your own rlogs.
Risks / limits
and only when the feature is on.
transport on the faulty car is clean, so no practical exposure).