ROS入门篇

2023-10-16 14:40:39 浏览数 (2)

ROS安装

我们这里使用的是Ubuntu 20.04系统来进行安装。

  • 添加ROS软件源
代码语言:javascript复制
sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main">/etc/apt/sources.list.d/ros-latest.list'
  • 添加秘钥
代码语言:javascript复制
sudo apt-key adv --keyserver 'hkp://keyserver.ubuntu.com:80' --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654
  • 安装ROS
代码语言:javascript复制
sudo apt-get update
sudo apt install ros-noetic-desktop-full

如果是Ubuntu 18.04,可以使用

代码语言:javascript复制
sudo apt install ros-melodic-desktop-full

进行安装,此次安装的时间会比较长。

添加环境变量

代码语言:javascript复制
sudo vim /etc/profile

内容如下

代码语言:javascript复制
source /opt/ros/noetic/setup.bash
export ROS_PACKAGE_PATH=${ROS_PACKAGE_PATH}:~/ros_ws/src

如果是Ubuntu 18.04,则为

代码语言:javascript复制
source /opt/ros/melodic/setup.bash
export ROS_PACKAGE_PATH=${ROS_PACKAGE_PATH}:~/ros_ws/src

执行

代码语言:javascript复制
source /etc/profile
sudo apt-get install python3-rosdep2
sudo vim /etc/hosts

如果是Ubuntu 18.04,则为

代码语言:javascript复制
source /etc/profile
sudo apt-get install python3-rosdep
sudo vim /etc/hosts

添加内容

代码语言:javascript复制
199.232.28.133 raw.githubusercontent.com

执行

代码语言:javascript复制
sudo rm /etc/ros/rosdep/sources.list.d/20-default.list
sudo rosdep init
rosdep update

如果是Ubuntu 18.04,则为

代码语言:javascript复制
sudo rosdep init
rosdep update
  • 安装rosinstall
代码语言:javascript复制
sudo apt install python3-rosinstall python3-rosinstall-generator python3-wstool
sudo apt-get install ros-noetic-roslaunch

如果是Ubuntu 18.04,则为

代码语言:javascript复制
sudo apt install python3-catkin-pkg
sudo apt install python3-rosinstall python3-rosinstall-generator python3-wstool
sudo apt-get install ros-melodic-roslaunch

安装完成后输入

代码语言:javascript复制
roscore

输出

代码语言:javascript复制
... logging to /home/guanjian/.ros/log/cb18d28e-35fe-11ee-b678-03d041231f43/roslaunch-guanjian-X99-38074.log
Checking log directory for disk usage. This may take a while.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://guanjian-X99:34311/
ros_comm version 1.16.0


SUMMARY
========

PARAMETERS
 * /rosdistro: noetic
 * /rosversion: 1.16.0

NODES

auto-starting new master
process[master]: started with pid [38083]
ROS_MASTER_URI=http://guanjian-X99:11311/

setting /run_id to cb18d28e-35fe-11ee-b678-03d041231f43
process[rosout-1]: started with pid [38093]
started core service [/rosout]

表示安装成功

  • 测试
代码语言:javascript复制
sudo apt install ros-noetic-rosbash
sudo apt install ros-noetic-catkin
sudo apt-get install ros-noetic-turtlesim

如果是Ubuntu 18.04,则为

代码语言:javascript复制
sudo apt install ros-melodic-rosbash
sudo apt install ros-melodic-catkin
sudo apt-get install ros-melodic-turtlesim

执行

代码语言:javascript复制
rosrun turtlesim turtlesim_node

会出现一个小海龟的画面

执行

代码语言:javascript复制
rosrun turtlesim turtle_teleop_key

这样就可以使用键盘的方向键来控制这个小海龟了。

  • 安装rqt工具

ros rqt工具是使用qt开发的一系列直观可视化工具

代码语言:javascript复制
sudo apt-get install ros-noetic-rqt
sudo apt-get install ros-noetic-rqt-graph
sudo apt-get install ros-noetic-rqt-common-plugins

如果是Ubuntu 18.04,则为

代码语言:javascript复制
sudo apt-get install ros-melodic-rqt
sudo apt-get install ros-melodic-rqt-graph
sudo apt-get install ros-melodic-rqt-common-plugins
  • 安装图像处理以及传输工具
代码语言:javascript复制
sudo apt-get install ros-noetic-cv-bridge
sudo apt-get install ros-noetic-image-transport

如果是Ubuntu 18.04,则为

代码语言:javascript复制
sudo apt-get install ros-melodic-cv-bridge
sudo apt-get install ros-melodic-image-transport

配置jetson nano opencv的目录(这里只针对18.04)

代码语言:javascript复制
cd /opt/ros/melodic/share/cv_bridge/cmake/
sudo vim cv_bridgeConfig.cmake

将94行和96行的两个opencv改成opencv4

ROS的核心概念

通讯机制

ROS是一个松耦合的分布式软件框架。上图中有很多的Node(节点),每个节点是在机器人系统中完成一个具体功能的进程,比方说有的Node是完成图像识别,有的Node是完成一个图像的驱动,它们之间会有一系列图像的传输。节点与节点之间位置也不是固定的,比如上图中有两台电脑,一个A,一个B,有些节点可以在A当中,有些节点可以在B当中,它们之间可以通过一系列的传输方式来完成通讯。每个节点的编程语言也不是固定的,可以使用Python,也可以使用C 。

上图中有三个节点,分别为摄像头节点、图像处理节点、图像显示节点。

  • 节点(Node)——执行单元
  • 执行具体任务的进程,独立运行的可执行文件;
  • 不同节点可使用不同的编程语言,可分布式运行在不同的主机;
  • 节点在系统中的名称必须是唯一的。
  • 节点管理器(ROS Master)——控制中心
  • 为节点提供命名和注册服务;
  • 跟踪和记录话题/服务通信,辅助节点相互查找,建立连接;
  • 提供参数服务器,节点使用此服务器存储和检索运行时参数。

节点之间的通讯有两种核心的通讯方式——话题、服务。

话题

  • 话题(Topic)——异步通信机制
  • 节点间用来传输数据的重要总线,它是一个数据管道;
  • 使用发布/订阅模型,数据由发布者传输到订阅者,同一个话题的订阅者或发布者可以不唯一。在上图中有一个发布节点(Publisher,比如一个摄像头节点,它会发布图像数据给别人做处理)以及两个订阅节点(Subscriber,需要处理摄像头发布的图像数据的节点,需要去订阅摄像头节点)。数据流向是单向的,从发布者到订阅者。
  • 消息(Message)——话题数据
  • 具有一定类型和数据结构,包括ROS提供的标准类型和用户自定义类型;
  • 使用编程语言无关的.msg文件定义,编译过程中生成对应的代码文件。

服务

  • 服务(Service)——同步通信机制
  • 使用客户端/服务器(C/S)模型,客户端发送请求数据,服务器完成处理后返回应答数据,它是一种双向通讯;
  • 使用编程语言无关的.srv文件定义请求和应答数据结构,编译过程中生成对应的代码文件。

话题与服务的区别

话题

服务

同步性

异步

同步

通信模型

发布/订阅

服务器/客户端

底层协议

ROSTCP/ROSUDP

ROSTCP/ROSUDP

反馈机制

缓冲区

实时性

节点关系

多对多

一对多(一个server)

适用场景

数据传输

逻辑处理

参数

它是在ROS Master里面维护一个参数服务器,所有节点都可以通过网络访问。

  • 参数(Parameter)——全局共享字典
  • 可通过网络访问的共享、多变量字典;
  • 节点使用此服务器来存储和检索运行时的参数;
  • 适合存储静态、非二进制的配置参数,不适合存储动态配置的数据。

文件系统

  • 功能包(Package)

ROS软件中的基本单元,包含节点源码、配置文件、数据定义等

  • 功能包清单(Package manifest)

记录功能包的基本信息,包含作者信息、许可信息、依赖选项、编译标志等

  • 元功能包(Meta Packages)

组织多个用于同一目的的功能包

ROS命令行工具

启动ROS Master

代码语言:javascript复制
roscore

运行一个节点

代码语言:javascript复制
rosrun turtlesim turtlesim_node

rosrun一般会有两个参数,第一个参数turtlesim是功能包名,第二个参数turtlesim_node是节点,它是一个仿真器节点,就是之前看到的小海龟。

运行另一个节点

代码语言:javascript复制
rosrun turtlesim turtle_teleop_key

该节点是用键盘上下左右键来控制小海龟移动的节点。

  • 分析工具
代码语言:javascript复制
rqt_graph

这是一个显示系统计算图的工具,ROS的核心通讯机制就是一个计算图,通过图可以很快的了解整个系统的全貌。

该界面列出了当前系统中的节点(椭圆部分),这里为2个节点,一个turtlesim小海龟仿真器节点,一个teleop_turtle键盘控制节点。两个节点存在着数据通讯,它们中间存在着一个话题/turtle1/cmd_vel,专门用来通讯的。

  • 无图形界面的节点查看命令
代码语言:javascript复制
rosnode

此时会返回它后面的参数以及参数的含义

代码语言:javascript复制
rosnode is a command-line tool for printing information about ROS Nodes.

Commands:
	rosnode ping	test connectivity to node
	rosnode list	list active nodes
	rosnode info	print information about node
	rosnode machine	list nodes running on a particular machine or list machines
	rosnode kill	kill a running node
	rosnode cleanup	purge registration information of unreachable nodes

Type rosnode <command> -h for more detailed usage, e.g. 'rosnode ping -h'

此时我们使用

代码语言:javascript复制
rosnode list

会返回所有活动的节点

代码语言:javascript复制
/rosout
/teleop_turtle
/turtlesim

其中的后面两个节点是我们启动的小海龟仿真器以及键盘控制器,而/rosout是ROS默认的话题,它是采集所有节点的日志信息用来提交给后面的界面做显示的,一般不用关心。

查看某一个节点具体的信息

代码语言:javascript复制
rosnode info /turtlesim

此时会返回

代码语言:javascript复制
Node [/turtlesim]
Publications: 
 * /rosout [rosgraph_msgs/Log]
 * /turtle1/color_sensor [turtlesim/Color]
 * /turtle1/pose [turtlesim/Pose]

Subscriptions: 
 * /turtle1/cmd_vel [geometry_msgs/Twist]

Services: 
 * /clear
 * /kill
 * /reset
 * /spawn
 * /turtle1/set_pen
 * /turtle1/teleport_absolute
 * /turtle1/teleport_relative
 * /turtlesim/get_loggers
 * /turtlesim/set_logger_level


contacting node http://guanjian-X99:46261/ ...
Pid: 4769
Connections:
 * topic: /rosout
    * to: /rosout
    * direction: outbound (57873 - 127.0.0.1:56794) [27]
    * transport: TCPROS
 * topic: /turtle1/cmd_vel
    * to: /teleop_turtle (http://guanjian-X99:40209/)
    * direction: inbound (46338 - guanjian-X99:43423) [29]
    * transport: TCPROS

其中Publications是该节点发布的信息,比如说/turtle1/pose turtlesim/Pose表示小海龟的位置。Subscriptions是该节点订阅的信息,这里它订阅了一个/turtle1/cmd_vel geometry_msgs/Twist,表示通过键盘指令发过去的数据,通过它订阅的数据控制小海龟的移动。Services是该节点发布的服务,通过其中的服务来完成一些配置。最下面的是一些Id号以及底层通讯机制。

  • 话题工具
代码语言:javascript复制
rostopic

此时会返回它后面的参数以及参数的含义

代码语言:javascript复制
rostopic is a command-line tool for printing information about ROS Topics.

Commands:
	rostopic bw	display bandwidth used by topic
	rostopic delay	display delay of topic from timestamp in header
	rostopic echo	print messages to screen
	rostopic find	find topics by type
	rostopic hz	display publishing rate of topic    
	rostopic info	print information about active topic
	rostopic list	list active topics
	rostopic pub	publish data to topic
	rostopic type	print topic or field type

Type rostopic <command> -h for more detailed usage, e.g. 'rostopic echo -h'

查看系统当前所有的话题列表

代码语言:javascript复制
rostopic list

此时返回

代码语言:javascript复制
/rosout
/rosout_agg
/turtle1/cmd_vel
/turtle1/color_sensor
/turtle1/pose

其中/turtle1/cmd_vel就为为小海龟发布控制指令的话题,键盘控制节点和小海龟控制节点就是通过该话题来通讯的。

发布指令让海龟运动

代码语言:javascript复制
rostopic pub /turtle1/cmd_vel geometry_msgs/Twist '[1.0,0.0,0.0]' '[0.0,0.0,0.0]'

这里的'1.0,0.0,0.0'指的是线速度linear,'0.0,0.0,0.0'指的是角速度angular。geometry_msgs/Twist指的是消息的数据结构。

该指令会让小海龟动起来,但是动了一小段之后会停下来,这是因为pub指令只会发布一次,为了让海龟一直动,需要给它一个循环

代码语言:javascript复制
rostopic pub -r 10 /turtle1/cmd_vel geometry_msgs/Twist '[1.0,0.0,0.0]' '[0.0,0.0,0.0]'

这里-r表示频率,以多少的频率去发布消息内容,-r 10表示10Hz,一秒钟发布10次。

查看消息数据结构的内容

代码语言:javascript复制
rosmsg show geometry_msgs/Twist

这里查看的是geometry_msgs/Twist的数据结构,返回

代码语言:javascript复制
geometry_msgs/Vector3 linear
  float64 x
  float64 y
  float64 z
geometry_msgs/Vector3 angular
  float64 x
  float64 y
  float64 z
  • 服务工具
代码语言:javascript复制
rosservice

此时会返回它后面的参数以及参数的含义

代码语言:javascript复制
Commands:
	rosservice args	print service arguments
	rosservice call	call the service with the provided args
	rosservice find	find services by service type
	rosservice info	print information about service
	rosservice list	list active services
	rosservice type	print service type
	rosservice uri	print service ROSRPC uri

Type rosservice <command> -h for more detailed usage, e.g. 'rosservice call -h'

查看当前的所有服务

代码语言:javascript复制
rosservice list

返回

代码语言:javascript复制
/clear
/kill
/reset
/rosout/get_loggers
/rosout/set_logger_level
/spawn
/teleop_turtle/get_loggers
/teleop_turtle/set_logger_level
/turtle1/set_pen
/turtle1/teleport_absolute
/turtle1/teleport_relative
/turtlesim/get_loggers
/turtlesim/set_logger_level

这里的所有服务都是海龟仿真器中的服务

产生一只新的海龟

代码语言:javascript复制
rosservice call /spawn "x: 2.0
y: 2.0
theta: 0.0
name: 'turtle2'" 
  • 话题记录
代码语言:javascript复制
rosbag record -a -O cmd_record

这里的record表示记录的意思,-a为保存所有的记录,-O表示将数据保存成压缩包,cmd_record为一个保存的名字

返回

代码语言:javascript复制
[ INFO] [1691623907.802537745]: Recording to 'cmd_record.bag'.
[ INFO] [1691623907.803795811]: Subscribing to /rosout_agg
[ INFO] [1691623907.806439843]: Subscribing to /rosout
[ INFO] [1691623907.809232127]: Subscribing to /turtle1/pose
[ INFO] [1691623907.811861515]: Subscribing to /turtle1/color_sensor
[ INFO] [1691623907.815194941]: Subscribing to /turtle1/cmd_vel

现在我们可以来操作小海龟做运动,运动结束之后在rosbag命令行窗口按下Ctrl-C,这样记录结果就保存下来了,保存的路径就是/home/user下,user为你自己的用户名,保存的文件名为cmd_record.bag。

  • 话题复现

话题复现,假设我们有一个无人机需要调试,但是不可能每次都让飞机飞起来去调试,我们会做一次飞行,使用rosbag record将所有的数据都保存下来,回到实验室中将数据复现出来再去做实验。现在我们来复现刚才小海龟移动的数据。

先将之前的终端全部关闭,重新打开两个终端,启动ROS Master以及小海龟仿真器节点。

代码语言:javascript复制
rosbag play cmd_record.bag

这样我们就可以看到小海龟走的路径跟记录时候是一样的了。

创建工作空间与功能包

  • 工作空间(workspace)是一个存放工程开发相关文件的文件夹,其下可分为
  • src:代码空间(Source Space),放置所有功能包的代码,配置文件
  • build:编译空间(Build Space),放置在编译过程中所产生的中间文件,无需关注
  • devel:开发空间(Development Space),放置编译生成的可执行文件,库和脚本
  • install:安装空间(Install Space),放置安装位置,内容与开发空间有一定重复

指令集

  • 创建工作空间(我这里是在Documents下)
代码语言:javascript复制
cd Documents/
mkdir -p catkin_ws/src
cd catkin_ws/src/
catkin_init_workspace

此时在工作空间中没有任何的代码,但依然可以编译。

  • 编译工作空间
代码语言:javascript复制
cd ..
catkin_make install

此时会产生另外三个文件夹——build、devel、install

  • 设置环境变量
代码语言:javascript复制
source devel/setup.bash
  • 创建功能包
代码语言:javascript复制
cd src
catkin_create_pkg test_pkg std_msgs rospy roscpp

这里test_pkg为功能包的名称,后面的项都是ROS的依赖,std_msgs为ROS定义的标准的消息结构,rospy为ROS的Python调用,roscpp为ROS的C 调用。

在同一个工作空间下,不允许存在同名功能包;不同工作空间下,允许存在同名功能包。

在test_pkg下会有

代码语言:javascript复制
CMakeLists.txt  include  package.xml  src

其中include和src是文件夹,include是放置C 头文件的,src是放置.cpp代码的。

  • 编译功能包

回到catkin_ws文件夹

代码语言:javascript复制
cd ..
catkin_make
  • 设置环境变量
代码语言:javascript复制
source devel/setup.bash
  •  查看工作空间环境变量
代码语言:javascript复制
echo $ROS_PACKAGE_PATH

此时返回

代码语言:javascript复制
/home/user/Documents/catkin_ws/src:/opt/ros/noetic/share

它表示ROS能够找到我们的功能包的位置(user为你自己的用户名)

  • 功能包中的两个重要文件

在test_pkg中会有两个重要文件——CMakeLists.txt和package.xml,我们来看一下package.xml的内容

代码语言:javascript复制
<?xml version="1.0"?>
<package format="2">
  <name>test_pkg</name>
  <version>0.0.0</version>
  <description>The test_pkg package</description>

  <!-- One maintainer tag required, multiple allowed, one person per tag -->
  <!-- Example:  -->
  <!-- <maintainer email="jane.doe@example.com">Jane Doe</maintainer> -->
  <maintainer email="guanjian@todo.todo">guanjian</maintainer>


  <!-- One license tag required, multiple allowed, one license per tag -->
  <!-- Commonly used license strings: -->
  <!--   BSD, MIT, Boost Software License, GPLv2, GPLv3, LGPLv2.1, LGPLv3 -->
  <license>TODO</license>


  <!-- Url tags are optional, but multiple are allowed, one per tag -->
  <!-- Optional attribute type can be: website, bugtracker, or repository -->
  <!-- Example: -->
  <!-- <url type="website">http://wiki.ros.org/test_pkg</url> -->


  <!-- Author tags are optional, multiple are allowed, one per tag -->
  <!-- Authors do not have to be maintainers, but could be -->
  <!-- Example: -->
  <!-- <author email="jane.doe@example.com">Jane Doe</author> -->


  <!-- The *depend tags are used to specify dependencies -->
  <!-- Dependencies can be catkin packages or system dependencies -->
  <!-- Examples: -->
  <!-- Use depend as a shortcut for packages that are both build and exec dependencies -->
  <!--   <depend>roscpp</depend> -->
  <!--   Note that this is equivalent to the following: -->
  <!--   <build_depend>roscpp</build_depend> -->
  <!--   <exec_depend>roscpp</exec_depend> -->
  <!-- Use build_depend for packages you need at compile time: -->
  <!--   <build_depend>message_generation</build_depend> -->
  <!-- Use build_export_depend for packages you need in order to build against this package: -->
  <!--   <build_export_depend>message_generation</build_export_depend> -->
  <!-- Use buildtool_depend for build tool packages: -->
  <!--   <buildtool_depend>catkin</buildtool_depend> -->
  <!-- Use exec_depend for packages you need at runtime: -->
  <!--   <exec_depend>message_runtime</exec_depend> -->
  <!-- Use test_depend for packages you need only for testing: -->
  <!--   <test_depend>gtest</test_depend> -->
  <!-- Use doc_depend for packages you need only for building documentation: -->
  <!--   <doc_depend>doxygen</doc_depend> -->
  <buildtool_depend>catkin</buildtool_depend>
  <build_depend>roscpp</build_depend>
  <build_depend>rospy</build_depend>
  <build_depend>std_msgs</build_depend>
  <build_export_depend>roscpp</build_export_depend>
  <build_export_depend>rospy</build_export_depend>
  <build_export_depend>std_msgs</build_export_depend>
  <exec_depend>roscpp</exec_depend>
  <exec_depend>rospy</exec_depend>
  <exec_depend>std_msgs</exec_depend>


  <!-- The export tag contains other, unspecified, tags -->
  <export>
    <!-- Other tools can request additional information be placed here -->

  </export>
</package>

它会包含一些基本信息:功能包的名称,版本号,描述,作者以及作者的email地址,还有下面的功能包的ROS依赖。之后我们也可以手动在这里添加依赖项。

然后是CMakeLists.txt中的内容

代码语言:javascript复制
cmake_minimum_required(VERSION 3.0.2)
project(test_pkg)

## Compile as C  11, supported in ROS Kinetic and newer
# add_compile_options(-std=c  11)

## Find catkin macros and libraries
## if COMPONENTS list like find_package(catkin REQUIRED COMPONENTS xyz)
## is used, also find other catkin packages
find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
)

