In this tutorial, we’ll look at how to create and run Python scripts under ROS2. This will enable you to create your own nodes and start developing under ROS.
Create a workspace
A good practice when developing under ROS2 is to create workspaces in which to install the desired packages, separate from the main installation.
Colcon installation
sudo apt install python3-colcon-common-extensions
Create folder
mkdir -p ~/tuto_ws/src cd ~/tuto_ws
Then copy a sample project, such as tutorial containing turtlesim
git clone https://github.com/ros/ros_tutorials.git -b iron
Then build the project using the command
colcon build
To load your workspace into a new terminal
source /opt/ros/iron/setup.bash cd ~/ros2_ws source install/local_setup.bash
N.B.: you can put the following commands in a ros_profile for the source in a single “source ros_profile” command
Creating a python package
ROS2 offers a simple tool for creating the file architecture of a Python or C++ package. You must specify at least the package name (my_package). You can also specify the node name.
In the ~/ros2_sw/src directory, enter the following command
ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name my_node my_package --license Apache-2.0
the python script is now in the directory ~/ros2_ws/src/my_package/my_package
Once the package has been created, you can add as many nodes as you like to this folder.
You’ll need to update the setup.py and package.xml files with dependencies and entry points
Installing dependencies
Before compiling the project, it is advisable to resolve the dependencies.
rosdep install -i --from-path src --rosdistro iron -y
Compiling the project
It is possible to compile the entire project or to select a particular package.
colcon build --packages_select my_package --symlink-install
N.B.: symlink-install avoids recompiling every time you change the Python script.
Launch node
Once the package has been compiled, you can launch the node with the command
ros2 run my_package my_node
The basic code when creating a node
def main(): print('Hi from my_package.') if __name__ == '__main__': main()
At launch, this code will simply display the text of the print
Creating a Publisher and Subscriber for Turtlesim
In this example, we’ll see how to create a package with two nodes, one controlling the turtle, the other reading the position.
Package creation
ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name projects control_turtle --license Apache-2.0
Here’s the desired file architecture
.
├── LICENSE
├── package.xml
├── projects
│ ├── control_turtle.py
│ └── read_turtle.py
├── setup.cfg
└── setup.py
Add Python scripts
- Publisher control_turtle.py
Import the type of message to be published Twist for turtle speed control
We connect to the topic by specifying the name (‘/turtle1/cmd_vel’) and the data type (Twist)
We create a timer that will allow us to publish regularly on the topic (create_timer).
Finally, we publish the line and angular velocity
#!/usr/bin/env python # -*- coding: utf-8 -*- # # control_turtle.py import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist import os # Node should have the same name as the node file nodename= os.path.splitext(os.path.basename(__file__))[0] class TurtleController(Node): def __init__(self): super().__init__(nodename) self.get_logger().info(nodename+" started") self.publisher_ = self.create_publisher( Twist, '/turtle1/cmd_vel', 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 msg = Twist() msg.linear.x = 1.0 msg.angular.z = 0.5 self.publisher_.publish(msg) self.get_logger().info('Publishing: "%s"' % msg) self.i += 1 def main(args=None): rclpy.init(args=args) turtle_controller = TurtleController() try: rclpy.spin(turtle_controller) turtle_controller.destroy_node() rclpy.shutdown() except: turtle_controller.get_logger().info(nodename+" stopped") if __name__ == '__main__': main()
- Subscriber read_turtle.py
Import the type of message to be published Pose
We connect to the topic by specifying the name (‘/turtle1/pose’) and the data type (Pose)
We write the listening function that runs when new data is available on the topic
Create a timer to display values at the desired frequency
Finally, we publish the line and angular velocity
#!/usr/bin/env python # -*- coding: utf-8 -*- # # control_turtle.py import rclpy #rospy from rclpy.node import Node from turtlesim.msg import Pose import os # Node should have the same name as the node file nodename= os.path.splitext(os.path.basename(__file__))[0] class TurtleReader(Node): def __init__(self): super().__init__(nodename) self.get_logger().info(nodename+" started") self.subscription = self.create_subscription( Pose, '/turtle1/pose', self.listener_callback, 10) self.subscription # prevent unused variable warning self.timer = self.create_timer(2, self.timer_callback) self.msg = None def listener_callback(self, msg): self.msg = msg def timer_callback(self): self.get_logger().info("I read %s" % self.msg ) def main(args=None): rclpy.init(args=args) turtle_reader = TurtleReader() try: rclpy.spin(turtle_reader) turtle_reader.destroy_node() rclpy.shutdown() except: turtle_reader.get_logger().info(nodename+" stopped") if __name__ == '__main__': main()
Modify setup.py
In the setup.py entry points, you must specify the node names and the function to be launched (main)
entry_points={ 'console_scripts': [ 'control_turtle = projects.control_turtle:main', 'read_turtle = projects.read_turtle:main', ], },
Compiling the package
colcon build --packages_select projects --symlink-install
Launch nodes
To launch a node in a terminal, you must source the project
source /opt/ros/iron/setup.bash cd ~/ros2_ws source install/local_setup.bash
In three different terminals, run the following commands
ros2 run turtlesim turtlesim_node #terminal1 ros2 run projects read_turtle #terminal2 ros2 run projects control_turtle #terminal3
Results
You can see the turtle describe the shape of a circle and its position evolve in the subscriber’s center.
Launching a node with arguments
Nodes can be configured at runtime. In this example, we’ll define input parameters for linear and angular velocity. To do this, we’ll modify the code in the above Advertiser
In node initialization, we declare parameters with default values
self.declare_parameter('x', 1.0) self.declare_parameter('z', 0.5)
We can then retrieve the values of these parameters and store them in variables that we use in the callback function
self.linx = self.get_parameter('linx').get_parameter_value().double_value self.angz = self.get_parameter('angz').get_parameter_value().double_value
You can now launch a node with and without the
ros2 run projects circle_turtle #default param ros2 run projects circle_turtle --ros-args -p linx:=0.5 -p angz:=1.0
Complete code
#!/usr/bin/env python # -*- coding: utf-8 -*- # # control_turtle.py import rclpy #rospy from rclpy.node import Node from std_msgs.msg import String from turtlesim.msg import Pose from geometry_msgs.msg import Twist import os nodename= os.path.splitext(os.path.basename(__file__))[0] class TurtleController(Node): def __init__(self): super().__init__(nodename) self.get_logger().info(nodename+" started") self.publisher_ = self.create_publisher( Twist, '/turtle1/cmd_vel', 10) self.declare_parameter('linx', 1.0) self.declare_parameter('angz', 0.5) self.linx = self.get_parameter('linx').get_parameter_value().double_value self.angz = self.get_parameter('angz').get_parameter_value().double_value 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 msg = Twist() msg.linear.x = self.linx msg.angular.z = self.angz self.publisher_.publish(msg) self.get_logger().info('Publishing: "%s"' % msg) self.i += 1 def main(args=None): #os.system('ros2 service call /reset std_srvs/srv/Empty') #reset pos rclpy.init(args=args) turtle_controller = TurtleController() try: rclpy.spin(turtle_controller) turtle_controller.destroy_node() rclpy.shutdown() except: turtle_controller.get_logger().info(nodename+" stopped") if __name__ == '__main__': main()
Open the various terminals with a script
To save time during development, you can open terminals and launch nodes from a single bash script
#open terminal and run turtlesim gnome-terminal -- $SHELL -c "source ros_profile && ros2 run turtlesim turtlesim_node;exec $SHELL" gnome-terminal -- $SHELL -c "source ros_profile && ros2 run projects control_turtle;exec $SHELL"
Troubleshoot
- SetupTools deprecation
Check setuptools version
python3 import setuptools setuptools.__version__
‘59.6.0’
Install pip
sudo apt install python3-pip
then install version 58.2.0 of setuptools
python3 -m pip install setuptools==58.2.0