上一篇文章我们提到, ROS开发会有以下核心的元素。
・⼯作空间(Workspace):开发过程的⼤本营,是放置各种开发⽂件的地⽅。
・功能包(Package):功能源码的聚集地,⽤于组织某⼀机器⼈功能。
・节点(Node):机器⼈的⼯作细胞,是代码编译⽣成的⼀个可执⾏⽂件。
・话题(Topic):节点间传递数据的桥梁,周期性传递各功能之间的信息。
・服务(Service):节点间的 “你问我答”,⽤于某些机器⼈功能和参数的配置。
・通信接口(Interface):数据传递的标准结构,规范了机器⼈的各种数据形态。
・参数(Parameter):机器⼈系统的全局字典,可定义或查询机器⼈的配置参数。
・动作(Action):完整⾏为的流程管理,控制机器⼈完成某些动作。
・分布式通信(DistributedCommunication):多计算平台的任务分配,实现快速组⽹。
・DDS(DataDistributionService):机器⼈的神经⽹络,完成数据的⾼效安全传送。
结下来我们就按照这个核心的概念层层的解析ROS的开发,了解并精通这个新的领域。
基础环境安装
我们的项目都放到robot的目录下面,我们切到这个目录下面后进行一系列操作, robot目录就是我们的⼯作空间。接下来我们在这个工作空间开始进行项目安装。
rosdep install --from-paths src --ignore-src -r -y # 使用rosdep自动安装src下的所有功能
sudo apt-get install python3-colcon-ros # 安装编译器
colcon bulid # 编译项目, 在robot目录下
source install/setup.bash 设置工作空间的环境变量,设置编译项目的环境变量
编译完成以后会产生如下的目录。
功能包
每个机器人具备很多功能,如移动控制、视觉感知、自主导航等,这些都可以是功能包.
ros2 pkg create --build-type ament_python xxxx # 创建一个功能包
上面的命令是创建一个python功能包, --build-type 如果是ament_ckmae 就是一个C++功能包。
功能包的结构
package.xml 包含的是功能包的版本描述、依赖声明等重要信息,这些信息对于包的正确构建和分发至关重要。
setup.py 文件是Python功能特有的,用于描述如何安装和分发该功能,包含元数据和依赖关系等。
编译功能包
Ros2编译功能包使用的是 colcon 编译器。
colcon build # 编译工作空间的功能包,目前在robot文件夹
sudo apt install python3-colcon-extensisons # 安装colcon的拓展功能
如果安装了colcon的拓展功能,可以通过如下的命令可以单独编译指定的功能包。
colcon build --packages-select <package_name> # 只是编译指定的功能包
colcon build --packages-up_to <package_name> # 只是编译指定的功能包以及相应的依赖
rm -rf install/ build/ log/ # 清除工作空间的编译历史
节点
完整的机器人不是一个物理的整体,是需要有多个节点协同工作的的。
机器人的身体内部通过计算机A当成眼睛,通过计算机B当成腿,这些功能甚至分布在不同的计算机中,这些就是机器人的细胞,这些节点相互写作就形成了一个完整的机器人系统。
节点的特点
- 每个节点都是一个进程
- 每个节点都是一个可以独立运行的可自行文件
- 每个节点可以使用不同的编程语言
- 每个节点可以分布式的运行在不同的主机板上
- 每个节点需要有独立的名称。
import rclpy
from rclpy.node import Node
import time
class HelloWorld(Node):
def __init__(self, name):
super().__init__(name)
while rclpy.ok(): # 判断系统正常运行
self.get_logger().info(f"Hello World {name}") # 日志输出
time.sleep(1) # 休眠控制循环时间
def main(args=None):
rclpy.init(args=args)
node = HelloWorld("node_hello_world") # 节点对象初始化(节点名不能有空格)
rclpy.spin(node) # 循环等待推出
node.destroy_node() # 销毁节点实例
rclpy.shutdown() # 关闭ROS2接口
if __name__ == '__main__':
main()
在setup.py 内配置入口。
from setuptools import find_packages, setup
package_name = 'learning_pkg_python'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']),
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='tcliuyong',
maintainer_email='583633876@qq.com',
description='TODO: Package description',
license='TODO: License declaration',
extras_require={
'test': [
'pytest',
],
},
entry_points={
'console_scripts': [
'node_helloworld_class = learning_pkg_python.node_helloworld_class:main',
],
},
)
编译整个公共空间
colcon build
ros2 run learning_pkg_python node_helloworld_class # 执行代码
话题
节点是实现机器人的多种功能,但是这个功能并不是独立的,他们之间要产生关系才能构建一个完整的机器人,这种联系的方式就是话题(Topic),也就是节点之间的桥梁。
上图就是摄像节点到监控节点的实例。
话题的通信模型
RIS中话题的通信模型是基于DDS的发布和订阅的,话题的数据的传输就是从一个节点到另一个节点,发送数据的对象被成为发布者,接受者被称为订阅者,每个话题都需要一个名字,传输数据也需要固定的数据类型。ROS2基于发布和订阅的模型有以下几个特点。
- 多对多的通信, 也就是一个节点可以订阅多个节点的服务。
- 异步通信,话题通信还能进行异步通信,主要是指发布者发布数据后不知道接受者什么时间能够接收到。
- 消息之间传递有确定的接口协议。
话题通信的demo
先来编写发布者的应用程序。
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class HelloWorldPub(Node):
def __init__(self, name):
super().__init__(name)
self.pub = self.create_publisher(String, 'chatter', 10)
self.timer = self.create_timer(0.5, self.timer_callback) # 注意: 不能叫 self.timers,那是 Node 的保留属性
def timer_callback(self):
msg = String()
msg.data = "Hello World"
self.pub.publish(msg)
self.get_logger().info('Hello World')
def main(args=None):
rclpy.init(args=args)
node = HelloWorldPub('hello_world')
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
再来编写订阅者的程序
import rclpy
from rclpy.node import Node
import time
from std_msgs.msg import String
class HelloWorldSub(Node):
def __init__(self, name):
super().__init__(name)
self.sub=self.create_subscription(String, "chatter", self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'收到消息: {msg.data}') # 打印收到的内容,才能看到通信效果
def main(args=None):
rclpy.init(args=args)
node = HelloWorldSub('hello_world_SUB')
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
‵‵‵
运行的时候需要开启两个终端, 不要忘记要重新编译项目 colcon build的命令。然后分别启动下面两个命令。
```bash
ros2 run learning_pkg_python topic_helloworld_sub
ros2 run learning_pkg_python topic_helloworld_pub
话题的操作命令
ros2 topic list
ros2 topic info <topic_name> # 查看话题信息
ros2 topic hz <topic_name> # 查看话题频率
ros2 topic bw <topic_name> # 查看话题带宽
ros2 topic echo <topic_name> # 查看话题数据
ros2 topic pub <topic_name> <msg_data> # 发布话题消息
服务
话题通信可以实现多个ROS2节点之间的单向通信,使用这种异步通信的机制,发布者无法准确的知道时候收到了信息,接下来我们介绍另一种常用的通信机制(服务),可以实现类似你问我答的通信效果。
对于服务来说有多种通信模型。我们一一来看。
- 同步通信:如浏览器一直转圈,不会有内容产生,也不允许理由新的动作就是同步通信
- 一对多通信:顾名思义就是服务端是唯一的, 但是谁都可以访问服务端提供的服务。
- 服务接口:和话题类似,服务通信的核心是传递数据,数据变成两部分,一个请求数据一个应当数据,这些数据和话题的消息是一样的,在ROS2的框架里, 话题是.msg文件定义, 服务用.srv定义
服务端代码
客户端代码
import sys
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts
class AddTwoClient(Node):
def __init__(self, name):
super().__init__(name)
# 创建客户端:接口类型和服务名都要与服务端完全一致
self.client = self.create_client(AddTwoInts, 'add_two_ints')
def call(self, a, b):
# 1. 先等服务端上线(服务通信必须先有服务端在,这和话题不同)
while not self.client.wait_for_service(timeout_sec=1.0):
self.get_logger().info('等待服务端上线...')
# 2. 构造请求并发送(异步调用,返回一个 future)
request = AddTwoInts.Request()
request.a = a
request.b = b
future = self.client.call_async(request)
# 3. 阻塞等待这一次"一问一答"完成
rclpy.spin_until_future_complete(self, future)
return future.result().sum
def main(args=None):
rclpy.init(args=args)
node = AddTwoClient('add_two_client')
# 支持命令行传参: ros2 run ... 3 5 (不传就用默认值)
a = int(sys.argv[1]) if len(sys.argv) > 1 else 3
b = int(sys.argv[2]) if len(sys.argv) > 2 else 5
result = node.call(a, b)
node.get_logger().info(f'问: {a} + {b} = ? 服务端答: {result}')
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
服务端代码
import rclpy
from rclpy.node import Node
# 服务接口:官方自带的 a+b 求和(请求: a,b → 响应: sum)
from example_interfaces.srv import AddTwoInts
class AddTwoServer(Node):
def __init__(self, name):
super().__init__(name)
# 创建服务端:接口类型、服务名、收到请求后的回调函数
# 服务名 'add_two_ints' 必须和客户端一致才能被找到
self.server = self.create_service(AddTwoInts, 'add_two_ints', self.handle_request)
def handle_request(self, request, response):
# request 里有客户端传来的 a 和 b,把结果填进 response 再返回
response.sum = request.a + request.b
self.get_logger().info(f'收到请求: {request.a} + {request.b},回答: {response.sum}')
return response # 必须返回 response,客户端才能收到
def main(args=None):
rclpy.init(args=args)
node = AddTwoServer('add_two_server')
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
运行这个命令
ros2 run learning_pkg_python service_helloworld_server # 服务端
ros2 run learning_pkg_python service_helloworld_client 3 5 # 客户端
总而言之
这里我们完成了ROS2的几个关键概念的理解,后续我们还需继续学习ROS2开发框架。