## System dependencies are found with CMake's conventions
# find_package(Boost REQUIRED COMPONENTS system)


## Uncomment this if the package has a setup.py. This macro ensures
## modules and global scripts declared therein get installed
## See http://ros.org/doc/api/catkin/html/user_guide/setup_dot_py.html
# catkin_python_setup()

################################################
## Declare ROS messages, services and actions ##
################################################

## To declare and build messages, services or actions from within this
## package, follow these steps:
## * Let MSG_DEP_SET be the set of packages whose message types you use in
##   your messages/services/actions (e.g. std_msgs, actionlib_msgs, ...).
## * In the file package.xml:
##   * add a build_depend tag for "message_generation"
##   * add a build_depend and a exec_depend tag for each package in MSG_DEP_SET
##   * If MSG_DEP_SET isn't empty the following dependency has been pulled in
##     but can be declared for certainty nonetheless:
##     * add a exec_depend tag for "message_runtime"
## * In this file (CMakeLists.txt):
##   * add "message_generation" and every package in MSG_DEP_SET to
##     find_package(catkin REQUIRED COMPONENTS ...)
##   * add "message_runtime" and every package in MSG_DEP_SET to
##     catkin_package(CATKIN_DEPENDS ...)
##   * uncomment the add_*_files sections below as needed
##     and list every .msg/.srv/.action file to be processed
##   * uncomment the generate_messages entry below
##   * add every package in MSG_DEP_SET to generate_messages(DEPENDENCIES ...)

## Generate messages in the 'msg' folder
# add_message_files(
#   FILES
#   Message1.msg
#   Message2.msg
# )

## Generate services in the 'srv' folder
# add_service_files(
#   FILES
#   Service1.srv
#   Service2.srv
# )

## Generate actions in the 'action' folder
# add_action_files(
#   FILES
#   Action1.action
#   Action2.action
# )

## Generate added messages and services with any dependencies listed here
# generate_messages(
#   DEPENDENCIES
#   std_msgs
# )

################################################
## Declare ROS dynamic reconfigure parameters ##
################################################

## To declare and build dynamic reconfigure parameters within this
## package, follow these steps:
## * In the file package.xml:
##   * add a build_depend and a exec_depend tag for "dynamic_reconfigure"
## * In this file (CMakeLists.txt):
##   * add "dynamic_reconfigure" to
##     find_package(catkin REQUIRED COMPONENTS ...)
##   * uncomment the "generate_dynamic_reconfigure_options" section below
##     and list every .cfg file to be processed

## Generate dynamic reconfigure parameters in the 'cfg' folder
# generate_dynamic_reconfigure_options(
#   cfg/DynReconf1.cfg
#   cfg/DynReconf2.cfg
# )

###################################
## catkin specific configuration ##
###################################
## The catkin_package macro generates cmake config files for your package
## Declare things to be passed to dependent projects
## INCLUDE_DIRS: uncomment this if your package contains header files
## LIBRARIES: libraries you create in this project that dependent projects also need
## CATKIN_DEPENDS: catkin_packages dependent projects also need
## DEPENDS: system dependencies of this project that dependent projects also need
catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES test_pkg
#  CATKIN_DEPENDS roscpp rospy std_msgs
#  DEPENDS system_lib
)

###########
## Build ##
###########

## Specify additional locations of header files
## Your package locations should be listed before other locations
include_directories(
# include
  ${catkin_INCLUDE_DIRS}
)

## Declare a C   library
# add_library(${PROJECT_NAME}
#   src/${PROJECT_NAME}/test_pkg.cpp
# )

## Add cmake target dependencies of the library
## as an example, code may need to be generated before libraries
## either from message generation or dynamic reconfigure
# add_dependencies(${PROJECT_NAME} ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

## Declare a C   executable
## With catkin_make all packages are built within a single CMake context
## The recommended prefix ensures that target names across packages don't collide
# add_executable(${PROJECT_NAME}_node src/test_pkg_node.cpp)

## Rename C   executable without prefix
## The above recommended prefix causes long target names, the following renames the
## target back to the shorter version for ease of user use
## e.g. "rosrun someones_pkg node" instead of "rosrun someones_pkg someones_pkg_node"
# set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "")

## Add cmake target dependencies of the executable
## same as for the library above
# add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

## Specify libraries to link a library or executable target against
# target_link_libraries(${PROJECT_NAME}_node
#   ${catkin_LIBRARIES}
# )

#############
## Install ##
#############

# all install targets should use catkin DESTINATION variables
# See http://ros.org/doc/api/catkin/html/adv_user_guide/variables.html

## Mark executable scripts (Python etc.) for installation
## in contrast to setup.py, you can choose the destination
# catkin_install_python(PROGRAMS
#   scripts/my_python_script
#   DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
# )

## Mark executables for installation
## See http://docs.ros.org/melodic/api/catkin/html/howto/format1/building_executables.html
# install(TARGETS ${PROJECT_NAME}_node
#   RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
# )

## Mark libraries for installation
## See http://docs.ros.org/melodic/api/catkin/html/howto/format1/building_libraries.html
# install(TARGETS ${PROJECT_NAME}
#   ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
#   LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
#   RUNTIME DESTINATION ${CATKIN_GLOBAL_BIN_DESTINATION}
# )

## Mark cpp header files for installation
# install(DIRECTORY include/${PROJECT_NAME}/
#   DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
#   FILES_MATCHING PATTERN "*.h"
#   PATTERN ".svn" EXCLUDE
# )

## Mark other files for installation (e.g. launch and bag files, etc.)
# install(FILES
#   # myfile1
#   # myfile2
#   DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
# )

#############
## Testing ##
#############

## Add gtest based cpp test target and link libraries
# catkin_add_gtest(${PROJECT_NAME}-test test/test_test_pkg.cpp)
# if(TARGET ${PROJECT_NAME}-test)
#   target_link_libraries(${PROJECT_NAME}-test ${PROJECT_NAME})
# endif()

## Add folders to be run by python nosetests
# catkin_add_nosetests(test)

有关CMakeLists.txt的介绍可以参考C 基础整理 中的CMake 的使用

发布者Publisher编程实现

C 实现

进入工作空间

代码语言:javascript复制
cd Documents/catkin_ws/src

创建一个新的话题功能包

代码语言:javascript复制
catkin_create_pkg learning_topic roscpp rospy std_msgs geometry_msgs turtlesim

进入到该功能包的src目录

代码语言:javascript复制
cd learning_topic/src

创建我们需要的C 文件

代码语言:javascript复制
vim velocity_publisher.cpp

内容如下

代码语言:javascript复制
/*
 *  该程序将发布turtle1/cmd_vel话题,消息类型geometry_msgs::Twist
*/

#include <ros/ros.h>
#include <geometry_msgs/Twist.h>

using namespace ros;

int main(int argc, char **argv) {
    //ROS节点初始化
    init(argc, argv, "velocity_publisher");
    //创建节点句柄
    NodeHandle n;
    //创建一个Publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10
    Publisher turtle_vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);
    //设置循环的频率
    Rate loop_rate(10);

    int count = 0;

    while(ok()) {
        //初始化geometry_msgs::Twist类型的消息
	geometry_msgs::Twist vel_msg;
        //设置小海龟的线动量
	vel_msg.linear.x = 0.5;
        //设置小海龟的角动量
	vel_msg.angular.z = 0.2;
        //发布消息
	turtle_vel_pub.publish(vel_msg);
	ROS_INFO("Publish turtle velocity command[%0.2f m/s, %0.2f rad/s]", vel_msg.linear.x, vel_msg.angular.z);
        //按照循环频率延时
	loop_rate.sleep();
    }

    return 0;
}

关于队列Publisher turtle_vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);以上图为例,如果Publisher发布的非常快,比如10000次/秒,底层的以太网可能发不出去,这里会有一个队列,先将10000次消息存到队列中,再根据实际发送的能力从队列(缓存)中去发送数据。这里有一个问题就是队列满了怎么办,我们这里设置的是10,如果底层能力太弱,ROS会默认把时间戳最老的数据给抛弃,先入队的数据会抛出,它会永远保存10个数据是最新的数据。此时会发生一些掉数据掉帧的情况。

回到learning_topic文件夹,修改CMakelists.txt。

代码语言:javascript复制
cd ..
vim CMakelists.txt

添加内容如下

代码语言:javascript复制
add_executable(velocity_publisher src/velocity_publisher.cpp)
target_link_libraries(velocity_publisher ${catkin_LIBRARIES})

回到catkin_ws文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

编译完成后设置环境变量

代码语言:javascript复制
source devel/setup.bash

如果我们不想每次编译后都去设置一次环境变量的话,可以直接修改/etc/profile

代码语言:javascript复制
sudo vim /etc/profile

添加内容

代码语言:javascript复制
source /home/user/Documents/catkin_ws/devel/setup.bash

其中user改成你自己的用户名

代码语言:javascript复制
source /etc/profile

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_topic velocity_publisher

这样我们就可以看到小海龟在绕着一个圈运动。

Python实现

进入包文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_topic

新建脚本文件夹

代码语言:javascript复制
mkdir scripts
cd scripts

创建我们需要的 Python文件

代码语言:javascript复制
vim velocity_publisher.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该程序将发布turtle1/cmd_vel话题,消息类型geometry_msgs::Twist

import rospy
from geometry_msgs.msg import Twist

def velocity_publisher():
    # ROS节点初始化
    rospy.init_node('velocity_publisher', anonymous=True)
    # 创建一个Publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10
    turtle_vel_pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=10)
    # 设置循环的频率
    rate = rospy.Rate(10)

    while not rospy.is_shutdown():
        # 初始化geometry_msgs::Twist类型的消息
        vel_msg = Twist()
        # 设置小海龟的线动量
        vel_msg.linear.x = 0.5
        # 设置小海龟的角动量
        vel_msg.angular.z = 0.2
        # 发布消息
        turtle_vel_pub.publish(vel_msg)
        rospy.loginfo("Publish turtle velocity command[%0.2f m/s, %0.2f rad/s]", vel_msg.linear.x, vel_msg.angular.z)
        # 按照循环频率延时
        rate.sleep()

if __name__ == '__main__':

    try:
        velocity_publisher()
    except rospy.ROSInterruptException:
        pass

设置可执行权限

代码语言:javascript复制
chmod 777 velocity_publisher.py

这里我们需要Python的运行环境,具体可以参考乌班图安装Pytorch、Tensorflow Cuda环境 中的安装 Anaconda。

安装两个需要的包

代码语言:javascript复制
conda activate py39
pip install pyyaml
pip install rospkg

执行命令(不同终端窗口)

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_topic velocity_publisher.py

这样就跟之前使用C 的效果是一样的了。

订阅者Subscriber的编程实现

C 实现

进入learning_topic功能包中

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_topic/src

创建我们需要的 C 文件

代码语言:javascript复制
vim pose_subscriber.cpp

内容如下

代码语言:javascript复制
/*
 *  该例程将订阅/turtle1/pose话题,消息类型turtlesim::Pose
*/

#include <ros/ros.h>
#include "turtlesim/Pose.h"

using namespace ros;

//接受到订阅的消息后,会进入消息回调函数
void poseCallback(const turtlesim::Pose::ConstPtr& msg) {
    //将接收的消息打印出来
    ROS_INFO("Turtle pose: x:%0.6f, y:%0.6f", msg->x, msg->y);
}

int main(int argc, char **argv) {
    //初始化ROS节点
    init(argc, argv, "pose_subscriber");
    //创建节点句柄
    NodeHandle n;
    //创建一个Subscriber,订阅名为/turtle1/pose的topic,注册回调函数poseCallback
    Subscriber pose_sub = n.subscribe("/turtle1/pose", 10, poseCallback);
    //循环等待回调函数
    spin();

    return 0;
}

