Introduction
ROS2 (Robot Operating System 2) is an open source framework and tools to build complex robot applications.
Getting to the Basics - Terminology
Here are some of the common names when interacting with ROS2,
- Node: a node is an executable file within a ROS2 package. Nodes will interact with one another via "handshake" process on shared network automatically. The communication between ros nodes happens in a hidden layer using the DDS middleware. DDS is responsible for moving data between nodes. Visualising the ROS communication graph between nodes help to understand the interaction between nodes.
Each node is a separate process usually for a specific task. For instance, a node for vision, and another for motor control.
Three Main Communication Methods
- Topics
- Services
- Actions
Topics - Pub/Sub Model
Imagine shouting into the void, and anyone who listens gets the message. That is what topic does.
It uses publisher-subscriber model to transmit data in unidirection. Since topic does not need to ensure if
anyone gets the message, it is called asynchronous.
Topic node is well suited for high frequency and continuous data stream like monitoring and logging for state tracking. To define a topic, we need a name and an interface. An interface is the data type of the message being sent.
Some common command tools involving topics are:
ros2 topic list: list all available topicsros2 topic info /chatter: check how many subscribers and publisher node to the topic/chatterand the data type being sent.
ros2 topic info /chatter
# returned output
Type: std_msgs/msg/String -> interface
Publisher count: 1
Subscription count: 1
To show the details of the interface, ros2 interface show std_msgs/msg/String returns string data, showing that the message sent is
in string with the name data.
For nodes to communicate between one another, they have to publish and subscribe to the same topic as if entering the same chatting room and share the same message type / interface as if speaking in same language.
Services - Client/Server Model
Services is another communication method in ros2. Unlike topics, services use a request-response mode, allowing for bidirectional conversation.
Services are usually synchronous as it waits for the server to reply after sending a request. It is suitable for events that happens exactly once
that requires confirmation from the server. Think of synchronous method like using a vending machine. When the button is pressed and money is inserted
into the vending machine, we would wait or the response (snack/coffee) from the vending machine.
Similar to topics, services are defined by a name and an interface but the interface of a service node contains both a request and a response.
Actions
Simply put, Actions is the same as service using client/server communication but is used to handle long running goals. It leverages on topics and services to achieve the goals intended and it offers the option to cancel
a task or monitor progress. For instance, the goal is for the robot to navigate to the kitchen, the behaviour of this task can be split asynchronously
into three main parts:
- [service] Goal: navigates to the kitchen
- [topic] Feedback: sends the progress of the robot's navigation; "Robot is 50% there"
- [service] Result: updates the tasks status and notify system when it has been completed
The interface as such, is also split accordingly into goal, feedback and result where goal is acts as the request, result acts as the response
and feedback is any other additional data that can be sent from the server during the execution of the goal.
Parameters
Parameters settings can be configured when running the node. To list all available parameters, run ros2 param list. For illustration, here is the returned output of the turtlesim node in the terminal with its configurable paramaters,
/turtlesim:
background_b
background_g
background_r
...
Here we have the turtlesim node with serveral parameters like background_b etc, showing that it is possible to change the background color of the turtlesim node window.
Launch Files
Launch file is a convenient way to launch module nodes and parameters from just one file. It is almost certained that there will be multiple nodes running when the project scales, each with its dedicated list of parameters. To keep things neat, launch files help to consolidate the modification and act as a single entry point to the execution of the program.
ROS2 Node Skeleton
Before starting ROS2 development, a special directory which typically called ros2_ws is created to contain all ros2 revelant scripts.
This workspace is where scripts are created and packages are built by running colcon build. Three new directories namely the log, install and build will be created after built.
- build: contains intermediate files needed for overall build
- log: contains log for each build
- install: contains all nodes that are built in workspace
colcon build should always be executed at the root directory and the workspace should be sourced after each built to inform the system about the new changes.
By including this line source ~/ros2_ws/install/setup.bash into our ~/.bashrc, newly opened terminal will be sourced automatically, saving some work.
A package has to be created to contain any nodes in the workspace. A package is a subset of our application responsible for certain tasks independent of one another.
For instance, considering a robotic arm for picking objects, relevant tasks involved include recognising the object, planning the motion of the arm and monitoring
the hardware of the robotic arms. These can be developed as individual packages such as the camera pkg, motion planning pkg and handware control pkg. It is a way
keep our application clean and organised.
To create a package in python, the build type has to be specified to ament_python. Optional dependencies can also be specified during the creation of the package as such,
ros2 pkg create my_py_pkg --build-type ament_python --dependencies rclpy. This would create the following directories,
/home/<user>/ros2_ws/src/my_py_pkg
├── my_py_pkg
│ └── __init__.py
├── package.xml
├── resource
│ └── my_py_pkg
├── setup.cfg
├── setup.py
└── test
├── test_copyright.py
├── test_flake8.py
└── test_pep257.py
There will be a directory of the same name inside the package which contains __init__.py. The implementation of the python node will be created in it.
The package.xml is another critical file which provides information about the package and its dependencies.
Running colcon build builds the entire workspace and all packages. Selective build can be executed by running with the argument --packages-select {package_name}.
Writing a Python Node
Locate to the package directory previously created and create a new file my_first_node.py for the creation of the python node.
The minimal implementation of a simple ros2 python node is as follows,
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
class MyFirstNode(Node):
def __init__(self):
super().__init__('my_node_name')
def main(args=None):
rclpy.init(args=args)
node = MyFirstNode()
rclpy.spin(node)
rclpy.shutdown()
if __name__ == "__main__":
main()
Leveraging on the python library for ros2 rclpy, the parent constructor super().__init__() allows us to access methods from the
previously defined Node class in rclpy via inheritance. After creating our first node class, the main() function executes the following,
rclpy.init(args=args): initialise ros2 communicationsnode = MyFirstNode(): create and initialise node objectrclpy.spin(node): enables the program and node to stay alive, otherwise the program exists after creating the noderclpy.shutdown(): node is killed withctrl Cand this shuts down the ros2 communication
Next, navigate to the entry_points and console_scripts in the setup.py file from the package directory and include this line,
entry_points={
'console_scripts': [
"test_node = my_py_pkg.my_first_node:main"
],
},
# syntax meaning
<executable_name> = <package_name>.<file_name>:<function_name>.
Executable Name v.s Node Name v.s File Name
- Executable name: defined in
setup.pyand execute withros2 run - Node name: defined in node object class, shows in
ros2 node listorrqt_graph - File name: code implementation script
Adding a Timer to our Node
Here is the sample code to create a timer and log every one second in the terminal with our tiny node,
import rclpy
from rclpy.node import Node
class MyFirstNode(Node):
def __init__(self):
super().__init__("my_tiny_node") # constructor to inherit methods from rclpy node class
# create timer
self.counter_ = 0
# create a timer to call print_hello every 1.0 s
self.timer_ = self.create_timer(1.0, self.print_hello)
def print_hello(self):
self.get_logger().info("Hello " + str(self.counter_))
self.counter_ += 1
The create_timer() method from the Node class first takes the rate at which the callback function is being called
and the callback function itself as the second argument. This means that print_hello() method will be called every 1 second.
Since the node is alive and spinning, the registered callback function can be called.
Topics
- topic is defined by name and interface
- publish-subscribe communication method - asynchronuous
- publishers and subscribers have to be on the same interface using the same data type
- a node can have multiple publishers and subscribers
Topic Publisher & Subscriber Code
Consider we want to write a number publisher, the interface of the message sent should be in float32.
import rclpy
from rclpy.node import Node
from example_interfaces.msg import Int64
class FirstPub(Node):
def __init__(self):
super().__init__("my_tiny_pub")
self.number_ = 2
# create publisher, args: interface, topic name, queue buffer
self.number_publisher_ = self.create_publisher(Int64,"number", 10)
# trigger callback every 1.0 to publish number
self.number_timer_ = self.create_timer(1.0, self.publish_number)
def publish_number(self):
# create interface to send data
msg = Int64()
msg.data = self.number_
self.number_publisher_.publish(msg)
def main(args=None):
rclpy.init(args=args)
my_pub = FirstPub()
rclpy.spin(my_pub)
rclpy.shutdown()
if __name__ == "__main__":
main()
Arguments for create_publisher():
- data type [interface]
- topic name
- queue size: if the rate of data published is faster than the rate of subscription, messages will be buffered so no messages are lost.
Now, we can write a subscriber node to listen to the topic number,
import rclpy
from rclpy.node import Node
from example_interfaces.msg import Int64 # must share the same interface
class FirstSub(Node):
def __init__(self):
super().__init__("my_tiny_sub")
self.number_subscriber = self.create_subscription(Int64, "number", self.number_callback,10)
self.counter_ = 0
def number_callback(self, msg: Int64):
self.counter_ += msg.data
self.get_logger().info("Counter: " + str(self.counter_))
Arguments for create_subscription():
- data type [interface]: has to share same interface as publisher
- topic name: has to share same name as published topic
- callback function: the registered callback function directly received message in the parameter whenever message is published on the topic
- queue size: if the rate of data published is faster than the rate of subscription, messages will be buffered so no messages are lost.
Data Distribution Service (DDS)
DDS is the middleware in charge of sending and receiving messages between nodes. Spinning up a node using rclpy or rclcpp
also includes the use of DDS functionality with API implemented in its library.
There are different implementations of DDS, such as Fast DDS, Cyclone DDS and Connext DDS. The default implementation is Fast DDS.
This is such a critical component that I would want to dive into it in the future. For now, here are some of the useful commands to check the DDS status and configuration,
ros2 doctor --report: check the overall health of the ros2 system and its components including the DDS implementationros2 doctor --report --include-dds: check the overall health of the ros2 system and its components including the DDS implementation with more details about the DDS
Tools to Inspect Topics
rqt_graphros2 topic {commands}--ros-args: modify certain parameters at runtime- Working with topic data using bags: to simulate the condition for certain environment for the ease of development
ros2 bag record /number -o bag1 - This creates a directory with
.mcapfile consisting of the recorded messages and YAML file with further details such as the recorded duration, topics and number of recorded messages - Run
ros2 bag play ~/bags/bag1/to replay the bag file
Creating Custom Interface for a Topic
- Create a dedicated package to create customed interface: follow naming convention
{robot/application name}_interfacesCustom interface packages must be declared as a C++ package (ament_cmake) so it can generate cross-language headers for both C++ and Python nodes.ros2 pkg create --build-type ament_cmake my_robot_interfaces - Navigate to the created directory, remove
/srcand/includeand create a new directory/msg - Update
package.xmlandCMakeLists.txt:
<!-- package.xml -->
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rosidl_default_generators</depend>
<member_of_group>rosidl_interface_packages</member_of_group>
# CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(my_robot_interfaces)
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/CustomTelemetry.msg"
)
ament_package()
ROS2 Debugging
Some of the useful commands I come across for troubleshooting and debugging,
There can be times when the node isn't running but still able to list the topic/service. That is due to stale data in the ros2 daemon cache.
ros2 daemon stop && ros2 daemon start
What's Next? (Moving Beyond the Basics)
We've covered how Nodes wake up, talk via Topics, and spin using timers. But ROS2 has two other massive communication pillars that we only scratched the surface of:
- Services: The synchronous, Request-Response model (like calling an API or using a vending machine).
- Actions: The asynchronous, long-running goal framework perfect for complex robot tasks (like driving to a kitchen while sending progress feedback).
Some of the concepts I would want to explore further include:
- Writing Service Server/Client nodes in Python.
- Creating Custom
.msgand.srvInterfaces (and finishing thatCMakeLists.txtconfiguration!). - Diving deep into the DDS (Data Distribution Service) layer to understand how ROS2 handles network discovery under the hood.
Until then, happy coding.