# How DesireHelper Manages Lane Change Decision-Making in openpilot

> Discover how DesireHelper in openpilot uses a finite-state machine to manage lane change decisions. Learn about driver inputs and vehicle state integration for automated maneuvers.

- Repository: [comma.ai/openpilot](https://github.com/commaai/openpilot)
- Tags: internals
- Published: 2026-03-05

---

**The DesireHelper class implements a finite-state machine in [`selfdrive/controls/lib/desire_helper.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/desire_helper.py) that converts driver inputs (blinkers, steering torque, blind-spot alerts) and vehicle state into a high-level desire signal consumed by the model daemon to execute automated lane changes.**

The **DesireHelper** module in the [commaai/openpilot](https://github.com/commaai/openpilot) repository serves as the central decision-making authority for automated lane changes. It bridges raw driver inputs and vehicle dynamics with the neural network's path planning by managing a four-state finite-state machine. This component validates safety preconditions, monitors real-time model confidence, and outputs a discrete desire enum that biases the downstream driving model's trajectory predictions.

## Core State Machine Architecture

The lane change decision-making in DesireHelper revolves around three primary abstractions defined in the source code: state enumerations, directional intent, and safety thresholds.

### LaneChangeState Enumerations

The state machine tracks four distinct phases of a lane change maneuver as defined in the `LaneChangeState` enum:

- **`off`** – Idle state waiting for driver input
- **`preLaneChange`** – Maneuver armed but not yet initiated
- **`laneChangeStarting`** – Active lateral movement detected
- **`laneChangeFinishing`** – Vehicle entering target lane, fading in lane-line probabilities

These states transition unidirectionally through the pipeline based on temporal constraints and model confidence scores.

### Safety Constants and Thresholds

Several critical constants govern the validation logic in [`desire_helper.py`](https://github.com/commaai/openpilot/blob/main/desire_helper.py):

- **`LANE_CHANGE_SPEED_MIN`** – Minimum velocity requirement (approximately 20 mph) preventing low-speed lane changes
- **`LANE_CHANGE_TIME_MAX`** – Hard timeout of 10 seconds to abort stalled maneuvers
- **`DT_MDL`** – Model timestep (~0.05s) used for probability fading and timer increments
- **`lane_change_ll_prob`** – Fade factor that smoothly suppresses lane-line detection influence during active maneuvers

## State Transition Logic in desire_helper.py

The `update()` method in `DesireHelper` processes sensor inputs on every control cycle. The implementation follows a strict hierarchy of pre-condition checks and state-dependent transitions.

### From off to preLaneChange (Arming the Maneuver)

A lane change begins only when specific safety criteria align. According to the source code in [`selfdrive/controls/lib/desire_helper.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/desire_helper.py), the transition from `off` to `preLaneChange` requires:

1. Exactly one turn signal active (`one_blinker`)
2. Rising edge detection (`not self.prev_one_blinker`) to prevent re-triggering
3. Vehicle speed above `LANE_CHANGE_SPEED_MIN`
4. Lateral control currently enabled

```python
if (self.lane_change_state == LaneChangeState.off
    and one_blinker
    and not self.prev_one_blinker
    and not below_lane_change_speed):
    self.lane_change_state = LaneChangeState.preLaneChange
    self.lane_change_direction = self.get_lane_change_direction(carstate)

```

This "arming" phase establishes the intended direction (left/right) while waiting for definitive driver commitment through steering input.

### Initiating the Lane Change (laneChangeStarting)

While in `preLaneChange`, the system continuously monitors for two driver-intention cues before committing to the maneuver:

- **Steering torque** applied in the same direction as the active blinker (`torque_applied`)
- **Blind-spot clearance** confirming no vehicles occupy the target lane (`not blindspot_detected`)

When both conditions satisfy simultaneously, the state advances to `laneChangeStarting`:

```python
if torque_applied and not blindspot_detected:
    self.lane_change_state = LaneChangeState.laneChangeStarting

```

This gate ensures the driver actively steers toward the intended lane while safety systems confirm the space is clear.

### Completion Detection and Fade Logic

Once in `laneChangeStarting`, the helper manages perception reliability through probabilistic fading. The `lane_change_ll_prob` variable decrements each cycle to reduce reliance on lane-line detections while the vehicle occupies the space between lanes:

```python
self.lane_change_ll_prob = max(self.lane_change_ll_prob - 2 * DT_MDL, 0.0)

```

The state transitions to `laneChangeFinishing` when two conditions converge:
- The model's `lane_change_prob` drops below 0.02
- The faded lane-line probability falls below 0.01

During `laneChangeFinishing`, the probability fades back in over approximately one second:

```python
self.lane_change_ll_prob = min(self.lane_change_ll_prob + DT_MDL, 1.0)

```

Once `lane_change_ll_prob` exceeds 0.99, the helper either returns to `off` (if the blinker is off) or re-enters `preLaneChange` to support successive lane changes.

### Timeout Handling and Safety Limits

A safety timer enforces the 10-second maximum duration for any active lane change. The `lane_change_timer` accumulates `DT_MDL` while in `laneChangeStarting` or `laneChangeFinishing` states:

```python
if self.lane_change_state not in (LaneChangeState.off, LaneChangeState.preLaneChange):
    self.lane_change_timer += DT_MDL
else:
    self.lane_change_timer = 0.0

```

If the timer exceeds `LANE_CHANGE_TIME_MAX` or lateral control disables, the state machine immediately resets to `off`, aborting any in-progress maneuver.

## Integration with the Model Daemon

After computing state transitions, `DesireHelper` maps the current `(direction, state)` pair to a high-level desire enum via the `DESIRES` lookup table:

```python
self.desire = DESIRES[self.lane_change_direction][self.lane_change_state]

```

In [`selfdrive/modeld/modeld.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/modeld/modeld.py), the model daemon instantiates a single `DesireHelper` instance (`DH = DesireHelper()`) and reads `DH.desire` each cycle. This value converts to a one-hot vector (`vec_desire`) that conditions the neural network's output trajectories, biasing the model toward completing the initiated lane change path.

## Practical Implementation Example

The following example demonstrates how to instantiate and update the DesireHelper outside the full openpilot runtime, illustrating the state progression from driver input to completion:

```python
from selfdrive.controls.lib.desire_helper import DesireHelper, LaneChangeState
from cereal import car

class MockCarState:
    def __init__(self, vEgo, leftBlinker, rightBlinker,
                 steeringPressed, steeringTorque,
                 leftBlindspot, rightBlindspot):
        self.vEgo = vEgo
        self.leftBlinker = leftBlinker
        self.rightBlinker = rightBlinker
        self.steeringPressed = steeringPressed
        self.steeringTorque = steeringTorque
        self.leftBlindspot = leftBlindspot
        self.rightBlindspot = rightBlindspot

# Initialize helper

dh = DesireHelper()

# Simulate driver activating left blinker at 25 mph with steering torque

car_state = MockCarState(
    vEgo=11.18,  # 25 mph in m/s

    leftBlinker=True,
    rightBlinker=False,
    steeringPressed=True,
    steeringTorque=200,  # Positive indicates left turn intent

    leftBlindspot=False,
    rightBlindspot=False,
)

# Simulate control cycles with decreasing model confidence

for i in range(30):
    # Model confidence drops after frame 10 (simulating lane entry)

    lane_prob = 0.8 if i < 10 else 0.0
    
    dh.update(car_state, lateral_active=True, lane_change_prob=lane_prob)
    
    if i % 5 == 0:
        print(f"Cycle {i:02d}: State={dh.lane_change_state.name}, "
              f"Desire={dh.desire}, LL_Prob={dh.lane_change_ll_prob:.3f}")

```

This output demonstrates the progression: `off` → `preLaneChange` → `laneChangeStarting` → `laneChangeFinishing` → `off`, with `lane_change_ll_prob` decaying during the transition and recovering during completion.

## Summary

- **DesireHelper** in [`selfdrive/controls/lib/desire_helper.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/controls/lib/desire_helper.py) implements a four-state finite-state machine managing automated lane changes in openpilot.
- State transitions depend on **rising-edge blinker detection**, **minimum speed thresholds** (20 mph), **steering torque validation**, and **blind-spot clearance**.
- The **10-second timeout** (`LANE_CHANGE_TIME_MAX`) and **lane-line probability fading** (`lane_change_ll_prob`) provide safety mechanisms against stalled or ambiguous maneuvers.
- The final **desire output** feeds into [`modeld.py`](https://github.com/commaai/openpilot/blob/main/modeld.py) as a one-hot vector that biases the driving model's trajectory predictions during active lane changes.

## Frequently Asked Questions

### How does DesireHelper prevent accidental lane changes?

DesireHelper requires a **rising edge** on the turn signal (`not self.prev_one_blinker`) while the vehicle maintains adequate speed, ensuring the driver intentionally initiated the signal rather than holding it from a previous maneuver. Additionally, the driver must apply **steering torque** in the target direction while blind-spot monitors confirm the lane is clear before the state advances from `preLaneChange` to `laneChangeStarting`.

### What happens if a lane change takes too long?

The system enforces a hard **10-second timeout** via `LANE_CHANGE_TIME_MAX`. If `lane_change_timer` exceeds this threshold while in `laneChangeStarting` or `laneChangeFinishing` states, the helper immediately resets to `off`, aborting the maneuver and restoring full lane-line detection authority.

### Why does DesireHelper fade out lane-line probabilities during a lane change?

The fade logic reduces `lane_change_ll_prob` from 1.0 to 0.0 during `laneChangeStarting` to prevent the vision system from conflicting with the lane change trajectory. As the vehicle crosses lane markings, traditional lane-line detections become unreliable; fading them out allows the model to rely on other environmental cues and the desire signal until the maneuver completes and probabilities fade back in during `laneChangeFinishing`.

### How does the model daemon consume the DesireHelper output?

The model daemon ([`selfdrive/modeld/modeld.py`](https://github.com/commaai/openpilot/blob/main/selfdrive/modeld/modeld.py)) instantiates `DesireHelper` as `DH` and calls `DH.update()` each frame with current vehicle state and model predictions. It then encodes `DH.desire` into a one-hot vector (`vec_desire`) that feeds into the neural network, effectively conditioning the model to generate trajectories consistent with the current lane change state and direction.