【ROS2】中级:tf2-编写广播器(Python)

目标:学习如何将机器人的状态广播到 tf2。

教程级别:中级

 时间:15 分钟

 目录

  •  背景

  •  先决条件

  •  任务

    • 1. 编写广播器节点

    • 2. 编写启动文件

    •  3. 构建

    •  4.运行

  •  摘要

 背景

在接下来的两个教程中,我们将编写代码来复现“tf2 入门”教程中的演示。之后,后续教程将专注于使用更高级的 tf2 功能来扩展演示,包括在变换查找和时间旅行中使用超时。

 先决条件

本教程假设您具有 ROS 2 的工作知识,并且您已经完成了 tf2 教程和 tf2 静态广播器教程(Python)的介绍。我们将重用上一个教程中的 learning_tf2_py 包。

在之前的教程中,您学习了如何创建工作区和创建包。

 任务

1. 编写广播器节点

让我们先创建源文件。转到我们在上一教程中创建的 learning_tf2_py 包。在 src/learning_tf2_py/learning_tf2_py 目录中,通过输入以下命令下载示例广播代码:

cxy@ubuntu2404-cxy:~$ cd ~/ros2_ws/src/learning_tf2_py/learning_tf2_py
cxy@ubuntu2404-cxy:~/ros2_ws/src/learning_tf2_py/learning_tf2_py$ wget https://raw.githubusercontent.com/ros/geometry_tutorials/ros2/turtle_tf2_py/turtle_tf2_py/turtle_tf2_broadcaster.py

现在使用您喜欢的文本编辑器打开名为 turtle_tf2_broadcaster.py 的文件。

# 版权 2021 开源机器人基金会
#
# 根据 Apache 许可证 2.0 版(“许可证”)授权;
# 除非符合许可证,否则您不得使用此文件。
# 您可以在以下网址获取许可证副本:
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# 除非适用法律要求或书面同意,否则根据许可证分发的软件
# 是按“原样”分发的,不附带任何明示或暗示的担保或条件。
# 请参阅许可证以了解管理权限和限制的特定语言。


import math  # 导入数学模块


from geometry_msgs.msg import TransformStamped  # 从geometry_msgs模块导入TransformStamped消息类型


import numpy as np  # 导入numpy模块并简写为np


import rclpy  # 导入rclpy模块
from rclpy.node import Node  # 从rclpy.node模块导入Node类


from tf2_ros import TransformBroadcaster  # 从tf2_ros模块导入TransformBroadcaster类


from turtlesim.msg import Pose  # 从turtlesim.msg模块导入Pose消息类型




# 这个函数是简化版的代码,来自
# https://github.com/matthew-brett/transforms3d/blob/f185e866ecccb66c545559bc9f2e19cb5025e0ab/transforms3d/euler.py
# 除了简化之外,这个版本还颠倒了顺序以返回x,y,z,w,这是ROS所偏好的方式。
def quaternion_from_euler(ai, aj, ak):
    ai /= 2.0  # 将ai除以2.0
    aj /= 2.0  # 将aj除以2.0
    ak /= 2.0  # 将ak除以2.0
    ci = math.cos(ai)  # 计算ai的余弦值
    si = math.sin(ai)  # 计算ai的正弦值
    cj = math.cos(aj)  # 计算aj的余弦值
    sj = math.sin(aj)  # 计算aj的正弦值
    ck = math.cos(ak)  # 计算ak的余弦值
    sk = math.sin(ak)  # 计算ak的正弦值
    cc = ci*ck  # 计算ci与ck的乘积
    cs = ci*sk  # 计算ci与sk的乘积
    sc = si*ck  # 计算si与ck的乘积
    ss = si*sk  # 计算si与sk的乘积


    q = np.empty((4, ))  # 创建一个空的四元数数组
    q[0] = cj*sc - sj*cs  # 计算四元数的第一个分量
    q[1] = cj*ss + sj*cc  # 计算四元数的第二个分量
    q[2] = cj*cs - sj*sc  # 计算四元数的第三个分量
    q[3] = cj*cc + sj*ss  # 计算四元数的第四个分量


    return q  # 返回四元数




