Skip to content

Intelligent Cruise Button Management (ICBM)

Code: openpilot/sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py (the servo), opendbc/sunnypilot/car/icbm_actuation_profile.py (per-brand ECU characteristics). Tests: car/tests/test_icbm_servo.py, test_icbm_overshoot.py, test_icbm_sla_*.py (closed loop against a simulated Mazda body ECU).

On button-actuated (non-pcmCruiseSpeed) cars openpilot cannot command acceleration. The stock ACC integrates cruise button presses into a dash set speed and decelerates according to that. ICBM is a servo that walks the dash onto the plan target (longitudinalPlanSP.vTarget) with synthesized presses, and hands the dash back to the driver's setpoint when the limiter releases. Measurements below are from the Mazda CX-5 2022 unless stated.

The servo

States: inactive, preActive (react timer running), holding, increasing, decreasing. Readiness requires CC.enabled and no override/cancel/resume and no physical button held; a driver press drops the servo to inactive and it re-enters through preActive after INACTIVE_TIMER.

Two error bands. Against a limiter-sourced target (SCC vision, SCC map, SLA), whose value jitters 1 to 2 display units frame to frame, the servo reacts only past REACT_DEADBAND = 2. Against the cruise source the target is the driver setpoint, a stable integer, so it is tracked exactly (deadband 1): a dash residual from a dropped press self-heals instead of stranding the dash 1 mph low (the "F2 ratchet"). Any error must persist for REACT_TIMER before acting, so a one-frame target glitch (a bad map sample) cannot trigger a button burst. Once moving, the servo runs to the exact target, not just inside the deadband.

Down moves

A limiter's decel is urgent, so down moves skip the quiet window while the limiter is live. Two guards: a residual overshoot gap left after the source flips back to cruise must not start a fresh descent (the lever is not a destination), and a genuine driver SET+ parks all down moves for DRIVER_PRESS_GRACE_T. Without overshoot in play a down move is a plain setpoint correction and stays unconditional.

Up moves (restore)

On cars whose profile sets decel_needs_stable_setpoint the ECU will not commit to decelerating while the set speed is moving, and limiter dips arrive in trains. Restoring between dips churns the dash and delays the next decel, so an up move waits for the plan target to hold still for RESTORE_QUIET_TIME, on every entry path. The quiet timer is keyed on the raw plan target, not the overshoot-adjusted command: the lever's slow release moved the command every few frames and pinned the timer at zero until the decay finished (route 126: 4.1 s of extra post-curve braking). It is held at zero through a confirm prompt so a decline or timeout still waits a full window.

With a valid vision lookahead (smartCruiseControl.vision.vAheadMin > 0) the profile replaces the stillness heuristic outright: restore immediately when nothing ahead binds below the target, hold while a dip is coming however quiet the target is, and abort a restore in progress when a dip appears. Route 126: 3 of 8 over-ceiling apexes were restore-fed, the car accelerating between bends into the next apex.

A genuine driver SET- parks up moves for the grace window (a refused re-anchor would otherwise restore the baseline right over a fresh -5); a press in the other direction cancels the other grace. buttonEvents carry only the wheel's own presses (forged frames echo on src 128+ and never reach carState), so the grace cannot latch on the servo's own sends.

Actuation profile

ICBMActuationProfile carries what the servo needs to know about a body ECU. DEFAULT_PROFILE is discrete taps only, no grid, no hold, no stable-setpoint requirement: the long-standing ICBM behaviour. A brand changes behaviour only by adding a measured entry.

Mazda CX-5 2022, from a 52-episode driver long-press corpus and an injected-press efficiency analysis over 674 rlog segments:

  • Taps register reliably at 5 Hz and move 1 mph. Pushing to ~9 Hz makes the ECU drop presses: ~0.47 steps per press versus ~0.93 at 5 Hz, so faster is slower.
  • A physical hold snaps the set speed to the next multiple of 5 mph about 0.6 s into the hold, then steps 5 mph every ~0.55 s, sometimes with a trailing step after release. The grid is confirmed in imperial display units only; metric users plan with taps.
  • MRCC will not start decelerating until the set speed stops changing.

The hold fold on other brands

The servo's sustained sends (increaseHold / decreaseHold) are a stream of presses, valid wherever taps are. A brand whose button interface has no native hold cadence (everything but Mazda so far) folds them onto the discrete tap of the same direction (tap_equivalent) instead of rejecting an unknown state, so enabling ICBM on a new brand needs no servo change.

Fast mode

The servo's 10 Hz hold stream never registers on the Mazda ECU as a held button: the wheel keeps broadcasting its genuine button-up frames, which interleave with the forged ones, so the ECU sees paced discrete presses. Across all recorded routes 149 of 149 stream-driven dash steps were 1 mph; route 126 measured 294 of 294 steps at 1 mph, zero grid snaps, 4.1 mph/s under hold frames and 3.8 mph/s under taps. It is still the fastest walk available, so the stream takes any move with real distance (FAST_MODE_MIN = 3 units remaining) and taps take the remainder, where the stream's in-flight frames would overshoot and ping-pong. If the dash does not move for FAST_STALL_T under the stream this ECU is not registering it at all; the servo faults to taps for the rest of the drive and logs icbm_fast_mode_fallback. The stream carries no grid or metric assumption, so metric users get it too.

