6
ROS Concepts

6.1 Introduction

Before diving into the vast world of ROS , it is worth looking at some concepts and ideas which are used. Most of the concept we will cover will be relatively high-level and therefore are here to give you more of an idea of the cogs and gears working in the system.

In a nutshell, ROS is a middle-ware 1 1 a software that facilitates communication and data management between applications, systems, and databases. software based on a strongly-typed , anonymous publish/subscribe mechanism which allows for message passing between different processes.

At the heart of any ROS system is the ROS graph where the ROS graph refers to the network of nodes in a ROS system and the connections between them by which they communicate.

6.2 Publisher and Subscriber Architecture

A common design pattern in used concurrency programming is the producer-consumer architecture , where;

  • One or more threads or processes act as a producer, where it adds elements to some shared data structure,

  • One or more other threads act as a consumer where it removes items from that structure and does something with them.

To demonstrate, imagine the producer is preparing coffee for the customers. This makes the customers, consumers. As the barista makes coffee, it creates a queue of coffees waiting to be consumed by the customers. Queues operate on a simple principle called a First In First Out () . This means items are removed in the same order that they’re put into the queue. The first item added, in this case coffee, will be the first item to be removed. So when a customer wants to consume another cup of coffee, they’ll grab one from the end of the line because it’s been in the queue the longest. The coffee represent elements of data for the consumer thread to process or perhaps packaged tasks for the consumer to execute.

An important factor to consider is the need of a protection to make sure the producer won’t try to add data to the queue when it’s full and the consumer won’t try to remove data from an empty buffer. Some programming languages may include implementations of a queue that’s considered thread-safe and handles all of these challenges under the hood, so we don’t have to, but some languages does not include that support. Therefore we need to use the combination of a mutex (mutual exclusion) and condition variables to implement your own thread-safe, synchronised queue.

We may run into scenarios where the producer cannot be paused if the queue fills up, for example, when it is an external source of streaming data that we can’t slow down, so it’s important to consider the rate at which items are produced and consumed from the queue. If the consumer can’t keep up with production, then we face a buffer overflow , and we’ll lose data. Some programming languages offer implementations of unbounded queues, which are implemented using linked lists to have an advertised unlimited capacity. But keep in mind, even those will be limited by the amount of physical memory in the computer. The rate at which the producer is adding items may not always be consistent. For example, in network applications, data might arrive in bursts of network packets, which fill the queue quickly. But if those bursts occur infrequently, the consumer has time to catch up between bursts.

We should consider the average rate at which items are produced and consumed as we want the average rate of production to be less than the average rate of consumption. But if more steps were required to process this data, then we could expand our simple producer-consumer setup into a pipeline of tasks. A pipeline consists of a chain of processing elements arranged so that the output of each element is the input to the next one. It’s basically a series of producer-consumer pairs connected together with some sort of buffer, like a queue, between each consecutive element.

6.3 Nodes - The Building Blocks

A node is an actor in the overall ROS graph, which uses a client library 2 2 This could either be rospy or roscpp depending on the chosen language. to communicate with other nodes .

Nodes can also communicate with other nodes within the same process, in a different process, or on a different machine and are considered the unit of computation in a ROS graph as each node should do one logical thing.

Nodes can either publish to named topics to deliver data to other nodes, or subscribe to named topics to get data from other nodes. They can also act as a service client to have another node perform a computation on their behalf, or as a service server to provide functionality to other nodes.

For long-running calculations, a node can act as an action client to have another node perform it on their behalf, or as an action server to provide functionality to other nodes. Nodes can provide configurable parameters to change behaviour during run-time.

Nodes are often a complex combination of publishers, subscribers, service servers, service clients, action servers, and action clients, all at the same time.

An advantage of using nodes is that it allows language agnostic programming . It means we can write one node in Python, another node in C++, and both can communicate without any problem. In addition they provide a great fault tolerance . As nodes only communicate through ROS , they are not directly linked. If one node crashes, it will not make the other nodes crash.

For all these actions to work, a node needs to know where others are. Therefore, to achieve this we use a method called a distributed discovery process .

6.4 The Discovery Process

Discovery of nodes happens automatically through the underlying middle-ware of ROS . The sequence of operations can be summarised as follows:

