目录
[1.1 Topic、msg、Service、srv 的关系](#1.1 Topic、msg、Service、srv 的关系)
[1.2 Service 和 Topic 有什么区别?](#1.2 Service 和 Topic 有什么区别?)
[1.3 本篇要实现什么?](#1.3 本篇要实现什么?)
[1.4 本篇涉及的功能包](#1.4 本篇涉及的功能包)
[二、自定义 srv 接口文件](#二、自定义 srv 接口文件)
[2.1 为什么需要自定义 LimoSrv.srv?](#2.1 为什么需要自定义 LimoSrv.srv?)
[2.2 LimoSrv.srv 文件位置](#2.2 LimoSrv.srv 文件位置)
[2.3 LimoSrv.srv 文件内容](#2.3 LimoSrv.srv 文件内容)
[2.4 LimoSrv.srv 字段解释](#2.4 LimoSrv.srv 字段解释)
[2.5 srv 文件和 msg 文件有什么区别?](#2.5 srv 文件和 msg 文件有什么区别?)
[2.6 srv 文件注意事项](#2.6 srv 文件注意事项)
[(2)必须使用 --- 分隔请求和响应](#(2)必须使用 --- 分隔请求和响应)
[(3)修改 .srv 文件后必须重新编译](#(3)修改 .srv 文件后必须重新编译)
[三、配置自定义接口包 limo_msgs](#三、配置自定义接口包 limo_msgs)
[3.1 rosidl_generate_interfaces 是什么?](#3.1 rosidl_generate_interfaces 是什么?)
[3.2 limo_msgs 的 CMakeLists.txt](#3.2 limo_msgs 的 CMakeLists.txt)
[3.3 CMakeLists.txt 核心解释](#3.3 CMakeLists.txt 核心解释)
[3.4 limo_msgs 的 package.xml](#3.4 limo_msgs 的 package.xml)
[3.5 package.xml 核心解释](#3.5 package.xml 核心解释)
[四、C++ 编写自定义 srv 服务端](#四、C++ 编写自定义 srv 服务端)
[4.1 服务端节点作用](#4.1 服务端节点作用)
[4.2 自定义 srv 服务端完整代码](#4.2 自定义 srv 服务端完整代码)
[4.3 服务端代码核心名字和对象总结](#4.3 服务端代码核心名字和对象总结)
[(5)geometry_msgs::msg::Twist cmd_vel;:创建速度消息对象](#(5)geometry_msgs::msg::Twist cmd_vel;:创建速度消息对象)
(9)cmd_pub_->publish(cmd_vel);:真正发布速度消息
[(10)rclcpp::Publisher ::SharedPtr cmd_pub_;](#(10)rclcpp::Publisher ::SharedPtr cmd_pub_;)
[(11)rclcpp::Service ::SharedPtr srv_;](#(11)rclcpp::Service ::SharedPtr srv_;)
[4.4 服务端核心对象总结表](#4.4 服务端核心对象总结表)
[4.5 服务端完整通信逻辑](#4.5 服务端完整通信逻辑)
[五、C++ 编写自定义 srv 客户端](#五、C++ 编写自定义 srv 客户端)
[5.1 客户端节点作用](#5.1 客户端节点作用)
[5.2 自定义 srv 客户端完整代码](#5.2 自定义 srv 客户端完整代码)
[5.3 客户端代码核心名字和对象总结](#5.3 客户端代码核心名字和对象总结)
(6)spin_until_future_complete():等待响应
[5.4 服务端和客户端的关系](#5.4 服务端和客户端的关系)
[六、配置代码包 limo_learning](#六、配置代码包 limo_learning)
[6.1 limo_learning 的 package.xml](#6.1 limo_learning 的 package.xml)
[6.2 limo_learning 的 CMakeLists.txt](#6.2 limo_learning 的 CMakeLists.txt)
[6.3 CMakeLists.txt 核心理解](#6.3 CMakeLists.txt 核心理解)
[7.1 先编译自定义接口包](#7.1 先编译自定义接口包)
[7.2 查看自定义 srv 是否生成成功](#7.2 查看自定义 srv 是否生成成功)
[7.3 再编译代码包](#7.3 再编译代码包)
[7.4 运行服务端](#7.4 运行服务端)
[7.5 运行客户端](#7.5 运行客户端)
[7.6 使用命令行直接调用服务](#7.6 使用命令行直接调用服务)
[7.7 使用命令查看 /cmd_vel](#7.7 使用命令查看 /cmd_vel)
[8.1 找不到 limo_msgs/srv/limo_srv.hpp](#8.1 找不到 limo_msgs/srv/limo_srv.hpp)
[8.2 ros2 interface show 找不到 LimoSrv](#8.2 ros2 interface show 找不到 LimoSrv)
[8.3 ros2 run 找不到 limo_srv_server 或 limo_srv_client](#8.3 ros2 run 找不到 limo_srv_server 或 limo_srv_client)
[8.4 客户端一直 Waiting for /limo_srv service](#8.4 客户端一直 Waiting for /limo_srv service)
[8.5 修改 LimoSrv.srv 后代码不生效](#8.5 修改 LimoSrv.srv 后代码不生效)
摘要
前两篇文章已经分别讲解了 ROS2 C++ 小车控制中的两类 Topic 通信案例。
第一篇主要讲解:Topic + 官方标准 msg
geometry_msgs/msg/Twist
sensor_msgs/msg/LaserScan
具体实现了:
- 向 /cmd_vel 发布 Twist 速度消息控制小车运动
- 订阅 /cmd_vel 速度消息并打印 linear.x 和 angular.z
- 订阅 /scan 雷达数据,并根据前方距离发布 /cmd_vel 控制小车
通过第一篇内容,我们已经知道:
- Topic 通信使用 msg 类型数据
- Publisher 负责发布消息
- Subscriber 负责订阅消息
- /cmd_vel 通常用于小车速度控制
- /scan 通常用于激光雷达数据订阅
第二篇主要讲解:Topic + 自定义 msg
limo_msgs/msg/LimoStatus
用来发布和订阅 LIMO 小车状态信息。
其中包括:
- 车辆状态 vehicle_state
- 控制模式 control_mode
- 电池电压 battery_voltage
- 错误码 error_code
- 运动模式 motion_mode
通过第二篇内容,我们进一步理解了:
- 自定义 msg 文件怎么写
- std_msgs/Header header 是什么
- limo_msgs 接口包如何配置
- rosidl_generate_interfaces 如何生成接口代码
- limo_learning 代码包如何依赖自定义接口包
- C++ 中如何发布和订阅自定义消息
到这里,我们已经完成了ROS2 中最常见的 Topic 通信:
Topic + 官方 msg
Topic + 自定义 msg
但是Topic 通信更适合持续发布和接收数据****,比如速度、状态、雷达、里程计等。
如果我们希望实现"一次请求、一次响应"的通信方式,例如:
- 客户端发送小车控制请求
- 服务端接收请求并执行
- 服务端返回是否执行成功
这时候就更适合使用 ROS2 的 Service 服务通信。
而 Service 使用的接口类型就是:
srv
所以本篇开始进入第三部分:使用自定义 srv 进行 ROS2 代码通信。
本篇主要围绕一个自定义服务接口展开:
limo_msgs/srv/LimoSrv
实现的功能是:
- 客户端发送 x、y、z 速度控制请求
- 服务端接收请求
- 服务端将请求转换成 Twist 速度消息
- 服务端发布到 /cmd_vel 控制小车
- 服务端返回 success 执行结果
本篇重点理解:
- srv 文件怎么写
- srv 中 --- 分隔符是什么意思
- Service Server 服务端是什么
- Service Client 客户端是什么
- request 请求数据是什么
- response 响应数据是什么
- C++ 中如何创建自定义 srv 服务端
- C++ 中如何创建自定义 srv 客户端
- 如何配置 CMakeLists.txt 和 package.xml
- 如何编译、运行和测试自定义 srv 服务通信
通过本篇文章,可以从前两篇的 Topic 通信继续扩展到 Service 通信,进一步掌握 ROS2 C++ 中"请求---响应"式的小车控制流程。
前言
在 ROS2 中,常见通信方式主要有三类:
Topic
Service
Action
它们分别对应不同的接口类型:
Topic 使用 msg
Service 使用 srv
Action 使用 action
前面两篇已经讲过 Topic 通信。
第一篇使用的是****ROS2 官方标准 msg:
geometry_msgs/msg/Twist
sensor_msgs/msg/LaserScan
第二篇使用的是****自定义 msg:
limo_msgs/msg/LimoStatus
本篇开始进入 ROS2 的第二种通信方式:
- Service 服务通信
Service 通信最大的特点是:
- 一次请求,一次响应
也就是说,它不像 Topic 那样持续发布数据,而是由客户端主动发起一次请求,服务端处理完后返回一次结果。
例如本篇要实现的效果是:
客户端发送 x、y、z 三个速度参数
↓
服务端收到请求
↓
服务端把 x、y、z 转换成 Twist 速度消息
↓
服务端发布到 /cmd_vel 控制小车
↓
服务端返回 success 表示执行成功
所以本篇的重点就是:
使用自定义 srv 实现一次请求控制小车运动
一、本章案例整体说明
1.1 Topic、msg、Service、srv 的关系
很多初学者刚开始学习 ROS2 时,容易把 Topic、msg、Service、srv 混在一起。
可以先记住这几句话:
Topic 使用 msg
Service 使用 srv
Action 使用 action
也就是说:
msg 是 Topic 话题通信的数据结构
srv 是 Service 服务通信的数据结构
action 是 Action 动作通信的数据结构
对应关系如下:
| 通信方式 | 接口类型 | 示例 | 作用 |
|---|---|---|---|
| Topic | msg | geometry_msgs/msg/Twist | 持续发布速度消息 |
| Topic | msg | limo_msgs/msg/LimoStatus | 持续发布小车状态 |
| Service | srv | limo_msgs/srv/LimoSrv | 一次请求,一次响应 |
| Action | action | limo_msgs/action/LimoAction | 长时间任务,持续反馈 |
所以本篇的核心是:
Service + 自定义 srv
也就是:
使用自定义 LimoSrv.srv 实现服务通信
1.2 Service 和 Topic 有什么区别?
- Topic 更像是广播。
- 发布者只管发布数据,订阅者自己接收。
例如:
limo_status_pub 不断发布 /limo_status
limo_status_sub 不断订阅 /limo_status
这种适合持续变化的数据,比如:
- 小车状态
- 雷达数据
- 速度指令
- IMU 数据
- 里程计数据
- Service 更像是问答。
- 客户端发起请求,服务端返回结果。
例如:
客户端:请让小车按照 x=0.2,z=0.0 运动
服务端:收到,执行成功
所以可以这样理解:
|---------|------------------|---------------------|
| 通信方式 | 特点 | 适合场景 |
| Topic | 持续发布,持续接收 | 状态数据、传感器数据、速度指令 |
| Service | 一次请求,一次响应 | 查询状态、触发动作、切换模式、请求控制 |
| Action | 发送目标,持续反馈,最终返回结果 | 导航、机械臂抓取、长时间运动任务 |
本篇只讲:
Service + 自定义 srv
1.3 本篇要实现什么?
本篇要实现一个完整的自定义 srv 服务通信案例。
我们会定义一个自定义服务接口:
LimoSrv.srv
它包含:
请求数据 request:
float32 x
float32 y
float32 z
响应数据 response:
bool success
然后在 C++ 中分别编写两个节点:
limo_srv_server:服务端节点
limo_srv_client:客户端节点
整体流程如下:
LimoSrv.srv 定义请求和响应结构
↓
配置 limo_msgs 接口包
↓
编译生成自定义 srv 接口代码
↓
limo_srv_server 创建 /limo_srv 服务
↓
limo_srv_client 发送 x、y、z 控制请求
↓
服务端接收 request 请求
↓
服务端发布 /cmd_vel 控制小车
↓
服务端返回 success 响应
↓
客户端接收 response 结果
简单来说就是:
客户端发送速度请求
↓
服务端接收请求
↓
服务端控制小车
↓
服务端返回执行结果
1.4 本篇涉及的功能包
建议仍然使用两个功能包:
limo_msgs 存放自定义接口
limo_learning 存放 C++ 节点代码
整体目录结构如下:
agilex_open_class_ws/
└── src
├── limo_msgs
│ ├── msg
│ │ └── LimoStatus.msg
│ ├── srv
│ │ └── LimoSrv.srv
│ ├── CMakeLists.txt
│ └── package.xml
│
└── limo_learning
├── src
│ ├── limo_srv_server.cpp
│ └── limo_srv_client.cpp
├── CMakeLists.txt
└── package.xml
其中:
|---------------|-----------------------------|
| 功能包 | 作用 |
| limo_msgs | 定义自定义 msg / srv / action 接口 |
| limo_learning | 编写 C++ 节点代码 |
本篇重点使用:
limo_msgs/srv/LimoSrv.srv
二、自定义 srv 接口文件
2.1 为什么需要自定义 LimoSrv.srv?
前面控制小车速度时,我们使用过:
geometry_msgs/msg/Twist
它可以向 /cmd_vel 发布速度指令。
但是如果我们希望通过 Service 方式控制小车,就不能直接使用 msg。
因为 Service 通信使用的是:
srv
所以我们需要自己定义一个服务接口:
limo_msgs/srv/LimoSrv.srv
它的作用是描述:
- 客户端要发送什么请求数据
- 服务端要返回什么响应数据
本案例中,客户端需要发送三个速度参数:
- x:前进后退速度
- y:左右平移速度
- z:旋转角速度
服务端需要返回一个执行结果:
- success:是否执行成功
2.2 LimoSrv.srv 文件位置
自定义 srv 文件一般放在接口包的 srv 目录下。
文件路径:
limo_msgs/srv/LimoSrv.srv
注意文件名建议使用大驼峰命名:
LimoSrv.srv
后面编译生成 C++ 头文件时,会变成小写加下划线形式:
#include "limo_msgs/srv/limo_srv.hpp"
也就是说:
LimoSrv.srv
↓ 编译生成
limo_srv.hpp
2.3 LimoSrv.srv 文件内容
新建文件:
limo_msgs/srv/LimoSrv.srv
内容如下:
float32 x
float32 y
float32 z
---
bool success
这里一定要注意中间的:
---
它是 srv 文件中非常重要的分隔符。
它把服务接口分成上下两部分:
请求部分 request
---
响应部分 response
所以这个文件可以理解为:
客户端发送:
float32 x
float32 y
float32 z
服务端返回:
bool success
2.4 LimoSrv.srv 字段解释
可以把 LimoSrv.srv 中的字段理解成下面这样:
|---------|---------|-------------|-----------------|
| 字段 | 类型 | 所属部分 | 含义 |
| x | float32 | request 请求 | linear.x,前进后退速度 |
| y | float32 | request 请求 | linear.y,左右平移速度 |
| z | float32 | request 请求 | angular.z,旋转角速度 |
| success | bool | response 响应 | 服务是否执行成功 |
例如客户端发送:
x = 0.2
y = 0.0
z = 0.0
可以理解为:
请求小车向前运动
如果服务端处理成功,就返回:
success = true
表示服务调用成功。
2.5 srv 文件和 msg 文件有什么区别?
前面写过:
LimoStatus.msg
它的内容类似:
std_msgs/Header header
uint8 vehicle_state
uint8 control_mode
float64 battery_voltage
uint16 error_code
uint8 motion_mode
- msg 文件只有一段内容。
- 它只描述一条消息的数据结构。
但是 srv 文件有两段内容:
请求部分
---
响应部分
例如:
float32 x
float32 y
float32 z
---
bool success
可以这样对比:
|------|-----------|---------|------------|
| 文件类型 | 是否有 --- | 通信方式 | 作用 |
| msg | 没有 | Topic | 描述话题中传输的数据 |
| srv | 有一个 --- | Service | 描述请求和响应数据 |
所以:
- msg 适合单向传输数据
- srv 适合一问一答
2.6 srv 文件注意事项
写 .srv 文件时要注意:
(1)字段后面不需要写分号
错误写法:
float32 x;
正确写法:
float32 x
(2)必须使用 --- 分隔请求和响应
例如:
float32 x
float32 y
float32 z
---
bool success
--- 上面是请求。
--- 下面是响应。
(3)修改 .srv 文件后必须重新编译
因为 .srv 文件不能直接被 C++ 使用。
ROS2 需要先根据 .srv 文件生成 C++ 头文件。
所以每次修改:
LimoSrv.srv
都需要重新编译:
colcon build --packages-select limo_msgs
source install/setup.bash
三、配置自定义接口包 limo_msgs
3.1 rosidl_generate_interfaces 是什么?
在 ROS2 中,我们自己编写的 .srv 文件,本质上只是一个****接口描述文件。
例如:
srv/LimoSrv.srv
它只是告诉 ROS2:
- 这个服务请求里面有哪些字段
- 这个服务响应里面有哪些字段
- 字段类型是什么
- 字段名称叫什么
但是这个 .srv 文件本身还不能直接被 C++ 或 Python 程序使用。
也就是说,C++ 程序不能直接拿 .srv 文件来 include。
我们最终在 C++ 中真正包含的是:
#include "limo_msgs/srv/limo_srv.hpp"
这个 .hpp 头文件不是手写出来的,而是 ROS2 编译时自动生成的。
这时候就需要用到:
rosidl_generate_interfaces
它的作用可以理解为:
自定义 .msg / .srv / .action 文件
↓
rosidl_generate_interfaces 参与编译生成
↓
生成 C++ / Python 可以使用的接口代码
↓
其它 ROS2 节点才能正常使用这些自定义接口
需要注意的是:
rosidl_generate_interfaces 不是终端命令
1. 它不是这样单独运行:
rosidl_generate_interfaces2. 而是写在
CMakeLists.txt里面。然后通过:
colcon build触发接口代码生成。
简单来说:
rosidl_generate_interfaces 就是 ROS2 自定义接口包的代码生成开关
3.2 limo_msgs 的 CMakeLists.txt
由于 limo_msgs 里面现在既有自定义 msg,也有自定义 srv。
所以 limo_msgs/CMakeLists.txt 可以这样写:
cpp
cmake_minimum_required(VERSION 3.8)
project(limo_msgs)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rosidl_default_generators REQUIRED)
find_package(std_msgs REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/LimoStatus.msg"
"srv/LimoSrv.srv"
DEPENDENCIES std_msgs
)
ament_package()
这里相比上一篇自定义 msg,多了一行:
cpp
"srv/LimoSrv.srv"
表示告诉 ROS2:
- 除了生成 LimoStatus.msg 对应的接口代码
- 还要生成 LimoSrv.srv 对应的服务接口代码
3.3 CMakeLists.txt 核心解释
查找构建工具:
find_package(ament_cmake REQUIRED)
表示当前功能包使用 ament_cmake 构建。
查找接口生成工具:
find_package(rosidl_default_generators REQUIRED)
表示当前功能包需要使用 ROS2 接口生成工具。
查找 std_msgs:
find_package(std_msgs REQUIRED)
因为前面自定义 LimoStatus.msg 中使用了:
std_msgs/Header header
所以这里需要保留 std_msgs 依赖。
生成自定义接口:
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/LimoStatus.msg"
"srv/LimoSrv.srv"
DEPENDENCIES std_msgs
)
这句是自定义接口包的核心。
其中:
"msg/LimoStatus.msg"
表示生成自定义消息接口。
"srv/LimoSrv.srv"
表示生成自定义服务接口。
DEPENDENCIES std_msgs
表示接口文件中依赖了 std_msgs。
最后:声明当前功能包
ament_package()
表示声明当前功能包是一个 ROS2 ament 功能包。
3.4 limo_msgs 的 package.xml
limo_msgs/package.xml 可以这样写:
XML
<?xml version="1.0"?>
<package format="3">
<name>limo_msgs</name>
<version>0.0.0</version>
<description>Custom messages and services for LIMO robot</description>
<maintainer email="agilex@todo.todo">agilex</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<depend>std_msgs</depend>
<member_of_group>rosidl_interface_packages</member_of_group>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
3.5 package.xml 核心解释
构建工具依赖:
<buildtool_depend>ament_cmake</buildtool_depend>
表示当前功能包使用 ament_cmake 构建。
接口生成依赖:
<build_depend>rosidl_default_generators</build_depend>
表示构建时需要接口生成工具。
运行时接口依赖:
<exec_depend>rosidl_default_runtime</exec_depend>
表示运行时需要接口相关运行环境。
标准消息依赖:
<depend>std_msgs</depend>
因为 LimoStatus.msg 中使用了:
std_msgs/Header
所以这里需要声明 std_msgs。
接口包声明:
<member_of_group>rosidl_interface_packages</member_of_group>
这一项非常重要。
- 它表示当前功能包属于 ROS2 接口包。
- 如果忘记这一项,自定义接口可能无法正常生成或无法被其它功能包使用。
四、C++ 编写自定义 srv 服务端
4.1 服务端节点作用
接下来在 limo_learning 功能包中写一个 C++ 服务端节点:
limo_srv_server
它的作用是:
- 创建 /limo_srv 服务
- 等待客户端发送 x、y、z 请求
- 收到请求后生成 Twist 速度消息
- 发布到 /cmd_vel 控制小车
- 返回 success 执行结果
整体流程如下:
创建 ROS2 节点
↓
创建 /cmd_vel 发布者
↓
创建 /limo_srv 服务端
↓
等待客户端请求
↓
收到 request 后解析 x、y、z
↓
发布 Twist 到 /cmd_vel
↓
返回 response->success
4.2 自定义 srv 服务端完整代码
文件路径:
limo_learning/src/limo_srv_server.cpp
代码如下:
cpp
// 引入智能指针相关头文件
// std::make_shared、std::shared_ptr 会用到
#include <memory>
// 引入函数绑定相关头文件
// std::bind 和 std::placeholders 会用到
#include <functional>
// 引入 ROS2 C++ 客户端库
// rclcpp 是 ROS2 中 C++ 编程最核心的库
#include "rclcpp/rclcpp.hpp"
// 引入 Twist 消息头文件
// 服务端收到请求后,会把请求转换成 Twist 速度消息发布到 /cmd_vel
#include "geometry_msgs/msg/twist.hpp"
// 引入自定义 srv 服务头文件
// 这个头文件由 limo_msgs/srv/LimoSrv.srv 编译后自动生成
#include "limo_msgs/srv/limo_srv.hpp"
// 定义一个自定义 srv 服务端节点类
// 该类继承自 rclcpp::Node,说明它是一个 ROS2 节点
class LimoSrvServer : public rclcpp::Node
{
public:
// 构造函数
// Node("limo_srv_server") 表示创建一个名为 limo_srv_server 的节点
LimoSrvServer() : Node("limo_srv_server")
{
// 创建一个 /cmd_vel 发布者
// 后面服务端收到请求后,会发布 Twist 消息控制小车
cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>(
"/cmd_vel",
10
);
// 创建一个服务端
// 服务类型是 limo_msgs::srv::LimoSrv
// 服务名称是 /limo_srv
// 当客户端发送请求时,会自动调用 srv_callback 回调函数
srv_ = this->create_service<limo_msgs::srv::LimoSrv>(
"/limo_srv",
std::bind(&LimoSrvServer::srv_callback,
this,
std::placeholders::_1,
std::placeholders::_2)
);
// 打印日志,表示服务端节点已经启动
RCLCPP_INFO(this->get_logger(), "limo_srv_server node has started.");
}
private:
// 服务回调函数
// 当客户端调用 /limo_srv 服务时,该函数会被自动执行
//
// request 表示客户端发送过来的请求数据
// response 表示服务端要返回给客户端的响应数据
void srv_callback(
const std::shared_ptr<limo_msgs::srv::LimoSrv::Request> request,
std::shared_ptr<limo_msgs::srv::LimoSrv::Response> response)
{
// 打印客户端发送过来的请求数据
RCLCPP_INFO(this->get_logger(),
"Receive request: x = %.2f, y = %.2f, z = %.2f",
request->x,
request->y,
request->z);
// 创建一个 Twist 速度消息对象
geometry_msgs::msg::Twist cmd_vel;
// 将客户端请求中的 x、y、z 转换成小车速度控制指令
//
// x 对应 linear.x,表示前进 / 后退速度
// y 对应 linear.y,表示左右平移速度
// z 对应 angular.z,表示绕 z 轴旋转速度
cmd_vel.linear.x = request->x;
cmd_vel.linear.y = request->y;
cmd_vel.linear.z = 0.0;
cmd_vel.angular.x = 0.0;
cmd_vel.angular.y = 0.0;
cmd_vel.angular.z = request->z;
// 发布 Twist 速度消息到 /cmd_vel 话题
// 真实小车底盘驱动节点订阅 /cmd_vel 后,会执行对应运动
cmd_pub_->publish(cmd_vel);
// 设置服务响应结果
// true 表示服务端已经成功接收并发布速度指令
response->success = true;
// 打印响应结果
RCLCPP_INFO(this->get_logger(),
"Send response: success = %d",
response->success);
}
private:
// /cmd_vel 速度发布者对象
// 用于发布 geometry_msgs::msg::Twist 类型的速度消息
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
// 自定义服务端对象
// 用于提供 /limo_srv 服务
rclcpp::Service<limo_msgs::srv::LimoSrv>::SharedPtr srv_;
};
int main(int argc, char * argv[])
{
// 初始化 ROS2 C++ 客户端库
rclcpp::init(argc, argv);
// 创建 LimoSrvServer 节点对象,并让节点持续运行
// spin 会让服务端一直等待客户端请求
rclcpp::spin(std::make_shared<LimoSrvServer>());
// 关闭 ROS2
rclcpp::shutdown();
return 0;
}
4.3 服务端代码核心名字和对象总结
(1)LimoSrvServer:自定义服务端节点类
class LimoSrvServer : public rclcpp::Node
LimoSrvServer 是我们自己定义的一个C++类。
它继承自:
rclcpp::Node
说明它是一个 ROS2 节点。
可以理解为:
LimoSrvServer = 一个专门提供小车控制服务的 ROS2 节点类
这个节点的主要作用是:
- 创建 /limo_srv 服务
- 接收客户端发来的 x、y、z 控制请求
- 把请求转换成 Twist 速度消息
- 发布到 /cmd_vel 控制小车
- 返回 success 执行结果
(2)"limo_srv_server":节点名称
LimoSrvServer() : Node("limo_srv_server")
这里的:
"limo_srv_server"
表示当前 ROS2 节点的名字。
启动后可以通过命令查看:
ros2 node list
正常情况下会看到:
/limo_srv_server
注意:
- LimoSrvServer 是 C++ 类名
- limo_srv_server 是 ROS2 节点名
(3)cmd_pub_:速度发布者对象
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
cmd_pub_ 是服务端类中的一个成员变量。
它的作用是:
向 /cmd_vel 发布 Twist 速度消息
在构造函数中,通过下面代码创建:
cpp
cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>(
"/cmd_vel",
10
);
可以理解为:
- 创建一个发布者 cmd_pub_
- 发布的消息类型是 geometry_msgs::msg::Twist
- 发布的话题名称是 /cmd_vel
- 队列长度是 10
(4)/cmd_vel:速度控制话题名称
"/cmd_vel"
/cmd_vel 是移动机器人中非常常见的速度控制话题。
它通常使用的消息类型是:
geometry_msgs::msg::Twist
在本篇服务端代码中,服务端收到客户端请求后,不是直接控制电机,而是把请求转换成 Twist 速度消息,然后发布到:
/cmd_vel
真正控制小车底盘的,通常是小车底盘驱动节点。
完整关系可以理解为:
客户端调用 /limo_srv
↓
服务端收到请求
↓
服务端发布 /cmd_vel
↓
底盘驱动节点订阅 /cmd_vel
↓
底盘驱动节点控制电机
↓
小车运动
所以:
/limo_srv 是服务通信入口
/cmd_vel 是最终速度控制话题
(5)geometry_msgs::msg::Twist cmd_vel;:创建速度消息对象
geometry_msgs::msg::Twist cmd_vel;
这句代码表示创建一个 Twist 速度消息对象,变量名叫:cmd_vel
cmd_vel
它的类型是:
geometry_msgs::msg::Twist
也就是 ROS2 官方提供的速度消息类型。
Twist 消息主要包含两部分:
linear 线速度
angular 角速度
在小车控制中,常用字段是:
cmd_vel.linear.x
cmd_vel.linear.y
cmd_vel.angular.z
在本篇代码中,服务端会把客户端请求中的:
request->x
request->y
request->z
赋值给 Twist 消息:
cmd_vel.linear.x = request->x;
cmd_vel.linear.y = request->y;
cmd_vel.angular.z = request->z;
也就是:
request->x → cmd_vel.linear.x
request->y → cmd_vel.linear.y
request->z → cmd_vel.angular.z
可以理解为:
把 srv 请求数据转换成 /cmd_vel 可以使用的速度消息
(6)为什么要这样赋值?
因为客户端请求里的字段是自定义的:
request->x
request->y
request->z
但是小车底盘真正接收的是 ROS2 标准速度话题:
/cmd_vel
而**/cmd_vel 使用的是 Twist 消息。**
所以服务端要做一次"转换":
cmd_vel.linear.x = request->x;
cmd_vel.linear.y = request->y;
cmd_vel.angular.z = request->z;
意思就是:
| 客户端请求 | 转换成 Twist 字段 | 控制效果 |
|---|---|---|
request->x |
cmd_vel.linear.x |
前进 / 后退 |
request->y |
cmd_vel.linear.y |
左右平移 |
request->z |
cmd_vel.angular.z |
左转 / 右转 |
(7)srv_:服务端对象
rclcpp::Service<limo_msgs::srv::LimoSrv>::SharedPtr srv_;
srv_ 是服务端类中的一个成员变量。
它的作用是:
- 创建并保存 /limo_srv 服务
- 等待客户端请求
- 收到请求后执行 srv_callback 回调函数
在构造函数中,通过下面代码创建:
cpp
srv_ = this->create_service<limo_msgs::srv::LimoSrv>(
"/limo_srv",
std::bind(&LimoSrvServer::srv_callback,
this,
std::placeholders::_1,
std::placeholders::_2)
);
可以理解为:
- 创建一个服务端 srv_
- 服务类型是 limo_msgs::srv::LimoSrv
- 服务名称是 /limo_srv
- 收到客户端请求后执行 srv_callback()
(8)"/limo_srv":服务名称
"/limo_srv"
/limo_srv 是本篇自定义服务的名称。
客户端想调用这个服务,就必须连接同一个服务名:
/limo_srv
服务名必须一致。
如果服务端创建的是:
/limo_srv
客户端却调用:
/limo_control
那就连接不上。
所以可以这样理解:
/limo_srv = 客户端和服务端约定好的服务名称
(9)cmd_pub_->publish(cmd_vel);:真正发布速度消息
cmd_pub_->publish(cmd_vel);
这句是服务端真正发布速度消息的地方。
它的意思是:
- 通过 cmd_pub_ 这个发布者
- 把 cmd_vel 这条 Twist 速度消息
- 发布到 /cmd_vel 话题上
注意:
- cmd_vel 只是一个消息对象
- cmd_pub_ 是发布者对象
- publish() 才是真正执行发布动作
如果没有这句:
cmd_pub_->publish(cmd_vel);
那么即使前面给 cmd_vel 赋值了,小车也不会收到速度指令。
所以这一句是服务端控制小车运动的关键代码。
(10)rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
这句是发布者成员变量的声明。
拆开理解:
rclcpp::Publisher
表示这是一个 ROS2 发布者
geometry_msgs::msg::Twist
表示这个发布者发布 Twist 速度消息
SharedPtr
表示使用智能指针管理这个发布者对象
cmd_pub_
表示这个发布者对象的变量名
完整理解就是:
cmd_pub_ 是一个发布 Twist 消息的 ROS2 发布者智能指针
它负责向:
/cmd_vel****发布速度控制消息。
(11)rclcpp::Service<limo_msgs::srv::LimoSrv>::SharedPtr srv_;
rclcpp::Service<limo_msgs::srv::LimoSrv>::SharedPtr srv_;
这句是服务端成员变量的声明。
拆开理解:
rclcpp::Service
表示这是一个 ROS2 服务端
limo_msgs::srv::LimoSrv
表示这个服务端使用自定义 LimoSrv 服务接口
SharedPtr
表示使用智能指针管理这个服务端对象
srv_
表示这个服务端对象的变量名
完整理解就是:
srv_ 是一个使用 limo_msgs/srv/LimoSrv 类型的 ROS2 服务端智能指针
它负责提供:
/limo_srv 服务。
(12)srv_callback():服务回调函数
cpp
void srv_callback(
const std::shared_ptr<limo_msgs::srv::LimoSrv::Request> request,
std::shared_ptr<limo_msgs::srv::LimoSrv::Response> response)
srv_callback() 是服务端回调函数。
当客户端调用 /limo_srv 服务时,ROS2 会自动执行这个函数。
其中:
request
表示客户端发送来的请求数据。
可以读取:
request->x
request->y
request->z
而:
response
表示服务端要返回给客户端的响应数据。
可以赋值:
response->success = true;
所以这个回调函数主要做三件事:
- 读取客户端请求 request
- 发布 /cmd_vel 速度消息
- 填写服务端响应 response
4.4 服务端核心对象总结表
| 名称 | 类型 / 写法 | 作用 |
|---|---|---|
LimoSrvServer |
class LimoSrvServer : public rclcpp::Node |
自定义服务端节点类,负责提供小车控制服务 |
limo_srv_server |
Node("limo_srv_server") |
ROS2 节点名称,可通过 ros2 node list 查看 |
cmd_pub_ |
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr |
速度发布者对象,用于发布 /cmd_vel |
/cmd_vel |
create_publisher(..., "/cmd_vel", 10) |
小车速度控制话题,底盘驱动节点通常订阅它 |
srv_ |
rclcpp::Service<limo_msgs::srv::LimoSrv>::SharedPtr |
服务端对象,用于提供 /limo_srv 服务 |
/limo_srv |
create_service(..., "/limo_srv", ...) |
自定义服务名称,客户端和服务端必须保持一致 |
geometry_msgs::msg::Twist cmd_vel; |
Twist 消息对象 | 创建一条速度消息,用来保存小车线速度和角速度 |
request->x |
LimoSrv::Request 字段 |
客户端请求中的 x 速度,对应 cmd_vel.linear.x |
request->y |
LimoSrv::Request 字段 |
客户端请求中的 y 速度,对应 cmd_vel.linear.y |
request->z |
LimoSrv::Request 字段 |
客户端请求中的 z 角速度,对应 cmd_vel.angular.z |
cmd_pub_->publish(cmd_vel); |
发布函数 | 通过 cmd_pub_ 把 cmd_vel 发布到 /cmd_vel |
srv_callback() |
服务回调函数 | 收到客户端请求后自动执行,负责处理 request 并填写 response |
response->success |
LimoSrv::Response 字段 |
4.5 服务端完整通信逻辑
服务端完整逻辑可以理解为:
创建 limo_srv_server 节点
↓
创建 /cmd_vel 发布器 cmd_pub_
↓
创建 /limo_srv 服务 srv_
↓
等待客户端请求
↓
收到 request->x、request->y、request->z
↓
生成 Twist 速度消息
↓
发布到 /cmd_vel
↓
设置 response->success = true
↓
返回客户端
一句话总结:
limo_srv_server 负责接收客户端服务请求,并把请求转换成 /cmd_vel 速度指令发布出去。
五、C++ 编写自定义 srv 客户端
5.1 客户端节点作用
前面已经写好了服务端:
limo_srv_server
它负责提供:
/limo_srv
服务。
接下来需要写一个客户端节点:
limo_srv_client
它的作用是:
- 连接 /limo_srv 服务
- 创建请求 request
- 填写 x、y、z 控制数据
- 发送请求给服务端
- 等待服务端返回 response
- 打印 success 结果
整体流程如下:
创建 ROS2 节点
↓
创建 Client 客户端
↓
等待 /limo_srv 服务出现
↓
创建 request 请求
↓
填写 x、y、z
↓
发送异步请求
↓
等待服务端响应
↓
打印 response->success
5.2 自定义 srv 客户端完整代码
文件路径:
limo_learning/src/limo_srv_client.cpp
代码如下:
cpp
// 引入智能指针相关头文件
// std::make_shared 会用到
#include <memory>
// 引入时间相关头文件
// 1s 会用到
#include <chrono>
// 引入 ROS2 C++ 客户端库
#include "rclcpp/rclcpp.hpp"
// 引入自定义 srv 服务头文件
// 这个头文件由 limo_msgs/srv/LimoSrv.srv 编译后自动生成
#include "limo_msgs/srv/limo_srv.hpp"
// 使用 chrono_literals 后,可以直接写 1s、500ms 这种时间单位
using namespace std::chrono_literals;
int main(int argc, char * argv[])
{
// 初始化 ROS2 C++ 客户端库
rclcpp::init(argc, argv);
// 创建一个普通 ROS2 节点,节点名称为 limo_srv_client
auto node = rclcpp::Node::make_shared("limo_srv_client");
// 创建一个客户端对象
// 服务类型是 limo_msgs::srv::LimoSrv
// 服务名称是 /limo_srv
auto client = node->create_client<limo_msgs::srv::LimoSrv>("/limo_srv");
// 等待服务端启动
// 如果 /limo_srv 服务还不存在,就每隔 1 秒等待一次
while (!client->wait_for_service(1s))
{
// 如果 ROS2 已经关闭,则退出程序
if (!rclcpp::ok())
{
RCLCPP_ERROR(node->get_logger(), "Interrupted while waiting for the service.");
return 0;
}
// 打印等待服务端的日志
RCLCPP_INFO(node->get_logger(), "Waiting for /limo_srv service...");
}
// 创建服务请求对象
// Request 对应 LimoSrv.srv 中 --- 上面的部分
auto request = std::make_shared<limo_msgs::srv::LimoSrv::Request>();
// 填充请求数据
// x 对应 linear.x,表示前进 / 后退速度
// y 对应 linear.y,表示左右平移速度
// z 对应 angular.z,表示旋转角速度
request->x = 0.2;
request->y = 0.0;
request->z = 0.0;
// 打印客户端即将发送的请求
RCLCPP_INFO(node->get_logger(),
"Send request: x = %.2f, y = %.2f, z = %.2f",
request->x,
request->y,
request->z);
// 异步发送服务请求
// result 用来保存未来服务端返回的响应结果
auto result = client->async_send_request(request);
// 等待服务端处理完成,并返回响应
if (rclcpp::spin_until_future_complete(node, result) ==
rclcpp::FutureReturnCode::SUCCESS)
{
// 获取服务端返回的响应数据
auto response = result.get();
// 打印服务端返回的 success 结果
RCLCPP_INFO(node->get_logger(),
"Receive response: success = %d",
response->success);
}
else
{
// 如果服务调用失败,打印错误日志
RCLCPP_ERROR(node->get_logger(), "Failed to call /limo_srv service.");
}
// 关闭 ROS2
rclcpp::shutdown();
return 0;
}
5.3 客户端代码核心名字和对象总结
(1)limo_srv_client:客户端节点名称
auto node = rclcpp::Node::make_shared("limo_srv_client");
这里创建了一个客户端节点。
节点名称是:
limo_srv_client
它的作用是:
- 向 /limo_srv 服务发送请求
- 接收服务端返回的响应
(2)client:客户端对象
auto client = node->create_client<limo_msgs::srv::LimoSrv>("/limo_srv");
client 是客户端对象。
它连接的服务名称是:
/limo_srv
它使用的服务类型是:
limo_msgs::srv::LimoSrv
也就是前面自定义的服务接口。
可以理解为:
client = 用来调用 /limo_srv 服务的客户端对象
(3)wait_for_service():等待服务端启动
client->wait_for_service(1s)
这句表示等待服务端启动。
因为Service 通信必须先有服务端提供服务,客户端才能调用。
- 所以客户端一般会先判断:/limo_srv 服务是否存在
- 如果服务端还没启动,客户端就继续等待。
(4)request:客户端请求对象
auto request = std::make_shared<limo_msgs::srv::LimoSrv::Request>();
request 表示客户端要发送给服务端的请求对象。
它对应的是 LimoSrv.srv 中:
---
上面的部分:
float32 x
float32 y
float32 z
后面通过:
request->x = 0.2;
request->y = 0.0;
request->z = 0.0;
给请求字段赋值。
可以理解为:
客户端请求小车 linear.x = 0.2,linear.y = 0.0,angular.z = 0.0
(5)async_send_request():发送请求
auto result = client->async_send_request(request);
这句是真正发送服务请求的地方。
它表示:
客户端把 request 请求发送给 /limo_srv 服务端
因为这是异步请求,所以返回的是一个未来结果:
result
简单理解就是:
请求已经发出去了,等待服务端稍后返回结果
(6)spin_until_future_complete():等待响应
rclcpp::spin_until_future_complete(node, result)
这句表示:
等待服务端处理完成,并返回响应结果
如果服务端成功返回,则进入:
rclcpp::FutureReturnCode::SUCCESS然后客户端就可以通过:
auto response = result.get();拿到服务端返回的数据。
(7)response:服务端响应对象
auto response = result.get();
response 是服务端返回给客户端的响应结果。
它对应的是 LimoSrv.srv 中:
---
下面的部分:
bool success
所以可以通过:
response->success
读取服务端返回的执行结果。
5.4 服务端和客户端的关系
到这里,自定义 srv 的服务端和客户端就都完整了。
服务端:
limo_srv_server负责创建:
/limo_srv服务,并等待请求。
客户端:
limo_srv_client负责调用:
/limo_srv服务,并发送请求。
完整通信流程如下:
LimoSrv.srv 定义请求和响应结构
↓
limo_srv_server 创建 /limo_srv 服务端
↓
limo_srv_client 创建 /limo_srv 客户端
↓
客户端填写 request->x、request->y、request->z
↓
客户端调用 async_send_request()
↓
服务端 srv_callback() 接收请求
↓
服务端发布 /cmd_vel 控制小车
↓
服务端填写 response->success
↓
客户端接收 response 并打印结果
一句话总结:
- LimoSrv.srv 负责定义服务格式
- limo_srv_client 负责发送请求
- limo_srv_server 负责处理请求并返回结果。
六、配置代码包 limo_learning
6.1 limo_learning 的 package.xml
因为 limo_learning 中使用了:
rclcpp ROS2 官方 C++ 客户端库
geometry_msgs Twist 速度消息
limo_msgs 自定义 LimoSrv 服务接口
所以 limo_learning/package.xml 中需要添加:
<depend>rclcpp</depend>
<depend>geometry_msgs</depend>
<depend>limo_msgs</depend>
完整示例:
XML
<?xml version="1.0"?>
<package format="3">
<name>limo_learning</name>
<version>0.0.0</version>
<description>ROS2 C++ practice package for LIMO robot</description>
<maintainer email="agilex@todo.todo">agilex</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>geometry_msgs</depend>
<depend>limo_msgs</depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
其中:
|---------------|----------------------------|
| 依赖 | 作用 |
| rclcpp | 创建 ROS2 C++ 节点、服务端、客户端 |
| geometry_msgs | 使用 geometry_msgs/msg/Twist |
| limo_msgs | 使用自定义 LimoSrv.srv |
6.2 limo_learning 的 CMakeLists.txt
limo_learning/CMakeLists.txt 可以这样配置:
cpp
cmake_minimum_required(VERSION 3.8)
project(limo_learning)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(geometry_msgs REQUIRED)
find_package(limo_msgs REQUIRED)
add_executable(limo_srv_server src/limo_srv_server.cpp)
ament_target_dependencies(limo_srv_server
rclcpp
geometry_msgs
limo_msgs
)
add_executable(limo_srv_client src/limo_srv_client.cpp)
ament_target_dependencies(limo_srv_client
rclcpp
limo_msgs
)
install(TARGETS
limo_srv_server
limo_srv_client
DESTINATION lib/${PROJECT_NAME}
)
ament_package()
如果你前面已经在同一个 limo_learning 包中写了:
limo_topic_cmd
limo_topic_sub
limo_scan_ctrl
limo_status_pub
limo_status_sub
那么这里不要覆盖原来的内容,而是继续追加:
add_executable(limo_srv_server src/limo_srv_server.cpp)
ament_target_dependencies(limo_srv_server
rclcpp
geometry_msgs
limo_msgs
)
add_executable(limo_srv_client src/limo_srv_client.cpp)
ament_target_dependencies(limo_srv_client
rclcpp
limo_msgs
)
并且在 install(TARGETS ...) 里面加入:
limo_srv_server
limo_srv_client
6.3 CMakeLists.txt 核心理解
这里最重要的是四类语句:
find_package
add_executable
ament_target_dependencies
install
可以这样理解:
|---------------------------|------------------------|
| 语句 | 作用 |
| find_package | 找到依赖包 |
| add_executable | 生成可执行程序 |
| ament_target_dependencies | 给具体程序绑定依赖 |
| install | 安装可执行程序,让 ros2 run 能找到 |
例如:
find_package(limo_msgs REQUIRED)
表示找到 limo_msgs 这个自定义接口包。
add_executable(limo_srv_server src/limo_srv_server.cpp)
表示把:
src/limo_srv_server.cpp
编译成:
limo_srv_server
这个可执行程序。
ament_target_dependencies(limo_srv_server
rclcpp
geometry_msgs
limo_msgs
)
表示 limo_srv_server 这个程序需要使用:
rclcpp
geometry_msgs
limo_msgs
为什么服务端需要 geometry_msgs?
因为服务端代码中使用了:
#include "geometry_msgs/msg/twist.hpp"
并且创建了:
geometry_msgs::msg::Twist cmd_vel;
为什么客户端不需要 geometry_msgs?
因为客户端只调用自定义服务:
#include "limo_msgs/srv/limo_srv.hpp"
并没有直接使用 Twist 消息。
最后一定要写:
install(TARGETS
limo_srv_server
limo_srv_client
DESTINATION lib/${PROJECT_NAME}
)
如果没有写 install,可能会出现:
colcon build 编译成功
但是 ros2 run 找不到节点
七、编译与运行测试
7.1 先编译自定义接口包
回到工作空间根目录:
cd ~/agilex_open_class_ws
先编译自定义接口包:
colcon build --packages-select limo_msgs
source install/setup.bash
为什么要先编译 limo_msgs?
因为 limo_learning 中的 C++ 代码会引用:
#include "limo_msgs/srv/limo_srv.hpp"
这个头文件不是手写出来的,而是 ROS2 根据:
LimoSrv.srv
自动生成的。
所以必须先编译接口包。
7.2 查看自定义 srv 是否生成成功
执行:
ros2 interface show limo_msgs/srv/LimoSrv
如果能正常显示:
float32 x
float32 y
float32 z
---
bool success
说明自定义 srv 接口已经生成成功。
7.3 再编译代码包
继续编译 limo_learning:
colcon build --packages-select limo_learning
source install/setup.bash
也可以直接完整编译整个工作空间:
colcon build
source install/setup.bash
但是在学习阶段,建议先分开编译:
先编译 limo_msgs
再编译 limo_learning
这样更容易理解接口包和代码包之间的依赖关系。
7.4 运行服务端
打开第一个终端:
cd ~/agilex_open_class_ws
source install/setup.bash
ros2 run limo_learning limo_srv_server
如果节点正常启动,会看到类似日志:
limo_srv_server node has started.
此时服务端已经启动,并且正在等待客户端请求。
可以查看当前服务列表:
ros2 service list
正常情况下应该能看到:
/limo_srv
也可以查看服务类型:
ros2 service type /limo_srv
正常情况下会显示:
limo_msgs/srv/LimoSrv
7.5 运行客户端
打开第二个终端:
cd ~/agilex_open_class_ws
source install/setup.bash
ros2 run limo_learning limo_srv_client
客户端会向 /limo_srv 服务发送请求:
x = 0.2
y = 0.0
z = 0.0
客户端终端可能会看到:
Send request: x = 0.20, y = 0.00, z = 0.00
Receive response: success = 1
服务端终端可能会看到:
Receive request: x = 0.20, y = 0.00, z = 0.00
Send response: success = 1
这说明:
客户端成功发送请求
服务端成功接收请求
服务端成功发布 /cmd_vel
服务端成功返回响应
客户端成功接收响应
7.6 使用命令行直接调用服务
除了写客户端代码,也可以直接通过终端调用服务。
确保服务端已经运行:
ros2 run limo_learning limo_srv_server
然后新开一个终端,执行:
ros2 service call /limo_srv limo_msgs/srv/LimoSrv "{x: 0.2, y: 0.0, z: 0.0}"
如果服务端正常运行,会返回类似结果:
response:
limo_msgs.srv.LimoSrv_Response(success=True)
这说明服务端成功接收到了终端发来的请求,并返回了响应。
7.7 使用命令查看 /cmd_vel
因为服务端收到请求后,会发布 /cmd_vel。
所以可以再开一个终端查看:
ros2 topic echo /cmd_vel
然后调用服务:
ros2 service call /limo_srv limo_msgs/srv/LimoSrv "{x: 0.2, y: 0.0, z: 0.0}"
如果一切正常,/cmd_vel 终端会看到类似:
linear:
x: 0.2
y: 0.0
z: 0.0
angular:
x: 0.0
y: 0.0
z: 0.0
这就说明:
服务端确实把 srv 请求转换成了 Twist 速度消息
八、常见问题总结
8.1 找不到 limo_msgs/srv/limo_srv.hpp
如果编译时报错:
limo_msgs/srv/limo_srv.hpp: No such file or directory
常见原因包括:
- limo_msgs 没有编译
- 没有 source install/setup.bash
- limo_learning 没有依赖 limo_msgs
- CMakeLists.txt 中没有 find_package(limo_msgs REQUIRED)
- ament_target_dependencies 中没有添加 limo_msgs
- LimoSrv.srv 没有写进 rosidl_generate_interfaces
解决方法:
cd ~/agilex_open_class_ws
colcon build --packages-select limo_msgs
source install/setup.bash
colcon build --packages-select limo_learning
source install/setup.bash
重点检查 limo_msgs/CMakeLists.txt 中是否有:
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/LimoStatus.msg"
"srv/LimoSrv.srv"
DEPENDENCIES std_msgs
)
8.2 ros2 interface show 找不到 LimoSrv
如果执行:
ros2 interface show limo_msgs/srv/LimoSrv
找不到接口,重点检查:
- LimoSrv.srv 文件路径是否正确
- 文件名是否是 LimoSrv.srv
- CMakeLists.txt 是否写了 "srv/LimoSrv.srv"
- limo_msgs 是否编译成功
- 当前终端是否 source install/setup.bash
- package.xml 是否写了 member_of_group
尤其是 package.xml 中不要漏掉:
<member_of_group>rosidl_interface_packages</member_of_group>
8.3 ros2 run 找不到 limo_srv_server 或 limo_srv_client
如果运行:
ros2 run limo_learning limo_srv_server
或者:
ros2 run limo_learning limo_srv_client
提示找不到可执行文件,重点检查:
- limo_learning 是否编译成功
- 是否 source install/setup.bash
- CMakeLists.txt 是否写了 add_executable
- CMakeLists.txt 是否写了 install
- ros2 run 后面的可执行文件名是否写对
对应的 CMakeLists.txt 中必须有:
add_executable(limo_srv_server src/limo_srv_server.cpp)
add_executable(limo_srv_client src/limo_srv_client.cpp)
并且 install 中必须包含:
install(TARGETS
limo_srv_server
limo_srv_client
DESTINATION lib/${PROJECT_NAME}
)
8.4 客户端一直 Waiting for /limo_srv service
如果客户端一直打印:
Waiting for /limo_srv service...
说明客户端没有找到服务端。
常见原因包括:
- 服务端没有启动
- 服务端启动失败
- 服务名不一致
- 当前终端没有 source install/setup.bash
重点检查:
服务端创建的是:
"/limo_srv"
客户端也必须连接:
"/limo_srv"
服务名必须完全一致。
8.5 修改 LimoSrv.srv 后代码不生效
如果修改了:
LimoSrv.srv
一定要重新编译接口包:
colcon build --packages-select limo_msgs
source install/setup.bash
如果 limo_learning 依赖了 limo_msgs,建议再编译代码包:
colcon build --packages-select limo_learning
source install/setup.bash
原因是:
.srv 文件不是直接被 C++ 使用的
ROS2 需要先根据 .srv 文件生成 C++ 头文件
然后 C++ 代码才能 include 和使用
九、本章总结
本篇是在前面 Topic 通信基础上的进一步扩展。
前面两篇主要讲:
- Topic + 官方 msg
- Topic + 自定义 msg
本篇主要讲:
Service + 自定义 srv
Service 通信是一种"请求-响应"通信方式。客户端先向服务端发送请求,服务端收到请求后执行对应处理逻辑,处理完成后再把结果返回给客户端。返回结果中通常会包含 success、message 等字段,用来告诉客户端本次服务调用是否成功。
不过还要补充一点:
客户端在真正发送请求前,一般会先判断:
- 这个服务现在存不存在?
- 服务端有没有启动?
因为Service 必须先有服务端提供服务,客户端才能调用。
所以完整流程应该是:
1. 启动服务端 Server
2. 服务端创建 /limo_srv 服务
3. 客户端启动后先等待 /limo_srv 服务出现
4. 服务存在后,客户端发送 Request 请求
5. 服务端收到请求,执行控制逻辑
6. 服务端返回 Response 响应
7. 客户端根据 success 判断是否调用成功
可以理解为:
服务端:我先开门营业,等别人来请求
客户端:我先看看店开没开,开了我再提要求
服务端:收到要求后处理,然后告诉你成不成功
所以 Service 不是客户端随便发,服务端被动响应这么简单,准确说是:
服务端先注册服务,客户端等待服务存在,然后客户端发送请求,服务端处理请求并返回响应结果。
本章实现了两个核心节点:
|-----------------|----------------------------------|
| 节点 | 作用 |
| limo_srv_server | 创建 /limo_srv 服务,接收请求并发布 /cmd_vel |
| limo_srv_client | 调用 /limo_srv 服务,发送 x、y、z 控制请求 |
本章最重要的理解是:
- Service 使用 srv
- srv 文件分为 request 请求和 response 响应
- request 和 response 中间使用 --- 分隔
- 服务端负责处理请求并返回响应
- 客户端负责发送请求并等待响应
本章完整通信流程如下:
LimoSrv.srv 定义服务接口
↓
rosidl_generate_interfaces 生成 C++ 服务代码
↓
limo_srv_server 创建 /limo_srv 服务
↓
limo_srv_client 调用 /limo_srv 服务
↓
客户端发送 request->x、request->y、request->z
↓
服务端收到请求
↓
服务端发布 Twist 到 /cmd_vel
↓
服务端返回 response->success
↓
客户端收到响应结果
一句话总结:
自定义 srv 适合一次请求、一次响应的通信场景。
到这里,ROS2 C++ 中自定义 srv 服务通信的完整流程就跑通了。
下一篇将继续讲解:
- Action + 自定义 action
- 实现长时间任务、过程反馈和最终结果返回。