fbpixel
Tags: , , ,

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo apt install python3-colcon-common-extensions
sudo apt install python3-colcon-common-extensions
sudo apt install python3-colcon-common-extensions

Create folder

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
mkdir -p ~/tuto_ws/src
cd ~/tuto_ws
mkdir -p ~/tuto_ws/src cd ~/tuto_ws
mkdir -p ~/tuto_ws/src
cd ~/tuto_ws

Then copy a sample project, such as tutorial containing turtlesim

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
git clone https://github.com/ros/ros_tutorials.git -b iron
git clone https://github.com/ros/ros_tutorials.git -b iron
git clone https://github.com/ros/ros_tutorials.git -b iron

Then build the project using the command

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
colcon build
colcon build
colcon build

To load your workspace into a new terminal

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
source /opt/ros/iron/setup.bash
cd ~/ros2_ws
source install/local_setup.bash
source /opt/ros/iron/setup.bash cd ~/ros2_ws source install/local_setup.bash
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name my_node my_package --license Apache-2.0
ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name my_node my_package --license Apache-2.0
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
rosdep install -i --from-path src --rosdistro iron -y
rosdep install -i --from-path src --rosdistro iron -y
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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
colcon build --packages_select my_package --symlink-install
colcon build --packages_select my_package --symlink-install
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ros2 run my_package my_node
ros2 run my_package my_node
ros2 run my_package my_node

The basic code when creating a node

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
def main():
print('Hi from my_package.')
if __name__ == '__main__':
main()
def main(): print('Hi from my_package.') if __name__ == '__main__': main()
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name projects control_turtle --license Apache-2.0
ros2 pkg create --build-type ament_python --license Apache-2.0 --node-name projects control_turtle --license Apache-2.0
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/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()
#!/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()
#!/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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/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()
#!/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()
#!/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)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
entry_points={
'console_scripts': [
'control_turtle = projects.control_turtle:main',
'read_turtle = projects.read_turtle:main',
],
},
entry_points={ 'console_scripts': [ 'control_turtle = projects.control_turtle:main', 'read_turtle = projects.read_turtle:main', ], },
entry_points={
        'console_scripts': [
                'control_turtle = projects.control_turtle:main',
                'read_turtle = projects.read_turtle:main',
        ],
},

Compiling the package

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
colcon build --packages_select projects --symlink-install
colcon build --packages_select projects --symlink-install
colcon build --packages_select projects --symlink-install

Launch nodes

To launch a node in a terminal, you must source the project

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
source /opt/ros/iron/setup.bash
cd ~/ros2_ws
source install/local_setup.bash
source /opt/ros/iron/setup.bash cd ~/ros2_ws source install/local_setup.bash
source /opt/ros/iron/setup.bash
cd ~/ros2_ws
source install/local_setup.bash

In three different terminals, run the following commands

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ros2 run turtlesim turtlesim_node #terminal1
ros2 run projects read_turtle #terminal2
ros2 run projects control_turtle #terminal3
ros2 run turtlesim turtlesim_node #terminal1 ros2 run projects read_turtle #terminal2 ros2 run projects control_turtle #terminal3
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.

turtlesim-read-control-turtle Creating a Python script under ROS2

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
self.declare_parameter('x', 1.0)
self.declare_parameter('z', 0.5)
self.declare_parameter('x', 1.0) self.declare_parameter('z', 0.5)
    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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
self.linx = self.get_parameter('linx').get_parameter_value().double_value
self.angz = self.get_parameter('angz').get_parameter_value().double_value
self.linx = self.get_parameter('linx').get_parameter_value().double_value self.angz = self.get_parameter('angz').get_parameter_value().double_value
    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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
ros2 run projects circle_turtle #default param
ros2 run projects circle_turtle --ros-args -p linx:=0.5 -p angz:=1.0
ros2 run projects circle_turtle #default param ros2 run projects circle_turtle --ros-args -p linx:=0.5 -p angz:=1.0
ros2 run projects circle_turtle #default param
ros2 run projects circle_turtle --ros-args -p linx:=0.5 -p angz:=1.0
turtlesim-circle-param Creating a Python script under ROS2

Complete code

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#!/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()
#!/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()
#!/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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
#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"
#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"
#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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
python3
import setuptools
setuptools.__version__
python3 import setuptools setuptools.__version__
python3
import setuptools
setuptools.__version__

‘59.6.0’

Install pip

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
sudo apt install python3-pip
sudo apt install python3-pip
sudo apt install python3-pip

then install version 58.2.0 of setuptools

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
python3 -m pip install setuptools==58.2.0
python3 -m pip install setuptools==58.2.0
python3 -m pip install setuptools==58.2.0

Sources