1.
When a node is started, it advertises its presence to other nodes on the network with the same ROS domain. 3 3 This is set with the ROS_DOMAIN_ID environment variable which is used to isolate multiple ROS systems from each other on the same network. Nodes respond to this advertisement with information about themselves so that the appropriate connections can be made and the nodes can communicate.
2.
Nodes periodically announce their presence so connections can be made with new-found entities, even after the initial discovery period .
3.
Nodes advertise to other nodes when they go offline.

Nodes will only establish connections with others having compatible Quality of Service. 4 4 In this context it means the description or measurement of the overall performance of a service, such as a telephony or computer network, or a cloud computing service, particularly the performance seen by the users of the network.

As an example to see whats going on let’s used the built-in example of talker-listener. As the name implies, one node advertises the presence and the other listens.

bash
. ~/ros2_humble/install/local_setup.bash ros2 run demo_nodes_cpp talker
text
[INFO] [1748930700.789182596] [talker]: Publishing: 'Hello World: 1' [INFO] [1748930701.785043679] [talker]: Publishing: 'Hello World: 2' [INFO] [1748930702.785231597] [talker]: Publishing: 'Hello World: 3' [INFO] [1748930703.785897764] [talker]: Publishing: 'Hello World: 4'

Now it is running in one, open another terminal window, and type:

bash
. ~/ros2_humble/install/local_setup.bash ros2 run demo_nodes_py listener
text
[INFO] [1748930759.815469762] [listener]: I heard: [Hello World: 60] [INFO] [1748930760.787999512] [listener]: I heard: [Hello World: 61] [INFO] [1748930761.787663179] [listener]: I heard: [Hello World: 62] [INFO] [1748930762.787127847] [listener]: I heard: [Hello World: 63]

Running the C++ talker node in one terminal will publish messages on a topic, and the Python listener node running in another terminal will subscribe to messages on the same topic.

We should see that these nodes discover each other automatically, and begin to exchange messages.

6.5 Communication Between Nodes

6.5.1 Description

ROS applications generally communicate through interfaces of one of three (3) types:

1.
topics,
2.
services,
3.
actions.

ROS uses a simplified description language 5 5 interface definition language. to describe these interfaces. This description allows simplification for ROS tools to automatically generate source code for the interface type in several target languages. 6 6 i.e., Python, C++ In this document we will describe the supported types:

msg

simple text files describing the fields of a ROS message. They are used to generate source code for messages in different languages.

srv

describes a service and composed of two (2) parts:

  • a request, and

  • a response.

The request and response are message declarations .

action

describes actions and composed of three (3) parts:

  • a goal,

  • a result, and

  • a feedback.

Each part is a message declaration itself .

6.5.2 Messages

Messages are a way for a ROS node to send data on the network to other ROS nodes, without expecting any responses .

As an example, let’s say a ROS node reads temperature data from a sensor. It can then publish that data on the ROS network using a Temperature message. Other nodes on the ROS network can subscribe to that data and receive the Temperature message.

Messages are described and defined in .msg files in the msg/ directory of a ROS package.

.msg files are composed of two (2) parts:

Fields

Information about a data type and its name

Constants

Similar to other languages, define an unchangeable variable.

Fields

Each field consists of a type and a name , separated by a space. The syntax for this would be the following:

text
fieldtype1 fieldname1 fieldtype2 fieldname2 fieldtype3 fieldname3

For example, lets say we want to define a 32-bit integer and a string variable. For that we would have to write the following:

text
int32 my_int string my_string

Field Types As with most statically-typed languages we need to define what type of variables we are working. In ROS , the fields type can be one of two (2) things:

  • A built-in type,

  • names of Message descriptions defined on their own, such as geometry_msgs/PoseStamped

The following is a table of all currently built-in variable types.

Type name C++ Python DDS type
bool bool builtins.bool boolean
byte uint8_t builtins.bytes* octet
char char builtins.int* char
float32 float builtins.float* float
float64 double builtins.float* double
int8 int8_t builtins.int* octet
uint8 uint8_t builtins.int* octet
int16 int16_t builtins.int* short
uint16 uint16_t builtins.int* unsigned short
int32 int32_t builtins.int* long
uint32 uint32_t builtins.int* unsigned long
int64 int64_t builtins.int* long long
uint64 uint64_t builtins.int* unsigned long long
string std::string builtins.str string
wstring std::u16string builtins.str wstring

