URDF统一机器人建模语言

URDF(Unified Robot Description Format)统一机器人描述格式,URDF使用XML格式描述机器人文件。

XML是 被设计用来传输和存储数据的可扩展标记语言,注意语言本身是没有含义的,只是规定了其数据格式

比如说下面这段信息:

<robot name="阿童木">
<link name="大手臂">具体的描述</link>
<joint name="胳膊肘">具体的描述</joint>
<link name="小手臂">具体的描述</link>
</robot>

在线代码格式化

XML格式注释: 标签: robot link

robot标签的属性 name: <robot name="fishbot"></robot>

robot标签的子标签 link: <robot name="fishbot"><link name="base_link"></link>link></robot>

使用XML定义的一个最简单的URDF模型可以像下面这样

<?xml version="1.0"?>
<robot name="flowerbot">
  <link name="base_link">
    <visual>
      <geometry>
        <cylinder length="0.18" radius="0.06"/>
      </geometry>
    </visual>
  </link>
</robot>

接着我们从下面四个方面介绍URDF:

  1. URDF的组成介绍
  2. URDF-Link介绍
  3. URDF-Joint介绍
  4. 创建一个简单的URDF并在RVIZ2中可视化

URDF的组成介绍

一般情况下,URDF由声明信息和两种关键组件共同组成

声明信息

声明信息包含两部分,第一部分是xml的声明信息,放在第一行 第二部分是机器人的声明,通过robot标签就可以声明一个机器人模型

<?xml version="1.0"?>
<robot name="flowerbot">
     <link></link>
     <joint></joint>
  ......
</robot>

两种关键组件(Joint&Link)

以平衡车为例,可以简化为如下五个部件组成

我们把左轮,右轮、支撑轮子,IMU和雷达部件称为机器人的Link

Link和Link之间的连接部分称之为Joint关节

Link介绍

通过link标签即可声明一个link, 属性name指定部件名字

  <link name="base_link">

  </link>

通过两行代码就可以定义好base_link,但现在的base_link是空的,我们还要声明我们的base_link长什么样,通过visual子标签就可以声明出来机器人的visual形状。

  <!-- base link -->
  <link name="base_link">
      <visual>
      <origin xyz="0 0 0.0" rpy="0 0 0"/>
      <geometry>
        <cylinder length="0.12" radius="0.10"/>
      </geometry>
    </visual>
  </link>

link标签定义

link的子标签列表

visual 显示形状
    <geometry> (几何形状)
        <box> 长方体
            标签属性: size - 长宽高
            举例:<box size="1 1 1" />
        <cylinder> 圆柱体
            标签属性: radius - 半径 length - 高度
            举例:<cylinder radius="1" length="0.5"/>
        sphere 球体
            属性:radius - 半径
            举例:<sphere radius="0.015"/>
        mesh 第三方导出的模型文件
            属性:filename
            举例: <mesh filename="package://robot_description/meshes/base_link.DAE"/>
    origin (可选:默认在物体几何中心)
            属性 xyz默认为零矢量 rpy弧度表示的翻滚、俯仰、偏航
            举例:<origin xyz="0 0 0" rpy="0 0 0" />
    material 材质
            属性 name 名字
        color
            属性 rgba a代表透明度
            举例:<material name="white"><color rgba="1.0 1.0 1.0 0.5" /> </material>
collision 碰撞属性
inertial 惯性参数 质量等

Joint介绍

joint为机器人关节,机器人关节用于连接两个机器人部件,主要写明父子关系

  • 父子之间的连接类型,包括是否固定的,可以旋转的等

  • 父部件名字

  • 子部件名字

  • 父子之间相对位置

  • 父子之间的旋转轴,绕哪个轴转

比如我们再建立一个雷达部件laser_link,然后将laser_link固定到base_link

<?xml version="1.0"?>
<robot name="fishbot">
    
  <!-- base link -->
  <link name="base_link">
      <visual>
      <origin xyz="0 0 0.0" rpy="0 0 0"/>
      <geometry>
        <cylinder length="0.12" radius="0.10"/>
      </geometry>
    </visual>
  </link>
    
  <!-- laser link -->
  <link name="laser_link">
      <visual>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <geometry>
        <cylinder length="0.02" radius="0.02"/>
      </geometry>
      <material name="black">
          <color rgba="0.0 0.0 0.0 0.5" /> 
      </material>
    </visual>
  </link>
    
  <!-- laser joint -->
    <joint name="laser_joint" type="fixed">
        <parent link="base_link" />
        <child link="laser_link" />
        <origin xyz="0 0 0.075" />
    </joint>

