目录
3.1 第一个ROS例程——小乌龟仿真
3.1.1 turtlesim功能包
该功能包的核心是turtlesim_node节点,提供一个可视化的乌龟仿真器,可以实现很多ROS基础功能的测试。
每一个ROS功能包都是一个独立的功能,其中可能包含一个或多个节点,这些功能对外使用话题、服务、参数等作为接口。
- 话题与服务:即功能包中的话题与服务接口
- 参数:功能包中的参数,开发者可以通过命令、程序等方式来获取这些参数并进行修改。
turtlesim功能包:订阅速度控制指令,实现乌龟在仿真器中的移动,同时发布乌龟的实时位姿信息,更新仿真器中的乌龟状态。也可以通过服务调用实现删除、新生乌龟等功能。
安装turtlesim功能包:
sudo apt-get install ros-melodic-turtlesim
3.1.2 控制乌龟的运动
- 打开第1个终端,运行ROS节点管理器——ROS Master:
roscore
显示如下所示的roscore运行界面:
图3-1 roscore运行界面 - 打开第2个终端,使用rosrun命令启动turtlesim仿真器节点,显示仿真界面:
rosrun turtlesim turtlesim_node
显示如下所示的可视化仿真器界面:
图3-2 小乌龟仿真器界面
- 打开第3个终端,运行键盘控制的节点:
rosrun turtlesim turtle_teleop_key
如下图所示:
图3-3 rosrun 键盘控制界面
- 在保证键盘激活的前提下,使用方向键,控制小乌龟移动。如下图所示:
图3-4 小乌龟在仿真器中的移动轨迹
3.2 创建工作空间和功能包
3.2.1 什么是工作空间
工作空间是一个存放工程开发相关文件的文件夹。
Melodic版本的ROS默认使用Catkin编译系统。
典型的工作空间中一般包含以下四个目录空间:
- scr:代码空间(Source Space),用于存储所有ROS功能包的源码文件。
- build:编译空间(Build Space),用于存储工作空间编译过程中产生的缓存信息和中间文件。
- devel:编译空间(Development Space),用了放置编译生成的可执行文件。
- install:安装空间(Install Space),非必需。在编译成功后,可以使用make install 命令将可执行文件安装到该空间中。运行该空间中的环境变量脚本,即可在终端中运行这些可执行文件。
3.2.2 创建工作空间
- 使用系统命令创建工作空间目录:
mkdir -p ~/catkin_ws/src
- 进入src文件目录:
cd ~/catkin_ws/src
- 执行工作空间初始化命令:
catkin_init_workspace
- 进入工作空间根目录:
cd ~/catkin_ws/
- 编译整个工作空间:
catkin_make
- 编译完成后,工作空间根目录自动生成build和devel文件夹。在devel中有几个setup.*sh形式的环境变量设置脚本。使用source命令运行这些脚步,使工作空间中的环境变量生效:
source devel/setup.bash
- 检查环境变量是否生效:
echo $ROS_PACKAGE_PATH
,如下消息,说明环境变量设置成功:
lxr@lxr-HP-ZBook-15-G3:~$ echo $ROS_PACKAGE_PATH
/opt/ros/melodic/share
备注:在终端使用source命令设置的环境变量只在当前终端生效。
如果希望在所有终端中都生效,则需要在终端的配置文件中加入环境变量的设置:(使用工作空间路径代替WORKSPACE):
echo "source /WORKSPACE/devel/setup.bash" >> ~/.bashrc
3.2.3 创建功能包
ROS中功能包的形式如下:
my_package/
CMakeLists.txt
package.xml
......
- CMakeLists.txt:记录了功能包的编译规则。
- package.xml:提供了功能包的元信息,也就是描述功能包属性的信息。
注意:ROS不允许在某个功能包中嵌套其他功能包,多个功能包必须平行放置在代码空间中。
ROS直接提供创建功能包的命令catkin_create_pkg
,命令使用如下:
catkin_create_pkg <package_name> [depend1] [depend2] [depend3]
参数解释:
- package_name:输入功能包的名称
- depend:所依赖的其他功能包名称
例子:使用catkin_create_pkg
命令创建learning_communication
功能包,该功能包依赖于std_msgs
、roscpp
、rospy
等功能包。
进入工作空间的src
文件夹,使用上述命令创建功能包:
cd ~/catkin_ws/src
catkin_create_pkg learning_communication std_msgs rospy roscpp
创建完成后,在src
文件中会生成一个learning_communication
功能包,其中已经包含package.xml和CMakeLists.txt文件。
然后回到工作空间根目录进行编译,并且设置环境变量:
cd ~/catkin_ws/
catkin_make
source ~/catkin_ws/devel/setup.bash
注意:
- 该环境变量设置只在当前终端有效,打开其他终端后,需要重新设置环境变量。
- 在同一个工作空间下,不允许存在同名功能包,否则在编译时会报错。
3.3 工作空间的覆盖
ROS允许多个工作空间并存,每个工作空间的创建、编译、运行方法相同,用户可以在不同项目的工作空间中创建所需要的功能包。
但是,不同的工作空间中可能存在相同命名的功能包,如果这些工作空间的环境变量都已经设置,那么在使用该功能包的时候,是否会发生冲突?如果不会,ROS又会帮我们选择哪一个功能包呢?
3.3.1 ROS中工作空间的覆盖
Overlaying,即工作空间的覆盖:
- 所有工作空间的路径会依次在ROS_PACKAGE_PATH环境变量中记录,当设置多个工作空间的环境变量后,新设置的路径在ROS_PACKAGE_PATH中会自动放置在最前端。
- 在运行时,ROS会优先查找最前端的工作空间中是否存在指定的功能包。如果不存在,就顺序向后查找其他工作空间,直到最后一个工作空间为止。
查看ROS相关的环境变量:
env | grep ros
图3-5 查看所有ROS相关的环境变量
3.3.2 工作空间覆盖示例
安装如下功能包:
sudo apt-get install ros-melodic-ros-tutorials
安装完成后,使用rospack命令查看功能包所放置的工作空间:
图3-6 查找功能包的存放路径
解释:roscpp_tutorials是ros-tutorials中的一个功能包,此时该功能包存放于ROS的默认工作空间下。为了人为制造同名功能包的情况,在catkin_ws/src中放置一个同名的功能包,可以GitHub上下载ros-tutorials功能包的源码:
cd ~/catkin_ws/src/
git clone git://github.com/ros/ros_tutorials.git
然后编译catkin_ws
工作空间并设置环境变量:
cd ~/catkin_ws/
catkin_make
source ./devel/setup.bash
环境变量设置成功后,再来查看roscpp_tutorials功能包的位置:
图3-7 再次查找功能包的存放路径
目前,由于工作空间的覆盖机制,roscpp_tutorials
功能包目前在我们创建的catkin_ws
工作空间下。这是因为在ROS_PACKAGE_PATH
环境变量中,catkin_ws
工作空间的路径在系统工作空间路径之前。
覆盖机制:
- 优点:可以让我们在开发过程中轻松替换系统或其他工作空间中原有的功能包。
- 缺点:也存在一些潜在风险,例如:
-
catkin_ws/ src/ package_a/ package_b/ # 依赖package_a overlay_ws/ src/ package_a
-
3.4 搭建Eclipse开发环境(略)
使用下节中的RoboWare进行开发
3.5 RoboWare简介
3.5.1 RoboWare的特点
RoboWare是一款直观、简单,并且易于操作的ROS集成开发环境,可进行ROS工作空间及包的管理、代码编辑、构建及调试。
- 易于安装及配置
- 辅助ROS开发,兼容indigo/jadc/kinetic/melodic版本
- 友好的编码体验
- C++和Python代码调试
- 远程部署及调试
- 内置Git功能
- 遵循ROS规范
3.5.2 RoboWare的安装和使用
官网下载已经作废。
GitHub地址:https://github.com/tonyrobotics/roboware-studio
百度云地址(推荐):https://pan.baidu.com/s/1D169dFyf2OD3SFBZVZy3pQ 密码:3iuk
在下载完成后,先不着急安装:
- 为了更好地使用RoboWare Studio,我们需要事先安装两个比较常用的插件:
sudo apt-get install python-pip
sudo python -m pip install pylint
- 为了获得更好的代码阅读体验,自动格式化整理代码,需要安装clang-format:
sudo apt-get install clang-format
- 接下来开始安装RoboWare:
进入下载文件夹,我这里是/home/lxr/Desktop/work
,然后进行安装
cd /home/lxr/Desktop/work
sudo dpkg -i roboware-studio_1.1.0-1514335284_amd64.deb
出现用户协议界面,按ESC键跳过,然后出现“您是否接受上述协议?”,选<是>,回车,自动开始安装。
- 安装完成,直接在终端输入:
roboware-studio
打开软件,如下图所示:
图3-8 roboware studio软件界面
- 有些同学打开软件后可能会提示git不是最新版本,依次执行下面的命令即可更新至最新版:
git --version
sudo add-apt-repository ppa:git-core/ppa
sudo apt-get update
sudo apt-get install git
git --version
OK!现在我们可以打开一个ROS工作区,然后再打开一个源码文件。当我们想对代码进行自动排版时,可以使用Ctrl+Shift+P
,输入“forma
t”回车,或者使用快捷键Ctrl+Shift+I
,或者直接在代码区右键点击“Format Document
”。
3.5.3 RoboWare官方使用手册
链接如下,请自行下载:
https://github.com/TonyRobotics/RoboWare/blob/master/Studio/RoboWare%E4%BD%BF%E7%94%A8%E6%89%8B%E5%86%8Cv0.7.2.pdf
3.6 话题中的Publisher与Subscriber
3.6.1 乌龟例程终端Publisher与Subscriber
按照3.1节所示运行乌龟例程,然后打开第4个终端,输入:
rqt_graph
该命令可查看系统中的节点关系图:
图3-9 乌龟例程中的节点关系图
解释:当前系统中存在两个节点:teleop_turtle(创建一个Publisher,用于发表键盘控制的速度指令),turtlesim(创建了一个Subscriber,用于订阅速度指令)。这里的话题是:/turtle1/cmd_vel。
Publisher和Subscriber是ROS系统中最基本、最常用的通信方式。
3.6.2 如何创建Publisher
Publisher主要作用:针对特定话题发布特定数据类型的消息。
为功能包learning_communication使用代码创建一个节点,节点中创建一个Publisher并发布字符串“Hello World”:
learning_communication/src/talker.cpp
/**
* 该例程将发布chatter话题,消息类型String
*/
//********************** 1. 头文件部分
//为避免繁杂的ROS功能包头文件,ros/ros.h已经帮我们包含了大部分ROS中通用的头文件
#include "ros/ros.h"
//节点会发布String类型的消息。该头文件根据String.msg的消息结构定义自动生成
//我们也可以自定义消息结构,并生成所需要的头文件
#include "std_msgs/String.h"
#include <sstream>
int main(int argc, char **argv) {
//****************** 2.初始化部分
// ROS节点初始化
//包含3个参数:前2个是命令行或launch文件输入的参数,用来完成命名重映射等功能
//第3个参数定义了Publisher节点的名称,必须独一无二,不允许同时存在相同名称的两个节点
ros::init(argc, argv, "talker");
// 创建节点句柄:方便对节点资源的使用和管理
ros::NodeHandle n;
// 在ROS Master端注册一个Publisher,发布名为chatter的topic,消息类型为std_msgs::String
//第2个参数表示消息发布队列的大小:当发布消息的实际速度较慢时,Publisher会将消息存储在一定空间的队列中;当消息数量超过队列大小时,ROS会自动删除队列中最早入队的消息。
ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
// 设置循环的频率,单位:Hz。当调用Rate::sleep()时,ROS节点会根据此处设置的频率休眠相应的时间,以保证循环维持一致的时间周期。
ros::Rate loop_rate(10);
//********************** 3.循环部分
//进入节点的主循环,在节点未发生异常的情况下将一直在循环中运行;一旦发生异常,ros::ok()就会返回false,跳出循环。
//异常包括:
//1.收到SIGINT信号(ctrl + C)
//2.被另外一个相同名称的节点踢掉线
//3.节点调用了关闭函数ros::shutdown()
//4.所以ros::NodeHandles句柄被销毁
int count = 0;
while (ros::ok()) {
// 初始化即将发布的消息:std_msgs::String类型的消息,该消息类型只有一个成员,即data,用来存储字符串数据。
std_msgs::String msg;
std::stringstream ss;
ss << "hello world " << count;
msg.data = ss.str();
//打印日志信息:将发布的消息在本地打印,以确保发出的数据符合要求。
ROS_INFO("%s", msg.data.c_str());
//发布封装完毕的消息msg:消息发布后,Master会查找订阅该话题的节点,并且帮助两个节点建立连接,完成消息的传输。
chatter_pub.publish(msg);
// 循环等待回调函数:处理节点订阅话题的所有回调函数。
ros::spinOnce();
// 按照循环频率延时:现在Publisher一个周期的工作已经完成,可以让节点休息一段时间,调用休眠函数,节点进入休眠状态。
//当然,节点不可能一直休眠下去,在之前设置了10Hz的休眠时间,节点休眠100ms后又会开始下一个周期的延期工作。
loop_rate.sleep();
++count;
}
return 0;
}
注意:虽然目前的发布节点并没有订阅任何消息,spinOnce函数不是必需的。但为了保证功能无误,建议所有节点都默认加入该函数。
一个Publisher的流程总结:
- 初始化ROS节点
- 向ROS Master注册节点信息,包括发布的话题名和话题中的消息类型。
- 按照一定频率循环发布消息。
3.6.3 如何创建Subscriber
创建一个Subscriber,以订阅Publisher节点发布的“Hello World”字符串,在learning_communication/src/listener.cpp下:
/**
* 该例程将订阅chatter话题,消息类型String
*/
#include "ros/ros.h"
#include "std_msgs/String.h"
//***************** 1.回调函数部分
// 接收到订阅的消息后,会进入消息回调函数
// 回调函数:是订阅节点接受消息的基础机制。当有消息到达时,会自动以消息指针作为参数,再调回回调参数,完成对消息的处理
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
// 将接收到的Publisher发布的消息打印出来
ROS_INFO("I heard: [%s]", msg->data.c_str());
}
//******************* 2.主函数部分
int main(int argc, char **argv)
{
// 初始化ROS节点
ros::init(argc, argv, "listener");
// 创建节点句柄
ros::NodeHandle n;
//订阅节点首先需要声明自己订阅的消息话题,该消息会在ROS Master中注册。Master会关注系统中是否存在发布该话题的节点,如果存在则会帮助两个节点建立连接,完成数据传输。
// 创建一个Subscriber,订阅名为chatter的topic,注册回调函数chatterCallback
//参数1:消息话题
//参数2:接收消息队列的大小,当消息入队数量超过设置的队列大小时,会自动舍弃时间戳最早的消息
//参数3:接收到话题消息后的回调函数
ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
// 循环等待回调函数:节点进入循环状态,当有消息到达时,会尽快调用回调函数完成处理。
//ros::spin()在ros::ok()返回false时退出。
ros::spin();
return 0;
}
Subscriber的主要流程:
- 初始化ROS节点
- 订阅需要的话题
- 循环等待话题消息,接收到消息后进入回调函数
- 在回调函数中完成消息处理
3.6.4 编译功能包
节点的代码是C++文件,在运行前需要讲代码编译成可执行文件。
ROS中的编译器使用的是CMake,编译规则通过功能包中的CMakeLists.txt文件设置的,使用catkin命令创建的功能包中会自动生成该文件,且已经配置多数选项。
我们现在只需要配置learning_communication/CMakeLists.txt文件,修改其中Publisher和Subscriber的内容:
#用于设置头文件的相对路径
#全局路径默认是功能包的所在目录:功能包的头文件一般会放到功能包根目录下的include文件夹中,所以此处需要添加该文件夹。
#该配置项还包含ROS catkin编译器默认包含的其他头文件路径:ROS默认安装路径、Linux系统路径等。
include_directories(
include
${catkin_INCLUDE_DIRS}
)
#用于设置需要编译的代码和生成的可执行文件。
#参数1:期望生成的可执行文件的名称
#参数2:参与编译的源码文件(cpp)。如果需要多个文件,在后面列出,以空格分割
add_executable(talker src/talker.cpp)
#用于设置链接库:很多功能需要使用系统或第三方的库函数,通过该选项可以配置执行文件链接的库文件。
#参数1:可执行文件的名称
#参数2:后面依次列出需要链接的库
target_link_libraries(talker ${catkin_LIBRARIES})
#用于设置依赖
#在很多应用中,定义语音无关的消息类型,消息类型会在编译过程中产生相应语言的代码。如果编译的可执行文件依赖这些动态生成的代码,则需要添加该功能包动态产生的消息代码进行配置
add_dependencies(talker ${PROJECT_NAME}_generate_messages_cpp)
#以下同上,设置listener的相关配置
add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
add_dependencies(talker ${PROJECT_NAME}_generate_messages_cpp)
以上编译内容会帮助系统生成两个可执行文件:talker和listener,放置在工作空间的~/catkin_ws/devel/lib/<package name>
路径下。
修改完成后,在工作空间的根目录下开始编译:
cd ~/catkin_ws/
catkin_make
3.6.5 运行Publisher与Subscriber
编译完成后,可以运行Publisher和Subscriber节点了。但在运行节点之前,需要在终端设置环境变量,否则无法找到功能包最终编译生成的可执行文件:
(当前终端有效)
cd ~/catkin_ws/
source ./devel/setup.bash
或者将环境变量的配置脚本添加到终端的配置文件中(全部终端有效):
echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc
source ~/.bashrc
- 启动roscore(终端1)
在运行节点之前,首先确保ROS Master已经成功启动:
roscore
- 启动Publisher(终端2)
Publisher 和 Subscriber节点的启动顺序在ROS中没有要求,这是先使用rosrun
命令启动Publisher(在工作空间根目录下运行):
cd ~/catkin_ws/
rosrun learning_communication talker
如果消息发布成功,显示如下:
图3-10 Publisher节点启动成功后的日志信息
- 启动Subscriber(终端3)
Publisher节点已经成功运行,接下来运行Subscriber节点,订阅Publisher节点发布的信息:
cd ~/catkin_ws/
rosrun learning_communication listener
如果消息订阅成功,显示如下:
图3-11 Subscriber节点启动成功后的日志消息
这个“Hello World”例程中的Publisher和Subscriber已经运行成功。也可以改变两者的运行顺序,先启动Subscriber节点,该节点会处于循环等待状态,直到Publisher启动后,终端才会显示订阅收到的消息内容。
3.6.6 自定义话题消息
在以上例程中,chatter话题的消息类型是ROS中预定义的String。在ROS的元功能包common_msgs中还提供了许多不同类型的功能包,如:std_msgs(标准数据类型)、geometry_msgs(几何学数据类型)、senor_msg(传感器数据类型)等。
但是在很多情况下,我们依旧需要针对自己的机器人应用设计特定的消息类型,ROS也提供了一套语言无关的消息类型定义方法。
msg文件:就是ROS中定义消息类型的文件,一般放置在功能包根目录下的msg文件夹中。在功能包编译的过程中,可以使用msg文件生成不同编程语言使用的代码文件。
注意:基础数据类型string、uint8都是语言无关的,编译阶段会变成各种语言对应的数据类型。
在msg文件中还可以定义常量。这些常量在发布或订阅消息数据时都可以直接使用,相当于C++中的宏定义。
例如:
string name
uint8 sex
uint8 age
uint8 unknown = 0
uint8 male = 1
uint8 female = 2
很多ROS消息定义中还会包含一个标准格式的头信息std_msgs/Header:
#Standard metadata for higher-level flow data types
uint32 seq
time stamp
string frame_id
- seq:消息的顺序标识,不需要手动设置,Publisher在发布消息时会自动累加
- stamp:消息中与数据相关联的时间戳,可以用于时间同步
- frame_id:消息中与数据相关联的参考坐标系id
- 此处定义的消息类型较为简单,可以不加头信息
为了使用自定义的消息类型,需要编译msg文件,编译注意以下两点:
- 在package.xml中添加功能包依赖
首先打开功能包的package.xml文件确保该文件中设置了以下编译和运行的相关依赖:
<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>
- 在CMakeLists.txt中添加编译选项
打开功能包的CMakeLists.txt文件,在find_package中添加消息生成依赖的功能包message_generation
,这样才能在编译时找到所需要的文件:
find_package(catkin REQUIRED COMPONENTS
geometry_msgs
roscpp
rospy
std_msgs
message_generation
)
cakin依赖也需要进行设置:
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES learning_communication
CATKIN_DEPENDS geometry_msgs roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
最后设置需要编译的msg文件:
add_message_files(
FILES
Person.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
在以上配置工作完成后,回到工作空间的根目录,使用catkin_make命令进行编译。编译成功后,使用如下命令查看自定义的Person消息类型:
rosmsg show Person
Person消息类型已经定义成功,在代码中可以向使用String一样使用Person的消息了,显示如下:
图3-12 查看自定义的Person消息类型
3.7 服务中的Server和Client
服务:是节点之间同步通信的一种方式,允许客户端(Client)节点发布请求(Request),有服务端(Server)节点处理后反馈应答。
3.7.1 乌龟例程中的服务
乌龟例程中的设置功能都以服务的形式提供。在乌龟例程运行状态下,使用如下命令查看系统中的服务列表:(打开4个终端)
rosservice list
图3-13 查看乌龟例程中的服务列表
可以使用代码或终端对列表中的服务进行调用。
例如:调用“/spawn”服务新生一只乌龟:
rosservice call /spawn 8.0 8.0 0.0 "turtle2"
参数:x,y,姿态,名称
调用成功后,仿真器新生一只乌龟:
图3-14 服务调用成功后,产生一只新的乌龟
终端会打印服务反馈的应答数据,即新生乌龟的名称:
图3-15 服务调用成功后的应答数据
服务一般分为两个部分:
- 服务端(Server):负责处理相应的功能,并且返回应答数据
- 客户端(Client):负责发布请求数据,等待Server处理
接下来,以一个简单的加法运算为例,具体研究ROS中的服务应用。在该例程中,Client发布两个需要相加的int类型变量,Server节点接收请求后完成运算并返回加法运算结果。
3.7.2 如何自定义服务数据
与话题消息类似,ROS中的服务数据可以通过srv
文件进行语言无关的接口定义,一般放置在功能包根目录下的srv
文件夹中。
srv文件夹包括请求和应答两个数据域,使用“___”进行分割。数据域中的内容和话题消息的数据类型相同。
针对加法例程中的服务需求,创建一个定义服务数据类型的srv文件~/catkin_ws/src/learning_communication/srv/AddTwoInts.srv
:
int64 a
int64 b
---
int64 sum
- 服务请求数据域:定义a、b,存储两个加数
- 服务应答数据域:定义求和结果sum,存储求和结果
注意:在完成服务数据类型的描述后,与话题消息一样,还需要在功能包的package.xml
和CMakeLists.txt
文件中配置依赖和编译规则,在编译过程中讲该描述文件装换成编程语言所能识别的代码。
- 打开package.xml,添加以下依赖(在3.6.6节定义话题消息时已经添加):
<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>
- 打开CMakeLists.txt文件,添加如下配置(message_generation在3.6.6节也已添加,新增FILES):
find_package(catkin REQUIRED COMPONENTS
geometry_msgs
roscpp
rospy
std_msgs
message_generation
)
add_service_files(
FILES
AddTwoInts.srv
)
message_generation包:可以根据话题消息、服务的类型描述文件产生相关的代码。
在功能包编译成功后,在服务的Server节点和Client节点的代码实现中就可以直接调用定义好的服务消息了。
接下来,编写Server和Client节点的代码,完成两数相加求和的服务过程。
3.7.3 如何创建Server
创建Server节点,提供加法运算的功能,返回求和之后的结果。~/catkin_ws/src/learning_communication/src/server.cpp
:
/**
* AddTwoInts Server
*/
//************* 1.头文件部分
//使用ROS中的服务,必须包含服务数据类型的头文件,这里使用的头文件是learning_communication/AddTwoInts.h,该头文件根据我们之前创建的服务数据类型的描述文件AddTwoInts.srv自动生成
#include "ros/ros.h"
#include "learning_communication/AddTwoInts.h"
//******************* 2.回调函数部分:是真正实现服务的部分,也是设计的重点
// service回调函数,输入参数req,输出参数res
//传入参数是我们在服务数据类型描述文件中声明的请求和应答的数据结构
bool add(learning_communication::AddTwoInts::Request &req,
learning_communication::AddTwoInts::Response &res)
{
// 将输入参数中的请求req数据相加,结果放到应答res变量中
res.sum = req.a + req.b;
ROS_INFO("request: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
ROS_INFO("sending back response: [%ld]", (long int)res.sum);
//反馈到Client,回调函数返回true
return true;
}
//**********************3.主函数部分
//
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "add_two_ints_server");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个名为add_two_ints的server,注册接收到server数据的回调函数add()
ros::ServiceServer service = n.advertiseService("add_two_ints", add);
// 循环等待回调函数(服务请求);一旦有服务请求,Server就跳入回调函数进行处理
ROS_INFO("Ready to add two ints.");
ros::spin();
return 0;
}
服务中的Server类似于话题中的Subscriber,实现流程如下:
- 初始化ROS节点
- 创建Server实例
- 循环等待服务请求,进入回调函数
- 在回调函数中完成服务功能的处理并反馈应答数据
3.7.4 如何创建Client
创建Client节点,通过终端输入的两个加数发布服务请求,等待应答结果。代码文件learning_communication/src/client.cpp
:
/**
* AddTwoInts Client
*/
#include <cstdlib>
#include "ros/ros.h"
#include "learning_communication/AddTwoInts.h"
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "add_two_ints_client");
// 从终端命令行获取两个加数
if (argc != 3)
{
ROS_INFO("usage: add_two_ints_client X Y");
return 1;
}
// 创建节点句柄
ros::NodeHandle n;
//****************** 1.创建Client
// 创建一个client,名称为add_two_ints的Client实例
// service消息类型是learning_communication::AddTwoInts
ros::ServiceClient client = n.serviceClient<learning_communication::AddTwoInts>("add_two_ints");
//******************2.发布服务请求
// 创建learning_communication::AddTwoInts类型的service消息
// 该消息包含两个成员变量:request、response;将节点运行时输入的两个参数作为需要相加的两个整型数存储到变量中。
learning_communication::AddTwoInts srv;
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
// 进行service调用,等待加法运算的应答结果
// 该调用过程会发生阻塞,调用成功返回true,访问srv.response即可获得服务请求的结果。
// 如果调用失败返回false,srv.response则不可用。
if (client.call(srv))
{
ROS_INFO("Sum: %ld", (long int)srv.response.sum);
}
else
{
ROS_ERROR("Failed to call service add_two_ints");
return 1;
}
return 0;
}
服务中的Client类似于话题中的Publisher,实现流程如下:
- 初始化ROS节点
- 创建一个Client实例
- 发布服务请求数据
- 等待Server处理之后的应答结果
3.7.5 编译功能包
编辑CMakeLists.txt文件,加入如下编译规则,与编译Publisher和Subscriber时的配置类似:
add_executable(server src/server.cpp)
target_link_libraries(server ${catkin_LIBRARIES})
add_dependencies(server ${PROJECT_NAME}_gencpp)
add_executable(client src/client.cpp)
target_link_libraries(client ${catkin_LIBRARIES})
add_dependencies(client ${PROJECT_NAME}_gencpp)
现在可以使用catkin_make
命令编译功能包了。
3.7.6 运行Server和Client
在运行节点之前,需要在终端设置环境变量,否则无法找到功能包最终编译生成的可执行文件:
(当前终端有效)
cd ~/catkin_ws/
source ./devel/setup.bash
(全部终端有效):
echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc
source ~/.bashrc
- 启动roscore(终端1)
在运行节点之前,启动ROS Master:
roscore
- 运行Server节点(终端2)
rosrun learning_communication server
图3-16 Server节点启动后的日志信息
- 运行Client节点(终端3)
打开新终端,运行Client节点,同时输入加法运算的两个加数值:
rosrun learning_communication client 3 5
Client发布服务请求,Server完成服务功能后反馈结果给Client。
图3-17 Server接受到服务调用后完成加法求解,并将结果反馈给Client
图3-18 Client启动后发布服务请求,并成功接收到反馈结果
3.8 ROS中的命名空间
计算图源:包括ROS中的节点、参数、话题和服务等。
计算图源的命名方式采用灵活的分层结构,便于在复杂的系统中集成和复用。命名示例如下:
- /foo
- /stanford/robot/name
- /wg/nodel
计算图源命名是ROS封装的一种重要机制:
- 每个资源定义在一个命名空间内,该命名空间内还可以创建更多资源。
- 但是处于不同命名空间内的资源不仅可以在所处命名空间内使用,还可以在全局范围内访问。
- 以上这种命名机制可以有效避免不同命名空间内的命名冲突。
3.8.1 有效的命名
一个有效的命名应该具备以下特点:
- 首字符必须是字母[a-z|A-ZB]、波浪线(~)或左斜杠(/)
- 后续字符可以是字母或数字[0-9|a-z|A-Z]、下划线(_)或者左斜杠(/)
3.8.2 命名解析
计算图源的名称分类:
- 基础名称,例如:base
- 基础名称用来描述资源本身,可以看作相对名称的一个子类
- 全局名称,例如:/global/name
- 首字符为左斜杠(/)的名称是全局名称,由左斜杠分开一系列命名空间。
- 全局名称的解析度最高,可以在全局范围内直接访问。
- 但是在系统中,全局名称越少越好,因为过多的全局名称会直接影响功能包的可移植性。
- 需要列出所有命名空间,在命名空间繁多的复杂系统中使用较为不便,所以使用相对名称代替。
- 相对名称,例如:relative/name
- 相对名称由ROS提供默认的命名空间,不需要带有开头的左斜杠。
- 相比全局名称,相对名称具有良好的移植性,用户可以直接将一个相对命名的节点移植到其他命名空间内,有效防止命名冲突。
- 相对名称的重点是如何确定默认的命名空间,ROS提供了三种方式:
- 通过命令参数设置。
- 调用
ros::init()
的ROS程序,会接受名为__ns
的命令行参数,可以为程序设置默认的命名空间,赋值方式为__ns:=default-namespace
- 调用
- 在launch文件中设置。在launch文件中可通过设置ns参数来确定默认命名空间:
<node name="turtlesim_node" pkg="turtlesim" type="turtlesim_node" ns="sim1" />
- 使用环境变量设置。在执行ROS程序的终端中设置默认命名空间的环境变量:
export ROS_NAMESPACE=default-namespace
- 通过命令参数设置。
- 私有名称,例如:~private/name
- 顾名思义,私有名称是一个节点内部私有的资源名称,只会在节点内部使用。
- 以波浪线(~)开始
- 与相对名称一样,其并不包含本身所在的命名空间,需要ROS为其解析
- 不同点在于,私有名称并不使用当前默认命名空间,而是用节点的全局名称作为命名空间。
将其中相对名称、全局名称、私有名称的解析方法归纳为如下表所示:
表3-1 ROS命名的解析方式
节点 | 相对名称(默认) | 全局名称 | 私有名称 |
---|---|---|---|
/node1 | Bar -> /bar | /bar -> /bar | ~bar -> /node1/bar |
/wg/node2 | Bar -> /wg/bar | /bar -> /bar | ~bar -> /wg/node2/bar |
/wg/node3 | foo/bar -> /wg/foo/bar | /foo/bar -> /foo/bar | ~foo/bar -> /wg/node3/foo/bar |
3.8.3 命名重映射
ROS可以支持我们同时打开多个相同的节点,而不会发生命名冲突:
- 原因:所以ROS节点内的资源名称都可以在节点启动时进行重映射。
命名重映射的语法如下:
name:=new_name
例如,将chatter重映射为/wg/chatter,可以在节点启动时进行重映射命名:
rosrun rospy_tutorials talker chatter:=/wg/chatter
注意:ROS的命名解析是在命名重映射之前发生的。所以当我们需要“foo:=bar”时,会将节点内的所有foo命名映射成bar;而如果我们重映射“/foo:=bar”时,ROS只会将全局解析为/foo的名称重映射为bar。
表3-2总结命名重映射和命名解析之间的关系:(看不懂!!!)
表3-2 命名重映射和命名解析之间的关系
节点命名空间 | 重映射参数 | 匹配名称 | 解析名称 |
---|---|---|---|
/ | foo:=bar | foo,/foo | /bar |
/baz | foo:=bar | foo,/baz/foo | baz/bar |
/ | /foo:=bar | foo,/foo | /bar |
/baz | /foo:=bar | /foo | /baz/bar |
/baz | /foo:=/a/b/c/bar | /foo | /a/b/c/bar |
3.9 分布式多机通信(未实践)
ROS是一种分布式软件框架,节点之间通过松耦合的方式进行组合。在很多应用场景下,节点可以运行在不同的计算平台上,通过Topic、Service进行通信。
但是ROS中只允许存在一个Master,在多机系统中Master只能运行在一台机器上,其他机器需要通过ssh的方式与Master进行通信。
所以在多机ROS系统中需要进行一些配置。下面以两台计算机为例,其中计算机hcx-pc作为主机运行Master,计算机raspi2作为从机运行节点。
3.9.1 设置IP地址
首先,需要确定ROS多机系统中的所以计算机处于同一个网络中。然后分别在计算机hcx-pc、raspi2上使用ifconfig
命令查看计算机的局域网IP地址。
查询结果:
- hcx-pc:192.168.31.198
- raspi2:192.168.31.14
然后,分别在两台计算机系统的/etc/hosts
文件中加入对方的IP地址和对应的计算机名:
# @hcx-pc, /etc/hosts
192.168.31.14 raspi2
# @raspi2, /etc/hosts
192.168.31.198 hcx-pc
设置完毕后,分别在两台计算机上使用ping命令连接对方,测试网络是否两台:
hcx@hcx-pc:~$ ping raspi2
robot@raspi2:~$ ping hcx-pc
如果双向网络都畅通,说明底层网络的通信已经没有问题,接下来设置ROS的相关环境变量。
3.9.2 设置ROS_MASTER_URI
因为系统中只能存在一个Master,所以从机raspi2需要知道Master的位置。
ROS Master的位置可以使用环境变量ROS_MASTER_URI进行定义,在从机raspi2上使用如下命令设置ROS_MASTER_URI:
export ROS_MASTER_URI=http://hcx-pc:11311
但是以上设置只能在输入的终端中生效,为了让所有打开的终端都能识别,最好使用如下命令,将环境变量的设置加入终端的配置文件中:
echo "export ROS_MASTER_URI=http://hcx-pc:11311" >> ~/.bashrc
3.9.3 多机通信测试
现在ROS多机系统已经配置完成,下面使用小乌龟例程进行测试。
首先,在主机hcx-pc上运行小乌龟的仿真器:
hcx-pc(终端1)
roscore
hcx-pc(终端2)
rosrun turtlesim turtlesim_node
现在,在从机raspi2上使用 rostopic list
,查看ROS系统中的话题列表。
应该可以看到在从机 raspi2上显示有turtle 相关的话题,这证明现在从机raspi2已经可以与主机hcx-pc上的ROS Master取得联系。
在从机raspi2 上发布一只小乌龟的速度控制消息:
rostopic pub -r 10 /turtle1/cmd_vel geometry_msgs/Twist "
linear: x:0.5
y:0.0
z:0.0
angular:
x:0.0
y:0.0
z:0.5
"
此时,主机hcx-pc中的小乌龟应该就开始移动了,ROS多机系统配置成功。
在实践应用中,可能需要使用两个以上的计算平台,可以使用相同的方法进行配置:主机运行Master,其他从机通过设置ROS_MASTER_URI环境变量确定Master位置即可。
参考文献
[1] 胡春旭编著.ROS机器人开发实现[M].机械工业出版社.2018:22-57
[2] 下载RoboWare Studio官网登录不上去