ROS2, controlling diff. drive robot from ROS2 node

Controlling our robot from ROS2 node

Introduction

In this section we are going to create a ROS2 node that will receive data (lidar) from the robot (implemented in diff_drive_controller_bot) and send it commands to move around without bumping into the walls.
Precice navigation is not something we are after in this section, so the algorithm I am going to use is extremely primitive: we only focus on sending and receiving data.

The code is available here.

Note that, using Gazebo's built-in editor, I have created a primitive labyrinth "world" called maze.sdf. As this world is going to be used by more than one package, I have placed it in src/worlds.

Few useful commands

Let's start our simulation and see what topics are active. Open a new terminal, and type:

                    # Before we started it:
                    
                    # In Terminal 1:
                    $ ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -r /cmd_vel:=/diff_cont/cmd_vel_unstamped
                
                    # In Terminal 2:
                    $ ros2 topic list -t /diff_cont/cmd_vel_unstamped [geometry_msgs/msg/Twist]
                
                    # After we started it:
                
                    # In Terminal 1:
                    $ ros2 launch diff_drive_controller_bot launch_sim.launch world:=src/worlds/shapes.sdf
                
                    # In Terminal 2:
                    $ ros2 topic list -t
                    /camera/camera_info [sensor_msgs/msg/CameraInfo]
                    /camera/image_raw [sensor_msgs/msg/Image]
                    /clock [rosgraph_msgs/msg/Clock]
                    /diff_cont/cmd_vel_unstamped [geometry_msgs/msg/Twist]
                    /dynamic_joint_states [control_msgs/msg/DynamicJointState]
                    /joint_states [sensor_msgs/msg/JointState]
                    /laser_controller/out [sensor_msgs/msg/LaserScan]
                    /odom [nav_msgs/msg/Odometry]
                    /performance_metrics [gazebo_msgs/msg/PerformanceMetrics]
                    /robot_description [std_msgs/msg/String]
                    /tf [tf2_msgs/msg/TFMessage]
                    /tf_static [tf2_msgs/msg/TFMessage]        
                

Additional

                    Try sending commands:

                        ros2 topic pub /demo/cmd_demo geometry_msgs/Twist '{linear: {x: 1.0}}' -1

                        ros2 topic pub /demo/cmd_demo geometry_msgs/Twist '{angular: {z: 0.1}}' -1

                    Try listening to odometry:

                        ros2 topic echo /demo/odom_demo

                    Try listening to TF:

                        ros2 run tf2_ros tf2_echo odom_demo chassis

                        ros2 run tf2_ros tf2_echo chassis right_wheel

                        ros2 run tf2_ros tf2_echo chassis left_wheel

                    Loading new map:
                        ros2 service call /map_server/load_map nav2_msgs/srv/LoadMap "{map_url:
                        $HOME/SnowCron/ros_projects/harsh/src/maps/map_01_binary.yaml}"

                    Odometry message:
                        ros2 topic pub /robot1/odom nav_msgs/Odometry "{header: {stamp: {sec: 28, nanosec: 786000000}, frame_id: 'robot1/odom'}, child_frame_id: 'robot1/base_link', pose: {pose: {position: {x: 0.006435876230684812, y: 0.5018193951070563, z: 0.1500022532872065}, orientation: {x: -1.202497725575462e-06, y: 4.545254229127168e-06, z: -0.009871243297678176, w: 0.9999512780799137}}, covariance: [1.0e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000000000000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000000000000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000000000000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001]}, twist: {twist: {linear: {x: -4.151254536741296e-05, y: -0.0003711630071032284, z: 0.0}, angular: {x: 0.0, y: 0.0, z: -0.00035124248507245457}}, covariance: [1.0e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0e-05, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000000000000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000000000000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1000000000000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001]}}"

                    Imu message:
                        ros2 topic pub /imu sensor_msgs/Imu "{header: {stamp: {sec: 99, nanosec: 1000000}, frame_id: 'base_link'}, orientation: {x: -3.2484036403009136e-07, y: -1.233682290372918e-07, z: -5.640383185432385e-06, w: 0.9999999999840328}, orientation_covariance: [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], angular_velocity: {x: -0.00017136447475541198, y: 0.00013966822040950878, z: 4.4918826200876715e-06}, angular_velocity_covariance: [4.0e-08, 0.0, 0.0, 0.0, 4.0e-08, 0.0, 0.0, 0.0, 4.0e-08], linear_acceleration: {x: -0.020305982380950664, y: -0.005017642374176924, z: 9.765243743038877}, linear_acceleration_covariance: [0.00028900000000000003, 0.0, 0.0, 0.0, 0.00028900000000000003, 0.0, 0.0, 0.0, 0.00028900000000000003]}"

                    Removing environment variable (colcon sets it, and sometimes we want to clean)                    
                        unset VARIABLE_NAME
                