</robot>

joint标签详解

joint属性

name 关节的名称
type 关节的类型
    revolute: 旋转关节,绕单轴旋转,角度有上下限,比如舵机0-180
    continuous: 旋转关节,可以绕单轴无限旋转,比如自行车的前后轮
    fixed: 固定关节,不允许运动的特殊关节
    prismatic: 滑动关节,沿某一轴线移动的关节,有位置极限
    planer: 平面关节,允许在xyz,rxryrz六个方向运动
    floating: 浮动关节,允许进行平移、旋转运动

joint的子标签

parent 父link名称
    <parent link="base_link" />
child子link名称
    <child link="laser_link" />
origin 父子之间的关系xyz rpy
    <origin xyz="0 0 0.014" />
axis 围绕旋转的关节轴
    <axis xyz="0 0 1" />

RVIZ2可视化移动机器人模型

URDF可视化的步骤如下:

  1. 建立机器人描述功能包
  2. 建立urdf文件夹编写urdf文件
  3. 建立launch文件夹,编写launch文件
  4. 修改setup.py配置,编译测试

建立功能包

轻车熟路,先创建一个chapt8_ws工作空间,然后建立功能包,包的类型选ament_python

ros2 pkg create fishbot_description --build-type ament_python

建立URDF文件

建立URDF文件夹,创建urdf文件

cd fishbot_description && mkdir urdf 
touch fishbot_base.urdf

完成后src下的目录结构:

├── fishbot_description
│   ├── __init__.py
├── package.xml
├── setup.cfg
├── setup.py
└── urdf
    └── fishbot_base.urdf

编辑fishbot_base.urdf

<?xml version="1.0"?>
<robot name="fishbot">
    
  <!-- base link -->
  <link name="base_link">
      <visual>
      <origin xyz="0 0 0.0" rpy="0 0 0"/>
      <geometry>
        <cylinder length="0.12" radius="0.10"/>
      </geometry>
    </visual>
  </link>
    
  <!-- laser link -->
  <link name="laser_link">
      <visual>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <geometry>
        <cylinder length="0.02" radius="0.02"/>
      </geometry>
      <material name="black">
          <color rgba="0.0 0.0 0.0 0.8" /> 
      </material>
    </visual>
  </link>
    
  <!-- laser joint -->
    <joint name="laser_joint" type="fixed">
        <parent link="base_link" />
        <child link="laser_link" />
        <origin xyz="0 0 0.075" />
    </joint>

</robot>

建立launch文件

在目录src/fishbot_description下创建launch文件夹并在其下新建display_rviz2.launch.py文件

mkdir launch
touch display_rviz2.launch.py

完成后的目录结构:

├── fishbot_description
│   ├── __init__.py
├── launch
│   └── display_rviz2.launch.py
├── package.xml
├── setup.cfg
├── setup.py
└── urdf
    └── fishbot_base.urdf
import os
from launch import LaunchDescription
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare


def generate_launch_description():
    package_name = 'fishbot_description'
    urdf_name = "fishbot_base.urdf"

    ld = LaunchDescription()
    pkg_share = FindPackageShare(package=package_name).find(package_name) 
    urdf_model_path = os.path.join(pkg_share, f'urdf/{urdf_name}')

    robot_state_publisher_node = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        arguments=[urdf_model_path]
        )

    joint_state_publisher_node = Node(
        package='joint_state_publisher_gui',
        executable='joint_state_publisher_gui',
        name='joint_state_publisher_gui',
        arguments=[urdf_model_path]
        )

    rviz2_node = Node(
        package='rviz2',
        executable='rviz2',
        name='rviz2',
        output='screen',
        )

    ld.add_action(robot_state_publisher_node)
    ld.add_action(joint_state_publisher_node)
    ld.add_action(rviz2_node)

    return ld

想要可视化模型需要三个节点参与

  • joint_state_publisher_gui 负责发布机器人关节数据信息,通过joint_states话题发布
  • robot_state_publisher_node 负责发布机器人模型信息robot_description,并将joint_states数据转换tf信息发布
  • rviz2_node负责显示机器人的信息

这里我们用到了joint_state_publisher_gui和robot_state_publisher两个包,如果你的系统没有安装这两个包,可以手动安装:

sudo apt install ros-$ROS_DISTRO-joint-state-publisher-gui ros-$ROS_DISTRO-robot-state-publisher

