[CAN 06] - From CAN Frames to ROS 2 Control

Connecting CAN-based robot hardware to ROS 2 through custom wrappers and ROS 2 Control hardware interfaces

Previous Posts:

Overview

Once a CAN-based actuator or sensor board is working at the SocketCAN level, the next step is usually integration with ROS 2.

At the CAN level, the interface is frame-based. A device is addressed by a CAN ID, and the payload is interpreted according to a device-specific protocol.

CAN ID: 0x101
Data: [0xA1, 0x00, 0x40, 0x3F, ...]

At the robot-control level, the interface is usually expressed in joint states and joint commands.

joint1 position = 0.52 rad
joint1 velocity = 1.30 rad/s
joint1 command  = 1.57 rad

The main task of a ROS 2 hardware integration layer is to translate between these two representations.

CAN protocol representation
  CAN IDs
  data bytes
  encoder counts
  status flags

ROS 2 control representation
  joint positions
  joint velocities
  joint efforts
  command interfaces
  state interfaces

This post summarizes two common approaches for connecting CAN-based robot hardware to ROS 2:

  1. a custom ROS 2 wrapper node
  2. a ROS 2 Control hardware interface

The focus is not on the full implementation yet, but on the structure of the integration layer.

Two Ways to Connect Hardware to ROS 2

There are two common integration patterns.

Method Description
Custom ROS 2 wrapper A user-defined ROS 2 node handles hardware communication and exposes topics, services, or actions.
ROS 2 Control hardware interface The hardware is implemented as a ROS 2 Control hardware component and accessed through standard command and state interfaces.

Both approaches are valid. The appropriate choice depends on the system size, timing requirements, and expected lifetime of the codebase.

Option 1: Custom ROS 2 Wrapper

In a custom wrapper approach, the hardware communication code is implemented directly inside a ROS 2 node or library used by that node.

For a CAN motor driver, the structure may look like this:

Custom ROS 2 Node
  ├── subscribe to command topic
  ├── encode command into CAN frame
  ├── send CAN frame through SocketCAN
  ├── receive CAN feedback frame
  ├── decode motor state
  └── publish state topic

This approach is flexible. The developer can choose the message format, timing model, state machine, and error handling structure.

It is often sufficient for:

- simple sensors
- diagnostic tools
- actuator bring-up
- gripper prototypes
- one-off hardware tests
- non-real-time monitoring

The main disadvantage is that the developer is responsible for the full structure of the driver.

This includes:

- command storage
- state storage
- timing
- timeout handling
- lifecycle behavior
- safety checks
- controller integration
- simulation/hardware switching

For small systems, this may be acceptable. For larger multi-joint systems, the custom wrapper can become difficult to maintain.

Option 2: ROS 2 Control Hardware Interface

ROS 2 Control separates the controller from the hardware communication layer.

The controller reads state interfaces and writes command interfaces. The hardware interface maps those interfaces to the physical hardware.

For a CAN-based robot, the structure is:

ROS 2 Controller
  ↓
ROS 2 Control
  ↓
Hardware Interface
  ↓
SocketCAN
  ↓
CAN Bus
  ↓
Motor Driver

The hardware interface is responsible for converting between ROS 2 Control interfaces and CAN protocol messages.

A ROS 2 Control hardware interface requires more structure than a custom wrapper. It typically includes:

- lifecycle callbacks
- state interface export
- command interface export
- hardware parameters
- pluginlib registration
- URDF/Xacro configuration
- read() and write() implementation

The advantage is that the hardware can be used with the ROS 2 Control ecosystem. Existing controllers can be reused, and the same robot description can be used to organize joints, interfaces, and controller configuration.

This approach is more appropriate for:

- robot arms
- robot hands
- mobile manipulators
- multi-actuator systems
- systems intended to grow over time
- platforms that need simulation/hardware consistency

Custom Wrapper vs ROS 2 Control

Method Pros Cons Suitable Use Cases
Custom ROS 2 wrapper Flexible, simple to start, easy to modify for bring-up Timing, lifecycle, safety, and controller structure must be implemented manually Sensors, simple actuators, grippers, debugging tools, short-term prototypes
ROS 2 Control hardware interface Standard controller interface, reusable controllers, cleaner scaling to multi-joint systems More boilerplate, requires ROS 2 Control conventions Robot arms, hands, mobile manipulators, long-term robot platforms

A custom wrapper is usually faster for initial hardware testing. ROS 2 Control is usually preferable when the hardware behaves as part of a robot joint system.

Topic Bridges and Low-Level Control

A CAN-to-topic bridge maps CAN frames to ROS 2 messages.

CAN frame
  ↔
ROS 2 topic

This is useful for:

- monitoring CAN traffic
- logging device feedback
- sending test commands
- visualizing sensor data
- diagnostics
- GUI tools

However, a topic bridge is not always the right abstraction for low-level joint control.

ROS 2 topics are message-passing primitives. Joint-level control is usually implemented as a periodic loop.

A typical control loop is:

read state
compute command
write command
repeat

For example, at 1 kHz, this loop must complete every 1 ms. If state feedback and command output are distributed across multiple topic callbacks, the timing becomes harder to reason about.

Important questions include:

- Which state sample was used by the controller?
- When was the command actually sent?
- Was the callback delayed?
- Was the message queued?
- Was the state already stale when the controller used it?

For high-level commands, topic-based communication is usually acceptable. For torque control, impedance control, or fast joint position control, the low-level loop should usually be organized more explicitly.

A practical split is:

Low-level periodic control
  → ROS 2 Control hardware interface

Monitoring, logging, GUI, diagnostics
  → ROS 2 topics

ROS 2 and Real-Time Behavior

ROS 2 does not automatically provide real-time control behavior.

Real-time behavior depends on several layers:

- operating system scheduling
- thread priorities
- executor behavior
- memory allocation
- callback structure
- DDS configuration
- CAN bus load
- SocketCAN receive behavior
- motor driver feedback rate
- actuator-side control rate

For example:

- If the CAN bus is overloaded, ROS 2 does not remove the bus bottleneck.
- If the motor driver sends feedback at 100 Hz, a 1 kHz controller does not receive fresh hardware state at 1 kHz.
- If the control loop blocks while waiting for a CAN frame, the control period is no longer stable.

For many research robots and prototypes, ROS 2 Control is a practical integration layer. For hard real-time motor control or safety-critical industrial systems, the lowest-level control and safety logic should usually be implemented closer to the hardware.

A common layered structure is:

Motor current loop / safety-critical logic
  → motor driver, MCU, FPGA, or RTOS

Joint-level control and hardware abstraction
  → ROS 2 Control or another real-time-capable control process

Planning, visualization, logging, user interfaces
  → ROS 2 nodes and topics

The Core of a Hardware Interface: read() and write()

ROS 2 Control includes several concepts: hardware components, lifecycle states, controller managers, plugins, URDF tags, command interfaces, and state interfaces.

For a hardware developer, the core of the interface is usually concentrated in two functions:

read();
write();

In simplified form:

read()
  hardware feedback → ROS 2 state interfaces

write()
  ROS 2 command interfaces → hardware commands

For a CAN-based robot:

read()
  CAN frame
  → decode protocol data
  → convert raw values to joint units
  → update state interfaces

write()
  command interface
  → convert joint command to protocol units
  → encode CAN frame
  → send frame

Most of the surrounding structure exists to make these two operations fit into the ROS 2 Control execution model.

read()

The read() function updates the hardware state visible to ROS 2 Control.

Suppose a motor driver sends the following feedback frame:

CAN ID: 0x201
Data: [0x34, 0x12, 0xA0, 0x00, 0x10, 0x00, 0x2B, 0x00]

The payload is meaningful only through the motor protocol. For example:

bytes 0-1: encoder position
bytes 2-3: velocity
bytes 4-5: current
byte 6: temperature
byte 7: status flags

The controller should not depend on this representation. It should receive state through standard interfaces:

joint1 position
joint1 velocity
joint1 effort

A simplified read() function is:

hardware_interface::return_type CanSystem::read(
  const rclcpp::Time & time,
  const rclcpp::Duration & period)
{
  // 1. Receive CAN frames
  // 2. Identify devices by CAN ID
  // 3. Decode raw payload data
  // 4. Convert raw values to joint units
  // 5. Update state variables

  return hardware_interface::return_type::OK;
}

The hardware interface usually stores state in member variables:

std::vector<double> hw_positions_;
std::vector<double> hw_velocities_;
std::vector<double> hw_efforts_;

After decoding, read() updates these variables:

hw_positions_[i] = decoded_position_rad;
hw_velocities_[i] = decoded_velocity_rad_per_sec;
hw_efforts_[i] = decoded_torque_nm;

These variables are then exposed through ROS 2 Control state interfaces.

In this sense, read() is not just a socket read operation. It is the conversion from device feedback to robot state.

write()

The write() function sends commands from ROS 2 Control to the hardware.

For example, a controller may produce:

joint1 position command = 1.57 rad

The motor driver may not accept floating-point radians directly. It may expect:

- encoder counts
- a scaled integer
- a device-specific command packet
- position, velocity, gain, and feedforward fields

The write() function converts the command interface value into the device protocol.

A simplified command path is:

1. read command from command interface
2. apply limits or validity checks
3. convert joint units to motor units
4. pack the value into CAN payload bytes
5. set CAN ID
6. send frame

A simplified write() function is:

hardware_interface::return_type CanSystem::write(
  const rclcpp::Time & time,
  const rclcpp::Duration & period)
{
  // 1. Read command variables
  // 2. Convert joint units into protocol units
  // 3. Encode CAN frames
  // 4. Send frames through SocketCAN

  return hardware_interface::return_type::OK;
}

Example:

double q_des = hw_commands_[i];

uint16_t raw_position = encode_position(q_des);

struct can_frame frame;
frame.can_id = command_ids_[i];
frame.can_dlc = 8;

frame.data[0] = raw_position & 0xFF;
frame.data[1] = (raw_position >> 8) & 0xFF;

socket_can_.write(frame);

The exact encoding depends on the motor driver protocol. The role of write() remains the same: convert ROS 2 commands into hardware commands.

Control Loop Structure

A ROS 2 Control update cycle can be represented as:

controller_manager update loop
  ↓
read()
  update hardware state

controller update()
  compute command

write()
  send command to hardware

For a CAN-based system:

Motor driver sends feedback frame
  ↓
SocketCAN receives frame
  ↓
hardware interface read()
  decode frame
  update joint state
  ↓
controller update()
  compute next command
  ↓
hardware interface write()
  encode command
  send CAN frame
  ↓
Motor driver receives command

This loop is the main connection between CAN frames and ROS 2 Control.

The main implementation questions are:

- Which state variables should read() update?
- Which command variables should write() consume?
- How are raw CAN payloads converted to joint units?
- How are joint commands converted to protocol units?
- What happens when feedback stops?
- What happens when a command is outside the valid range?
- How are device faults represented?

These questions define most of the hardware interface behavior.

Unit Conversion

The controller should operate in robot-level units.

position: rad
velocity: rad/s
effort: Nm

The motor driver may operate in protocol-level units.

encoder counts
integer velocity units
current units
scaled torque commands
status bytes

The hardware interface defines the mapping.

read():
  protocol units → robot units

write():
  robot units → protocol units

This mapping should be explicit and centralized. Hidden conversions inside scattered callback code make the driver harder to validate.

Choosing an Integration Approach

Use a custom ROS 2 wrapper when:

- the device is simple
- timing requirements are loose
- the goal is bring-up or diagnostics
- the device is not part of a tight control loop
- the interface is unlikely to grow

Use ROS 2 Control when:

- the hardware represents robot joints
- multiple actuators must be managed together
- existing controllers should be reused
- command and state interfaces should be standardized
- simulation and hardware should share a similar interface
- the system is expected to grow

For a single actuator, either approach may be acceptable. For a robot hand, arm, or mobile manipulator, ROS 2 Control usually provides a cleaner long-term structure.

Summary

CAN-based hardware can be connected to ROS 2 either through a custom wrapper or through ROS 2 Control.

A custom wrapper is simple and flexible, but the driver structure is fully user-defined.

A ROS 2 Control hardware interface requires more boilerplate, but provides a standard separation between controllers and hardware communication.

The core of a ROS 2 Control hardware interface is:

read()
  hardware feedback → ROS 2 state

write()
  ROS 2 command → hardware command

For CAN systems:

read()
  CAN frame → decoded joint state

write()
  joint command → encoded CAN frame

The next step is implementation. The following post will focus on the timing structure of the CAN interface: when frames are read, when commands are sent, how buffers are handled, and how stale or missing feedback should be detected.