ROS基础篇

ROS:robot operating system,是一款旨在为机器人研究和开发提供代码复用的支持。

  • ROS是一款分布式进程(也就是“节点”)框架
  • 支持多种语言,包括Java,C++,Python等。
  • 大量的工具,库,协议。
  • 优秀的进程通讯机制。

1. ROS的安装

ROS系统虽然叫做系统,但是其寄生于Linux系统之上,在学习期间,可以安装虚拟机进行学习。


2. ROS的使用

2.1 简单使用

下面将演示基于终端Terminator的简单流程,也可以用vscode。

2.1.1 安装Terminator

由于使用ros时需要打开多个ros终端,则可以安装终端分频工具,

sudo apt install terminator

对此,该工具常用的快捷键:

Ctrl+Shift+W                    //关闭当前终端
Ctrl+Shift+O //水平分割终端
Ctrl+Shift+E //垂直分割终端
Ctrl+Shift+X //最大化显示当前终端

Alt+Up //移动到上面的终端
Alt+Down //移动到下面的终端
Alt+Left //移动到左边的终端
Alt+Right //移动到右边的终端

2.1.2 创建工程及依赖项

mkdir -p 自定义空间名称/src	# 生成工作空间,其下有一个src目录,用于存放源代码
cd 自定义空间名称 # 切换目录指令
catkin_make # 编译,即初始化
cd src # 进入src目录下
catkin_create_pkg 自定义ROS包名 roscpp rospy std_msgs # 创建包,并为其添加依赖项:roscpp、rospy、std_msgs

// 例:
mkdir -p lancit_ws/src
cd lancit_ws
catkin_make
cd src
catkin_create_pkg pkg_hello roscpp rospy std_msgs

打开终端,依次键入上述代码,将会完成:生成工作空间、进入工作空间、工作空间初始化,创建包及其依赖项

  • roscpp:C++实现库
  • rospy:python实现库
  • std_msgs:标准消息库

图例

image-20240518161241284

image-20240518161517507

安装tree工具,就可以在终端输出文件目录结构,然后我们输入tree,观察到对应文件目录下,

sudo apt install tree

image-20240518161840031


2.1.3 输出“Hello World!"

下面演示使用C++实现“Hello World!"

  1. 新建hello.cpp文件
cd pkg_hello/src	# 进入自己创建的包下的src目录下
touch hello.cpp # 新建源代码文件
sudo gedit hello.cpp # 自带文本编辑器打开,并编辑代码

源码

#include "ros/ros.h" 

int main(int argc, char *argv[])
{
ros::init(argc,argv,"print_hello"); // 初始化节点,该节点名为print_hello
ROS_INFO("hello world!"); // 在ros终端上显示
return 0;
}
  1. 对CMakeLists.txt编辑

对src目录下的CMakeLists.txt文件进行编辑

# 位于137行,取消注释,作用:声明可执行文件,同时将hello.cpp映射到hello上
add_executable(hello
src/hello.cpp
)

# 位于150行,告诉Cmake当链接此可执行文件时需要链接哪些库,使用上述映射名
target_link_libraries(hello
${catkin_LIBRARIES}
)
  1. 编译
cd lancit_ws	# 进入工作区
catkin_make # 编译
  1. 执行

打开Terminator,输入

source ./devel/setup.bash # 刷新环境变量
roscore

右击Terminator空白处,选择水平分割,即新开一终端

source ./devel/setup.bash	# 设置环境变量
rosrun pkg_hello hello # rosrun 包名 c++节点

结果:

image-20240518173349387


2.2 ROS文件架构


2.1.2 ROS 文件系统

ros文件系统,即ROS源代码组织形式,如:

image-20240518184800452
  • catkin_workspace:工作区。
  • build编译空间,用于存放CMake和catkin的缓存信息、配置信息和其他中间文件。
  • devel开发空间,用于存放编译后生成的目标文件,包括头文件、动态&静态链接库、可执行文件等。
  • src:源码
    • package:功能包,包含多个节点、库与配置文件,包名所有字母小写,只能由字母、数字与下划线组成
      • CMakeLists.txt配置编译规则,比如源文件、依赖项、目标文件。
      • package.xml包信息,比如:包名、版本、作者、依赖项… (以前版本是 manifest.xml)。
      • scripts存储python文件
      • msg消息通信格式文件
      • srv服务通信格式文件
      • include:头文件。
      • launch:可一次性运行多个节点。
      • src存储C++文件