joint_state_publisher_gui,还有一个兄弟叫做joint_state_publisher两者区别在于joint_state_publisher_gui运行起来会跳出一个界面,通过界面可以操作URDF中能动的关节

修改setup.py

导入头文件

from glob import glob
import os

加入目录安装

('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),

完整

from setuptools import find_packages, setup
from glob import glob
import os

package_name = 'fishbot_description'

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']),
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='shios',
    maintainer_email='shios@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
        ],
    },
    
)

编译测试

编译

colcon build

运行测试

source install/setup.bash
ros2 launch fishbot_description display_rviz2.launch.py

添加robotmodel模块,分别选择link名称如下,即可看到机器人的模型显示

创建一个两轮差速模型

机器人除了雷达之外,还需要IMU加速度传感器以及可以驱动的轮子,所以我们还需要再创建两个差速驱动轮和一个支撑轮。

  1. IMU传感器部件与关节
  2. 左轮子部件与关节
  3. 右轮子部件与关节
  4. 支撑轮子部件与关节

添加IMU传感器

修改颜色

透明度修改只需要在base_link中添加material

<link name="base_link">
    <visual>
        <origin xyz="0 0 0.0" rpy="0 0 0"/>
    <geometry>
        <cylinder length="0.12" radius="0.10"/>
    </geometry>
    <material name="blue">
        <color rgba="0.1 0.1 1.0 0.5" /> 
    </material>
    </visual>
</link>

添加imu

  <link name="imu_link">
      <visual>
      <origin xyz="0 0 0.0" rpy="0 0 0"/>
      <geometry>
            <box size="0.02 0.02 0.02"/>
      </geometry>
    </visual>
  </link>

  <!-- imu joint -->
  <joint name="imu_joint" type="fixed">
      <parent link="base_link" />
      <child link="imu_link" />
      <origin xyz="0 0 0.02" />
  </joint>

添加右轮

添加关节

关节名称为right_wheel_link

轮子的宽为4cm,直径为6.4cm,几何形状是个圆柱体,所以geometry配置如下:

<geometry>
    <cylinder length="0.04" radius="0.032"/>
</geometry>

需要注意的是,圆柱默认的朝向是向上的

我们可通过origin的rpy改变轮子的旋转角度,让其绕x轴旋转pi/2,所以origin的配置为

<origin xyz="0 0 0" rpy="1.57079 0 0"/>

颜色换黑色,可以得到下面的配置:

  <link name="right_wheel_link">
      <visual>
        <origin xyz="0 0 0" rpy="1.57079 0 0"/>
        <geometry>
          <cylinder length="0.04" radius="0.032"/>
        </geometry>
          <material name="black">
            <color rgba="0.0 0.0 0.0 0.5" /> 
          </material>
      </visual>
  </link>

添加joint

我们把左轮子的中心固定在机器人左后方

需要注意的是origin和axis值的设置

先看origin

因为base_link的高度是0.12,我们

  • z表示child相对parent的z轴上的关系,想将轮子固定在机器人的下表面,所以origin的z向下偏移0.12/2=0.06m(向下符号为负)

  • y表示child相对parent的y轴上的关系,base_link的半径是0.10,所以我们让轮子的y轴向负方向偏移0.10m(向左符号为负)

  • x表示child相对parent的x轴上的关系,向后偏移则是x轴向后进行偏移,我们用个差不多的值0.02m(向后符号为负)

再看axis

轮子是会转动的,那应该按照哪个轴转动呢?从上图可以看出是绕着y轴的逆时针方向,所以axis的设置为:

<axis xyz="0 1 0" />
  <joint name="right_wheel_joint" type="continuous">
      <parent link="base_link" />
      <child link="right_wheel_link" />
      <origin xyz="-0.02 -0.10 -0.06" />
      <axis xyz="0 1 0" />
  </joint>

添加左轮

左轮就是右轮的映射,不再赘述

  <link name="left_wheel_link">
      <visual>
        <origin xyz="0 0 0" rpy="1.57079 0 0"/>
        <geometry>
          <cylinder length="0.04" radius="0.032"/>
        </geometry>
          <material name="black">
            <color rgba="0.0 0.0 0.0 0.5" /> 
          </material>
      </visual>
  </link>
    
  <joint name="left_wheel_joint" type="continuous">
      <parent link="base_link" />
      <child link="left_wheel_link" />
      <origin xyz="-0.02 0.10 -0.06" />
      <axis xyz="0 1 0" />
  </joint>

添加支撑轮

