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.
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).
- Camera and joint nodes publish observations
- The policy node caches the newest of each
- A timer ticks at the control rate
- The node checks freshness and runs the policy
- It publishes the action to the controller side
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
- Graph of nodes; topics, services, actions.
- QoS tunes delivery.
- Timer-driven policy node with freshness checks.
- 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.
(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.
(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.