2.1.3 package.xml文件详解

<?xml version="1.0"?>
<!-- 格式: 以前是 1,推荐使用格式 2 -->
<package format="2">
<!-- 包名 -->
<name>demo01_hello_vscode</name>
<!-- 版本 -->
<version>0.0.0</version>
<!-- 描述信息 -->
<description>The demo01_hello_vscode package</description>
<!-- 维护人员 -->
<maintainer email="xuzuo@todo.todo">xuzuo</maintainer>
<!-- 许可证信息,ROS核心组件默认 BSD -->
<license>TODO</license>

<!-- 依赖的构建工具,这是必须的 -->
<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>

2.1.4 CMakeLists.txt文件详解

作用:决定功能包,要依赖哪些package,要编译生成哪些目标,如何编译等。

cmake_minimum_required(VERSION 3.0.2) # 所需cmake最低版本
project(demo01_hello_vscode) # 包名称,会被 ${PROJECT_NAME} 的方式调用
# add_compile_options(-std=c++11) # 编译器选项

find_package() # 找到编译需要的其他CMake/Catkin package

catkin_python_setup() # catkin新加宏,打开catkin的Python Module的支持

add_message_files() # catkin新加宏,添加自定义Message文件

add_service_files() # catkin新加宏,添加自定义Service文件

add_action_files() # catkin新加宏,添加自定义Action文件

generate_message() # catkin新加宏,生成不同语言版本的msg/srv/action接口

catkin_package() # catkin新加宏,生成当前package的cmake配置,供依赖本包的其他软件包调用

add_library() # 添加库并指出该库的源文件

add_executable() # 添加可执行文件

add_dependencies() # 定义目标文件依赖于其他目标文件,确保其他目标已被构建

target_link_libraries() # 链接

catkin_add_gtest() # catkin新加宏,生成测试

install() # 安装至本机

注意:cmakelist文件中函数的顺序不能随意交换。

2.1.5 .launch文件说明

当我们运行多个程序(节点),需要开启多个终端,这样操作显然效率低下,故使用launch文件实现一次启动多个节点

按照2.1.2内容,在包下新建launch文件夹,新建.launch文件,内容如下:

<launch>
<node pkg="helloworld" type="demo_hello" name="hello" output="screen" />
<node pkg="turtlesim" type="turtlesim_node" name="t1"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="key1" />
</launch>
  • node:包含某个节点。
  • pkg:功能包。
  • type:被运行的节点文件。
  • name:给节点命名。
  • output:输出信息。

运行launch文件:

roslaunch 包名 launch文件名

例如,使用wheeltec科技的16线雷达,使用雷达驱动包,

roslaunch lslidar_driver lslidar_c16.launch

2.1.6 文件系统指令

”一切基于文件“,对于ROS文件系统的操作都可以像对普通文件那样操作:增、删、查、改、执行

catkin_create_pkg 自定义包名 依赖包		# 创建新的ROS功能包
sudo apt install xxx # 安装 ROS功能包
sudo apt purge xxx # 删除某个功能包
rospack list # 列出所有功能包

rospack find 包名 # 查找某个功能包是否存在,如果存在返回安装路径

roscd 包名 # 进入某个功能包

rosls 包名 # 列出某个包下的文件

apt search xxx # 搜索某个功能包
rosed 包名 文件名 # 修改功能包文件
  • 执行

    • roscore启动主节点,同时输出的还有:rosout 和 parameter server,
      • rosout:日志输出。
      • parameter server:参数服务器,它并不是一个node,而是存储参数配置的一个服务器。
      • 运行ros程序前都需要键入roscore,启动master后,node才能注册和通讯
    roscore 	# 是ROS的系统先决条件节点和程序的集合,必须运行roscore才能使ROS节点进行通信。
    roscore 将启动:
    ros master
    ros 参数服务器
    rosout 日志节点

    # 用法:
    roscore 或 roscore -p xxxx # 指定端口
    • rosrun:运行某一节点
    rosrun 包名 可执行文件名 # 运行指定的ROS节点

    # 例如
    rosrun turtlesim turtlesim_node
    • roslaunch
    roslaunch 包名 launch文件名 # 执行某个包下的 launch 文件

