ROS2笔记

鱼香ROS后记,以上内容全部出自鱼香ROS,仅个人记录学习。

用g++编译ROS2的C++节点

first_ros2_node.cpp中输入下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
// 包含rclcpp头文件,如果Vscode显示红色的波浪线也没关系
// 我们只是把VsCode当记事本而已,谁会在意记事本对代码的看法呢,不是吗?
#include "rclcpp/rclcpp.hpp"

int main(int argc, char **argv)
{
// 调用rclcpp的初始化函数
rclcpp::init(argc, argv);
// 调用rclcpp的循环运行我们创建的first_node节点
rclcpp::spin(std::make_shared<rclcpp::Node>("first_node"));
return 0;
}

编译

接着我们使用g++来编译 first_ros2_node节点。正常的话一定会报错。

1
g++ first_ros2_node.cpp

报错内容如下:

1
2
3
4
5
root@490925f19143:~/d2lros2/d2lros2/chapt2/basic# g++ first_ros2_node.cpp 
first_ros2_node.cpp:3:10: fatal error: rclcpp/rclcpp.hpp: No such file or directory
3 | #include "rclcpp/rclcpp.hpp"
| ^~~~~~~~~~~~~~~~~~~
compilation terminated.

一定要记住这个错误 No such file or directory,这将是你接下来机器人学习工作生涯中最常见的错误之一。

最终解决一层一层的头文件导入后因该是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
g++ first_ros2_node.cpp \
-I/opt/ros/jazzy/include/rclcpp/ \
-I /opt/ros/jazzy/include/rcl/ \
-I /opt/ros/jazzy/include/rcutils/ \
-I /opt/ros/jazzy/include/rmw \
-I /opt/ros/jazzy/include/rcl_yaml_param_parser/ \
-I /opt/ros/jazzy/include/rosidl_runtime_c \
-I /opt/ros/jazzy/include/rosidl_typesupport_interface \
-I /opt/ros/jazzy/include/rcpputils \
-I /opt/ros/jazzy/include/builtin_interfaces \
-I /opt/ros/jazzy/include/rosidl_runtime_cpp \
-I /opt/ros/jazzy/include/tracetools \
-I /opt/ros/jazzy/include/rcl_interfaces \
-I /opt/ros/jazzy/include/libstatistics_collector \
-I /opt/ros/jazzy/include/statistics_msgs \
-I /opt/ros/jazzy/include/service_msgs \
-I /opt/ros/jazzy/include/type_description_interfaces \
-I /opt/ros/jazzy/include/rosidl_dynamic_typesupport \
-I /opt/ros/jazzy/include/rosidl_typesupport_introspection_cpp \

运行完上面这段代码,你会发现报的错误变了。

