新建   文章列表   相册  后台  网摘
 

Kreos

Kreos 无所不能

小鱼-第3章 订阅和发布--话题通信探索(学习笔记)

  记得我刚开始做“鱼香 ROS”公众号的时候没有人关注,自己就经常推荐朋友订阅我的公众号,我也会每天发布技术相关的文章、图片或视频,推送给我的订阅者。

和公众号类似的订阅-发布机制用途非常广泛,比如在物联网等领域中常用到的 MQTT协议也是一种基于订阅和发布的通信协议。

在机器人的世界中,为了能够感知环境信息和执行动作,往往装载了很多传感器和执行器,常见的传感器有摄像头、雷达和惯性测量单元等,常见的执行器有各种类型的底盘、机械臂关节电动机和电动夹爪等。为了方便各类传感器和执行器数据在机器人系统中传递,ROS 2 也引入了一套订阅发布机制,即话题通信。本章将针对话题通信进行详细的讲解。

 

3.1 话题通信介绍

ROS 2 的话题机制有四个关键点,分别是发布者、订阅者、话题名称和话题类型。如图3-1 所示,发布者相当于公众号的作者,订阅者则是读者,话题名称相当于公众号名字,消息接口则相当于公众号的内容类型。

小鱼(发布节点) 话题名称(鱼香 ROS)消息接口(图文/视频) 读者(订阅节点)

image

 图 3-1 话题机制与公众号类比

  在第 1 章运行海龟模拟器和键盘控制节点时,我们通过 rqt工具看到,节点之间通过订阅和发布同名的话题来传输控制指令,但当时并未详细介绍。

下面继续以海龟模拟器为例,进一步讲解话题通信。

打开终端,输入代码清单 3-1 中的命令,打开海龟模拟器。

代码清单 3-1 打开海龟模拟器
 $ ros2 run turtlesim turtlesim_node 
---
Warning: Ignoring XDG SESSION TYPE-wayland on Gnome. Use QT QPA PLATPORM-waylandl 
    to run on Wayland anyway.
[INFO] [1681383588.540304114] [turtlesim]: Starting turtlesim with node name / turtlesim 
[INFO] [1681383588.548572512] [turtlesim]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000]

打开后,使用命令行来查看 turtlesim 节点的信息,打开新的终端,输入代码清单 3-2 中的指令。 代码清单 3-2 使用命令行查看节点信息

$ ros2 node info /turtlesim 
---
/turtlesim
  Subscribers:
    /parameter_events: rcl_interfaces/msg/ParameterEvent
    /turtlel/cmd_vel: geometry_msgs/msg/Twist
  Publishers:
    /parameter_events: rcl_interfaces/msg/ParameterEvent
    /rosout: rcl_interfaces/msg/Log
    /turtlel/color_sensor: turtlesim/msg/Color
    /turtlel/pose: turtlesim/msg/Pose ...

  ros2 node info 指令用于查看节点的详细信息,其中 Subscribers 部分是 /turtlesim 节点订阅的所有话题,

可以看出该节点订阅了名/turtlel/cmd_vel的话题,

用于接收控制指令,它的消息接口是geometry_msgs/msg/Twist。

  Publishers部分下是该节点发布的所有话题,需要关注的话题是/turtlel/pose,该话题用于发布当前海龟的位置和速度,

它的消息接口是turtlesim/msg/Pose,对于话题下面的服务和置动作相关的信息,这里先忽略。

尝试使用命令订阅/turtlel/pose,实时输出海龟当前的位姿,在任意终端输入代码清单3-3中的命令。

代码清单3-3 输出海龟当前的位姿

$ ros2 topic echo /turtlel/pose
---
x: 5.544444561004639
Y: 5.544444561004639
theta: 0.0
linear_velocity: 0.0
angular_velocity: 0.0
...

 

   ros2 topic echo是topic模块下用于输出话题数据的命令,该命令运行后会输出收到的话

题数据,其中,x和y是海龟的位置;theta是海龟的朝向;linear_velocity是海龟的线速度,
也就是海龟前进的速度,前进为正,后退为负;angular_velocity是角速度,是海龟绕自身旋
转的速度,逆时针为正,顺时针为负。
  通过命令行工具可以很方便地查看话题数据,同样可以发布数据,但对某个话题发布数
据前需要确定其消息接口,消息接口就像公众号发文时的内容类型一样,发布前需要确认要

发图片、视频还是图文。使用代码清单3-4中的命令可以查看某个话题的具体信息。
代码清单3-4 查看某个话题的具体信息

$ ros2 topic info /turtlel/cmd_vel -v
---
Type: geometry_msgs/msg/Twist
Publisher count: 0
Subscription count: 1
Node name: turtlesim
Node namespace: /
Topic type: geometry_msgs/msg/Twist
Endpoint type: SUBSCRIPTION
GID: 01.0f.65.91.4a.32.f1.d4.01.00.00.00.00.00.1b.04.00.00.00.00.00.00.00.00
Qos profile:
  Reliability: RELIABLE
  History (Depth): UNKNOWN
  Durability: VOLATILE
  Lifespan: Infinite
  Deadline: Infinite
  Liveliness: AUTOMATIC
  Liveliness lease duration: Infinite

 

可以看到其消息接口为Topic type:geometry_msgs/msg/Twist,接着使用代码清单3-5中
的指令来查看该消息接口的详细定义。
代码清单3-5 查看消息接口的详细定义

$ros2 interface show geometry_msgs/msg/Twist
---
# This expresses velocity in free space broken into its linear and angular parts.
Vector3  linear
    float64 x
    float64 y
    float64 z
Vector31  angular
    float64 x
    float64 y
    float64 z

 

    ros2 interface show 命令用于输出接口的定义内容,其格式后续在自定义消息接口时会进
一步讲解。从结果可以看出,geometry_msgs/msg/Twist接口包含了6个变量,其中,linear
是线速度,其下的x、y和z三个变量分别代表机器人在三个方向上的运动速度,单位为m/s,
因为ROS中规定机器人前进的方向为x,所以linear.x就是机器人前进方向上的速度;
angular是角速度,其下的x、y和z三个变量分别表示机器人绕x、y、z三个轴的旋转速度,
单位为rad/s,因为海龟生活的是二维世界,所以它只能绕z轴(即垂直向上的方向)旋转。
知道了消息接口的具体内容,就可以通过代码清单3-6中的命令进行发布了。
代码清单3-6 使用命令行发布线速度

$ ros2 topic pub /turtlel/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 1.0} }"

publisher: beginning loop
publishing #1: geometry_msgs.msg.Twist (linear=geometry_msgs.msg. Vector3(x=1.0,
    y=0.0, z=0.0), angular=geometry_msgs.msg.Vector3(x=0.0, y=0.0, z=0.0))
publishing #2: geometry_msgs.msg.Twist(linear=geometry_msgs.msg.Vector3(x=1.0,
    y=0.0, z=0.0), angular=geometry_msgs.msg.Vector3(x=0.0, y=0.0, z=0.0))

 

运行代码清单3-6中的命令时,需要注意格式。大括号用于区分消息结构层级,冒号后
需要添加空格用于区分。命令运行后,就会看到不断输出的发布结果,这里给定线速度中x
为1.0m/s。观察海龟模拟器窗口,可以看到海龟向前移动了,如图3-2所示。

image

 图3-2 海龟移动轨迹

