Skip to the lesson
FixTheRobot Home / VLA / Phase 9 / 9.5

VLA · Phase 9 · VLA Deployment and Robot Integration

9.5 — ROS 2 Integration

Connecting the policy to a robot through ROS 2 nodes and messages.

  • Roadmap phase 9
  • VLA Deployment and Robot Integration
  • About 13 minutes to read

ROS 2 is how robot parts usually talk. This lesson explains its core ideas and models them in small Python code you can run, ending with the shape of a policy node.

Claims about papers, models and textbooks carry numbered references, listed with what each one is cited for in the References. Code marked “Toy model” is our own illustration, not the cited system.

Learning objectives

After this lesson, you should be able to:

  • Explain nodes, topics, services and actions.
  • Explain how QoS history depth affects freshness.
  • Describe the structure of a timer-driven policy node.
  • Name where commands go next (ros2_control).
  1. Camera and joint nodes publish observations
  2. The policy node caches the newest of each
  3. A timer ticks at the control rate
  4. The node checks freshness and runs the policy
  5. It publishes the action to the controller side
A policy node in the ROS 2 graph.

The one-sentence idea

Cache in callbacks, act on a timer, check freshness.

Match QoS to the data: fresh frames, safe commands.

ROS 2 in one page

ROS 2 is the most common middleware for connecting parts of a robot. Its building blocks: the system is a graph of nodes; nodes find each other automatically (discovery); they exchange data over topics (publish/subscribe channels, possibly several publishers and subscribers), and offer or use services and actions; nodes can live in one process, several processes or several machines.[1] Delivery is tuned with quality-of-service (QoS) profiles: reliability from TCP-like reliable to UDP-like best-effort, a history of the last N messages or all of them, and further policies such as durability, deadline, lifespan and liveliness.[1]

This course runs in a browser, where ROS 2 cannot run. So the examples below are small Python models of the concepts, written to be faithful to how the pieces behave. Real code uses the ROS 2 client libraries (rclpy for Python), whose exact API depends on your distribution: follow the documentation for yours.

A topic is a named channel

A publisher sends messages to a named topic; every current subscriber to that topic gets them. Publisher and subscriber know nothing about each other, only about the topic name and message type. A minimal model:

class Bus:
    def __init__(self): self.subscribers = {}                       # topic name -> list of callbacks
    def subscribe(self, topic, callback): self.subscribers.setdefault(topic, []).append(callback)
    def publish(self, topic, message):
        for callback in self.subscribers.get(topic, []): callback(message)

bus = Bus()
log = []
bus.subscribe("/camera/front/image", lambda m: log.append(("policy", m)))
bus.subscribe("/camera/front/image", lambda m: log.append(("recorder", m)))
bus.subscribe("/joint_states", lambda m: log.append(("policy", m)))

bus.publish("/camera/front/image", "frame 1")
bus.publish("/joint_states", "joints 1")
bus.publish("/unused", "nobody listens")

for who, what in log: print(who, "received", what)

Output:

policy received frame 1
recorder received frame 1
policy received joints 1

Toy model. A tiny publish/subscribe model, not ROS 2 itself.

Two nodes receive the camera frame, one receives the joint state, and a message on an unused topic goes nowhere without any error: that last point is a classic source of confusion (a mistyped topic name silently receives nothing).

QoS in action: a slow subscriber and the history depth

A policy node is slower than the camera. With “keep last N” history, the middleware keeps only the newest N messages for a subscriber that cannot keep up; older ones are dropped. That is usually what you want for images (lesson 9.1). Simulate it:

from collections import deque

def run(depth, frames=10, consume_every=3):
    queue, processed = deque(maxlen=depth), []                       # maxlen drops the oldest when full
    for t in range(frames):
        queue.append(t)                                              # a frame arrives every tick
        if t % consume_every == consume_every - 1 and queue:        # the policy finishes one frame every 3 ticks
            processed.append(queue.popleft())
    return processed

for depth in (1, 5, 10):
    done = run(depth)
    print(f"depth {depth:>2}: policy processed frames {done}  (newest frame arrived: 9)")

Output:

depth  1: policy processed frames [2, 5, 8]  (newest frame arrived: 9)
depth  5: policy processed frames [0, 1, 4]  (newest frame arrived: 9)
depth 10: policy processed frames [0, 1, 2]  (newest frame arrived: 9)

With depth 1 the policy always works on a recent frame (frames 2, 5, 8); with depth 10 it works through a backlog and its answers are about old frames. Choose the depth to match what you need: freshness for images, completeness for commands you cannot afford to lose.

The shape of a policy node

