How openpilot Handles CAN Communication: Architecture and Implementation

openpilot handles CAN communication through a layered architecture that abstracts hardware I/O via the Panda device, decodes messages using DBC-driven parsers, and routes commands through vehicle-specific CarInterface implementations.

The commaai/openpilot repository implements a sophisticated vehicle communication system that translates raw CAN bus traffic into structured self-driving commands. This architecture separates hardware access from high-level logic, enabling support for hundreds of vehicle models through a unified interface. Understanding how openpilot manages CAN (Controller Area Network) messaging reveals the design patterns that make cross-platform autonomous driving feasible.

The Five-Layer CAN Communication Stack

openpilot's CAN handling follows a strict separation of concerns across five architectural layers, each implemented in specific modules within the repository.

Hardware Interface Layer

The lowest layer manages physical CAN bus access through the Panda device—a dedicated CAN router that bridges the vehicle's OBD-II port to the host computer via USB or Ethernet. The pandad daemon (selfdrive/pandad/pandad.py) initializes the Panda hardware, reads raw CAN frames, and publishes them to the internal messaging system.


# Simplified excerpt from pandad.py

can_raw = panda.can_recv()
msg = messaging.new_message('can')
msg.can = can_raw
pm.send('can', msg)

Message Bus Layer

Raw CAN frames travel through Cereal, openpilot's typed Pub/Sub messaging framework. This decouples hardware producers from consumers like the control logic and UI. The can topic carries raw frame data, while pandaStates carries device health information. Services are defined in cereal/services.py, ensuring synchronized inter-process communication across the entire system.

CAN Parsing and Packing Layer

The transition between raw bytes and Python objects happens in the opendbc module. CANParser (opendbc/can/parser.py) converts incoming hex frames into typed signals using vehicle-specific DBC (Database CAN) files. Conversely, CANPacker (opendbc/can/packer.py) assembles outgoing actuator commands into byte arrays formatted for the vehicle's ECU.

Car Interface Abstraction

Vehicle-specific logic resides in selfdrive/car/interface.py and its per-make implementations (e.g., honda_interface.py). Each CarInterface instantiates a CANParser for inbound signals and a CANPacker for outbound commands, providing a uniform API regardless of manufacturer. This abstraction allows the main control loop to call generic update() and apply() methods without knowing underlying CAN IDs or scaling factors.

Control Loop Integration

The main controlsd process (selfdrive/control/controlsd.py) closes the feedback loop. It subscribes to parsed CarState objects, runs longitudinal and lateral planners, and transmits steering torque and braking commands back through the CANPacker to the Panda hardware.

Step-by-Step CAN Message Flow

Understanding the end-to-end pipeline requires tracing a single frame from the vehicle bus to the actuation command and back:

  1. Hardware Acquisition: pandad polls the Panda device for new frames via panda.can_recv() and publishes them on the can topic.

  2. Subscription: The car interface's SubMaster receives raw frames through sm['can'].

  3. Parsing Initialization: During setup, CarInterface.__init__ creates a CANParser loaded with the vehicle's DBC definition and required message checks:

    self.parser = CANParser(dbc_name, checks, bus=0)
  4. Signal Extraction: Each control iteration feeds raw messages to the parser, which updates a typed CarState object:

    can_msgs = sm['can']
    self.parser.update(can_msgs)
    car_state = CarState.from_parser(self.parser)
  5. Decision Computation: The planner consumes CarState to calculate desired steering angles and acceleration profiles.

  6. Command Packing: The CANPacker constructs specific messages like LKAS_COMMAND or ACC_COMMAND:

    packer = CANPacker(dbc_name)
    commands = packer.make_can_msg('STEERING_TORQUE', 0, [torque_l, torque_r])
  7. Transmission: Packed frames return through the messaging bus to pandad, which writes them to the vehicle CAN bus via panda.can_send().

Practical Code Examples

Instantiating Parser and Packer for a Specific Vehicle