回到 learning_topic 文件夹,修改 CMakelists.txt。

代码语言:javascript复制
cd ..
vim CMakelists.txt

添加内容如下

代码语言:javascript复制
add_executable(pose_subscriber src/pose_subscriber.cpp)
target_link_libraries(pose_subscriber ${catkin_LIBRARIES})

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun turtlesim turtle_teleop_key
rosrun learning_topic pose_subscriber

此时我们会看见,我们使用键盘来移动小海龟,pose_subscriber中会实时返回小海龟的坐标

代码语言:javascript复制
[ INFO] [1691889920.721039829]: Turtle pose: x:7.258318, y:6.718236
[ INFO] [1691889920.736960560]: Turtle pose: x:7.226329, y:6.717391
[ INFO] [1691889920.752859056]: Turtle pose: x:7.194340, y:6.716546
[ INFO] [1691889920.768800015]: Turtle pose: x:7.162352, y:6.715701
[ INFO] [1691889920.784756857]: Turtle pose: x:7.130363, y:6.714856
[ INFO] [1691889920.800640019]: Turtle pose: x:7.098374, y:6.714011
[ INFO] [1691889920.816949030]: Turtle pose: x:7.066385, y:6.713166
[ INFO] [1691889920.832867584]: Turtle pose: x:7.034396, y:6.712321
[ INFO] [1691889920.848822764]: Turtle pose: x:7.002407, y:6.711476
[ INFO] [1691889920.864732401]: Turtle pose: x:6.970418, y:6.710631
[ INFO] [1691889920.880662398]: Turtle pose: x:6.938430, y:6.709786
[ INFO] [1691889920.896609434]: Turtle pose: x:6.906441, y:6.708941
[ INFO] [1691889920.912486155]: Turtle pose: x:6.874452, y:6.708097
[ INFO] [1691889920.928392700]: Turtle pose: x:6.842463, y:6.707252
[ INFO] [1691889920.944258964]: Turtle pose: x:6.810474, y:6.706407
[ INFO] [1691889920.961082117]: Turtle pose: x:6.778485, y:6.705562
[ INFO] [1691889920.976929758]: Turtle pose: x:6.746497, y:6.704717
[ INFO] [1691889920.992809891]: Turtle pose: x:6.714508, y:6.703872
[ INFO] [1691889921.008716570]: Turtle pose: x:6.682519, y:6.703027
[ INFO] [1691889921.024568443]: Turtle pose: x:6.650530, y:6.702182
[ INFO] [1691889921.040418382]: Turtle pose: x:6.618541, y:6.701337
[ INFO] [1691889921.056245800]: Turtle pose: x:6.586552, y:6.700492
[ INFO] [1691889921.072179865]: Turtle pose: x:6.554564, y:6.699647
[ INFO] [1691889921.088081910]: Turtle pose: x:6.522575, y:6.698802
[ INFO] [1691889921.104950901]: Turtle pose: x:6.490586, y:6.697958
[ INFO] [1691889921.120758048]: Turtle pose: x:6.458597, y:6.697113
[ INFO] [1691889921.136643736]: Turtle pose: x:6.426608, y:6.696268
[ INFO] [1691889921.152551417]: Turtle pose: x:6.394619, y:6.695423
[ INFO] [1691889921.168359967]: Turtle pose: x:6.362630, y:6.694578
[ INFO] [1691889921.184151132]: Turtle pose: x:6.330642, y:6.693733
[ INFO] [1691889921.200989473]: Turtle pose: x:6.298653, y:6.692888
[ INFO] [1691889921.216881443]: Turtle pose: x:6.266664, y:6.692043
[ INFO] [1691889921.232730165]: Turtle pose: x:6.234675, y:6.691198
[ INFO] [1691889921.248579035]: Turtle pose: x:6.202686, y:6.690353
[ INFO] [1691889921.264415767]: Turtle pose: x:6.170697, y:6.689508
[ INFO] [1691889921.280488459]: Turtle pose: x:6.138709, y:6.688663
[ INFO] [1691889921.296428648]: Turtle pose: x:6.106719, y:6.687818
[ INFO] [1691889921.312561137]: Turtle pose: x:6.074731, y:6.686973
[ INFO] [1691889921.328623851]: Turtle pose: x:6.042742, y:6.686129
[ INFO] [1691889921.344572117]: Turtle pose: x:6.010753, y:6.685284
[ INFO] [1691889921.360503389]: Turtle pose: x:5.978765, y:6.684439
[ INFO] [1691889921.376510552]: Turtle pose: x:5.946775, y:6.683594
[ INFO] [1691889921.392376821]: Turtle pose: x:5.914787, y:6.682749
[ INFO] [1691889921.408252255]: Turtle pose: x:5.882798, y:6.681904
[ INFO] [1691889921.424182561]: Turtle pose: x:5.850809, y:6.681059
[ INFO] [1691889921.440997168]: Turtle pose: x:5.818820, y:6.680214
[ INFO] [1691889921.456974244]: Turtle pose: x:5.786831, y:6.679369
[ INFO] [1691889921.472901624]: Turtle pose: x:5.754842, y:6.678524
[ INFO] [1691889921.488752417]: Turtle pose: x:5.722854, y:6.677679
[ INFO] [1691889921.504541853]: Turtle pose: x:5.690865, y:6.676834
[ INFO] [1691889921.520481420]: Turtle pose: x:5.658876, y:6.675989
[ INFO] [1691889921.536294508]: Turtle pose: x:5.626887, y:6.675144
[ INFO] [1691889921.552146408]: Turtle pose: x:5.594898, y:6.674299
[ INFO] [1691889921.569002808]: Turtle pose: x:5.562910, y:6.673454
[ INFO] [1691889921.584837421]: Turtle pose: x:5.530921, y:6.672609
[ INFO] [1691889921.600731891]: Turtle pose: x:5.498932, y:6.671764
[ INFO] [1691889921.616585402]: Turtle pose: x:5.466943, y:6.670919
[ INFO] [1691889921.632283348]: Turtle pose: x:5.434954, y:6.670074
[ INFO] [1691889921.648109869]: Turtle pose: x:5.402965, y:6.669230
[ INFO] [1691889921.664930356]: Turtle pose: x:5.370976, y:6.668385
[ INFO] [1691889921.680713380]: Turtle pose: x:5.338987, y:6.667540
[ INFO] [1691889921.696535478]: Turtle pose: x:5.306999, y:6.666695
[ INFO] [1691889921.712416030]: Turtle pose: x:5.275010, y:6.665850
[ INFO] [1691889921.728232197]: Turtle pose: x:5.243021, y:6.665005
[ INFO] [1691889921.745038074]: Turtle pose: x:5.211032, y:6.664160
[ INFO] [1691889921.760931901]: Turtle pose: x:5.179043, y:6.663315
[ INFO] [1691889921.776834055]: Turtle pose: x:5.147054, y:6.662470
[ INFO] [1691889921.792767539]: Turtle pose: x:5.115066, y:6.661625
[ INFO] [1691889921.808459123]: Turtle pose: x:5.083077, y:6.660780
[ INFO] [1691889921.824286895]: Turtle pose: x:5.051088, y:6.659935
[ INFO] [1691889921.840047529]: Turtle pose: x:5.019099, y:6.659091
[ INFO] [1691889921.856934123]: Turtle pose: x:4.987110, y:6.658246
[ INFO] [1691889921.872885179]: Turtle pose: x:4.955122, y:6.657401
[ INFO] [1691889921.888850674]: Turtle pose: x:4.923132, y:6.656556
[ INFO] [1691889921.904637071]: Turtle pose: x:4.891144, y:6.655711
[ INFO] [1691889921.920525400]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889921.936351946]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889921.952293251]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889921.968227998]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889921.984134339]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.000248528]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.016049234]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.032805745]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.048755014]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.064654370]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.080874716]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.096607718]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.112706638]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.128915941]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.144914936]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.160984702]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.177018643]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.193078747]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.209113678]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.224127436]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.240169768]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.256324080]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.272499755]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.288732790]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.304789579]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.320181587]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.336206526]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.352138685]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.368140737]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.384148108]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.400700501]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.416652277]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.432593842]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.448592295]: Turtle pose: x:4.859155, y:6.654866
[ INFO] [1691889922.464510380]: Turtle pose: x:4.844141, y:6.626607
[ INFO] [1691889922.480414251]: Turtle pose: x:4.829126, y:6.598348
[ INFO] [1691889922.496464895]: Turtle pose: x:4.814111, y:6.570089
[ INFO] [1691889922.512310489]: Turtle pose: x:4.799097, y:6.541830
[ INFO] [1691889922.528349207]: Turtle pose: x:4.784082, y:6.513571
[ INFO] [1691889922.544370980]: Turtle pose: x:4.769068, y:6.485312
[ INFO] [1691889922.560248083]: Turtle pose: x:4.754053, y:6.457054
[ INFO] [1691889922.577109783]: Turtle pose: x:4.739038, y:6.428795
[ INFO] [1691889922.593036954]: Turtle pose: x:4.724024, y:6.400536
[ INFO] [1691889922.608748334]: Turtle pose: x:4.709010, y:6.372277
[ INFO] [1691889922.624707025]: Turtle pose: x:4.693995, y:6.344018
[ INFO] [1691889922.640377773]: Turtle pose: x:4.678980, y:6.315759
[ INFO] [1691889922.656179957]: Turtle pose: x:4.663966, y:6.287500
[ INFO] [1691889922.673095800]: Turtle pose: x:4.648952, y:6.259242
[ INFO] [1691889922.689029385]: Turtle pose: x:4.633937, y:6.230983
[ INFO] [1691889922.704937355]: Turtle pose: x:4.618922, y:6.202724
[ INFO] [1691889922.720833524]: Turtle pose: x:4.603908, y:6.174465
[ INFO] [1691889922.736665756]: Turtle pose: x:4.588893, y:6.146206
[ INFO] [1691889922.752540440]: Turtle pose: x:4.573879, y:6.117947
[ INFO] [1691889922.768437486]: Turtle pose: x:4.558865, y:6.089688
[ INFO] [1691889922.784340470]: Turtle pose: x:4.543850, y:6.061430
[ INFO] [1691889922.800856292]: Turtle pose: x:4.528835, y:6.033170
[ INFO] [1691889922.816584659]: Turtle pose: x:4.513821, y:6.004911
[ INFO] [1691889922.832443907]: Turtle pose: x:4.498806, y:5.976653
[ INFO] [1691889922.848296081]: Turtle pose: x:4.483792, y:5.948394
[ INFO] [1691889922.864194256]: Turtle pose: x:4.468777, y:5.920135
[ INFO] [1691889922.880319566]: Turtle pose: x:4.453763, y:5.891876
[ INFO] [1691889922.896377267]: Turtle pose: x:4.438748, y:5.863617
[ INFO] [1691889922.912208261]: Turtle pose: x:4.423734, y:5.835358
[ INFO] [1691889922.928163217]: Turtle pose: x:4.408719, y:5.807099
[ INFO] [1691889922.944079033]: Turtle pose: x:4.393704, y:5.778841
[ INFO] [1691889922.961042491]: Turtle pose: x:4.378690, y:5.750582
[ INFO] [1691889922.976970418]: Turtle pose: x:4.363676, y:5.722323
[ INFO] [1691889922.992911458]: Turtle pose: x:4.348661, y:5.694064
[ INFO] [1691889923.008767385]: Turtle pose: x:4.333647, y:5.665805
[ INFO] [1691889923.024388033]: Turtle pose: x:4.318632, y:5.637546
[ INFO] [1691889923.040400809]: Turtle pose: x:4.303617, y:5.609287
[ INFO] [1691889923.056351176]: Turtle pose: x:4.288603, y:5.581028
[ INFO] [1691889923.072266925]: Turtle pose: x:4.273589, y:5.552770
[ INFO] [1691889923.088966404]: Turtle pose: x:4.258574, y:5.524511
[ INFO] [1691889923.104873895]: Turtle pose: x:4.243559, y:5.496252
[ INFO] [1691889923.120768099]: Turtle pose: x:4.228545, y:5.467993
[ INFO] [1691889923.136684839]: Turtle pose: x:4.213531, y:5.439734
[ INFO] [1691889923.152652861]: Turtle pose: x:4.198516, y:5.411475
[ INFO] [1691889923.168529918]: Turtle pose: x:4.183501, y:5.383216
[ INFO] [1691889923.184425569]: Turtle pose: x:4.168487, y:5.354958
[ INFO] [1691889923.200321049]: Turtle pose: x:4.153472, y:5.326698
[ INFO] [1691889923.216245066]: Turtle pose: x:4.138458, y:5.298440
[ INFO] [1691889923.232117480]: Turtle pose: x:4.123443, y:5.270181
[ INFO] [1691889923.248347382]: Turtle pose: x:4.108429, y:5.241922
[ INFO] [1691889923.264763495]: Turtle pose: x:4.093414, y:5.213663
[ INFO] [1691889923.280874344]: Turtle pose: x:4.078400, y:5.185404
[ INFO] [1691889923.296933154]: Turtle pose: x:4.063385, y:5.157145
[ INFO] [1691889923.312867114]: Turtle pose: x:4.048371, y:5.128886
[ INFO] [1691889923.328798936]: Turtle pose: x:4.033356, y:5.100627
[ INFO] [1691889923.344686159]: Turtle pose: x:4.018342, y:5.072369
[ INFO] [1691889923.360580762]: Turtle pose: x:4.003327, y:5.044110
[ INFO] [1691889923.376477719]: Turtle pose: x:3.988312, y:5.015851
[ INFO] [1691889923.392365338]: Turtle pose: x:3.973298, y:4.987592
[ INFO] [1691889923.408245284]: Turtle pose: x:3.958283, y:4.959333
[ INFO] [1691889923.424220910]: Turtle pose: x:3.943269, y:4.931074
[ INFO] [1691889923.440230704]: Turtle pose: x:3.928254, y:4.902815
[ INFO] [1691889923.456247677]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.472085474]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.488275063]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.504411872]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.520561024]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.536727905]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.552894362]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.569033430]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.584207344]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.600343488]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.616486007]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.632656715]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.648807247]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.664947728]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.680075728]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.696253947]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.712429119]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.728567929]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.744711745]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.760895074]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.777058333]: Turtle pose: x:3.913240, y:4.874557
[ INFO] [1691889923.792188376]: Turtle pose: x:3.913240, y:4.874557

Python实现

进入脚本文件夹创建Python文件

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_topic/scripts
vim pose_subscriber.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程将订阅/turtle1/pose话题,消息类型turtlesim::Pose

import rospy
from turtlesim.msg import Pose
# 接受到订阅的消息后,会进入消息回调函数
def poseCallback(msg):
    # 将接收的消息打印出来
    rospy.loginfo("Turtle pose: x:%0.6f, y:%0.6f", msg.x, msg.y)

def pose_subscriber():
    # 初始化ROS节点
    rospy.init_node('pose_subscriber', anonymous=True)
    # 创建一个Subscriber,订阅名为/turtle1/pose的topic,注册回调函数poseCallback
    rospy.Subscriber("/turtle1/pose", Pose, poseCallback)
    # 循环等待回调函数
    rospy.spin()

if __name__ == '__main__':

    pose_subscriber()

设置可执行权限

代码语言:javascript复制
chmod 777 pose_subscriber.py

执行命令 (不同终端窗口)

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun turtlesim turtle_teleop_key
rosrun learning_topic pose_subscriber.py

图像传输的发布者与订阅者

进入工作空间

代码语言:javascript复制
cd Documents/catkin_ws/src

创建一个新的功能包

代码语言:javascript复制
catkin_create_pkg learning_transfer_img sensor_msgs cv_bridge roscpp std_msgs image_transport

进入到该功能包的 src 目录

代码语言:javascript复制
cd learning_transfer_img/src

创建发布者的 C 文件

代码语言:javascript复制
vim img_publisher.cpp

内容如下

代码语言:javascript复制
#include <ros/ros.h>
#include <image_transport/image_transport.h>
#include <opencv2/highgui/highgui.hpp>
#include <cv_bridge/cv_bridge.h>

using namespace ros;
using namespace cv;
using namespace std;
using namespace sensor_msgs;
     
int main(int argc, char** argv){
    //ROS节点初始化
    init(argc, argv, "image_publisher");
    //创建节点句柄
    NodeHandle n;
    //创建一个图像传输发布者,发布名为/camera/image的topic
    image_transport::ImageTransport it(n);
    image_transport::Publisher pub = it.advertise("/camera/image", 1);
    VideoCapture cap(0);
    if(!cap.isOpened()) return 1;
    Mat frame;
    //初始化一个sensor_msgs::ImagePtr类型的消息
    ImagePtr msg;
    Rate loop_rate(1);
    while (ok()) {
        cap >> frame;
        if(!frame.empty()) {
            //将opencv的Mat转化成sensor_msgs::ImagePtr类型的消息
            msg = cv_bridge::CvImage(std_msgs::Header(), "bgr8", frame).toImageMsg();
            //发布消息
            pub.publish(msg);
        }
        //按照循环频率延时
        loop_rate.sleep();
    }
    return 0;
}

