Skip to content

Ford: parse the PSCM LatCtl status broadcast + opt-in hands-on context for steerSaturated#155

Open
jrneulight wants to merge 2 commits into
BluePilotDev:bp-devfrom
jrneulight:bp-7.0-pscm-status
Open

Ford: parse the PSCM LatCtl status broadcast + opt-in hands-on context for steerSaturated#155
jrneulight wants to merge 2 commits into
BluePilotDev:bp-devfrom
jrneulight:bp-7.0-pscm-status

Conversation

@jrneulight

@jrneulight jrneulight commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

The evidence that motivated this

  • The Q3 PSCM broadcasts its own lateral-control status on Lane_Assist_Data3_FD1 (CAN 972, 33 Hz) on CAN platforms too — openpilot only parses the message on CANFD, and only for a signal that is dead on Q3 (LatCtlSte_D_Stat).
  • Mining five drives (00000027--97********, 00000010--93********, 00000001--be********, 00000002--71********, 00000003--a1********) found two live, load-bearing signals:
    • LaActAvail_D_Actl — the PSCM declares lateral actuation unavailable below a single sharp ~40 km/h line: 130 of 153 observed 0↔2 transitions sit at 24.2–25.0 mph in both directions, avail=0 covers 100% of sub-threshold frames on every route, and it reads 0 even while disengaged with nothing on the wire (it is the PSCM's own speed policy, not a reaction to commands). This is the root cause of the long-observed Q3 low-speed authority derate: below the line the PSCM serves LatCtl commands in a degraded best-effort regime. Per the DBC VAL_ table this is a feature matrix — bit1 = LCA/LKA centering available, bit0 = LDW not suppressed; states 1/3 are the LDW-available variants seen while active lateral control isn't running.
    • LaHandsOff_B_Actl — a graded hands-on detector ~2x more sensitive than steeringPressed: it flags hands at 0.5–1.0 Nm column torque in 82–93% of frames where steeringPressed fires 2–9%.
  • steerSaturated storms on windy roads trace to sub-threshold resisting hands (delivery drops to ~0.95 vs ~0.99 for helping hands), and the PSCM's own hands bit confirms hands-on through most alert windows — the alert mostly tells a hands-on driver what their hands are doing.

Why the fix is shaped this way

  • Telemetry first. The message joins the parser as non-critical (float('nan') frequency, the BCM_Lamp_Stat_FD1 pattern) with a seen-latch, so a Ford that doesn't broadcast it can never break CAN validity and dataAvailable stays honest.
  • A global alternative — raising the steerSaturated angle-error threshold (controls: raise the angle-mode steerSaturated threshold 2.5 -> 3.5 deg #154) — was
    considered and withdrawn: it trades alert timing for every driver to address a cause
    specific to hands-on driving. This toggle addresses the cause directly and only for
    drivers who opt in.
  • The alert context is opt-in and conservative. With the previously-unwired FordPrefHideSteerSaturatedAlerts toggle enabled, steerSaturated is hidden only while the EPS itself reports hands on the wheel; if the PSCM reports hands-off, the alert always shows. Default OFF — zero behavior change until opted in.
  • carStateBP joins selfdrived's SubMaster ignore lists: only brands whose carstate builds the message publish it (Ford today), so it must never gate all_checks()/commIssue on other cars.
  • The param is registered in common/params_keys.hbluepilot/params/params.json alone is not consulted by Params(), and reading an unregistered key raises UnknownKeyName (found the hard way: selfdrived crash-looped on-device until this line existed).

The evidence afterwards

Validation drive 0000002a--ce******** (8 segments): live-parsed values vs an independent raw-CAN bit decode of 972 = 0.00% mismatch on both signals over 41,714 samples; live avail transitions at 24.6–24.9 mph, right on the 40 km/h line; carStateBP steady at 100 Hz; zero process crashes.

What each change is

  • cereal/custom.capnp: CarStateBP.pscmLatCtl (dataAvailable, laActAvail, laActDeny, laHandsOff, tjaHandsOnConfidence).
  • opendbc/car/ford/carstate.py: Lane_Assist_Data3_FD1 added to the non-CANFD parser list, non-critical.
  • opendbc/sunnypilot/car/ford/carstate_ext.py: seen-latched populate of the new struct.
  • selfdrive/selfdrived/selfdrived.py: SubMaster entry (ignore-listed) + the opt-in hands-on alert context.
  • common/params_keys.h: register FordPrefHideSteerSaturatedAlerts.
  • settings_ui_src/pages/vehicle.yaml + regenerated settings_ui.json (via tools/compile_settings_ui.py; --check clean): the toggle.
  • selfdrive/ui/bp/layouts/settings/bluepilot.py + mici/.../vehicle_mici.py: the same toggle on the on-device vehicle settings (both surfaces, directly under the pinion toggle).

Test it yourself

  • Any Q3 CAN drive: carStateBP.pscmLatCtl.laActAvail flips 0↔2 crossing ~40 km/h; cross-check against raw CAN 972 (Motorola bits 5–4 of byte 0).
  • Toggle OFF (default): no behavior difference anywhere.
  • Toggle ON: steerSaturated appears only when the PSCM reports hands-off during the episode.

Risks / limits

  • Non-Ford cars: the message is never published; the ignore lists + seen-latch make the whole path inert (selfdrived tests green; process replay unaffected).
  • The hands-on suppression can hide an honest saturation alert while the driver is physically holding the wheel — that is its purpose, it is opt-in, and hands-off alerts are never suppressed.
  • avail consumers should key on bit1 (values 0/1 = centering policy-suppressed); bit0 only reflects LDW suppression.

… carStateBP

The PSCM broadcasts its lateral-control availability policy and hands-on
estimate on Lane_Assist_Data3_FD1 at 33Hz on CAN platforms too, not just
CANFD. Parse it (non-critical, seen-latched) and publish LaActAvail_D_Actl,
LaActDeny_B_Actl, LaHandsOff_B_Actl and TjaHandsOnCnfdnc_B_Est under
carStateBP.pscmLatCtl so every route carries the PSCM's own account of
whether it is serving lateral control and whether it detects hands.
Per the DBC VAL_ table LaActAvail_D_Actl is a feature matrix: bit1 =
LCA/LKA centering available, bit0 = LDW not suppressed.
@jrneulight
jrneulight force-pushed the bp-7.0-pscm-status branch from a19a2e3 to 61fa7c1 Compare July 20, 2026 19:15
…dcast

The param existed but was connected to nothing. Wire it conservatively:
steerSaturated is hidden only while the EPS itself reports hands on the
wheel (LaHandsOff_B_Actl == 0) -- the PSCM detects grip at ~0.5 Nm where
steeringPressed needs ~1.0, and those sub-threshold resisting hands both
cause the alert storms and prove the driver is already holding the wheel.
If the EPS reports hands-off, the alert always shows. The toggle is exposed
on both settings surfaces: the sunnylink source (settings_ui.json
regenerated with the compiler) and the on-device vehicle settings (TICI and
MICI layouts, directly under the pinion toggle).
carStateBP joins selfdrived's SubMaster ignore lists: only brands whose
carstate builds the message publish it (Ford today), so it must never gate
all_checks()/commIssue on other cars.
The key is registered in common/params_keys.h -- bluepilot/params/params.json
is a separate registry that Params() does not consult, and reading an
unregistered key raises UnknownKeyName (selfdrived crash-looped on device
until this line existed).
@jrneulight
jrneulight force-pushed the bp-7.0-pscm-status branch from 61fa7c1 to 4f5ac1a Compare July 20, 2026 19:19
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