如果要发布角速度,则需要修改angular的z变量的值,比如给定海龟1.0rad/s的角速
度,命令如代码清单3-7所示。
代码清单3-7 通过命令行发布角速度控制海龟

$ ros2 topic pub /turtlel/cmd_vel geometry_msgs/msg/Twist "{angular: {z: 1.0}}"

 

    虽然可以在命令行里轻松实现话题的发布和订阅,但要想更灵活地使用话题,还需要学
习如何在程序中使用。

3.2 Python 话题订阅与发布

  了解完话题,接下来通过一个具体的例子学习如何使用Python进行话题的发布和订阅。
结合第2章中的小说示例,我将带你来做一个小说朗读器。首先创建一个小说发布节点,用
来下载和发布novel话题;然后创建一个小说阅读节点,用于订阅novel话题,合成语音并
进行播放。

3.2.1 通过话题发布小说

  首先在主目录下创建chapt3文件夹,并用VS Code打开,在该文件夹下创建工作空间
topic_ws;然后在topic_ws下创建src目录;最后打开终端,进入src,输入代码清单3-8中
的命令,创建demo_python_topic功能包。

 

代码清单3-8 创建ament_python类型功能包并添加依赖

$ ros2 pkg create demo_python_topic -- build-type ament_python -- dependencies
  rclpy example_interfaces -- license Apache-2.0
---
going to create a new package
package name: demo_python_topic
destination directory: /home/fishros/chapt3/topic_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['fishros <fishros@todo.todo>']
licenses: ['Apache-2.0']
build type: ament_python
dependencies: ['rclpy', 'std_msgs']
...

 

代码清单3-8中创建功能包的指令和前面有所不同,通过 -- dependencies rclpy example_
interfaces添加了rclpy和example_interfaces 依赖,其中example_interfaces是接下来要用到
的消息接口。此时打开功能包清单文件package.xml,你会发现rclpy和example_interfaces依
赖项目已经被自动添加。
创建完功能包以后,在src/demo_python_topic/demo_python_topic下创建novel_pub_node.
py 文件,在该文件中编写如代码清单3-9所示的代码。
代码清单3-9 src/demo_python_topic/demo_python_topic/novel_pub_node.py

import rclpy
from rclpy.node import Node
import requests
from example_interfaces.msg import String
from queue import Queue

class NovelPubNode (Node) :
def _init_(self, node_name) :
super () ._ init_(node_name)
self.novels_queue _= Queue()#创建队列,存放小说
# 创建话题发布者,发布小说
self.novel_publisher _= self.create_publisher (String, 'novel', 10)
self.timer _= self.create_timer(5, self.timer_callback)#创建定时器

def download_novel(self, url):
response = requests.get (url)
response.encoding = 'utf-8'
self.get_logger().info(f'下载完成:{ur1}')
for line in response.text.splitlines():#按行分割,放入队列
self.novels_queue _. put (line)

def timer_callback(self):
if self.novels_queue _· qsize()>0:#队列中有数据,取出发布一行
msg=String()#实例化一个消息

msg.data=self.novels_queue _· get()#对消息结构体进行赋值
self.novel_publisher _· publish(msg) #发布消息
self.get_logger().info(f'发布了一行小说:{msg.data}')

def main():
rclpy.init ()
node = NovelPubNode ('novel_pub' )
node.download_novel('http://localhost:8000/novell.txt')
rclpy.spin(node)
rclpy. shutdown ()

 

在代码清单3-9中,首先导入了rclpy相关库,然后导入了http请求库requests用于下载
小说。因为小说是由文字组成的,所以从example_interfaces库中导入了String类型的接口。
关于该接口的详细定义,可以通过命令行工具查看,命令及结果如代码清单3-10所示。
代码清单3-10 查看example_interfaces/msg/String 接口定义

$ ros2 interface show example_interfaces/msg/String
---
# This is an example message of using a primitive datatype, string.
# If you want to test with this that's fine, but if you are deploying
# it into a system you should create a semantically meaningful message type.
# If you want to embed it in another message, use the primitive data type instead.
string data

 

  从代码清单3-10的结果可以看出,该接口包含一个类型为string、名字为data的数据,
我们可以使用它来表示文字。
  代码清单3-9导入库之后,定义了一个NovelPubNode节点,在_init_方法内创建了
一个队列,用于存放要发布的小说段落。然后调用self.create_publisher(String,'novel',10)
创建话题发布者,该方法传入了三个参数:第一个参数是话题的接口类型,用的是上面从
example_interfaces.msg 导入的String;第二个参数是话题的名称;第三个参数和ROS 2的服
务质量策略有关,在本书的第10章有详细的介绍,这里你只需要知道10是表示保存历史消
息的队列长度。创建发布者后,调用self.create_timer(5,self.timer_callback)创建一个定时器,
它的第一个参数为周期,第二个参数为回调函数。创建完成后,节点会每间隔5s调用一次回
调函数 timer_callback。
  该节点对外提供了一个download_novel方法,参数为小说地址,该方法会调用requests
库下载小说,并按照行分割,然后调用novels_queue_的put方法将其放入队列中。接下来
是timer_callback方法,如果检测到小说队列长度大于0,它就会创建一个String类型的对象
msg,再从队列中取一个数据,赋值给msg的属性data,然后用发布者将msg发布出来。
  在main函数中实例化节点。首先调用download_novel下载小说,然后调用spin。
  在 setup.py中对novel_pub_node进行注册,运行2.5.3节介绍的Python本地服务器,并
多给小说添加几行。重新构建工程,接着运行novel_pub_node,命令及结果如代码清单3-11
所示。

代码清单3-11 运行novel_pub_node

$ ros2 run demo_python_topic novel_pub_node
---
[INFO] [1681459937.288372889] [novel_pub]:下载完成:http://localhost:8000/novel1.
txt
[INFO][1681459939.501703634] [novel_pub]:发布了一行小说:第一章 少年路上修仙路,因诛仙
力量被驱逐。
[INFO] [1681459944.507272228][novel_pub]:发布了一行小说:在一个古老的村庄里有个少年。

 

可以看到小说已经被成功下载和发布了,但是否真的发布出来了,要通过命令行来测
试,打开一个新的终端,输入代码清单3-12中的命令。
代码清单3-12 查看话题列表

$ ros2 topic list -v
---
Published topics:
* /novel [std_msgs/msg/String] 1 publisher
...

 

这里加上了-v表示详细信息,可以看到/novel这个话题。接着我们使用代码清单3-13
中的命令行输出实时的话题内容。
代码清单3-13 使用命令行输出 novel话题数据

$ ros2 topic echo /novel
---
data:这个古老的村庄位于一个幽静的山谷中,周围环绕着葱翠的森林和宁静的溪流。
---
data:少年叫李明,他在这个村庄中已经生活了十六年。
---

 

可以看到,程序运行结果中正常输出了小说的内容。虽然在终端看小说很有意思,但如
果能让计算机帮我们读出来就更好了,下面我们尝试订阅小说话题,并合成语音。

3.2.2 订阅小说并合成语音

在订阅小说话题之前,先来解决语音合成的问题。在Python中可以通过安装第三方库来
合成语音。打开终端,依次输入代码清单3-14中的命令,安装语音合成引擎。
代码清单3-14 安装语音合成引擎

$ sudo apt install python3-pip -y
$ sudo apt install espeak-ng -y
$ pip3 install espeakng

 

    代码清单3-14中的第一个命令是用apt安装Python3的包管理工具pip3,第二个命令是
