Another alternative approach to using generate_launch_description. Unlike one in a previous section, this one is good enough to use instead of generate_launch_description approach (one I use in other sections). It doesn't require compilation, allows to easily debug ROS2 nodes... The only problem is, it is not popular AT ALL, so if you choose it, you will be mostly on your own.
The code for this section is located in multi_bot_02 part of an archive. Note that archive does not include some common files like maps and worlds, that are used by all projects and therefore are located outside of them. They are part of an archive, too. Also note that we use ROS2 Galactic now.
As launch description approach is heavily used everywhere, I can not recommend alternatives: there will be too much efforts involved. However, it allows you to dramatically simplify certain things either, like, for example, debugging... So it is for you to decide.
To tell the truth, I am not sure why ROS2 examples use the launch description approach. First, we put all our nodes together, using generate_launch_description(), and only then ROS2 runs them. It runs those nodes "somewhere" in code that we do not control, and it makes debugging a very difficult task.
Also, parameters we pass to Node constructors are whapped in LaunchDescription classes, and we can not use them as we would use the underlying primitives. For example, you can not pass a string "robot" and then modify it, making it "robot1".
See, as ROS2 "collects" everything before running it, the above mentioned "robot" string doesn't exist inside generate_launch_description() - it is just a placeholder. To modify it, we have to do a rather irritating tricks, which boils down to creating another node that will modify the string ("robot") later, at the "execution" stage.
Ok, I understand that it sounds a bit complicated. Let me provide a more realistic example. Let's say, I want to load the nav2_params.yaml file, perform some heavy modifications (by "heavy" I mean that using "RewrittenYaml" approach, as we did in a previous section, becomes inconvenient) and then use this modified file instead of the original one. A situation like that may occure if we write a multi-robot project, and we want to add prefixes to all these countless "odom" and "base_link" topics, that are listed in nav2_params.yaml.
Here is the problem: we want to modify yaml or whatever else file on the fly and pass it to ROS2. But we can not do it in generate_launch_description, because it uses two-pass approach: first, everything is collected, then it is processed. In other words, generate_launch_description() is the wrong place to put code to modify the string - and we do not have the right place!
There are some ways of solving this problem. For example, we can wrap our code into the node, or (and this is what I am going to do) we get rid of using "LaunchConfiguration" alltogether.
First approach uses OpaqueFunction:
def launch_setup(context, *args, **kwargs): # Access the value of the 'my_param' command-line argument my_param_value = context.launch_configurations['rviz_config_file'] ... #This function can become the beginning # (we now have arguments and we can use it) # for the node: setup_action = OpaqueFunction(function=launch_setup) ld = LaunchDescription() ld.add_action(setup_action)
There are some minor inconveniences (how do we return value?), but it is solvable. However, as I mentioned, I had enough of this approach: it doesn't give us anything, and demands a lot. So let's use a simple "main" function instead:
main() ld.add_action(gazebo_cmd) ... launch_service = launch.LaunchService() launch_service.include_launch_description(ld) launch_service.run()
With this approach we:
a. Have more control over the execution loop
b. Can debug it
c. Can alter command line (or whatever) parameters that we pass
between our files.
d. Are not limited by strange and poorly documented syntax
Here is the main .py file of our project. The idea is to create nodes and then to "spin" them in a loop, that's all.
Above, we have something that would be impossible to do using LaunchDescription approach: we called a function! The function is bringup.getBringupNodes(), it simply moves some functionality to a different file.
From bringup.py, we call 3 functions, for slam, localization and navigation. Here is the SLAM module:
Here, we used a neat trick: we downloaded the nav2_params.yaml file and modified it on-the-fly. In this particular case we could probably do without it, but it is a great tool for future.
Then we saved a modified file, and used it, instead.
Localization is used both by SLAM and Mapping, so it is moved to a separate module.
Navigation is based on ROS2 Nav2.
There is no compiling!
SLAM:
# Terminal 1: $ ros2 topic pub cmd_vel geometry_msgs/msg/Twist '{linear: {x: 0.5, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}' # or $ ros2 run teleop_twist_keyboard teleop_twist_keyboard # Terminal 2: # And there is no Terminal2! See below.
We have Python file, so we can run it from our dev. environment. As I use VS Code, let's take a look at running from it.
First of all, you need to set command line parameters. In vs code it is done via launch.json file. Click "debug" icon, then settings icon (not the one at the bottom, but one on top). You will be offered to create launch.json. Here is mine:
{ "version": "0.2.0", "configurations": [ { "name": "Python: Current File", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": true, "cwd": "~/SnowCron/ros_projects/harsh", "purpose": ["debug-in-terminal"], "args": [ "--world", "maze_with_charger_and_obstacle.sdf", "--map", "map.yaml", "--keepout_mask", "keepout_mask.yaml", "--slam", "False", "--rviz_config_file", "robot_map_single.rviz", ] } ] }
First thing to pay attention to here is "cwd": "~/SnowCron/ros_projects/harsh", this line is important because I started the vs code from ~/SnowCron/ros_projects/harsh/src/multi_bot_01 (so I can see all my files in a project tree, and no files from other folders). But in order to run both old launch files and new .py file, I need them to have the same working directory. As you remember, we launch our launch files from ~/SnowCron/ros_projects/harsh.
There is a persistent bug in vs code, so be ready to invest some time in a research: in my case it fails to set the cwd sometimes.
Of course, you can run from the command line as well, but then you will not be able to debug the code.
Second thing to pay attention to is
"args": [ "--world", "maze_with_charger_and_obstacle.sdf", "--map", "map.yaml", "--keepout_mask", "keepout_mask.yaml", "--slam", "True", "--rviz_config_file", "robot_slam_single.rviz", ]
These are command line parameters, for these file names to work, the cwd should be set to ~/SnowCron/ros_projects/harsh. The code does the rest: see the single_simulation.py.
Now you can run and debug the code from vs code.
Navigation.
# Terminal 1: $ cd ~/SnowCron/ros_projects/harsh/src/multi_bot_03/scripts $ python3 03_charger_docking.py
Note that for mapping to work, you have to make two changes in launch.json:
{ "version": "0.2.0", "configurations": [ { "name": "Python: Current File", "type": "python", "request": "launch", "program": "${file}", "console": "integratedTerminal", "justMyCode": true, "cwd": "~/SnowCron/ros_projects/harsh", "purpose": ["debug-in-terminal"], "args": [ "--world", "maze_with_charger_and_obstacle.sdf", "--map", "map.yaml", "--keepout_mask", "keepout_mask.yaml", "--slam", "False", "--rviz_config_file", "robot_map_single.rviz", ] } ] }
We changed --slam from True to False, and used a different RViz config file.
rclpy https://roboticsbackend.com/rclpy-params-tutorial-get-set-ros2-params-with-python/