2.1.7 计算图

前面介绍的是ROS文件结构,是磁盘上 ROS 程序的存储结构,是静态的,而 ros 程序运行之后,不同的节点之间是错综复杂的,ROS 中提供了一个实用的工具:rqt_graph。

rqt_graph能够创建一个显示当前系统运行情况的动态图形。ROS 分布式系统中**不同进程需要进行数据交互,计算图可以以点对点的网络形式表现数据交互过程。**rqt_graph是rqt程序包中的一部分。

  • 安装
sudo apt install ros-<distro>-rqt
sudo apt install ros-<distro>-rqt-common-plugins # <distro>换成对应的ubuntu版本,如:kinetic、melodic、Noetic
  • 使用
rosrun rqt_graph rqt_graph

例如,在下面使用话题通讯时,发送者节点msgs_pub通过话题simple_chat向订阅者节点msgs_sub发送数据:

image-20240520224659634

2.3 ROS节点及函数

我参考了这位大佬的帖子,感谢!:ROS节点

2.3.1 ROS节点

在ROS系统里,最小的进程单元就是节点(node)

  • 一个软件包里可以有多个可执行文件,可执行文件在运行之后就成了一个进程(process),这个进程在ROS中就叫做节点
  • 从程序角度来说,node就是一个可执行文件(通常为C++编译生成的可执行文件、Python脚本)被执行,加载到了内存之中。
  • 从功能角度来说,通常一个node负责者机器人的某一个单独的功能。由于机器人的功能模块非常复杂,我们往往不会把所有功能都集中到一个node上,而会采用分布式的方式,把鸡蛋放到不同的篮子里。
  • 例如有一个node来控制底盘轮子的运动,有一个node驱动摄像头获取图像,有一个node驱动激光雷达…

如何管理这些繁杂的节点呢,ROS提供给我们的节点管master,master管理中心,主节点

  • node首先master处进行注册,之后master会将该node纳入整个ROS程序中
  • node之间的通信也是先由master进行“牵线”,才能两两的进行点对点通信
  • 当ROS程序启动时,第一步首先启动master,由节点管理器处理依次启动node。

查看节点,rosnode

# 列出当前运行的node信息
rosnode list
# 显示出node的详细信息
rosnode info node_name
# 结束某个node
rosnode kill node_name
# 测试连接节点
rosnode ping
# 列出在特定机器或列表机器上运行的节点
rosnode machine
# 清除不可到达节点的注册信息
rosnode cleanup
# 查看rosnode命令的用法
rosnode help

2.3.2 node函数

  • ros::init():初始化

    • 通过调用ros::init()函数来初始化node
    • 此函数向ROS系统传递命令行参数定义node名字其它参数
    • 在调用roscpp其它函数前必须先调用ros::init(),其函数原型为:
    void ros::init(int &argc, char **argv, const std::string &name, uint32_t options=0)
  • ros::Publisher:发布消息,将节点设置成发布者,并将所发布主题的类型和名称告知节点管理器。

  • ros::Subscriber:订阅消息,将节点设置成接收者,并将所接受主题的类型和名称告知节点管理器。

  • ros::Rate loop_rate:循环速率

  • ros::spin():事件循环

  • ros::spinOnce():单次事件

  • ros::start():启动

  • ros::shutdown():关闭

  • ros::ok():当接受到ctrl+c 信号或者 ros::shutdown() 调用时,为false,终止运行节点。

  • add_executable(pub_node src/pub_node.cpp):最后执行文件节点名、源文件


3. ROS通讯

ROS系统的每个节点都是一个进程,即分布式框架,为实现各节点之间的通讯,数据传输,需要进行节点通讯:

  • 话题通讯:发布订阅
  • 服务通讯:请求响应
  • 参数服务器:参数共享

3.1 话题通讯

3.1.1 理论模型