class FramePublisher(Node):  # 定义FramePublisher类,继承自Node类


    def __init__(self):
        super().__init__('turtle_tf2_frame_publisher')  # 调用父类的构造函数,初始化节点名称为'turtle_tf2_frame_publisher'


        # 声明并获取`turtlename`参数
        self.turtlename = self.declare_parameter(
            'turtlename', 'turtle').get_parameter_value().string_value


        # 初始化变换广播器
        self.tf_broadcaster = TransformBroadcaster(self)


        # 订阅turtle{1}{2}/pose主题,并在每个消息上调用handle_turtle_pose回调函数
        self.subscription = self.create_subscription(
            Pose,  # 消息类型,这里是 Pose 类型
            f'/{self.turtlename}/pose',  # 主题名称,这里是根据乌龟的名字动态生成的主题
            self.handle_turtle_pose,  # 回调函数,当接收到消息时调用这个函数
            1  # 队列大小,表示最多可以缓存多少条消息
            )
        self.subscription  # 防止未使用变量警告


    def handle_turtle_pose(self, msg):
        t = TransformStamped()  # 创建TransformStamped对象


        # 读取消息内容并将其分配给相应的tf变量
        t.header.stamp = self.get_clock().now().to_msg()  # 设置时间戳
        t.header.frame_id = 'world'  # 设置父坐标系ID
        t.child_frame_id = self.turtlename  # 设置子坐标系ID


        # 乌龟只存在于2D,因此我们从消息中获取x和y平移坐标,并将z坐标设置为0
        t.transform.translation.x = msg.x  # 设置x平移坐标
        t.transform.translation.y = msg.y  # 设置y平移坐标
        t.transform.translation.z = 0.0  # 设置z平移坐标为0


        # 出于同样的原因,乌龟只能绕一个轴旋转,这就是为什么我们将x和y的旋转设置为0,并从消息中获取z轴的旋转
        q = quaternion_from_euler(0, 0, msg.theta)  # 计算四元数
        t.transform.rotation.x = q[0]  # 设置四元数的x分量
        t.transform.rotation.y = q[1]  # 设置四元数的y分量
        t.transform.rotation.z = q[2]  # 设置四元数的z分量
        t.transform.rotation.w = q[3]  # 设置四元数的w分量


        # 发送变换
        self.tf_broadcaster.sendTransform(t)




def main():  # 定义主函数
    rclpy.init()  # 初始化ROS客户端库
    node = FramePublisher()  # 创建FramePublisher节点
    try:
        rclpy.spin(node)  # 让节点保持运行,直到被中断
    except KeyboardInterrupt:  # 如果收到键盘中断异常,则跳过
        pass


    rclpy.shutdown()  # 关闭ROS客户端库

de2b7640763c6616fbf6100b5b4a75f0.png

ec96dbd307506bc9839cb6789ec73866.png

1.1 检查代码

现在,让我们来看一下与发布乌龟姿态到 tf2 相关的代码。首先,我们定义并获取一个参数 turtlename ,它指定了一个乌龟的名称,例如 turtle1 或 turtle2 。

self.turtlename = self.declare_parameter(
  'turtlename', 'turtle').get_parameter_value().string_value

随后,节点订阅主题 {self.turtlename}/pose 并在每条传入消息上运行函数 handle_turtle_pose 。

self .subscription = self.create_subscription(
    Pose,
    f'/{self.turtlename}/pose',
    self.handle_turtle_pose,
    1)

现在,我们创建一个 TransformStamped 对象,并为其提供适当的元数据。

  1. 我们需要为正在发布的变换加上一个时间戳,我们将通过调用 self.get_clock().now() 来用当前时间来标记它。这将返回 Node 使用的当前时间。

  2. 然后我们需要设置我们正在创建的 Link 的父框架的名称,在这种情况下是 world 。

  3. 最后,我们需要设置我们正在创建的 Link 的子节点的名称,在这种情况下,这就是乌龟本身的名称。

处理函数为 turtle 姿态消息广播此 turtle 的平移和旋转,并将其作为从框架 world 到框架 turtleX 的变换进行发布。

t = TransformStamped()


# Read message content and assign it to
# corresponding tf variables
t.header.stamp = self.get_clock().now().to_msg()
t.header.frame_id = 'world'
t.child_frame_id = self.turtlename

在这里,我们将 3D 乌龟姿势的信息复制到 3D 变换中。

# Turtle only exists in 2D, thus we get x and y translation
# coordinates from the message and set the z coordinate to 0
t.transform.translation.x = msg.x
t.transform.translation.y = msg.y
t.transform.translation.z = 0.0


# For the same reason, turtle can only rotate around one axis
# and this why we set rotation in x and y to 0 and obtain
# rotation in z axis from the message
q = quaternion_from_euler(0, 0, msg.theta)
t.transform.rotation.x = q[0]
t.transform.rotation.y = q[1]
t.transform.rotation.z = q[2]
t.transform.rotation.w = q[3]

最后我们将构建的转换传递给 TransformBroadcaster 的 sendTransform 方法,由它来负责广播。

