水熊虫
水熊虫
不认命,就拼命!
265文章 53分类

机器人开发(一)--ROS组件介绍和使用

上一篇文章我们提到, 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 设置工作空间的环境变量,设置编译项目的环境变量

image-1793452089879

编译完成以后会产生如下的目录。

功能包

每个机器人具备很多功能,如移动控制、视觉感知、自主导航等,这些都可以是功能包.

ros2 pkg create --build-type ament_python xxxx # 创建一个功能包

上面的命令是创建一个python功能包, --build-type 如果是ament_ckmae 就是一个C++功能包。

功能包的结构

image-1793494432551

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/ # 清除工作空间的编译历史

节点

完整的机器人不是一个物理的整体,是需要有多个节点协同工作的的。
image-1793495172208

机器人的身体内部通过计算机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),也就是节点之间的桥梁。
image-1793512112662

上图就是摄像节点到监控节点的实例。

话题的通信模型

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节点之间的单向通信,使用这种异步通信的机制,发布者无法准确的知道时候收到了信息,接下来我们介绍另一种常用的通信机制(服务),可以实现类似你问我答的通信效果。 image-1793514091431 对于服务来说有多种通信模型。我们一一来看。 - 同步通信:如浏览器一直转圈,不会有内容产生,也不允许理由新的动作就是同步通信 - 一对多通信:顾名思义就是服务端是唯一的, 但是谁都可以访问服务端提供的服务。 - 服务接口:和话题类似,服务通信的核心是传递数据,数据变成两部分,一个请求数据一个应当数据,这些数据和话题的消息是一样的,在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开发框架。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×