该模型中涉及到三个角色:

  • ROS Master (管理者)
  • Talker (发布者)
  • Listener (订阅者)

ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅。

整个流程由以下步骤实现:

image-20240531151646449
  1. Talker注册

Talker启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。

  1. Listener注册

Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。

  1. ROS Master实现信息匹配

ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过RPC 向 Listener 发送 Talker 的 RPC 地址信息

  1. Listener向Talker发送请求

Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。

  1. Talker确认请求

Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息

  1. Listener与Talker件里连接

Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。

  1. Talker向Listener发送消息

连接建立后,Talker 开始向 Listener 发布消息。

注意1:上述实现流程中,前五步使用的 RPC协议,最后两步使用的是 TCP 协议

注意2: Talker 与 Listener 的启动无先后顺序要求

注意3: Talker 与 Listener 都可以有多个

注意4: Talker 与 Listener 连接建立后,不再需要 ROS Master。也即,即便关闭ROS Master,Talker 与 Listern 照常通信。


3.1.2 话题通讯简单操作 (C++)

需求:

编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布文本消息,订阅方订阅消息并将消息内容打印输出。

  1. 新建包,
mkdir -p test/src
cd test
catkin_make
cd src
catkin_create_pkg pubsub roscpp rospy std_msgs
  1. 发布方,保存在test/src/pubsub/src/pub_node.cpp
#include "ros/ros.h"
#include "std_msgs/String.h" // 普通文本类型的消息
#include <sstream>

int main(int argc, char *argv[])
{
setlocale(LC_ALL,""); // 设置编码
ros::init(argc,argv,"talker"); // 初始化,参数1、2为后期节点传值,参数3为节点名称(唯一)
ros::NodeHandle nh; // 实例化句柄,该类封装了一些ROS常用功能

// 参数1: 要发布到的话题
// 参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10); // 实例化发布者对象

std_msgs::String msg; // 编写动态数据,msg.data = "你好啊!!!";
std::string msg_front = "Hello 你好!"; // 消息前缀

int count = 0; // 消息计数器
ros::Rate r(1); // 逻辑(一秒1次)

while (ros::ok()) // 节点不死
{
std::stringstream ss; // 使用 stringstream 拼接字符串与编号
ss << msg_front << count;
msg.data = ss.str();
pub.publish(msg); // 发布消息
ROS_INFO("发送的消息:%s",msg.data.c_str()); // 打印调试信息
r.sleep(); // 根据前面制定的发送频率自动休眠,休眠时间 = 1/频率;
count++; // 循环结束前,让 count 自增
ros::spinOnce(); // 调用回调函数后,可继续向下执行,可搭配Rate sleep实现自定义监听频率。
}
return 0;
}
  1. 订阅方,保存在test/src/pubsub/src/sub_node.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"

void doMsg(const std_msgs::String::ConstPtr& msg_p){
ROS_INFO("我听见:%s",msg_p->data.c_str());
// ROS_INFO("我听见:%s",(*msg_p).data.c_str());
}

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"listener");
ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
ros::spin(); // 循环读取接收的数据,并调用回调函数处理,不回向下执行

return 0;
}
  1. 配置 CMakeLists.txt,记住一定是包下src中的。
add_executable(pub_node
src/pub_node.cpp
)
add_executable(sub_node
src/sub_node.cpp
)

target_link_libraries(pub_node
${catkin_LIBRARIES}
)
target_link_libraries(sub_node
${catkin_LIBRARIES}
)
  1. 执行
  • 在工作空间根目录编译:catkin_make。
  • 键入echo "source ~/ros_workspace/devel/setup.bash" >> ~/.bashrc,下次使用直接source ~/.bashrc
  • 键入roscore,启动主节点。
  • 分屏两个终端,依次输入rosrun pubsub pub_noderosrun pubsub sub_node
  • 结果:
image-20240518223550921

此时工作目录:

image-20240518223945114
  1. 注意

注意1:不要忘记新建包是在src目录下新建的。


3.1.3 自定义msg

在上面我们通过std_msgs实现了简单文本数据的发送,下面我们演示怎样自定义数据结构。

