Chapter 12: ROS/ROS2 Foundations (Practical Intro)
Lesson 2: Nodes, Topics, Services, Actions
This lesson formalizes the core ROS/ROS2 communication primitives—nodes, topics, services, and actions. We move from conceptual definitions to a rigorous view based on directed graphs, sampled-data streams, and queueing/latency analysis. We then implement each primitive in Python, C++, Java, and MATLAB/Simulink, emphasizing how architectural choices affect correctness and real-time behavior.
1. Nodes as Computational Graph Vertices
A node is the smallest executable unit in ROS/ROS2. Conceptually, ROS systems form a directed communication graph: \( G = (V, E) \), where each vertex \( v \in V \) is a node, and each directed edge \( e \in E \) is a typed communication channel.
We partition the edge set into disjoint classes: \( E = E_T \cup E_S \cup E_A \), corresponding to topics (publish–subscribe), services (request–response), and actions (goal–feedback–result).
\[ E_T = \{(v_i, v_j, \mathsf{topic}\,k)\},\quad E_S = \{(v_i, v_j, \mathsf{service}\,\ell)\},\quad E_A = \{(v_i, v_j, \mathsf{action}\,m)\}. \]
Each node defines: (i) a state \( x_i(t) \), (ii) callbacks that update state by consuming incoming messages, and (iii) outputs that publish or respond. If we discretize by callback executions indexed by \( n \in \mathbb{N} \), a node update can be modeled as:
\[ x_i[n+1] = f_i\!\big(x_i[n],\, u_i[n]\big), \]
where \( u_i[n] \) collects the messages received by node \( i \) at the \( n \)-th callback. This viewpoint will let us reason about timing and stability of communication.
flowchart TD
N1["sensor_node"] -->|topic: /scan| N2["localization_node"]
N2 -->|topic: /pose| N3["controller_node"]
N3 -->|topic: /cmd_vel| N4["actuator_node"]
N3 -->|service: /reset_odometry| N2
N3 -->|action: /navigate_to_pose| N5["planner_node"]
N5 -->|feedback: progress| N3
N5 -->|result: done| N3
The diagram above is a typical ROS computation graph. The same node can participate in multiple edges, possibly of different types.
2. Topics: Publish–Subscribe as Stochastic Message Streams
A topic provides one-to-many, asynchronous publish–subscribe communication. Suppose a publisher emits messages at times \( t_k \), producing a stream \( \{m_k\}_{k\ge 0} \). The nominal sampling period is \( T_p \), so ideally \( t_{k+1}-t_k = T_p \).
A subscriber processes messages via callbacks taking (possibly variable) time \( T_c \). If messages are queued with depth \( d \), the subscriber behaves like a single-server queue with arrival rate \( \lambda = 1/T_p \) and service rate \( \mu = 1/T_c \).
\[ \lambda = \frac{1}{T_p},\qquad \mu = \frac{1}{T_c}. \]
Stability condition (no unbounded backlog). For a GI/G/1 queue, a sufficient and necessary stability condition is \( \rho = \lambda/\mu < 1 \). In ROS terms:
\[ \rho = \frac{\lambda}{\mu} = \frac{T_c}{T_p} < 1 \quad \Longleftrightarrow \quad T_c < T_p. \]
Proof sketch. Let \( Q[n] \) be queue length right after the \( n \)-th callback finishes. Over one period, expected arrivals are \( \mathbb{E}[A]=\lambda T_c \) and exactly one job is served. Thus
\[ \mathbb{E}[Q[n+1]-Q[n]] = \lambda T_c - 1 = \frac{T_c}{T_p}-1. \]
If \( T_c/T_p < 1 \), the drift is negative, so the Markov chain \( Q[n] \) is positive recurrent and backlog stays bounded. If \( T_c/T_p \ge 1 \), drift is nonnegative and the backlog diverges, implying message drops when depth is finite. ∎
In ROS2, DDS Quality-of-Service (QoS) parameters tune this queueing behavior. Common ones include: \( d \) (history depth), reliability (best-effort vs reliable), and durability (volatile vs transient-local). Mathematically, reliability trades drop probability \( P_{\text{drop}} \) against latency \( L \):
\[ P_{\text{drop}} \downarrow \;\Rightarrow\; L \uparrow \quad\text{(retransmissions / buffering)}. \]
We will not go deeper into DDS theory here; only note that ROS2 topics are realized on top of DDS to provide configurable real-time behavior.
3. Services: Request–Response and Deadlock Avoidance
A service is a synchronous RPC-like interaction: a client sends request \( q \), and a server returns response \( r \). The round-trip time (RTT) is:
\[ T_{\text{RTT}} = T_{\text{req}} + T_{\text{proc}} + T_{\text{res}}, \]
where \( T_{\text{req}} \) and \( T_{\text{res}} \) are transport delays, and \( T_{\text{proc}} \) is server processing time.
Services induce a directed call graph \( G_S = (V, E_S) \). A key correctness property is deadlock freedom.
Theorem (acyclic call graph ⇒ deadlock-free). If \( G_S \) is acyclic and each service handler terminates, then all service calls complete (no deadlock).
Proof. Since \( G_S \) is acyclic, there exists a topological order \( v_{(1)},\dots,v_{(|V|)} \) such that all edges go from lower to higher indices. Consider any in-flight call chain. It follows edges strictly increasing in this order, so it cannot revisit a node. Thus the chain length is at most \( |V| \). Since each handler terminates, the chain resolves from the last node backward, and the initial caller receives a response. ∎
Practically: avoid circular service dependencies (e.g., A calls B while B calls A).
4. Actions: Goal–Feedback–Result Protocol
An action supports long-running tasks that need intermediate feedback and cancellation. It can be modeled as a finite-state protocol between client and server.
Let a goal be submitted at time \( t_0 \). Feedback is published at rate \( f_b \) Hz and completion occurs after server runtime \( T_g \). Expected number of feedback messages is:
\[ N_b = \left\lfloor f_b \, T_g \right\rfloor. \]
If feedback callbacks take time \( T_{fb} \), then the same queue stability argument as for topics yields: \( T_{fb} < 1/f_b \) to avoid feedback backlog.
stateDiagram-v2
[*] --> Idle
Idle --> GoalSent: send_goal()
GoalSent --> Executing: goal_accepted
GoalSent --> Idle: goal_rejected
Executing --> Executing: feedback
Executing --> Canceling: cancel_request
Canceling --> Executing: cancel_rejected
Canceling --> Canceled: cancel_accepted
Executing --> Succeeded: result_success
Executing --> Aborted: result_abort
Canceled --> Idle
Succeeded --> Idle
Aborted --> Idle
In ROS1 actions are implemented via multiple topics (goal, feedback, result, cancel). ROS2 provides a first-class action API on top of DDS.
5. Python Lab (ROS2 rclpy)
Python uses rclpy. Below are minimal patterns for each
primitive.
5.1 Topic Publisher and Subscriber
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float64
class SinePublisher(Node):
def __init__(self):
super().__init__('sine_publisher')
self.pub = self.create_publisher(Float64, '/u_cmd', 10)
self.t = 0.0
self.timer = self.create_timer(0.01, self.step) # 100 Hz
def step(self):
msg = Float64()
msg.data = 1.0 # placeholder input
self.pub.publish(msg)
class UListener(Node):
def __init__(self):
super().__init__('u_listener')
self.sub = self.create_subscription(
Float64, '/u_cmd', self.cb, 10)
def cb(self, msg):
self.get_logger().info(f"u_cmd={msg.data}")
def main():
rclpy.init()
pub_node = SinePublisher()
sub_node = UListener()
executor = rclpy.executors.MultiExecutor()
executor.add_node(pub_node)
executor.add_node(sub_node)
executor.spin()
if __name__ == "__main__":
main()
5.2 Service Server and Client
from example_interfaces.srv import AddTwoInts
class AdderServer(Node):
def __init__(self):
super().__init__('adder_server')
self.srv = self.create_service(AddTwoInts, '/add', self.handle)
def handle(self, req, res):
res.sum = req.a + req.b
return res
class AdderClient(Node):
def __init__(self):
super().__init__('adder_client')
self.cli = self.create_client(AddTwoInts, '/add')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info("waiting for /add ...")
def call_add(self, a, b):
req = AddTwoInts.Request()
req.a, req.b = a, b
future = self.cli.call_async(req)
rclpy.spin_until_future_complete(self, future)
return future.result().sum
5.3 Action Server and Client
from rclpy.action import ActionServer, ActionClient
from example_interfaces.action import Fibonacci
class FibActionServer(Node):
def __init__(self):
super().__init__('fib_server')
self.server = ActionServer(
self, Fibonacci, '/fib', self.execute_cb)
async def execute_cb(self, goal_handle):
order = goal_handle.request.order
seq = [0, 1]
for i in range(2, order):
seq.append(seq[-1] + seq[-2])
goal_handle.publish_feedback(Fibonacci.Feedback(sequence=seq))
await rclpy.sleep(0.1)
goal_handle.succeed()
return Fibonacci.Result(sequence=seq)
class FibActionClient(Node):
def __init__(self):
super().__init__('fib_client')
self.client = ActionClient(self, Fibonacci, '/fib')
async def send_goal(self, n):
await self.client.wait_for_server()
goal = Fibonacci.Goal(order=n)
gh_future = self.client.send_goal_async(goal, feedback_callback=self.fb_cb)
goal_handle = await gh_future
result_future = goal_handle.get_result_async()
result = await result_future
return result.result.sequence
def fb_cb(self, feedback_msg):
self.get_logger().info(f"feedback={feedback_msg.feedback.sequence}")
6. C++ Lab (ROS2 rclcpp / rclcpp_action)
6.1 Topic Publisher/Subscriber
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/float64.hpp>
class UCmdPub : public rclcpp::Node {
public:
UCmdPub() : Node("u_cmd_pub") {
pub_ = create_publisher<std_msgs::msg::Float64>("/u_cmd", 10);
timer_ = create_wall_timer(
std::chrono::milliseconds(10),
std::bind(&UCmdPub::step, this)
);
}
private:
void step() {
std_msgs::msg::Float64 msg;
msg.data = 1.0;
pub_->publish(msg);
}
rclcpp::Publisher<std_msgs::msg::Float64>::SharedPtr pub_;
rclcpp::TimerBase::SharedPtr timer_;
};
6.2 Service Server
#include <rclcpp/rclcpp.hpp>
#include <example_interfaces/srv/add_two_ints.hpp>
class AddServer : public rclcpp::Node {
public:
AddServer() : Node("add_server") {
srv_ = create_service<example_interfaces::srv::AddTwoInts>(
"/add",
[](const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> req,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> res) {
res->sum = req->a + req->b;
}
);
}
private:
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr srv_;
};
6.3 Action Server (Fibonacci)
#include <rclcpp/rclcpp.hpp>
#include <rclcpp_action/rclcpp_action.hpp>
#include <example_interfaces/action/fibonacci.hpp>
class FibServer : public rclcpp::Node {
public:
using Fibonacci = example_interfaces::action::Fibonacci;
using GoalHandle = rclcpp_action::ServerGoalHandle<Fibonacci>;
FibServer() : Node("fib_server") {
server_ = rclcpp_action::create_server<Fibonacci>(
this, "/fib",
std::bind(&FibServer::handle_goal, this, std::placeholders::_1, std::placeholders::_2),
std::bind(&FibServer::handle_cancel, this, std::placeholders::_1),
std::bind(&FibServer::handle_accept, this, std::placeholders::_1)
);
}
private:
rclcpp_action::Server<Fibonacci>::SharedPtr server_;
rclcpp_action::GoalResponse handle_goal(
const rclcpp_action::GoalUUID &,
std::shared_ptr<const Fibonacci::Goal> goal) {
return goal->order > 0 ? rclcpp_action::GoalResponse::ACCEPT_AND_EXECUTE
: rclcpp_action::GoalResponse::REJECT;
}
rclcpp_action::CancelResponse handle_cancel(
const std::shared_ptr<GoalHandle>) {
return rclcpp_action::CancelResponse::ACCEPT;
}
void handle_accept(const std::shared_ptr<GoalHandle> gh) {
std::thread{std::bind(&FibServer::execute, this, gh)}.detach();
}
void execute(const std::shared_ptr<GoalHandle> gh) {
auto feedback = std::make_shared<Fibonacci::Feedback>();
auto result = std::make_shared<Fibonacci::Result>();
feedback->sequence = {0,1};
for(int i=2; i<gh->get_goal()->order; ++i){
feedback->sequence.push_back(feedback->sequence[i-1]+feedback->sequence[i-2]);
gh->publish_feedback(feedback);
rclcpp::sleep_for(std::chrono::milliseconds(100));
}
result->sequence = feedback->sequence;
gh->succeed(result);
}
};
7. Java Lab (ROS2 rcljava)
Java support in ROS2 is provided by rcljava. The pattern
mirrors rclpy/rclcpp.
7.1 Topic Publisher/Subscriber
import org.ros2.rcljava.RCLJava;
import org.ros2.rcljava.node.Node;
import org.ros2.rcljava.publisher.Publisher;
import org.ros2.rcljava.subscription.Subscription;
import std_msgs.msg.Float64;
public class TopicDemo {
public static void main(String[] args) {
RCLJava.rclJavaInit();
Node node = RCLJava.createNode("java_topic_demo");
Publisher<Float64> pub =
node.createPublisher(Float64.class, "/u_cmd");
Subscription<Float64> sub =
node.createSubscription(Float64.class, "/u_cmd",
msg -> System.out.println("u_cmd=" + msg.getData()));
node.createWallTimer(10, () -> {
Float64 m = new Float64();
m.setData(1.0);
pub.publish(m);
});
RCLJava.spin(node);
}
}
7.2 Service Client (AddTwoInts)
import example_interfaces.srv.AddTwoInts;
import org.ros2.rcljava.client.Client;
Client<AddTwoInts> cli =
node.createClient(AddTwoInts.class, "/add");
// build request
AddTwoInts.Request req = new AddTwoInts.Request();
req.setA(3);
req.setB(7);
// async call
cli.asyncSendRequest(req).thenAccept(res -> {
System.out.println("sum=" + res.getSum());
});
Actions are available in newer rcljava releases with an API similar to rclcpp_action; the logic matches the Python/C++ examples.
8. MATLAB / Simulink Lab (ROS Toolbox / ROS2 Toolbox)
MATLAB offers ROS and ROS2 communication through the Robotics System Toolbox. Below are minimal scripts.
8.1 Topic Pub/Sub (ROS2)
% Create ROS2 node
node = ros2node("/matlab_node");
% Publisher
pub = ros2publisher(node, "/u_cmd", "std_msgs/Float64");
msg = ros2message(pub);
% Subscriber with callback
sub = ros2subscriber(node, "/u_cmd", "std_msgs/Float64", ...
@(m) disp(["u_cmd = ", num2str(m.data)]));
% Publish at 100 Hz for 1 second
rate = rateControl(100);
for k = 1:100
msg.data = 1.0;
send(pub, msg);
waitfor(rate);
end
8.2 Service Client
% Service client for /add
cli = ros2svcclient(node, "/add", "example_interfaces/AddTwoInts");
req = ros2message(cli);
req.a = int64(3);
req.b = int64(7);
res = call(cli, req);
disp(["sum = ", num2str(res.sum)]);
8.3 Action Client (conceptual script)
% Fibonacci action client
ac = ros2actionclient(node, "/fib", "example_interfaces/Fibonacci");
goal = ros2message(ac);
goal.order = int32(8);
% Send goal and wait for result
result = sendGoalAndWait(ac, goal);
disp("sequence:");
disp(result.sequence);
In Simulink, you can use ROS2 Publish/Subscribe blocks to represent topic edges. Treat each Simulink model as a node and each ROS2 block pair as a topic interface. Services and actions are supported through ROS Toolbox blocks as well, but we keep the first exposure script-based for clarity.
9. Problems and Solutions
Problem 1 (Computation Graph Modeling): Consider 4 nodes \( v_1,\dots,v_4 \). Node \( v_1 \) publishes on topic A to \( v_2 \) and \( v_3 \); \( v_2 \) publishes on topic B to \( v_4 \); \( v_3 \) calls a service on \( v_4 \). Write \( E_T \) and \( E_S \).
Solution:
\[ E_T=\{(v_1,v_2,\mathsf{A}),\,(v_1,v_3,\mathsf{A}),\,(v_2,v_4,\mathsf{B})\}, \quad E_S=\{(v_3,v_4,\mathsf{srv})\}. \]
Problem 2 (Queue Stability and Drops): A publisher runs at \( 50 \) Hz (so \( T_p=0.02 \) s). Subscriber callback time is \( T_c=0.03 \) s with queue depth \( d=10 \). Will messages drop in steady-state?
Solution: The utilization is \( \rho=T_c/T_p=0.03/0.02=1.5 \), so \( \rho>1 \). The queue is unstable and backlog grows until the finite depth saturates; therefore drops occur.
Problem 3 (Service Deadlock): Two nodes A and B each provide one service. A’s service handler calls B, and B’s service handler calls A. Is the system deadlock-free?
Solution: The call graph contains a directed cycle A→B→A. The acyclicity condition of the theorem is violated; deadlock can occur if both handlers wait synchronously for the other’s response.
Problem 4 (Action Feedback Load): An action goal takes \( T_g=12 \) s. Feedback rate is \( f_b=5 \) Hz and feedback callback time is \( T_{fb}=0.15 \) s. Compute \( N_b \) and check stability.
Solution:
\[ N_b=\lfloor f_b T_g\rfloor=\lfloor 5\times 12\rfloor=60. \]
Stability requires \( T_{fb} < 1/f_b = 0.2 \) s. Since \( 0.15 < 0.2 \), feedback processing is stable (no backlog).
Problem 5 (Choosing Primitive): For each case, choose topic, service, or action: (i) continuous wheel velocity commands at 100 Hz, (ii) “reset odometry” called occasionally, (iii) “navigate to pose” lasting tens of seconds with cancellation.
Solution: (i) topic (high-rate stream), (ii) service (short request–response), (iii) action (long-running with feedback/cancel).
10. Summary
We modeled ROS systems as directed graphs of nodes connected by typed edges. Topics realize asynchronous streams whose correctness depends on queue stability \( T_c/T_p < 1 \). Services provide synchronous RPC behavior and are deadlock-free under acyclic call graphs. Actions implement long-running goals with feedback and cancellation, again subject to stability constraints on feedback load. You now have working multi-language patterns for all four primitives.
11. References
- Quigley, M., Conley, K., Gerkey, B., Faust, J., Foote, T., Leibs, J., Wheeler, R., & Ng, A. (2009). ROS: An open-source Robot Operating System. ICRA Workshop on Open Source Software.
- Gerkey, B., Vaughan, R., & Howard, A. (2003). The Player/Stage project: Tools for multi-robot and distributed sensor systems. ICRA, 317–323.
- Pardo-Castellote, G. (2003). OMG Data-Distribution Service: Architectural overview. ICRA Workshop on Middleware for Robotics.
- Maruyama, Y., Kato, S., & Azumi, T. (2016). Exploring the performance of ROS2. International Conference on Embedded Software, 1–10.
- Macenski, S., Foote, T., Gerkey, B., Lalancette, F., & Woodall, W. (2022). Robot Operating System 2 (ROS2): Design, architecture, and uses in the wild. Science Robotics, 7(66), eabm6074.
- Koubaa, A. (2017). Robot Operating System (ROS): The complete reference (selected theoretical chapters on communication and QoS). Springer Tracts in Advanced Robotics.
- Liu, C. L., & Layland, J. W. (1973). Scheduling algorithms for multiprogramming in a hard-real-time environment. Journal of the ACM, 20(1), 46–61.
- Kleinrock, L. (1975). Queueing Systems, Volume 1: Theory. Wiley-Interscience. (queue stability foundations)