# Send the transformation
self.tf_broadcaster.sendTransform(t)
1.2 添加一个入口点 

要允许 ros2 run 命令运行您的节点,您必须将入口点添加到 setup.py (位于 src/learning_tf2_py 目录中)。

在 'console_scripts': 括号之间添加以下行:

'turtle_tf2_broadcaster = learning_tf2_py.turtle_tf2_broadcaster:main',

2. 编写启动文件

现在为这个演示创建一个启动文件。在 src/learning_tf2_py 目录中创建一个 launch 文件夹。使用你的文本编辑器,在 launch 文件夹中创建一个名为 turtle_tf2_demo_launch.py 的新文件,并添加以下几行:

from launch import LaunchDescription  # 从launch模块导入LaunchDescription类
from launch_ros.actions import Node  # 从launch_ros.actions模块导入Node类




def generate_launch_description():  # 定义generate_launch_description函数
    return LaunchDescription([  # 返回一个LaunchDescription对象
        Node(
            package='turtlesim',  # 指定节点所属的包名为'turtlesim'
            executable='turtlesim_node',  # 指定可执行文件名为'turtlesim_node'
            name='sim'  # 指定节点名称为'sim'
        ),
        Node(
            package='learning_tf2_py',  # 指定节点所属的包名为'learning_tf2_py'
            executable='turtle_tf2_broadcaster',  # 指定可执行文件名为'turtle_tf2_broadcaster'
            name='broadcaster1',  # 指定节点名称为'broadcaster1'
            parameters=[
                {'turtlename': 'turtle1'}  # 设置参数'turtlename'的值为'turtle1'
            ]
        ),
    ])
2.1 检查代码 

首先,我们从 launch 和 launch_ros 包中导入所需的模块。应当指出, launch 是一个通用的启动框架(不特定于 ROS 2),而 launch_ros 包含了 ROS 2 特定的内容,比如我们在这里导入的节点。

from launch import LaunchDescription
from launch_ros.actions import Node

现在我们运行节点,启动 turtlesim 模拟并使用我们的 turtle_tf2_broadcaster 节点将 turtle1 状态广播到 tf2。

Node(
    package='turtlesim',
    executable='turtlesim_node',
    name='sim'
),
Node(
    package='learning_tf2_py',
    executable='turtle_tf2_broadcaster',
    name='broadcaster1',
    parameters=[
        {'turtlename': 'turtle1'}
    ]
),
 2.2 添加依赖项 

返回到 learning_tf2_py 目录,该目录中包含 setup.py 、 setup.cfg 和 package.xml 文件。

打开 package.xml 并使用文本编辑器。根据启动文件的导入语句添加以下依赖项:

<exec_depend>launch</exec_depend>
<exec_depend>launch_ros</exec_depend>

此声明在执行其代码时需要额外的 launch 和 launch_ros 依赖项。

确保保存文件。

<?xml version="1.0"?> <!-- XML版本为1.0 -->
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <!-- XML模型的链接和模型类型 -->
<package format="3"> <!-- 包格式为3 -->
  <name>learning_tf2_py</name> <!-- 包名为learning_tf2_py -->
  <version>0.0.0</version> <!-- 版本号为0.0.0 -->
  <description>Learning tf2 with rclpy</description> <!-- 包的描述 -->
  <maintainer email="cxy@126.com">cxy</maintainer> <!-- 维护者的信息 -->
  <license>Apache-2.0</license> <!-- 许可证信息 -->


  <exec_depend>geometry_msgs</exec_depend> <!-- 执行依赖:geometry_msgs -->
  <exec_depend>python3-numpy</exec_depend> <!-- 执行依赖:python3-numpy -->
  <exec_depend>rclpy</exec_depend> <!-- 执行依赖:rclpy -->
  <exec_depend>tf2_ros_py</exec_depend> <!-- 执行依赖:tf2_ros_py -->
  <exec_depend>turtlesim</exec_depend> <!-- 执行依赖:turtlesim -->
  
  <exec_depend>launch</exec_depend> <!-- 执行依赖:launch -->
  <exec_depend>launch_ros</exec_depend> <!-- 执行依赖:launch_ros -->
  
  <test_depend>ament_copyright</test_depend> <!-- 测试依赖:ament_copyright -->
  <test_depend>ament_flake8</test_depend> <!-- 测试依赖:ament_flake8 -->
  <test_depend>ament_pep257</test_depend> <!-- 测试依赖:ament_pep257 -->
  <test_depend>python3-pytest</test_depend> <!-- 测试依赖:python3-pytest -->


  <export>
    <build_type>ament_python</build_type> <!-- 构建类型为ament_python -->
  </export>
