前言
在学习了ROS 2官方教程之后,我决定以自导自演的方式,给自己出题来实践所学知识,以加深对ROS 2的各个概念和使用方式的理解。我将把我的学习过程记录在博客中,并将代码放在我的GitHub仓库中https://github.com/YuAndWang/ROS2_Case(当然博客中也会贴上代码),后面有新的案例将会放到对应的章节之后,供大家参考和学习。
欢迎━(`∀´)ノ亻! 大家一起出题,一起探讨!希望我的实践过程对你理解ROS 2有所帮助。如果你有任何问题或建议,欢迎提出!谢谢~~~
11.话题topic
11.1案例1:random_topic
本次的发布者节点非常简单,它每隔一秒发布一个随机数到指定的主题上。
建立功能包
打开一个新的终端, cd 到我的工作空间目录。并且应该在 src 目录中创建包,而不是在工作空间的根目录中创建包。因此,cd 到 my_ws/src,并运行包创建命令:
cd my_ws/src
ros2 pkg create --build-type ament_python random_topic_py
发布者 random_topic_pub.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 王仰旭
@说明: 发布一个随机数,订阅者订阅
"""
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
import random
class RandomPublisher(Node):
def __init__(self):
super().__init__('random_publisher')
self.pub = self.create_publisher(String, 'topic', 10)
timer_period = 1
self.timer = self.create_timer(timer_period, self.timer_callback)
def create_random(self):
a = random.randint(0,100) #产生从0到100的随机数
return str(a)
def timer_callback(self):
msg = String()
msg.data = self.create_random()
self.pub.publish(msg)
self.get_logger().info('Publishing: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
random_publisher = RandomPublisher()
rclpy.spin(random_publisher)
random_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
代码解释说明:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
import random
class RandomPublisher(Node):
def __init__(self):
super().__init__('random_publisher')
self.pub = self.create_publisher(String, 'topic', 10) # 创建一个发布者,使用String消息类型,在名为'topic'的主题上发布消息,队列大小为10(消息类型、话题名、队列长度)
timer_period = 1
self.timer = self.create_timer(timer_period, self.timer_callback) # 创建一个定时器,每隔1秒触发一次timer_callback方法
def create_random(self):
a = random.randint(0,100) # 产生从0到100的随机数
return str(a)
def timer_callback(self):
msg = String() # 创建一个String类型的消息对象
msg.data = self.create_random() # 设置消息对象的数据为随机生成的数值
self.pub.publish(msg) # 发布消息到主题上
self.get_logger().info('Publishing: "%s"' % msg.data) # 记录发布的消息内容到日志
这段代码定义了一个名为RandomPublisher
的类,继承自Node
类。该类用于创建一个ROS节点,它具有以下功能:
- 在构造函数
__init__
中,节点被初始化为random_publisher
,并创建一个发布者(publisher)对象,使用String
消息类型,在名为topic
的主题上发布消息。发布者队列的大小被设置为10,表示最多可以存储10个未处理的消息。其中参数分别为(消息类型、话题名、队列长度)。 - 创建一个定时器,每隔1秒调用一次
timer_callback
方法。 create_random
方法用于生成一个0到100之间的随机数,并将其转换为字符串类型。timer_callback
方法在定时器触发时被调用,它创建一个String
消息对象,将随机数作为消息的数据,然后将消息发布到topic
主题上。同时,通过get_logger().info
方法输出日志,记录发布的消息内容。
以下是主函数的实现:
def main(args=None):
rclpy.init(args=args) # 初始化ROS 2
random_publisher = RandomPublisher() # 创建RandomPublisher对象
rclpy.spin(random_publisher) # 进入主循环,等待节点执行完毕
random_publisher.destroy_node() # 销毁节点
rclpy.shutdown() # 关闭ROS 2
if __name__ == '__main__':
main()
在主函数中,首先调用rclpy.init
初始化ROS 2。然后创建一个RandomPublisher
对象,这将启动一个ROS节点,并执行节点的功能。接下来,使用rclpy.spin
函数进入主循环,等待节点执行完毕。最后,调用destroy_node
方法销毁节点,再调用rclpy.shutdown
关闭ROS 2。
这是一个简单的发布者节点,每秒发布一个随机数到指定的主题上。其他订阅者节点可以订阅相同的主题,接收并处理这些随机数消息。
订阅者 random_topic_sub.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 王仰旭
@说明: 发布一个随机数,订阅者订阅
"""
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class RandomSubscriber(Node):
def __init__(self):
super().__init__('random_subscriber')
self.sub = self.create_subscription(String, 'topic', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = RandomSubscriber()
rclpy.spin(minimal_subscriber)
minimal_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
代码解释说明:
这段代码是一个使用ROS 2编写的订阅者节点,它会订阅指定主题上的消息,并在接收到消息时打印消息内容。下面是对代码不同部分的概述和解释:
- 导入所需的库和模块:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
在这段代码中,我们导入了rclpy
模块以及用于创建节点的Node
类,还导入了用于订阅消息的String
消息类型。
- 定义订阅者节点类:
class RandomSubscriber(Node):
def __init__(self):
super().__init__('random_subscriber')
self.sub = self.create_subscription(String, 'topic', self.listener_callback, 10)
# 创建一个订阅者对象,使用String消息类型,订阅名为'topic'的主题,队列大小为10(消息类型、话题名、订阅者回调函数、队列长度)
在RandomSubscriber
类中,我们继承了Node
类,并在初始化方法中完成以下操作:
- 调用
super().__init__('random_subscriber')
来初始化节点,节点名称为random_subscriber
。 - 使用
create_subscription
方法创建一个订阅者对象,订阅名为topic
的主题,消息类型为String
,队列大小为10。 - 将订阅者对象赋值给
self.sub
。
- 定义回调函数:
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
# 当接收到消息时,回调函数listener_callback被触发,打印接收到的消息内容
listener_callback
是一个回调函数,当接收到消息时被触发。在本例中,它打印接收到的消息内容。
- 主函数:
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = RandomSubscriber()
# 初始化ROS 2并创建一个RandomSubscriber对象
rclpy.spin(minimal_subscriber)
# 进入主循环,等待节点执行完毕
minimal_subscriber.destroy_node()
rclpy.shutdown()
# 销毁节点并关闭ROS 2
if __name__ == '__main__':
main()
在主函数中,我们进行了以下操作:
- 调用
rclpy.init
初始化ROS 2。 - 创建一个
RandomSubscriber
对象,这将启动一个ROS节点,成为订阅者。 - 调用
rclpy.spin
进入主循环,等待节点执行完毕。 - 销毁订阅者节点对象,释放资源。
- 调用
rclpy.shutdown
关闭ROS 2。
这段代码实现了一个简单的订阅者节点,它会订阅指定主题上的消息,并在接收到消息时打印消息内容。你可以在博客中详细介绍每个部分的作用和意义,帮助读者更好地理解你的代码和ROS 2中订阅者节点的工作原理。
配置package.xml文件
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>random_topic_py</name>
<version>0.0.0</version>
<description>random publisher and subscriber using rclpy</description>
<maintainer email="wangyx6432@gmail.com">wang</maintainer>
<license>BSD</license>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
配置setup.py文件
from setuptools import setup
package_name = 'random_topic_py'
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']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='wang',
maintainer_email='wangyx6432@gmail.com',
description='random publisher and subscriber using rclpy',
license='BSD',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'random_topic_pub = random_topic_py.random_topic_pub:main',
'random_topic_sub = random_topic_py.random_topic_sub:main',
],
},
)
检查setup.cfg文件
[develop]
script_dir=$base/lib/random_topic_py
[install]
install_scripts=$base/lib/random_topic_py
setup.cfg文件用于配置ROS 2的包安装和开发时的相关设置。告诉setuptools把编译后的可执行文件放在lib中,因为运行时ros2 run命令将会在那里寻找它们。
编译
cd my_ws/
colcon build
运行
打开新终端,刷新环境
source ~/my_ws/install/setup.bash
ros2 run random_topic_py random_topic_pub
ros2 run random_topic_py random_topic_sub
效果gif
11.2案例2:random_topic_interface
这一次想实现一个服务客户端节点,用于向服务端发送服务请求,比较输入的两个整数的大小,并获取最大值和最小值。
实现过程如下:
自定义服务interface srv
需要继续下面服务的学习,需要先定义该服务所使用的消息格式。用request和response来定义请求和响应,以实现我们的服务逻辑。
实现过程如下:
建立功能包
打开一个新的终端, cd 到我的工作空间目录。并且应该在 src 目录中创建包,而不是在工作空间的根目录中创建包。因此,cd 到 my_ws/src,并运行包创建命令:
cd my_ws/src
ros2 pkg create --build-type ament_cmake mine_interface
根据官网的文档,我这里命名"mine_interface"是新包的名称。它是一个CMake包,但这并不限制你可以在哪种类型的包中使用你的消息和服务。可以在一个CMake包中创建自定义接口,然后在C++或Python节点中使用它。
建立对应文件夹
如果是消息msg
,则在工作空间内建立msg
文件夹,
如果是服务srv
,则在工作空间内建立srv
文件夹,
后面还会学到动作action
,则在工作空间内建立action
文件夹,
可以cd
进入到功能包后,用命令创建文件夹:
cd my_ws/src/mine_interface
mkdir msg
创建自定义消息文件
这里需要创建消息,我在mine_interface
功能包下建立了msg
文件夹,在文件夹中创建了一个名为RandomAverage.msg
的服务文件,并在其中定义了请求和响应的消息格式。以下是文件内容:
int32 random1 # 随机数1
int32 random2 # 随机数2
float32 ave # 平均值
上述内容定义了一个自定义的服务消息类型RandomAverage
,其中包含三个字段:random1
、random2
和ave
,分别表示随机数1、随机数2和平均值。
配置package.xml文件
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>mine_interface</name>
<version>0.0.0</version>
<description>mine interface</description>
<maintainer email="wangyx6432@gmail.com">wang</maintainer>
<license>BSD</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
配置CMakeLists.txt文件
记得这里需要在setup.py
文件中添加入口点(entry points),以便能够通过命令行运行你的节点。
cmake_minimum_required(VERSION 3.5)
project(mine_interface)
# Default to C99
if(NOT CMAKE_C_STANDARD)
set(CMAKE_C_STANDARD 99)
endif()
# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# find dependencies
find_package(ament_cmake REQUIRED)
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/RandomAverage.msg"
)
if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# uncomment the line when a copyright and license is not present in all source files
#set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# uncomment the line when this package is not in a git repo
#set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()
ament_package()
编译
在工作空间中执行编译命令colcon build
,这将会编译你的工作空间中的所有包,包括这个自定义消息包mine_interface
。
cd my_ws/
colcon build
效果
编译成功后,你会在工作空间下的install/mine_interface/share/mine_interface/srv
目录中看到对应的.srv
文件,这意味着你的消息包编译成功了!可以开始使用这些自定义的消息类型来定义你的服务,并在节点中实现相应的功能!
建立功能包
打开一个新的终端, cd 到我的工作空间目录。并且应该在 src 目录中创建包,而不是在工作空间的根目录中创建包。因此,cd 到 my_ws/src,并运行包创建命令:
cd my_ws/src
ros2 pkg create --build-type ament_python random_topic_interface_py
发布者 random_topic_interface_pub.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 王仰旭
@说明: 自定义消息——发布两个随机数及其平均数
"""
import rclpy
from rclpy.node import Node
from mine_interface.msg import RandomAverage
import random
class RandomPublisher(Node):
def __init__(self):
super().__init__('random_interface_pub')
self.pub = self.create_publisher(RandomAverage, 'topic', 10)
timer_period = 1
self.timer = self.create_timer(timer_period, self.timer_callback)
def timer_callback(self):
msg = RandomAverage()
msg.random1 = random.randint(0,100)
msg.random2 = random.randint(0,100)
msg.ave = (msg.random1 + msg.random2) / 2
self.pub.publish(msg)
self.get_logger().info('Publishing: %d, %d, ave: %f' % (msg.random1, msg.random2, msg.ave))
def main(args=None):
rclpy.init(args=args)
node = RandomPublisher()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
订阅者 random_topic_interface_sub.py
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 王仰旭
@说明: 自定义消息——订阅发布者发布的两个随机数及其平均数
"""
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
from mine_interface.msg import RandomAverage # 自定义的服务接口
class RandomSubscriber(Node):
def __init__(self):
super().__init__('random_interface_sub')
self.sub = self.create_subscription(RandomAverage, "topic", self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info('Received: %d, %d, ave: %f' % (msg.random1, msg.random2, msg.ave))
def main(args=None):
rclpy.init(args=args)
node = RandomSubscriber()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
配置package.xml文件
记得这里要加入【mine_interface】,声明你的包依赖于自定义的消息包 mine_interface。
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>compare_srvcli_py</name>
<version>0.0.0</version>
<description>random with interface using rclpy</description>
<maintainer email="wangyx6432@gmail.com">wang</maintainer>
<license>BSD</license>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<depend>mine_interface</depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
配置setup.py文件
记得这里需要在setup.py
文件中添加入口点(entry points),以便能够通过命令行运行你的节点。
from setuptools import setup
package_name = 'random_topic_interface_py'
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']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='wang',
maintainer_email='wangyx6432@gmail.com',
description='random with interface using rclpy',
license='BSD',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'random_topic_interface_pub = random_topic_interface_py.random_topic_interface_pub:main',
'random_topic_interface_sub = random_topic_interface_py.random_topic_interface_sub:main',
],
},
)
配置setup.cfg文件
setup.cfg文件用于配置ROS 2的包安装和开发时的相关设置。检查一下是否正确配置了安装和开发时的脚本目录。这里会告诉setuptools把编译后的可执行文件放在lib中,因为运行时ros2 run命令将会在那里寻找它们。
[develop]
script_dir=$base/lib/random_topic_interface_py
[install]
install_scripts=$base/lib/random_topic_interface_py
编译
cd my_ws/
colcon build
运行
打开新终端,刷新环境
source ~/my_ws/install/setup.bash
ros2 run random_topic_interface_py random_topic_interface_pub
ros2 run random_topic_interface_py random_topic_interface_sub