1
2
3
4
5
6
7
8
9
10
11
12
/usr/bin/ld: /tmp/ccoA8hho.o: in function `main':
first_ros2_node.cpp:(.text+0x37): undefined reference to `rcutils_get_default_allocator'
/usr/bin/ld: first_ros2_node.cpp:(.text+0x5c): undefined reference to `rclcpp::InitOptions::InitOptions(rcutils_allocator_s)'
/usr/bin/ld: first_ros2_node.cpp:(.text+0x7d): undefined reference to `rclcpp::init(int, char const* const*, rclcpp::InitOptions const&, rclcpp::SignalHandlerOptions)'
/usr/bin/ld: first_ros2_node.cpp:(.text+0x89): undefined reference to `rclcpp::InitOptions::~InitOptions()'
/usr/bin/ld: first_ros2_node.cpp:(.text+0xb1): undefined reference to `rclcpp::spin(std::shared_ptr<rclcpp::Node>)'
/usr/bin/ld: first_ros2_node.cpp:(.text+0xe9): undefined reference to `rclcpp::InitOptions::~InitOptions()'
/usr/bin/ld: /tmp/ccoA8hho.o: in function `void __gnu_cxx::new_allocator<rclcpp::Node>::construct<rclcpp::Node, char const (&) [11]>(rclcpp::Node*, char const (&) [11])':
first_ros2_node.cpp:(.text._ZN9__gnu_cxx13new_allocatorIN6rclcpp4NodeEE9constructIS2_JRA11_KcEEEvPT_DpOT0_[_ZN9__gnu_cxx13new_allocatorIN6rclcpp4NodeEE9constructIS2_JRA11_KcEEEvPT_DpOT0_]+0x86): undefined reference to `rcutils_get_default_allocator'
/usr/bin/ld: first_ros2_node.cpp:(.text._ZN9__gnu_cxx13new_allocatorIN6rclcpp4NodeEE9constructIS2_JRA11_KcEEEvPT_DpOT0_[_ZN9__gnu_cxx13new_allocatorIN6rclcpp4NodeEE9constructIS2_JRA11_KcEEEvPT_DpOT0_]+0xb7): undefined reference to `rclcpp::NodeOptions::NodeOptions(rcutils_allocator_s)'
/usr/bin/ld: first_ros2_node.cpp:(.text._ZN9__gnu_cxx13new_allocatorIN6rclcpp4NodeEE9constructIS2_JRA11_KcEEEvPT_DpOT0_[_ZN9__gnu_cxx13new_allocatorIN6rclcpp4NodeEE9constructIS2_JRA11_KcEEEvPT_DpOT0_]+0xe7): undefined reference to `rclcpp::Node::Node(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, rclcpp::NodeOptions const&)'
collect2: error: ld returned 1 exit status

原因在于g++找不到库文件,解决方法就是我们帮助它定位到库文件的位置,并通过-L参数指定库目录,-l(小写L)指定库的名字。

1
-L /opt/ros/jazzy/lib/ \

https://cdn.ziyourufeng.eu.org/51hhh/img_bed/main/img/2024/10_30/image_7c945163af32ebac8a7962720057139f.png

得到可执行程序 first_node,运行 ./first_node即可执行。

使用make编译ROS2节点

安装make

1
sudo apt install make

编写Makefile

在first_ros2_node.cpp同目录下新建 Makefile,然后将上面的g++编译指令用下面的形式写到Makefile里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
build:
g++ first_ros2_node.cpp \
-I/opt/ros/jazzy/include/rclcpp/ \
-I /opt/ros/jazzy/include/rcl/ \
-I /opt/ros/jazzy/include/rcutils/ \
-I /opt/ros/jazzy/include/rmw \
-I /opt/ros/jazzy/include/rcl_yaml_param_parser/ \
-I /opt/ros/jazzy/include/rosidl_runtime_c \
-I /opt/ros/jazzy/include/rosidl_typesupport_interface \
-I /opt/ros/jazzy/include/rcpputils \
-I /opt/ros/jazzy/include/builtin_interfaces \
-I /opt/ros/jazzy/include/rosidl_runtime_cpp \
-I /opt/ros/jazzy/include/tracetools \
-I /opt/ros/jazzy/include/rcl_interfaces \
-I /opt/ros/jazzy/include/libstatistics_collector \
-I /opt/ros/jazzy/include/statistics_msgs \
-I /opt/ros/jazzy/include/service_msgs \
-I /opt/ros/jazzy/include/type_description_interfaces \
-I /opt/ros/jazzy/include/rosidl_dynamic_typesupport \
-I /opt/ros/jazzy/include/rosidl_typesupport_introspection_cpp \
-L /opt/ros/jazzy/lib/ \
-lrclcpp -lrcutils \
-o first_node

# 顺便小鱼加个clean指令,用来删掉first_node
clean:
rm first_node


编译

在Makefile同级目录输入

1
make build

make指令调用了脚本里的build下的指令,对代码进行了编译。同级目录下也产生了first_node可执行文件(绿色代表可执行)。

使用 make clean指令即可删掉 first_node节点。

运行测试

1
./first_node

https://cdn.ziyourufeng.eu.org/51hhh/img_bed/main/img/2024/10_30/image_e1dbdfe30cb9474d736c6e2ea293f73c.png

新开终端

1
ros2 node list

https://cdn.ziyourufeng.eu.org/51hhh/img_bed/main/img/2024/10_30/image_c4d4c2093a26c1fc9d8fcb5a03e7af41.png

使用CMakeLists.txt编译ROS2节点

虽然通过make调用Makefile编译代码非常的方便,但是还是需要我们手写gcc指令来编译,那有没有什么办法可以自动生成Makefile呢?

答案是有的,那就是cmake工具。

cmake通过调用CMakeLists.txt直接生成Makefile。

安装Cmake

1
sudo apt install cmake

新建CMakeLists.txt

d2lros2/d2lros2/chapt2/basic新建 CMakeLists.txt,输入下面内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
cmake_minimum_required(VERSION 3.22)

project(first_node)

#include_directories 添加特定的头文件搜索路径 ,相当于指定g++编译器的-I参数
include_directories(/opt/ros/humble/include/rclcpp/)
include_directories(/opt/ros/humble/include/rcl/)
include_directories(/opt/ros/humble/include/rcutils/)
include_directories(/opt/ros/humble/include/rcl_yaml_param_parser/)
include_directories(/opt/ros/humble/include/rosidl_runtime_c/)
include_directories(/opt/ros/humble/include/rosidl_typesupport_interface/)
include_directories(/opt/ros/humble/include/rcpputils/)
include_directories(/opt/ros/humble/include/builtin_interfaces/)
include_directories(/opt/ros/humble/include/rmw/)
include_directories(/opt/ros/humble/include/rosidl_runtime_cpp/)
include_directories(/opt/ros/humble/include/tracetools/)
include_directories(/opt/ros/humble/include/rcl_interfaces/)
include_directories(/opt/ros/humble/include/libstatistics_collector/)
include_directories(/opt/ros/humble/include/statistics_msgs/)

# link_directories - 向工程添加多个特定的库文件搜索路径,相当于指定g++编译器的-L参数
link_directories(/opt/ros/humble/lib/)

# add_executable - 生成first_node可执行文件
add_executable(first_node first_ros2_node.cpp)

# target_link_libraries - 为first_node(目标) 添加需要动态链接库,相同于指定g++编译器-l参数
# 下面的语句代替 -lrclcpp -lrcutils
target_link_libraries(first_node rclcpp rcutils)

编译代码

我们一般会创建一个新的目录,运行cmake并进行编译,这样的好处是不会显得那么乱。

1
2
mkdir build
cd build

创建好文件夹,接着运行cmake指令,..代表到上级目录找 CMakeLists.txt

1
cmake ..

运行完cmake你应该可以在build目录下看到cmake自动生成的Makefile了,接着就可以运行make指令进行编译

1
make

运行完上面的指令,就可以在build目录下发现 first_node节点了。

https://cdn.ziyourufeng.eu.org/51hhh/img_bed/main/img/2024/10_30/image_17a39f9dc46e755c003071d3ec7d8b95.png

CMake依赖查找流程

上面我们用g++、make、cmake三种方式来编译ros2的C++节点。用cmake虽然成功了,但是CMakeLists.txt的内容依然非常的臃肿,我们需要将其进一步的简化。

优化CMakeList.txt

将上面的CmakLists.txt改成下面的样子

1
2
3
4
5
6
cmake_minimum_required(VERSION 3.22)
project(first_node)

find_package(rclcpp REQUIRED)
add_executable(first_node first_ros2_node.cpp)
target_link_libraries(first_node rclcpp::rclcpp)

接着继续生成和编译

1
2
cmake ..
make

https://cdn.ziyourufeng.eu.org/51hhh/img_bed/main/img/2024/10_30/image_8b78f23ad3c2c24cd854932e17c7bee8.png

https://cdn.ziyourufeng.eu.org/51hhh/img_bed/main/img/2024/10_30/image_6368201c457e5d0d361d9f21a3c21947.png

find_package查找路径

find_package查找路径对应的环境变量如下。

1
2
3
4
5
<package>_DIR
CMAKE_PREFIX_PATH
CMAKE_FRAMEWORK_PATH
CMAKE_APPBUNDLE_PATH
PATH

打开终端,输入指令:

1
echo $PATH

结果

1
PATH=/opt/ros/humble/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

观察PATH变量,你会发现 /opt/ros/humble/bin赫然在其中,PATH中的路径如果以 binsbin结尾,则自动回退到上一级目录,接着检查这些目录下的

1
2
3
<prefix>/(lib/<arch>|lib|share)/cmake/<name>*/          (U)
<prefix>/(lib/<arch>|lib|share)/<name>*/ (U)
<prefix>/(lib/<arch>|lib|share)/<name>*/(cmake|CMake)/ (U)

cmake找到这些目录后,会开始依次找 <package>Config.cmakeFind<package>.cmake文件。找到后即可执行该文件并生成相关链接信息。

打开 /opt/ros/humble/share/rclcpp/cmake你会发现 rclcppConfig.cmake就在其中。

编写ROS2的Python节点

d2lros2/d2lros2/chapt2/basic新建 second_ros2_node.py,输入下面的内容

1
2
3
4
5
6
7
8
# 导入rclpy库,如果Vscode显示红色的波浪线也没关系
# 我们只是把VsCode当记事本而已,谁会在意记事本对代码的看法呢,不是吗?
import rclpy
from rclpy.node import Node
# 调用rclcpp的初始化函数
rclpy.init()
# 调用rclcpp的循环运行我们创建的second_node节点
rclpy.spin(Node("second_node"))

运行Python节点

打开终端,输入指令

1
2
ls
python3 second_ros2_node.py

打开新的终端,输入

1
ros2 node list

https://cdn.ziyourufeng.eu.org/51hhh/img_bed/main/img/2024/10_30/image_32f14d4eaf52b57848546d0c04008792.png

完美,四行代码写了个ROS2的Python节点。

那么问题来了,我们import rclpy,rclpy到底在哪里?python是如何找到的?

Python包查找流程

Python3运行 import rclpy时候如何找到它的呢?答案是通过环境变量 PYTHONPATH

Ctrl+C打断节点运行,接着输入下面指令

1
echo $PYTHONPATH

结果

1
/opt/ros/humble/lib/python3.10/site-packages:/opt/ros/humble/local/lib/python3.10/dist-packages

你会发现里面有关于humble的python路径,在上面两个目录下找一下rclpy,看看能不能找到rclpy

查找第一个路径

1
ls -l /opt/ros/humble/lib/python3.10/site-packages | grep rclpy

没找到,第二个

1
ls -l /opt/ros/humble/local/lib/python3.10/dist-packages/ | grep rclpy

找到了

1
2
drwxr-xr-x 1 root root 4096 Jun  3 04:45 rclpy
drwxr-xr-x 2 root root 4096 May 23 22:23 rclpy-3.3.4-py3.10.egg-info

删除路径实验

使用 export指令可以重新修改环境变量的值,尝试修改掉 PYTHONPATH中ROS 2 相关内容后之后再运行代码,看看是否还可以导入 rclpy

1
2
3
export PYTHONPATH=/opt/ros/humble/lib/python3.10/site-packages
echo $PYTHONPATH #重新echo查看
python3 second_ros2_node.py

提示如下

1
2
3
4
5
root@490925f19143:~/d2lros2/d2lros2/chapt2/basic# python3 second_ros2_node.py 
Traceback (most recent call last):
File "/root/d2lros2/d2lros2/chapt2/basic/second_ros2_node.py", line 3, in <module>
import rclpy
ModuleNotFoundError: No module named 'rclpy'

请你记住这个报错信息 ModuleNotFoundError: No module named 'xxx',这也是你未来学习过程中可能会经常会遇到的。

下次遇到时,你会想起小鱼这篇文章,然后对它轻蔑的一笑,接着找到这个库所在的目录,把它加到环境里。

ROS节点

ROS2中每一个节点也是只负责一个单独的模块化的功能(比如一个节点负责控制车轮转动,一个节点负责从激光雷达获取数据、一个节点负责处理激光雷达的数据、一个节点负责定位等等)

上面举了一个激光雷达的例子,一个节点负责获取激光雷达的扫描数据,一个节点负责处理激光雷达数据,比如去除噪点。

那节点与节点之间就必须要通信了,那他们之间该如何通信呢?ROS2早已为你准备好了一共四种通信方式:

  • 话题-topics
  • 服务-services
  • 动作-Action
  • 参数-parameters

如何启动一个节点?

知道了节点的概念之后,我们该如何启动一个节点呢?

因为工作空间和包的概念,小鱼放到了下一讲,这里大家跟着小鱼一起运行一个节点,感受一下。

使用指令:

1
ros2 run <package_name> <executable_name>

指令意义:启动 包下的 中的节点。

使用样例:

1
ros2 run turtlesim turtlesim_node

大家可以尝试一下上面的指令,就是我们在第一章中启动小乌龟模拟器的那条指令。

运行之后可以看到一只小乌龟,接下来就可以试试下一节中提到的几个指令来查看节点信息和列表。

通过命令行界面查看节点信息

ROS2的CLI,就是和ROS2相关的命令行操作。什么是命令行界面呢?这里小鱼再讲解一个概念,CLI(Command-Line Interface)和GUI(Graphical User Interface)

  • GUI(Graphical User Interface)就是平常我们说的图形用户界面,大家用的Windows是就是可视化的,我们可以通过鼠标点击按钮等图形化交互完成任务。
  • CLI(Command-Line Interface)就是命令行界面了,我们所用的终端,黑框框就是命令行界面,没有图形化。

很久之前电脑还是没有图形化界面的,所有的交互都是通过命令行实现,就学习机器人而言,命令行操作相对于图形化优势更加明显。

ROS2为我们提供了一系列指令,通过这些指令,可以实现对ROS2相关模块信息的获取设置等操作。

运行节点(常用)

1
ros2 run <package_name> <executable_name>

查看节点列表(常用):

1
ros2 node list

查看节点信息(常用):

1
ros2 node info <node_name>

重映射节点名称

1
ros2 run turtlesim turtlesim_node --ros-args --remap __node:=my_turtle

运行节点时设置参数

1
ros2 run example_parameters_rclcpp parameters_basic --ros-args -p rcl_log_level:=10