Skip to content

Ford: extend the angle-mode stall rescue below 9 m/s with the pinion measurement#153

Open
jrneulight wants to merge 2 commits into
BluePilotDev:bp-devfrom
jrneulight:bp-7.0-stall-lowspeed
Open

Ford: extend the angle-mode stall rescue below 9 m/s with the pinion measurement#153
jrneulight wants to merge 2 commits into
BluePilotDev:bp-devfrom
jrneulight:bp-7.0-stall-lowspeed

Conversation

@jrneulight

@jrneulight jrneulight commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Ford: extend the angle-mode stall rescue below 9 m/s with the pinion measurement

The post-override stall-rescue pulse is gated above 9 m/s and on the deviation clip
actually binding. Both conditions exist because the yaw-derived measurement is untrustworthy
at low speed. With the pinion measurement enabled (#145, merged) neither reason holds — the
geometric measurement is at its most accurate exactly there — and the current predicate
structurally cannot fire below the gate.

Flag off, gating is bit-identical to today. This was previously part of #145 and was split
out so the measurement feature could be reviewed on its own.

The evidence that motivated this

Route 00000018--9b********, a low-speed drive: silent lane-exit understeer under 10 mph,
delivery falling from 0.42 toward ~0 as speed dropped, with no alert raised.

The stall detector could never have rescued it, and not because of tuning. The predicate is:

_stalled = (... and v_ego > 9.0 and abs(_stall_gap) > _STALL_GAP_MIN ...)
if _stalled:
  if self.bp_curvature_deviation_limited and self.stall_blip_cooldown_s <= 0.0:
    self.stall_blip_hold_s += _STEER_DT

bp_curvature_deviation_limited is set only inside if v_ego > 9: — the deviation clip's
own gate. So below 9 m/s the clip never runs, its flag can never set, and the accumulator
can never charge. The v_ego > 9.0 term makes that unreachable anyway. The two gates are
the same threshold twice, and between them the low-speed case is a dead branch: replaying
route 18's stalled turn, the old gating never fires at any point.

Why the fix is shaped this way

Two changes, both behind bp_pinion_curvature_enabled:

  • The stall gate drops to 5 m/s, not to zero. 5.0 is not a new number — it is
    FORD_PATH_OFFSET_LIMITS.angle_error_min_speed, the low-speed floor ford.h already uses
    for the path-offset check. Reusing the existing floor keeps one notion of "too slow to
    reason about steering geometry" instead of inventing a second.
  • Below the clip's gate the accumulator charges on the raw desired-vs-measured gap
    instead of on the clip's flag, because there the clip provably cannot bind. Above the
    gate, charging is unchanged and still keyed to the clip.

The bare 9 literals became _DEVIATION_CLIP_GATE_MS, so the coupling between the clip's
gate and the detector's gate is visible rather than implied by two matching magic numbers.

Nothing here widens when a pulse is allowed to fire in the yaw case, and nothing changes
the pulse itself.

The evidence afterwards

  • Replay of route 18's stalled turn: the old gating never fires; the new gating fires
    during the stall, ahead of the driver's intervention.
  • Flag off is bit-identical: nothing charges below 9 m/s, unit-tested directly rather
    than argued.
  • Route 00000021--2e1a******** (a dedicated low-speed drive taken to characterise this
    region) shows the honest limit of the approach — see Risks below.

On-road since opening (update): route 00000027--97******** produced a clean
low-speed rescue at 16 mph — delivery recovered to 0.96 after the pulse — exactly the
episode class this extension exists for. Context worth having for review: the low-speed
authority derate has since been root-caused to the PSCM's own availability policy
(broadcast on CAN 972, LaActAvail_D_Actl: a hard ~40 km/h line — see #155). The pulse
cures the press-attenuation component that stacks on top of that policy, not the policy
itself, which matches both this route's success and route 21's honest-limit episodes. A
follow-up (after this lands) gates the pulse on that broadcast so purely policy-bound
deficits stop spending futile 300 ms releases.

What each change is

One commit touching two files: the gate/charging change in lateral_angle_ext.py, and
TestLowSpeedStallRescue in the Ford ext tests — pinion-on rescues below the clip gate,
pinion-on stays inert below 5 m/s, and yaw mode is unchanged below the gate.

Test it yourself

pytest opendbc_repo/opendbc/sunnypilot/car/ford/tests/ -n0

On a car with Use Pinion Yaw Sensor enabled: after a manual correction at low speed,
expect a brief steering release-and-retake pulse instead of a frozen wheel.

Risks / limits

  • Land Ford: fire the stall-rescue pulse only on a fractional delivery deficit #148 first. This increases exposure to the detector's existing arming
    condition (absolute gap only), which route 0000001e--be******** showed also fires on
    honest deep-curve entry transients. Ford: fire the stall-rescue pulse only on a fractional delivery deficit #148 replaces that trigger with a fractional delivery
    deficit (< 0.65×) and was replay-validated jointly with this change. Merged in the other
    order, this widens a known-imperfect trigger into a new speed range.
  • Not yet road-validated on this exact commit. The replay evidence is solid, but the
    acceptance drive predates the current tip; treat the on-road behaviour as unproven.
  • A pulse does not fix everything down here. Route 21 measured a speed-dependent PSCM
    authority derate — experienced delivery ~0.16/0.23/0.47/0.71/0.75 at
    1–2.5/2.5–4/4–5.5/5.5–7/7–9 m/s — that coexists with post-press attenuation. A reset pulse
    addresses only the attenuation, so post-pulse improvement in this band is mixed rather
    than uniform. Below roughly 4 m/s the derate looks like a firmware authority wall that no
    amount of gating or gain can recover; this change is aimed at the 5–9 m/s band where a
    stalled PSCM genuinely can be released.
  • Flag off: no behaviour change (unit-tested).

The post-override stall blip was gated on v_ego > 9.0 and on the deviation clip
actually binding -- both because the yaw-derived measurement is untrustworthy at low
speed. With the pinion measurement enabled those reasons vanish (the geometric
measurement is at its best at low speed), and the old gating provably deadlocks:
below the clip's own 9 m/s gate the clip can never bind, so the accumulator can never
charge, observed on-road as a post-override PSCM stall through an entire ~60 m-radius
turn at 19 mph that ended in a driver rescue.

With STEER_ANGLE_CURVATURE enabled: the stall gate drops to 5 m/s (matching ford.h's
path-offset low-speed floor) and the accumulator charges on the raw desired-vs-measured
gap where the clip cannot bind. Replaying the stalled-turn segment: the old gating
never fires; the new gating fires during the stall, before the driver intervention.
Flag off: gating is bit-identical to before (unit-tested).
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