In addition, array-like feature are also supported

Type name C++ Python DDS type
static array std::array<T, N> builtins.list* T[N]
unbounded dynamic array std::vector builtins.list sequence
bounded dynamic array custom_class<T, N> builtins.list* sequence<T,N>
bounded string std::string builtins.str* string

All types which are more permissive than their ROS definition enforce the ROS constraints in range and length by software.

Example of message definition using arrays and bounded types:

text
int32[] unbounded_integer_array int32[5] five_integers_array int32[<=5] up_to_five_integers_array string string_of_unbounded_size string<=10 up_to_ten_characters_string string[<=5] up_to_five_unbounded_strings string<=10[] unbounded_array_of_strings_up_to_ten_characters_each string<=10[<=5] up_to_five_strings_up_to_ten_characters_each

Naming Conventions There are some restrictions in how these variables are named and therefore are worth of mention. Field names must be lowercase alphanumeric characters with underscores for separating words . They must start with an alphabetic character, and they must NOT end with an underscore or have two (2) consecutive underscores.

Default Values Default values can be set to any field in the message type. Currently default values are NOT supported for string arrays and complex types. 7 7 types NOT present in the built-in-types which applies to all nested messages.

Defining a default value is done by adding a third element to the field definition line. For example:

text
fieldtype fieldname fielddefaultvalue

An example implementation would be:

text
uint8 x 42 int16 y -2000 string full_name "John Doe" int32[] samples [-200, -100, 0, 100, 200]

For example, in the first line we define a 8-bit unsigned integer (uint8), call it x , and give it a default value of 42 .

String values must be defined in ' or " quotes and currently string values are NOT escaped.

Constants Each constant definition is like a field description with a default value, except that this value can never be changed programmatically . This value assignment is indicated by use of an equal (=) sign. The syntax is:

text
constanttype CONSTANTNAME=constantvalue

For example:

text
int32 X=123 int32 Y=-123 string FOO="foo" string EXAMPLE='bar'

Constants names have to be UPPERCASE .

Services

Services are a request/response communication, where the client (requester) is waiting for the server (responder) to make a short computation and return a result. Services are described and defined in .srv files in the srv directory of a ROS package.

A service description file consists of a request and a response msg type, separated by ---. Any two .msg files concatenated with a --- are a legal service description. Here is a very simple example of a service that takes in a string and returns a string:

text
uint32 request --- uint32 response

We can of course get much more complicated: 8 8 if we want to refer to a message from the same package we must NOT mention the package name.

text
uint32 a uint32 b --- uint32 sum

We cannot embed another service inside of a service.

Actions

Actions are a long-running request/response communication, where the action client (requester) is waiting for the action server ( the responder ) to take some action and return a result. In contrast to services, actions can be long-running, 9 9 This could be many seconds up to minutes. provide feedback while they are happening, and can be interrupted.

Action definitions have the following form:

text
<request_type> <request_fieldname> --- <response_type> <response_fieldname> --- <feedback_type> <feedback_fieldname>

Similar to services, the request fields are before and the response fields are after the first triple-dash (---), respectively. There is also a third set of fields after the second triple-dash, which is the fields to be sent when sending feedback. There can be:

  • arbitrary numbers of request fields (including zero),

  • arbitrary numbers of response fields (including zero), and

  • arbitrary numbers of feedback fields (including zero).

The <request_type>, <response_type>, and <feedback_type> follow all of the same rules as the <type> for a message.

The <request_fieldname>, <response_fieldname>, and <feedback_fieldname> follow all of the same rules as the <fieldname> for a message.

As an example, the Fibonacci action definition contains the following:

text
int32 order --- int32[] sequence --- int32[] sequence

This is an action definition where the action client is sending a single int32 field representing the number of Fibonacci steps to take, and expecting the action server to produce an array of int32 containing the complete steps. Along the way, the action server may also provide an intermediate array of int32 contains the steps accomplished up until a certain point.

6.6 Topics

