← Back to writing

ROS2 Fundamentals

What is ROS2 and how to use it properly

robotics

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

  1. Topics
  2. Services
  3. 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 topics
  • ros2 topic info /chatter: check how many subscribers and publisher node to the topic /chatter and 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:

  1. [service] Goal: navigates to the kitchen
  2. [topic] Feedback: sends the progress of the robot's navigation; "Robot is 50% there"
  3. [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 communications
  • node = MyFirstNode(): create and initialise node object
  • rclpy.spin(node): enables the program and node to stay alive, otherwise the program exists after creating the node
  • rclpy.shutdown(): node is killed with ctrl C and 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.py and execute with ros2 run
  • Node name: defined in node object class, shows in ros2 node list or rqt_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 implementation
  • ros2 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_graph
  • ros2 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 .mcap file 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

  1. Create a dedicated package to create customed interface: follow naming convention {robot/application name}_interfaces Custom 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
  2. Navigate to the created directory, remove /src and /include and create a new directory /msg
  3. Update package.xml and CMakeLists.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 .msg and .srv Interfaces (and finishing that CMakeLists.txt configuration!).
  • Diving deep into the DDS (Data Distribution Service) layer to understand how ROS2 handles network discovery under the hood.

Until then, happy coding.

Appreciate this?

Leave a mark.

Loading...