用apt安装语音合成引擎espeak-ng,最后一个命令是使用pip3安装espeakng的Python库,方
便我们在代码中调用。安装好依赖后就可以编写节点代码了,在src/demo_python_topic/demo_
python_topic下创建novel_sub_node.py文件,在该文件中编写如代码清单3-15所示的代码。

代码清单3-15 src/demo_python_topic/demo_python_topic/novel_sub_node.py

import rclpy
from rclpy.node import Node
from example_interfaces.msg import String
import threading
from queue import Queue
import time
import espeakng

class NovelSubNode (Node) :
  def _init_(self, node_name) :
    super () ._ init_(node_name)
    self.novels_queue _= Queue ()
    self.novel_subscriber _= self.create_subscription(
    String, 'novel', self.novel_callback, 10)
    self.speech_thread _= threading.Thread(target=self.speak_thread)
    self.speech_thread _. start ()

  def novel_callback(self, msg):
    self.novels_queue _. put (msg.data)

  def speak_thread(self):
    speaker = espeakng.Speaker ()
    speaker.voice = 'zh'
    while rclpy.ok():
      if self.novels_queue _. qsize() > 0:
        text = self.novels_queue _. get ()
        self.get_logger () .info(f' Eik {text}')
        speaker.say(text)
        speaker.wait ()
      else:
        time.sleep(1)

def main(args=None):
  rclpy.init (args=args)
  node = NovelSubNode("novel_read")
  rclpy.spin(node)
  rclpy.shutdown ()

 

  在上面的代码中,导入库的部分和订阅节点并无太大不同,只是多导入了时间库time和
语音合成库espeakng。对于NovelSubNode类,在初始化方法中先创建一个用于存放接收到
的小说的队列,因为朗读速度没有接收的速度快,所以使用队列存储接收到的数据。然后调
用self.create_subscription(String,'novel',self.novel_callback,10)创建一个话题订阅者,第一个
参数String表示话题接口类型;第二个参数 novel是话题的名称;第三个参数novel_callback
是回调函数,当调用spin时接收novel话题事件,会自动调用该函数并将数据作为参数传
入,novel_callback函数将收到的数据放到队列的尾部;第四个参数是服务质量配置,10表

 

示历史消息队列长度。
创建完订阅者后,创建一个朗读线程speech_thread_,该线程对应的方法是speak_thread,
在该方法中创建了一个espeakng.Speaker类的对象speaker,设置speaker的声音为中文‘zh',
调用rclpy.ok实时检测rclpy的状态,如果正常,则不断循环判断队列中是否有数据,如果
有,则通过novels_queue _. get()拿出一条队首的数据,接着调用speaker.say进行朗读,调用
wait 等待朗读结束,在没有数据的情况下,则调用time.sleep(1)使当前线程休眠1s。
在main函数中实例化NovelSubNode节点,调用spin。最后在setup.py中对 novel_sub_
node进行注册,编译后运行,如代码清单3-16所示。
代码清单3-16 运行小说朗读节点

$ ros2 run demo_python_topic novel_sub_node

[INFO][1681469282.747851102] [novel_read]:正在朗读 只要努力,一定可以掌握 ROS 2。

 

如果你没有看到任何输出,可能是因为没有发布话题,你可以手动通过命令行发布数
据,或者直接启动小说发布节点即可。
至此,我们就完成了使用Python发布与订阅话题的学习,顺便学习了如何在代码中进行
语音合成。稍事休息,接着来学习如何在C++中使用话题。

3.3 C++话题订阅与发布

在实际的机器人开发项目中做运动控制时,C++用得比较多。本节我们就利用C++和海
龟模拟器,先通过话题发布速度,控制海龟画圆,接着通过话题订阅海龟的当前位置,根据
当前位置和目标位置的差距来修正控制命令,实现对海龟位置的闭环控制。

3.3.1 发布速度控制海龟画圆
当海龟模拟器节点运行起来后,会自动生成一只海龟。模拟器节点会订阅名称为/
turtle1/cmd_vel的话题用于接收控制命令,该话题的接口类型是geometry_msgs/msg/Twist。
订阅的同时,该节点会发布其当前位置,该话题的接口类型是turtlesim/msg/Pose。为了能够
在代码中订阅和发布对应的话题,在创建功能包时需要加入对geometry_msgs和turtlesim的
依赖。打开终端,进入chapt3/topic_ws/src目录,输入代码清单3-17中的命令来创建demo_
cpp_topic功能包。

代码清单3-17 创建demo_cpp_topic功能包并添加依赖

$ ros2 pkg create demo_cpp_topic -- build-type ament_cmake -- dependencies rclcpp
geometry_msgs turtlesim -- license Apache-2.0
---
going to create a new package
package name: demo_cpp_topic
destination directory: /home/fishros/chapt3/topic_ws/src

package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['fishros <fishros@todo.todo>']
licenses: ['Apache-2.0']
build type: ament_cmake
dependencies: ['rclcpp', 'geometry_msgs', 'turtlesim']

---

 

创建完功能包后,在src/demo_cpp_topic/src下创建turtle_circle.cpp文件,在该文件中编
写如代码清单3-18所示的代码。
代码清单3-18 src/demo_cpp_topic/src/turtle_circle.cpp

#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/twist.hpp"
#include <chrono>//引入时间相关头文件
// 使用时间单位的字面量,可以在代码中使用s和ms 表示时间
using namespace std :: chrono_literals;

class TurtleCircle : public rclcpp: :Node

