🤖 ROS2: Middleware for Real Robotics
The operating system for robots: pub/sub, TF2, Nav2, and VLA integration. ROS2 is the industry standard for production robotics—understanding its architecture is essential for any real-world deployment.
ROS2 architecture: nodes publish to the DDS bus; the VLA Inference node receives sensor data and publishes actions to the hardware.
ROS2 Humble/Iron: DDS Architecture
The “Linux of robotics,” reimagined with distributed middleware and no master node. DDS ensures real-time communication and automatic node discovery across any network topology.
🏗️ What ROS2 is
ROS2 is the standard middleware framework in the robotics industry. Unlike ROS1, it uses DDS (Data Distribution Service) as the transport layer, providing configurable QoS, automatic node discovery, and real-time communication without a central master node.
Any robot going into industrial production uses ROS2. It’s the “Linux of robotics.” Integrating a VLA model into ROS2 is required for real-world deployment. Current distributions (Humble LTS, Iron) are stable and supported through 2027+.
✅ Use ROS2 when
- ✓ Deployment on real hardware with multiple sensors
- ✓ Integration with Nav2/MoveIt2 for planning
- ✓ Team with multiple developers working in parallel
- ✓ Need for rosbag2 to replay data
- ✓ Communication between processes on different CPUs
❌ Avoid when
- ✗ Solo prototype without sensor integration
- ✗ Pure ML training loop (PyTorch is enough)
- ✗ Simulation only in MuJoCo/PyBullet
- ✗ Projects with a team without ROS knowledge
- ✗ Environments where DDS overhead is prohibitive
Topics, Services, and Actions
The three communication patterns in ROS2. Each solves a different problem—choosing the wrong one causes subtle bugs that are hard to debug in production.
Topics — Asynchronous Pub/Sub
Continuous data streaming: camera image, joint states, poses.
A publisher publishes without knowing who's listening. N publishers → N possible subscribers. High frequency, no delivery guarantee by default. Ideal for VLA inference output.
ros2 topic echo /image_raw
ros2 topic hz /vla/actions # medir frequência
Services — Synchronous Request/Reply
One-off calls with guaranteed responses: change mode, activate sensor.
Synchronous by nature — the client waits for the response. Use for configuration and state control, never for streaming. A timeout is required to avoid deadlocks.
ros2 service call /switch_mode std_srvs/srv/SetBool "{data: true}"
Actions — Long Tasks with Feedback
Move the arm to a position, navigate to a waypoint, execute a complex task.
Goal + periodic Feedback + final Result. Can be canceled at any time. VLA receives high-level commands ("pick up object") via Action and publishes low-level actions via Topic.
ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose \
"{pose: {header: {frame_id: 'map'}, pose: {position: {x: 1.0}}}}"
💡 VLA Architecture Tip
VLA inference is published at /vla/actions (Topic, 25 Hz). Task commands arrive as Action Goals. Model configuration arrives via Service. Use rqt_graph to visualize the communication graph and identify bottlenecks.
TF2: 3D Transform Tree
TF2 maintains a tree of transformations between all of the robot’s reference frames, updated in real time. A VLA needs to know exactly where the camera is relative to the gripper.
Typical TF2 tree: world → map → odom → base_link branches into camera and manipulator arm.
📐 Why TF2 is critical for VLA
- → VLA predicts gripper pose in camera frame — TF2 converts to world frame automatically
- → Temporal buffer enables lookups into the past: synchronizes camera (30 Hz) with joint states (1000 Hz)
- →
robot_state_publisherautomatically publishes the TF tree from the robot’s URDF - → No TF2: calculating transformations manually for each frame introduces errors and delays
// Transform lookup in Python (rclpy)
import rclpy
from tf2_ros import Buffer, TransformListener
self.tf_buffer = Buffer()
self.tf_listener = TransformListener(self.tf_buffer, self)
# Converter ponto de camera_frame para base_link
try:
t = self.tf_buffer.lookup_transform(
'base_link', # frame destino
'camera_link', # frame origem
rclpy.time.Time() # timestamp (Time() = mais recente)
)
# t.transform.translation.{x,y,z}
# t.transform.rotation.{x,y,z,w}
except Exception as e:
self.get_logger().warn(f'TF lookup failed: {e}')
Nav2 and MoveIt2: Navigation and Manipulation
Nav2 and MoveIt2 execute the safe path planning delegated by VLA. The combination of VLA (what to do) + Nav2/MoveIt2 (how to get there without colliding) is the standard for mobile manipulators.
🗺 Nav2 — Autonomous Navigation
- • Behavior Trees for complex navigation logic
- • costmap2d: local + global cost map
- • Planner Server: NavFn, Smac (A* on a 2D grid)
- • Controller Server: DWB, MPPI, RPP
- • Recovery Behaviors: spin, back-up, clear costmap
🦾 MoveIt2 — Manipulation
- • SRDF: Semantic Robot Description Format
- • Planning Scene: static and dynamic collisions
- • OMPL planners: RRT*, PRM, EST
- • Servo: real-time control for teleop and VLA
- • Pick-and-Place: grasp → transport → place pipeline
🔗 VLA + Nav2/MoveIt2: division of responsibilities
💡 MoveIt Servo for closed-loop VLA
For VLAs with continuous control (e.g., ACT, Diffusion Policy), use moveit_servo instead of the standard pipeline. Servo accepts high-frequency velocity/pose commands (100+ Hz) and performs collision checking in real time — ideal for policies that continuously publish joint velocities.
ROS2 + VLA: Inference Node
The inference node is the bridge between the ML world and the ROS2 world. Integrating PyTorch/ONNX into ROS2 presents unique challenges: GIL, serialization latency, camera-action synchronization, and GPU memory management.
// VLA node skeleton in rclpy
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image, JointState
from geometry_msgs.msg import TwistStamped
import message_filters
import onnxruntime as ort
import numpy as np
class VLAInferenceNode(Node):
def __init__(self):
super().__init__('vla_inference')
# Carregar modelo ONNX (inferência otimizada, sem GIL pesado)
self.session = ort.InferenceSession(
'policy.onnx',
providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
)
# Sincronizar câmera + joint states temporalmente
img_sub = message_filters.Subscriber(self, Image, '/image_raw')
js_sub = message_filters.Subscriber(self, JointState, '/joint_states')
self.sync = message_filters.ApproximateTimeSynchronizer(
[img_sub, js_sub], queue_size=5, slop=0.05
)
self.sync.registerCallback(self.inference_callback)
# Publisher de ações (25 Hz)
self.action_pub = self.create_publisher(
TwistStamped, '/vla/actions', 10
)
def inference_callback(self, img_msg, js_msg):
# Preprocessar imagem (sem alocação dinâmica no hot path)
img = np.frombuffer(img_msg.data, dtype=np.uint8)
joints = np.array(js_msg.position, dtype=np.float32)
# Inferência ONNX
outputs = self.session.run(None, {
'image': img[None], 'joints': joints[None]
})
action = outputs[0][0] # (6,) joint velocities
# Publicar
msg = TwistStamped()
msg.header.stamp = self.get_clock().now().to_msg()
# ... preencher msg com action
self.action_pub.publish(msg)
def main():
rclpy.init()
node = VLAInferenceNode()
rclpy.spin(node)
if __name__ == '__main__':
main()
✅ Best Practices
- ✓ ONNX Runtime for inference (without PyTorch's GIL)
- ✓
message_filtersfor temporal synchronization - ✓ Pre-allocated numpy buffers (no malloc in the loop)
- ✓ Multi-threaded executor separating CB from inference
- ✓ Composable nodes for zero-copy intra-process communication
❌ Common pitfalls
- ✗ PyTorch in a callback (GIL blocks ROS2 executor)
- ✗
np.zeros()every frame (malloc in the hot path) - ✗ Single-thread executor with 30 Hz camera + inference
- ✗ Ignore DDS serialization latency for large arrays
- ✗ No model warmup (first inference is 10× slower)
💡 TensorRT for maximum performance
To achieve 50+ Hz inference on an embedded GPU (Jetson), convert the model to TensorRT before deployment: trtexec --onnx=policy.onnx --saveEngine=policy.trt --fp16. Typical 3-5× gain over standard ONNX Runtime with INT8/FP16 quantization.
Launch Files and Production Deployment
Deploying robotics systems in production is systems engineering, not just ML. A well-structured launch file with lifecycle management and restart policies is what separates a demo from a real product.
Python Launch File
Orchestrates all nodes with parameters, remappings, and conditionals.
from launch import LaunchDescription
from launch_ros.actions import Node, ComposableNodeContainer
def generate_launch_description():
return LaunchDescription([
Node(
package='vla_inference',
executable='vla_node',
name='vla_inference',
parameters=[{'model_path': 'policy.trt', 'freq_hz': 25}],
remappings=[('/image_raw', '/camera/color/image_raw')]
),
# ... mais nós
])
systemd Unit File
Ensures automatic restart after failures and automatic boot.
[Unit]
Description=VLA Robot System
After=network.target
[Service]
ExecStart=/bin/bash -c "source /opt/ros/humble/setup.bash && \
ros2 launch vla_bringup robot.launch.py"
Restart=on-failure
RestartSec=5s
User=robot
[Install]
WantedBy=multi-user.target
rosbag2 for Playback and Debugging
Records all topics for offline replay and post-mortem analysis.
# Gravar: todos os topics críticos
ros2 bag record /image_raw /joint_states /vla/actions /tf
# Reproduzir com clock simulado
ros2 bag play recording_2026/ --clock
# Inspecionar bag
ros2 bag info recording_2026/
💡 Health Checks with Lifecycle Nodes
In production, use lifecycle_manager of Nav2 as the standard: it checks whether each critical node has reached the state active before enabling the system. If the VLA node doesn't activate within 30 s, the lifecycle manager aborts startup and triggers the fallback policy.
✅ Module Summary
Next module
2.5 — Training: Imitation Learning, ACT, Diffusion Policy