13.3. Case Study: Using ROS¶
In this section, we will guide you through the process of installing ROS 2 and configuring the environment. We will also provide a few code examples to deepen your understanding of ROS 2 usage and concepts introduced in the previous section.
Our environment consists of the following: ROS 2 Foxy Fitzroy, the
latest ROS 2 long-term support (LTS) release at the time of writing;
Ubuntu Focal 20.04; and Python 3.8 (our examples use Python 3.8.10)
installed in Ubuntu Focal. Ubuntu Focal is the official software for
installing ROS 2 Foxy Fitzroy. If you install ROS 2 via Debian packages
(recommended), you must use the version of Python 3 on Ubuntu. This is
because many Python dependencies of ROS 2 will be automatically
installed in the Python 3 path on Ubuntu via apt install
(instead of
via pip install
). In other words, when you select the ROS 2 version,
the versions of Ubuntu and Python you need are automatically determined.
If you want to use a Python virtual environment, you must also use the
Python interpreter provided by Ubuntu and add the site-packages
option during the creation stage because we need the ROS 2 dependencies
installed in the Python 3 path.
For example, a pipenv user can use the following command to create a
virtual environment that uses the installed Python 3 and has
site-packages
:
pipenv --python $(/usr/bin/python3 -V | cut -d" " -f2) --site-packages
Due to Python 3 being installed, virtual environments created with conda may experience some compatibility issues. Other versions of ROS 2 are installed and operate in essentially the same way.
In this section and subsequent cases, we may use ROS 2, Ubuntu, and Python to refer to ROS 2 Foxy Fitzroy, Ubuntu Focal, and Python 3.8, respectively.
Cases in this section can be found in the ROS 2 official tutorial, which is very detailed and ideal for ROS 2 beginners. You can learn more about ROS 2 there.
1. Installing ROS 2 Foxy Fitzroy
Following the official tutorial makes it relatively easy to install ROS 2 on Ubuntu (e.g., installing ROS 2 Foxy Fitzroy and Ubuntu Focal). The installation procedure of ROS 2 described in this section is primarily based on this tutorial.
2. Setting the system to support the UTF-8 locale
Before installation, ensure that the Ubuntu system locale is set to a
value that supports UTF-8. You can check the current locale settings via
the locale command. If the value of LANG
ends with .UTF-8
, the
system already supports the UTF-8 locale. Otherwise, use the following
commands to set it as the UTF-8 value in English (US). You can change
the language code to match your desired language.
sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
3. Setting the software source
You also need to add the software source of ROS 2 to the system by running the following commands:
sudo apt update && sudo apt install curl gnupg2 lsb-release
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
4. Installing ROS 2
Once the preceding steps are complete, update the software source cache and then install the ROS 2 Desktop version. This is the preferred version because it contains the ROS 2 framework and most of the software libraries commonly used in ROS 2 development, such as rviz.
sudo apt update
sudo apt install ros-foxy-desktop
In addition, install colcon (a build tool for ROS 2) and rosdep (which can help us quickly install a dependency required by the ROS 2 project).
sudo apt-get install python3-colcon-common-extensions python3-rosdep
Now you have installed ROS 2. However, an additional step is required to set up the environment before using ROS 2.
5. Setting the environment
For any installed version of ROS 2 (and ROS), you need to set up the
required environment by sourcing the corresponding setup
script
before using the version. For example, for the newly installed ROS 2
Foxy Fitzroy, you can run the following command in the terminal to set
up the environment required by ROS 2:
source /opt/ros/foxy/setup.bash
If you are using a shell other than bash
, you may need to change the
file name extension of setup
to the name of the corresponding shell.
For example, if you are using zsh, you would need to run the command
source /opt/ros/foxy/setup.zsh
.
To avoid entering preceding command every time you use ROS 2, you can
add this command to your .bashrc
file (or .zshrc
and other
corresponding shell files). This way, every new command-line terminal
you build will be automatically set to the environment that ROS 2
requires.
The advantage of following this process to set up the environment is
that you can install multiple versions of ROS 2 (and ROS), and then
source the setup.bash
file of the corresponding version when you use
it without interference from other versions.
If you are a heavy user of Python, adding setup.bash
to .bashrc
may cause issues. This is because all your virtual environments will
automatically introduce the environment setup of ROS 2, and the Python
libraries in ROS 2 will be added to the paths of your virtual
environments. Although virtual environments may detect ROS 2 libraries,
those libraries will not be used or disrupt the program operation in
your virtual environments.
To avoid such issues, if you plan to develop a ROS 2 project primarily
using Python, you can create a virtual environment for the project and
add source /opt/ros/foxy/setup.bash
to the activate
script of
the virtual environment.
Note that you may need to add this source
command to the very
beginning or close to the end of the script. (If, for example, you are a
pipenv user, you need to add this command before the
hash -r 2>/dev/null
command instead of putting it at the end.)
Otherwise, the following error may occur when you activate the virtual
environment:
Shell for UNKNOWN_VIRTUAL_ENVIRONMENT already activated.
No action taken to avoid nested environments.
6. Testing the installation
After you run the source
command, you can test whether ROS 2 is
successfully installed and the environment is set up correctly. You can
execute printenv | grep -i R̂OS
on the command line where the
source
command is executed. The output should contain the following
three environment variables:
ROS_VERSION=2
ROS_PYTHON_VERSION=3
ROS_DISTRO=foxy
In addition, you can open another two terminal windows that have
executed the source
command, and then execute
Code code/ch17/code_terminal_1
on terminal 1 and
Code code/ch17/code_terminal_2
on terminal 2.
code/ch17/code_terminal_1
ros2 run demo_nodes_cpp talker
code/ch17/code_terminal_2
ros2 run demo_nodes_py listener
If the installation was successful and the source
command is
executed, talker
shows that it is broadcasting messages, and
listener
shows that it has heard the messages.
Congratulations, you have now successfully installed ROS 2 and set up the environment. The following sections will provide a few examples to illustrate the core concepts of ROS 2 introduced in the previous section.
13.3.1. Node Creation¶
In this subsection, we will create a ROS 2 project and use Python to
write an example of Hello World
to show the basic structure of ROS 2
nodes.
1. Creating a new ROS 2 project
First, create a folder in an appropriate location. This folder will be
the root directory of the ROS 2 project, that is, the “workspace”
introduced in the previous section. Because this workspace is manually
created, it is an overlay workspace. In contrast, the source
command
executed earlier will help you prepare the underlay workspace on which
the overlay workplace is based.
Suppose you create a workspace and name it as openmlsys-ros2
.
mkdir openmlsys-ros2
cd openmlsys-ros2
In this case, you then need to create a Python virtual environment for
the workspace and add the source
command to the corresponding
activate
script, as described in the previous subsection about
environment setup.
By default, all commands in the case study subsections are executed in this newly built virtual environment. Because different tools for managing virtual environments have different commands, we do not provide examples of executable commands here.
Next, create a subfolder named src
in this workspace folder. Within
this subfolder, create different ROS 2 packages. They are independent of
each other, but can call each other’s functions to meet the requirements
of the entire ROS 2 project.
After the src
subfolder is created, call the colcon build
command, which is a common build tool for ROS 2. This command attempts
to build the entire ROS 2 project (i.e., all packages in the current
workspace). After this command is executed successfully, three new
folders will be created in the workspace: build
, install
, and
log
. build
includes the intermediate outputs, install
contains the final outputs (i.e., well-built packages) of the build
process, and log
stores the process logs.
At this point, with this new framework for the ROS 2 project, you are ready to start coding.
2. Creating a Python package under the ROS 2 framework
In the src
folder, create a ROS 2 package where you will write the
case of Hello World
.
cd src
ros2 pkg create --build-type ament_python --dependencies rclpy std_msgs --node-name hello_world_node my_hello_world
pkg create
in the ros2
command can help you quickly create a
framework for the ROS 2 package. The build-type
parameter indicates
that this is a pure Python package, and the dependencies
parameter
indicates that the package will use two dependencies — rclpy
and
std_msgs
. The node-name
parameter shows that the package created
has a ROS 2 node named hello_world_node
, and that my_hello_world
at the end is the name of the new package.
Go to the new package folder my_hello_world
. Once the preceding
command is executed, a Python package folder named my_hello_world
is
created. It is named after the package and contains files of
__init__.py
and hello_world_node.py
. The latter exists because
you use the node_name
parameter. This is the Python package folder
in which you will write your Python code.
There are another two folders: resource
and test
. The former
helps ROS 2 locate the Python package, and can be ignored. The latter is
used to accommodate all the test code, and it already contains three
test files.
In addition to these three folders, there are three files:
package.xml
, setup.cfg
, and setup.py
.
package.xml
is the standard configuration file for the ROS 2
package. In contains a lot of pre-generated content. However, you still
need to fill in or update the content in version
, description
,
maintainer
, and license
. You are advised to fill in all content
completely each time you create a ROS 2 package. In addition, rclpy
and std_msgs
are listed as dependencies because you used the
dependencies
parameter. To add or modify dependencies, you can
directly modify the depend
list in package.xml
. In addition to
the most commonly used depend
(for build
, export
, and
execution
), you also have build_depend
, build_export_depend
,
exec_depend
, test_depend
, buildtool_depend
, and
dec_depend
. For details about package.xml
, see the Wiki page.
Both setup.cfg
and setup.py
are files related to the Python
package. However, ROS 2 also uses them to learn how to install the
Python package to the install
folder and what entry points —
programs that can be directly called by the ROS 2 command-line commands
— need to be registered. Notice that the name hello_world_node
has
been set to my_hello_world/hello_world_node.py
— the alias of the
main()
function in the Python file — in console_scripts
, a
subitem of entry_points
in setup.py
. You can call this function
directly later using the ROS 2 command-line commands and this new name
as follows:
# ros2 run <package_name> <entry_point>
ros2 run my_hello_world hello_world_node
To add a new entry point, you can directly add it here.
In addition to the entry points, you should promptly update the items of
version
, maintainer
, maintainer_email
, description
, and
license
in setup.py
.
3. First ROS 2 node
Open the Python file my_hello_world/hello_world_node.py
and delete
its content in order to write the desired code.
First, introduce the necessary packages:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
rclpy
— ROS Client Library for Python — allows you to use various
functions within the ROS 2 framework through Python. The Node
class
is the base class for all ROS 2 nodes. Your node class also needs to
inherit this base class. std_msgs
contains some standard message
formats predefined by ROS 2 for intra-framework communication. You need
to use the String
message format to pass string information.
Next, define your own ROS 2 node:
class HelloWorldNode(Node):
def __init__(self):
super().__init__('my_hello_world_node')
self.msg_publisher = self.create_publisher(String, 'hello_world_topic', 10)
timer_period = 1.
self.timer = self.create_timer(timer_period, self.timer_callback)
self.count = 0
def timer_callback(self):
msg = String()
msg.data = f'Hello World: {self.count}'
self.msg_publisher.publish(msg)
self.get_logger().info(f'Publishing: "{msg.data}"')
self.count += 1
As mentioned earlier, your node class HelloWorldNode
inherits from
the Node
base class.
In the __init__()
method, call the initialization method of the base
class to name your node as my_hello_world_node
. Next, create an
information publisher that publishes the information of the string type
to the topic hello_world_topic
and maintains a buffer of size 10.
Then, create a timer that calls the timer_callback()
method once
every second. Finally, initialize a counter to count the total number of
messages that have been published.
In the timer_callback()
method, create a simple message of
Hello World
with a counter and send it through the message
publisher. Then log the operation and increment the count by 1.
After the Hello World
node class is defined, start defining the
main()
function, which is the entry point in setup.py
.
def main(args=None):
rclpy.init(args=args)
hello_world_node = HelloWorldNode()
rclpy.spin(hello_world_node)
hello_world_node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
This main()
function is relatively simple. First, start the ROS 2
framework with the rclpy.init()
method. Then, create an instance of
HelloWorldNode
and add it to the running ROS 2 framework by means of
rclpy.spin()
. This ensures that the instance participates in the ROS
2 event loop and runs correctly. rclpy.spin()
is a blocking method
that runs continually until it is blocked (e.g., when the ROS 2
framework stops running). At this time, your node will be destroyed and
the ROS 2 framework closed. If you do not destroy an unused node, the
garbage collector will destroy it.
At this point, you have created your first ROS 2 node.
4. First build and run
You are now ready to build the new package. This involves installing the Python package you have written into a place where ROS 2 can find it rather than building a Python project.
# cd <workspace>
cd openmlsys-ros2
colcon build --symlink-install
Running this build command will build all Python and C++ packages in the
src
folder in the workspace and then install them in the install
folder. The –symlink-install
option requires colcon
to build
symlink
for the Python package so that installation is not
replicated. In this way, any subsequent changes in src
will be
reflected directly in install
, instead of repeatedly executing the
build command.
After the build is completed successfully, the package is still not
ready to be used yet. For example, executing
ros2 run my_hello_world hello_world_node
now will likely result in
the message Package ’my_hello_world’ not found
.
To use the package, you need to let ROS 2 know where the install
folder is. Source the local_setup.bash
file in the install
folder as follows:
source install/local_setup.bash
Unlike earlier when we added setup.bash
to the activate
script
in the virtual environment in order to eliminate the need for sourcing
the file separately each time, we cannot do the same with
install/local_setup.bash
. Doing so would cause a problem.
Specifically, to run the ROS 2 package, you have to source both
setup.bash
and install/local_setup.bash
(either through the
activate
script or manually). However, the condition of building the
pure Python ROS 2 package with C++ dependencies is sourcing only
setup.bash
but not local_setup.bash
. Subsequent cases will show
such a condition for a pure Python ROS 2 package that uses the package
of custom message interfaces (your own C++ package).
After successfully sourcing install/local_setup.bash
, you can call
the written node.
From now on, unless otherwise specified, setup.bash
and
install/local_setup.bash
in any newly created window are sourced and
the colcon build
command is executed in a terminal window where only
setup.bash
is sourced and install/local_setup.bash
is ignored.
ros2 run my_hello_world hello_world_node
Information similar to the following will be displayed:
[INFO] [1653270247.805815900] [my_hello_world_node]: Publishing: "Hello World: 0"
[INFO] [1653270248.798165800] [my_hello_world_node]: Publishing: "Hello World: 1"
You can also open a terminal window and execute
ros2 topic echo /hello_world_topic
. Information similar to the
following will be displayed:
data: 'Hello World: 23'
---
data: 'Hello World: 24'
---
This means that your information is published to the target topic. The
command ros2 topic echo <topic_name>
outputs the information
received by the topic with the specified name.
Congratulations, you have now successfully run your first ROS 2 node.
5. A message subscriber node
After a message is published, a message subscriber is also needed to consume the published information.
Create a file named message_subscriber.py
in the folder where
hello_world_node.py
is located, as shown in
Code ch17/messageSubscriber
:
ch17/messageSubscriber
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MessageSubscriber(Node):
def __init__(self):
super().__init__('my_hello_world_subscriber')
self.msg_subscriber = self.create_subscription(
String, 'hello_world_topic', self.subscriber_callback, 10
)
def subscriber_callback(self, msg):
self.get_logger().info(f'Received "{msg.data}"')
def main(args=None):
rclpy.init(args=args)
message_subscriber = MessageSubscriber()
rclpy.spin(message_subscriber)
message_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == "__main__":
main()
This newly created file and its message subscriber node class are
similar to the file and its HelloWorld
node class mentioned earlier.
During initialization, you need to use the base class initialization
method to assign the name my_hello_world_subscriber
to the node, and
then create a message subscriber to subscribe to messages under the
topic hello_world_topic
. You also need to specify the
subscriber_callback()
method to process the received messages. In
subscriber_callback()
, log the received messages. The main()
method for this node class is similar to that for the HelloWorld
node class.
Before using this new node, add it as an entry point by adding the
following line in the appropriate location of setup.py
:
'message_subscriber = my_hello_world.message_subscriber:main'
If you run ros2 run my_hello_world message_subscriber
in the
terminal window now, you will get an error message similar to “No
executable found”. That is because ROS 2 can be aware of the newly added
entry point only after rebuilding the entire ROS 2 project.
Execute colcon build –symlink-install
again in the workspace
directory. After the build is completed successfully, open two terminal
windows and make sure that the two setup files are sourced. Then call
them separately with the ros2
commands:
# in terminal 1
ros2 run my_hello_world hello_world_node
# in terminal 2
ros2 run my_hello_world message_subscriber
After the commands are run, the “Hello World: N” message is published continuously in terminal window 1, and the “Hello World: N” message is received continuously in terminal window 2.
Congratulations, you have now successfully created a pair of ROS 2 nodes, one for sending messages and the other for subscribing to and receiving messages.
13.3.2. Parameter Reading¶
Nodes in a real-world project are far more complex than the simple example provided earlier because, for example, they are parameterized. This subsection will show you how to get a node to read a parameter.
Create a new file named parametrised_hello_world_node.py
in the
folder where hello_world_node.py
is located, as shown in
Code code/ch17/parametrisedHelloWorldNode
:
code/ch17/parametrisedHelloWorldNode
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class ParametrisedHelloWorldNode(Node):
def __init__(self):
super().__init__('parametrised_hello_world_node')
self.msg_publisher = self.create_publisher(String, 'hello_world_topic', 10)
timer_period = 1.
self.timer = self.create_timer(timer_period, self.timer_callback)
self.count = 0
self.declare_parameter('name', 'world')
def timer_callback(self):
name = self.get_parameter('name').get_parameter_value().string_value
msg = String()
msg.data = f'Hello {name}: {self.count}'
self.msg_publisher.publish(msg)
self.get_logger().info(f'Publishing: "{msg.data}"')
self.count += 1
def main(args=None):
rclpy.init(args=args)
hello_world_node = ParametrisedHelloWorldNode()
rclpy.spin(hello_world_node)
hello_world_node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
HelloWorldNode
class is similar to the
previous HelloWorld
node class except for the following two
differences:self.declare_parameter()
method in the initialization methods to declare the new node instance
to the ROS 2 framework. The new node instance has a parameter named
name
, and the initial value of this parameter is world
.name
parameter in the callback function timer_callback()
and uses that
value to form the content of the message to be sent.First, register the main()
method of the new file as a new entry
point. Similarly, add the following line to the corresponding position
in setup.py
. Also, execute colcon build –symlink-install
in the
workspace root directory to rebuild the project.
'parametrised_hello_world_node = my_hello_world.parametrised_hello_world_node:main'
After the build is successfully completed, if you execute
ros2 run my_hello_world parametrised_hello_world_node
in the
terminal, you will see that this parameterized HelloWorld
node runs
normally and continuously publishes messages like “Hello World: N”. In
this case, the node uses the initial value world
.
After you execute ros2 param list
in a new terminal, you will see
the following information:
/parametrised_hello_world_node:
name
use_sim_time
This indicates that the node parametrised_hello_world_node
does
declare and use a name
parameter. Another parameter named
use_sim_time
is a default parameter given by ROS 2 to indicate
whether the node uses the simulated time inside the ROS 2 framework
rather than the system time of the computer.
To assign the value ROS2
to the name
parameter, run the
following command in the terminal:
ros2 param set /parametrised_hello_world_node name "ROS2"
If the assignment is successful, the command returns “Set parameter
successful”, and the terminal window where the parameterized
HelloWorld
node is running continuously changes the messages it
publishes to “Hello ROS2: N”.
Congratulations, you now know how to make ROS 2 nodes (and other types of ROS 2 programs) use parameters.
13.3.3. Server-Client Service Mode¶
As mentioned in the previous section, the ROS 2 framework has both a publisher-subscriber communication mode and a server-client communication mode. In this subsection, we will use a simple service that concatenates two strings to demonstrate how the server-client mode works.
1. Custom service interfaces
Before coding for the server and client, you need to define the communication interfaces between them.
There are three types of interfaces in the ROS 2 framework.
The message/msg interface for nodes in publisher-subscriber mode: It is
used for unidirectional message delivery and only defines the format of
such messages. The service/srv interface for service nodes in
server-client mode: It is used for bidirectional message delivery and
defines the format of the requests sent from the client to the server
and the format of the responses sent from the server to the client. The
action interface for action nodes in action mode: It is used for
bidirectional message delivery and intermediate progress feedback. It
defines the format of the requests sent by the action initiating node to
the action node, the format of the results sent by the action node to
the action initiating node, and the format of the intermediate progress
feedback sent by the action node to the action initiating node. The
std_msgs.msg.String
message interface in the predefined package
std_msgs
is used for the HelloWorld
nodes defined earlier.
Because the message interface is only responsible for defining the
format of unidirectional messages, predefined interface types are
readily available. For services and actions, however, you need to define
an interface type because the interfaces are responsible for defining
the format of bidirectional communications. Next, let’s define the
service type interface for the string concatenation service.
First, create a new package in the src
folder of the workspace,
which is dedicated to maintaining custom message, service, and action
interfaces.
cd openmlsys-ros2/src
ros2 pkg create --build-type ament_cmake my_interfaces
This is a new C++ package, not a Python one, because custom interface
types of ROS 2 can only support C++ packages. Once you have created the
package, update the related items in package.xml
.
To facilitate maintenance, custom interfaces are generally placed in the
corresponding subfolders. As such, you can create three subfolders in
the new folder src/my_interfaces
: msg
, srv
, and action
.
cd my_interfaces
mkdir msg srv action
Next, create the service interfaces you want to define in the srv
subfolder.
cd srv
touch ConcatTwoStr.srv
After that, add the following contents to ConcatTwoStr.srv
:
string str1
string str2
---
string ret
The contents above - - - are the format of the requests sent by the client to the server, and those below are the format of the responses sent by the server to the client.
Once the interfaces are defined, you also need to modify
CMakeLists.txt
. Open my_interfaces/CMakeLists.txt
and add the
following code before the line if(BUILD_TESTING)
:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/ConcatTwoStr.srv"
)
The code above instructs the build tool to find the required
rosidl_default_generators
package and to build the custom interfaces
you specified.
After updating CMakeLists.txt
, you also need to add
rosidl_default_generators
to package.xml
as a dependency of the
custom interface package. Open the package.xml
file and add the
following code before the line
<test_depend>ament_lint_auto</test_depend>
:
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
After package.xml
is updated, you can build the custom interface
package.
cd openmlsys-ros2
colcon build --packages-select my_interfaces
Because the package my_hello_world
has no changes, only the package
my_interfaces
is selected using the option –packages-select
.
Also, the option –symlink-install
is not used because the custom
interface package is a C++ package that must be rebuilt after each
change.
When running this build command, error messages such as
ModuleNotFoundError: No module named ’XXX’
(XXX can be em
,
catkin_pkg
, lark
, numpy
, or other Python packages) may be
displayed. Most of these errors occur because the Python virtual
environment does not point to Python 3 of the Ubuntu system or include
site-packages
. In this case, deleting the current virtual
environment and recreate a new one as described at the beginning of this
section may resolve the problem.
To verify whether the build is completed successfully, run
ros2 interface show my_interfaces/srv/ConcatTwoStr
in a new terminal
window. The terminal will display the definition of the custom service
interface ConcatTwoStr
if the build succeeded.
Now that you have defined the required service interfaces, you are ready to code the server and the client.
2. ROS 2 server
Create a file named concat_two_str_service.py
in the folder where
hello_world_node.py
is located, as shown in
Code ch17/concatTwoStrService
:
ch17/concatTwoStrService
from my_interfaces.srv import ConcatTwoStr
import rclpy
from rclpy.node import Node
class ConcatTwoStrService(Node):
def __init__(self):
super().__init__('concat_two_str_service')
self.srv = self.create_service(ConcatTwoStr, 'concat_two_str', self.concat_two_str_callback)
def concat_two_str_callback(self, request, response):
response.ret = request.str1 + request.str2
self.get_logger().info(f'Incoming request\nstr1: {request.str1}\nstr2: {request.str2}')
return response
def main(args=None):
rclpy.init(args=args)
concat_two_str_service = ConcatTwoStrService()
rclpy.spin(concat_two_str_service)
concat_two_str_service.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
The process of building a service is similar to that of building a
general node, with both the service and node being inherited from the
same base class rclpy.node.Node
. In this file, first introduce the
custom service interface ConcatTwoStr
from the built package
my_interfaces
. Use self.create_service()
to create a server
object, and specify the service interface type as ConcatTwoStr
, the
service name as concat_two_str
, and the callback function for
processing service requests as self.concat_two_str_callback
. In the
callback function self.concat_two_str_callback()
, compute str1
and str2
obtained by the request object, assign the result to
ret
of the response object, and record logs. Note that the structure
of the request and response objects conforms to the definition in
ConcatTwoStr.srv
.
In addition, add the main()
method of this file as an entry point to
setup.py
.
'concat_two_str_service = my_hello_world.concat_two_str_service:main'
13.3.4. Client¶
Create a file named concat_two_str_client_async.py
in the folder
where hello_world_node.py
is located, as shown in
Code ch17/concatTwoStrClientAsync
:
ch17/concatTwoStrClientAsync
import sys
from my_interfaces.srv import ConcatTwoStr
import rclpy
from rclpy.node import Node
class ConcatTwoStrClientAsync(Node):
def __init__(self):
super().__init__('concat_two_str_client_async')
self.cli = self.create_client(ConcatTwoStr, 'concat_two_str')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = ConcatTwoStr.Request()
def send_request(self):
self.req.str1 = sys.argv[1]
self.req.str2 = sys.argv[2]
self.future = self.cli.call_async(self.req)
def main(args=None):
rclpy.init(args=args)
concat_two_str_client_async = ConcatTwoStrClientAsync()
concat_two_str_client_async.send_request()
while rclpy.ok():
rclpy.spin_once(concat_two_str_client_async)
if concat_two_str_client_async.future.done():
try:
response = concat_two_str_client_async.future.result()
except Exception as e:
concat_two_str_client_async.get_logger().info(
'Service call failed %r' % (e,))
else:
concat_two_str_client_async.get_logger().info(
'Result of concat_two_str: (%s, %s) -> %s' %
(concat_two_str_client_async.req.str1, concat_two_str_client_async.req.str2, response.ret))
break
concat_two_str_client_async.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Coding the client is slightly more complex than coding the server. In
the initialization method for the client node, first create a client
object, and specify the service interface type as ConcatTwoSt
and
the service name as concat_two_str
. Then, through a while
loop,
the client will wait for the corresponding server to go online before
proceeding to the next step. This technique of using a loop for waiting
is used by many clients. When the server goes online, the initialization
method creates a template of the service request object and temporarily
stores the template in the req
attribute of the client node. In
addition to the initialization method, the client node also defines
another method — send_request()
— to read the first two parameters
of the command line when the program is started, store those parameters
in the service request object, and then send the object to the server
asynchronously.
In the main()
method, first create a client and send a service
request, and then use a while
loop to wait for the server to return
the result and log the result. rclpy.ok()
is used to check whether
ROS 2 is running properly so that that the client does not get trapped
in an infinite loop if ROS 2 stops running before the service ends.
rclpy.spin_once()
differs from rclpy.spin()
in that the former
executes an event loop only once whereas the latter executes an event
loop until ROS 2 stops. As such, using rclpy.spin_once()
here is
more suitable because you already have a while
loop. Note that the
object concat_two_str_client.future
provides a number of methods to
help determine the current state of the service request.
You also need to add the main()
method of this file as an entry
point to setup.py
.
'concat_two_str_client_async = my_hello_world.concat_two_str_client_async:main'
Now that the server and client are prepared, you can rebuild the package
my_hello_world
in the workspace root directory.
cd openmlsys-ros2
colcon build --packages-select my_hello_world --symlink-install
Then, run the following commands in the respective terminal windows:
# in terminal 1
ros2 run my_hello_world concat_two_str_client_async Hello World
# in terminal 2
ros2 run my_hello_world concat_two_str_service
Information similar to the following will be shown if all goes well:
# in terminal 1
[INFO] [1653525569.843701600] [concat_two_str_client_async]: Result of concat_two_str: (Hello, World) -> HelloWorld
# in terminal 2
[INFO] [1653516701.306543500] [concat_two_str_service]: Incoming request
str1: Hello
str2: World
Congratulations, you have now created the custom interface type, server node, and client node in the ROS 2 framework.
13.3.5. Action Mode¶
The previous subsection describes the server-client mode within the ROS 2 framework. This subsection introduces the action mode by summing each element of a number sequence one by one.
1. Custom action interfaces
Before coding the action-related node, you need to define the action information interface.
You can use the package my_interfaces
built earlier, create a new
file MySum.action
in my_interfaces/action
, and add the following
contents:
# Request
int32[] list
---
# Result
int32 sum
---
# Feedback
int32 sum_so_far
The action information interface is simple: The request of the action
has only one item, list
of an integer sequence. The result of the
action has only one integer, item sum
. And the intermediate feedback
has only one integer, item sum_so_far
, to calculate the accumulated
sum to the current position.
Next, add this information interface to CMakeLists.txt
.
Specifically, add action/MySum.action
after srv/ConcatTwoStr.srv
in the method rosidl_generate_interfaces()
.
Finally, build the changed package by running
colcon build –packages-select my_interface
in the workspace root
directory.
2. ROS 2 action server
Create a file named my_sum_action_server.py
in the folder where
hello_world_node.py
is located, as shown in
Code ch17/mySumActionServer
:
ch17/mySumActionServer
import rclpy
from rclpy.action import ActionServer
from rclpy.node import Node
from my_interfaces.action import MySum
class MySumActionServer(Node):
def __init__(self):
super().__init__('my_sum_action_server')
self._action_server = ActionServer(
self, MySum, 'my_sum', self.execute_callback
)
def execute_callback(self, goal_handle):
self.get_logger().info('Executing goal...')
feedback_msg = MySum.Feedback()
feedback_msg.sum_so_far = 0
for elm in goal_handle.request.list:
feedback_msg.sum_so_far += elm
self.get_logger().info(f'Feedback: {feedback_msg.sum_so_far}')
goal_handle.publish_feedback(feedback_msg)
goal_handle.succeed()
result = MySum.Result()
result.sum = feedback_msg.sum_so_far
return result
def main(args=None):
rclpy.init(args=args)
my_sum_action_server = MySumActionServer()
rclpy.spin(my_sum_action_server)
if __name__ == '__main__':
main()
Similarly, create an action server object in its initialization method
for this action server node class. Specify the defined MySum
as the
information interface type, my_sum
as the action name, and the
self.execute_callback
method as the callback function for action
execution.
Next, define the action when a new target is received in the
self.execute_callback()
method. Here, you can treat a target as the
request
part of the defined MySum
information interface. This is
because the target here is the structure that contains the information
related to the purpose of the action request, that is, the part defined
by the request
part.
When you receive a target, first create a feedback message object
feedback_msg
from MySum
and use the sum_so_far
item as an
accumulator. Then traverse the data in the list
item in the target
request and accumulate the data item by item. Each time an item is
accumulated, a feedback message is sent via the
goal_handle.publish_feedback()
method. Finally, when the computation
is complete, use goal_handle.succeed()
to indicate that the action
has been successfully performed. In addition, create a new result object
through MySum
, fill in the result value, and return it.
In the main()
function, create an instance of the action server node
class and call rclpy.spin()
to add the instance to the event loop.
Finally, add main()
as an entry point by adding the following line
in the appropriate position in setup.py
:
'my_sum_action_server = my_hello_world.my_sum_action_server:main'
13.3.6. Action Client¶
Create a file named my_sum_action_client.py
in the folder where
hello_world_node.py
is located, as shown in
Code ch17/mySumActionClient
:
ch17/mySumActionClient
import sys
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from my_interfaces.action import MySum
class MySumActionClient(Node):
def __init__(self):
super().__init__('my_sum_action_client')
self._action_client = ActionClient(self, MySum, 'my_sum')
def send_goal(self, list):
goal_msg = MySum.Goal()
goal_msg.list = list
self._action_client.wait_for_server()
self._send_goal_future = self._action_client.send_goal_async(
goal_msg, feedback_callback=self.feedback_callback
)
self._send_goal_future.add_done_callback(self.goal_response_callback)
def goal_response_callback(self, future):
goal_handle = future.result()
if not goal_handle.accepted:
self.get_logger().info('Goal rejected...')
return
self.get_logger().info('Goal accepted.')
self._get_result_future = goal_handle.get_result_async()
self._get_result_future.add_done_callback(self.get_result_callback)
def get_result_callback(self, future):
result = future.result().result
self.get_logger().info(f'Result: {result.sum}')
rclpy.shutdown()
def feedback_callback(self, feedback_msg):
feedback = feedback_msg.feedback
self.get_logger().info(f'Received feedback: {feedback.sum_so_far}')
def main(args=None):
rclpy.init(args=args)
action_client = MySumActionClient()
action_client.send_goal([int(elm) for elm in sys.argv[1:]])
rclpy.spin(action_client)
if __name__ == '__main__':
main()
This action client node class is slightly more complex than the server node class introduced earlier, because you need to send requests, receive feedback, and process results.
First, create an action client object in the initialization method of
the action client node class and specify MySum
as the message
interface type and my_sum
as the action name.
Then, declare the self.send_goal()
method to generate and send a
target/request. Specifically, create a target object from MySum
and
assign the received list
parameter to the list
attribute of the
target object. After the action server is ready, send the target
asynchronously and specify self.feedback_callback
as the feedback
callback function. Finally, set self.goal_response_callback
as the
callback function for the asynchronous operation that sends the target
information.
In self.goal_response_callback()
, check whether the target request
is accepted and log the result. If the target request is accepted,
obtain the future
object of the asynchronous operation through
goal_handle.get_result_async()
, and set self.get_result_callback
as the callback function of the final result through the future
object.
In the callback function self.get_result_callback()
of the final
result, obtain the accumulation result and log it. Finally, call
rclpy.shutdown()
to end the current node.
In the feedback message callback function self.feedback_callback()
,
obtain the feedback message and log it. Because the callback function
for the feedback message may be executed multiple times, you are advised
to keep it as lightweight as possible by simplifying the process logic.
Finally, in the main()
method, create an instance of the action
client node class that converts the parameters of the command line into
the target number sequence that needs to be summed. Call the
send_goal()
method of the action client node class instance and
transfer the target sum number sequence to initiate the sum request.
Similarly, add main()
as an entry point by adding the following line
in the appropriate position in setup.py
:
'my_sum_action_client = my_hello_world.my_sum_action_client:main'
Now that the action server and action client are ready, you can rebuild
the package my_hello_world
in the workspace root directory.
cd openmlsys-ros2
colcon build --packages-select my_hello_world --symlink-install
Then, run the following commands in the respective terminal windows:
# in terminal 1
ros2 run my_hello_world my_sum_action_client 1 2 3
# in terminal 2
ros2 run my_hello_world my_sum_action_server
Information similar to the following will be shown if all goes well:
# in terminal 1
[INFO] [1653561740.000499500] [my_sum_action_client]: Goal accepted.
[INFO] [1653561740.001171900] [my_sum_action_client]: Received feedback: 1
[INFO] [1653561740.001644000] [my_sum_action_client]: Received feedback: 3
[INFO] [1653561740.002327500] [my_sum_action_client]: Received feedback: 6
[INFO] [1653561740.002761600] [my_sum_action_client]: Result: 6
# in terminal 2
[INFO] [1653561739.988907200] [my_sum_action_server]: Executing goal...
[INFO] [1653561739.989213900] [my_sum_action_server]: Feedback: 1
[INFO] [1653561739.989549000] [my_sum_action_server]: Feedback: 3
[INFO] [1653561739.989855400] [my_sum_action_server]: Feedback: 6
Congratulations, you have now created the custom interface type, action server node, and action client node in the ROS 2 framework!