Topics are one of the three (3) primary styles of interfaces provided by ROS . Topics should be used for continuous data streams , like sensor data, robot state, etc. As stated earlier, ROS is a strongly-typed , anonymous publish/subscribe system. Let’s break down that sentence and explain it a bit more.

6.6.1 Publisher - Subscriber Architecture

A publish/subscribe system is one in which there are:

1.
producers of data (publishers),
2.
consumers of data (subscribers).

The publishers and subscribers know how to contact each other through the concept of a topic , which is a common name so that the entities can find each other.

When we create a publisher, we must also give it a string which is the name of the topic and the same goes for the subscriber.

Any publishers and subscribers that are on the same topic name can directly communicate with each other . There may be zero or more publishers and zero or more subscribers on any particular topic. When data is published to the topic by any of the publishers, all subscribers in the system will receive the data.

This system is also known as a bus as it resembles a bus bar from used in power engineering. This concept of a bus is part of what makes ROS a powerful and flexible system. Publishers and subscribers can come and go as needed, meaning that debugging and introspection are natural extensions to the system.

As an example, if we want to record data, we can use the ros2 bag record command. Under the hood, ros2 bag record creates a new subscriber to whatever topic we tell it, without interrupting the flow of data to the other parts of the system.

6.6.2 Anonymity

Another fact mentioned in the introduction is that ROS is anonymous . This means that when a subscriber gets a piece of data, it doesn’t generally know or care which publisher originally sent it. 10 10 If needed though, it can find out. The benefit to this architecture is that publishers and subscribers can be swapped out at will without affecting the rest of the system.

6.6.3 Strongly-Typed

As a last point, the publish/subscribe system is strongly-typed . That has two (2) meanings in this context:

  • The types of each field in a ROS message are typed, and that type is enforced at various levels. For instance, if the ROS message contains:

    text
    uint32 field1 string field2
  • Then the code will ensure the field1 is always an unsigned integer and field2 is always a string .

  • The semantics of each field are well-defined .

    • There is no automated mechanism to ensure this, but all of the core ROS types have strong semantics associated with them.

    • For instance, the IMU message contains a 3-dimensional vector for the measured angular velocity, and each of the dimensions is specified to be in radians/second. Other interpretations should NOT be placed into the message.

6.7 Services

In ROS , a service refers to a remote procedure call. In other words,

a node can make a remote procedure call to another node which will do a computation and return a result.

This structure is reflected in how a service message definition looks:

text
uint32 request --- uint32 response

In ROS , services are expected to return quickly, as the client is generally waiting on the result. Services should never be used for longer running processes , in particular processes that might need to be pre-empted for exceptional situations.

If we have a service that will be doing a long-running computation, consider using an action .

Services are identified by a service name, which looks much like a topic name. 11 11 but is in a different namespace. A service consists of two (2) parts:

  • the service server,

  • the service client.

6.7.1 Service Server

A service server is the entity that will accept a remote procedure request, and perform some computation on it. For instance, suppose the ROS message contains the following:

text
uint32 a uint32 b --- uint32 sum

The service server would be the entity that receives this message, adds a and b together, and returns the sum.

There should only ever be one (1) service server per service name. It is undefined which service server will receive client requests in the case of multiple service servers on the same service name.

Service Client

A service client is an entity that will request a remote service server to perform a computation on its behalf. Following the previous example, the service client is the entity that creates the initial message containing a and b, and waits for the service server to compute the sum and return the result.

Unlike service server, there can be a number of service clients using the same service name.

6.8 Actions

In ROS , an action refers to a long-running remote procedure call with feedback and the ability to cancel or pre-empt the goal. As an example, the high-level state machine running a robot may call an action to tell the navigation subsystem to travel to a way point, which may take several seconds, or even minutes to do. Along the way, the navigation subsystem can provide feedback on how far along it is, and the high-level state machine has the option to cancel or preempt the travel to that way point.

This structure is reflected in how an action message definition looks:

text
<request_type> <request_fieldname> --- <response_type> <response_fieldname> --- <feedback_type> <feedback_fieldname>

In ROS , actions are expected to be long running procedures, as there is overhead in setting up and monitoring the connection.