创建订阅者的 C 文件

代码语言:javascript复制
vim img_subscriber.cpp

内容如下

代码语言:javascript复制
#include <ros/ros.h>
#include <image_transport/image_transport.h>
#include <opencv2/highgui/highgui.hpp>
#include <cv_bridge/cv_bridge.h>

using namespace ros;
using namespace cv;
using namespace std;
using namespace sensor_msgs;
 
void imageCallback(const ImageConstPtr& msg) {
    try {
        //将sensor_msgs::ImagePtr类型的消息转化成opencv的Mat并显示出来
        imshow("view", cv_bridge::toCvShare(msg, "bgr8")->image);
        waitKey(1);
    } catch (cv_bridge::Exception& e) {
        ROS_ERROR("Could not convert from '%s' to 'bgr8'.", msg->encoding.c_str());
    }
}
 
int main(int argc, char **argv) {
    //ROS节点初始化
    init(argc, argv, "img_subscriber");
    //创建节点句柄
    NodeHandle n;
    namedWindow("view");
    startWindowThread();
    //创建一个image_transport::Subscriber,订阅名为/camera/image的topic,注册回调函数imageCallback
    image_transport::ImageTransport it(n);
    image_transport::Subscriber sub = it.subscribe("/camera/image", 1, imageCallback);
    //循环等待回调函数
    spin();
    destroyWindow("view");
    return 0;
}

回到 learning_transfer_img 文件夹,修改 CMakelists.txt。

代码语言:javascript复制
cd ..
vim CMakelists.txt

添加内容如下

代码语言:javascript复制
find_package(OpenCV)
include_directories(${OpenCV_INCLUDE_DIRS})

add_executable(img_publisher src/img_publisher.cpp)
target_link_libraries(img_publisher ${catkin_LIBRARIES})
target_link_libraries(img_publisher ${OpenCV_LIBS})

add_executable(img_subscriber src/img_subscriber.cpp)
target_link_libraries(img_subscriber ${catkin_LIBRARIES})
target_link_libraries(img_subscriber ${OpenCV_LIBS})

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun learning_transfer_img img_subscriber
rosrun learning_transfer_img img_publisher

话题消息的定义与使用

C 实现

进入包文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_topic

新建消息文件夹

代码语言:javascript复制
mkdir msg
cd msg

创建我们需要的消息文件

代码语言:javascript复制
vim Person.msg

内容如下

代码语言:javascript复制
string name
uint8 sex
uint8 age

uint8 unknown = 0
uint8 male    = 1
uint8 femal   = 2

这里我们创建的是一个关于个人的信息。它是语言无关的,ROS会根据定义来编译成C 或Python的程序。

设置编译规则

退回learning_topic文件夹,修改package.xml,添加动态生成程序的功能包依赖

代码语言:javascript复制
cd ..
vim package.xml

添加的内容如下

代码语言:javascript复制
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

其中<build_depend>为编译依赖,<exec_depend>为执行依赖。

修改CMakeLists.txt

代码语言:javascript复制
vim CMakeLists.txt

内容如下

代码语言:javascript复制
find_package(catkin REQUIRED COMPONENTS
  geometry_msgs
  roscpp
  rospy
  std_msgs
  turtlesim
  message_generation
)

add_message_files(FILES Person.msg)
generate_messages(DEPENDENCIES std_msgs)

catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES learning_topic
   CATKIN_DEPENDS geometry_msgs roscpp rospy std_msgs turtlesim message_runtime
#  DEPENDS system_lib
)

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

此时我们会在Documents/catkin_ws/devel/include/learning_topic目录下看到多了一个Person.h的文件,它是由ROS自动编译生成的,内容如下

代码语言:javascript复制
// Generated by gencpp from file learning_topic/Person.msg
// DO NOT EDIT!


#ifndef LEARNING_TOPIC_MESSAGE_PERSON_H
#define LEARNING_TOPIC_MESSAGE_PERSON_H


#include <string>
#include <vector>
#include <memory>

#include <ros/types.h>
#include <ros/serialization.h>
#include <ros/builtin_message_traits.h>
#include <ros/message_operations.h>


namespace learning_topic
{
template <class ContainerAllocator>
struct Person_
{
  typedef Person_<ContainerAllocator> Type;

  Person_()
    : name()
    , sex(0)
    , age(0)  {
    }
  Person_(const ContainerAllocator& _alloc)
    : name(_alloc)
    , sex(0)
    , age(0)  {
  (void)_alloc;
    }



   typedef std::basic_string<char, std::char_traits<char>, typename std::allocator_traits<ContainerAllocator>::template rebind_alloc<char>> _name_type;
  _name_type name;

   typedef uint8_t _sex_type;
  _sex_type sex;

   typedef uint8_t _age_type;
  _age_type age;



// reducing the odds to have name collisions with Windows.h 
#if defined(_WIN32) && defined(unknown)
  #undef unknown
#endif
#if defined(_WIN32) && defined(male)
  #undef male
#endif
#if defined(_WIN32) && defined(femal)
  #undef femal
#endif

  enum {
    unknown = 0u,
    male = 1u,
    femal = 2u,
  };


  typedef boost::shared_ptr< ::learning_topic::Person_<ContainerAllocator> > Ptr;
  typedef boost::shared_ptr< ::learning_topic::Person_<ContainerAllocator> const> ConstPtr;

}; // struct Person_

typedef ::learning_topic::Person_<std::allocator<void> > Person;

typedef boost::shared_ptr< ::learning_topic::Person > PersonPtr;
typedef boost::shared_ptr< ::learning_topic::Person const> PersonConstPtr;

// constants requiring out of line definition

   

   

   



template<typename ContainerAllocator>
std::ostream& operator<<(std::ostream& s, const ::learning_topic::Person_<ContainerAllocator> & v)
{
ros::message_operations::Printer< ::learning_topic::Person_<ContainerAllocator> >::stream(s, "", v);
return s;
}


template<typename ContainerAllocator1, typename ContainerAllocator2>
bool operator==(const ::learning_topic::Person_<ContainerAllocator1> & lhs, const ::learning_topic::Person_<ContainerAllocator2> & rhs)
{
  return lhs.name == rhs.name &&
    lhs.sex == rhs.sex &&
    lhs.age == rhs.age;
}

template<typename ContainerAllocator1, typename ContainerAllocator2>
bool operator!=(const ::learning_topic::Person_<ContainerAllocator1> & lhs, const ::learning_topic::Person_<ContainerAllocator2> & rhs)
{
  return !(lhs == rhs);
}


} // namespace learning_topic

namespace ros
{
namespace message_traits
{





template <class ContainerAllocator>
struct IsMessage< ::learning_topic::Person_<ContainerAllocator> >
  : TrueType
  { };

template <class ContainerAllocator>
struct IsMessage< ::learning_topic::Person_<ContainerAllocator> const>
  : TrueType
  { };

template <class ContainerAllocator>
struct IsFixedSize< ::learning_topic::Person_<ContainerAllocator> >
  : FalseType
  { };

template <class ContainerAllocator>
struct IsFixedSize< ::learning_topic::Person_<ContainerAllocator> const>
  : FalseType
  { };

template <class ContainerAllocator>
struct HasHeader< ::learning_topic::Person_<ContainerAllocator> >
  : FalseType
  { };

template <class ContainerAllocator>
struct HasHeader< ::learning_topic::Person_<ContainerAllocator> const>
  : FalseType
  { };


template<class ContainerAllocator>
struct MD5Sum< ::learning_topic::Person_<ContainerAllocator> >
{
  static const char* value()
  {
    return "2df326301c118b26e23f3606868068cc";
  }

  static const char* value(const ::learning_topic::Person_<ContainerAllocator>&) { return value(); }
  static const uint64_t static_value1 = 0x2df326301c118b26ULL;
  static const uint64_t static_value2 = 0xe23f3606868068ccULL;
};

template<class ContainerAllocator>
struct DataType< ::learning_topic::Person_<ContainerAllocator> >
{
  static const char* value()
  {
    return "learning_topic/Person";
  }

  static const char* value(const ::learning_topic::Person_<ContainerAllocator>&) { return value(); }
};

template<class ContainerAllocator>
struct Definition< ::learning_topic::Person_<ContainerAllocator> >
{
  static const char* value()
  {
    return "string namen"
"uint8 sexn"
"uint8 agen"
"n"
"uint8 unknown = 0n"
"uint8 male    = 1n"
"uint8 femal   = 2n"
;
  }

  static const char* value(const ::learning_topic::Person_<ContainerAllocator>&) { return value(); }
};

} // namespace message_traits
} // namespace ros

namespace ros
{
namespace serialization
{

  template<class ContainerAllocator> struct Serializer< ::learning_topic::Person_<ContainerAllocator> >
  {
    template<typename Stream, typename T> inline static void allInOne(Stream& stream, T m)
    {
      stream.next(m.name);
      stream.next(m.sex);
      stream.next(m.age);
    }

    ROS_DECLARE_ALLINONE_SERIALIZER
  }; // struct Person_

} // namespace serialization
} // namespace ros

namespace ros
{
namespace message_operations
{

template<class ContainerAllocator>
struct Printer< ::learning_topic::Person_<ContainerAllocator> >
{
  template<typename Stream> static void stream(Stream& s, const std::string& indent, const ::learning_topic::Person_<ContainerAllocator>& v)
  {
    s << indent << "name: ";
    Printer<std::basic_string<char, std::char_traits<char>, typename std::allocator_traits<ContainerAllocator>::template rebind_alloc<char>>>::stream(s, indent   "  ", v.name);
    s << indent << "sex: ";
    Printer<uint8_t>::stream(s, indent   "  ", v.sex);
    s << indent << "age: ";
    Printer<uint8_t>::stream(s, indent   "  ", v.age);
  }
};

} // namespace message_operations
} // namespace ros

#endif // LEARNING_TOPIC_MESSAGE_PERSON_H

这是一个C 的头文件

创建发布者与订阅者

进入learning_topic的源码文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_topic/src

创建发布者

代码语言:javascript复制
vim person_publisher.cpp

内容如下

代码语言:javascript复制
/*
 * 该例程将发布/person_info话题,自定义消息类型learning_topic::Person
 */

#include <ros/ros.h>
#include "learning_topic/Person.h"

using namespace ros;

int main(int argc, char **argv) {
    //ROS节点初始化
    init(argc, argv, "person_publisher");
    //创建节点句柄
    NodeHandle n;
    //创建一个Publisher,发布名为/person_info的topic,消息类型为learning_topic::Person,队列长度为10
    Publisher person_info_pub = n.advertise<learning_topic::Person>("/person_info", 10);
    //设置循环的频率
    Rate loop_rate(1);

    int count = 0;
    while(ok()) {
        //初始化learning_topic::Person类型的消息
	learning_topic::Person person_msg;
	person_msg.name = "Tom";
	person_msg.age = 18;
	person_msg.sex = learning_topic::Person::male;
        //发布消息
	person_info_pub.publish(person_msg);

	ROS_INFO("Publish Person Info: name:%s age:%d sex:%d", person_msg.name.c_str(), person_msg.age, person_msg.sex);
        //按照循环频率延时
	loop_rate.sleep();
    }

    return 0;
}

创建订阅者

代码语言:javascript复制
vim person_subscriber.cpp

内容如下

代码语言:javascript复制
/*
 * 该例程将订阅/person_info话题,自定义消息类型learning_topic::Person
 */

#include <ros/ros.h>
#include "learning_topic/Person.h"

using namespace ros;

//接收到订阅消息后,会进入消息回调函数
void personInfoCallback(const learning_topic::Person::ConstPtr& msg) {
    //将接收到的消息打印出来
    ROS_INFO("Subcribe Person Info: name:%s age:%d sex:%d", msg->name.c_str(), msg->age, msg->sex);
}

int main(int argc, char **argv) {
    //初始化ROS节点
    init(argc, argv, "person_subscriber");
    //创建节点句柄
    NodeHandle n;
    //创建一个Subscriber,订阅名为/person_info的topic,注册回调函数personInfoCallback
    Subscriber person_info_sub = n.subscribe("/person_info", 10, personInfoCallback);
    //循环等待回调函数
    spin();

    return 0;
}

回到 learning_topic 文件夹,修改 CMakelists.txt

代码语言:javascript复制
cd ..
vim CMakelists.txt

添加内容如下

代码语言:javascript复制
add_executable(person_publisher src/person_publisher.cpp)
target_link_libraries(person_publisher ${catkin_LIBRARIES})
add_dependencies(person_publisher ${PROJECT_NAME}_generate_messages_cpp)

add_executable(person_subscriber src/person_subscriber.cpp)
target_link_libraries(person_subscriber ${catkin_LIBRARIES})
add_dependencies(person_subscriber ${PROJECT_NAME}_generate_messages_cpp)

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun learning_topic person_subscriber
rosrun learning_topic person_publisher

这样在发布者这边就会一直发布人员的消息

代码语言:javascript复制
[ INFO] [1692058319.598902884]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058320.599045308]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058321.599024124]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058322.599034397]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058323.599010303]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058324.599021551]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058325.599007207]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058326.598998586]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058327.598991095]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058328.599036958]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058329.599018178]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058330.599023816]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058331.599021355]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058332.599022894]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058333.599017425]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1692058334.599010853]: Publish Person Info: name:Tom age:18 sex:1

而在订阅者这边就会开始接收人员的消息

代码语言:javascript复制
[ INFO] [1692058320.599351163]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058321.599261038]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058322.599291722]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058323.599267429]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058324.599267875]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058325.599275965]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058326.599178858]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058327.599225550]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058328.599308836]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058329.599291376]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058330.599301795]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058331.599281126]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058332.599277659]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058333.599294180]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1692058334.599276274]: Subcribe Person Info: name:Tom age:18 sex:1

此时如果我们关闭ROS Master,即roscore,这两个发布者和订阅者节点的通讯并不会收到影响。只不过我们没办法再去扩展新节点与这两个节点进行通信。

Python实现

进入包脚本文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_topic/scripts

创建发布者

代码语言:javascript复制
vim person_publisher.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程将发布/person_info话题,自定义消息类型learning_topic::Person

import rospy
from learning_topic.msg import Person

def velocity_publisher():
    # ROS节点初始化
    rospy.init_node('person_publisher', anonymous=True)
    # 创建一个Publisher,发布名为/person_info的topic,消息类型为learning_topic::Person,队列长度为10
    person_info_pub = rospy.Publisher('/person_info', Person, queue_size=10)
    # 设置循环的频率
    rate = rospy.Rate(10)

    while not rospy.is_shutdown():
        # 初始化learning_topic::Person类型的消息
        person_msg = Person()
        person_msg.name = "Tom"
        person_msg.age = 18
        person_msg.sex = Person.male
        # 发布消息
        person_info_pub.publish(person_msg)
        rospy.loginfo("Publish person message[%s, %d, %d]", person_msg.name, person_msg.age, person_msg.sex)
        # 按照循环频率延时
        rate.sleep()

if __name__ == '__main__':

    try:
        velocity_publisher()
    except rospy.ROSInterruptException:
        pass

创建订阅者

代码语言:javascript复制
vim person_subscriber.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程将订阅/person_info话题,自定义消息类型learning_topic::Person

import rospy
from learning_topic.msg import Person

# 接收到订阅消息后,会进入消息回调函数
def personInfoCallback(msg):
    rospy.loginfo("Subcribe Person info: name:%s age:%d sex:%d", msg.name, msg.age, msg.sex)
    
def person_subscriber():
    # 初始化ROS节点
    rospy.init_node('person_subscriber', anonymous=True)
    # 创建一个Subscriber,订阅名为/person_info的topic,注册回调函数personInfoCallback
    rospy.Subscriber('/person_info', Person, personInfoCallback)
    # 循环等待回调函数
    rospy.spin()
    
if __name__ == '__main__':
    
    person_subscriber()

赋予它们可执行权限

代码语言:javascript复制
chmod 777 person_publisher.py
chmod 777 person_subscriber.py

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun learning_topic person_subscriber.py
rosrun learning_topic person_publisher.py

服务(Service)的客户端实现

C 实现

进入工作空间

代码语言:javascript复制
cd Documents/catkin_ws/src

创建一个新的服务功能包

代码语言:javascript复制
catkin_create_pkg learning_service roscpp rospy std_msgs geometry_msgs turtlesim

进入到该功能包的 src 目录

