- Create workspace
$ mkdir -p bumperbot_ws/src
- initialize
$ colcon build
Generates automatically build
(intermediate files) install
(final executable files) and log
folders
- create a python package named
bumperbot_py_examples
:
$ cd src
$ ros2 pkg create --build-type ament_python bumperbot_py_examples
going to create a new package
package name: bumperbot_py_examples
destination directory: /home/mhered/bumperbot_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['mhered <[email protected]>']
licenses: ['TODO: License declaration']
build type: ament_python
dependencies: []
creating folder ./bumperbot_py_examples
creating ./bumperbot_py_examples/package.xml
creating source folder
creating folder ./bumperbot_py_examples/bumperbot_py_examples
creating ./bumperbot_py_examples/setup.py
creating ./bumperbot_py_examples/setup.cfg
creating folder ./bumperbot_py_examples/resource
creating ./bumperbot_py_examples/resource/bumperbot_py_examples
creating ./bumperbot_py_examples/bumperbot_py_examples/__init__.py
creating folder ./bumperbot_py_examples/test
creating ./bumperbot_py_examples/test/test_copyright.py
creating ./bumperbot_py_examples/test/test_flake8.py
creating ./bumperbot_py_examples/test/test_pep257.py
Note difference vs the command for a C++ package:
$ ros2 pkg create --build-type ament_cmake bumperbot_cpp_examples
If we create also the C++ package and we rebuild the workspace two packages are created:
$ cd .. && colcon build
Starting >>> bumperbot_cpp_examples
Starting >>> bumperbot_py_examples
Finished <<< bumperbot_cpp_examples [0.62s]
Finished <<< bumperbot_py_examples [0.70s]
Summary: 2 packages finished [0.92s]
- activate the workspace so the packages are recognized by sourcing the workspace (only affects current terminal). He claims it is good practice to do it from a different terminal to the one we are using to build it (??)
$ source install/setup.bash
$ ros2 pkg list
ackermann_msgs...
builtin_interfaces
bumperbot_cpp_examples
bumperbot_py_examples
camera_calibration_parsers
...
zstd_vendor
Typical structure of a python package
$ tree ~/bumperbot_ws/src/bumperbot_py_examples/
~/bumperbot_ws/src/bumperbot_py_examples/
├── bumperbot_py_examples
│ └── __init__.py
├── package.xml
├── resource
│ └── bumperbot_py_examples
├── setup.cfg
├── setup.py
└── test
├── test_copyright.py
├── test_flake8.py
└── test_pep257.py
3 directories, 8 files
The bumperbot_py_examples
folder contains the python code.
-
create a python file inside it called
simple_publisher.py
-
setup.py
contains the instructions to make an executable. Add an entry point so that whensimple_publisher
node is called, it executes functionmain
insimple_publisher
file insidebumperbot_py_examples
package :
...
entry_points={create a simple_subscriber.py file inside
'console_scripts': [
'simple_publisher = bumperbot_py_examples.simple_publisher:main',
],
- in
package.xml
declare execution dependencies (the two libraries we included in the script)
<exec_depend>rclpy</exec_depend>
<exec_depend>std-msgs</exec_depend>
- build with colcon
$ colcon build
Starting >>> bumperbot_cpp_examples
Starting >>> bumperbot_py_examples
Finished <<< bumperbot_cpp_examples [0.20s]
Finished <<< bumperbot_py_examples [0.64s]
Summary: 2 packages finished [0.83s]
- in a different terminal source the workspace and run the node
$ source install/setup.bash
$ ros2 run bumperbot_py_examples simple_publisher
[INFO] [1719217038.723989913] [simple_publisher]: Publishing at 1 Hz
- monitor the topic to check it is broadcasting every 1sec
$ ros2 topic list
/chatter
/parameter_events
/rosout
$ ros2 topic echo /chatter
data: 'Hello, ROS2 - counter: 35 '
---
data: 'Hello, ROS2 - counter: 36 '
---
data: 'Hello, ROS2 - counter: 37 '
---
data: 'Hello, ROS2 - counter: 38 '
---
data: 'Hello, ROS2 - counter: 39 '
---
$ ros2 topic info /chatter --verbose
Type: std_msgs/msg/String
Publisher count: 1
Node name: simple_publisher
Node namespace: /
Topic type: std_msgs/msg/String
Endpoint type: PUBLISHER
GID: 01.0f.3a.b1.6d.42.75.04.01.00.00.00.00.00.11.03.00.00.00.00.00.00.00.00
QoS profile:
Reliability: RMW_QOS_POLICY_RELIABILITY_RELIABLE
Durability: RMW_QOS_POLICY_DURABILITY_VOLATILE
Lifespan: 2147483651294967295 nanoseconds
Deadline: 2147483651294967295 nanoseconds
Liveliness: RMW_QOS_POLICY_LIVELINESS_AUTOMATIC
Liveliness lease duration: 2147483651294967295 nanoseconds
Subscription count: 0
$ ros2 topic hz /chatter
average rate: 1.000
min: 1.000s max: 1.000s std dev: 0.00002s window: 2
average rate: 1.000
min: 1.000s max: 1.000s std dev: 0.00006s window: 4
average rate: 1.000
min: 1.000s max: 1.000s std dev: 0.00005s window: 5
average rate: 1.000
-
create
simple_subscriber.py
file insidebumperbot_py_examples
folder -
Add an entry point in
setup.py
:
...
entry_points={create a simple_subscriber.py file inside
'console_scripts': [
'simple_publisher = bumperbot_py_examples.simple_publisher:main',
'simple_subscriber = bumperbot_py_examples.simple_subscriber:main',
],
- build with colcon
$ colcon build
- in a different terminal source the workspace and run the node
$ source install/setup.bash
$ ros2 run bumperbot_py_examples simple_subscriber
[INFO] [1719219470.084111799] [simple_subscriber]: I heard: Hello, ROS2 - counter: 716
[INFO] [1719219470.972924372] [simple_subscriber]: I heard: Hello, ROS2 - counter: 717
[INFO] [1719219471.972834813] [simple_subscriber]: I heard: Hello, ROS2 - counter: 718
...s
Certain nodes take parameters , i.e. settings to configure their behaviour
-
create file
simple_parametric.py
insidebumperbot_py_examples
folder and write the code for parametric node -
add the entry point in
setup.py
:
entry_points={
'console_scripts': [
...
bumperbot_py_examples.simple_parametric:main',
],
- add a missing dependency in
package.xml
:
<exec_depend>rcl_interfaces</exec_depend>
-
as usual, build with colcon, then source in a different terminal.
-
run the node with default param values with:
$ ros2 run bumperbot_py_examples simple_parametric
- check available parameters, and their values:
$ ros2 param list
/simple_parametric:
simple_int_param
simple_string_param
use_sim_time
$ ros2 param get /simple_parametric simple_int_param
Integer value is: 0
$ ros2 param get /simple_parametric simple_string_param
String value is: hello
- You can run the node passing custom params:
$ ros2 run bumperbot_py_examples simple_parametric --ros-args -p simple_int_param:=30
$ ros2 param get /simple_parametric simple_int_param
Integer value is: 30
- you can also modify parameters at run time:
$ ros2 param get /simple_parametric simple_string_param
String value is: Hello
$ ros2 param set /simple_parametric simple_string_param "Goodbye"
Set parameter successful
$ ros2 param get /simple_parametric simple_string_param
String value is: Goodbye
And with the implementation we made, the callback function yields the following terminal output:
$ ros2 run bumperbot_py_examples simple_parametric
[INFO] [1719431653.982840885] [simple_parametric]: Parameter simple_string_param changed to : Goodbye
Spawn two turtles:
$ ros2 run turtlesim turtlesim_node
$ ros2 service call /spawn turtlesim/srv/Spawn "x: 4.0
y: 4.0
theta: 0.0
name: 'turtle2'"
turtle1/pose
and turtle2/pose
topics publish their respective poses.
Create a node simple_turtlesim_kinematics.py
inside bumperbot_py_examples
folder, that publishes the translation vector between turtle2
and turtle1
To find the type of messages:
$ ros2 topic info /turtle1/pose
Type: turtlesim/msg/Pose
Publisher count: 1
Subscription count: 0
So we add a dependency
from turtlesim.msg import Pose
Write the node:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from turtlesim.msg import Pose
class SimpleTurtlesimKinematics(Node):
def __init__(self):
# constructor
#init node
super().__init__('simple_turtlesim_kinematics')
# init two subscribers
self.turtle1_pose_sub_ = self.create_subscription(
Pose,
'/turtle1/pose',
self.turtle1PoseCallback,
10
)
self.turtle2_pose_sub_ = self.create_subscription(
Pose,
'/turtle2/pose',
self.turtle2PoseCallback,
10
)
# 2 variables
self.last_turtle1_pose_ = Pose()
self.last_turtle2_pose_ = Pose()
def turtle1PoseCallback(self, msg):
# save last turtle1 pose
self.last_turtle1_pose_ = msg
def turtle2PoseCallback(self, msg):
# save last turtle2 pose
self.last_turtle2_pose_ = msg
# calculate and log translation and rotation
Tx = self.last_turtle2_pose_.x - self.last_turtle1_pose_.x
Ty = self.last_turtle2_pose_.y - self.last_turtle1_pose_.y
theta = self.last_turtle2_pose_.theta - self.last_turtle1_pose_.theta
theta_deg = 180*theta/np.pi
self.get_logger().info(f"""\n
Translation vector turtle1 -> turtle2 \n
Tx: {Tx:.2f} \n
Ty: {Ty:.2f} \n
Rotation matrix turtle1 -> turtle2 \n
theta (rad): {theta:.2f} rad \n
theta (deg): {theta_deg:.2f} deg \n
[R11 R12] : [{np.cos(theta):.2f}\t{-np.sin(theta):.2f}] \n
[R21 R22] : [{np.sin(theta):.2f}\t{np.cos(theta):.2f}] \n
""")
def main():
rclpy.init()
simple_turtlesim_kinematics = SimpleTurtlesimKinematics()
rclpy.spin(simple_turtlesim_kinematics)
simple_turtlesim_kinematics.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
install it in setup.py
:
entry_points={
'console_scripts': [
...
'simple_turtlesim_kinematics = bumperbot_py_examples.simple_turtlesim_kinematics:main',
]
and update dependencies in package.xml
<exec_depend>turtlesim</exec_depend>
build, source and run the node:
$ ros2 run bumperbot_py_examples simple_turtlesim_kinematics
[INFO] [1727391554.455939178] [simple_turtlesim_kinematics]:
Translation vector turtle1 -> turtle2
Tx: -4.22
Ty: -1.28
Rotation matrix turtle1 -> turtle2
theta (rad): -2.02 rad
theta (deg): -115.51 deg
[R11 R12] : [-0.43 0.90]
[R21 R22] : [-0.90 -0.43]
...
Subscribes to \cmd_vel
commands coming from the joystick V, W, calculates simple_velocity_controller/commands
Note the bumperbot_controller
package was created for cpp nodes so we need to make some changes manually:
- create a
bumperbot_controller
folder, an__init__.py
empty file inside, then create asimple_controller.py
node with:
- two parameters
self.wheel_radius_
andself.wheel_separation
that describe the robot geometry - a subscriber that listens to joystick commands of type
TwistStamped
in topicbumperbot_controller/cmd_vel
. The joystick message contains the target robot linear and angular velocities. - the subscriber has a callback function
self.velCallback
that computes wheel commands from robot linear and angular velocities using the equations derived in Section_7_Differential_Kinematics.md - a publisher for wheel speed commands of type
Float64MultiArray
in topicsimple_velocity_controller/commands
(see details of the topic and message type used by the ros2 controller in Section_5_Control.md)
- In
CMakeLists.txt
add dependencies to:ament_cmake_python
(needed to build and compile python nodes)- dependencies used in the node:
rclpy
,std_msgs
,geometry_msgs
- Also in
CMakeLists.txt
install the nodesimple_controller.py
...
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)
find_package(rclpy REQUIRED)
find_package(std_msgs REQUIRED)
find_package(geometry_msgs REQUIRED)
ament_python_install_package(${PROJECT_NAME})
...
install (
PROGRAMS ${PROJECT_NAME}/simple_controller.py
DESTINATION lib/${PROJECT_NAME}
)
- Add the dependencies also in
package.xml
:
...
<buildtool_depend>ament_cmake_python</buildtool_depend>
...
<depend>rclpy</depend>
<depend>std_msgs</depend>
<depend>geometry_msgs</depend>
...
- in package
bumperbot_py_examples
add a new nodesimple_tf_kinematics.py
. Publishes a simple static translation of 30cm along the z-axis betweenbumperbot_base
andbumperbot_top
frames using theStaticTransformBroadcaster()
class - declare the new node in
setup.py
:
entry_points={
'console_scripts': [
...
'simple_tf_kinematics = bumperbot_py_examples.simple_tf_kinematics:main',
],
},
- add dependencies to
package.xml
...
<exec_depend>tf2_ros</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
...
build, source and execute, then inspect the tf_static
topic :
(T1): $ colcon build
(T2): $ source install/setup.bash
(T2): $ ros2 run bumperbot_py_examples simple_tf_kinematics
[INFO] [1730576203.836503059] [simple_tf_kinematics]: Publishing static transform from bumperbot_base to bumperbot_top
(T3): $ ros2 topic echo /tf_static
transforms:
- header:
stamp:
sec: 1730576203
nanosec: 824236889
frame_id: bumperbot_base
child_frame_id: bumperbot_top
transform:
translation:
x: 0.0
y: 0.0
z: 0.3
rotation:
x: 0.0
y: 0.0
z: 0.0
w: 1.0
---
We can also visualize the fixed transform in rviz running $ rviz2
, then selecting bumperbot_base
as fixed frame then Add > TF and tick Show Names
Modify simple_tf_kinematics.py
to add a dynamic transform of TransformBroadcaster()
class. Define a timer to update the pose and publish it every 0.1 seconds with the callback function timerCallback
. No need to add new dependencies.
build, source and execute, inspect the tf
topic and visualize in rviz as before (selecting odom
as fixed frame).
(T1): $ colcon build
(T2): $ source install/setup.bash
(T2): $ ros2 run bumperbot_py_examples simple_tf_kinematics
[INFO] [1730580408.588504111] [simple_tf_kinematics]: Publishing static transform from bumperbot_base to bumperbot_top
(T3): $ ros2 topic echo /tf
...
transforms:
- header:
stamp:
sec: 1730579116
nanosec: 278956793
frame_id: odom
child_frame_id: bumperbot_base
transform:
translation:
x: 3.599999999999995
y: 0.0
z: 0.0
rotation:
x: 0.0
y: 0.0
z: 0.0
w: 1.0
---
transforms:
- header:
stamp:
sec: 1730579116
nanosec: 378906524
frame_id: odom
child_frame_id: bumperbot_base
transform:
translation:
x: 3.649999999999995
y: 0.0
z: 0.0
rotation:
x: 0.0
y: 0.0
z: 0.0
w: 1.0
---
...