If we need a short running remote procedure call, consider using a service instead.

Actions are identified by an action name, which looks much like a topic name (but is in a different namespace) and consists of two parts:

1.
the action server,
2.
the action client.

6.8.1 Action Server

The action server is the entity that will accept the remote procedure request and perform some procedure on it. It is also responsible for sending out feedback as the action progresses and should react to cancellation/preemption requests.

For instance, consider an action to calculate the Fibonacci sequence with the following interface we looked at previously:

text
int32 order --- int32[] sequence --- int32[] sequence

The action server is the entity that receives this message, starts calculating the sequence up to order (providing feedback along the way), and finally returns a full result in sequence.

There should only ever be one action server per action name. It is undefined which action server will receive client requests in the case of multiple action servers on the same action name.

6.8.2 Action Client

An action client is an entity that will request a remote action server to perform a procedure on its behalf. Following the previous example, the action client is the entity that creates the initial message containing the order, and waits for the action server to compute the sequence and return it (with feedback along the way).

Unlike action server, there can be a number of action clients using the same action name.

6.9 Parameters

Parameters in ROS are associated with individual nodes . Parameters are used to configure nodes at startup, and during runtime without changing the code . The lifetime of a parameter is tied to the lifetime of the node. 12 12 though the node could implement some sort of persistence to reload values after restart. Parameters are addressed by node name , node namespace , parameter name , and parameter namespace .

Providing a parameter namespace is optional.

Each parameter consists of a key , a value , and a descriptor . The key is a string and the value is one of the following types:

bool, int64, float64, string, byte[], bool[], int64[], float64[] or string[].

By default all descriptors are empty, but can contain parameter descriptions, value ranges, type information, and additional constraints.

6.9.1 A Detailed Look

Declaring Parameters

By default, a node needs to declare all of the parameters that it will accept during its lifetime.

We do this so the type and name of the parameters are well-defined at node startup time, which reduces the chances of misconfiguration later on.

For some types of nodes, not all of the parameters will be known ahead of time . In these cases, the node can be instantiated with allow_undeclared_parameters set to true, which will allow parameters to be get and set on the node even if they haven’t been declared.

Types of Parameters

Each parameter on a ROS node has one of the pre-defined parameter types as mentioned.

By default, any attempts to change the type of a declared parameter at runtime will fail . This prevents common mistakes, such as putting a boolean value into an integer parameter.

If a parameter needs to be multiple different types, and the code using the parameter can handle it, this default behaviour can be changed. When the parameter is declared, it should be declared using a ParameterDescriptor with the dynamic_typing member variable set to true.

Parameter Callbacks

A ROS node can register two (2) different types of callbacks to be informed when changes are happening to parameters. Both of the callbacks are optional .

1.
The first is known as a set parameter callback, and can be set by calling from the node Application Programming Interface (API) , 13 13 A way for two or more computer programs or components to communicate with each other. It is a type of software interface, offering a service to other pieces of software. add_on_set_parameters_callback. The callback is passed a list of immutable Parameter objects, and returns an rcl\_interfaces/msg/SetParametersResult.

The primary purpose of set parameter callback is to give the user the ability to inspect the upcoming change to the parameter and explicitly reject the change.

It is important to make sure that set parameter callbacks have no side-effects. Since multiple set parameter callbacks can be chained, there is no way for an individual callback to know if a later callback will reject the update. If the individual callback were to make changes to the class it is in, for instance, it may get out-of-sync with the actual parameter. To get a callback after a parameter has been successfully changed, we may need to look into the lower option.

2.
The second type of callback is known as an on parameter event callback, and can be set by calling on_parameter_event from the parameter client API s. The callback is passed an rcl\_interfaces/msg/ParameterEvent object, and returns nothing. This callback will be called after all parameters in the input event have been declared, changed, or deleted.

The primary purpose of on parameter event callback is to give the user the ability to react to changes from parameters that have successfully been accepted.

6.9.2 Parameter Interaction

ROS 2 nodes can perform parameter operations through node API s. External processes can perform parameter operations via parameter services that are created by default when a node is instantiated.

The services that are created by default are:

node\_name/describe\_parameters : Uses service type rcl\_interfaces/srv/DescribeParameters. Given a list of parameter names, returns a list of descriptors associated with the parameters.