代码语言:javascript复制
cd learning_service/src

创建我们需要的 C 文件

代码语言:javascript复制
vim turtle_spawn.cpp

内容如下

代码语言:javascript复制
/*
 * 该例程将请求/spawn服务,服务数据类型turtlesim::Spawn
 */

#include <ros/ros.h>
#include <turtlesim/Spawn.h>

using namespace ros;

int main(int argc, char** argv) {
    //初始化ROS节点
    init(argc, argv, "turtle_spawn");
    //创建节点句柄
    NodeHandle node;
    //发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
    service::waitForService("/spawn");
    ServiceClient add_turtle = node.serviceClient<turtlesim::Spawn>("/spawn");
    //初始化turtlesim::Spawn的请求数据
    turtlesim::Spawn srv;
    srv.request.x = 2.0;
    srv.request.y = 2.0;
    srv.request.name = "turtle2";
    //请求服务调用
    ROS_INFO("Call service to spwan turtle[x:%0.6f, y:%0.6f, name:%s]", srv.request.x, srv.request.y, srv.request.name.c_str());

    add_turtle.call(srv);
    //显示服务调用结果
    ROS_INFO("Spwan turtle successfully [name:%s]", srv.response.name.c_str());

    return 0;
}

该功能主要是在小海龟仿真器中产生第二只海龟

回到 learning_service 文件夹,修改 CMakelists.txt。

代码语言:javascript复制
cd ..
vim CMakelists.txt

添加内容如下

代码语言:javascript复制
add_executable(turtle_spawn src/turtle_spawn.cpp)
target_link_libraries(turtle_spawn ${catkin_LIBRARIES})

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_service turtle_spawn

此时我们已经可以看到在海龟仿真器窗口中产生了第二只海龟。并打印日志

代码语言:javascript复制
[ INFO] [1692399646.686858318]: Call service to spwan turtle[x:2.000000, y:2.000000, name:turtle2]
[ INFO] [1692399646.697959476]: Spwan turtle successfully [name:turtle2]

日志中的x,y是产生的第二只海龟的坐标,名字为turtle2。而第二行的name:turtle2则为服务端的应答结果。

在turtlesim_node节点中也相应的有打印日志

代码语言:javascript复制
[ INFO] [1692399646.697808644]: Spawning turtle [turtle2] at x=[2.000000], y=[2.000000], theta=[0.000000]

Python实现

进入包文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_service

新建脚本文件夹

代码语言:javascript复制
mkdir scripts
cd scripts

创建我们需要的 Python 文件

代码语言:javascript复制
vim turtle_spawn.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程将请求/spawn服务,服务数据类型turtlesim::Spawn

import sys
import rospy
from turtlesim.srv import Spawn

def turtle_spawn():
    # 初始化ROS节点
    rospy.init_node('turtle_spawn')
    # 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
    rospy.wait_for_service('/spawn')
    try:
        add_turtle = rospy.ServiceProxy('/spawn', Spawn)
        # 请求服务调用
        response = add_turtle(2.0, 2.0, 0.0, "turtle2")
        return response.name
    except rospy.ServiceException as e:
        print("Service call failed: %s"%e)

if __name__ == "__main__":
    # 显示服务调用结果
    print("Spwan turtle successfully [name:%s]" %(turtle_spawn()))

赋予它可执行权限

代码语言:javascript复制
chmod 777 turtle_spawn.py

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_service turtle_spawn.py

服务(Service)的服务端实现

C 实现

进入learning_service的功能包中

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_service/src

创建我们需要的 C 文件

代码语言:javascript复制
vim turtle_command_server.cpp

内容如下

代码语言:javascript复制
/*
 * 该例程将执行/turtle_command服务,服务数据类型std_srvs/Trigger
 */

#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
#include <std_srvs/Trigger.h>

using namespace ros;

Publisher turtle_vel_pub;
bool pubCommand = false;
//service回调函数,输入参数req,输出参数res
bool commandCallback(std_srvs::Trigger::Request &req, std_srvs::Trigger:: Response &res) {
    pubCommand = !pubCommand;
    //显示请求数据
    ROS_INFO("Publish turtle velocity command [%s]", pubCommand==true?"Yes":"No");
    //设置反馈数据
    res.success = true;
    res.message = "Change turtle command state!";

    return true;
}

int main(int argc, char **argv) {
    //ROS节点初始化
    init(argc, argv, "turtle_command_server");
    //创建节点句柄
    NodeHandle n;
    //创建一个名为/turtle_command的server,注册回调函数commandCallback
    ServiceServer command_service = n.advertiseService("/turtle_command", commandCallback);
    //创建一个Publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10
    turtle_vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);
    //循环等待回调函数
    ROS_INFO("Ready to receive turtle command.");
    //设置循环的频率
    Rate loop_rate(10);

    while(ok()) {
        //查看一次回调函数队列
	spinOnce();
        //如果标志为true,则发布速度指令
	if (pubCommand) {
	    geometry_msgs::Twist vel_msg;
	    vel_msg.linear.x = 0.5;
	    vel_msg.angular.z = 0.2;
	    turtle_vel_pub.publish(vel_msg);
	}
        //按照循环频率延时
	loop_rate.sleep();
    }

    return 0;
}

这是一个接收请求,让小海龟做绕圈运动的服务。

回到 learning_service 文件夹,修改 CMakelists.txt。

代码语言:javascript复制
cd ..
vim CMakelists.txt

添加内容如下

代码语言:javascript复制
add_executable(turtle_command_server src/turtle_command_server.cpp)
target_link_libraries(turtle_command_server ${catkin_LIBRARIES})

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_service turtle_command_server
rosservice call /turtle_command "{}"

执行完成后,我们看到小海龟在不断的做绕圈运动。

当我们再次发送请求

代码语言:javascript复制
rosservice call /turtle_command "{}"

小海龟就会停止运动。

turtle_command_server服务端会打印这样两条日志

代码语言:javascript复制
[ INFO] [1692495991.674337329]: Publish turtle velocity command [Yes]
[ INFO] [1692496732.674292008]: Publish turtle velocity command [No]

这里Yes表示小海龟开始运动,No表示小海龟停止运动。

Python实现

进入包脚本文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_service/scripts

创建我们需要的 Python 文件

代码语言:javascript复制
vim turtle_command_server.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程将执行/turtle_command服务,服务数据类型std_srvs/Trigger

import rospy
import _thread,time
from geometry_msgs.msg import Twist
from std_srvs.srv import Trigger,TriggerResponse

pubCommand = False
# 创建一个Publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10
turtle_vel_pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=10)

def command_thread():
    while True:
        if pubCommand:
            vel_msg = Twist()
            vel_msg.linear.x = 0.5
            vel_msg.angular.z = 0.2
            turtle_vel_pub.publish(vel_msg)

        time.sleep(0.1)

# service回调函数,输入参数req,输出参数res
def commandCallback(req):
    global pubCommand
    pubCommand = bool(1 - pubCommand)
    # 显示请求数据
    rospy.loginfo("Publish trutle velocity command![%d]", pubCommand)
    # 设置反馈数据
    return TriggerResponse(1, "Change turtle command state!")


def turtle_command_server():
    # ROS节点初始化
    rospy.init_node('turtle_command_server')
    # 创建一个名为/turtle_command的server,注册回调函数commandCallback
    s = rospy.Service('/turtle_command', Trigger, commandCallback)
    # 循环等待回调函数
    print("Ready to receive turtle command.")

    _thread.start_new_thread(command_thread, ())
    rospy.spin()

if __name__ == "__main__":

    turtle_command_server()

赋予可执行权限

代码语言:javascript复制
chmod 777 turtle_command_server.py

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_service turtle_command_server.py
rosservice call /turtle_command "{}"

再次执行

代码语言:javascript复制
rosservice call /turtle_command "{}"

此时服务端总共会打印

代码语言:javascript复制
[INFO] [1692498798.819324]: Publish trutle velocity command![1]
[INFO] [1692498830.254050]: Publish trutle velocity command![0]

服务数据的定义与使用

C 实现

进入包文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_service

新建数据文件夹

代码语言:javascript复制
mkdir srv
cd srv

创建我们需要的数据文件

代码语言:javascript复制
vim Person.srv

内容如下

代码语言:javascript复制
string name
uint8 age
uint8 sex

uint8 unknown = 0
uint8 male    = 1
uint8 female  = 2
---
string result

这里跟话题消息的不同在于该数据文件包含了请求和响应的部分,中间以---分割。

设置编译规则

退回 learning_service 文件夹,修改 package.xml,添加动态生成程序的功能包依赖

代码语言:javascript复制
cd ..
vim package.xml

添加的内容如下

代码语言:javascript复制
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

其中 <build_depend> 为编译依赖,<exec_depend > 为执行依赖。

修改 CMakeLists.txt

代码语言:javascript复制
vim CMakeLists.txt

内容如下

代码语言:javascript复制
find_package(catkin REQUIRED COMPONENTS
  geometry_msgs
  roscpp
  rospy
  std_msgs
  turtlesim
  message_generation
)

add_service_files(FILES Person.srv)
generate_messages(DEPENDENCIES std_msgs)

catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES learning_service
   CATKIN_DEPENDS geometry_msgs roscpp rospy std_msge turtlesim message_runtime
#  DEPENDS system_lib
)

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

此时我们会在Documents/catkin_ws/devel/include/learning_service目录下看到多了三个文件 Person.h 、PersonRequest.h、PersonResponse.h,它们是由 ROS 自动编译生成的,我们在使用的时候只需要使用Person.h头文件即可。Person.h内容如下

代码语言:javascript复制
// Generated by gencpp from file learning_service/Person.msg
// DO NOT EDIT!


#ifndef LEARNING_SERVICE_MESSAGE_PERSON_H
#define LEARNING_SERVICE_MESSAGE_PERSON_H

#include <ros/service_traits.h>


#include <learning_service/PersonRequest.h>
#include <learning_service/PersonResponse.h>


namespace learning_service
{

struct Person
{

typedef PersonRequest Request;
typedef PersonResponse Response;
Request request;
Response response;

typedef Request RequestType;
typedef Response ResponseType;

}; // struct Person
} // namespace learning_service


namespace ros
{
namespace service_traits
{


template<>
struct MD5Sum< ::learning_service::Person > {
  static const char* value()
  {
    return "c198113e7dd9cc5c9fd5f271c8479b39";
  }

  static const char* value(const ::learning_service::Person&) { return value(); }
};

template<>
struct DataType< ::learning_service::Person > {
  static const char* value()
  {
    return "learning_service/Person";
  }

  static const char* value(const ::learning_service::Person&) { return value(); }
};


// service_traits::MD5Sum< ::learning_service::PersonRequest> should match
// service_traits::MD5Sum< ::learning_service::Person >
template<>
struct MD5Sum< ::learning_service::PersonRequest>
{
  static const char* value()
  {
    return MD5Sum< ::learning_service::Person >::value();
  }
  static const char* value(const ::learning_service::PersonRequest&)
  {
    return value();
  }
};

// service_traits::DataType< ::learning_service::PersonRequest> should match
// service_traits::DataType< ::learning_service::Person >
template<>
struct DataType< ::learning_service::PersonRequest>
{
  static const char* value()
  {
    return DataType< ::learning_service::Person >::value();
  }
  static const char* value(const ::learning_service::PersonRequest&)
  {
    return value();
  }
};

// service_traits::MD5Sum< ::learning_service::PersonResponse> should match
// service_traits::MD5Sum< ::learning_service::Person >
template<>
struct MD5Sum< ::learning_service::PersonResponse>
{
  static const char* value()
  {
    return MD5Sum< ::learning_service::Person >::value();
  }
  static const char* value(const ::learning_service::PersonResponse&)
  {
    return value();
  }
};

// service_traits::DataType< ::learning_service::PersonResponse> should match
// service_traits::DataType< ::learning_service::Person >
template<>
struct DataType< ::learning_service::PersonResponse>
{
  static const char* value()
  {
    return DataType< ::learning_service::Person >::value();
  }
  static const char* value(const ::learning_service::PersonResponse&)
  {
    return value();
  }
};

} // namespace service_traits
} // namespace ros

#endif // LEARNING_SERVICE_MESSAGE_PERSON_H

PersonRequest.h内容如下

代码语言:javascript复制
// Generated by gencpp from file learning_service/PersonRequest.msg
// DO NOT EDIT!


#ifndef LEARNING_SERVICE_MESSAGE_PERSONREQUEST_H
#define LEARNING_SERVICE_MESSAGE_PERSONREQUEST_H


#include <string>
#include <vector>
#include <memory>

#include <ros/types.h>
#include <ros/serialization.h>
#include <ros/builtin_message_traits.h>
#include <ros/message_operations.h>


namespace learning_service
{
template <class ContainerAllocator>
struct PersonRequest_
{
  typedef PersonRequest_<ContainerAllocator> Type;

  PersonRequest_()
    : name()
    , age(0)
    , sex(0)  {
    }
  PersonRequest_(const ContainerAllocator& _alloc)
    : name(_alloc)
    , age(0)
    , sex(0)  {
  (void)_alloc;
    }



   typedef std::basic_string<char, std::char_traits<char>, typename std::allocator_traits<ContainerAllocator>::template rebind_alloc<char>> _name_type;
  _name_type name;

   typedef uint8_t _age_type;
  _age_type age;

   typedef uint8_t _sex_type;
  _sex_type sex;



// reducing the odds to have name collisions with Windows.h 
#if defined(_WIN32) && defined(unknown)
  #undef unknown
#endif
#if defined(_WIN32) && defined(male)
  #undef male
#endif
#if defined(_WIN32) && defined(female)
  #undef female
#endif

  enum {
    unknown = 0u,
    male = 1u,
    female = 2u,
  };


  typedef boost::shared_ptr< ::learning_service::PersonRequest_<ContainerAllocator> > Ptr;
  typedef boost::shared_ptr< ::learning_service::PersonRequest_<ContainerAllocator> const> ConstPtr;

}; // struct PersonRequest_

typedef ::learning_service::PersonRequest_<std::allocator<void> > PersonRequest;

typedef boost::shared_ptr< ::learning_service::PersonRequest > PersonRequestPtr;
typedef boost::shared_ptr< ::learning_service::PersonRequest const> PersonRequestConstPtr;

// constants requiring out of line definition

   

   

   



template<typename ContainerAllocator>
std::ostream& operator<<(std::ostream& s, const ::learning_service::PersonRequest_<ContainerAllocator> & v)
{
ros::message_operations::Printer< ::learning_service::PersonRequest_<ContainerAllocator> >::stream(s, "", v);
return s;
}


template<typename ContainerAllocator1, typename ContainerAllocator2>
bool operator==(const ::learning_service::PersonRequest_<ContainerAllocator1> & lhs, const ::learning_service::PersonRequest_<ContainerAllocator2> & rhs)
{
  return lhs.name == rhs.name &&
    lhs.age == rhs.age &&
    lhs.sex == rhs.sex;
}

template<typename ContainerAllocator1, typename ContainerAllocator2>
bool operator!=(const ::learning_service::PersonRequest_<ContainerAllocator1> & lhs, const ::learning_service::PersonRequest_<ContainerAllocator2> & rhs)
{
  return !(lhs == rhs);
}


} // namespace learning_service

namespace ros
{
namespace message_traits
{





template <class ContainerAllocator>
struct IsMessage< ::learning_service::PersonRequest_<ContainerAllocator> >
  : TrueType
  { };

template <class ContainerAllocator>
struct IsMessage< ::learning_service::PersonRequest_<ContainerAllocator> const>
  : TrueType
  { };

template <class ContainerAllocator>
struct IsFixedSize< ::learning_service::PersonRequest_<ContainerAllocator> >
  : FalseType
  { };

template <class ContainerAllocator>
struct IsFixedSize< ::learning_service::PersonRequest_<ContainerAllocator> const>
  : FalseType
  { };

template <class ContainerAllocator>
struct HasHeader< ::learning_service::PersonRequest_<ContainerAllocator> >
  : FalseType
  { };

template <class ContainerAllocator>
struct HasHeader< ::learning_service::PersonRequest_<ContainerAllocator> const>
  : FalseType
  { };


template<class ContainerAllocator>
struct MD5Sum< ::learning_service::PersonRequest_<ContainerAllocator> >
{
  static const char* value()
  {
    return "b3f7ec37d11629ec3010e27635cf22a9";
  }

  static const char* value(const ::learning_service::PersonRequest_<ContainerAllocator>&) { return value(); }
  static const uint64_t static_value1 = 0xb3f7ec37d11629ecULL;
  static const uint64_t static_value2 = 0x3010e27635cf22a9ULL;
};

template<class ContainerAllocator>
struct DataType< ::learning_service::PersonRequest_<ContainerAllocator> >
{
  static const char* value()
  {
    return "learning_service/PersonRequest";
  }