在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型。

  • 例如:String、Int32、Int64、Char、Bool、Empty…
  • 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,
  • 比如: 激光雷达的信息… std_msgs 由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型。

msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:

  • int8, int16, int32, int64 (或者无符号类型: uint)*
  • float32, float64
  • string
  • time, duration
  • other msg files
  • variable-length array[] and fixed-length array[C]
  • header

ROS中还有一种特殊类型:Header标头包含时间戳和ROS中常用的坐标帧信息,会经常看到msg文件的第一行具有Header标头

**自定义数据:**姓名、身高、年龄等。

流程:

  1. 新建功能包
mkdir -p demo/demo31/src
cd demo/demo31
catkin_make
cd src
ctakin_create_pkg demo_msgs roscpp rospy std_msgs
  1. 定义msg文件

功能包下新建 msg 目录,

cd demo_msgs
mkdir msg
code .

添加文件 Person.msg,

string name
uint16 age
float64 height
  1. 编辑配置文件

**package.xml **中添加编译依赖与执行依赖

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
<!--
exce_depend 以前对应的是 run_depend 现在非法
-->

**CMakeLists.txt **中编辑 msg 相关配置

find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
# 需要加入 message_generation, 必须有 std_msgs
## 配置 msg 源文件
add_message_files(
FILES
Person.msg
)
# 生成消息时依赖于 std_msgs
generate_messages(
DEPENDENCIES
std_msgs
)
# 执行时依赖
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES demo02_talker_listener
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
  1. 编译

编译后的中间文件查看:

C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)

image-20240519145059593
  • Python 需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/msg)。

  • 后续调用相关 msg 时,是从这些中间文件调用的。


3.1.4 自定义话题通讯 (C++)

需求:

编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布自定义消息,订阅方订阅自定义消息并将消息内容打印输出。

流程:

  1. vscode 配置
cd demo/demo31
code .

避免报错,配置.vscode下的 c_cpp_properties.json 的 includepath属性:

{
"configurations": [
{
"browse": {
"databaseFilename": "",
"limitSymbolsToIncludedHeaders": true
},
"includePath": [
"/opt/ros/noetic/include/**",
"/usr/include/**",
"/xxx/yyy工作空间/devel/include/**" //配置 head 文件的路径
],
"name": "ROS",
"intelliSenseMode": "gcc-x64",
"compilerPath": "/usr/bin/gcc",
"cStandard": "c11",
"cppStandard": "c++17"
}
],
"version": 4
}
  1. 发布方
#include "ros/ros.h"
#include "demo02_talker_listener/Person.h"

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"talker_person");
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<demo02_talker_listener::Person>("chatter_person",1000);
demo02_talker_listener::Person p;
p.name = "sunwukong";
p.age = 2000;
p.height = 1.45;

ros::Rate r(1);
while (ros::ok())
{
pub.publish(p);
p.age += 1;
ROS_INFO("我叫:%s,今年%d岁,高%.2f米", p.name.c_str(), p.age, p.height);

r.sleep();
ros::spinOnce();
}
return 0;
}
  1. 订阅方
#include "ros/ros.h"
#include "demo02_talker_listener/Person.h"

void doPerson(const demo02_talker_listener::Person::ConstPtr& person_p){
ROS_INFO("订阅的人信息:%s, %d, %.2f", person_p->name.c_str(), person_p->age, person_p->height);
}

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"listener_person");
ros::NodeHandle nh;
ros::Subscriber sub = nh.subscribe<demo02_talker_listener::Person>("chatter_person",10,doPerson);
ros::spin();
return 0;
}
  1. 配置 CMakeLists.txt

需要添加 add_dependencies 用以设置所依赖的消息相关的中间文件,注意位置,在catkin_package后。

add_executable(talker src/talker.cpp)
add_executable(listener src/listener.cpp)

add_dependencies(talker ${PROJECT_NAME}_generate_messages_cpp)
add_dependencies(listener ${PROJECT_NAME}_generate_messages_cpp)