支撑轮子固定在机器人的前方,用个球体,半径用0.016m,小球的直径为0.032m与左右轮子半径相同,然后向下偏移0.016+0.06=0.076m,向下值为负,同时把支撑论向前移动一些,选个0.06m

<link name="caster_link">
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <geometry>
          <sphere radius="0.016"/>
      </geometry>
        <material name="black">
          <color rgba="0.0 0.0 0.0 0.5" /> 
        </material>
    </visual>
  </link>
    
  <joint name="caster_joint" type="fixed">
      <parent link="base_link" />
      <child link="caster_link" />
      <origin xyz="0.06 0.0 -0.076" />
  </joint>

最终URDF文件

<?xml version="1.0"?>
<robot name="fishbot">
    
  <!-- base link -->
  <link name="base_link">
      <visual>
      <origin xyz="0 0 0.0" rpy="0 0 0"/>
      <geometry>
        <cylinder length="0.12" radius="0.10"/>
      </geometry>
      <material name="blue">
          <color rgba="0.1 0.1 1.0 0.5" /> 
      </material>
    </visual>
  </link>
    
  <!-- laser link -->
  <link name="laser_link">
      <visual>
      <origin xyz="0 0 0" rpy="0 0 0"/>
      <geometry>
        <cylinder length="0.02" radius="0.02"/>
      </geometry>
      <material name="black">
          <color rgba="0.0 0.0 0.0 0.5" /> 
      </material>
    </visual>
  </link>
    
  <!-- laser joint -->
  <joint name="laser_joint" type="fixed">
      <parent link="base_link" />
      <child link="laser_link" />
      <origin xyz="0 0 0.075" />
  </joint>

  <link name="imu_link">
      <visual>
      <origin xyz="0 0 0.0" rpy="0 0 0"/>
      <geometry>
            <box size="0.02 0.02 0.02"/>
      </geometry>
    </visual>
  </link>

  <!-- imu joint -->
  <joint name="imu_joint" type="fixed">
      <parent link="base_link" />
      <child link="imu_link" />
      <origin xyz="0 0 0.02" />
  </joint>


  <link name="left_wheel_link">
      <visual>
        <origin xyz="0 0 0" rpy="1.57079 0 0"/>
        <geometry>
          <cylinder length="0.04" radius="0.032"/>
        </geometry>
          <material name="black">
            <color rgba="0.0 0.0 0.0 0.5" /> 
          </material>
      </visual>
  </link>
    
  <joint name="left_wheel_joint" type="continuous">
      <parent link="base_link" />
      <child link="left_wheel_link" />
      <origin xyz="-0.02 0.10 -0.06" />
      <axis xyz="0 1 0" />
  </joint>

  <link name="right_wheel_link">
      <visual>
        <origin xyz="0 0 0" rpy="1.57079 0 0"/>
        <geometry>
          <cylinder length="0.04" radius="0.032"/>
        </geometry>
          <material name="black">
            <color rgba="0.0 0.0 0.0 0.5" /> 
          </material>
      </visual>
  </link>
    
  <joint name="right_wheel_joint" type="continuous">
      <parent link="base_link" />
      <child link="right_wheel_link" />
      <origin xyz="-0.02 -0.10 -0.06" />
      <axis xyz="0 1 0" />
  </joint>

  <link name="caster_link">
    <visual>
      <origin xyz="0 0 0" rpy="1.57079 0 0"/>
      <geometry>
          <sphere radius="0.016"/>
      </geometry>
        <material name="black">
          <color rgba="0.0 0.0 0.0 0.5" /> 
        </material>
    </visual>
  </link>
    
  <joint name="caster_joint" type="fixed">
      <parent link="base_link" />
      <child link="caster_link" />
      <origin xyz="0.06 0.0 -0.076" />
      <axis xyz="0 1 0" />
  </joint>



</robot>

测试运行

编译测试

colcon build
source install/setup.bash
ros2 launch fishbot_description display_rviz2.launch.py

最终结果

打印joint_states话题

ros2 topic echo /joint_states

通过joint_state_gui改变关节tf中关节角度

在JointStatePublisher中,拖动滑动条,观察

  1. rviz2中tf的变换
  2. joint_states中的值的变换

论如何让车轮着地?

在RVIZ中的机器人轮子是在地面之下的,原因在于我们fixed-frame选择的是base_link,base_link的位置本来就在left_wheel_link和right_wheel_link之上,那该怎么办呢?

增加一个虚拟link和关节,这个关节与base_link相连,位置位于base_link向下刚好到车轮下表面的位置