When implementing support for a new model, developers instantiate the parsing stack with the appropriate DBC file:

from opendbc.can.parser import CANParser
from opendbc.can.packer import CANPacker

DBC = "honda_civic_2022"
checks = [
    ("STEERING_TORQUE", 100),
    ("VEHICLE_SPEED", 50),
]

parser = CANParser(DBC, checks, bus=0)  # Inbound parser

packer = CANPacker(DBC)                 # Outbound packer

This pattern appears in tools/sim/lib/simulated_car.py, demonstrating how the same abstraction works in simulation environments.

Subscribing to CAN Messages in Control Processes

Any process requiring vehicle state creates a SubMaster listening to the can topic:

import messaging

sm = messaging.SubMaster(['can'], poll='can')
while True:
    sm.update()  # Blocks until new frames arrive

    can_raw = sm['can']
    parser.update(can_raw)
    speed = parser.vl["VEHICLE_SPEED"]  # Access decoded signal

This implementation mirrors the pattern found in selfdrive/control/controlsd.py, where the main control thread waits for fresh CAN data before executing planning logic.

Sending Actuator Commands

To command steering torque, the controller packs values according to the DBC specification and publishes via the Panda driver:

torque = int(0.2 * 0x1000)
msg = packer.make_can_msg("STEERING_TORQUE", bus=0,
                          values=[torque & 0xFF, (torque >> 8) & 0xFF])

pm = messaging.PubMaster(['can'])
pm.send('can', messaging.new_message('can', 0, [msg]))

This code structure, derived from selfdrive/car/honda/car_controller.py, ensures that high-level torque requests translate to correctly formatted CAN frames on the appropriate bus.

Summary

  • Hardware abstraction: The Panda device and pandad daemon handle all physical CAN I/O, isolating the main system from USB and timing details.
  • DBC-driven parsing: CANParser and CANPacker in opendbc/ use Database CAN files to translate between raw bytes and semantic signals without vehicle-specific code changes.
  • Layered architecture: Five distinct layers—hardware, messaging, parsing, interface, and control—ensure that adding new vehicles requires only DBC files and interface subclasses rather than core logic modifications.
  • Pub/Sub decoupling: The Cereal messaging system allows asynchronous communication between pandad, controlsd, and car-specific interfaces through the can topic.

Frequently Asked Questions

How does openpilot support multiple vehicle manufacturers with one codebase?

openpilot uses DBC (Database CAN) files to describe each vehicle's unique CAN message layout. The generic CANParser and CANPacker classes read these definitions at runtime, while vehicle-specific CarInterface subclasses handle semantic mapping. This means the core control logic in controlsd remains manufacturer-agnostic, calling uniform methods like update() and apply() regardless of whether the vehicle is a Honda, Toyota, or Volkswagen.

What is the role of the Panda device in CAN communication?

The Panda acts as a safety-focused CAN router and translator. It connects to the vehicle's OBD-II port and exposes a USB/Ethernet interface to the host computer. The pandad process manages this connection, reading raw frames via panda.can_recv() and writing commands via panda.can_send(). This hardware isolation ensures that the main openpilot processes never directly access CAN buses, providing a security and safety boundary.

How are CAN message frequencies and timeouts enforced?

When initializing a CANParser, developers provide a checks list containing tuples of (message_name, frequency_hz). The parser tracks arrival timestamps for each defined message and sets a valid flag to False if expected messages arrive late or stop appearing. This mechanism, visible in selfdrive/car/interface.py, allows safety-critical code to detect communication failures and enter a fallback state immediately.

Where does the conversion from high-level steering commands to CAN bytes happen?

The translation occurs in two stages. First, the CarController class (e.g., selfdrive/car/honda/car_controller.py) calculates desired actuator values like steering torque. Then, it calls CANPacker.make_can_msg() with the signal name and physical values. The packer lookups the DBC definition to apply scaling factors and byte offsets, returning a raw CAN frame that pandad transmits to the vehicle.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →