[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:
- What is CAN?
- Setting up SocketCAN on Linux
- SocketCAN Communication with ESP32
- Gripper Motor Control with CAN Bus
- PCAN Device Driver Installation on Linux
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:
- a custom ROS 2 wrapper node
- 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.