target_link_libraries(talker
${catkin_LIBRARIES}
)
target_link_libraries(istener
${catkin_LIBRARIES}
)
  1. 执行
  • 在工作空间根目录编译:catkin_make。
  • 键入echo "source ~/demo/demo31/devel/setup.bash" >> ~/.bashrc,下次使用直接source ~/.bashrc
  • 键入roscore,启动主节点。
  • 分屏两个终端,依次输入rosrun pubsub pub_noderosrun pubsub sub_node
  • 结果:
image-20240519155938058

此时src目录:

image-20240519160203652

3.2 服务通讯

服务通信是基于请求响应模式的,是一种应答机制。

3.2.1 理论模型

服务通信较之于话题通信更简单些,该模型中涉及到三个角色:

  • ROS master(管理者), 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,。
  • Server(服务端),Server 返回响应信息。
  • Client(客户端),Client 发送请求信息。
image-20240531151610361

整个流程由以下步骤实现:

  1. Server注册

Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

  1. Client注册

Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

  1. ROS Master实现信息匹配

ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP 地址信息。

  1. Client发送请求

Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。

  1. Server发送响应

Server 接收、解析请求的数据,并产生响应结果返回给 Client。

注意:

1.客户端请求被处理时,需要保证服务器已经启动;

2.服务端和客户端都可以存在多个。


3.2.2 自定义srv

需求:

服务通信中,客户端提交两个整数至服务端,服务端求和并响应结果到客户端,请创建服务器与客户端通信的数据载体。

  1. 新建功能包
mkdir -p demo/demo32/src
cd demo/demo32
catkin_make
cd src
ctakin_create_pkg demo_msgs roscpp rospy std_msgs
  1. 定义srv文件

功能包下新建 srv 目录,添加 test_msgs.srv 文件,

cd demo_msgs
mkdir srv
code .

服务通信中,数据分成两部分:请求与响应,用---分割,

# 客户端请求时发送的两个数字
int32 num1
int32 num2
---
# 服务器响应发送的数据
int32 sum
  1. 编辑配置文件

package.xml 中添加编译依赖与执行依赖

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

CMakeLists.txt 编辑 srv 相关配置

find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
# 需要加入 message_generation,必须有 std_msgs
# 新定义宏,添加自定义service文件
add_service_files(
FILES
AddInts.srv
)

# 新定义宏,添加msgs/srv/action接口
generate_messages(
DEPENDENCIES
std_msgs
)

# 执行时依赖
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES demo02_talker_listener
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)

注意: 官网没有在 catkin_package 中配置 message_runtime,经测试配置也可以。

  1. 编译
  • 跟自定义msg一样的,下面展示目录结构,
image-20240519170709543

3.2.3 自定义服务通讯 (c++)

  1. 服务端,保存为src/AddInts_Server.cpp
/*
需求:
编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
客户端再解析

服务器实现:
1.包含头文件
2.初始化 ROS 节点
3.创建 ROS 句柄
4.创建 服务 对象
5.回调函数处理请求并产生响应
6.由于请求有多个,需要调用 ros::spin()

*/
#include "ros/ros.h"
#include "demo_msgs/AddInts.h"

// bool 返回值由于标志是否处理成功
bool doReq(demo_msgs::AddInts::Request& req,
demo_msgs::AddInts::Response& resp){
int num1 = req.num1;
int num2 = req.num2;
ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);

if (num1 < 0 || num2 < 0) //逻辑处理
{
ROS_ERROR("提交的数据异常:数据不可以为负数");
return false;
}
//如果没有异常,那么相加并将结果赋值给 resp
resp.sum = num1 + num2;
return true;
}

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 2.初始化 ROS 节点
ros::init(argc,argv,"AddInts_Server");
// 3.创建 ROS 句柄
ros::NodeHandle nh;
// 4.创建 服务 对象
ros::ServiceServer server = nh.advertiseService("AddInts",doReq);
ROS_INFO("服务已经启动....");
// 5.回调函数处理请求并产生响应
// 6.由于请求有多个,需要调用 ros::spin()
ros::spin();
return 0;
}
  1. 客户端,保存为src/AddInts_Client.cpp
