ROS2开发笔记——Topic样例(二)

2023-05-16 Views2299字11 min read

一、新建工作空间

ROS采用工作空间的概念,在工作空间目录下一般设src目录存放源代码。ROS采用包的形式管理项目,所有工作空间src目录下一般有多个包。ROS可以采用python或C++进行开发,也可以同时采用这两种语言混合开发,因此一个典型ROS工作空间如下所示,

workspace_folder/
    src/
      cpp_package_1/
          CMakeLists.txt
          include/cpp_package_1/
          package.xml
          src/

      py_package_1/
          package.xml
          resource/py_package_1
          setup.cfg
          setup.py
          py_package_1/
      ...
      cpp_package_n/
          CMakeLists.txt
          include/cpp_package_n/
          package.xml
          src/

下面,我们新建自己的工作空间,

$ mkdir -p ~/ros2_ws/src
$ cd  ~/ros2_ws/src

二、新建包

ROS 提供了工具帮助我们按照模板新建包,<buildtype><build-type> 指定了编译类型,也就是指定了我们使用什么语言进行开发,使用C++则<buildtype><build-type>为 ament_cmake, 而使用python则是ament_python。<packagename><package_name>是我们的包名字。

ros2 pkg create --build-type <build-type> <package_name>

下面,我们新建一个py_pubsub包,学习使用python进行ROS主题通信的开发,

$ ros2 pkg create --build-type ament_python py_pubsub

我们看一下新建的py_pubsub的目录结构,

py_pubsub/
      py_pubsub/
      resource/py_pubsub
      test/
      package.xml
      setup.cfg
      setup.py

2.1、package.xml

首先,查看第一个package.xml文件,首先它包含了一些包的名字、版本号、描述信息、许可以及维护者的信息。更重要的是package.xml指定了这个包的依赖,方便编译器在编译的时候能够找到对应的依赖。依赖以_depend结尾的标签指定,<testdepend><test_depend>顾名思义是测试时用到的依赖,还有<builddepend><build_depend>编译依赖,<execdepend><exec_depend>执行依赖,以及<buildexportdepend><build_export_depend>导出依赖等。

<?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>py_pubsub</name>
 <version>0.0.0</version>
 <description>TODO: Package description</description>
 <maintainer email="user@todo.todo">user</maintainer>
 <license>TODO: License declaration</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>

加入我们项目中需要用到一个包,但是我们系统上没有,那么编译的时候必然会报错,那么我们怎么安装这些外部依赖包呢?这需要用到rosdep这个工具。使用rosdep之前,需要先对它进行初始化并更新,

$ sudo rosdep init
$ rosdep update

但是sudo rosdep init需要从GitHub下载数据,由于国内的原因,运行这个命令必然会失败。这成为了每个学习ros的人必然要面对的一个问题。国情原因我理解,只是不知道为什么国内ros社区没有去维护一个国内版本,可能是许可的原因吧,以致大家需要使用一些邪魔外道的手段才可以解决这个问题。

不过,现在博主鱼香ROS,自己维护了国内版rosdepc,算是解决了这个问题,rosdepc的安装与使用

$ pip install rosdepc
$ rosdepc init
$ rosdepc update

检查并安装依赖,

rosdepc install --from-paths src -y --ignore-src

--from-paths src 表示检查src目录下的package.xml中的依赖,-y就是默认yes,--ignore-src表示加入package.xml中指定的某个依赖是当前工作空间中的就忽略,因为编译之后必然会安装。

2.2、setup.py

setup.py是python包的安装指令文件,指定了怎么安装这个包,同样也包含该包的一些相关信息,我们需要注意的是执行程序入口entry_points,示例中定义了一个执行程序入口为my_node,该入口指向my_py_pkg.my_node:main函数,后面写代码的时候会有具体示例。

from setuptools import setup

package_name = 'py_pubsub'

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='TODO',
 maintainer_email='TODO',
 description='TODO: Package description',
 license='TODO: License declaration',
 tests_require=['pytest'],
 entry_points={
     'console_scripts': [
            # 'my_node = my_py_pkg.my_node:main'
     ],
   },
)

2.3、setup.cfg

setup.cfg是为了执行ros2 run命令时能够找到对应的可执行程序,

[develop]
script_dir=$base/lib/py_pubsub
[install]
install_scripts=$base/lib/py_pubsub

2.4、其他

剩下的是三个目录,一个是跟包同名的目录,用于存放我们的源代码文件,该目录下还有__init__.py文件。另外一个是resource目录,底下也是跟包同名的目录,用于存放相关资源文件,比如图片啥的。最后就是自动的测试脚本了,暂时不用管。

三、编写publisher节点

在源代码的存放目录新建一个名为publisher_member_function.py的python文件,publisher节点示例如下,