来,让我们给base_link添加一个父link-base_footprint,新增的URDF代码如下:

  <!-- Robot Footprint -->
  <link name="base_footprint"/>

  <joint name="base_joint" type="fixed">
    <parent link="base_footprint"/>
    <child link="base_link"/>
    <origin xyz="0.0 0.0 0.076" rpy="0 0 0"/>
  </joint>

因为是虚拟关节,我们不用对这个link的形状进行描述,joint的origin设置为**xyz="0.0 0.0 0.076"**表示关节base_footprint向上0.076就是base_link

控制移动机器人轮子运动

我们需要自己编写节点,取代joint_state_publisher发送关节位姿给robot_state_pubsher,robot_state_publisher发送tf控制机器人的关节转动。

  1. 新建节点
  2. 创建发布者
  3. 编写发布逻辑
  4. 编译测试

新建节点

方便起见,我们就在fishbot_describle包中新建节点

touch fishbot_description/fishbot_description/rotate_wheel.py
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node


class RotateWheelNode(Node):
    def __init__(self,name):
        super().__init__(name)
        self.get_logger().info(f"node {name} init..")

def main(args=None):
    """
    ros2运行该节点的入口函数
    1. 导入库文件
    2. 初始化客户端库
    3. 新建节点
    4. spin循环节点
    5. 关闭客户端库
    """
    rclpy.init(args=args) # 初始化rclpy
    node = RotateWheelNode("rotate_fishbot_wheel")  # 新建一个节点
    rclpy.spin(node) # 保持节点运行,检测是否收到退出指令(Ctrl+C)
    rclpy.shutdown() # 关闭rclpy

配置下setup.py

    entry_points={
        'console_scripts': [
            "rotate_wheel= fishbot_description.rotate_wheel:main"
        ],
    },

编译运行

colcon build
source install/setup.bash
ros2 run fishbot_description rotate_wheel

创建发布者

创建发布者之前,要知道robot_state_pubsher所订阅的话题类型是什么?

ros2 topic info /joint_states
Type: sensor_msgs/msg/JointState
Publisher count: 1
Subscription count: 1
ros2 interfaces show sensor_msgs/msg/JointState
# This is a message that holds data to describe the state of a set of torque controlled joints.
#
# The state of each joint (revolute or prismatic) is defined by:
#  * the position of the joint (rad or m),
#  * the velocity of the joint (rad/s or m/s) and
#  * the effort that is applied in the joint (Nm or N).
#
# Each joint is uniquely identified by its name
# The header specifies the time at which the joint states were recorded. All the joint states
# in one message have to be recorded at the same time.
#
# This message consists of a multiple arrays, one for each part of the joint state.
# The goal is to make each of the fields optional. When e.g. your joints have no
# effort associated with them, you can leave the effort array empty.
#
# All arrays in this message should have the same size, or be empty.
# This is the only way to uniquely associate the joint name with the correct
# states.

std_msgs/Header header

string[] name
float64[] position
float64[] velocity
float64[] effort

知道了话题类型,我们就可以来创建发布者了.

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
# 1.导入消息类型JointState
from sensor_msgs.msg import JointState

class RotateWheelNode(Node):
    def __init__(self,name):
        super().__init__(name)
        self.get_logger().info(f"node {name} init..")
        # 2.创建并初始化发布者成员属性pub_joint_states_
        self.pub_joint_states_ = self.create_publisher(JointState,"joint_states", 10) 

编写发布逻辑

多线程定频发布Rate

为了能够一直循环使用rate,我们单独开一个线程用于发布joint_states话题数据,在ROS2程序中单独开线程进行话题发布的方法为:

import threading
from rclpy.node import Node

class RotateWheelNode(Node):
    def __init__(self):
        # 创建一个Rate和线程
        self.pub_rate = self.create_rate(5) #5Hz
        # 创建线程
        self.thread_ = threading.Thread(target=self._thread_pub)
        self.thread_.start()

    def _thread_pub(self):
        while rclpy.ok():
            #做一些操作,使用rate保证循环频率
            self.pub_rate.sleep()

构造发布数据

接着我们来构造发布的数据:

joint_states有一个头和四个数组需要赋值(可通过ros2 interface指令查询)

