ROS 2
Nodes, topics, publishers, subscribers, and services in ROS 2 with Python and C++ examples
ROS stands for Robot Operating System. Despite the name it is not an operating system but a framework of libraries and tools for building robot software. It splits a robot's software into small programs called nodes that communicate with each other. These docs cover ROS 2, which is what the ros2 command line tool belongs to.
Example Python Node
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class BasicNode(Node):
def __init__(self):
super().__init__("BasicNodePractice")
# Create a publisher we can publish data to
self._publisher = self.create_publisher(String, "our_publisher", 10)
self._subscriber = self.create_subscription(
String, "some_other_topic", self._subscriber_callback, 10
)
def publish_some_data(self, data: str):
pub_data = String()
pub_data.data = data
self._publisher.publish(pub_data)
def _subscriber_callback(self, msg: String):
print("Received data: " + msg.data)
def main():
rclpy.init()
basic_node = BasicNode()
try:
rclpy.spin(basic_node)
except KeyboardInterrupt:
pass
finally:
basic_node.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()This node creates a publisher and a subscriber. rclpy.spin() keeps the node running so its callbacks are processed until you stop it.
Example C++ node
Header file for the node here
class ExampleCppNode : public rclcpp::Node {
rclcpp::Publisher<messages::msg::ExampleMsg>::SharedPtr m_publisher;
rclcpp::Subscription<messages::msg::ExampleMsg>::SharedPtr m_subscriber;
rclcpp::TimerBase::SharedPtr m_timer;
rclcpp::ServiceBase::SharedPtr m_service;
rclcpp::Client<messages::srv::ExampleService>::SharedPtr m_client;
public:
void messageCallback(const messages::msg::ExampleMsg &ptr);
// Small function that publishes data a few times a second
void timerCallback();
explicit ExampleCppNode();
};Source code for the node here
ExampleExampleCppNode() : Node("ExampleCppNode") {
using namespace messages::msg;
using namespace std::chrono_literals;
m_publisher = this->create_publisher<ExampleMsg>("CppPublisher", 10);
/* This is the same as the way below, we create a `placeholder` value that is
some dynamic argument, same as a lambda m_subscriber =
this->create_subscription<ExampleMsg>( "PythonPublisher", 10,
std::bind(&ExamplemessageCallback, this, _1));
*/
// A subscription needs some callable object, so we create this lambda to pass
// to it
auto functionLambda = [this](const ExampleMsg &msg) {
this->messageCallback(msg);
};
m_subscriber = this->create_subscription<ExampleMsg>("PythonPublisher", 10,
functionLambda);
std::chrono::milliseconds duration{500};
using namespace messages::srv;
auto serviceLambda = [this](ExampleService::Request::SharedPtr req,
ExampleService::Response::SharedPtr resp) {
resp->response = req->a + req->b;
return resp;
};
m_service =
this->create_service<ExampleService>("cpp_service", serviceLambda);
m_client = this->create_client<ExampleService>("python_service");
auto wait_res = m_client->wait_for_service(std::chrono::seconds(10));
if (!wait_res) {
RCLCPP_ERROR(this->get_logger(), "Failed to initialize service");
}
m_timer = this->create_timer(duration,
std::bind(&ExampletimerCallback, this));
}
void ExampletimerCallback() {
using namespace messages::msg;
ExampleMsg msg{};
msg.a = rand();
msg.b = rand();
this->m_publisher->publish(msg);
auto request =
std::make_shared<messages::srv::ExampleService::Request>();
request->a = rand();
request->b = rand();
// Don't spin from inside a callback, the executor is already spinning this
// node. Handle the response asynchronously instead.
m_client->async_send_request(
request,
[this](rclcpp::Client<messages::srv::ExampleService>::SharedFuture f) {
RCLCPP_INFO(this->get_logger(), "Received value was %ld",
f.get()->response);
});
}
void ExamplemessageCallback(
const messages::msg::ExampleMsg &ptr) {
RCLCPP_INFO(this->get_logger(), "New data found A: %ld, B: %ld", ptr.a,
ptr.b);
}Nodes
A node is a single program in a ROS system that does one job, such as reading a sensor or driving a motor. You can run a node with
$ ros2 run <package name> <executable name>List all running nodes with
$ ros2 node listRemapping a node name
Remapping lets us reassign default ROS names such as the node name, topic names, and service names when a node is launched.
Remapping does not rename a node that is already running. It starts a new instance of the executable under a different name. For example, if turtlesim and teleop_turtle are already running, this starts a second turtlesim node named my_turtle:
$ ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtleros2 node list will then return
/my_turtle
/turtlesim
/teleop_turtleExamining a node
Run ros2 node info <node name> to see the topics, services, and actions a node uses. For example, ros2 node info /my_turtle prints output like this.
/my_turtle
Subscribers:
/parameter_events: rcl_interfaces/msg/ParameterEvent
/turtle1/cmd_vel: geometry_msgs/msg/Twist
Publishers:
/parameter_events: rcl_interfaces/msg/ParameterEvent
/rosout: rcl_interfaces/msg/Log
/turtle1/color_sensor: turtlesim_msgs/msg/Color
/turtle1/pose: turtlesim_msgs/msg/Pose
Service Servers:
/clear: std_srvs/srv/Empty
/kill: turtlesim_msgs/srv/Kill
/my_turtle/describe_parameters: rcl_interfaces/srv/DescribeParameters
/my_turtle/get_parameter_types: rcl_interfaces/srv/GetParameterTypes
/my_turtle/get_parameters: rcl_interfaces/srv/GetParameters
/my_turtle/list_parameters: rcl_interfaces/srv/ListParameters
/my_turtle/set_parameters: rcl_interfaces/srv/SetParameters
/my_turtle/set_parameters_atomically: rcl_interfaces/srv/SetParametersAtomically
/reset: std_srvs/srv/Empty
/spawn: turtlesim_msgs/srv/Spawn
/turtle1/set_pen: turtlesim_msgs/srv/SetPen
/turtle1/teleport_absolute: turtlesim_msgs/srv/TeleportAbsolute
/turtle1/teleport_relative: turtlesim_msgs/srv/TeleportRelative
Service Clients:
Action Servers:
/turtle1/rotate_absolute: turtlesim_msgs/action/RotateAbsolute
Action Clients:This shows which topics the node publishes and subscribes to. Here, /my_turtle subscribes to /parameter_events and /turtle1/cmd_vel.
Topics
Topics are meant for continuous streams of data, such as sensor readings. They provide an asynchronous, one-way way to send data from publishers to subscribers. Think of a topic as a radio channel, you can tune into that radio channel and absorb that information or you can broadcast information down that radio channel.
Messages
You can define your own message type in a .msg file, which looks something like this.
int32 x
int32 yWrite the field type first and then the field name. See the ROS 2 interface documentation for more information.
Topic statistics
Topic statistics are built-in measurements for a topic, such as how old a message is when it arrives at a subscriber and the average time between messages. They are disabled by default and are enabled per subscription. Statistics are only available for message types that have a header with a timestamp. In C++ you enable them through the subscription options:
rclcpp::SubscriptionOptions options;
options.topic_stats_options.state = rclcpp::TopicStatisticsState::Enable;Pass options as the last argument to create_subscription. The statistics are published on the /statistics topic by default.
Publishers and subscribers
One of the main concepts in ROS is publishers and subscribers. The node examples above each define a publisher and a subscriber. A publisher sends data into a topic.
Think of the publisher as a talker and the subscriber as a listener: the publisher broadcasts data on a radio channel (a topic) and the subscriber listens on that channel and does something with the data. In the examples above we only print the data we receive.
For any given topic there can be multiple publishers and multiple subscribers.
Code definition of publishers and subscribers
We define a publisher with a message type (usually a class name), a topic name as a string, and an integer queue depth. The queue depth is the shorthand for a quality of service (QoS) profile that keeps the last N messages if the subscriber can't keep up.
In Python:
self._publisher = self.create_publisher(ExampleMsg, "PythonPublisher", 10)In C++:
m_publisher = this->create_publisher<ExampleMsg>("CppPublisher", 10);The angle brackets are a C++ template, a feature we will cover in other documents. We provide the message type inside the brackets. Note that C++ takes the topic name and queue depth as arguments, and Python takes the message type as its first argument.
A subscriber takes the same arguments plus a callback function, so it has something to do with each message it receives. Python handles this easily because any callable works, while C++ has a few different options.
In Python:
self._subscriber = self.create_subscription(
ExampleMsg, "CppPublisher", self._subscriber_callback, 10
)In C++:
auto functionLambda = [this](const ExampleMsg &msg) {
this->messageCallback(msg);
};
m_subscriber = this->create_subscription<ExampleMsg>("PythonPublisher", 10,
functionLambda);Above we create a lambda that captures a pointer to our own instance and takes a reference to an ExampleMsg. The lambda is a callable object that is copied into the subscription.
this->create_subscription<ExampleMsg>(
"PythonPublisher", 10,
std::bind(&ExampleCppNode::messageCallback, this, std::placeholders::_1));Here we create a callable object with std::bind, a C++ standard library function that wraps a function together with some of its arguments. This callable calls the member function messageCallback on this, and std::placeholders::_1 forwards the first argument of the call, the message, to it.
Services
A service in ROS is similar to a function call in most programming languages. You pass it some arguments and it returns a value. Like messages, you define a service in a file. The part above --- is the request and the part below it is the response.
int64 a
int64 b
---
int64 sumYou can also use predefined service types that ROS provides.
A service has a server and a client. The server provides the response and the client makes the request. The server needs a callback function that receives the request and fills in the response.
Here is how we define a service server.
In Python:
self.create_service(ExampleService, "python_service", self._service_callback)
def _service_callback(self, request, response):
response.sum = request.a + request.b
return responseIn C++:
auto serviceLambda = [](const ExampleService::Request::SharedPtr req,
ExampleService::Response::SharedPtr resp) {
resp->sum = req->a + req->b;
};
m_service =
this->create_service<ExampleService>("cpp_service", serviceLambda);A C++ service callback fills in the response it is given and returns nothing. In both languages we provide the service type, a service name, and a callback.
Here is how we define a client.
In Python:
self._client = self.create_client(ExampleService, "cpp_service")In C++:
m_client = this->create_client<ExampleService>("python_service");Making requests
Blocking calls
A synchronous call blocks its thread until a response arrives. If you make one from inside a callback on a single-threaded executor, the node can never process the response and will deadlock. Keep this in mind when writing synchronous code.
We show both synchronous and asynchronous requests so you can decide which to use.
Synchronous approach
In Python:
request = ExampleService.Request()
request.a = 123
request.b = 456
response = self._client.call(request)In C++, a client only has an asynchronous send function, so we send the request and then block until the future is ready:
auto request = std::make_shared<ExampleService::Request>();
request->a = 123;
request->b = 456;
auto future = client->async_send_request(request);
// Block this thread until the response arrives
if (rclcpp::spin_until_future_complete(node, future) ==
rclcpp::FutureReturnCode::SUCCESS) {
auto response = future.get();
RCLCPP_INFO(node->get_logger(), "Sum: %ld", response->sum);
} else {
RCLCPP_ERROR(node->get_logger(), "Service call failed.");
}We send a request and block until we receive a response.
Asynchronous approach
In Python:
future = self._client.call_async(request)
future.add_done_callback(self._client_done)
def _client_done(self, future):
self.get_logger().info(f"Service response: {future.result().sum}")In C++:
auto request = std::make_shared<ExampleService::Request>();
request->a = 123;
request->b = 456;
m_client->async_send_request(
request,
[this](rclcpp::Client<messages::srv::ExampleService>::SharedFuture f) {
RCLCPP_INFO(this->get_logger(), "Received value was %ld",
f.get()->sum);
});For an asynchronous request we get a future object back, which represents a response that hasn't arrived yet. We attach a callback that runs when the response is ready and does something with the data.
The main difference is that an asynchronous request does not block the thread, so the node can keep running while it waits for the response.
The example repository uses the asynchronous approach for one main reason. The C++ node and the Python node call each other's services. If one node blocked inside a callback while waiting for the other, and the other did the same, both nodes would freeze waiting on each other.