  static const char* value(const ::learning_service::PersonRequest_<ContainerAllocator>&) { return value(); }
};

template<class ContainerAllocator>
struct Definition< ::learning_service::PersonRequest_<ContainerAllocator> >
{
  static const char* value()
  {
    return "string namen"
"uint8 agen"
"uint8 sexn"
"n"
"uint8 unknown = 0n"
"uint8 male    = 1n"
"uint8 female  = 2n"
;
  }

  static const char* value(const ::learning_service::PersonRequest_<ContainerAllocator>&) { return value(); }
};

} // namespace message_traits
} // namespace ros

namespace ros
{
namespace serialization
{

  template<class ContainerAllocator> struct Serializer< ::learning_service::PersonRequest_<ContainerAllocator> >
  {
    template<typename Stream, typename T> inline static void allInOne(Stream& stream, T m)
    {
      stream.next(m.name);
      stream.next(m.age);
      stream.next(m.sex);
    }

    ROS_DECLARE_ALLINONE_SERIALIZER
  }; // struct PersonRequest_

} // namespace serialization
} // namespace ros

namespace ros
{
namespace message_operations
{

template<class ContainerAllocator>
struct Printer< ::learning_service::PersonRequest_<ContainerAllocator> >
{
  template<typename Stream> static void stream(Stream& s, const std::string& indent, const ::learning_service::PersonRequest_<ContainerAllocator>& v)
  {
    s << indent << "name: ";
    Printer<std::basic_string<char, std::char_traits<char>, typename std::allocator_traits<ContainerAllocator>::template rebind_alloc<char>>>::stream(s, indent   "  ", v.name);
    s << indent << "age: ";
    Printer<uint8_t>::stream(s, indent   "  ", v.age);
    s << indent << "sex: ";
    Printer<uint8_t>::stream(s, indent   "  ", v.sex);
  }
};

} // namespace message_operations
} // namespace ros

#endif // LEARNING_SERVICE_MESSAGE_PERSONREQUEST_H

PersonResponse.h内容如下

代码语言:javascript复制
// Generated by gencpp from file learning_service/PersonResponse.msg
// DO NOT EDIT!


#ifndef LEARNING_SERVICE_MESSAGE_PERSONRESPONSE_H
#define LEARNING_SERVICE_MESSAGE_PERSONRESPONSE_H


#include <string>
#include <vector>
#include <memory>

#include <ros/types.h>
#include <ros/serialization.h>
#include <ros/builtin_message_traits.h>
#include <ros/message_operations.h>


namespace learning_service
{
template <class ContainerAllocator>
struct PersonResponse_
{
  typedef PersonResponse_<ContainerAllocator> Type;

  PersonResponse_()
    : result()  {
    }
  PersonResponse_(const ContainerAllocator& _alloc)
    : result(_alloc)  {
  (void)_alloc;
    }



   typedef std::basic_string<char, std::char_traits<char>, typename std::allocator_traits<ContainerAllocator>::template rebind_alloc<char>> _result_type;
  _result_type result;





  typedef boost::shared_ptr< ::learning_service::PersonResponse_<ContainerAllocator> > Ptr;
  typedef boost::shared_ptr< ::learning_service::PersonResponse_<ContainerAllocator> const> ConstPtr;

}; // struct PersonResponse_

typedef ::learning_service::PersonResponse_<std::allocator<void> > PersonResponse;

typedef boost::shared_ptr< ::learning_service::PersonResponse > PersonResponsePtr;
typedef boost::shared_ptr< ::learning_service::PersonResponse const> PersonResponseConstPtr;

// constants requiring out of line definition



template<typename ContainerAllocator>
std::ostream& operator<<(std::ostream& s, const ::learning_service::PersonResponse_<ContainerAllocator> & v)
{
ros::message_operations::Printer< ::learning_service::PersonResponse_<ContainerAllocator> >::stream(s, "", v);
return s;
}


template<typename ContainerAllocator1, typename ContainerAllocator2>
bool operator==(const ::learning_service::PersonResponse_<ContainerAllocator1> & lhs, const ::learning_service::PersonResponse_<ContainerAllocator2> & rhs)
{
  return lhs.result == rhs.result;
}

template<typename ContainerAllocator1, typename ContainerAllocator2>
bool operator!=(const ::learning_service::PersonResponse_<ContainerAllocator1> & lhs, const ::learning_service::PersonResponse_<ContainerAllocator2> & rhs)
{
  return !(lhs == rhs);
}


} // namespace learning_service

namespace ros
{
namespace message_traits
{





template <class ContainerAllocator>
struct IsMessage< ::learning_service::PersonResponse_<ContainerAllocator> >
  : TrueType
  { };

template <class ContainerAllocator>
struct IsMessage< ::learning_service::PersonResponse_<ContainerAllocator> const>
  : TrueType
  { };

template <class ContainerAllocator>
struct IsFixedSize< ::learning_service::PersonResponse_<ContainerAllocator> >
  : FalseType
  { };

template <class ContainerAllocator>
struct IsFixedSize< ::learning_service::PersonResponse_<ContainerAllocator> const>
  : FalseType
  { };

template <class ContainerAllocator>
struct HasHeader< ::learning_service::PersonResponse_<ContainerAllocator> >
  : FalseType
  { };

template <class ContainerAllocator>
struct HasHeader< ::learning_service::PersonResponse_<ContainerAllocator> const>
  : FalseType
  { };


template<class ContainerAllocator>
struct MD5Sum< ::learning_service::PersonResponse_<ContainerAllocator> >
{
  static const char* value()
  {
    return "c22f2a1ed8654a0b365f1bb3f7ff2c0f";
  }

  static const char* value(const ::learning_service::PersonResponse_<ContainerAllocator>&) { return value(); }
  static const uint64_t static_value1 = 0xc22f2a1ed8654a0bULL;
  static const uint64_t static_value2 = 0x365f1bb3f7ff2c0fULL;
};

template<class ContainerAllocator>
struct DataType< ::learning_service::PersonResponse_<ContainerAllocator> >
{
  static const char* value()
  {
    return "learning_service/PersonResponse";
  }

  static const char* value(const ::learning_service::PersonResponse_<ContainerAllocator>&) { return value(); }
};

template<class ContainerAllocator>
struct Definition< ::learning_service::PersonResponse_<ContainerAllocator> >
{
  static const char* value()
  {
    return "string resultn"
"n"
;
  }

  static const char* value(const ::learning_service::PersonResponse_<ContainerAllocator>&) { return value(); }
};

} // namespace message_traits
} // namespace ros

namespace ros
{
namespace serialization
{

  template<class ContainerAllocator> struct Serializer< ::learning_service::PersonResponse_<ContainerAllocator> >
  {
    template<typename Stream, typename T> inline static void allInOne(Stream& stream, T m)
    {
      stream.next(m.result);
    }

    ROS_DECLARE_ALLINONE_SERIALIZER
  }; // struct PersonResponse_

} // namespace serialization
} // namespace ros

namespace ros
{
namespace message_operations
{

template<class ContainerAllocator>
struct Printer< ::learning_service::PersonResponse_<ContainerAllocator> >
{
  template<typename Stream> static void stream(Stream& s, const std::string& indent, const ::learning_service::PersonResponse_<ContainerAllocator>& v)
  {
    s << indent << "result: ";
    Printer<std::basic_string<char, std::char_traits<char>, typename std::allocator_traits<ContainerAllocator>::template rebind_alloc<char>>>::stream(s, indent   "  ", v.result);
  }
};

} // namespace message_operations
} // namespace ros

#endif // LEARNING_SERVICE_MESSAGE_PERSONRESPONSE_H

它们都是C 的头文件

创建服务端与客户端

进入 learning_service 的源码文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_service/src

创建服务端

代码语言:javascript复制
vim person_server.cpp

内容如下

代码语言:javascript复制
/*
 * 该例程将执行/show_person服务,服务数据类型learning_service::Person
 */

#include <ros/ros.h>
#include "learning_service/Person.h"

using namespace ros;
//service回调函数,输入参数req,输出参数res
bool personCallback(learning_service::Person::Request &req, learning_service::Person::Response &res) {
    //显示请求数据
    ROS_INFO("Person: name:%s  age:%d  sex:%d", req.name.c_str(), req.age, req.sex);
    //设置反馈数据
    res.result = "OK";

    return true;
}

int main(int argc, char **argv) {
    //ROS节点初始化
    init(argc, argv, "person_server");
    //创建节点句柄
    NodeHandle n;
    //创建一个名为/show_person的server,注册回调函数personCallback
    ServiceServer person_service = n.advertiseService("/show_person", personCallback);
    //循环等待回调函数
    ROS_INFO("Ready to show person informtion");
    spin();

    return 0;
}

创建客户端

代码语言:javascript复制
vim person_client.cpp

内容如下

代码语言:javascript复制
/*
 * 该例程将请求/show_person服务,服务数据类型learning_service::Person
 */

#include <ros/ros.h>
#include "learning_service/Person.h"

using namespace ros;

int main(int argc, char **argv) {
    //初始化ROS节点
    init(argc, argv, "person_client");
    //创建节点句柄
    NodeHandle node;
    //发现/show_person服务后,创建一个服务客户端,连接名为/show_person的service
    service::waitForService("/show_person");
    ServiceClient person_client = node.serviceClient<learning_service::Person>("/show_person");
    //初始化learning_service::Person的请求数据
    learning_service::Person srv;
    srv.request.name = "Tom";
    srv.request.age = 20;
    srv.request.sex = learning_service::Person::Request::male;
    //请求服务调用
    ROS_INFO("Call service to show person[name:%s, age:%d, sex:%d]", srv.request.name.c_str(), srv.request.age, srv.request.sex);

    person_client.call(srv);
    //显示服务调用结果
    ROS_INFO("Show person result: %s", srv.response.result.c_str());

    return 0;
}

回到 learning_service文件夹,修改 CMakelists.txt

代码语言:javascript复制
cd ..
vim CMakelists.txt

添加内容如下

代码语言:javascript复制
add_executable(person_server src/person_server.cpp)
target_link_libraries(person_server ${catkin_LIBRARIES})
add_dependencies(person_server ${PROJECT_NAME}_gencpp)

add_executable(person_client src/person_client.cpp)
target_link_libraries(person_client ${catkin_LIBRARIES})
add_dependencies(person_client ${PROJECT_NAME}_gencpp)

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun learning_service person_client
rosrun learning_service person_server

执行完成后在客户端返回

代码语言:javascript复制
[ INFO] [1692650896.468258699]: waitForService: Service [/show_person] has not been advertised, waiting...
[ INFO] [1692650916.053619203]: waitForService: Service [/show_person] is now available.
[ INFO] [1692650916.053701403]: Call service to show person[name:Tom, age:20, sex:1]
[ INFO] [1692650916.055466855]: Show person result: OK

在服务端打印日志

代码语言:javascript复制
[ INFO] [1692650916.045710322]: Ready to show person informtion
[ INFO] [1692650916.055297356]: Person: name:Tom  age:20  sex:1

Python实现

进入包脚本文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_service/scripts

创建服务端

代码语言:javascript复制
vim person_server.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程将执行/show_person服务,服务数据类型learning_service::Person

import rospy
from learning_service.srv import Person, PersonResponse

# service回调函数,输入参数req,输出参数res
def personCallback(req):
    # 显示请求数据
    rospy.loginfo("Person: name:%s  age:%d   sex:%d", req.name, req.age, req.sex)
    # 设置反馈数据
    return PersonResponse("OK")

def person_server():
    # ROS节点初始化
    rospy.init_node('person_server')
    # 创建一个名为/show_person的server,注册回调函数personCallback
    s = rospy.Service('/show_person', Person, personCallback)
    # 循环等待回调函数
    print("Ready to show person informtion")
    rospy.spin()

if __name__ == "__main__":

    person_server()

创建客户端

代码语言:javascript复制
vim person_client.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程将请求/show_person服务,服务数据类型learning_service::Person

import sys
import rospy
from learning_service.srv import Person, PersonRequest

def person_client():
    # 初始化ROS节点
    rospy.init_node('person_client')
    # 发现/show_person服务后,创建一个服务客户端,连接名为/show_person的service
    rospy.wait_for_service('/show_person')
    try:
        person_client = rospy.ServiceProxy('/show_person', Person)
        # 请求服务调用,输入请求数据
        response = person_client("Tom", 20, PersonRequest.male)
        return response.result
    except rospy.ServiceException as e:
        print("Service call failed: %s"%e)

if __name__ == "__main__":
    # 显示服务调用结果
    print("Show person result : %s" %(person_client()))

赋予它们可执行权限

代码语言:javascript复制
chmod 777 person_server.py 
chmod 777 person_client.py

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun learning_service person_client.py
rosrun learning_service person_server.py

参数的使用与编程方法

在ROS Master当中有一个参数服务器——Parameter Server,它是一个全局字典用来保存各个节点的配置参数。在上图中,参数服务器保存了三个值——机器人的名字、半径和高度。这些参数是各个节点都可以全局访问的。比方说在Node A中要查询机器人的名字,就会发送向ROS Master发送请求,并返回my_robot的结果。所有的节点都可以进行这样的处理。

参数服务器的使用

  • 命令行的使用

先运行ROS Master以及小海龟仿真器节点

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node

 输入

代码语言:javascript复制
rosparam

返回

代码语言:javascript复制
rosparam is a command-line tool for getting, setting, and deleting parameters from the ROS Parameter Server.

Commands:
	rosparam set	set parameter
	rosparam get	get parameter
	rosparam load	load parameters from file
	rosparam dump	dump parameters to file
	rosparam delete	delete parameter
	rosparam list	list parameter names

表示该命令有这些参数可以设置

查询有多少参数节点

代码语言:javascript复制
rosparam list

返回

代码语言:javascript复制
/rosdistro
/roslaunch/uris/host_guanjian_x99__40731
/rosversion
/run_id
/turtlesim/background_b
/turtlesim/background_g
/turtlesim/background_r

其中

代码语言:javascript复制
/turtlesim/background_b
/turtlesim/background_g
/turtlesim/background_r

表示小海龟仿真器的背景颜色

代码语言:javascript复制
/rosdistro
/roslaunch/uris/host_guanjian_x99__40731
/rosversion
/run_id

为ROS Master本身提供的参数,可以提供ROS的版本号,运行的进程id。

获取小海龟仿真器背景颜色RGB中的B值

代码语言:javascript复制
rosparam get /turtlesim/background_b

返回

代码语言:javascript复制
255

获取小海龟仿真器背景颜色RGB中的G值 

代码语言:javascript复制
rosparam get /turtlesim/background_g

返回

代码语言:javascript复制
86

获取小海龟仿真器背景颜色RGB中的R值  

代码语言:javascript复制
rosparam get /turtlesim/background_r

返回

代码语言:javascript复制
69

获取ROS的发行版

代码语言:javascript复制
rosparam get /rosdistro

返回

代码语言:javascript复制
'noetic

  '

 获取ROS的版本号

代码语言:javascript复制
rosparam get /rosversion

返回

代码语言:javascript复制
'1.16.0

  '

修改小海龟仿真器背景色RGB的B值

代码语言:javascript复制
rosparam set /turtlesim/background_b 100

这里我们需要注意的是,虽然我们修改了背景色,但是不会马上生效,我们需要发送一个服务请求来使得小海龟仿真器节点重新读取参数服务器的参数

代码语言:javascript复制
rosservice call /clear "{}"

此时小海龟仿真器的背景颜色就发生了变化。

将参数服务器的参数保存为文件

代码语言:javascript复制
rosparam dump param.yaml

保存后该文件的内容如下

代码语言:javascript复制
rosdistro: 'noetic

  '
roslaunch:
  uris:
    host_guanjian_x99__40731: http://guanjian-X99:40731/
rosversion: '1.16.0

  '
run_id: b1762296-4477-11ee-98d0-8defeaece2a0
turtlesim:
  background_b: 100
  background_g: 86
  background_r: 69

我们可以修改该文件,再重新加载回参数服务器中 。比如我们将小海龟仿真器节点的RGB值都修改为0

代码语言:javascript复制
rosdistro: 'noetic

  '
roslaunch:
  uris:
    host_guanjian_x99__40731: http://guanjian-X99:40731/
rosversion: '1.16.0

  '
run_id: b1762296-4477-11ee-98d0-8defeaece2a0
turtlesim:
  background_b: 0
  background_g: 0
  background_r: 0

进行加载

代码语言:javascript复制
rosparam load param.yaml

再次发送服务请求

代码语言:javascript复制
rosservice call /clear "{}"

此时我们就可以看到小海龟仿真器的背景色就变成黑色了。

删除某一个参数,比如我们删除background_g