/*
服务器实现:
1.包含头文件
2.初始化 ROS 节点
3.创建 ROS 句柄
4.创建 客户端 对象
5.请求服务,接收响应
*/
// 1.包含头文件
#include "ros/ros.h"
#include "demo_msgs/AddInts.h"

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
// 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3
if (argc != 3)
// if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径)
{
ROS_ERROR("请提交两个整数");
return 1;
}
ros::init(argc,argv,"AddInts_Client");
ros::NodeHandle nh;
// 创建客户端对象
ros::ServiceClient client = nh.serviceClient<demo_msgs::AddInts>("AddInts");
//等待服务启动成功
//方式1
ros::service::waitForService("AddInts");
//方式2:client.waitForExistence();

demo_msgs::AddInts ai; // 组织请求数据
ai.request.num1 = atoi(argv[1]);
ai.request.num2 = atoi(argv[2]);
bool flag = client.call(ai); // 发送请求,返回 bool 值,标记是否成功

if (flag)
{
ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);
}
else
{
ROS_ERROR("请求处理失败....");
return 1;
}
return 0;
}
  1. 配置 CMakeLists.txt
add_executable(AddInts_Server src/AddInts_Server.cpp)
add_executable(AddInts_Client src/AddInts_Client.cpp)

add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp)
add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp)

target_link_libraries(AddInts_Server
${catkin_LIBRARIES}
)
target_link_libraries(AddInts_Client
${catkin_LIBRARIES}
)
  1. 执行
  • 需要先启动服务:rosrun 包名 服务
  • 然后再调用客户端 :rosrun 包名 客户端 参数1 参数2

结果:

优化:

  • 在客户端发送请求前添加:client.waitForExistence();

  • 或:ros::service::waitForService("AddInts");

  • 这是一个阻塞式函数,只有服务启动成功后才会继续执行。

  • 此处可以使用 launch 文件优化,但是需要注意 args 传参特点。


3.3 参数通讯

3.3.1 理论模型

参数服务器实现是最为简单的,该模型如下图所示,该模型中涉及到三个角色:

  • ROS Master (管理者)
  • Talker (参数设置者)
  • Listener (参数调用者)

ROS Master 作为一个公共容器保存参数,Talker 可以向容器中设置参数,Listener 可以获取参数。

image-20240531151726649

整个流程由以下步骤实现:

  1. Talker 设置参数

Talker 通过 RPC 向参数服务器发送参数(包括参数名与参数值),ROS Master 将参数保存到参数列表中。

  1. Listener 获取参数

Listener 通过 RPC 向参数服务器发送参数查找请求,请求中包含要查找的参数名。

  1. ROS Master 向 Listener 发送参数值

ROS Master 根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给 Listener。

参数可使用数据类型:

  • 32-bit integers
  • booleans
  • strings
  • doubles
  • iso8601 dates
  • lists
  • base64-encoded binary data
  • 字典

注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制的简单数据


3.3.2 参数通讯 (c++)

**需求:**实现参数服务器参数的增删改查操作。

在 C++ 中实现参数服务器数据的增删改查,可以通过两套 API 实现:

  • ros::NodeHandle
  • ros::param

下面为具体操作演示,

  1. 参数服务器新增(修改)参数
/*
参数服务器操作之新增与修改(二者API一样)_C++实现:
在 roscpp 中提供了两套 API 实现参数操作
ros::NodeHandle
setParam("键",值)
ros::param
set("键","值")

示例:分别设置整形、浮点、字符串、bool、列表、字典等类型参数
修改(相同的键,不同的值)

*/
#include "ros/ros.h"

int main(int argc, char *argv[])
{
ros::init(argc,argv,"set_update_param");

std::vector<std::string> stus;
stus.push_back("zhangsan");
stus.push_back("李四");
stus.push_back("王五");
stus.push_back("孙大脑袋");

std::map<std::string,std::string> friends;
friends["guo"] = "huang";
friends["yuang"] = "xiao";

//NodeHandle--------------------------------------------------------
ros::NodeHandle nh;
nh.setParam("nh_int",10); //整型
nh.setParam("nh_double",3.14); //浮点型
nh.setParam("nh_bool",true); //bool
nh.setParam("nh_string","hello NodeHandle"); //字符串
nh.setParam("nh_vector",stus); // vector
nh.setParam("nh_map",friends); // map

//修改演示(相同的键,不同的值)
nh.setParam("nh_int",10000);

//param--------------------------------------------------------
ros::param::set("param_int",20);
ros::param::set("param_double",3.14);
ros::param::set("param_string","Hello Param");
ros::param::set("param_bool",false);
ros::param::set("param_vector",stus);
ros::param::set("param_map",friends);

//修改演示(相同的键,不同的值)
ros::param::set("param_int",20000);

return 0;
}
  1. 参数服务器获取参数