node\_name/get\_parameter\_types : Uses service type rcl\_interfaces/srv/GetParameterTypes. Given a list of parameter names, returns a list of parameter types associated with the parameters.

node\_name/get\_parameters : Uses service type rcl\_interfaces/srv/GetParameters. Given a list of parameter names, returns a list of parameter values associated with the parameters.

node\_name/list\_parameters : Uses service type rcl\_interfaces/srv/ListParameters. Given an optional list of parameter prefixes, returns a list of the available parameters with that prefix. If the prefixes are empty, returns all parameters.

node\_name/set\_parameters : Uses service type rcl\_interfaces/srv/SetParameters. Given a list of parameter names and values, attempts to set the parameters on the node. Returns a list of results from trying to set each parameter; some of them may have succeeded and some may have failed.

node\_name/set\_parameters\_atomically : Uses service type rcl\_interfaces/srv/SetParametersAtomically. Given a list of parameter names and values, attempts to set the parameters on the node. Returns a single result from trying to set all parameters, so if one failed, all of them failed.

Some additional information regarding parameters are as follows:

Setting Initial Parameters Values when a Node is Running Initial parameter values can be set when running the node either through individual command-line arguments, or through YAML files.

Information : The .yaml Format

A human-readable data serialization language. It is commonly used for configuration files and in applications where data is being stored or transmitted. Stands for YAML Ain’t Markup Language .

Setting Initial Parameters Values when a Node is Launching Initial parameter values can also be set when running the node through the ROS 2 launch facility.

Manipulating parameter values at runtime The ros2 param command is the general way to interact with parameters for nodes that are already running. ros2 param uses the parameter service API as described above to perform the various operations.

6.10 Working with Command Line

ROS 2 includes a suite of command-line tools for introspecting a ROS 2 system.

The main entry point for the tools is the command ros2, which itself has various sub-commands for introspecting and working with nodes, topics, services, and more.

To see all available sub-commands run:

bash
ros2 --help
text
usage: ros2 [-h] [--use-python-default-buffering] Call `ros2 <command> -h` for more detailed usage. ... ros2 is an extensible command-line tool for ROS 2. options: -h, --help show this help message and exit --use-python-default-buffering Do not force line buffering in stdout and instead use the python default buffering, which might be affected by PYTHONUNBUFFERED/-u and depends on whatever stdout is interactive or not Commands: action Various action related sub-commands bag Various rosbag related sub-commands component Various component related sub-commands daemon Various daemon related sub-commands doctor Check ROS setup and other potential issues interface Show information about ROS interfaces launch Run a launch file lifecycle Various lifecycle related sub-commands multicast Various multicast related sub-commands node Various node related sub-commands param Various param related sub-commands pkg Various package related sub-commands run Run a package specific executable security Various security related sub-commands service Various service related sub-commands topic Various topic related sub-commands wtf Use `wtf` as alias to `doctor` Call `ros2 <command> -h` for more detailed usage.

ROS 2 uses a distributed discovery process for nodes to connect to each other. As this process purposefully does not use a centralized discovery mechanism, it can take time for ROS nodes to discover all other participants in the ROS graph. Because of this, there is a long-running daemon in the background that stores information about the ROS graph to provide faster responses to queries, e.g. the list of node names.

The daemon is automatically started when the relevant command-line tools are used for the first time. We can run ros2 daemon –help for more options for interacting with the daemon.

6.11 Launch File

A ROS 2 system typically consists of many nodes running across many different processes (and even different machines). While it is possible to run each of these nodes separately, it gets cumbersome quite quickly.

The launch system in ROS 2 is meant to automate the running of many nodes with a single command. It helps the user describe the configuration of their system and then executes it as described. The configuration of the system includes what programs to run, where to run them, what arguments to pass them, and ROS-specific conventions which make it easy to reuse components throughout the system by giving them each a different configuration. It is also responsible for monitoring the state of the processes launched, and reporting and/or reacting to changes in the state of those processes.

All of the above is specified in a launch file, which can be written in Python, XML, or YAML. This launch file can then be run using the ros2 launch command, and all of the nodes specified will be run.