“ 经常有朋友问如何学习仿真测试,于是想着把自己的一些经验和理解分享出来,希望能有所帮助。不过视野和技术有限,所说不一定对,供大家批评和参考。这是第24篇,ROS2入门。”
首先向大家抱歉,之前的想法是争取能从CARLA系列开始每周发一篇文章,可惜各种琐事缠身,没能成功。最近看到CARLA 0.9.15已经发布了,更是焦急。终于在周末赶出这篇ROS2的入门,供大家参考。
下一步的计划是使用ROS2、基于CARLA中的一些内容,开发一个演示性的自动驾驶算法,便于仿真测试的学习,敬请期待吧。
正文如下:
更详细的内容见ROS2官方文档:
https://docs.ros.org/en/foxy/index.html
ROS2的安装
ROS2的安装步骤如下(系统为Ubuntu 20.04,ROS2版本为Foxy):
(1)设置语言环境支持UTF-8
ROS2的运行需要UTF-8语言环境,Ubuntu 20.04系统默认是支持的,不过一些简易系统环境(如docker)中可能不支持,需要采用下述方法进行检查和安装。
打开一个终端并输入“locale”,若返回包含“LANG=en_US.UTF-8”等字段的内容,则说明当前系统支持UTF-8,若没有的话,需要安装如下命令进行配置并再次输入“locale”进行确认。
sudo apt update && sudo apt install locales
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
(2)设置安装源
安装ROS2前需要指定其安装源,不过在此之前需要先将Ubuntu universe软件库添加到当前系统,可按照如下步骤进行:
添加Ubuntu universe软件库
sudo apt install software-properties-common
sudo add-apt-repository universe
添加ROS2 GPG密钥
sudo apt update && sudo apt install curl -y
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
添加ROS2软件库到源列表
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(. /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
(3)安装
为了确保ROS2安装在最新的系统上,我们先更新软件库和软件,然后安装ROS2 Foxy:
更新软件库和软件
sudo apt update
sudo apt upgrade
安装ROS2 Foxy
sudo apt install ros-foxy-desktop python3-argcomplete
安装开发工具
sudo apt install ros-dev-tools
(4)试运行
按照以上步骤安装好ROS2后,可通过输入如下命令进行确认,若能看到终端输出“ros2 is an extensible command-line tool for ROS 2”字样,说明安装成功。注意,运行ROS2前需要通过“source”命令进行环境设置。
环境设置
source /opt/ros/foxy/setup.bash
运行ROS2
ros2
ROS2的核心概念
为便于对ROS2的理解和使用,下面先对工作空间、功能包、节点、话题等核心概念进行说明,然后通过一个Python编程实例加深对这些概念的理解。
(1)工作空间
工作空间(workspace)可以理解为我们进行开发的工程目录,所有的编程、配置和编译等工作都在该目录下完成。一个常规的工作空间包含如下图所示的目录结构。
工作空间下一般有src、build、install、log四个文件夹及用户自定义的其他文件夹,build、install、log文件夹在编译后自动生成。
src为代码文件夹,下一般为用户开发的各个功能包(package)源代码,功能包是ROS2中组织代码的基本容器,是能完成某项具体功能的相对完整单元,每个功能包可包含一个或多个节点。
build为编译文件夹,保存编译需要的各项配置和编译时生成的各项中间文件。
install为安装文件夹,放置编译生成的可执行文件和local_setup.bash、setup.bash等环境设置文件。
log为日志文件夹,保存编译和运行时的产生的日志。
ROS2中提供了下层工作空间(underlay)和上层工作空间(overlay)的概念。上层工作空间可以依赖和使用下层工作空间中现有的功能包,当功能包有重复或者冲突时,则使用上层工作空间的内容,对上层工作空间进行修改时不影响下层工作空间。一个工作空间可作为多个其他工作空间的下层工作空间,一个工作空间也可以依赖多个下层空间,工作空间之间可以有多层依赖关系,通过下层工作空间和上层工作空间的概念,可实现开发的解耦和重复使用,提高开发效率。如下图所示,workspace_5作为上层工作空间,以workspace_2和workspace_3作为下层工作空间,可以使用这两个工作空间中的功能包;同时,workspace_5也是workspace_6的下层工作空间,从而建立起多层依赖关系。
那么,如何确定工作空间的层次关系呢?这就需要用到install文件夹中的环境配置文件local_setup.bash、setup.bash。local_setup.bash文件用于“激活”当前工作空间,setup.bash文件用于“激活”当前工作空间及当前工作空间依赖的下层工作空间。当前工作空间编译之前“激活”的工作空间,即为当前工作空间的下层工作空间,编译后在install文件夹下生成的setup.bash文件中记录了当前工作工作空间所依赖的下层工作空间信息。
一般将ROS2的安装目录作为最下层的工作空间,在编译用户自己的工作空间之前,通过如下命令将ROS2的安装目录激活:
source /opt/ros/foxy/setup.bash
通过如下命令可以查看当前终端ROS2相关的环境变量:
printenv | grep -i ROS
(2)节点、话题和消息
节点(node)是ROS2中用于实现单一、模块化功能的单元,一个完整的机器人控制系统由许多协同工作的节点组成,不同节点间可以通过话题(topic)、服务(service)和动作(action)等方式实现通信。
节点通过话题进行通信的方式,采用发布-订阅模型,可类比为人通过对讲机通话。人要通过对讲机发布消息时,先设定通话频道,按下通话键,然后使用约定好的语言(英语、普通话、方言等)向全频道进行广播式通话,对通话内容感兴趣的人收听并进行回复;节点要发布信息时,需要建立发布者对象(publisher),在约定好的话题上发布信息,消息格式(message)定义了信息的数据结构,对该话题的信息感兴趣的节点,建立订阅者对象(subscriber)接收信息并进行处理,如下表所示。
话题的发布是广播式的,并不指定需要订阅的节点,由感兴趣的节点自行订阅;一般情况下,发布节点周期性的发布话题,而订阅节点对话题的处理是回调式的,即订阅到话题后才对其进行相应的处理,未订阅到话题时不处理。
每个话题可以被多个节点发布,也可以被多个节点订阅;每个节点可以发布或者订阅多个话题;发布者和接收者的话题名称和消息格式相同时,才能进行通信。
下图给出了节点通过话题进行通信的示例。车辆位置话题/topic_loc由定位节点localization_node发布,并被规划节点planning_node和控制节点control_node订阅;planning_node根据当前位置和目标位置的关系计算得到前方目标轨迹点列表,并通过目标轨迹点话题/topic_path发布;control_node根据当前位置和前方目标轨迹计算得到对车辆的控制命令(如加速度、方向盘转角等),并控制车辆实现对目标轨迹点的跟踪。
(3)服务
节点通过服务通信的方式,采用请求-响应模型,可类比于在餐馆时的客人和服务员的关系。在餐馆吃饭时,客人的筷子掉到地上了,跟服务员说“请帮我拿一双筷子”,然后服务员提供了一双新的筷子;客户端节点需要向服务端节点请求相关信息时,需要向服务端发送请求(request),服务端接收请求后根据事先定义好的逻辑发送相应的响应(response),如下表所示。
服务通信的方式是点对点的,即客户端向服务端发送请求且服务端将响应发送给该客户端;一般情况下,客户端发送请求是事件型的,即在需要的时候才发送请求,而服务端对请求的处理是回调式的,即接收到请求后才对其进行处理,并进行响应,未收到请求时不处理。
某个服务只能由一个服务端来提供,可以由多个客户端请求;每个节点可以向多个服务端节点发送请求,某个节点也可以提供多个服务;客户端和服务端的服务名称和格式相同时,才能进行通信。
下图给出了节点通过服务进行通信的示例。地图节点map_node作为服务端提供了地图服务map_service,在接收到请求request(内容为某个位置的坐标)后,回复相应的响应response(内容为该位置附近200米的地图信息)。预测节点prediction_node和规划节点planning_node在需要某个位置附近的地图信息时,可向地图节点发出请求,并得到需要的信息。
(4)动作
节点通过动作通信的方式,采用客户端-服务端模型,可以看成是话题和服务的结合体,可类比于微波炉的工作过程。将待加热的食物放入微波炉,设置好加热时间后,微波炉开始进行加热并通过定时旋钮的转动(或数字显示)提示剩余时间,方便用户掌握进度,在食物加热结束后会通过声音提醒;节点通过动作通信时,客户端节点首先向服务端节点发送期望目标,服务端接收到后根据目标进行计算(或者控制其他对象),并发布中间状态,等到目标达成后,服务端还会反馈实际结果,如下表所示。
动作可以看成是话题和服务的结合体:客户端发送目标和服务端发送实际结果的过程与服务类似,不过动作的目标在执行过程中可以被取消;服务端发布中间状态的过程与话题类似。动作的通信方式是点对点的,客户端节点向动作服务端节点发送目标,并得到中间状态和实际结果;动作目标的发送一般是事件型的,即在需要的时候才发布。
某个动作只能由一个服务端来提供,并由该服务端实现目标,同一时刻只能执行一个客户端的目标,先执行完一个客户端的动作目标才能执行下一个。客户端和服务端的动作名称和格式相同时,才能进行通信。
下图给出了节点通过动作进行通信的示例。导航节点navigation_node作为导航动作navi_action的服务端,在接收到人机交互节点(车载大屏)hmi_node通过目标服务goal service发出的目标地点后,控制车辆向目标地点行驶,并通过反馈话题/current_pos周期性反馈实时位置,该话题被hmi_node接收后用于在大屏显示的地图上更新车辆位置;当车辆到达目标地点后,navigation_node会通过结果服务向hmi_node发送车辆的实际位置。
ROS2 Python编程实例
下面我们一个发布者节点和订阅者节点的编程实例,说明使用Python开发ROS2节点的步骤,并说明ROS2中一些常用工具的使用方法。
(1)创建工作空间
建议为每个项目都建立一个单独的目录作为工作空间,并且将功能包放置在src文件夹下,我们按照这个原则通过如下命令建立工作空间:
进入用于学习的目录,此处以home文件夹为例
cd ~
创建工作空间
mkdir -p sim_ws/src
激活ROS2安装目录作为下层工作空间(每个终端都需要输入一次)
source /opt/ros/foxy/setup.bash
(2)创建功能包
功能包是ROS2中进行代码管理的基本单元,一般会将完成某项具体功能的所有代码放放置于一个功能包中,便于管理、使用和分享。功能包可以使用C++或者Python开发,这里我们使用Python进行开发,并将编程实例放置于一个功能包中(命名为“py_pub_sub_example”)。命令如下:
进入src目录
cd sim_ws/src
创建功能包
ros2 pkg create --build-type ament_python py_pub_sub_example
可以在终端的日志上看到创建了一系列默认的文件(夹),其功能如下:
与功能包同名的子文件夹:包含“__init__.py”及其他文件,实现了该功能包的功能;
resource文件夹:自动生成的文件夹,无需修改;
test文件夹:测试相关的文件;
package.xml文件:包含该功能包的描述信息,如基础信息和依赖项等;
setup.cfg文件:功能包生成的可执行文件的路径信息,无需修改;
setup.py文件:包含如何安装该功能包的说明,如可执行文件的名称和入口。
(3)创建发布者节点
每个功能包中可以包含多个节点,我们将发布者和订阅者节点都放置在功能包py_pub_sub_example中,下面我们来进行发布者节点的开发。
在sim_ws/src/py_pub_sub_example/py_pub_sub_example文件夹下创建名称为my_publisher.py的文件,并输入下面的代码
1.# 导入python接口及Node类
2.import rclpy
3.from rclpy.node import Node
4.
5.# 导入待使用的消息类型
6.from std_msgs.msg import String
7.
8.
9.class MyPublisher(Node):
10.
11. def __init__(self):
12. # 调用父类Node的构造函数
13. super().__init__("my_publisher")
14.
15. # 创建发布者
16. self.my_pub = self.create_publisher(String, "/my_topic", 10)
17.
18. # 创建发布定时器
19. self.time_step = 0.5
20. self.my_pub_timer = self.create_timer(self.time_step, self.my_pub_run)
21.
22. self.count = 0
23.
24. def my_pub_run(self):
25. # 更新待发布的信息
26. msg = String()
27. msg.data = f"hello wolrd, count = {self.count}"
28.
29. # 发布信息
30. self.my_pub.publish(msg)
31.
32. # 打印状态,更新计数
33. self.get_logger().info(f"pub msg: {msg.data}")
34. self.count += 1
35.
36.
37.def main(args=None):
38. # 初始化rclpy
39. rclpy.init(args=args)
40.
41. # 创建节点的实例化对象
42. my_pub = MyPublisher()
43.
44. # 启动ROS2运行循环
45. rclpy.spin(my_pub)
46.
47. # 销毁节点,关闭rclpy
48. my_pub.destroy_node()
49. rclpy.shutdown()
50.
51.
52.if __name__ == "__main__":
53. main()
代码的第2~3行导入了ROS2的Python接口及Node类,这是每次使用Python进行ROS2编程所必须的;第6行导入了本节点要使用的消息类型(字符串String),大家可根据实际情况导入自己需要使用的消息类型。
代码的第37行开始的main函数是节点的入口,节点的运行分为四个步骤:①初始化rclpy(第39行);②创建节点的实例化对象(第42行);③启动ROS2运行循环,节点按照其定义的内容工作(第45行);④运行结束时销毁节点,并关闭rclpy(第48~49行)。
代码的第9~34行定义了MyPublisher类,该类继承与Node类,在其构造函数__init__中首先要调用父类Node类的构造函数(代码第13行)。发布消息需要按照如下步骤:
创建发布者,如第16行调用create_publisher方法创建了发布者my_pub,create_publisher方法需要三个参数:要发布的消息的类型(此处为字符串String)、话题名称(此处为/my_topic)和消息队列长度(此处设置为10)
创建发布定时器,如第19~20行调用create_time方法创建了定时器my_pub_timer,定时器相当于一个周期运行的触发器,运行周期为create_time方法第一个参数(此处为self.time_step,单位为秒,即为0.5s),触发时调用create_time方法第二个参数指定的回调函数(此处为self.my_pub_run)。
回调函数运行时,更新待发布的信息并发布。本实例中发布一个字符串消息,并用每次递增的计数count标识消息的不同(第26~27行),并使用发布者my_puy的publish方法发布更新后的消息。
(4)创建订阅者节点
在sim_ws/src/py_pub_sub_example/py_pub_sub_example文件夹下创建名称为my_subscriber.py的文件,并输入下述代码。
1.# 导入python接口及Node类
2.import rclpy
3.from rclpy.node import Node
4.
5.# 导入待使用的消息类型
6.from std_msgs.msg import String
7.
8.
9.class MySubscriber(Node):
10.
11. def __init__(self):
12. # 调用父类Node的构造函数
13. super().__init__('my_subscriber')
14.
15. # 创建订阅者
16. self.my_sub = self.create_subscription(
17. String,
18. "/my_topic",
19. self.my_sub_callback,
20. 10)
21.
22. def my_sub_callback(self, msg):
23. self.get_logger().info(f"sub msg: {msg.data}")
24.
25.
26.def main(args=None):
27. # 初始化rclpy
28. rclpy.init(args=args)
29.
30. # 创建节点的实例化对象
31. my_sub = MySubscriber()
32.
33. # 启动ROS2运行循环
34. rclpy.spin(my_sub)
35.
36. # 销毁节点,关闭rclpy
37. my_sub.destroy_node()
38. rclpy.shutdown()
39.
40.
41.if __name__ == "__main__":
42. main()
代码的第1~6行同样导入了ROS2的Python接口、Node类及本节点要使用的消息类型(字符串String)。
代码的第26行开始的main函数是节点的入口,节点的运行的四个步骤与发布者节点中相同。
代码 的第9~23行定义了MySubscriberr类,该类继承与Node类,在其构造函数__init__中首先要调用父类Node类的构造函数(代码第13行)。订阅消息需要按照如下步骤:
创建订阅者,如第16行调用create_subscription方法创建了订阅者my_sub,create_subscriptio方法需要四个参数:要订阅的话题的类型(此处为字符串String)、话题名称(此处为/my_topic)、对订阅到的消息进行处理的回调函数(此处为my_sub_callback)和消息队列长度(此处为10)。
收到订阅话题的内容时,回调函数对其进行处理布。本实例中对收到的内容进行了打印显示。
(5)配置节点
要想使节点能够正确编译和运行还需要对package.xml和setup.py文件进行一些配置。
package.xml文件主要需要对功能包的基础信息和依赖信息进行配置。对于功能包名称(name)、版本(version)、描述(description)、维护者(maintainer)和许可证(license)等描述信息,大家可根据自己的实际情况进行修改,也可以暂不修改。由于我们上面开发的发布者节点中使用到了rclpy和std_msgs,所以需要添加依赖项内容,如下述代码的第10~11行。
1.
2.
3.<package format="3">
4. <name>py_pub_sub_example</name>
5. <version>0.0.0</version>
6. <description>TODO: Package description</description>
7. <maintainer email="adsim.sun@outlook.com">sun</maintainer>
8. <license>TODO: License declaration</license>
9.
10. <exec_depend>rclpy</exec_depend>
11. <exec_depend>std_msgs</exec_depend>
12.
13. <test_depend>ament_copyright</test_depend>
14. <test_depend>ament_flake8</test_depend>
15. <test_depend>ament_pep257</test_depend>
16. <test_depend>python3-pytest</test_depend>
17.
18. <export>
19. <build_type>ament_python</build_type>
20. </export>
21.</package>
setup.py文件中也包含了功能包名称、维护者等基础信息,与package.xml文件中保持一致即可。setup.py文件主要需要修改程序入口信息,并于ROS2知道节点从哪里开始运行,如下述代码的第23行所示,其“=”之前指明了运行时的发布者节点my_publisher的可执行文件名称为“my_pub”,“=”之后指明了程序的入口为py_pub_sub_example功能包之下的my_publisher节点文件中的main函数,第24行则对订阅者节点my_subscriber的内容进行了定义。
1.from setuptools import setup
2.
3.package_name = 'py_pub_sub_example'
4.
5.setup(
6. name=package_name,
7. version='0.0.0',
8. packages=[package_name],
9. data_files=[
10. ('share/ament_index/resource_index/packages',
11. ['resource/' + package_name]),
12. ('share/' + package_name, ['package.xml']),
13. ],
14. install_requires=['setuptools'],
15. zip_safe=True,
16. maintainer='sun',
17. maintainer_email='adsim.sun@outlook.com',
18. description='TODO: Package description',
19. license='TODO: License declaration',
20. tests_require=['pytest'],
21. entry_points={
22. 'console_scripts': [
23. 'my_pub = py_pub_sub_example.my_publisher:main',
24. 'my_sub = py_pub_sub_example.my_subscriber:main',
25. ],
26. },
27.)
(6)编译功能包
ROS2使用colcon工具进行功能包的编译,常用命令如下(大家可输入colcon build -h查看所以命令选项):
colcon build:编译当前工作空间下所有功能包
colcon build --packages-ignore:编译除其后输入的特定功能包之外的功能包(可以输入多个功能包)
colcon build --packages-select:仅编译其后输入的特定功能包(可以输入多个功能包)
colcon build --packages-up-to:仅编译其后输入的特定功能包及所依赖的功能包(可以输入多个功能包)
现在让我们来编译py_pub_sub_example功能包,在终端输入如下命令:
colcon命令需要在工作空间目录输入,所以需要切换到工作空间目录
cd sim_ws
编译功能包
colcon build --packages-select py_pub_sub_example
终端日志会显示编译过程,当最后看到“Finished <<< py_pub_sub_example”说明编译成功。这时可以看到sim_ws文件夹下生成了build、install、log文件夹,其中生成了名称为“py_pub_sub_example”的文件夹。
(7)运行节点
运行节点需要使用“ros2 run”命令,其格式为“ros2 run <package_name> <executable_name>”,即其后需要跟随要运行的功能包名称和可执行文件(节点)名称,我们在setup.py文件中将发布者节点my_publisher的可执行文件名称命名为了“my_pub”。需要注意的是,在使用当前工作空间相关内容之前,需要激活当前工作空间作为上层工作空间。
运行my_publisher节点的命令如下:
激活当前工作空间为上层工作空间
cd sim_ws
source install/local_setup.bash
运行my_publisher节点
ros2 run py_pub_sub_example my_pub
可以在终端看到如下的打印,这与我们定义的相同,说明my_publisher在正常工作:
[INFO] [1700993665.223704308] [my_publisher]: pub msg: hello wolrd, count = 0
[INFO] [1700993665.715716302] [my_publisher]: pub msg: hello wolrd, count = 1
[INFO] [1700993666.215880032] [my_publisher]: pub msg: hello wolrd, count = 2
[INFO] [1700993666.715762377] [my_publisher]: pub msg: hello wolrd, count = 3
打开一个新的终端,运行my_subscriber节点的命令如下:
激活工作空间
cd sim_ws
source /opt/ros/foxy/setup.bash
source install/local_setup.bash
运行my_publisher节点
ros2 run py_pub_sub_example my_sub
可以在终端看到如下的打印,这与我们定义的相同,说明my_subscriber在正常工作:
[1700999390.402580403] [my_subscriber]: sub msg: hello wolrd, count = 0 ] [
[1700999390.885612111] [my_subscriber]: sub msg: hello wolrd, count = 1 ] [
[1700999391.385495054] [my_subscriber]: sub msg: hello wolrd, count = 2 ] [
[1700999391.885446263] [my_subscriber]: sub msg: hello wolrd, count = 3 ] [
(8)使用ros2 node/ros2 topic/rqt工具查看相关信息
“ros2 node”命令可以用来查看当前运行的所有节点的列表或者某个节点所包含的话题、服务和动作等信息,大家可打开一个新的终端,尝试输入如下命令、查看相应的输出:
激活工作空间
cd sim_ws
source /opt/ros/foxy/setup.bash
source install/local_setup.bash
查看当前运行的节点列表
ros2 node list
查看 my_publisher节点相关信息
ros2 node info /my_publisher
“ros2 topic”命令可以用来当前运行发布的所用话题的列表或某个话题的内容、周期等信息,大家可打开一个新的终端,尝试输入如下命令、查看相应的输出:
激活工作空间
cd sim_ws
source /opt/ros/foxy/setup.bash
source install/local_setup.bash
查看当前运行的话题列表
ros2 topic list
查看/my_topic话题的内容
echo /my_topic ros2 topic
查看/my_topic话题的频率
ros2 topic hz /my_topic
大家还可以通过“ros2 node -h”和“ros2 topic -h”命令查看这两个命令的所有参数及其用途。
rqt是ROS2的一个GUI框架,利用其中的插件(plugins)可以通过图形化的方式进行与ROS2相关的操作,如查看服务、发布话题、绘制信号曲线等,启动rqt的方式非常简单,在终端输入“rqt”即可:
激活工作空间
cd sim_ws
source /opt/ros/foxy/setup.bash
source install/local_setup.bash
启动rqt
rqt
刚刚启动的rqt的界面是空白的,可以从其Plugins菜单下选择相关的插件并使用。我们首先选择Plugins→Introspection→Node Graph工具,利用该工具可以查看节点之间通信关系。在my_publisher和my_subscriber运行的情况下,rqt界面如下图所示,从中可以清晰的看到目前有两个节点(my_publisher和my_subscriber)在运行,两者通过话题/my_topic进行通信,且通过话题的箭头方向可以得知my_publisher为发布者,my_subscriber为订阅者。
我们再选择Plugins→Topics→Topic Monitor工具,通过该工具可对感兴趣的话题进行查看。我们点击勾选“/my_topic”话题前的复选框,可以对其类型(Type)、频率(Hz)和内容(Value)等信息进行查看,如下图所示。