ROS 2 Notes
Contents
ROS 2 Basics
This chapter is adapted from the ROS 2 Tutorials: CLI tools.
Notes:
- Install ROS 2 here. The distribution used in this note is “ROS 2 Kilted Kaiju” (Optimized for Ubuntu 24.04).
- ROS 2 is installed in a protected system directory. To make ROS commands available in the terminal, source the setup files in every new terminal window:
source /opt/ros/kilted/setup.bash # for ROS 2 Kilted on UbuntuOr append it to the shell’s startup script (
~/.bashrc).
Nodes
Each node in ROS should be responsible for a single, modular purpose, e.g. a sensor node that reads data from a laser scanner, or a motor node that sends commands to the wheel motor. Each node can send and receive data from other nodes via topics, services, actions, or parameters. A full robotics system is comprised of many nodes working in concert.
Create a TurtleSim node
The command ros2 run launches an executable from a package:
ros2 run <package_name> <executable_name>
For example, to run turtlesim:
ros2 run turtlesim turtlesim_nodeThis creates a node called /turtlesim.
List nodes
ros2 node list will show the names of all running nodes. This is useful when a system is running many nodes and needs to keep track of them.
Open a new terminal while TurtleSim is still running and enter:
ros2 node list The terminal will output:
/turtlesim
Remapping nodes
Remapping allows us to reassign default node properties, like node name, topic names, services names, etc., to custom values. For example, in the third terminal, launch another turtlesim_node, but reassign its name to differ from the node launched in the first terminal:
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle
Run ros2 node list in the second terminal again, there should be two different node names:
/my_tirtle
/turtlesim
Access node info
To access more information about the nodes, run:
ros2 node info <node_name>
ros2 node info returns a list of subscribers, publishers, services, and actions. i.e., the ROS graph connections that interact with that note:
$ ros2 node info /turtlesim
/turtlesim
Subscribers:
...
Publishers:
...
Service Servers:
...
Service Clients:
...
Action Servers:
...
Action Clients:
...
Topics
Topics are a vital element of the ROS graph that act as a bus for nodes to exchange messages. A node may publish data to any number of topics and simultaneously have subscriptions to them.
Setup two TurtleSim
nodes
Open a terminal and run:
ros2 run turtlesim turtlesim_nodeOpen another terminal and run:
ros2 run turtlesim turtle_teleop_keyThis creates two nodes named /turtlesim (the simulator) and /teleop_turtle (the keyboard controller), respectively.
Visualize ROS graph
We can visualize active nodes and topics using rqt_graph. Open a new terminal and enter:
ros2 run rqt_graph rqt_graph
With the nodes /turtlesim and /teleop_turtle running, we should see:
The graph is depicting how the /turtlesim node and the /teleop_turtle node are communicating with each other over topics. For example, the /teleop_turtle node is publishing data (the keystrokes that command the turtle to move around) to the /turtle1/cmd_vel topic, and the /turtlesim node is subscribed to that topic to receive the data.
List topics
Running the ros2 topic list command in a new terminal will return a list of all the topics currently active in the system:
$ ros2 topic list
/parameter_events
/rosout
/turtle1/cmd_vel
/turtle1/color_sensor
/turtle1/pose
ros2 topic list -t will return the same list of topics, this time with the topic type appended in brackets:
$ ros2 topic list -t
/parameter_events [rcl_interfaces/msg/ParameterEvent]
/rosout [rcl_interfaces/msg/Log]
/turtle1/cmd_vel [geometry_msgs/msg/Twist]
/turtle1/color_sensor [turtlesim_msgs/msg/Color]
/turtle1/pose [turtlesim_msgs/msg/Pose]
These attributes, particularly the type, are how nodes know they’re talking about the same information as it moves over topics.
Echo topic
To see the data being published on a topic, use:
ros2 topic echo <topic_name>
As discussed earlier, /teleop_turtle publishes data to /turtlesim over the /turtle1/cmd_vel topic. Use echo to introspect that topic:
ros2 topic echo /turtle1/cmd_vel
In the terminal where turtle_teleop_key is running, press ↑ to move the turtle forward. The echo terminal will now publish the position command given to the turtle:
linear:
x: 2.0
y: 0.0
z: 0.0
angular:
x: 0.0
y: 0.0
z: 0.0
Access topic info
Topics don’t have to be one-to-one communication; they can be one-to-many, many-to-one, or many-to-many. To see info on a specific topic, run:
ros2 topic info <topic_name>
For example, information on the /turtle1/cmd_vel topic:
$ ros2 topic info /turtle1/cmd_vel
Type: geometry_msgs/msg/Twist
Publisher count: 1
Subscription count: 2
As discussed before, the one publisher on the /turtle1/cmd_vel topic is the /teleop_turtlenode, which publishes the position commands based on the keystrokes. The two subscribers are the /turtlesim node, which controls the turtle, and the echo node that listens to the commands and outputs them onto its terminal.
For more detailed information about a topic, use the --verbose (or -v) flag. This will return additional details including node name and namespaces of publishers and subscribers, topic type, and QoS profiles.
Topic type
Nodes send data over topics using messages. Publishers and subscribers must send and receive the same type of message to communicate. Earlier, running ros2 topic list -t shows what message type is used for each topic. To list the message type for a specific topic, use ros2 topic type:
$ ros2 topic type /turtle1/cmd_vel
geometry_msgs/msg/Twist
This tells us that the /turtle1/cmd_vel topic communicates with geometry_msgs/msg/Twist type messages. To learn the structure of this message type, use the ros2 interface show command:
$ ros2 interface show geometry_msgs/msg/Twist
# This expresses velocity in free space broken into its linear and angular parts.
Vector3 linear
float64 x
float64 y
float64 z
Vector3 angular
float64 x
float64 y
float64 z
Publish message onto topic
Knowing the message structure allows manually publishing data to a topic from the command line using:
ros2 topic pub <topic_name> <msg_type> '<args>'The 'args' argument is the actual data passed to the topic. There are four main ways to use the pub command:
- Publishing dictionary strings: Pass the data in the form of YAML strings:
ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}"or
ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0}, angular: {z: 1.8}}"The values that are left out will be set as its default value for the message type.
- Publishing an empty message:
ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/TwistThis will publish the default values for the message type. In this case, this is equivalent to
ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}" --rate 1
For the autocomplete feature in c. and d., see this link.
The turtle (and commonly the real robots which it is meant to emulate) require a steady stream of commands to operate continuously. So by default, pub publish the same data repeatedly at 1 Hz. To publish data to the topic only once, add the --once option:
ros2 topic pub --once -w 2 /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}"-w 2 is an optional argument meaning “wait until there are two active subscribers on the topic”. This is sometimes necessary as in ROS 2, after a publisher is initialized, it takes a few milliseconds (or seconds) for the “Handshake” to happen between the publisher and the subscribers.
With the --once tag, the turtle will stop before finishing a whole circle.
Topic bandwidth
The bandwidth currently used by a topic can be viewed using:
ros2 topic bw <topic_name>
For example, bandwidth of the /turtle1/pose topic:
1 Check your setup
2 Check a system
1 Check your setup
2 Check a system
$ ros2 topic bw /turtle1/pose
Subscribed to [/turtle1/pose]
1.52 KB/s from 63 messages
Message size mean: 0.02 KB min: 0.02 KB max: 0.02 KB
The bandwidth reflects the receiving rate on the subscription, which may be affected by the platform resources and QoS configuration, and may not exactly match the publisher’s bandwidth.
Find topic using type
To list all available topics using a given message type, use:
ros2 topic find <topic_type>
For example, turtle1/cmd_vel uses message type geometry_msgs/msg/Twist. Find all topics that uses this message type:
$ ros2 topic find geometry_msgs/msg/Twist
/turtle1/cmd_vel
Services
Services are another method of communication for nodes in the ROS graph. While topics allow nodes to subscribe to data streams and get continual updates, services only provide data when they are specifically called by a client.
Similar to the “Topics” section, begin by launching the two TurtleSim nodes, /turtlesim (the simulator) and /teleop_turtle (the keyboard controller). Open a new terminal and run:
ros2 run turtlesim turtlesim_nodeOpen another terminal and run:
ros2 run turtlesim turtle_teleop_key
List services
Running the ros2 service list command in a new terminal will return a list of all the services currently active in the system:
$ ros2 service list
/clear
/kill
/reset
/spawn
...
Service type
Services have types that describe how the request and response data of a service is structured. Service types are defined similarly to topic types, except service types have two parts: one message for the request and another for the response.
To find out the type of a service, use:
ros2 service type <service_name>
For example, here is the type for the /clear service:
$ ros2 service type /clear
std_srvs/srv/EmptyTo see the types for all the active services at the same time, we can append the --show-types (or -t) flag, to the list command:
$ ros2 service list -t
/clear [std_srvs/srv/Empty]
/kill [turtlesim_msgs/srv/Kill]
/reset [std_srvs/srv/Empty]
/spawn [turtlesim_msgs/srv/Spawn]
...
Access service info
To see information of a particular service, use:
ros2 service info <service_name>
For example, we can find the count of clients and services for the /clear service:
$ ros2 service info /clear
Type: std_srvs/srv/Empty
Clients count: 0
Services count: 1
Find service using type
If we want to find all the services of a specific type, use the command:
ros2 service find <type_name>
For example, we can find all the Empty typed services like this:
$ ros2 service find std_srvs/srv/Empty
/clear
/reset
Service structure
We can call service from the command line, but first we need to know the structure of the input argument. To see the detailed structure of a service’s type, use:
ros2 interface show <type_name>
Let’s introspect the request and response arguments of the /spawn service. From the results of ros2 service list -t, we know /spawn's type is turtlesim_msgs/srv/Spawn. Run the command:
$ ros2 interface show turtlesim_msgs/srv/Spawn
float32 x
float32 y
float32 theta
string name # Optional. A unique name will be created and returned if this is empty
---
string nameThe --- separates the request structure (above) from the response structure (below). In this case, the information above the --- line tells us the arguments needed to call /spawn. x, y, and theta determine the 2D pose of the spawned turtle, and name is optional. The information below the line can help us understand the data type of the response we get from the call.
Some services do not require data to be exchanged. As an example, here is the structure of an Empty type:
$ ros2 interface show std_srvs/srv/Empty
---The Empty type means the client sends no data when making a service request and receives no data when receiving a response. The “request” is simply the act of the client calling for service. When the server receives the call, it executes a function and sends back an empty response. This response confirms to the client that the function is executed.
Call for service
We can call a service using:
ros2 service call <service_name> <service_type> <arguments>The <argument> part is optional. For example, since we know that Empty typed services don’t have any arguments:
$ ros2 service call /clear std_srvs/srv/Empty
waiting for service to become available...
requester: making request: std_srvs.srv.Empty_Request()
response:
std_srvs.srv.Empty_Response()This command will clear the TurtleSim window of any lines the turtle has drawn.
Now let’s spawn a new turtle by calling /spawn and setting arguments. Input <argument> in a service call from the command line need to be YAML syntax:
$ ros2 service call /spawn turtlesim_msgs/srv/Spawn "{x: 2, y: 2, theta: 0.2, name: ''}"
requester: making request: turtlesim_msgs.srv.Spawn_Request(x=2.0, y=2.0, theta=0.2, name='')
response:
turtlesim_msgs.srv.Spawn_Response(name='turtle2')The TurtleSim window will update with the newly spawned turtle.
Echo service
To see the data communication between a service client and a service server, we can echo the service using:
ros2 service echo <service_name | service_type> <arguments>ros2 service echo depends on service introspection of a service client and server, that is disabled by default. To enable it, users must call configure_introspection after creating a service client or server. First, start up the introspection_client and introspection_service service introspection demo:
ros2 launch demo_nodes_cpp introspect_services_launch.pyOpen another terminal and run the following to enable service introspection for introspection_client and introspection_service:
ros2 param set /introspection_service service_configure_introspection contents
ros2 param set /introspection_client client_configure_introspection contentsSee the “Change parameter” section for how to set a node’s parameter using ros2 param set. Now we are able to see the service communication between introspection_client and introspection_service via ros2 service echo:
ros2 service echo --flow-style /add_two_ints
1 Check your setup
2 Check a system
Parameters
A parameter is a configuration value of a node. A node can store parameters as integers, floats, booleans, strings, and lists. In ROS 2, each node maintains its own parameters. For more background on parameters, see the concept document.
Similar to the previous sections, begin by launching the two TurtleSim nodes, /turtlesim (the simulator) and /teleop_turtle (the keyboard controller). Open a new terminal and run:
ros2 run turtlesim turtlesim_nodeOpen another terminal and run:
ros2 run turtlesim turtle_teleop_key
List parameters
To see the parameters belonging to the nodes, open a new terminal and use the ros2 param list command:
$ ros2 param list
/teleop_turtle:
qos_overrides./parameter_events.publisher.depth
qos_overrides./parameter_events.publisher.durability
qos_overrides./parameter_events.publisher.history
qos_overrides./parameter_events.publisher.reliability
scale_angular
scale_linear
start_type_description_service
use_sim_time
/turtlesim:
background_b
background_g
background_r
holonomic
qos_overrides./parameter_events.publisher.depth
qos_overrides./parameter_events.publisher.durability
qos_overrides./parameter_events.publisher.history
qos_overrides./parameter_events.publisher.reliability
start_type_description_service
use_sim_timeThis shows the node namespaces, /teleop_turtle and /turtlesim, followed by each node’s parameters.
The namespaces of the parameters and its name are separated using dots. Every node has the parameter use_sim_time; it’s not unique to turtlesim.
Parameter type
To display the type and current value of a parameter, use the command:
ros2 param get <node_name> <parameter_name>
Let’s find out the current value of /turtlesim's parameter background_g:
$ ros2 param get /turtlesim background_g
Integer value is: 86Now we know background_g holds an integer value.
Change parameter
To change a parameter’s value at runtime, use the command:
ros2 param set <node_name> <parameter_name> <value>
Let’s change /turtlesim's background color:
$ ros2 param set /turtlesim background_r 150
Set parameter successfulThe background of the TurtleSim window should change to purple.
Setting parameters with the set command will only change them in the current session, not permanently. However, the following sections show how we can save our settings and reload them the next time we start a node.
View parameters
We can view all of a node’s current parameter values by using the command:
ros2 param dump <node_name>
This command prints to the standard output (stdout) by default but we can also direct the parameter values into a file to save them for later. To save our current configuration of turtlesim's parameter into the file turtlesim.yaml, use:
ros2 param dump /turtlesim > turtlesim.yaml/turtlesim:
ros__parameters:
background_b: 255
background_g: 86
background_r: 150
holonomic: false
qos_overrides:
/parameter_events:
publisher:
depth: 1000
durability: volatile
history: keep_last
reliability: reliable
start_type_description_service: true
use_sim_time: false
Dumping parameters comes in handy if we want to reload the node with the same parameters in the future.
Load parameters
We can load parameters from a file to a currently running node using the command:
ros2 param load <node_name> <parameter_file>
To load the turtlesim.yaml file generated with ros2 param dump into /turtlesim node’s parameters, enter the command:
$ ros2 param load /turtlesim turtlesim.yaml
Set parameter background_b successful
Set parameter background_g successful
Set parameter background_r successful
Set parameter holonomic successful
Set parameter qos_overrides./parameter_events.publisher.depth failed: parameter 'qos_overrides./parameter_events.publisher.depth' cannot be set because it is read-only
Set parameter qos_overrides./parameter_events.publisher.durability failed: parameter 'qos_overrides./parameter_events.publisher.durability' cannot be set because it is read-only
Set parameter qos_overrides./parameter_events.publisher.history failed: parameter 'qos_overrides./parameter_events.publisher.history' cannot be set because it is read-only
Set parameter qos_overrides./parameter_events.publisher.reliability failed: parameter 'qos_overrides./parameter_events.publisher.reliability' cannot be set because it is read-only
Set parameter start_type_description_service failed: parameter 'start_type_description_service' cannot be set because it is read-only
Set parameter use_sim_time successful
Read-only parameters can only by modified at startup and not afterwards. To start a node using our saved parameter values, use:
ros2 run <package_name> <executable_name> --ros-args --params-file <file_name>This is the same command to start a node, with the added flags --ros-args and --params-file, followed by the file we want to load. The --ros-args flag indicates that the arguments behind the flag are for the ROS middleware, not for the node’s internal logic.
Stop the running TurtleSim node, and try reloading it with the saved parameters, using:
ros2 run turtlesim turtlesim_node --ros-args --params-file turtlesim.yamlThe TurtleSim window should appear as usual, but with the purple background we set earlier.
Actions
Actions are one of the communication types in ROS 2 and are intended for long running tasks. They consists or three parts: a goal, feedback, and a result.
Actions are built on topics and services. Their functionality is similar to services, except actions can be canceled. They also provide steady feedback, as opposed to services which return a single response.
Actions use a client-server model, similar to the publisher-subscriber model described in the topics section. An “action client” node sends a goal to an “action server” node that acknowledges the goal and returns a stream of feedback and a result.
Similar to the previous sections, begin by launching the two TurtleSim nodes, /turtlesim (the simulator) and /teleop_turtle (the keyboard controller). Open a new terminal and run:
ros2 run turtlesim turtlesim_nodeOpen another terminal and run:
ros2 run turtlesim turtle_teleop_key
An action example
When we launch the /teleop_turtle node, we should see the following message in the terminal:
Use g|b|v|c|d|e|r|t keys to rotate to absolute orientations. 'f' to cancel a rotation.
'q' to quit.This is an example of an action client (part of the /teleop_turtle node) and an action server (part of the /turtlesim node). Each time we pressed one of these keys, the /teleop_turtle node sending a goal to the /turtlesim node, which is to rotate the turtle to face a particular direction. A message relaying the result of the goal should display on the server side once the turtle completes its mission:
[INFO] [1768967021.807981421] [turtlesim]: Rotation goal completed successfully
Both the client and the server can stop a goal mid-execution. Try pressing the C key, and then the F key on the client-side before the turtle can complete its rotation. In the /turtlesim terminal we should see the message:
[INFO] [1768967157.520063245] [turtlesim]: Rotation goal canceledBy hitting the F key, the action client sends a signal to the server that cancels the goal.
Now try hitting the D key, then the G key before the first rotation can complete. In the /turtlesim terminal we should see the message:
[WARN] [1768967386.128088538] [turtlesim]: Rotation goal received before a previous goal finished. Aborting previous goalThe action server chose to abort the first goal because it got a new one.
List actions
To see the list of actions a node provides, /turtlesim in this case, open a new terminal and use the ros2 node info command, as previously introduced here:
$ ros2 node info /turtlesim
/turtlesim
Subscribers:
...
Publishers:
...
Service Servers:
...
Service Clients:
Action Servers:
/turtle1/rotate_absolute: turtlesim_msgs/action/RotateAbsolute
Action Clients:The command returns a list /turtlesim's subscribers, publishers, services, action servers and action clients. Notice that the /turtle1/rotate_absolute action for /turtlesim is under Action Servers. This means /turtlesim responds to and provides feedback for the /turtle1/rotate_absolute action.
The /teleop_turtle node, on the other hand, has the name /turtle1/rotate_absolute under Action Clients. This means that /teleop_turtle sends goals for that action name:
$ ros2 node info /teleop_turtle
/teleop_turtle
Subscribers:
Publishers:
...
Service Servers:
...
Service Clients:
Action Servers:
Action Clients:
/turtle1/rotate_absolute: turtlesim_msgs/action/RotateAbsolute
To identify all the actions in the ROS graph, run the command:
$ ros2 action list
/turtle1/rotate_absoluteThis is the only action in the ROS graph right now. From earlier we know that there is one action client (part of /teleop_turtle) and one action server (part of /turtlesim). We can confirm this using the ros2 action info command:
$ ros2 action info /turtle1/rotate_absolute
Action: /turtle1/rotate_absolute
Action clients: 1
/teleop_turtle
Action servers: 1
/turtlesim
Action type
Actions have types, similar to topics and services. To find /turtle1/rotate_absolute's type, add the --show-types (or -t) flag to the ros2 action list command:
$ ros2 action list -t
/turtle1/rotate_absolute [turtlesim_msgs/action/RotateAbsolute]
We can also use ros2 action type command to check the action type for a specific action:
$ ros2 action type /turtle1/rotate_absolute
turtlesim_msgs/action/RotateAbsolute
To send or execute an action goal, we need to know the structure of the action type. Similar to topic type and service type, we use the ros2 interface show command to see the specific structure of an action type:
$ ros2 interface show turtlesim_msgs/action/RotateAbsolute
# The desired heading in radians
float32 theta
---
# The angular displacement in radians to the starting position
float32 delta
---
# The remaining rotation in radians
float32 remainingThe section of this message above the first --- is the structure (data type and name) of the goal request. The next section is the structure of the result. The last section is the structure of the feedback.
Send goal
To send an action goal from the command line, we follow this syntax:
ros2 action send_goal <action_name> <action_type> <values><values> need to be in YAML format.
Enter the following command to make the turtle face the top:
$ ros2 action send_goal /turtle1/rotate_absolute turtlesim_msgs/action/RotateAbsolute "{theta: 1.57}"
Waiting for an action server to become available...
Sending goal:
theta: 1.57
Goal accepted with ID: 2104a128edce4d1cb2b9c7a236fdfdc6
Result:
delta: -1.5679999589920044
Goal finished with status: SUCCEEDEDAll goals have a unique ID, shown in the return message. We can also see the result, in this case, a field with the name delta, which is the displacement to the starting position.
To see the feedback of this goal, add --feedback to the ros2 action send_goal command:
$ ros2 action send_goal /turtle1/rotate_absolute turtlesim_msgs/action/RotateAbsolute "{theta: 1.57}" --feedback
Waiting for an action server to become available...
Sending goal:
theta: 1.57
Feedback:
remaining: 1.5540000200271606
Goal accepted with ID: 2127d196ee4a40d68678dc1337c550da
Feedback:
remaining: 1.5380001068115234
....
Result:
delta: -1.5360000133514404
Goal finished with status: SUCCEEDEDThe client will continue to receive feedback, the remaining radians, until the goal is complete.
Echo action
To see the data communication between an action client and an action server, we can echo the action data using:
ros2 action echo <action_name> <optional_arguments | action_type>Similar to ros2 service echo, ros2 action echo depends on action introspection of an action client and server, which is disabled by default. To enable it, users must call configure_introspection after creating an action client or server.
Start up the fibonacci_action_server and fibonacci_action_client, enabling action_server_configure_introspection parameter for demonstration:
ros2 run action_tutorials_cpp fibonacci_action_server --ros-args -p action_server_configure_introspection:=contentsros2 run action_tutorials_py fibonacci_action_client --ros-args -p action_client_configure_introspection:=contents
Now we are able to see the action communication between fibonacci_action_server and fibonacci_action_client:
$ ros2 action echo /fibonacci example_interfaces/action/Fibonacci --flow-style
interface: STATUS_TOPIC
status_list: [{goal_info: {goal_id: {uuid: [136, 139, 22, 141, 35, 188, 77, 23, 181, 17, 109, 104, 219, 41, 133, 158]}, stamp: {sec: 1768971634, nanosec: 489750450}}, status: 2}]
---
interface: FEEDBACK_TOPIC
goal_id:
uuid: [136, 139, 22, 141, 35, 188, 77, 23, 181, 17, 109, 104, 219, 41, 133, 158]
feedback:
sequence: [0, 1, 1, 2, 3, 5, 8, 13, 21]
---
interface: FEEDBACK_TOPIC
goal_id:
uuid: [136, 139, 22, 141, 35, 188, 77, 23, 181, 17, 109, 104, 219, 41, 133, 158]
feedback:
sequence: [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
---
...
Client Libraries
This chapter is adapted from the ROS 2 Tutorials: Client libraries.
Notes:
- This chapter uses Python whenever available. For instructions in C++, visit the original tutorial.
- Source the ROS setup files in every new terminal window or in
~/.bashrc:source /opt/ros/kilted/setup.bash # for ROS 2 Kilted on Ubuntu
Workspace
Overview
A ROS workspace is a structured directory used to develop, manage, and build custom packages. The standard layout consists of four main directories:
- The
scr/directory is where we place our custom packages. This is the only directory we manually create and edit.
- The
install/directory is where the final, executable versions of our packages are built. ROS 2 looks here when runningros2 runorros2 launch.
- The
build/directory stores intermediate files (e.g.,CMakecache). Each package gets its own subfolder here.
- The
log/directory contains various logging information for everycolconcommand execution.
Create a workspace
To create a ROS workspace and clone the ros_tutorials repository into the src/ directory, use:
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
git clone https://github.com/ros/ros_tutorials.git -b kilted
In the src/ directory, we will now see two folders titled turtlesim and turtlesim_msgs. This is the two packages we used in the previous ROS 2 Basics chapter.
Resolve dependencies
Before building the workspace, we need to resolve the package dependencies. We may have all the dependencies already, but the best practice is the check for dependencies every time we build.
Navigate to the root of the workspace and resolve the dependencies:
cd ..
rosdep install -i --from-path src --rosdistro kilted -y
If this is the first time using rosdep, the package will prompt us to rosdep init and rosdep update first.
Build packages using colcon
colcon is an iteration of the ROS build tools. Use colcon build to build packages in the src/ directory into executable versions in the install/ directory:
colcon build
colcon build will create the install/, build/ and log/ directories discussed here.
In standard builds, colcon copies files from src/ to install/. For Python developers, a more efficient way is to use the --symlink-install flag to create a symbolic link instead of copies, so that when modifying a Python script or YAML config in the src/ folder, the changes are immediate active in the install/ folder, without having to re-run colcon build every time a line of code is changed.
To run tests for the packages we just built, use:
colcon test
Source the overlay
Before we can use any of the newly built executables or libraries, we will need to add them to our path in every new terminal. Use:
source install/setup.bashThis will source both the overlay packages in the install/ directory as well as the underlay it was created in (the global packages). This is equivalent as using:
source /opt/ros/kilted/setup.bash
source install/local_setup.bash
Modify the overlay
The overlay of the ros2_ws environment takes precedence over the contents of the underlay. We can modify the TurtleSim package in our overlay by editing the title bar on the TurtleSim window. To do this, locate the turtle_frame.cpp file in ~/ros2_ws/src/ros_tutorials/turtlesim/src, find the function setWindowTitle("TurtleSim");, change the value "TurtleSim" to "MyTurtleSim", and save the file. Build the package using colcon build again, and on a terminal where the ros2_ws environment is sourced, run:
ros2 run turtlesim turtlesim_nodeThe title bar on the TurtleSim window is now changed to “MyTurtleSim”.
To see that the underlay is still intact, open a brand new terminal and source only the ROS 2 installation. Run TurtleSim again. The title bar on the TurtleSim window is still “TurtleSim”. The modifications in the overlay did not actually affect anything in the underlay.
Package
Overview
A package is an organizational unit for ROS 2 code. If we want to install our code or share it with others, we’ll need to organize it into a package. A ROS 2 Python package requires the following contents:
package.xmlfile containing the meta information about the package.
resource/<package_name>marker file for the package.
setup.cfgis required when a package has executables, soros2 runcan find them.
setup.pycontaining instructions for how to install the package.
<package_name>dictionary with the same name as the package, used by ROS 2 tools to find the package, contains__init__.pyand the executable functions, if any.
Create a package
We will continue to use the workspace created in the last section, ros2_ws, for our new package. Packets are created in the \src folder, navigate to that folder:
cd ~/ros2_ws/src
The command syntax for creating a new Python package in ROS 2 is:
ros2 pkg create --build-type ament_python <package_name>Optional arguments include --node-name and --license. --node-name creates a simple executable in the package that contains a basic “Hello World” ROS 2 node template. --license declares the license information for the package.
For example, to create the package my_package, run:
ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name my_node my_packageThe package files will generated under folder src/my_package/.
Build the package
To build only the my_package package, return to the root of the workspace, and colcon build using the --package select flag:
cd ~/ros2_ws
colcon build --packages-select my_package
Use the package
Before using the package, source the local setup file:
source install/local_setup.bash
Run the my_node executable using ros2 run:
ros2 run my_package my_nodeThe executable will return a message to the terminal:
Hi from my_package.
Write Publisher & Subscriber using Python
In this section, we will create nodes that pass information in the form of string messages to each other over a topic. The example here is a simple “talker” and “listener” system; one node publishes data and the other subscribes to the topic so it can receive that data.
First, we will create a Python package called py_pubsub. Run:
ros2 pkg create --build-type ament_python --license Apache-2.0 py_pubsub
Write the publisher node
Navigate to ros2_ws/src/py_pubsub/py_pubsub, the dictionary where the executables usually live. Download the example talker code by:
wget https://raw.githubusercontent.com/ros2/examples/kilted/rclpy/topics/minimal_publisher/examples_rclpy_minimal_publisher/publisher_member_function.py
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from std_msgs.msg import String
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%s"' % msg.data)
self.i += 1
def main(args=None):
try:
with rclpy.init(args=args):
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
- The
Nodeclass provides all the tools our custom class needs to register itself as a node in the ROS network.Node.__init__(node_name: str)creates a node namednode_name.
- The
Node.create_publisher(msg_type: Type, topic: str, qos_profile: QoSProfile | int)function creates a publisher that publishes messages of typemsg_type, over a topic namedtopic. Queue size (defined by theqos_profileargument) is a required QoS (quality of service) setting that limits the amount of queued messages if a subscriber is not receiving them fast enough. The function returns the publisher and stores it in a callback group.
- The
Node.create_timer(time_period_sec: float, callback: func)function creates a timer (Timertype) that runs thecallbackfunction everytime_period_secseconds. The function returns the timer and stores it in a callback group.
- The
Publisher.publish(msg: msgT)function publishesmsg.
- The
Node.get_logger().info(message: str)function publishesmessageonto the terminal.
- The
rclpy.spin(node: Node)function by default creates aSingleThreadedExecutorand adds thenodeto it. The executor initiates a blocking infinite loop on the main thread, and utilizes a wait set to remain in a highly efficient sleep state until the operating system signals a trigger, such as a timer reaching its period or a message arriving for a subscription. Once awakened, the executor identifies the specific item within its callback groups and dispatches the corresponding callback function, executing the Python logic on the main thread before returning to its idle state to wait for the next event.
Write the subscriber node
At ros2_ws/src/py_pubsub/py_pubsub download the example listener code by:
wget https://raw.githubusercontent.com/ros2/examples/kilted/rclpy/topics/minimal_subscriber/examples_rclpy_minimal_subscriber/subscriber_member_function.py
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10)
self.subscription # prevent unused variable warning
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
def main(args=None):
try:
with rclpy.init(args=args):
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
- See definitions of class
Node, functionsNode.get_logger().infoandrclpy.spinat the publisher node.
- The
Node.create_subscription(msg_type: Type, topic: str, callback: func, qos_profile: QoSProfile | int)function creates a subscription that receives messages of typemsg_type, over a topic namedtopic. When a message is received on the topic, the subscription will call thecallbackfunction and provide the message as an argument. For a subscriber, queue size (defined by theqos_profileargument) limits the amount of queued messages if the subscriber is not processing them fast enough.
Add dependencies
Open py_pubsub/package.xml. Update the <description>, <maintainer> and <license> tags. Right above the </package> flag, add the dependencies corresponding to the node’s import statement:
<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>This declares the package needs <rclpy> and <std_msgs> when its code is executed. As mentioned here, rosdep will install these dependencies if they are not already installed in the system.
Add entry points
Open py_pubsub/setup.py. Match the maintainer, maintainer_email, description and license fields to the package.xml file. Edit the entry_points block with:
entry_points={
'console_scripts': [
'talker = py_pubsub.publisher_member_function:main',
'listener = py_pubsub.subscriber_member_function:main',
],
},
The entry_points block tells ROS 2 how to map a terminal command to the Python code. By defining a console_script, colcon generates a system executable that points to the specific function. After build, we can launch the main() functions of the publisher and subscriber using:
ros2 run py_pubsub talker
ros2 run py_pubsub listener
Build and run
It’s good practice to run rosdep in the root of our workspace (ros2_ws) to check for missing dependencies before building:
rosdep install -i --from-path src --rosdistro kilted -y
Still in the root of the ros2_ws workspace, build the new package py_pubsub with:
colcon build --packages-select py_pubsub
Source the setup files:
source install/setup.bash
Run the talker node. The terminal should start publishing info messages every 0.5 seconds:
$ ros2 run py_pubsub talker
[INFO] [1767795374.253063135] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [1767795374.744554532] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [1767795375.244599482] [minimal_publisher]: Publishing: "Hello World: 2"
...
In another terminal, source the setup files, and run the listener node. The listener will start printing messages to the console, starting at whatever message count the publisher is on at that time:
$ source install/setup.bash
$ ros2 run py_pubsub listener
[INFO] [1767795506.331003064] [minimal_subscriber]: I heard: "Hello World: 19"
[INFO] [1767795506.820101615] [minimal_subscriber]: I heard: "Hello World: 20"
[INFO] [1767795507.319984207] [minimal_subscriber]: I heard: "Hello World: 21"
...
Write Service & Client using Python
When nodes communicate using services, the node that sends a request for data is called the client node, and the one that responds to the request is the service node. The structure of the request and response is determined by a .srv file.
The example used here is a simple integer addition system; one node requests the sum of two integers, and the other responds with the result. To create the package:
ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfacesThe --dependecies argument will automatically add the necessary dependency lines to package.xml.
Write a service node
Inside the ros2_ws/src/py_srvcli/py_srvcli dictionary, create a new file called service_member_function.py and paste the following code within:
from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
class MinimalService(Node):
def __init__(self):
super().__init__('minimal_service')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)
def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))
return response
def main():
try:
with rclpy.init():
minimal_service = MinimalService()
rclpy.spin(minimal_service)
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()
- See definitions of class
Node, functionsNode.get_logger().infoandrclpy.spinat the publisher node.AddTwoIntsis a service type from theexample_interfacespackage.
- The
Node.create_service(srv_type: Type, srv_name: str, callback: func, qos_profile: QoSProfile | int)function creates a service namedsrv_name, with typesrv_type. When a request is received, the subscription will call thecallbackfunction and provide the request as an argument. The callback function should return the response of the service.
Write the client node
Inside the ros2_ws/src/py_srvcli/py_srvcli directory, create a new file called client_member_function and paste the following code within:
from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = AddTwoInts.Request()
def send_request(self):
self.req.a = 41
self.req.b = 1
return self.cli.call_async(self.req)
def main(args=None):
try:
with rclpy.init(args=args):
minimal_client = MinimalClientAsync()
future = minimal_client.send_request()
rclpy.spin_until_future_complete(minimal_client, future)
response = future.result()
minimal_client.get_logger().info(
'Result of add_two_ints: for %d + %d = %d' %
(minimal_client.req.a, minimal_client.req.b, response.sum))
except (KeyboardInterrupt, ExternalShutdownException):
pass
if __name__ == '__main__':
main()- See definitions of class
Node, functionsNode.get_logger().infoandrclpy.spinat the publisher node.AddTwoIntsis a service type from theexample_interfacespackage.
Client.wait_for_service(timeout_sec: float | None)checks if a service matching the type and name is available everytimeout_secseconds.
futureis a variable of theFutureclass, which represents the outcome of a task in the future. When the service responses,future.done()will returnTrue. The response can be accessed byfuture.result().
rclpy.spin_until_future_complete(node: Node, future: Future)will spin the node (similar torclpy.spin()) until the future is completed (future.done()returnsTrue). Do not use this function in a ROS 2 callback (read this article for details).
Add entry points
The entry_points field of the setup.py file should look like:
entry_points={
'console_scripts': [
'service = py_srvcli.service_member_function:main',
'client = py_srvcli.client_member_function:main',
],
},
Build and run
In the root of our workspace (ros2_ws), check for missing dependencies before building:
rosdep install -i --from-path src --rosdistro kilted -y
Build the new package:
colcon build --packages-select py_srvcli
Open a new terminal, navigate to ros2_ws, and source the setup files:
source install/setup.bash
Run the service node:
ros2 run py_srvcli serviceThe node will wait for the client’s request.
Open another terminal and source the setup files from inside ros2_ws again. Start the client node. The clients sends the request to the service, which computes the sum and returns the result:
$ ros2 run py_srvcli client
[INFO] [1770641867.895159842] [minimal_client_async]: Result of add_two_ints: for 41 + 1 = 42Launch
Launch files written in Python can start or stop different nodes as well as trigger and act on various events. The package providing this framework is launch_ros, which uses the non-ROS-specific launch framework underneath.
Write a launch file for nodes
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='turtlesim',
namespace='turtlesim1',
executable='turtlesim_node',
name='sim',
arguments=['--ros-args', '--log-level', 'info']
),
Node(
package='turtlesim',
namespace='turtlesim2',
executable='turtlesim_node',
name='sim',
ros_arguments=['--log-level', 'warn']
),
Node(
package='turtlesim',
executable='mimic',
name='mimic',
remappings=[
('/input/pose', '/turtlesim1/turtle1/pose'),
('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'),
]
)
])
- Each launch file must contain a
generate_launch_description()function that returns aLaunchDescriptionobject. ROS 2 executes this function and launches every action (e.g. nodes or processes) contained within that returned object.
- The
Nodeclass typically represents a ROS 2 executable that contains one node:- The
packageandexecutableparameters identify which program to run, whileargumentsprovides raw command-line strings. This is functionally identical to typingros2 run <package> <executable> <arguments>in the terminal.
- The
namespaceandnameparameters defines how the node is identified in the ROS graph. For example, a node assignednamespace='turtlesim1'andname='sim'will be globally identified asturtlesim1/sim, overriding the default node name (e.g.,turtlesim) hardcoded in the package.
- The
--ros-argsflag indicates that the arguments behind the flag are for the ROS middleware, not for the node’s internal logic. The--log-levelsets the severity threshold for terminal output:infoprints all standard status updates, whilewarnsuppresses everything except essential warnings and errors. The--ros-argsflag can be dropped if using theros_argumentsparameter instead of theargumentparameter.
- The
remappingstag can change topics, services and name a node is assigned to (see remapping nodes section). Here, the code redirects the topics of themimicnode: instead of subscribing to/input/pose,mimicwill now listen to the poses published onturtlesim1/turtle1/pose; instead of publishing those poses on/output/cmd_vel,mimicwill now publish them on/turtlesim2/turtle1/cmd_vel.
- The
To launch the file, enter the directory where turtlesim_mimic_launch.py is stored, and use:
ros2 launch turtlesim_mimic_launch.pyTwo TurtleSim windows will open.
To see the system in action, open a new terminal and run the ros2 topic pub command on the /turtlesim1/turtle1/cmd_vel to get the first turtle moving:
ros2 topic pub -r 1 /turtlesim1/turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: -1.8}}"Both turtles will follow the same path.
While the system is still running, open a new terminal and run rqt_graph to get a better idea of the relationship between the nodes in the launch file:
ros2 run rqt_graph rqt_graph
ROS 2 Gazebo Integration
This chapter is adapted from the Gazebo Docs:
Notes:
- Install a compatible Gazebo distribution and the Gazebo/ROS Pairing here. The distribution used in this note is “Gazebo Ionic”, compatible with “ROS 2 Kilted Kaiju”.
- Source the ROS setup files
Exchange message between ROS 2 and Gazebo
The ros_gz_bridge package provides a bidirectional network bridge that enables communication between ROS 2 and Gazebo. Since the two systems use different message definitions, the bridge performs real-time translation between Gazebo message types and ROS 2 types. See the supported message type mappings here.
Launching the bridge via terminal
We can launch a bidirectional bridge so we can have ROS as the publisher and Gazebo as the subscriber or vice versa. The general syntax is
ros2 run ros_gz_bridge parameter_bridge <gz_topic_name>@<ros_msg_type>@<gz_msg_type>
The ROS message type is followed by an @, [, or ] symbol where @ is a bidirectional bridge, [ is a bridge from Gazebo to ROS, and ] is a bridge from ROS to Gazebo.
For example, to access Gazebo keystrokes within ROS 2, first launch a simulation instance with gz sim. Within the GUI, activate the Key publisher plugin via the top-right ⋮ menu. Gazebo will now publish our keystrokes onto its /keyboard/keypresstopic. Then, verify the keystrokes message type by running gz topic -i -t /keyboard/keypress in the terminal, and identify a corresponding ROS type by referencing the ros_gz_bridge mapping. Finally, establish a bridge from Gazebo to ROS in the second terminal using:
ros2 run ros_gz_bridge parameter_bridge /keyboard/keypress@std_msgs/msg/Int32@gz.msgs.Int32
Verify the data stream in a third terminal by echoing the ROS 2 /keyboard/keypress topic:
ros2 topic echo /keyboard/keypress
Pressing keys while the Gazebo window is focused will now output the corresponding integer key codes to the ROS 2 terminal.
Launching the bridge using a config file
We can also launch the bridge using a YAML file for configuration:
- ros_topic_name: "/keyboard/keypress"
gz_topic_name: "/keyboard/keypress"
ros_type_name: "std_msgs/msg/Int32"
gz_type_name: "gz.msgs.Int32"
direction: GZ_TO_ROS # BIDIRECTIONAL or ROS_TO_GZ
# ...other bridge configs...
The following command will launch the same bridge that transfers keystrokes:
ros2 run ros_gz_bridge parameter_bridge --ros-args -p config_file:=bridge_config.yaml