A typical policy node does three things: subscribe to observations and cache the newest of each; run a timer at the control rate; and in each timer tick, check that the observations are fresh, run the policy and publish an action. Keeping the policy call off the subscription callbacks means a slow model cannot block message handling. Modelled in plain Python with a virtual clock:

class PolicyNode:
    def __init__(self, max_age=0.2):
        self.latest = {}                                             # topic -> (time received, message)
        self.max_age = max_age
        self.published = []
    def on_message(self, now, topic, message):                      # subscription callback: only store
        self.latest[topic] = (now, message)
    def on_timer(self, now):                                        # control-rate timer
        needed = ("image", "joints")
        if any(topic not in self.latest for topic in needed):
            self.published.append((now, "WAIT: missing input")); return
        if any(now - self.latest[topic][0] > self.max_age for topic in needed):
            self.published.append((now, "HOLD: stale input")); return
        self.published.append((now, "ACTION from " + self.latest["image"][1]))

node = PolicyNode()
node.on_timer(0.0)                                                   # nothing has arrived yet
node.on_message(0.05, "joints", "joints@0.05"); node.on_message(0.06, "image", "frame@0.06")
node.on_timer(0.10)                                                  # both fresh
node.on_timer(0.50)                                                  # camera stopped sending
for t, what in node.published: print(f"t={t:.2f}: {what}")

Output:

t=0.00: WAIT: missing input
t=0.10: ACTION from frame@0.06
t=0.50: HOLD: stale input

The node never acts on missing or old input; it says so. Which action to publish in those cases (hold, slow down, stop) is a safety decision; see lesson 9.7.

Handing commands to controllers

In ROS 2 the usual route from commands to motors is ros2_control. Hardware is exposed as state and command interfaces, and controllers (managed by a controller manager, with states unconfigured, inactive and active) read and write them.[2] For trajectories, the Joint Trajectory Controller accepts trajectory messages; its documentation recommends the follow_joint_trajectory action interface when execution monitoring is desired, and requires at least position feedback from the hardware.[2] The next lesson looks at this handover.

Common misconceptions

Misconception 1 “A mistyped topic raises an error.”

Correction It silently receives nothing.

Misconception 2 “Reliable delivery is always best.”

Correction For images, a deep reliable queue adds delay.

Misconception 3 “Run the model inside the subscription callback.”

Correction A slow model then blocks message handling.

Engineering takeaways

  1. Graph of nodes; topics, services, actions.
  2. QoS tunes delivery.
  3. Timer-driven policy node with freshness checks.
  4. ros2_control takes the commands.

Callbacks store; timers act.

Knowledge check

Try each question first, then reveal the explanation.

Question 1

What does keep-last depth 1 do for a slow subscriber?

Explanation

It always keeps only the newest message.

Question 2

Why not run the model in the subscription callback?

Explanation

It would block receiving new messages.

Question 3

Which ROS 2 piece takes joint commands to the hardware?

Explanation

ros2_control controllers and hardware interfaces.

References

Numbered notes in the text, like [1], point here. Each entry says what it is cited for and how that was checked. Papers and books can be revised or misread, so follow the links before relying on a detail. Code marked “Toy model” is our own illustration of an idea, not the code of the cited system.

  1. Open Robotics / ROS 2 community (n.d.). ROS 2 documentation: Concepts and About Quality of Service Settings. docs.ros.org

    Cited for:

    • The ROS graph is the network of nodes and their connections; a node is an entity that uses ROS to communicate, nodes can be in the same process, different processes or different machines, and discover each other automatically. Topics are publish/subscribe channels with possibly several publishers and subscribers; nodes can also provide or use services and actions. Checked against the publication record.
    • A set of QoS policies combine into a profile; reliability can be set from reliable (TCP-like) to best-effort (UDP-like); history keeps the last N samples (depth) or all samples; other policies include durability, deadline, lifespan and liveliness. Checked against the publication record.
  2. ros2_control community (n.d.). ros2_control and Joint Trajectory Controller documentation. control.ros.org

    Cited for:

    • Controllers managed by the controller manager have three main states: unconfigured, inactive and active. A hardware interface exposes the robot’s joints through state interfaces and command interfaces. Checked against the publication record.
    • The Joint Trajectory Controller accepts trajectory messages and, when execution monitoring is desired, the follow_joint_trajectory action interface is recommended; it needs at least position feedback from the hardware. Checked against the publication record.

What’s next

Next: handing policy actions to low-level controllers.

Next lesson

9.6 — Policy execution and low-level controller interfaces

Handing policy actions to the controllers that move the joints.

Back to the VLA roadmap