/*
参数服务器操作之查询_C++实现:
在 roscpp 中提供了两套 API 实现参数操作
ros::NodeHandle

param(键,默认值)
存在,返回对应结果,否则返回默认值

getParam(键,存储结果的变量)
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值

getParamCached键,存储结果的变量)--提高变量获取效率
存在,返回 true,且将值赋值给参数2
若果键不存在,那么返回值为 false,且不为参数2赋值

getParamNames(std::vector<std::string>)
获取所有的键,并存储在参数 vector 中

hasParam(键)
是否包含某个键,存在返回 true,否则返回 false

searchParam(参数1,参数2)
搜索键,参数1是被搜索的键,参数2存储搜索结果的变量

ros::param ----- 与 NodeHandle 类似
*/

#include "ros/ros.h"

int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"get_param");

*/
//param--------------------------------------------------------
ROS_INFO("++++++++++++++++++++++++++++++++++++++++");
int res3 = ros::param::param("param_int",20); //存在
int res4 = ros::param::param("param_int2",20); // 不存在返回默认
ROS_INFO("param获取结果:%d,%d",res3,res4);

// getParam 函数
int param_int_value;
double param_double_value;
bool param_bool_value;
std::string param_string_value;
std::vector<std::string> param_stus;
std::map<std::string, std::string> param_friends;

ros::param::get("param_int",param_int_value);
ros::param::get("param_double",param_double_value);
ros::param::get("param_bool",param_bool_value);
ros::param::get("param_string",param_string_value);
ros::param::get("param_vector",param_stus);
ros::param::get("param_map",param_friends);

ROS_INFO("getParam获取的结果:%d,%.2f,%s,%d",
param_int_value,
param_double_value,
param_string_value.c_str(),
param_bool_value
);
for (auto &&stu : param_stus)
{
ROS_INFO("stus 元素:%s",stu.c_str());
}

for (auto &&f : param_friends)
{
ROS_INFO("map 元素:%s = %s",f.first.c_str(), f.second.c_str());
}

// getParamCached()
ros::param::getCached("param_int",param_int_value);
ROS_INFO("通过缓存获取数据:%d",param_int_value);

//getParamNames()
std::vector<std::string> param_names2;
ros::param::getParamNames(param_names2);
for (auto &&name : param_names2)
{
ROS_INFO("名称解析name = %s",name.c_str());
}
ROS_INFO("----------------------------");

ROS_INFO("存在 param_int 吗? %d",ros::param::has("param_int"));
ROS_INFO("存在 param_intttt 吗? %d",ros::param::has("param_intttt"));

std::string key;
ros::param::search("param_int",key);
ROS_INFO("搜索键:%s",key.c_str());

return 0;
}
  1. 参数服务器删除参数
/* 
参数服务器操作之删除_C++实现:

ros::NodeHandle
deleteParam("键")
根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false

ros::param
del("键")
根据键删除参数,删除成功,返回 true,否则(参数不存在),返回 false
*/

#include "ros/ros.h"
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
ros::init(argc,argv,"delete_param");

ros::NodeHandle nh;
bool r1 = nh.deleteParam("nh_int");
ROS_INFO("nh 删除结果:%d",r1);

bool r2 = ros::param::del("param_int");
ROS_INFO("param 删除结果:%d",r2);

return 0;
}

3.4 常用命令

当需要自定义节点和其他某个已经存在的节点通信时,如何获取对方的话题、以及消息载体的格式呢?

如下:

  • rosnode : 操作节点
  • rostopic : 操作话题
  • rosservice : 操作服务
  • rosmsg : 操作msg消息
  • rossrv : 操作srv消息
  • rosparam : 操作参数

可以使用-h选项获取更多帮助。

上述命令是动态的。