Open up a new terminal window, and type the following command to make the robot move forward at a speed of 1.0 meters per second:

                    $ ros2 topic pub /demo/cmd_demo geometry_msgs/Twist '{linear: {x: 1.0}}' -1
                

A robot moves, as expected.

Let's start gazebo again:

                    $ gazebo
                

... and insert our (not Foxy sample) robot in it. Now we can open a terminal and issue a command:

                    $ ros2 topic pub /demo/cmd_vel geometry_msgs/Twist '{linear: {x: 1.0}}' -1
                

... to make our robot move.

Directory structure

                    $ cd ~/SnowCron/ros_projects/harsh
                    $ tree --dirsfirst src/ros2_teleop/
                    src/ros2_teleop/
                    ├── launch
                    │   └── ros2_teleop.launch.py
                    ├── resource
                    │   └── ros2_teleop             # Empty
                    ├── ros2_teleop
                    │   ├── __init__.py             # Empty
                    │   └── robot_controller.py
                    ├── package.xml
                    ├── setup.cfg
                    └── setup.py
                

As you can see, it is just a Python ROS2 node, with no URDF involved. It will run from Terminal, and exchange data with diff_drive_controller_bot.

ros2_teleop.launch.py

                    import os
                    from launch import LaunchDescription
                    from launch_ros.actions import Node
                    
                    def generate_launch_description():
                    
                    return LaunchDescription([
                        Node(package='ros2_teleop', executable='robot_controller', output='screen')
                    ])
                

Our package is called ros2_teleop because it works under ROS2 and does things similar to what teleop_twist_keyboard does.

We instruct the system to start a node and look for a "main" function in robot_controller.py

package.xml

                                            
                

Nothing unexpected in this file: human-readable info, like (C) and e.mail and dependencies.

setup.cfg

                    [develop]
                    script-dir=$base/lib/ros2_teleop
                    [install]
                    install-scripts=$base/lib/ros2_teleop
                

An automatically (if you use ROS2 utility to create a package) file.

setup.py

                    import os
                    from setuptools import setup
                    from glob import glob
                    
                    package_name = 'ros2_teleop'
                    
                    setup(
                        name=package_name,
                        version='0.0.0',
                        packages=[package_name],
                        data_files=[
                            ('share/ament_index/resource_index/packages',
                                ['resource/' + package_name]),
                            ('share/' + package_name, ['package.xml']),
                            (os.path.join('share', package_name), glob('launch/*.launch.py'))
                        ],
                        install_requires=['setuptools'],
                        zip_safe=True,
                        maintainer='harsh',
                        maintainer_email='contact@mail.com',
                        description='TODO: Package description',
                        license='TODO: License declaration',
                        tests_require=['pytest'],
                        entry_points={
                            'console_scripts': [
                            'robot_controller = ros2_teleop.robot_controller:main'
                            ],
                        },
                    )
                

The only interesting string here is
'robot_controller = ros2_teleop.robot_controller:main'
If you (like myself) prefer copying projects rather than creating them from scratch, you need to pay attention to package names in places like this: ROS2 does not produce meaningful error messages if package name is wrong, and your code will "just not work".

robot_controller.py

This is the file containing most important code, it subscribes to info and publishes commands.

Note that in the listing below, I only provided whatever is used for a demo, while in an archive (above) there are some commented code that can be used in some cases (like odometry).

                    
                

(C) snowcron.com, all rights reserved

Please read the disclaimer