Skip to content

Nissan: drive the LKAS HUD from lateral engagement - #318

Open
downquark7 wants to merge 1 commit into
FrogAi:MAKE-PRS-HEREfrom
downquark7:feature/update-carcontroller
Open

Nissan: drive the LKAS HUD from lateral engagement#318
downquark7 wants to merge 1 commit into
FrogAi:MAKE-PRS-HEREfrom
downquark7:feature/update-carcontroller

Conversation

@downquark7

@downquark7 downquark7 commented Aug 20, 2026

Copy link
Copy Markdown

Description

On Nissan the ProPILOT HUD is driven by CC.enabled:

can_sends.append(nissancan.create_lkas_hud_msg(self.packer, CS.lkas_hud_msg, CC.enabled, ...))

nissan/interface.py never sets openpilotLongitudinalControl, so Nissan inherits the defaults (pcmCruise = True, openpilotLongitudinalControl = False) and CC.enabled only goes true when the stock ACC is engaged.

With Always On Lateral that's the wrong condition. AOL steers with cruise off, and controlsd.py already folds it into the lateral flag:

CC.latActive = (self.sm['selfdriveState'].active or self.sm['frogpilotCarState'].alwaysOnLateralEnabled) and ...

So the LKAS frame goes out with LKA_ACTIVE set and real torque behind it, while the HUD frame in the very same tick reports nothing. The cluster shows no steering wheel icon and no green lane lines while the car is actively being steered.

Why the steering wheel icon matters

It's the driver's only in-cluster confirmation that the car is steering itself. The openpilot UI lives on a separate screen that's usually outside the primary sightline, so when AOL is holding the lane and the dash shows nothing, the driver gets a direct contradiction: the wheel is moving on its own but every instrument says the system is off. That's the exact ambiguity a HUD exists to remove — is that resistance in the wheel openpilot steering, or is it a fault? Am I responsible for this lane or is the car?

An honest icon resolves it in both directions. Lit means keep supervising the lateral task. Dark means you own the wheel. Right now it's dark in a state where the driver very much should be supervising. The green lane lines carry the same meaning and are wrong for the same reason, so they follow the same signal here.

Why not just CC.latActive

That was my first instinct and it's too narrow. NissanCarSpecs doesn't override minSteerSpeed (defaults 0.0) and steerAtStandstill is never set, so the gate in controlsd.py reduces to:

standstill = abs(CS.vEgo) <= max(self.CP.minSteerSpeed, 0.3) or CS.standstill   # -> vEgo <= 0.3 or standstill
CC.latActive = ... and (not standstill or self.CP.steerAtStandstill)            # -> ... and not standstill

with no escape hatch. latActive therefore drops at every stop, and the icon would blink off at each red light — a visible regression against today's behavior, where CC.enabled holds through the stop. frogpilotPlan.lateralCheck and steerFaultTemporary add more flicker on top of that.

The EPS declining to hold an angle at 0 mph is an actuation limit, not a change in engagement, so the HUD shouldn't track it.

The fix

Latch the last engaged state across the standstill gate, and keep CC.enabled as a fallback for anyone not running AOL:

if abs(CS.out.vEgo) > max(self.CP.minSteerSpeed, 0.3) and not CS.out.standstill:
  self.lat_engaged_last = CC.latActive
lat_engaged = CC.latActive or CC.enabled or (self.lat_engaged_last and CS.out.cruiseState.available)

The refresh condition is the exact inverse of controlsd's standstill predicate, so the hold covers precisely the window the gate blanks — including creeping between 0 and 0.3 m/s, which CS.out.standstill alone would miss (it's wheel-speed-zero on Nissan, carstate.py:60).

The held state is guarded by cruiseState.available because that is literally what arms AOL on a non-Hyundai platform:

elif frogpilot_toggles.always_on_lateral_main:
  self.always_on_lateral_allowed = carState.cruiseState.available

so switching ProPILOT off while stopped clears the icon on the next frame instead of leaving it lit until the car moves.

Because CC.enabled is still in the disjunction, this is a strict superset of the current condition — it can only ever light the icon where it is currently dark, and can never darken it where it currently lights. No existing behavior changes for anyone not using Always On Lateral.

Scenario Today With this PR
AOL on, cruise off, driving off lit
AOL on, cruise off, creeping < 0.3 m/s off lit
AOL on, cruise off, stopped at a light off lit
Cruise engaged, driving lit lit
Cruise engaged, stopped at a light lit lit
Fully disengaged off off

Scope

Nissan only — nissan/carcontroller.py and nissan/nissancan.py, +14/−5. No shared files, no cereal changes, no new toggle, no safety or DBC changes (every signal already exists with the right value table). create_lkas_hud_msg has exactly one caller, and its enabled parameter is renamed to lat_engaged to match what it now means.

NISSAN_ALTIMA is unaffected — it gets no HUD message at all because of its ADAS bus topology.

Verification

  • Replayed the gate logic across the scenarios in the table above, confirming every AOL case flips off→lit and every existing case is byte-identical, including the drive→stop→ProPILOT-off sequence.
  • Confirmed create_lkas_hud_msg has a single call site, so the signature change is contained.
  • Confirmed against the DBC that LARGE_STEERING_WHEEL_ICON is a lateral indicator: VAL_ 689 LARGE_STEERING_WHEEL_ICON 0 "NO_STEERINGWHEEL" 1 "GRAY_STEERINGWHEEL" 2 "GREEN_STEERINGWHEEL" 3 "GREEN_STEERINGWHEEL_FLASHING".
  • Not yet confirmed on the car — this is static and logic-level verification only. Happy to add a drive if you'd like one before merging.

On not plumbing alwaysOnLateralEnabled

The alternative would be exposing frogpilotCarState.alwaysOnLateralEnabled to the car layer, which reads cleaner but touches cereal and the shared car-port API for a single-brand HUD fix. interfaces.py forwards only (c, self.CS, now_nanos, frogpilot_toggles) to the controller, so there's no way to reach it without widening that signature for every brand. Given cruiseState.available is the same condition AOL arms on, the local version gets the same behavior with no blast radius. Happy to switch if you'd rather have it plumbed properly.

Prior art

Same fix, same reasoning, merged upstream in sunnypilot: sunnypilot/opendbc#371. There it reads CC_SP.mads.enabled, which is their MADS state-machine flag including the paused state — engagement rather than actuation. FrogPilot has no CC_SP equivalent exposed to car ports, hence the latch. Ford, Honda, VW and Chrysler already drive their LKAS indicators from lateral state upstream in opendbc; Nissan and Rivian are the remaining ports still on CC.enabled.

@github-actions

Copy link
Copy Markdown

Thanks for contributing! A maintainer will review your pull request soon.

The PROPILOT HUD was gated on CC.enabled, which on Nissan is only true when
the stock ACC is engaged. Under Always On Lateral openpilot commands the EPS
with CC.enabled false, so the cluster showed no steering wheel icon and no
green lane lines while the car was actively being steered.

CC.latActive on its own is too narrow: the EPS won't hold an angle at a
standstill, so controlsd forces it false below 0.3 m/s and the icon would
blink off at every light. Hold the last engaged state across that same gate,
guarded by cruiseState.available so it clears when ProPILOT is switched off,
and keep CC.enabled as a fallback. The HUD can only light up in cases where
it is currently dark and never goes dark where it currently lights.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@downquark7
downquark7 force-pushed the feature/update-carcontroller branch from 6ec597a to 21a7c28 Compare August 20, 2026 17:32
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