代码语言:javascript复制
rosparam delete /turtlesim/background_g

 此时我们读取参数列表

代码语言:javascript复制
rosparam list

返回

代码语言:javascript复制
/rosdistro
/roslaunch/uris/host_guanjian_x99__40731
/rosversion
/run_id
/turtlesim/background_b
/turtlesim/background_r

再次发送服务请求

代码语言:javascript复制
rosservice call /clear "{}"

小海龟仿真器的背景色也会发生相应的变化。

  • 编程开发(C 实现)

进入工作空间

代码语言:javascript复制
cd Documents/catkin_ws/src

创建一个新的包

代码语言:javascript复制
catkin_create_pkg learning_parameter roscpp rospy std_srvs

进入learning_parameter的功能包中

代码语言:javascript复制
cd learning_parameter/src

创建我们需要的C 文件

代码语言:javascript复制
vim parameter_config.cpp

内容如下

代码语言:javascript复制
/*
 * 该例程设置/读取小海龟例程中的参数
 */

#include <string>
#include <ros/ros.h>
#include <std_srvs/Empty.h>

using namespace ros;

int main(int argc, char **argv) {
    int red, green, blue;
    //ROS节点初始化
    init(argc, argv, "parameter_config");
    //创建节点句柄
    NodeHandle node;
    //读取背景颜色参数
    param::get("/turtlesim/background_r", red);
    param::get("/turtlesim/background_g", green);
    param::get("/turtlesim/background_b", blue);

    ROS_INFO("Get Background Color[%d, %d, %d]", red, green, blue);
    //设置背景颜色参数
    param::set("/turtlesim/background_r", 255);
    param::set("/turtlesim/background_g", 255);
    param::set("/turtlesim/background_b", 255);

    ROS_INFO("Set Background Color[255, 255, 255]");
    //读取背景颜色参数
    param::get("/turtlesim/background_r", red);
    param::get("/turtlesim/background_g", green);
    param::get("/turtlesim/background_b", blue);

    ROS_INFO("Re-get Background Color[%d, %d, %d]", red, green, blue);
    //调用服务,刷新背景颜色
    service::waitForService("/clear");
    ServiceClient clear_background = node.serviceClient<std_srvs::Empty>("/clear");
    std_srvs::Empty srv;
    clear_background.call(srv);

    sleep(1);

    return 0;
}

回到 learning_parameter文件夹,修改 CMakelists.txt。

代码语言:javascript复制
cd ..
vim CMakelists.txt

添加内容如下

代码语言:javascript复制
add_executable(parameter_config src/parameter_config.cpp)
target_link_libraries(parameter_config ${catkin_LIBRARIES})

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_parameter parameter_config

返回

代码语言:javascript复制
[ INFO] [1693175924.083145386]: Get Background Color[69, 86, 255]
[ INFO] [1693175924.086266645]: Set Background Color[255, 255, 255]
[ INFO] [1693175924.087441186]: Re-get Background Color[255, 255, 255]

并且小海龟仿真器窗口的背景颜色变成了白色。

  • 编程开发(Python实现)

进入包文件夹,并创建脚本文件夹,进入该文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_parameter
mkdir scripts
cd scripts

创建我们需要的 Python 文件

代码语言:javascript复制
vim parameter_config.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程设置/读取小海龟例程中的参数

import sys
import rospy
from std_srvs.srv import Empty

def parameter_config():
    # ROS节点初始化
    rospy.init_node('parameter_config', anonymous=True)
    # 读取背景颜色参数
    red = rospy.get_param('/turtlesim/background_r')
    green = rospy.get_param('/turtlesim/background_g')
    blue = rospy.get_param('/turtlesim/background_b')

    rospy.loginfo("Get Backgroud Color[%d, %d, %d]", red, green, blue)
    # 设置背景颜色参数
    rospy.set_param("/turtlesim/background_r", 255)
    rospy.set_param("/turtlesim/background_g", 255)
    rospy.set_param("/turtlesim/background_b", 255)
    # 读取背景颜色参数
    red = rospy.get_param('/turtlesim/background_r')
    green = rospy.get_param('/turtlesim/background_g')
    blue = rospy.get_param('/turtlesim/background_b')

    rospy.loginfo("Get Backgroud Color[%d, %d, %d]", red, green, blue)
    # 调用服务,刷新背景颜色
    rospy.wait_for_service('/clear')
    try:
        clear_background = rospy.ServiceProxy('/clear', Empty)

        response = clear_background()
        return response
    except rospy.ServiceException as e:
        print("Service call failed: %s"%e)

if __name__ == "__main__":

    parameter_config()

赋予可执行权限

代码语言:javascript复制
chmod 777 parameter_config.py

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_parameter parameter_config.py

坐标变换管理系统

机器人的位置坐标变换是一系列的4*4的矩阵运算,具体可以参考SLAM知识点整理 中的刚体运动

TF坐标变换

TF是ROS中的提供机器人坐标变换运算的工具。

  1. 5秒前,机器人头部坐标系相对于全局坐标系的关系是什么样的?
  2. 机器人夹取的物体相对于机器人中心坐标系的位置在哪里?
  3. 机器人中心坐标系相对于全局坐标系的位置在哪里?

如何实现

  1. 广播TF变换
  2. 监听TF变换

这里并不是我们之前说的话题Topic和服务Service。在启动了ROS Master以及TF之后就会在后台去维护叫做TF树。所有坐标系都是通过这种树形结构去保存在TF树当中。任意一个节点想去查询任意两个坐标系之间的变换关系,可以直接通过TF树去查询得到。

上图最左边是一个机器人,红色部分是机器人的底盘,我们会在其中心点做一个坐标系,称之为base_link。底盘上面有一个激光雷达,激光雷达需要发射雷达信号来获取深度信息,我们也会在其中心部分建立一个坐标系。激光雷达所有检测到的物体都是建立在该坐标系下的位置的,该坐标系称为base_laser。在中间的图中表示这两个坐标系之间的平移关系——上下20cm,左右10cm,它们之间没有旋转关系。在最右边的图中,当激光雷达检测到前面有一堵墙,距离0.3米,此时我们需要将其转换为base_link坐标系下的坐标,这个是可以通过TF来得到的。通常我们关注的是base_link坐标系的距离,而不是base_laser的距离。通过TF变换,我们知道墙在base_link下是0.4米。变换方式如

坐标变换实例

安装小海龟坐标变换组件

代码语言:javascript复制
sudo apt-get install ros-noetic-turtle-tf

如果是Ubuntu 18.04的系统则为

代码语言:javascript复制
sudo apt-get install ros-melodic-turtle-tf

更换我们的conda环境,此处的环境需要与我们之前Python编程的环境一致,并且安装numpy

代码语言:javascript复制
conda activate py39
pip install numpy

启动一个脚本,这是一个脚本文件,可以一次性启动很多节点

代码语言:javascript复制
roslaunch turtle_tf turtle_tf_demo.launch

启动完成后,我们会看见一个出现两只小海龟的窗口,并且有一只小海龟向着另一只小海龟移动

启动小海龟按键控制节点

代码语言:javascript复制
rosrun turtlesim turtle_teleop_key

此时我们用键盘移动一只小海龟,另外一只小海龟会进行跟随。

对小海龟的信息进行监听,新打开一个终端,同样需要切换conda环境

代码语言:javascript复制
conda activate py39
rosrun tf view_frames

运行后会报一个

代码语言:javascript复制
TypeError: cannot use a string pattern on a bytes-like object

这是因为该功能的源文件是使用Python 2写的,而我们的环境是Python 3,故我们需要修改

代码语言:javascript复制
sudo vim /opt/ros/noetic/lib/tf/view_frames

修改内容为

代码语言:javascript复制
vstr = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE).communicate()[1]
vstr = str(vstr)

同时修改文件夹和view_frames所有者

代码语言:javascript复制
sudo chown user:user /opt/ros/noetic/lib/tf
sudo chown user:user /opt/ros/noetic/lib/tf/view_frames

重新运行

代码语言:javascript复制
rosrun tf view_frames

会在/opt/ros/noetic/lib/tf下产生两个新的文件

代码语言:javascript复制
frames.gv
frames.pdf

 这里我们主要来看一下frames.pdf文件

它的意思是在当前的仿真器中一共有三个坐标系,一个是world,它是一个全局坐标系,表示的是整个仿真器的坐标原点。这个坐标点是永远不会动的。另外两个坐标系,一个叫turle1,一个叫tutle2,分别是位于两只小海龟上的。turle1和tutle2会不断的重合,但相对于world来讲是不断的变化的。

  • 命令行工具
代码语言:javascript复制
rosrun tf tf_echo turtle1 turtle2

这里的turtle1和turtle2代表两个坐标系。

此时当我们使用键盘控制小海龟移动,该工具会记录两个坐标系之间的变换记录

代码语言:javascript复制
At time 1693616316.041
- Translation: [-0.909, 1.462, 0.000]
- Rotation: in Quaternion [0.000, 0.000, -0.548, 0.837]
            in RPY (radian) [0.000, -0.000, -1.160]
            in RPY (degree) [0.000, -0.000, -66.444]
At time 1693616317.049
- Translation: [-2.223, 0.791, 0.000]
- Rotation: in Quaternion [0.000, 0.000, -0.225, 0.974]
            in RPY (radian) [0.000, 0.000, -0.454]
            in RPY (degree) [0.000, 0.000, -25.995]
At time 1693616318.041
- Translation: [-1.314, 1.677, 0.000]
- Rotation: in Quaternion [0.000, 0.000, -0.496, 0.868]
            in RPY (radian) [0.000, 0.000, -1.038]
            in RPY (degree) [0.000, 0.000, -59.484]
At time 1693616319.049
- Translation: [-2.085, 0.923, 0.000]
- Rotation: in Quaternion [0.000, 0.000, -0.232, 0.973]
            in RPY (radian) [0.000, 0.000, -0.469]
            in RPY (degree) [0.000, 0.000, -26.852]
At time 1693616320.041
- Translation: [-1.256, 0.547, 0.000]
- Rotation: in Quaternion [0.000, 0.000, -0.204, 0.979]
            in RPY (radian) [0.000, 0.000, -0.412]
            in RPY (degree) [0.000, 0.000, -23.582]
At time 1693616321.049
- Translation: [-0.749, 0.326, 0.000]
- Rotation: in Quaternion [0.000, 0.000, -0.204, 0.979]
            in RPY (radian) [0.000, 0.000, -0.411]
            in RPY (degree) [0.000, 0.000, -23.544]

这里描述的是turtle2坐标系是怎么通过一个变换变成turtle1坐标系。它分成两个部分——Translation(平移)和Rotation(旋转)。

Translation的三个数值表示x,y,z三个坐标上的平移,Rotation有三种表达方式,第一种为Quaternion(四元数),有关四元数的概念可以参考SLAM知识点整理 中的四元数。第二个是通过弧度单位来描述的RPY,即通过x轴,y轴,z轴来旋转。第三个是通过角度单位来描述的RPY。

  • 可视化工具
代码语言:javascript复制
rosrun rviz rviz -d `rospack find turtle_tf`/rviz/turtle_rviz.rviz

打开之后,需要将Fixed Frame修改为world,然后点击Add按钮,添加TF的选项。此时我们再操作键盘控制小海龟移动,会看到rviz工具中的turtle2坐标系不断地在向turtle1坐标系移动。

在网格的中间有一个原点,它就是world坐标原点,在左边有一红一绿两个点,它就是turle1和turle2两个坐标系。

tf坐标系广播与监听的编程实现

  • C 实现

进入工作空间

代码语言:javascript复制
cd Documents/catkin_ws/src

创建一个新的功能包

代码语言:javascript复制
catkin_create_pkg learning_tf roscpp rospy tf turtlesim

进入到该功能包的 src 目录

代码语言:javascript复制
cd learning_tf/src

创建我们需要的广播器C 文件

代码语言:javascript复制
vim turtle_tf_broadcaster.cpp

内容如下

代码语言:javascript复制
/*
 * 该例程产生tf数据,并计算、发布turtle2的速度指令
 */

#include <ros/ros.h>
#include <tf/transform_broadcaster.h>
#include <turtlesim/Pose.h>
#include <string>

using namespace ros;
using namespace std;

string turtle_name;

void poseCallback(const turtlesim::PoseConstPtr& msg) {
    //创建tf的广播器
    static tf::TransformBroadcaster br;
    //初始化tf数据
    //transform是坐标变换矩阵
    tf::Transform transform;
    //坐标平移,turtle1相对于world的,围绕x,y,z坐标轴的平移变换
    transform.setOrigin(tf::Vector3(msg->x, msg->y, 0.0));
    //四元数
    tf::Quaternion q;
    //坐标旋转,turtle1相对于world的,围绕x,y,z坐标轴的旋转变换
    q.setRPY(0, 0, msg->theta);
    transform.setRotation(q);
    //广播world与海龟坐标系turtle1之间的tf数据
    //后台的tf树会将广播的数据插入进去
    br.sendTransform(tf::StampedTransform(transform, Time::now(), "world", turtle_name));
}

int main(int argc, char** argv) {
    //初始化ROS节点
    init(argc, argv, "my_tf_broadcaster");
    //输入参数作为海龟的名字
    if (argc != 2) {
	ROS_ERROR("need turtle name as argument");
	return -1;
    }
    turtle_name = argv[1];
    //订阅海龟的位姿话题
    NodeHandle node;
    Subscriber sub = node.subscribe(turtle_name   "/pose", 10, &poseCallback);
    //循环等待回调函数
    spin();

    return 0;
}

再创建我们需要的监听器C 文件

代码语言:javascript复制
vim turtle_tf_listener.cpp

内容如下

代码语言:javascript复制
/*
 * 该例程监听tf数据,并计算、发布turtle2的速度指令
 */

#include <ros/ros.h>
#include <tf/transform_listener.h>
#include <geometry_msgs/Twist.h>
#include <turtlesim/Spawn.h>

using namespace ros;

int main(int argc, char** argv) {
    //初始化ROS节点
    init(argc, argv, "my_tf_listener");
    //创建节点句柄
    NodeHandle node;
    //请求产生新海龟turtle2
    service::waitForService("/spawn");
    ServiceClient add_turtle = node.serviceClient<turtlesim::Spawn>("/spawn");
    turtlesim::Spawn srv;
    add_turtle.call(srv);
    //创建发布turtle2速度控制指令的发布者,让新海龟动起来
    Publisher turtle_vel = node.advertise<geometry_msgs::Twist>("/turtle2/cmd_vel", 10);
    //创建tf的监听器
    tf::TransformListener listener;

    Rate rate(10.0);
    while(node.ok()) {
        //获取turtle1与turtle2坐标系之间的tf数据
	tf::StampedTransform transform;
	try {
            //等待变换,系统中是否存在turtle1和turtle2两个坐标系,等待3秒
	    listener.waitForTransform("/turtle2", "/turtle1", Time(0), Duration(3.0));
            //查询变换,如果存在查询两个坐标系的关系,查询结果保存在transform中
	    listener.lookupTransform("/turtle2", "/turtle1", Time(0), transform);
	} catch (tf::TransformException &ex) {
	    ROS_ERROR("%s", ex.what());
	    Duration(1.0).sleep();
	    continue;
	}
        //根据turtle1与turtle2坐标系之间的位置关系,发布turtle2的速度控制指令,让海龟2向海龟1做移动
	geometry_msgs::Twist vel_msg;
        //角速度
	vel_msg.angular.z = 4.0 * atan2(transform.getOrigin().y(), transform.getOrigin().x());
        //线速度
	vel_msg.linear.x = 0.5 * sqrt(pow(transform.getOrigin().x(), 2)   pow(transform.getOrigin().y(), 2));
	turtle_vel.publish(vel_msg);

	rate.sleep();
    }
    return 0;
}

回到 learning_tf 文件夹,修改 CMakelists.txt。

代码语言:javascript复制
cd ..
vim CMakelists.txt

添加内容如下

代码语言:javascript复制
add_executable(turtle_tf_broadcaster src/turtle_tf_broadcaster.cpp)
target_link_libraries(turtle_tf_broadcaster ${catkin_LIBRARIES})

add_executable(turtle_tf_listener src/turtle_tf_listener.cpp)
target_link_libraries(turtle_tf_listener ${catkin_LIBRARIES})

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../..
catkin_make

执行该程序,在不同的终端窗口执行

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_tf turtle_tf_broadcaster __name:=turtle1_tf_broadcaster /turtle1
rosrun learning_tf turtle_tf_broadcaster __name:=turtle2_tf_broadcaster /turtle2
rosrun learning_tf turtle_tf_listener
rosrun turtlesim turtle_teleop_key

通过键盘操作第一只小海龟移动,我们会发现第二只小海龟在进行跟随。

  • Python实现

进入包文件夹

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_tf

