PTENES
Start / Technical / Module 2.4
MODULE 2.4

🤖 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.

6
Topics
45
Minutes
Technical
Level
Practice
Type
DDS Middleware — Topics · Services · Actions 📷 Camera Node /image_raw 🔄 TF2 Tree /tf · /tf_static 🧠 VLA Inference /vla/actions · 25 Hz 🦾 MoveIt2 /joint_trajectory 🗺 Nav2 /cmd_vel 🤖 Robot Hardware ros2_control · HAL drivers · actuators

ROS2 architecture: nodes publish to the DDS bus; the VLA Inference node receives sensor data and publishes actions to the hardware.

1

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
DDS
Fast-RTPS or Cyclone DDS as the implementation
Nodes
Independent processes with single/multi-thread executors
QoS Profiles
reliability, durability, history configurable per topic
colcon
Build system: colcon build --symlink-install
Lifecycle Nodes
States: unconfigured → inactive → active → finalized
ament
ament_cmake for C++, ament_python for Python
2

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.

3

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.

🌐 world 🗺 map 📍 odom 🤖 base_link 📷 camera_link 🦾 tool0 ✋ gripper_link

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_publisher automatically 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}')
4

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

🧠
VLA
Decide what do: "pick up the red cup," "go to the table"
↓
Action Goal
6-DOF pose or 2D waypoint sent as Action
🦾
Nav2 / MoveIt2
Resolve such as get there without collisions or singularities

💡 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.

5

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_filters for 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.

6

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.

1

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
    ])
2

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
3

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/
Lifecycle Nodes
configure → activate → deactivate → cleanup
Parameters YAML
params.yaml loaded via launch, overridable at runtime
Docker + ROS2
osrf/ros:humble-desktop as the base; DDS via host network
Event Handlers
OnProcessExit: restart node if it crashes during launch
Launch Arguments
ros2 launch pkg robot.launch.py use_sim:=true
rosbag2
Faithful dataset replay for offline training

💡 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

✓
DDS without a master node — ROS2 uses Data Distribution Service with configurable QoS; any node automatically discovers any other node on the network.
✓
Topics / Services / Actions — each pattern solves a problem: asynchronous streaming, synchronous request/reply, and long-running tasks with feedback and cancellation.
✓
TF2 is required for VLA — converts poses from the camera frame to the world frame in real time, with a temporal buffer for sensor synchronization.
✓
Optimized inference nodes — ONNX/TensorRT, message_filters, pre-allocated buffers, and a multi-threaded executor are the difference between 5 Hz and 50 Hz control.

Next module

2.5 — Training: Imitation Learning, ACT, Diffusion Policy