ROS机器人操作系统学习日记(1)
今天我们具体学习了ROS中的工作空间的概念及创建、功能包的概念及创建、ROS话题发布者和话题订阅者的实现。同时在掌握主要的知识中,我们还补充了许多细节点的内容,如句柄的理解、环境变量、spinOnce函数以及智能指针。
工作空间:
ros工作空间是用于存放ros功能包的一个文件夹,通常是以ws结尾。这个文件夹的位置没有要求,基本上可以放在任何地方。
1、创建工作空间文件夹
创建文件夹可以使用
mkdir test_ws
2、创建src文件夹
使用cd test_ws进入工作空间,在工作空间下使用mkdir src创建src文件夹,这个文件夹用来存放我们功能包的源代码。
cd test_ws
mkdir src
3、初始化工作空间
使用cd ~/test_ws/src进入src文件夹,再使用catkin_init_workspace初始化工作空间,此时会在src目录下创建一个CMakeLists.txt文件
cd ~/test_ws/src
catkin_init_workspace
4、编译工作空间
当初次创建项目或者编写完一个节点后我们需要去编译生成可执行文件,这时候就需要首先进入工作空间(!注意:工作空间是以ws结尾先前创建的文件夹,不是在src目录下的),然后使用catkin_make来编译工作空间。
cd ~/test_ws
catkin_make
编译完成后会在当前工作空间下多出来两个文件夹,分别是devel和build。
build
目录:存储编译过程中的中间文件和目标文件。由 catkin_make
自动管理,不需要手动修改。如果你需要清理构建结果,可以使用 catkin_make clean
devel
目录:存储开发过程中生成的可执行文件、库文件和 ROS 环境配置文件。用于设置和测试 ROS 环境。
5、添加工作空间到环境变量
每当你构建 ROS 工作空间时(使用 catkin_make
或 colcon build
),setup.bash
文件会被生成在工作空间的 devel
目录中。这个文件包含了设置环境变量所需的路径信息,例如 ROS 包的路径、库路径等。
把工作空间添加到环境变量中就可以在终端中方便地使用功能包和ros指令了,如果不添加环境变量需要每次打开终端后手动source一下。
echo "source ~/test_ws/devel/setup.bash" >> ~/.bashrc
添加后需要刷新环境变量或者重新开启终端才可生效。source ~/.bashrc
可以立即应用 .bashrc
文件中的更改,让新的环境变量设置立即生效。
source ~/.bashrc
为什么要将 source
添加到 .bashrc
中?
-
方便访问:将
source ~/test_ws/devel/setup.bash
添加到~/.bashrc
文件中,可以确保每次打开一个新的终端时,都会自动执行这个命令。这意味着你不需要每次打开终端后都手动执行source ~/test_ws/devel/setup.bash
。 -
自动加载:
~/.bashrc
是 Bash shell 的配置文件,它会在每次打开新的终端时被自动加载。通过在这个文件中添加source
命令,你可以确保所有终端会话都能自动配置好 ROS 环境变量。
如果不将工作空间添加到环境变量中可能会带来以下问题或不便:
-
每次手动配置:
- 每次打开一个新的终端窗口,你都需要手动运行
source ~/test_ws/devel/setup.bash
才能配置 ROS 环境。如果忘记运行这个命令,你可能会遇到找不到 ROS 命令或功能包的问题。
- 每次打开一个新的终端窗口,你都需要手动运行
-
命令不可用:
- 在新终端中,如果没有配置正确的环境变量,
rosrun
、roslaunch
等 ROS 命令将无法识别,可能会出现“命令未找到”或其他错误。
- 在新终端中,如果没有配置正确的环境变量,
-
功能包不可见:
- 如果环境变量没有正确设置,ROS 可能找不到你的功能包和相关文件。这会导致在运行 ROS 节点或程序时出现错误
# ROS 命令可能不可用
rosrun my_package my_node
另外:之后每次编译一个节点后,需要运行 source devel/setup.bash
,为了确保新的编译文件和环境变量设置被正确加载到当前的终端会话中。(当然如果直接打开一个新的终端由于我们之前设置过环境变量应该可以直接用(没有测试过))
为什么需要 source devel/setup.bash
-
使新的编译文件可用:
在编译过程中生成的可执行文件和库文件被放置在devel
目录中。运行source devel/setup.bash
会更新环境变量,使得 ROS 命令和工具能够找到这些新的文件。 -
更新环境变量:
setup.bash
文件会设置一些必要的环境变量,这些变量会指向新编译的功能包和库文件。如果不运行这个命令,环境变量不会更新,可能导致新编译的节点无法被识别或运行。
功能包:
功能包是用于存放每个节点程序的文件夹,功能包需要在工作空间的src文件夹下创建。
1、创建功能包:
进入工作空间下的src文件夹,使用catkin_create_pkg创建功能包
cd ~/test_ws/src
catkin_create_pkg learn_topic std_msgs rospy roscpp geometry_msgs turtlesim
catkin_create_pkg命令的参数具体解释如下:
catkin_create_pkg <功能包名称> <依赖库1> <依赖库2> <依赖库3>
当然如果在编写的过程中需要添加额外的依赖库,也可以补充到功能包目录下的CMakeLists.txt文件中。
2、功能包结构:
进入我们创建好的功能包文件夹,我们可以看到一下几个文件。
include:存放需要编译程序的头文件。
src:存放节点程序的源码文件。
CMakeLists.txt:编译所需要的文件,其中包括声明需要连接哪些库,需要哪些依赖以及生成哪些程序。
package.xml:声明了编译需要的一些依赖以功能包的一些信息,包括功能包版本、功能包创建的作者。
在ros中节点程序可以由cpp编写也可以由python编写,生成的src文件夹是用来存放cpp编写的节点程序,如果我们想要存放python编写的程序需要新建一个scripts文件夹用来存放。
mkdir scripts
ROS命令工具:
在ROS运行过程中,往往需要通过一些调试命令来查看当前程序中的运行状态,比如说运行的节点,使用的话题等等。
目前我学习到的常用的命令包括:
1、运行相关:
#它启动了 ROS 所需的所有基本服务,包括 ROS Master、参数服务器、以及日志服务。
是任何 ROS 程序启动前都需要运行的命令。
roscore
#用于启动 ROS 功能包中的可执行文件(节点)
rosrun <功能包名> <可执行文件>
2、节点相关rosnode
#可以查看当前运行中程序的所有节点
rosnode list
#可以查看节点的具体信息,包括节点发布/订阅的话题、提供的服务等
rosnode info <节点名>
3、话题相关rostopic
#查看当前运行中程序的所有话题
rostopic list
#可以查看话题的具体信息,包括话题类型、发布者/订阅者等
rostopic info <话题名>
4、节点话题可视化rqt_graph
#可视化查看当前程序中的节点和话题的交互关系
rqt_graph
ROS订阅/发布机制
ROS 的订阅/发布机制是实现 ROS 节点之间通信的核心机制,它允许节点通过发布消息到话题(topic)和订阅话题来交换数据。这个机制提供了一种松耦合的方式,让节点可以相互通信而不需要直接知道对方的细节。
节点可以通过话题发布消息,也可以通过话题订阅消息。
(!注意:我们所说的话题类型实际上就是消息的数据类型。如下geometry_msgs/Twist就是/turtle1/cmd_vel话题的类型,其本身就是该消息的数据类型。)
ROS话题发布者:
以下代码我们将以ROS中自带的小海龟turtle作为事例学习。
一般的创建发布者步骤如下:
- 初始化ROS节点
- 创建句柄
- 向ROS Master注册节点信息,包括发布的话题名和话题中的消息类型以及队列长度
- 创建并且初始化消息数据
- 按照一定频率循环发送消息
这样看可能不是很好理解,我们可以简化理解:
(1)初始化ROS节点
简单理解:启动和注册你的工作任务(节点)到系统中。就像在一个办公室中登记你的名字和职位。
解释:你需要告诉ROS系统你正在启动一个新的节点,节点的名称就像你在系统中的标识。
(2)创建句柄
简单理解:拿到办公室钥匙,开始使用办公室中的资源。
解释:NodeHandle
是你与ROS系统互动的工具,你用它来访问和管理你需要的资源(例如话题、服务等)。
怎样理解句柄:
句柄(NodeHandle
)是与ROS系统进行交互的主要接口。它提供了一种方式来访问和操作ROS网络中的不同功能,比如创建发布者、订阅者、服务客户端和服务端。创建句柄的主要目的是为了与ROS的底层通信机制进行交互并管理节点的资源。(ps:句柄就是获取功能的工具,通过句柄我们可以创建发布者、订阅者)。
句柄的作用
-
管理节点的资源:
NodeHandle
管理和维护节点的资源,例如发布者和订阅者的创建和销毁。通过句柄,你可以方便地管理节点的生命周期和它所拥有的资源。 -
访问ROS系统功能:
NodeHandle
提供了接口来访问ROS系统的功能,包括创建发布者 (advertise
)、创建订阅者 (subscribe
)、创建服务 (advertiseService
)、创建服务客户端 (serviceClient
) -
处理参数:
NodeHandle
还提供了访问参数服务器的功能,使得你可以获取或设置参数。 -
节点通信:通过
NodeHandle
,节点可以进行通信,包括发布消息到话题、订阅话题、服务调用等。
(3) 向ROS Master注册节点信息
简单理解:告诉系统你要处理的任务的详细信息,比如你要处理的工作内容和需要的资源。
解释:你向ROS Master报告你要发布的消息类型、话题名称以及消息队列的长度。这样ROS系统知道你的节点会发布什么样的数据,并为其分配资源。这里其实就是通过句柄创建节点中需要使用到的资源,如发布者、订阅者等
(4)创建并且初始化消息数据
简单理解:准备你要发送的数据,就像在写一份要发送的报告或文件。
解释:你创建一个消息对象,并填充你要发布的数据。确保消息格式与系统预期的一致。
(5) 按照一定频率循环发送消息
简单理解:定期发送报告或更新,让系统保持最新状态。
解释:你在循环中定期发布消息,使用一个时间间隔(频率)来确保数据定期更新。就像定时发送状态更新一样。
总结
- 初始化ROS节点:注册你的工作任务。
- 创建句柄:获取操作系统中管理任务的工具。
- 向ROS Master注册节点信息:告诉系统你要做什么。
- 创建并且初始化消息数据:准备和设置要发送的数据。
- 按照一定频率循环发送消息:定期更新数据,保持信息流通。
创建过程
CPP:
1、节点程序编写:
在我们先前创建的learn_topic的src文件夹,新建一个cpp文件,命名为turtle_velocity_publisher.cpp,
#include <ros/ros.h>
#include <geometry_msgs/Twist.h>
int main(int argc, char **argv)
{
ros::init(argc, argv, "turtle_velocity_publisher");//ROS节点初始化
ros::NodeHandle n;//这里是创建句柄
//创建一个Publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10
ros::Publisher turtle_vel_pub = n.advertise<geometry_msgs::Twist>
("/turtle1/cmd_vel", 10);
ros::Rate loop_rate(10);//设置循环的频率
while (ros::ok())
{
//初始化需要发布的消息,类型需与Publisher一致
geometry_msgs::Twist turtle_vel_msg;
turtle_vel_msg.linear.x = 0.8;
turtle_vel_msg.angular.z = 0.6;
turtle_vel_pub.publish(turtle_vel_msg);// 发布速度消息
//打印发布的速度内容
ROS_INFO("Publsh turtle velocity command[%0.2f m/s, %0.2f rad/s]",
turtle_vel_msg.linear.x, turtle_vel_msg.angular.z);
loop_rate.sleep();//按照循环频率延时
}
return 0;
}
我们注意到在cpp中我们通过ros::init来初始化ROS节点,其中"turtle_velocity_publisher"为我们创建节点的名称。随后我们使用ros::NodeHandle来创建句柄以获取ROS提供的资源。值得注意的是在后面我们使用python编写节点代码时是不用显式创建句柄的。在cpp中我们使用了advertise创建一个发布者,我们需要提供话题名称、类型、等待队列长度三个基础要素,其中函数签名如下
advertise<话题类型(消息数据结构)>(“话题名”,队列长度)
在程序中,我们需要以一定的频率去发布我们的消息,在cpp中我们创建了一个ros::Rate对象实例loop_rate并为其指定了频率为10,在循环最后我们调用loop_rate.sleep(),这样程序就会按照指定的频率休眠一会。在循环体中我们使用Publisher对象的publish方法发布了速度消息。
这样就是一个完整的发布者节点程序。
2、修改CMakelist.txt文件
在功能包文件夹中找到CMakelists.txt,在build区域下,添加如下内容:
add_executable(turtle_velocity_publisher src/turtle_velocity_publisher.cpp)
target_link_libraries(turtle_velocity_publisher ${catkin_LIBRARIES})
说明:
“add_executable”
功能:指定生成的可执行文件及其源代码文件。
语法:
add_executable(<target_name> <source_files>)
示例:
add_executable(turtle_velocity_publisher src/turtle_velocity_publisher.cpp)
turtle_velocity_publisher
:这是生成的可执行文件的名称。src/turtle_velocity_publisher.cpp
:这是编译时需要的源代码文件的位置。
解释:
- 这行代码告诉CMake创建一个名为
turtle_velocity_publisher
的可执行文件,并且编译源文件src/turtle_velocity_publisher.cpp
来生成这个可执行文件。
“target_link_libraries”
功能:指定在编译生成的可执行文件时需要链接的库。
语法:
target_link_libraries(<target_name> <libraries>)
示例:
target_link_libraries(turtle_velocity_publisher ${catkin_LIBRARIES})
turtle_velocity_publisher
:这是之前定义的可执行文件的名称。${catkin_LIBRARIES}
:这是链接的库的列表,通常由ROS的catkin
构建系统提供。
解释:
target_link_libraries
告诉CMake在编译turtle_velocity_publisher
可执行文件时,链接catkin
构建系统所提供的库。${catkin_LIBRARIES}
是一个变量,它包含了与catkin
工作空间相关的所有库,这些库是在catkin
的find_package
模块中定义的。- !注意
${catkin_LIBRARIES}
包含了所有catkin
包和其他依赖包所需要链接的库。通常情况下,你不需要修改或替换${catkin_LIBRARIES}
,因为它会根据你的CMakeLists.txt
文件中的find_package
和catkin_package
设置自动生成。
概念强化
(1)一个功能包下可以生成多个可执行文件,一个可执行文件就相当于一个节点,可以使用rosrun命令执行功能包下的不同节点(可执行文件)
rosrun <package_name> <executable_name>
<package_name>
是功能包的名称。
<executable_name>
是你在功能包的 CMakeLists.txt
文件中通过 add_executable
定义的可执行文件的名称。
tips:在CMakeLists.txt中你可以定义多个可执行文件
例子:
假设你有一个功能包 my_package
,并在 CMakeLists.txt
中定义了两个可执行文件 node1
和 node2
:
add_executable(node1 src/node1.cpp)
target_link_libraries(node1 ${catkin_LIBRARIES})
add_executable(node2 src/node2.cpp)
target_link_libraries(node2 ${catkin_LIBRARIES})
并在catkin_make编译后可以分别使用rosrun命令启动这两个节点
rosrun my_package node1
rosrun my_package node2
(2)在 add_executable
命令中指定的可执行文件名称(例如 turtle_velocity_publisher
)和源代码文件的名称(例如 turtle_velocity_publisher.cpp
)是可以不同的。
在先前的例子中:
add_executable(turtle_velocity_publisher src/turtle_velocity_publisher.cpp)
turtle_velocity_publisher
是生成的可执行文件的名称。src/turtle_velocity_publisher.cpp
是源代码文件的位置。
可执行文件名称与源代码文件名称的关系
- 生成的可执行文件名称(
turtle_velocity_publisher
):- 这是你在编译过程中指定的文件名,它决定了编译后生成的实际可执行文件的名称。这个名称通常是在运行
rosrun
时用到的。
- 这是你在编译过程中指定的文件名,它决定了编译后生成的实际可执行文件的名称。这个名称通常是在运行
- 源代码文件名称(
turtle_velocity_publisher.cpp
):- 这是包含代码的文件,它为你定义的可执行文件提供了实现。源文件的名称不需要与生成的可执行文件的名称相同。
例子:
你可以在 CMakeLists.txt
中定义如下:
add_executable(my_node src/turtle_velocity_publisher.cpp)
- 这里,
my_node
是生成的可执行文件的名称,而turtle_velocity_publisher.cpp
是包含代码的源文件。
如果你将生成的可执行文件命名为 my_node
,你将使用以下命令来运行它:
rosrun my_package my_node
my_package
是功能包的名称。my_node
是生成的可执行文件的名称。
总结
- 名称不同:
add_executable
中指定的可执行文件名称可以和源代码文件的名称不同。这种做法在实际开发中很常见,因为可执行文件的名称不一定需要反映源文件的名称。 - 用途:源代码文件名称用于管理和组织代码,而可执行文件名称用于在编译后标识和运行程序。
3、编译
首先进入工作空间,在执行catkin_make对刚编写的代码进行编译
cd ~/test_ws
catkin_make
编译通过后,需要重新source一下当前的环境变量,才能找到或者更新程序,终端输入
cd ~/test_ws
source devel/setup.bash
这里为什么要重新source当前的环境变量,我们在前面讲解工作空间的时候提到了:
之后每次编译一个节点后,需要运行 source devel/setup.bash
,为了确保新的编译文件和环境变量设置被正确加载到当前的终端会话中。(当然如果直接打开一个新的终端由于我们之前设置过环境变量应该可以直接用(没有测试过))
为什么需要 source devel/setup.bash
-
使新的编译文件可用:
在编译过程中生成的可执行文件和库文件被放置在devel
目录中。运行source devel/setup.bash
会更新环境变量,使得 ROS 命令和工具能够找到这些新的文件。 -
更新环境变量:
setup.bash
文件会设置一些必要的环境变量,这些变量会指向新编译的功能包和库文件。如果不运行这个命令,环境变量不会更新,可能导致新编译的节点无法被识别或运行。
4、运行程序
终端运行:
#运行roscore
roscore
#运行小海龟节点程序,
rosrun turtlesim turtlesim_node
#运行发布者节点程序,持续给小海龟发送速度,
rosrun learn_topic turtle_velocity_publisher
如上图所示,小海龟接收到发布的消息后,会按照规定的速度进行运动。
Python:
1、节点程序编写:
在learn_topic的功能包下的scripts文件夹中新建一个py文件,命名为turtle_velocity_publisher.py。
import rospy
from geometry_msgs.msg import Twist
def turtle_velocity_publisher():
rospy.init_node('turtle_velocity_publisher', anonymous=True) # ROS节点初始化
# 创建一个小海龟速度发布者,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,8代表消息队列长度
turtle_vel_pub = rospy.Publisher('/turtle1/cmd_vel', Twist, queue_size=8)
rate = rospy.Rate(10) #设置循环的频率
while not rospy.is_shutdown():
# 初始化geometry_msgs::Twist类型的消息
turtle_vel_msg = Twist()
turtle_vel_msg.linear.x = 0.8
turtle_vel_msg.angular.z = 0.6
# 发布消息
turtle_vel_pub.publish(turtle_vel_msg)
rospy.loginfo("linear is :%0.2f m/s, angular is :%0.2f rad/s",
turtle_vel_msg.linear.x, turtle_vel_msg.angular.z)
rate.sleep()# 按照循环频率延时
if __name__ == '__main__':
try:
turtle_velocity_publisher()
except rospy.ROSInterruptException:
pass
在使用python编写节点程序时,流程基本上和cpp是一致的,但是有几个不同点我们需要注意一下:
(1)首先是在初始化节点时,我们指定了”anonymous=True
”,它的作用是为你的 ROS 节点名称添加一个随机的后缀,使得节点名称在整个 ROS 网络中是唯一的。例如,如果你设置的节点名称是 turtle_velocity_publisher,实际的节点名称可能会是 turtle_velocity_publisher_83d7e1e8-f1f1-4c0c-91d4-ccb68b67df3b
这样的形式。
(2)我们之前提到在cpp的方式中需要声明一个NodeHandle句柄来向系统注册信息,而在python的方式中我们不需要显式的去声明NodeHandle,可以直接使用rospy.Publisher创建发布者。
2、修改py文件权限
python文件不同于cpp文件,它不需要编译可以直接执行,但是在Linux中我们需要为它添加可执行权限。
sudo chmod a+x turtle_velocity_publisher.py
其中a+x表示
a
代表 "all"(所有用户),包括文件的拥有者、同组用户和其他用户。+x
代表 "add execute permission"(添加执行权限)。这表示对指定的文件添加执行权限。
3、运行程序
终端运行,我们会看到和之前一样的效果:
#运行roscore
roscore
#运行小海龟节点程序,
rosrun turtlesim turtlesim_node
#运行发布者节点程序,持续给小海龟发送速度,
rosrun learn_topic turtle_velocity_publisher
ROS话题订阅者:
在ROS中创建一个订阅者和发布者的流程基本类似。
一般的创建订阅者步骤如下:
- 初始化ROS节点
- 创建句柄
- 订阅需要的话题
- 循环等待话题消息,接收到消息后进入回调函数
- 在回调函数中完成消息处理
(1)初始化ROS节点:
初始化ROS节点,注册节点的名字,并连接到ROS网络
(2)创建句柄:
NodeHandle
是ROS中与节点进行交互的接口,所有的发布、订阅、服务、参数操作等,都需要通过 NodeHandle
对象进行。它通过内部机制连接到ROS网络,处理节点与ROS系统之间的通信。
(3)订阅需要的话题:
创建一个订阅者,订阅者将监听指定的话题,并在有消息到达时调用指定的回调函数。
(4)循环等待话题消息,接收到消息后进入回调函数:
等待并处理所有到达的消息。当消息到达时,程序会调用相应的回调函数来处理该消息。
(5)在回调函数中完成消息处理:
回调函数是处理接收到的消息的地方。当节点从订阅的话题上接收到消息时,ROS会自动调用这个回调函数,并将消息内容传递给它。在回调函数中,你可以对消息进行任何所需的处理,如打印、存储、发布新的消息等。
总结:
1、初始化ROS节点:启动并注册节点,确保节点能与ROS系统通信。
2、创建句柄:创建与ROS系统的接口,提供访问ROS功能的方法。
3、订阅话题:通过订阅者监听特定的话题,以接收相关消息。
4、循环等待消息:保持节点运行,等待并处理到达的消息。
5、消息处理:在回调函数中定义如何处理接收到的消息数据。
创建过程
CPP
1、节点程序编写:
在learn_topic功能包下的src文件夹中创建一个cpp文件,命名为turtle_pose_subscriber.cpp
#include <ros/ros.h>
#include "turtlesim/Pose.h"
// 接收消息后,会进入消息回调函数,回调函数里边会对接收到的数据进行处理
void turtle_poseCallback(const turtlesim::Pose::ConstPtr& msg)
{
// 打印接收到的消息
ROS_INFO("Turtle pose: x:%0.3f, y:%0.3f", msg->x, msg->y);
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "turtle_pose_subscriber");// 初始化ROS节点
ros::NodeHandle n;//这里是创建句柄
// 创建一个订阅者,订阅的话题是/turtle1/pose的topic,poseCallback是回调函数
ros::Subscriber pose_sub = n.subscribe("/turtle1/pose", 10,turtle_poseCallback);
ros::spin(); // 循环等待回调函数
return 0;
}
实现一个订阅者最重要的实际上就是回调函数的部分,在回调函数中接收订阅话题的消息并进行处理。初始化的部分其实和发布者一样都是先初始化节点然后声明一个句柄,通过句柄我可以对话题进行订阅。最后我们有一个ros::spin函数,它是ROS节点生命周期的核心函数,确保节点能够处理并响应外部事件和消息,是实现ROS节点回调机制的基础。通过ros::spin()
,节点能够持续工作、处理消息,并且对外部事件作出响应。(tips:有关回调(如订阅/服务/计时器)的地方会用到spin函数)
ros::spin的主要功能
1. 保持节点运行
ros::spin()
会让程序进入一个无限循环。这意味着在ros::spin()
之后的代码(如果有的话)在正常情况下不会被执行,除非节点被终止。通过这个循环,节点能够不断处理订阅的消息和其他事件。
2. 处理消息队列
ros::spin()
会检查ROS的消息队列。如果有新的消息到达(例如订阅的消息、服务请求等),它会自动调用相应的回调函数进行处理。这个过程是同步的,也就是说,ros::spin()
会等待所有回调函数完成处理之后,才会继续检查队列中的下一个消息。
3. 保证节点响应性
由于ros::spin()
在循环中持续处理消息队列,它能够保证节点及时响应外部的消息和事件。无论是订阅的消息、服务请求,还是定时器事件,ros::spin()
都会确保这些事件被处理。
4. 阻塞执行
ros::spin()
是一个阻塞函数,这意味着一旦调用它,程序将停留在这个函数中,直到节点被关闭。通常,我们在主函数的最后调用ros::spin()
,因为它会阻止任何后续代码的执行。
拓展
(1)我们注意到在回调函数turtle_poseCallback中接收消息的参数是这样的
void turtle_poseCallback(const turtlesim::Pose::ConstPtr& msg)
#const turtlesim::Pose::ConstPtr& msg表示一个指向 std_msgs::String 类型消息
#的常量智能指针。通过这个指针,回调函数可以读取消息数据,但不能对数据进行修改。
std_msgs::String::ConstPtr
: 表示指向std_msgs::String
类型消息的常量智能指针。ConstPtr
: 这是std_msgs::String
消息的智能指针类型。它实际上是boost::shared_ptr<const std_msgs::String>
的别名。智能指针提供了自动内存管理的功能,避免了手动释放内存的需要。& msg
: 表示msg
是这个指针的引用,避免了参数复制。const
: 确保回调函数中不能修改消息数据,只能读取。
例子
如果是接收的是Twist类型的消息,我们可以这么写,表示指向geometry_msgs::Twist类型信息的常量智能指针。
// 回调函数,当接收到 Twist 类型的消息时会调用该函数
void twistCallback(const geometry_msgs::Twist::ConstPtr& msg)
总结
const <消息类型>::ConstPtr& msg
使用这种作为回调函数的参数接收订阅消息是一个很好的默认选择,因为它结合了效率和安全性。
(2)我们注意到在订阅话题时我们并没有指定话题类型,不像之前在创建发布者的时候在<>中指定了话题类型。
#发布话题
ros::Publisher turtle_vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);
#订阅话题
ros::Subscriber pose_sub = n.subscribe("/turtle1/pose", 10,turtle_poseCallback);
这是因为在 C++ 中使用 ROS 时,我们实际上是通过回调函数来间接指定消息类型的,虽然我们没有在创建的代码中显式指定消息类型,但编译器通过回调函数 turtle_poseCallback
的签名推断出了消息的类型。而在Python中我们需要显式地去指定,我们在下面会看到。
当然C++ 中的 ROS 还提供了另一种 subscribe
调用方式,可以显式地指定消息类型。
ros::Subscriber pose_sub = n.subscribe<turtlesim::Pose>("/turtle1/pose", 10, turtle_poseCallback);
(3)我们注意到ros::spin(),我们之前提到过它会让程序进入一个无限循环通过这个循环,节点能够不断处理订阅的消息和其他事件。但是如果我们想要在每一次回调函数执行完后去处理自己的一些逻辑那么该怎么做?我们需要用到
ros::spinOnce()
ros::spinOnce()
只处理一次消息队列中的回调,然后立即返回,不会进入循环。
ros::spinOnce()
的使用场景
ros::spinOnce()
通常用于需要在主循环中执行其他逻辑的场景中,而不仅仅依赖于回调函数。例如,当你需要在节点中不断地进行其他操作(如更新传感器数据、控制机器人动作等),并且还需要处理来自ROS网络的消息时,可以使用 ros::spinOnce()
。
ros::spinOnce()
典型用法
使用 ros::spinOnce()
时,你通常会在主循环中调用它,而不是使用 ros::spin()
让程序一直等待。示例如下:
int main(int argc, char **argv)
{
ros::init(argc, argv, "example_node");
ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe("example_topic", 1000, exampleCallback);
ros::Rate loop_rate(10); // 设定循环频率为10Hz
while (ros::ok()) // 主循环
{
// 执行一次回调处理
ros::spinOnce();
// 这里可以加入其他需要反复执行的逻辑
// 例如读取传感器数据、更新状态等
loop_rate.sleep(); // 根据设定的频率睡眠
}
return 0;
}
ros::spinOnce()
的工作机制
-
一次性处理消息:每次调用
ros::spinOnce()
时,它会检查消息队列,并处理所有当前可用的消息。处理完之后,函数立即返回。 -
控制循环频率:在使用
ros::spinOnce()
的情况下,你通常会配合使用ros::Rate
来控制循环频率,这样可以定期检查并处理消息,而不是持续阻塞在等待消息上。 -
主循环中的其他操作:由于
ros::spinOnce()
不会阻塞程序执行,你可以在主循环中继续执行其他任务,比如更新状态、控制动作等。这使得程序在处理ROS消息的同时,还能够执行其他逻辑。
简单来说,ros::spin()在逻辑上与以下代码片段相当:
while (ros::ok())
{
ros::spinOnce();
}
ros::spinOnce()
总结
ros::spinOnce()
适用于需要同时处理ROS消息和执行其他任务的场景。它允许你在一个主循环中进行多种操作,而不仅仅是等待消息的到来。这使得它非常适合实时性要求较高、需要定期执行某些任务的ROS节点。例如,机器人控制程序中,可能需要定期处理传感器数据、执行控制算法,并且还需要响应来自ROS网络的命令和消息,这种情况下 ros::spinOnce()
是一个理想的选择。
2、修改CMakeLists.txt文件
在功能包文件夹中找到CMakelists.txt,在build区域下,添加如下内容:
add_executable(turtle_pose_subscriber src/turtle_pose_subscriber.cpp)
target_link_libraries(turtle_pose_subscriber ${catkin_LIBRARIES})
3、编译
首先进入工作空间,在执行catkin_make对刚编写的代码进行编译
cd ~/test_ws
catkin_make
编译通过后,需要重新source一下当前的环境变量,才能找到或者更新程序,终端输入
cd ~/test_ws
source devel/setup.bash
4、运行程序
终端运行:
#运行roscore
roscore
#运行小海龟节点
rosrun turtlesim turtlesim_node
#运行小海龟键盘控制节点
rosrun turtlesim turtle_teleop_key
#运行订阅者节点,接收小海龟发送的位置数据
rosrun learn_topic turtle_pose_subscriber
运行结果如下图所示
这个时候我们可以打开rqt_graph工具来查看节点之间的关系
椭圆代表的是节点,矩形表示的是话题,箭头表示话题消息传递的方向。我们可以看到跟我们的现象是一致的。
Python:
1、节点程序编写
在learn_topic的功能包下的scripts文件夹中新建一个py文件,命名为turtle_pose_subscriber.py。
import rospy
from turtlesim.msg import Pose
def poseCallback(msg):
rospy.loginfo("Turtle pose: x:%0.3f, y:%0.3f", msg.x, msg.y)
def turtle_pose_subscriber():
rospy.init_node('turtle_pose_subscriber', anonymous=True)# ROS节点初始化
# 创建一个Subscriber,订阅名为/turtle1/pose的topic,注册回调函数poseCallback
rospy.Subscriber("/turtle1/pose", Pose, poseCallback)
rospy.spin()# 循环等待回调函数
if __name__ == '__main__':
turtle_pose_subscriber()
这个流程也是基本上和cpp的版本差不多的。唯一我们需要注意的是在订阅话题时,在Python中我们指定了话题的类型Pose。这是因为在Python中我们无法像在cpp那样通过签名推断出了消息的类型。
rospy.Subscriber("/turtle1/pose", Pose, poseCallback)
2、修改py文件权限
python文件不需要编译,但是要修改文件为可执行文件
sudo chmod a+x turtle_pose_subscriber.py
3、运行程序
#运行roscore
roscore
#运行小海龟节点
rosrun turtlesim turtlesim_node
#运行小海龟键盘控制节点
rosrun turtlesim turtle_teleop_key
#运行订阅者节点,接收小海龟发送的位置数据
rosrun learn_topic turtle_pose_subscriber.py
最终我们可以看到和上面一样的效果
总结
今天我们具体学习了ROS中的工作空间的概念及创建、功能包的概念及创建、ROS话题发布者和话题订阅者的实现。同时在掌握主要的知识中,我们还补充了许多细节点的内容,如句柄的理解、环境变量、spinOnce函数以及智能指针。

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)