</package> <!-- 包定义结束 -->
 2.3 更新 setup.py 

重新打开 setup.py 并添加该行,以便安装来自 launch/ 文件夹的启动文件。 data_files 字段现在应该如下所示:

data_files=[
    ...
    (os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*launch.[pxy][yma]*'))),
],

同时在文件顶部添加适当的导入:

import os
from glob import glob

您可以在本教程中了解更多关于创建启动文件的信息。

import os
from glob import glob
from setuptools import find_packages, setup  # 导入setuptools模块的find_packages和setup函数


package_name = 'learning_tf2_py'  # 定义包名为learning_tf2_py


setup(  # 使用setup函数进行包的设置
    name=package_name,  # 包名
    version='0.0.0',  # 版本号
    packages=find_packages(exclude=['test']),  # 使用find_packages函数找到所有的包,排除名为'test'的包
    data_files=[  # 数据文件
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),  # 资源文件
        ('share/' + package_name, ['package.xml']),  # 包的XML描述文件
        (os.path.join('share', package_name, 'launch'), glob(os.path.join('launch', '*launch.[pxy][yma]*'))),  # launch文件
    ],
    install_requires=['setuptools'],  # 安装依赖,需要setuptools
    zip_safe=True,  # 设置zip_safe为True,表示此包可以安全地作为zip文件进行分发
    maintainer='cxy',  # 维护者名字
    maintainer_email='cxy@126.com',  # 维护者邮箱
    description='TODO: Package description',  # 包的描述
    license='Apache-2.0',  # 许可证信息
    tests_require=['pytest'],  # 测试依赖,需要pytest
    entry_points={  # 入口点
        'console_scripts': [  # 控制台脚本
          'static_turtle_tf2_broadcaster = learning_tf2_py.static_turtle_tf2_broadcaster:main',  # 静态turtle_tf2广播器的主函数
          'turtle_tf2_broadcaster = learning_tf2_py.turtle_tf2_broadcaster:main',  # turtle_tf2广播器的主函数
        ],
    },
)

 3. 构建

在工作区的根目录运行 rosdep 以检查缺失的依赖项。

rosdep install -i --from-path src --rosdistro jazzy -y

仍在工作区的根目录下,构建您的包:

colcon build --packages-select learning_tf2_py

打开一个新的终端,导航到工作区的根目录,然后加载设置文件:

. install/setup.bash

4. 运行

现在运行启动文件,它将启动 turtlesim 仿真节点和 turtle_tf2_broadcaster 节点:

ros2 launch learning_tf2_py turtle_tf2_demo_launch.py

在第二个终端窗口中输入以下命令:

ros2 run turtlesim turtle_teleop_key

您现在将看到 turtlesim 模拟已经开始,您可以控制一只乌龟。

2eba59988fb844a40825f0b73da90c79.png

现在,使用 tf2_echo 工具来检查海龟姿势是否真的正在向 tf2 广播:

ros2 run tf2_ros tf2_echo world turtle1

这应该会显示第一只乌龟的姿势。使用箭头键绕着乌龟开(确保你的 turtle_teleop_key 终端窗口是活跃的,不是你的模拟器窗口)。在你的控制台输出中,你会看到类似这样的东西:

cxy@ubuntu2404-cxy:~$ ros2 run tf2_ros tf2_echo world turtle1
[INFO] [1720690130.610127107] [tf2_echo]: Waiting for transform world ->  turtle1: Invalid frame ID "world" passed to canTransform argument target_frame - frame does not exist
At time 1720690131.206094840
- Translation: [3.543, 6.060, 0.000]
- Rotation: in Quaternion [0.000, 0.000, 0.982, -0.188]
- Rotation: in RPY (radian) [0.000, -0.000, -2.763]
- Rotation: in RPY (degree) [0.000, -0.000, -158.319]
- Matrix:
 -0.929  0.369  0.000  3.543
 -0.369 -0.929  0.000  6.060
  0.000  0.000  1.000  0.000
  0.000  0.000  0.000  1.000

如果您运行 tf2_echo 以实现 world 和 turtle2 之间的转换,您不应该看到任何转换,因为第二只乌龟还没有出现。然而,一旦我们在下一个教程中添加了第二只乌龟, turtle2 的姿态将会广播到 tf2。

 摘要

在本教程中,您学习了如何将机器人的姿态(乌龟的位置和方向)广播到 tf2,以及如何使用 tf2_echo 工具。为了实际使用广播到 tf2 的变换,您应该继续学习下一教程,关于创建 tf2 监听器。

f6927cd8294cf1929f51b3a0116e5460.png

  • 10
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值