std_msgs/Header header #时间戳信息 和 frame_id
string[] name
float64[] position
float64[] velocity
float64[] effort
# 这是一个持有数据的信息,用于描述一组扭矩控制的关节的状态。
#
# 每个关节(渐进式或棱柱式)的状态由以下因素定义。
# #关节的位置(rad或m)。
# #关节的速度(弧度/秒或米/秒)和
# #在关节上施加的力(Nm或N)。
#
# 每个关节都由其名称来唯一标识
# 头部规定了记录关节状态的时间。所有的联合状态
# 必须是在同一时间记录的。
#
# 这个消息由多个数组组成,每个部分的联合状态都有一个数组。
# 目标是让每个字段都是可选的。例如,当你的关节没有
# 扭矩与它们相关,你可以让扭矩数组为空。
#
# 这个信息中的所有数组都应该有相同的大小,或者为空。
# 这是唯一能将关节名称与正确的
# 状态。
string[] name #关节名称数组
float64[] position #关节位置数组
float64[] velocity #关节速度数组
float64[] effort #扭矩数据
name

name是关节的名称,要与urdf中的定义的关节名称相同,根据我们的URDF定义有

self.joint_states.name = ['left_wheel_joint','right_wheel_joint']
position

表示关节转动的角度值,因为关节类型为continuous,所以其值无上下限制,初始值赋值为0.0

# 关节的位置
self.joint_states.position = [0.0,0.0]

我们采用速度控制机器人轮子转动,所以机器人的位置更新则可以通过下面式子计算得出

某一段时间内轮子转动的角度 = (当前时刻-上一时刻)* 两个时刻之间的轮子转速

delta_time =  time.time()-last_update_time
# 更新位置
self.joint_states.position[0]  += delta_time*self.joint_states.velocity[0]
self.joint_states.position[1]  += delta_time*self.joint_states.velocity[1]
velocity

因为我们采用速度进行控制,所以对外提供一个速度更改接口。

def update_speed(self,speeds):
    self.joint_speeds = speeds

完成后代码

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
# 1.导入消息类型JointState
from sensor_msgs.msg import JointState

import threading
import time

class RotateWheelNode(Node):
    def __init__(self,name):
        super().__init__(name)
        self.get_logger().info(f"node {name} init..")
        # 创建并初始化发布者成员属性pub_joint_states_
        self.joint_states_publisher_ = self.create_publisher(JointState,"joint_states", 10) 
        # 初始化数据
        self._init_joint_states()
        self.pub_rate = self.create_rate(30)
        self.thread_ = threading.Thread(target=self._thread_pub)
        self.thread_.start()

    
    def _init_joint_states(self):
        # 初始左右轮子的速度
        self.joint_speeds = [0.0,0.0]
        self.joint_states = JointState()
        self.joint_states.header.stamp = self.get_clock().now().to_msg()
        self.joint_states.header.frame_id = ""
        # 关节名称
        self.joint_states.name = ['left_wheel_joint','right_wheel_joint']
        # 关节的位置
        self.joint_states.position = [0.0,0.0]
        # 关节速度
        self.joint_states.velocity = self.joint_speeds
        # 力 
        self.joint_states.effort = []

    def update_speed(self,speeds):
        self.joint_speeds = speeds

    def _thread_pub(self):
        last_update_time = time.time()
        while rclpy.ok():
            delta_time =  time.time()-last_update_time
            last_update_time = time.time()
            # 更新位置
            self.joint_states.position[0]  += delta_time*self.joint_states.velocity[0]
            self.joint_states.position[1]  += delta_time*self.joint_states.velocity[1]
            # 更新速度
            self.joint_states.velocity = self.joint_speeds
            # 更新 header
            self.joint_states.header.stamp = self.get_clock().now().to_msg()
            # 发布关节数据
            self.joint_states_publisher_.publish(self.joint_states)
            self.pub_rate.sleep()

def main(args=None):
    rclpy.init(args=args) # 初始化rclpy
    node = RotateWheelNode("rotate_fishbot_wheel")  # 新建一个节点
    node.update_speed([15.0,-15.0])
    rclpy.spin(node) # 保持节点运行,检测是否收到退出指令(Ctrl+C)
    rclpy.shutdown() # 关闭rclpy

编译测试

编译程序

colcon build --packages-select fishbot_description
source install/setup.bash

此时运行关节数据发布节点

ros2 run fishbot_description rotate_wheel

测试之前还需要修改下display_rviz2.launch.py文件,注释其joint_state_publisher节点

# ld.add_action(joint_state_publisher_node)
ld.add_action(robot_state_publisher_node)
ld.add_action(rviz2_node)

先运行rviz和robot_state_publisher

source install/setup.bash
ros2 launch fishbot_description display_rviz2.launch.py

观察此时rviz界面,可以看到轮子疯狂转动