{

private:
rclcpp :: TimerBase :: SharedPtr timer _; // 定时器智能指针
rclcpp: : Publisher<geometry_msgs: :msg: : Twist>: : SharedPtr publisher _; // ***
智能指针

public:
explicit TurtleCircle(const std :: string& node_name) : Node (node_name)

{

// 调用继承而来的父类函数创建订阅者
publisher _= this->create_publisher<geometry_msgs: :msg: :Twist>("/turtle1/cmd
vel", 10);
// 调用继承而来的父类函数创建定时器
timer_ = this->create_wall_timer (1000ms, std: :bind(&TurtleCircle :: timer_
callback, this) );

}

private:
  void timer_callback()
  {
    auto msg = geometry_msgs: :msg: :Twist ();
    msg.linear.x = 1.0;
    msg.angular.z = 0.5;
    publisher _- >publish(msg);
  }
};

int main(int argc, char *argv [] )

{

    rclcpp :: init (argc, argv);
    auto node = std: :make_shared<TurtleCircle>("turtle_square");
    rclcpp :: spin(node);
    rclcpp :: shutdown ();
    return 0;


 

代码清单3-19 src/demo_cpp_topic/CMakeLists.txt

在代码清单3-18中,首先包含了ROS 2客户端库rclcpp和消息接口geometry_msgs/
msg/twist.hpp,然后引入了时间库头文件,并使用using来声明使用时间单位字面量,字面量
是C++14中的新特性,引入后可以直接使用数字加单位(s或ms等)来表示时间,让代码更
加直观。
导入头文件后,接着定义了一个TurtleCircle类,为其添加了定时器的共享指针timer_
和话题发布者的共享指针publisher_两个属性,然后在构造函数中分别对这两个属性进行初
始化。
this->create_publisher 方法是从父类继承而来的,用于初始化发布者。◇是C++的模板
语法,被包裹的geometry_msgs :: msg :: Twist是话题的接口类型,该方法的第一个参数是话题
的名称,和海龟订阅的话题名称要保持一致才能通信;第二个参数是10,和Python中一样,
与ROS 2的服务质量有关,在第10章中有详细讲解,这里的10表示历史队列长度。
this->create_wall_timer同样来自父类节点,用于初始化定时器。该方法的第一个参数是
调用周期,这里设置为1000ms,表示间隔1s调用一次;第二个参数是回调函数,这里将成
员方法timer_callback通过std :: bind变成可以直接调用的回调函数。
在timer_callback方法内,首先创建了一个geometry_msgs :: msg :: Twist类型的消息对象,
然后为前进方向的线速度x赋值1m/s,接着将绕z轴的旋转角速度设置为0.5rad/s,此时海
龟的转弯半径应该是2m(1m/s÷0.5rad/s)。
代码完成后,在CMakeLists.txt中添加 turtle_circle节点,并添加依赖,主要添加指令如
代码清单3-19所示。

add_executable(turtle_circle src/turtle_circle.cpp)
ament_target_dependencies (turtle_circle rclcpp geometry_msgs)

install (TARGETS
  turtle_circle
  DESTINATION lib/${PROJECT_NAME}
)
ament_package ()

 

保存并构建demo_cpp_topic功能包,运行海龟模拟器,然后运行turtle_circle节点。观
察海龟模拟器,结果如图3-3所示。

image

 

图3-3 海龟模拟器画圆轨迹

3.3.2 订阅Pose实现闭环控制
通过发布速度控制命令到话题/turtle1/cmd_vel便可以控制海龟移动,通过订阅/turtle1/
pose可以获取海龟的实时位置。实时位置和速度控制都有了,就可以闭环控制海龟移动到指
定位置。所谓闭环控制是指通过对输出进行测量和反馈来调节系统的输入,使系统的输出更
加接近期望值。在本节中,我们将通过不断检测海龟当前位置和目标位置之间的误差,来实
时调整发布的指令,最终使海龟到达指定目标点。
在 src/demo_cpp_topic/src中新建文件turtle_control.cpp,编写如代码清单3-20所示的代码。
代码清单 3-20 turtle_control.cpp

#include "geometry_msgs/msg/twist.hpp"
#include "rclcpp/rclcpp.hpp"
#include "turtlesim/msg/pose.hpp"

class TurtleController : public rclcpp: :Node {
public:
TurtleController () : Node("turtle_controller") {
velocity_publisher_ = this->create_publisher<geometry_
msgs: :msg: :Twist>("/turtle1/cmd_vel", 10);
pose_subscription_ = this->create_subscription<turtlesim: :msg :: Po
se>("/turtle1/pose", 10,
std :: bind (&TurtleController :: on_pose_received_, this,
std :: placeholders ::_ 1));

private:
void on_pose_received_(const turtlesim: :msg: : Pose: :SharedPtr pose) {
//TODO:收到位置计算误差,发布速度指令

private:
rclcpp: : Subscription<turtlesim: :msg: : Pose> :: SharedPtr pose_subscription _;

rclcpp: : Publisher<geometry_msgs: :msg: : Twist>: :SharedPtr velocity_publisher _;
double target_x_{1.0};//目标位置x,设置默认值1.0
double target_y_{1.0};//目标位置y,设置默认值1.0
double k_{1.0};//比例系数,控制输出=误差×比例系数
double max_speed_{3.0};//最大线速度,设置默认值3.0

int main(int argc, char ** argv) {
rclcpp :: init (argc, argv);
auto node = std: : make_shared<TurtleController>();
rclcpp :: spin(node);
rclcpp: : shutdown ();
return 0;

{

 

在代码清单3-20中首先引入了ROS 2相关头文件。turtlesim/msg/Pose是位置话题消息
接口,在3.1节查看模拟器节点详细信息时曾看到过它,该消息的详细定义可以使用代码清
单3-21中的命令查看。
代码清单3-21 查看turtlesim/msg/Pose 接口定义

$ ros2 interface show turtlesim/msg/Pose

float32 x
float32 y
float32 theta

float32 linear_velocity
float32 angular_velocity

 

接着代码清单3-20中定义了一个TurtleController类,让其继承Node,并直接在构造
函数中设置节点名称为turtle_controller。在构造函数中,首先创建了话题发布者velocity_
publisher_,接着调用了create_subscription方法,通过模板语法◇设置话题消息接口为
turtlesim :: msg :: Pose。create_subscription方法有三个入口参数,第一个参数"/turtlel/pose"是
话题名称;第二个参数10是历史队列长度;第三个参数是通过成员方法封装的回调函数,
当后台收到数据时,就会调用该回调函数进行处理。
为了能够在每次收到当前位置就做出反应,我们把核心处理逻辑放到回调函数中,接下
来完善一下on_pose_received_方法,填入代码清单3-22中的代码。
代码清单3-22 turtle_control.cpp/on_pose_received_方法

void on_pose_received_(const turtlesim: :msg: : Pose: : SharedPtr pose) {
auto message = geometry_msgs: : msg: : Twist () ;
// 1.记录当前位置
double current_x = pose->x;
double current_y = pose->y;
RCLCPP_INFO(this->get_logger(),"当前位置:(x=%f,y=%f)",current_x, current_y);

//2. 计算与目标之间的距离,以及与当前海龟朝向的角度差

double distance =
std: : sqrt ((target_x _- current_x) * (target_x - current_x) + (target_y_ -
current_y) * (target_y _- current_y) ) ;
double angle =
std :: atan2(target_y- current_y, target x - current_x) - pose->theta;

// 3. 控制策略:距离大于0.1继续运动,角度差大于0.2则原地旋转,否则直行
if (distance > 0.1) {
if(fabs(angle)>0.2)
{

Jelse{
// 通过比例控制器计算输出速度
message.linear.x = k_ * distance;

message.angular.z = fabs (angle);

1

}

// 4.限制最大值并发布消息
if (message.linear.x > max_speed_) {
message.linear.x = max_speed _;

velocity_publisher _- >publish(message);

 

该回调函数的参数是收到数据的共享指针,所以第一步便是以指针的方式获取当前海龟
的位置,并记录下来。第二步,通过欧式距离公式求出当前位置和目标位置之间的距离和角
度,此时使用的atan2的函数原型为double atan2(double y,double x),用于计算y比x的反正
切,表现在几何上就是计算目标点位置相对当前位置的角度,将结果和当前的朝向做差,得
出angle。第三步就是核心的闭环控制策略,如果距离大于0.1,则计算角速度和线速度;接
着判断角度差是否大于0.2,大于0.2,则发送角速度控制海龟转向,否则根据距离和比例系
数计算出线速度,不难看出,距离差越大,线速度越大。第四步则是限制线速度最大值后,
将控制消息发布出去。
在CMakeLists.txt中添加代码清单3-23中的指令,对turtle_control节点进行注册。
代码清单3-23 在CMakeLists.txt 中注册节点

add_executable(turtle_control src/turtle_control.cpp)
ament_target_dependencies (turtle_control rclcpp geometry_msgs turtlesim)

install (TARGETS
turtle_control
turtle_circle
DESTINATION lib/${PROJECT_NAME}

)

ament_package ()

 

完成后,先运行海龟模拟器,接着编译并运行turtle_control节点,如代码清单3-24所示。
代码清单3-24 运行海龟控制节点

ros2 run demo_cpp_topic turtle_control

[INFO][1681554826.220945971][turtle_controller]:当前位置:(x=5.544445,y=5.544445)
[INFO][1681554826.237126893][turtle_controller]:当前位置:(x=5.544445,y=5.544445)

[INFO][1699337616.177492400][turtle_controller]:当前位置:(x=0.994040,y=1.098285)

 

观察海龟模拟器,可以看到海龟以一个优美的弧线成功移动到了目标位置,如图3-4所示。

image

 图3-4 控制海龟移动到目标点

好了,到这里你已经掌握了如何在代码中使用话题订阅和发布数据,顺便又学了闭环控
制。稍事休息,下一节我们来做个小项目进行实践。

3.4话题通信最佳实践

既然学习了话题通信,相信你已经迫不及待想要小试一下身手,确实也是时候实战一下
了。你可能觉得目前手头上并没有一台机器人可供实践使用,但你可能忽略了可以作为机器
人大脑的核心组件 -- 你的计算机。
在实际的项目中,对系统状态的监测非常重要。这次我们将利用前面学习的知识来制作
一个系统状态监测与可视化工具。准备好了吗?准备好后请在chapt3下创建topic_practice_
ws工作空间,方便继续开展下面的工作。

3.4.1 完成工程架构设计
这是你的第一个实战项目,作为你的导师,我有必要带你从头开始完成。当你接到一个

 

任务或者项目时,首先要确定的就是需求,所以我作为该项目的提出者,现在再次明确对这
个小工具的需求。
第一,通过这个小工具可以看到系统的实时状态信息,包括记录信息的时间、主机名
称、CPU使用率、内存使用率、内存总大小、剩余内存、网络接收数据量和网络发送数据量;
第二,要有一个简单的界面,可以将系统信息显示出来;第三,要能在局域网内其他主机上
查看数据。
看到上面的需求后,你可能会觉得不知道从哪里下手。没关系,现在我先简单概括一下
这个小工具的主要功能:第一,要能获取系统状态信息;第二,要有一个展示界面;第三,
要能共享数据。Python中有很多有用的库,通过它们就可以方便地获取系统状态信息,C++
也可以方便地调用Qt进行界面显示,而ROS 2在局域网内会自动发现其他节点,我们只需
要把系统状态用话题发布出来就可以了。所以我们可以编写一个Python节点获取系统信息并
通过话题发布出来,接着用C++编写一个显示节点,订阅这个话题并进行显示。系统结构如
图3-5所示。

/sys status

图3-5 小工具系统结构

确定了系统中每个节点的功能及节点之间的关系后,整个工程的结构就算确定下来了。
但我们知道,每个话题都有对应的接口类型,ROS 2中已有的接口并没有满足这一需求的,
所以此时我们需要自己定义一个新的接口。那么在正式编写节点代码前,我们先学习如何自
定义通信接口吧。

3.4.2 自定义通信接口

打开终端后,在topic_practice_ws/src目录下输入代码清单3-25中的命令。
代码清单3-25 创建一个接口功能包

$ ros2 pkg create status_interfaces -- build-type ament_cmake -- dependencies
rosidl_default_generators builtin_interfaces -- license Apache-2.0

going to create a new package
package name: status_interfaces
destination directory: /home/fishros/chapt3/topic_practice_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['fishros <fishros@todo.todo>']
licenses: ['Apache-2.0']
build type: ament_cmake
dependencies: ['rosidl_default_generators', 'builtin_interfaces']

/sys_status_pub

/sys_status_display

# 系统名称
# CPU 使用率
# 内存使用率
# 内存总量
# 剩余有效内存
# 网络发送数据总量
# 网络接收数据总量

 

上面的指令用于创建一个status_interfaces功能包(构建类型为ament_cmake),并为其添
加builtin_interfaces 和rosidl_default_generators两个依赖。builtin_interfaces是ROS 2中已有
的一个消息接口功能包,可以使用其时间接口Time,表示记录信息的时间。rosidl_default_
generators用于将自定义的消息文件转换为C++、Python源码的模块。
在ROS2中,话题消息定义文件需要放置到功能包的msg目录下,文件名必须以大写字
母开头且只能由大、小写字母及数字组成。接着在功能包下创建msg目录,并在该目录下新
建文件SystemStatus.msg,然后在文件中编写代码清单3-26的内容。
代码清单3-26 src/status_interfaces/msg/SystemStatus.msg

builtin_interfaces/Time stamp # 记录时间戳
string host_name
float32 cpu_percent
float32 memory_percent
float32 memory_total
float32 memory_available
float64 net_sent
float64 net_recv

 

这样就得到了一个消息接口定义文件,可以看出,定义消息文件的语法类似于在C++中
定义变量,以类型加名称的方式进行定义,其中#后是注释。记录时间戳使用的是builtin_
interfaces接口功能包下的Time类型。其余所有数据的类型,分别由string、float32和
float64组成,这三种是ROS 2内置的基础类型,除了这三种之外,ROS 2还定义了如代码清
单3-27所示的9种数据类型。
代码清单3-27 ROS 2消息接口支持的9种数据类型

bool
byte
char
float32, float64
int8,uint8
int16,uint16
int32,uint32
int64,uint64
string

 

定义好数据接口文件后,需要在CMakeLists.txt中对该文件进行注册,声明其为消息接
口文件,并为其添加builtin_interfaces 依赖,完成后CMakeLists.txt中的主要代码如代码清
单3-28所示。

代码清单3-28 src/status_interfaces/CMakeLists.txt

# find dependencies
find_package (ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(builtin_interfaces REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
"msg/SystemStatus.msg"
DEPENDENCIES builtin_interfaces

)

ament_package ()

 

因为自定义的消息接口中使用了builtin_interfaces/Time来表示时间,所以在上面的文件
中,使用find_package查找builtin_interfaces,并在rosidl_generate_interfaces中添加 builtin_
interfaces作为依赖。
除了修改CMakeLists.txt外,最好在功能包清单文件package.xml中添加声明,完成后,
主要文件内容如代码清单3-29所示。

代码清单3-29 topic_practice_ws/src/status_interfaces/package.xml

<license>Apache-2.0</license>
<member_of_group>rosidl_interface_packages</member_of_group>
<buildtool_depend>ament_cmake</buildtool_depend>

# 系统名称
# CPU 使用率
# 内存使用率
# 内存总量
# 剩余有效内存
# 网络发送数据总量
# 网络接收数据总量

 

在package.xml中添加member_of_group是为了声明该功能包是一个消息接口功能包,
方便ROS 2对其做额外处理。接着来构建功能包,构建完成后,可以使用代码清单3-30中
的命令来确定消息接口是否构建成功。
代码清单3-30 检查接口是否构建成功

$ source install/setup.bash
$ ros2 interface show status_interfaces/msg/SystemStatus

builtin_interfaces/Time stamp # 记录时间
int32 sec
uint32 nanosec
builtin_interfaces/Time stamp #记录时间戳
string host_name
float32 cpu_percent
float32 memory_percent
float32 memory_total
float32 memory_available
float64 net_sent
float64 net_recv

 

需要注意的是,在使用命令行查看前,同样使用source指令来告诉ROS 2功能包的安
装位置。除了使用命令行工具来查看是否构建成功外,你还可以查看install/status_interfaces/
include/目录下是否生成了C++头文件,以及install/status_interfaces/local/lib/python3.10/dist-
packages目录下是否生成了status_interfaces的Python库。
到这里便完成了自定义消息接口的工作。有了消息接口,接下来我们就可以编写代码,
使用接口来传递数据了。

3.4.3 系统信息获取与发布
在src目录下,使用代码清单3-31中的命令创建status_publisher功能包,并添加消息接
口 status_interfaces 和客户端库rclpy作为其依赖。
代码清单3-31 创建 status_publisher 功能包

$ ros2 pkg create status_publisher -- build-type ament_python -- dependencies rclpy
status_interfaces -- license Apache-2.0

 

接着在功能包对应目录下创建文件sys_status_pub.py,在该文件中编写如代码清单3-32
所示的代码。

import rclpy
from rclpy.node import Node
from status_interfaces.msg import SystemStatus # #
import psutil
import platform

class SysStatusPub (Node) :
def _init_(self, node_name) :
super () ._ init_(node_name)
self.status_publisher _= self.create_publisher (
SystemStatus, 'sys_status', 10)
self.timer = self.create_timer(1, self.timer_callback)

def timer_callback(self):
cpu_percent = psutil.cpu_percent ()
memory_info = psutil.virtual_memory ()
net_io_counters = psutil.net_io_counters ()

msg = SystemStatus ()
msg.stamp = self.get_clock().now().to_msg()
msg.host_name = platform.node ()
msg.cpu_percent = cpu_percent
msg.memory_percent = memory_info.percent
msg.memory_total = memory_info.total / 1024 / 1024
msg.memory_available = memory_info.available / 1024 / 1024
msg.net_sent = net_io_counters.bytes_sent / 1024 / 1024
msg.net_recv = net_io_counters.bytes_recv / 1024 / 1024

self.get_logger() .info(f' **: {str(msg)}')
self.status_publisher _. publish(msg)

 

代码清单3-32 status_publisher/status_publisher/sys_status_pub.py

def main():
rclpy.init ()
node = SysStatusPub('sys_status_pub' )
rclpy.spin(node)
rclpy.shutdown ()

 

在代码清单3-32中,首先导入rclpy和Node,接着从status_interfaces.msg 导入 SystemStatus
类,然后导入psutil和platform。通过psutil可以获取系统的CPU、内存以及网络信息,通
过platform模块可以获取当前主机的名称。
定义SysStatusPub类,并让其继承Node。然后在初始化函数中创建发布者status_
publisher_和定时器timer,定时器会每隔1s调用一次timer_callback来发布数据。在timer_
callback方法中,利用从Node继承而来的内置方法get_clock().now()获取当前节点时钟
时间,并通过to_msg()转换成builtin_interfaces.msg.Time消息对象赋值给stamp。然后利
用psutil获取CPU占用率、内存信息和网络信息。下一步是构造SystemStatus消息,通
过platform.node获取主机名称,依次对剩余数据进行赋值。因为默认的数据单位都是字节
(B),连续除上两次1024后将单位改为MB。最后将数据转成字符串输出,并调用 status
publisher_将数据发布出去。
main函数中都是基础操作,这里不展开介绍。接下来在setup.py中对sys_status_pub节
点进行注册,使用代码清单3-33中的命令进行编译运行。
代码清单3-33 运行系统状态发布节点

$ ros2 run status_publisher sys_status_pub

[INFO] [1681661308.321525494] [sys_status_pub]: *: status_interfaces.msg.
SystemStatus (stamp=builtin_interfaces.msg.Time (sec=1681661308, nanosec=
315946639), host_name='fishros-VirtualBox', cpu_percent=3.0, memory_percent=
59.6, memory_total=3923.5078125, memory_available=1583.6796875, net_sent=
2.044133186340332, net_recv=1.3831653594970703)

 

除了可以在终端中看到数据,还可以通过命令行工具查看。打开新的终端,依次输入代
码清单3-34中的两个命令。
代码清单3-34 使用命令行输出/sys_status话题数据
$ source install/setup.bash # 输出话题需要的消息接口,使用source添加到环境变量

$ ros2 topic echo /sys_status

stamp:
sec: 1681662249
nanosec: 315808820
host_name: fishros-VirtualBox
cpu_percent: 16.799999237060547
memory_percent: 60.900001525878906
memory_total: 3923.5078125
memory_available: 1532.8125
net_sent: 2.4008235931396484
net_recv: 1.6205368041992188

 

到这里,我们便把系统状态获取以及发布部分完成了,接下来一起编写一个界面来将数
据可视化。

 

代码清单3-35 创建status_display功能包

status_interfaces

3.4.4 在功能包中使用Qt

Qt是一个跨平台软件设计与开发工具。ROS 2中的大量工具都使用Qt作为界面设计工
具,比如海龟和可视化工具rqt(ROS Qt-based Gui Toolkit,ROS基于Qt的图形化工具集)
都是基于Qt完成的,所以这里我们也使用Qt来完成界面显示功能。首先来学习下如何在功
能包中使用Qt。
打开终端,输入代码清单3-35中的命令,新建status_display,并添加status_interfaces
作为其依赖。

$ ros2 pkg create status_display -- build-type ament_cmake -- dependencies rclcpp
-- license Apache-2.0

接着在该功能包的status_display/src目录下新建hello_qt.cpp,编写代码清单3-36中的
代码。

#include <QApplication>
#include <QLabel>
#include <QString>

int main (int argc, char* argv[]) {
QApplication app(argc, argv);
QLabel* label =new QLabel();
QString message = QString :: fromStdString("Hello Qt!");
label->setText (message);
label->show();
app.exec();
return 0;

 

代码清单3-36 简单的显示界面

在代码清单3-36中分别引入了三个头文件,其中,QApplication提供了Qt应用类
QApplication;QLabel是Qt中用于显示文本的组件,下面我们将用它来显示数据;QString
是Qt中的字符串类,用于存储字符串。
接下来在主函数中创建了一个QApplication的对象app<然后又创建了一个QLabel类的
对象指针,并使用new为其分配了一块动态内存。接着调用QString :: fromStdString从字符串
创建一个QString的对象message,调用lable的setText方法,设置其显示内容为message。
设置完成后,使用label->show();将该文本组件显示出来。调用app.exec进行事件循环,需
要注意的是,app.exec和ROS 2的spin有异曲同工之妙,都是在方法内不断循环处理事件,
因此都会阻塞程序继续往下执行。
编写完代码,接着修改CMakeLists.txt,添加hello_qt节点,并为其添加Qt相关依赖,
最终CMakeLists.txt的主要内容如代码清单3-37所示。

代码清单3-37 topic_practice_ws/src/status_display/CMakeLists.txt

cmake_minimum_required (VERSION 3.8)

find_package (status_interfaces REQUIRED)
find_package (Qt5 REQUIRED COMPONENTS Widgets)

add_executable(hello_qt src/hello_qt.cpp)
target_link_libraries(hello_qt Qt5 :: Widgets)

install (TARGETS hello_qt
DESTINATION lib/${PROJECT_NAME})

ament_package ()

 

因为Qt是由多个组件构成的,这里使用find_packages时,指定查找了Qt5下的图形用
户界面组件Widgets。需要注意的是,因为Qt5是和ROS 2无关的第三方库,所以没有使用
ament_target_dependencies添加依赖,而是使用target_link_libraries进行添加,添加时同样指
定组件为Widgets。
完成代码编写以及配置后,在终端依次输入代码清单3-38中的命令来构建功能包并运行
节点。

代码清单3-38 运行 hello_qt
$ colcon build -- packages-select status_display
$ source install/setup.bash
$ ros2 run status_display hello_qt

看到图3-6所示的界面,你的第一个Qt项目就完成了。趁热打铁,我们来学习如何结合
ROS 2来使用Qt显示话题数据。

h ...     x
Hello Qt!

 

图3-6 Qt界面

3.4.5 订阅数据并用 Qt显示
在 src/status_display/src下新建sys_status_display.cpp,编写代码清单3-39中的代码。
代码清单3-39 使用Qt显示数据

#include <QApplication>
#include <QLabel>
#include <QString>
#include "rclcpp/rclcpp.hpp"
#include "status_interfaces/msg/system_status.hpp"
using SystemStatus = status_interfaces: :msg: : SystemStatus;
class SysStatusDisplay : public rclcpp: :Node {
public:
SysStatusDisplay () : Node("sys_status_display") {

}

A

subscription _= this->create_subscription<SystemStatus>(
"sys_status", 10, [&] (const SystemStatus: :SharedPtr msg) -> void {
label _- >setText (get_qstr_from_msg (msg));

// 创建一个空的 SystemStatus 对象,转化成 QString 进行显示
label _= new QLabel(get_qstr_from_msg(std :: make_shared
<SystemStatus>()));
label _- >show();

QString get_qstr_from_msg(const SystemStatus :: SharedPtr msg) {
// TODO:将msg中的内容提取出来并组装成字符串
return QString :: fromStdString("");

private:
rclcpp: : Subscription<SystemStatus>: :SharedPtr subscription _;
QLabel* label _;

}:

这里首先引入了Qt相关头文件、rclcpp和通过自定义接口生成的system_status.hpp
头文件。为了使代码更易读且更简洁,这里使用了using指令,为status_interfaces :: msg ::
SystemStatus 建立了一个别名SystemStatus。然后定义了一个SysStatusDisplay类,使其继
承rclcpp :: Node。在构造函数中,直接给定了节点名称为sys_status_display。接着调用了
create_subscription对subscription_进行初始化,这里使用Lambda表达式作为回调函数,
Lambda表达式的参数列表中的&符表示它可以通过引用的方式直接捕获外部变量,这也是
可以在表达式中直接调用label_设置文本的原因。初始化订阅者后,继续对label_进行初始
化,这里使用函数嵌套调用,创建了一个空的SystemStatus消息,最后调用get_qstr_from_
msg将其转换为QString 对象进行显示。
继续完善get_qstr_from_msg方法,在该方法中添加如代码清单3-40所示的代码。
代码清单3-40 sys_status_display.cpp/get_qstr_from_msg 方法
QString get_qstr_from_msg(const SystemStatus :: SharedPtr msg) {
std :: stringstream show_str;
show_str
<< " =========== 系统状态可视化显示工具 ============ \n"
<< "数据时间:\t" << msg->stamp.sec << "\ts\n"
<< "用户名:\t" << msg->host_name << "\t\n"
<< "CPU使用率:\t" << msg->cpu_percent << "\t%\n"
<< "内存使用率:\t" << msg->memory_percent << "\t%\n"
<< "内存总大小:\t" << msg->memory_total << "\tMB\n"
<< "剩余有效内存:\t" << msg->memory_available << "\tMB\n"
<< "网络发送量:\t" << msg->net_sent << "\tMB\n"
<< "网络接收量:\t" << msg->net_recv << "\tMB\n"
<< " ====..

return QString :: fromStdString (show_str.str());

}

 

}

我们知道rclcpp :: spin和app.exec都会阻塞程序运行,如果先调用rclcpp :: spin,就会导致
app.exec无法执行,进而无法处理Qt相关事件,最终导致无法正常显示。反之先调用app.
exec,就会导致rclcpp :: spin无法执行,同样会导致ROS 2无法正常处理事件,最后无法正
常接收和发送数据。所以在上面的代码中使用了多线程,将spin单独放到一个线程中进行处
理,当界面退出后,app.exec方法退出,此时调用rclcpp :: shutdown,spin_thread会自动结束,
程序正常退出。
写好代码,修改CMakeLists.txt,添加代码清单3-42中的命令,对sys_status_display节
点进行注册。

在代码清单3-40中,我们使用了stringstream和其运算符“ << ”,分别提取了msg中
的数据并进行组装,其中时间戳部分只用到了秒。接着通过其成员方法str()将其转换成
std :: string类型,然后给到QString :: fromStdString函数,生成一个QString对象并返回。
最后来编写主函数,在代码清单3-40的最后,添加如代码清单3-41所示的代码。
代码清单3-41 sys_status_display.cpp/main 函数
int main(int argc, char* argv[]) {
rclcpp :: init (argc, argv);
QApplication app(argc, argv);
auto node = std: : make_shared<SysStatusDisplay>();
std: : thread spin_thread ( [&] () -> void { rclcpp :: spin(node); });
spin_thread.detach();
app.exec ();
rclcpp :: shutdown();
return 0;

代码清单3-42 topic_practice_ws/src/status_display/CMakeLists.txt

add_executable(sys_status_display src/sys_status_display.cpp)
target_link_libraries(sys_status_display Qt5 :: Widgets) # 对于非ROS功能包使用Cmake
原生指令进行链接库
ament_target_dependencies (sys_status_display rclcpp status_interfaces)

install (TARGETS
hello_qt
sys_status_display
DESTINATION lib/${PROJECT_NAME})

ament_package ()

接着构建并运行该节点,运行结果如图3-7所示。
因为没有启动发布者,所以没有数据,打开新的终端,启动发布者后,再次观察界面,
结果如图3-8所示。
好了,看到界面中不断跳动的数据,你的内心一定很开心吧。是的,这个项目涉及了很
多新的知识点,确实不容易。不过不管怎么说,你的第一个实践项目已经成功完成了,你肯

定也想把它好好地保存起来吧,那么下一节就来学习代码版本管理工具Git,帮助我们更好
地管理代码。稍事休息,然后一口气完成本章剩余部分的学习吧。

-□X

x

0

S

0

%

06

O

0

0

0

MB

MB

MB
MB

sys_status_display
■=系统状态可视化显示工具
数据时间:
主机名称:
CPU使用率:
内存使用率:
内存总大小:
剩余有效内存:
网络发送量:
网络接收量:

1681671509
fishros-VirtualBox
6.3

57.4
3923.51 MB

1277.4 MB

4.63225 MB
4.06388 MB

sys_status_display
:■=系统状态可视化显示工具:
数据时间:
主机名称:
CPU使用率:
内存使用率:
内存总大小:
剩余有效内存:
网络发送量:
网络接收量:

-

X

图3-7 默认显示结果

图3-8 接收话题数据显示结果

新建代码仓库

3.5 ROS 2基础之 Git入门

ROS 2除了系统本身,另一个让人着迷的地方就是其强大的社区。在Github等代码托管
平台上,有着无数的优秀开源程序供我们使用,而这些程序无一例外都是使用Git进行版本
管理,所以掌握好Git才能更好地拥抱ROS 2的生态。
Git是一个免费的开源分布式版本控制系统工具,它是由Linux内核开发者Linus
Torvalds在2005年开发的。在前面的学习中,我们已经安装过Git,下面直接来学习如何使
用它。

3.5.1 新建代码仓库

在2.5.3节我们已经简单地使用了Git,从网上下载了一个C++网络库代码到本地。但下
载代码只是Git最基本的功能。现在一起来学习如何在本地建立一个Git仓库。
在创建仓库之前,需要对Git进行一些配置,第一个需要配置的是你的用户名和邮箱,
这样当你提交代码时,Git就会把你的信息一起放到提交中,在多人协作开发同一仓库时,
就可以用于区分哪些代码是谁提交的。打开任意一个终端,输入代码清单3-43中的命令进行
配置。

代码清单3-43 配置 Git用户和邮箱
$ git config -- global user.name "Fish"#你的用户名
$ git config -- global user.email "fish@fishros.com"#配置你的邮箱

配置好用户信息后,接着来配置默认的分支名称,使用代码清单3-44中的命令将默认的
分支名称修改为master,分支概念后续会再次学习。
代码清单3-44 配置默认分支
$ git config -- global init.defaultBranch master

完成所有配置后,可以使用代码清单3-45中的命令查看所有的配置,以此确认对应项是
否配置成功。

代码清单3-45 查看Git配置

$ git config -1

user.name=Fish
user.email=fish@fishros.com
init.defaultbranch=master
core.repositoryformatversion=0
core.filemode=true
core.bare=false
core.logallrefupdates=true

接着在本地创建一个代码仓库,进入chapt3/topic_practice_ws目录,输入代码清单3-46
中的命令进行创建。

$ git init

已初始化空的Git 仓库于/home/fishros/chapt3/topic_practice_ws/.git/

可以看到,终端提示已经在指定目录初始化了一个空的仓库。在Linux中以“.”开头的
文件和文件夹属于隐藏目录,使用ls-a才能查看,如代码清单3-47所示。
代码清单3-47 查看所有文件(包含隐藏内容)

代码清单3-46 初始化仓库

$ 1s -a

build .git
install

当我们提交代码时,所有更改都会保存到.git目录下,同样,如果你想删除仓库,直接
运行rm-rf.git删除目录就可以了。

3.5.2 学会提交代码

有了仓库,接下来我们来提交代码。提交代码只需要用到add和commit两个命令,add
命令用于将修改的文件添加到Git的暂存区中,而commit命令则将暂存区中的修改提交到本
地Git仓库中。比如我们想把文件src/status_interfaces/package.xml添加到暂存区,可以使用
代码清单3-48中的命令。
代码清单3-48 将package.xml 添加到暂存区
$ git add src/status_interfaces/package.xml

一次添加一个文件太慢,可以一次性把整个目录添加到暂存区,比如添加src目录下的
所有文件,可以使用代码清单3-49中的命令。

log src .vscode

代码清单3-49 添加一个目录到暂存区

$ git add src

如果工程目录比较多,一次添加一个目录还是太慢,在add指令后加上一个“.”,即可
添加所有的文件,打开终端,运行代码清单3-50中的命令就可以了。
代码清单3-50 添加当前终端目录所有文件到暂存区

$ git add .

运行完add指令,如果暂存区里有文件,其实就可以直接提交了。但我不建议你直接提
交,因为在我们的工程中,install、build和log这三个目录是编译产生的目录,并非工程代码,
而使用add.就将所有文件都添加到暂存区了,现在直接提交就会把它们也提交进去。所以
可以使用另外一个命令,将文件从暂存区都踢出来,这个命令就是reset,在终端中输入代码
清单3-51中的命令。

$ git reset

接着只添加src目录到暂存区,然后调用commit进行提交,依次输入代码清单3-52中
的命令。

$ git add src
$ git commit -m"完成状态发布与显示功能”

[master(根提交)243169b]完成状态发布与显示功能
19 files changed, 968 insertions (+)
create mode 100644 src/status_display/CMakeLists.txt
create mode 100644 src/status_display/LICENSE
create mode 100644 src/status_display/package.xml

代码清单3-51 将暂存区所有文件踢出

代码清单3-52 添加 src目录到暂存区并提交

注意,我们在commit指令后加上了-m选项并附上了当前代码的描述信息,在提交时一
定要添加上描述信息,否则就会提交失败。提交完成后,就可以使用log命令查看历史的提
交记录,输入代码清单3-53中的命令。
代码清单3-53 查看提交日志

Tue Apr 18 00:52:27 2023 +0800

$ git log

commit 243169b5b4bb18db534013340ca802b3954df55b (HEAD -> master)
Author: Fish <fish@fishros.com>
Date:

完成状态发布与显示功能

可以看到历史的提交记录,其中包括编号、作者名字、邮箱、提交时间和描述信息。好
了,到这里你已经成功地完成了第一次代码提交。

3.5.3

学会使用Git忽略文件
相比使用add加目录或文件进行添加,使用git add.命令显然更加方便,但在工程中存
在不想被提交的文件或目录,又不想一个个手动添加,该怎么办呢?此时就可以使用Git忽
略文件。在topic_practice_ws下新建文件.gitignore,接着将想要忽略的目录或者文件名称写
到文件中,这里我们将ROS 2构建过程中产生的目录添加进去,最终内容如代码清单3-54
所示。

代码清单 3-54 topic_practice_ws/.gitignore

build/
install/
log/

完成之后,再把.gitignore文件提交,经常提交代码是一个好习惯,在终端中依次输入
代码清单3-55中的命令。

$ git add .gitignore
$ git commit -m"添加Git忽略文件”

[master 3071f21] 添加Git忽略文件
3 files changed, 98 insertions (+)
create mode 100644 .gitignore

提交完成后,再次使用add.和commit看看能否提交成功,在终端中输入代码清单3-56
中的命令。

$ git add
$ git commit -m"测试提交临时目录”

位干分支 master
无文件要提交,干净的工作区

从提示可以看出,install、build和log这三个目录已经被成功忽略掉了。.gitignore除了
可以对指定文件夹和文件进行忽略外,还可以使用通配符“*”来忽略某一类的文件,比如
在.gitignore 中添加 *. log,就会忽略所有以,log为后缀的文件。
好了,关于Git的学习到这里就先告一段落了。虽然只是简单的几个命令,但其实你已
经掌握了Git的基本用法,后续我们也还会继续学习Git的更多用法。现在让我们对本章做
一个小结吧。

代码清单3-55 提交.gitignore 文件

代码清单3-56 测试.gitignore是否生效

3.6 小结与点评

本章可谓是干货满满,我们围绕着ROS 2话题这一通信机制,不仅学习了如何在Python

和C++中使用话题进行通信,还学习了语音合成和闭环运动控制。话题通信是ROS2四大
通信机制的第一个,也是在机器人开发中用得最多的一个。
在话题通信最佳实践环节,你一定收获颇丰。我们在话题实践的同时,不仅掌握了如
何自定义通信接口,还学习了如何获取系统实时状态信息,以及在ROS 2中使用Qt制作界
面的方法。通过最佳实践这一例子,相信你对话题通信以及ROS 2的使用已经有了更深的
认识。
另外,在本章的最后,我们进一步学习了使用Git建立仓库和提交代码的方法。当然这
也只是一个初步的学习,后续我们还会继续探讨更多Git的相关内容。

 

 

 

posted on 2026-01-16 09:56  前沿风暴  阅读(89)  评论(0)    收藏  举报

导航


校内网 © 2004-2026

京公网安备 33010602011771号 京ICP备2021040463号-3