import rclpy # ROS提供的python接口
from rclpy.node import Node  # 应该是ROS节点的父类了

from std_msgs.msg import String # 从标准信息类型库中导入字符串类型,其实就是数据类型


class MinimalPublisher(Node): # 定义类并继承Node

    def __init__(self):
        super().__init__('minimal_publisher')
        self.publisher_ = self.create_publisher(String, 'topic', 10) # 创建一个发布者,该发布者在一个名为'topic'(可以定义为其他名字)的主题上发布一个String类型的信息,10表示缓冲队列的大小,队列大小是一项必需的 QoS(服务质量)设置,如果订阅者接收消息的速度不够快,它会限制排队消息的数量。
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.timer_callback) # 创建一个计时器,该计时器按一定时间间隔(0.5秒)调用回调函数self.timer_callback
        self.i = 0

    def timer_callback(self): # 回调函数,构造并发布信息
        msg = String()
        msg.data = 'Hello World: %d' % self.i
        self.publisher_.publish(msg) # 发布信息(这是给订阅者发布的)
        self.get_logger().info('Publishing: "%s"' % msg.data) # 同时还发布到终端 console
        self.i += 1


def main(args=None):
    rclpy.init(args=args) # 初始化节点

    minimal_publisher = MinimalPublisher() # 实例化发布者节点

    rclpy.spin(minimal_publisher) # 启动发布者节点

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    minimal_publisher.destroy_node() # 销毁发布者节点
    rclpy.shutdown() # 关闭节点


if __name__ == '__main__':
    main()

因为我们在发布者节点中调用了两个库:rclpy和std_msgs,这两个库是运行时需要的,数据执行依赖,所以我们打开这个包下的package.xml文件,添加以下两行,

<exec_depend>rclpy</exec_depend>
<exec_depend>std_msgs</exec_depend>

然后,我们需要为程序调用定义入口,打开setup.py文件,添加如下代码,

entry_points={
        'console_scripts': [
                'talker = py_pubsub.publisher_member_function:main',
        ],
},

其中,talker是自定义的名字,调用时如下所示,

$ ros2 run py_pubsub talker

四、编写subscriber节点

该节点简单接受publisher发布的信息并打印到终端,

import rclpy
from rclpy.node import Node

from std_msgs.msg import String


class MinimalSubscriber(Node):

    def __init__(self):
        super().__init__('minimal_subscriber')
        self.subscription = self.create_subscription(
            String,
            'topic',
            self.listener_callback,
            10) # 新建订阅者,接收'topic'的数据并调用self.listener_callback函数
        self.subscription  # prevent unused variable warning

    def listener_callback(self, msg):
        self.get_logger().info('I heard: "%s"' % msg.data) # 打印到终端


def main(args=None):
    rclpy.init(args=args)

    minimal_subscriber = MinimalSubscriber()

    rclpy.spin(minimal_subscriber)

    # Destroy the node explicitly
    # (optional - otherwise it will be done automatically
    # when the garbage collector destroys the node object)
    minimal_subscriber.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

由于它和发布者程序调用的依赖是一样的,所以我们不需要再一次添加依赖,但是需要给它添加执行入口,打开setup.py,按如下所示添加,

entry_points={
        'console_scripts': [
                'talker = py_pubsub.publisher_member_function:main',
                'listener = py_pubsub.subscriber_member_function:main',
        ],
},

五、编译与运行

按照惯例,编译前检查一下依赖,

$ rosdepc install -i --from-path src --rosdistro humble -y

rosdepc可以自定义很多参数,我们看到多少学多少,这里--rosdistro humble是指定ROS版本,不指定也行,这是多版本混合开发才需要的。

ROS2采用的编译器是colcon,回到工作空间的根目录ros2_ws,执行编译,

$ colcon build --packages-select py_pubsub

--packages-select py_pubsub是指定只编译py_pubsub,如果不指定,直接执行colcon build会编译整个工作空间中所有的包。

编译之后,我们会发现工作空间中多了三个目录,build/用于存放编译过程的中间文件,install/就是安装目录,就是我们把刚才的py_pubsub包安装到这里了,log/顾名思义日志文件。

ros2_ws/
    build/
    install/
    log/
    src/

我们要使用刚编译的py_pubsub,还需要执行以下以下命令来配置环境变量,

$ source install/setup.bash

运行发布者节点,

$ ros2 run py_pubsub talker
[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [minimal_publisher]: Publishing: "Hello World: 4"
...

运行订阅者节点,

$ ros2 run py_pubsub listener
[INFO] [minimal_subscriber]: I heard: "Hello World: 10"
[INFO] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [minimal_subscriber]: I heard: "Hello World: 14"

好了!至此入门ros开发第一课结束了,从开发、编译、运行整个流程都走了一遍了,下一步学习服务通信的开发!

EOF