Decel overshoot

A stock ACC's deceleration scales with the gap between the dash set speed and the actual speed, not the target. Commanding dash = target produces almost nothing until the car is already several mph over it, so it arrives at curves hot. When a limiter source demands decel (aTarget < -min_decel and vEgo > vTarget), the servo commands the dash below vEgo by the gap that yields the requested decel, capped at the plan target from above (down-only: a stale command fail-safes to the car slowing). The command tracks vEgo down through the manoeuvre and rises back on its own as the car converges and aTarget relaxes.

Mazda inverse map, from 422k hands-off cruise samples across 447 rlog segments: about 0.09 m/s^2 per mph of gap, dead below ~2 mph, saturating near -0.75 m/s^2 by ~9 mph. gap_v carries a lead over the steady-state inverse because the gap the ECU actually sees lags the command: the lever's rise is limited by the dash walk (~4 mph/s), not by DECEL_OVERSHOOT_RISE, so a manoeuvre spends its first seconds short of the request. Route 135 measured a 2.65 s median from a limiter taking the plan source to the car pulling -0.5 m/s^2; commanding the deeper gap up front pays that walk back, and the request falls as the car converges so the lever still lets go on its own.

The lever is only valid while the servo can pull it and while the limiter that asked for it is live. It never integrates behind a block (driver press, confirm prompt, SET+ grace): winding up there only banks a stale gap to dump when the block lifts, and a limiter still asking rebuilds a full gap in ~0.5 s at the rise rate. It releases slowly while the limiter is live (aTarget flaps between the ECU's coast, downshift and brake stages) and at the build rate once the plan is back on cruise, where a residual only holds the dash down and stalls the restore.

Restore quiet window

Sized from an 11-route, 57k-frame sweep of the recorded target streams, scoring "regret" (restores that a following dip made pointless) against speed lost to waiting. The churn suppression is all bought in the first second: regret 67.7% with no wait, 27.0% at 1.0 s; 3.0 s only reaches 26.2% while nearly doubling the speed given up.

Interaction with the cruise arbiter

The servo reads the SLA session state one message hop late. A pending confirm prompt (session_state == preActive) parks any move and holds the quiet timer at zero; card additionally vetoes emission with same-frame state (cruise-arbiter.md).

Constants

name value measurement route
INACTIVE_TIMER 0.4 s upstream settle after readiness n/a
REACT_TIMER 0.3 s glitch filter, upstream n/a
REACT_DEADBAND 2 units (limiter) / 1 (cruise) limiter jitter 1 to 2 units/frame ICBM corpus
RESTORE_QUIET_TIME 1.0 s regret 67.7% -> 27.0%; 3.0 s reaches 26.2% at twice the speed cost 11 routes, 57k frames
DRIVER_PRESS_GRACE_T 3.0 s +5 reverted within 1.4 s route 126 t=341
FAST_MODE_MIN 3 units stream in-flight overshoot below this route 126
FAST_STALL_T 1.5 s dash never moved under the stream n/a
decel_bp / gap_v (mazda) [0.02..0.73] m/s^2 -> [2..10] mph ~0.09 m/s^2 per mph, dead < 2 mph, saturates ~9 mph 422k samples, 447 segments
max_gap (mazda) 10 mph response saturated same
min_decel (mazda) 0.15 m/s^2 gentle coast-downs left to stock same
DECEL_OVERSHOOT_RISE 10 mph/s full gap in ~0.5 s, inside REACT_TIMER n/a
DECEL_OVERSHOOT_RELEASE 3 mph/s no pumping between ECU decel stages route 126
tap_rate_hz (mazda) 5 Hz 0.93 steps/press at 5 Hz vs 0.47 at ~9 Hz 674 segments
longpress_step / first / period (mazda) 5 mph / 0.6 s / 0.55 s physical hold grid 52 episodes
servo walk rate (mazda) 4 mph/s 294/294 steps at 1 mph; 4.1 hold, 3.8 taps route 126

Tried and rejected

  • Taps at ~9 Hz. The ECU drops presses; net progress is half that of 5 Hz.
  • Planning synthesized holds on the 5 mph grid. Forged holds never snap; 149/149 stream-driven steps were 1 mph. The native grid timing only applies to a physical hold and must not size the actuation lead either (scc-curve-planning.md).
  • A 3.0 s restore quiet window. Regret improves by 0.8 points over 1.0 s while the speed lost to waiting nearly doubles.
  • A deadband against the cruise-source target. Stranded the dash 1 mph under the setpoint after a dropped press.
  • The preActive route bypassing the quiet window. The servo chased a stale target before card had settled the press's own effects.
  • Integrating the overshoot behind a blocked emission. A confirm prompt banked a gap for its whole 5 s window and the timeout dumped it as a SET- burst (user report 2026-08-29).
  • Releasing the lever slowly after the source is back on cruise. The residual held the dash down and stalled the restore.
  • Keying the quiet timer on the overshoot-adjusted command. Pinned at zero by the lever's own decay (route 126, 4.1 s extra braking).
  • Restoring on target stillness when a vision lookahead is available. Restored between bends and fed the next apex (route 126, 3 of 8 over-ceiling apexes).
  • An immediate walk-back after a genuine driver press. Reads as a fight (route 126 t=341).