新建脚本文件夹

代码语言:javascript复制
mkdir scripts
cd scripts

创建我们需要的广播器Python 文件

代码语言:javascript复制
vim turtle_tf_broadcaster.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程产生tf数据,并计算、发布turtle2的速度指令

import roslib
roslib.load_manifest('learning_tf')
import rospy
import tf
import turtlesim.msg
import sys

def handle_turtle_pose(msg, turtlename):
    # 创建tf的广播器
    br = tf.TransformBroadcaster()
    # 广播world与海龟坐标系turtle1之间的tf数据
    br.sendTransform((msg.x, msg.y, 0), tf.transformations.quaternion_from_euler(0, 0, msg.theta), rospy.Time.now(), turtlename, "world")

if __name__ == '__main__':
    # 初始化ROS节点
    rospy.init_node(sys.argv[1])
    # 输入参数作为海龟的名字
    turtlename = sys.argv[2]
    # 订阅海龟的位姿话题
    rospy.Subscriber('/%s/pose' % turtlename, turtlesim.msg.Pose, handle_turtle_pose, turtlename)
    # 循环等待回调函数
    rospy.spin()

再创建我们需要的监听器Python文件

代码语言:javascript复制
vim turtle_tf_listener.py

内容如下

代码语言:javascript复制
#!/home/user/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-
# 该例程监听tf数据,并计算、发布turtle2的速度指令

import roslib
roslib.load_manifest('learning_tf')
import rospy
import math
import tf
import geometry_msgs.msg
import turtlesim.srv

if __name__ == '__main__':
    # 初始化ROS节点
    rospy.init_node('turtle_tf_listener')
    # 创建tf的监听器
    listener = tf.TransformListener()
    # 请求产生新海龟turtle2
    rospy.wait_for_service('/spawn')
    spawner = rospy.ServiceProxy('/spawn', turtlesim.srv.Spawn)
    spawner(4, 2, 0, 'turtle2')
    # 创建发布turtle2速度控制指令的发布者,让新海龟动起来
    turtle_vel = rospy.Publisher('turtle2/cmd_vel', geometry_msgs.msg.Twist, queue_size=1)

    rate = rospy.Rate(10.0)
    while not rospy.is_shutdown():
        try:
            # 查询变换,如果存在查询两个坐标系的关系,查询结果保存在trans中
            trans, rot = listener.lookupTransform('/turtle2', '/turtle1', rospy.Time(0))
        except (tf.LookupException, tf.ConnectivityException, tf.ExtrapolationException):
            continue
        # 根据turtle1与turtle2坐标系之间的位置关系,发布turtle2的速度控制指令,让海龟2向海龟1做移动
        angular = 4 * math.atan2(trans[1], trans[0])
        linear = 0.5 * math.sqrt(trans[0]**2   trans[1]**2)
        cmd = geometry_msgs.msg.Twist()
        cmd.linear.x = linear
        cmd.angular.z = angular
        turtle_vel.publish(cmd)

        rate.sleep()

设置可执行权限

代码语言:javascript复制
chmod 777 turtle_tf_broadcaster.py
chmod 777 turtle_tf_listener.py

执行命令 (不同终端窗口)

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun learning_tf turtle_tf_broadcaster.py turtle1_tf_broadcaster turtle1
rosrun learning_tf turtle_tf_broadcaster.py turtle2_tf_broadcaster turtle2
rosrun learning_tf turtle_tf_listener.py
rosrun turtlesim turtle_teleop_key

launch启动文件

语法

launch文件:通过XML文件实现多节点的配置和启动(可自动启动ROS Master)

代码语言:javascript复制
<launch>
    <node pkg="turtlesim" name="sim1" type="turtlesim_node" />
    <node pkg="turtlesim" name="sim2" type="turtlesim_node" />
</launch>

这其中<launch>是根节点。

  • <node>为启动节点
  • pkg:节点所在的功能包名称
  • type:节点的可执行文件名称
  • name:节点运行时的名称
  • output:是否将日志信息打印到终端
  • respawn:如果节点进程挂掉,是否重启
  • required:必须要启动的节点
  • ns:命名空间,避免命名冲突
  • args:运行时参数

其中前3个是最基础的属性,后面的属性是可选的。

  • <param>为设置ROS系统运行中的参数,存储在参数服务器中
代码语言:javascript复制
<param name="output_frame" value="odom" />
  1. name:参数名
  2. value:参数值
  3. <rosparam>加载参数文件中的多个参数
代码语言:javascript复制
<rosparam file="params.yaml" command="load" ns="params" />
  • <arg>launch文件内部的局部变量,仅限于launch文件使用
代码语言:javascript复制
<arg name="arg-name" default="arg-value" />
  1. name:参数名
  2. value:参数值

调用

代码语言:javascript复制
<param name="foo" value="$(arg arg-name)" />
<node name="node" pkg="package" type="type" args="$(arg arg-name)" />

这里的两个调用都是调用的同一个arg。它们彼此之间没有调用关系。

  • <remap>重映射ROS计算图资源的命名
代码语言:javascript复制
<remap from="/turtlebot/cmd_vel" to="/cmd_vel" />
  1. from:原命名
  2. to:映射之后的命名

这里需要注意的是如果重映射了,那么原名就不存在了,只有映射后的名字。

  • <include>嵌套,包含其他launch文件,类似C语言中的头文件包含
代码语言:javascript复制
<include file="$(dirname)/other.launch" />

file:包含的其他launch文件路径

更多标签可以参考https://wiki.ros.org/roslaunch/XML

操作

进入工作空间

代码语言:javascript复制
cd Documents/catkin_ws/src

创建一个新的功能包,这里我们不需要任何依赖

代码语言:javascript复制
catkin_create_pkg learning_launch

进入到该功能包,并且创建launch文件夹

代码语言:javascript复制
cd learning_launch
mkdir launch
cd launch

创建我们需要的第一个launch文件

代码语言:javascript复制
vim simple.launch

内容如下

代码语言:javascript复制
<launch>
    <node pkg="learning_topic" type="person_subscriber" name="talker" output="screen" />
    <node pkg="learning_topic" type="person_publisher" name="listener" output="screen" />
</launch>

回到 catkin_ws 文件夹,开始编译

代码语言:javascript复制
cd ../../..
catkin_make

启动launch文件

代码语言:javascript复制
roslaunch learning_launch simple.launch

日志输出

代码语言:javascript复制
... logging to /home/guanjian/.ros/log/8f043736-4a15-11ee-94ed-eb1c4a5ce82f/roslaunch-guanjian-X99-4414.log
Checking log directory for disk usage. This may take a while.
Press Ctrl-C to interrupt
Done checking log file disk usage. Usage is <1GB.

started roslaunch server http://guanjian-X99:43073/

SUMMARY
========

PARAMETERS
 * /rosdistro: noetic
 * /rosversion: 1.16.0

NODES
  /
    listener (learning_topic/person_publisher)
    talker (learning_topic/person_subscriber)

auto-starting new master
process[master]: started with pid [4426]
ROS_MASTER_URI=http://localhost:11311

setting /run_id to 8f043736-4a15-11ee-94ed-eb1c4a5ce82f
process[rosout-1]: started with pid [4436]
started core service [/rosout]
process[talker-2]: started with pid [4439]
process[listener-3]: started with pid [4440]
[ INFO] [1693716695.734350100]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1693716696.734435776]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1693716696.734844895]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1693716697.734414495]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1693716697.734667411]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1693716698.734408905]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1693716698.734658015]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1693716699.734416177]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1693716699.734658584]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1693716700.734417712]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1693716700.734663411]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1693716701.734412627]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1693716701.734663706]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1693716702.734420615]: Publish Person Info: name:Tom age:18 sex:1
[ INFO] [1693716702.734660215]: Subcribe Person Info: name:Tom age:18 sex:1
[ INFO] [1693716703.734366533]: Publish Person Info: name:Tom age:18 sex:1

这里我们可以看到两个节点都运行在同一个终端里面。这是之前发布者和订阅者的一个样例,先发布者发布一个人的信息,订阅者收到消息打印人的信息。

回到learning_launch的launch文件夹,创建第二个launch文件

代码语言:javascript复制
cd src/learning_launch/launch
vim turtlesim_parameter_config.launch

内容如下

代码语言:javascript复制
<launch>
    <param name="/turtle_number" value="2" />

    <node pkg="turtlesim" type="turtlesim_node" name="turtlesim_node">
        <param name="turtle_name1" value="Tom" />
        <param name="trutle_name2" value="Jerry" />

        <rosparam file="$(find learning_launch)/config/param.yaml" command="load" />
    </node>

    <node pkg="turtlesim" type="turtle_teleop_key" name="turtle_teleop_key" output="screen" />
</launch>

这里有一个参数/turtle_number表示海龟的数量为2,会存入到参数服务器中。第一个节点为运行小海龟仿真器节点,它其中有两个参数,表示两只海龟,一个叫Tom,一个叫Jerry。然后会导入一个参数文件param.yaml。find learning_launch表示搜索功能包的指令,会输出该功能包的一个完整路径。第二个节点就是启动键盘控制海龟的节点。

在learning_launch功能包中新建config文件夹,并创建param.yaml

代码语言:javascript复制
cd ..
mkdir config
cd config
vim param.yaml

内容如下

代码语言:javascript复制
A: 123
B: "hello"

group:
        C: 456
        D: "hello"

启动launch文件

代码语言:javascript复制
roslaunch learning_launch turtlesim_parameter_config.launch

此时会打开小海龟仿真器节点,并且可以通过键盘来控制小海龟移动。

现在我们来查看一下参数服务器的参数,新打开一个终端,运行

代码语言:javascript复制
rosparam list

返回

代码语言:javascript复制
/rosdistro
/roslaunch/uris/host_guanjian_x99__37297
/rosversion
/run_id
/turtle_number
/turtlesim_node/A
/turtlesim_node/B
/turtlesim_node/background_b
/turtlesim_node/background_g
/turtlesim_node/background_r
/turtlesim_node/group/C
/turtlesim_node/group/D
/turtlesim_node/trutle_name2
/turtlesim_node/turtle_name1

查看/turtle_number

代码语言:javascript复制
rosparam get /turtle_number

返回

代码语言:javascript复制
2

查看/turtlesim_node/turtle_name1

代码语言:javascript复制
rosparam get /turtlesim_node/turtle_name1

返回

代码语言:javascript复制
Tom

查看/turtlesim_node/A

代码语言:javascript复制
rosparam get /turtlesim_node/A

返回

代码语言:javascript复制
123

查看/turtlesim_node/B

代码语言:javascript复制
rosparam get /turtlesim_node/B

返回

代码语言:javascript复制
hello

查看/turtlesim_node/group/C

代码语言:javascript复制
rosparam get /turtlesim_node/group/C

返回

代码语言:javascript复制
456

回到包learning_launch的launch文件夹下,创建第三个launch文件

代码语言:javascript复制
vim start_tf_demo_c  .launch

内容如下

代码语言:javascript复制
<launch>
    <!-- Turlesim Node-->
    <node pkg="turtlesim" type="turtlesim_node" name="sim" />
    <node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen" />

    <node pkg="learning_tf" type="turtle_tf_broadcaster" args="/turtle1" name="turtle1_tf_broadcaster" />
    <node pkg="learning_tf" type="turtle_tf_broadcaster" args="/turtle2" name="turtle2_tf_broadcaster" />

    <node pkg="learning_tf" type="turtle_tf_listener" name="listener" />
</launch>

这是之前坐标系变换的例子,先启动海龟仿真器节点,然后是海龟仿真器键盘控制节点,然后是learing_tf下的两个广播器,最后就是learing_tf下的监听器,来监听两个坐标系之间的关系,并且发布海龟2的速度指令,让海龟2去向海龟1移动。

启动launch文件

代码语言:javascript复制
roslaunch learning_launch start_tf_demo_c  .launch

效果跟之前一样。

创建第四个launch文件

代码语言:javascript复制
vim start_tf_demo_py.launch

内容如下

代码语言:javascript复制
<launch>
    <!-- Turtlesim Node-->
    <node pkg="turtlesim" type="turtlesim_node" name="sim" />
    <node pkg="turtlesim" type="turtle_teleop_key" name="teleop" output="screen" />

    <node name="turtle1_tf_broadcaster" pkg="learning_tf" type="turtle_tf_broadcaster.py">
        <param name="turtle" type="string" value="turtle1" />
    </node>
    <node name="turtle2_tf_broadcaster" pkg="learning_tf" type="turtle_tf_broadcaster.py">
        <param name="turtle" type="string" value="turtle2" />
    </node>

    <node pkg="learning_tf" type="turtle_tf_listener.py" name="listener" />
</launch>

回到learning_tf的scripts文件夹,修改一下turtle_tf_broadcaster.py

代码语言:javascript复制
cd Documents/catkin_ws/src/learning_tf/scripts
vim turtle_tf_broadcaster.py

修改内容如下

代码语言:javascript复制
#!/home/guanjian/anaconda3/envs/py39/bin/python
# -*- coding: utf-8 -*-

import roslib
roslib.load_manifest('learning_tf')
import rospy
import tf
import turtlesim.msg

def handle_turtle_pose(msg, turtlename):
    br = tf.TransformBroadcaster()
    br.sendTransform((msg.x, msg.y, 0), tf.transformations.quaternion_from_euler(0, 0, msg.theta), rospy.Time.now(), turtlename, "world")

if __name__ == '__main__':

    rospy.init_node('turtle_tf_broadcaster')
    turtlename = rospy.get_param('~turtle')
    rospy.Subscriber('/%s/pose' % turtlename, turtlesim.msg.Pose, handle_turtle_pose, turtlename)
    rospy.spin()

启动launch文件

代码语言:javascript复制
roslaunch learning_launch start_tf_demo_py.launch

效果跟之前一样。

常用可视化工具

qt工具

启动小海龟仿真器以及键盘控制节点

代码语言:javascript复制
roscore
rosrun turtlesim turtlesim_node
rosrun turtlesim turtle_teleop_key
  • 日志查看工具
代码语言:javascript复制
rqt_console

当我们操作小海龟撞墙的时候,rqt_console工具就会收集日志

代码语言:javascript复制
[ WARN] [1693830491.863016173]: Oh no! I hit the wall! (Clamping from [x=5.655495, y=-0.030404])
  • 绘图工具
代码语言:javascript复制
rqt_plot

在该工具的顶端有一个话题(Topic)的选择,我们输入/后可以看到当前ROS中所有的话题,此时我们选择/turtle1/pose,当我们使用键盘操作小海龟运动后,可以看到小海龟的各种数值在该工具中显示,如线速度、角速度等。

  • 显示摄像头图像
代码语言:javascript复制
rqt_image_view

该工具可以显示摄像头的图像,以及深度图,红外图等输出图像。

  • 综合工具
代码语言:javascript复制
rqt

该工具可以打开rqt的所有工具,不限于上面所说的工具。

机器人开发数据可视化界面Rviz

该工具可以显示机器人开发过程中的机器人模型、坐标、运动规划、导航、点云、图像。

Rviz是一款三维可视化工具,可以很好的兼容基于ROS软件框架的机器人平台。

  1. 在rviz中,可以使用XML对机器人、周围物体等任何实物进行尺寸、质量、位置、材质、关节等属性的描述,并且在界面中呈现出来。
  2. 同时,rviz还可以通过图形化的方式,实时显示机器人传感器的信息、机器人的运动状态、周围环境的变化等信息。
  3. 总而言之,rviz通过机器人模型参数、机器人发布的传感信息等数据,为用户进行所有可监测信息的图形化显示。用户和开发者也可以在rviz的控制界面下,通过按钮、滑动条、数值等方式,控制机器人的行为。
代码语言:javascript复制
rosrun rviz rviz

这是刚刚打开时的画面,点Add,选择激光雷达

就可以去配置激光雷达的话题名

打开各种功能后,可以是下面的画面。

三维物理仿真平台Gazebo

Gazebo是一款功能强大的三维物理仿真平台,Rviz是一个数据显示平台,必须有数据才可以显示,而Gazebo是一个仿真平台,它可以在没有数据的情况下来创建数据。

  1. 具备强大的物理引擎。
  2. 高质量的图形渲染。
  3. 方便的编程与图形接口。
  4. 开源免费。

其典型应用场景包含

  1. 测试机器人算法
  2. 机器人的设计
  3. 现实场景下的回溯测试

安装

代码语言:javascript复制
sudo apt-get install ros-noetic-simulators

如果是Ubuntu 18.04则为

代码语言:javascript复制
sudo apt-get install ros-melodic-simulators

启动其中的一个模型

代码语言:javascript复制
roslaunch gazebo_ros willowgarage_world.launch

如果是第一次启动的话时间会比较长。

加载各种功能后可以是这个样子

0 人点赞