ROS2 C++ 小车控制完整实战(三):自定义 srv 服务通信保姆级教程

目录

摘要

前言

一、本章案例整体说明

[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 文件注意事项)

(1)字段后面不需要写分号

[(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 服务端代码核心名字和对象总结)

(1)LimoSrvServer:自定义服务端节点类

(2)"limo_srv_server":节点名称

(3)cmd_pub_:速度发布者对象

(4)/cmd_vel:速度控制话题名称

[(5)geometry_msgs::msg::Twist cmd_vel;:创建速度消息对象](#(5)geometry_msgs::msg::Twist cmd_vel;:创建速度消息对象)

(6)为什么要这样赋值?

(7)srv_:服务端对象

(8)"/limo_srv":服务名称

(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_;)

(12)srv_callback():服务回调函数

[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 客户端代码核心名字和对象总结)

(1)limo_srv_client:客户端节点名称

(2)client:客户端对象

(3)wait_for_service():等待服务端启动

(4)request:客户端请求对象

(5)async_send_request():发送请求

(6)spin_until_future_complete():等待响应

(7)response:服务端响应对象

[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

ROS2 C++ 小车控制完整实战(一):一文搞懂 Topic 发布订阅、Twist 消息与 /cmd_vel 速度控制-CSDN博客https://blog.csdn.net/m0_58954356/article/details/161829804?spm=1001.2014.3001.5502包括:

复制代码
geometry_msgs/msg/Twist
sensor_msgs/msg/LaserScan

具体实现了:

  1. 向 /cmd_vel 发布 Twist 速度消息控制小车运动
  2. 订阅 /cmd_vel 速度消息并打印 linear.x 和 angular.z
  3. 订阅 /scan 雷达数据,并根据前方距离发布 /cmd_vel 控制小车

通过第一篇内容,我们已经知道:

  1. Topic 通信使用 msg 类型数据
  2. Publisher 负责发布消息
  3. Subscriber 负责订阅消息
  4. /cmd_vel 通常用于小车速度控制
  5. /scan 通常用于激光雷达数据订阅

第二篇主要讲解:Topic + 自定义 msg

ROS2 C++ 小车控制完整实战(二):自定义 msg 消息发布与订阅保姆级教程-CSDN博客https://blog.csdn.net/m0_58954356/article/details/161834874?spm=1001.2014.3001.5502也就是自己定义:

复制代码
limo_msgs/msg/LimoStatus

用来发布和订阅 LIMO 小车状态信息。

其中包括:

  1. 车辆状态 vehicle_state
  2. 控制模式 control_mode
  3. 电池电压 battery_voltage
  4. 错误码 error_code
  5. 运动模式 motion_mode

通过第二篇内容,我们进一步理解了:

  1. 自定义 msg 文件怎么写
  2. std_msgs/Header header 是什么
  3. limo_msgs 接口包如何配置
  4. rosidl_generate_interfaces 如何生成接口代码
  5. limo_learning 代码包如何依赖自定义接口包
  6. C++ 中如何发布和订阅自定义消息

到这里,我们已经完成了ROS2 中最常见的 Topic 通信:

复制代码
Topic + 官方 msg
Topic + 自定义 msg

但是Topic 通信更适合持续发布和接收数据****,比如速度、状态、雷达、里程计等。

如果我们希望实现"一次请求、一次响应"的通信方式,例如:

  1. 客户端发送小车控制请求
  2. 服务端接收请求并执行
  3. 服务端返回是否执行成功

这时候就更适合使用 ROS2 的 Service 服务通信。

而 Service 使用的接口类型就是:

复制代码
srv

所以本篇开始进入第三部分:使用自定义 srv 进行 ROS2 代码通信。

本篇主要围绕一个自定义服务接口展开:

复制代码
limo_msgs/srv/LimoSrv

实现的功能是:

  1. 客户端发送 x、y、z 速度控制请求
  2. 服务端接收请求
  3. 服务端将请求转换成 Twist 速度消息
  4. 服务端发布到 /cmd_vel 控制小车
  5. 服务端返回 success 执行结果

本篇重点理解:

  1. srv 文件怎么写
  2. srv 中 --- 分隔符是什么意思
  3. Service Server 服务端是什么
  4. Service Client 客户端是什么
  5. request 请求数据是什么
  6. response 响应数据是什么
  7. C++ 中如何创建自定义 srv 服务端
  8. C++ 中如何创建自定义 srv 客户端
  9. 如何配置 CMakeLists.txt 和 package.xml
  10. 如何编译、运行和测试自定义 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 有什么区别?

  1. Topic 更像是广播。
  2. 发布者只管发布数据,订阅者自己接收。

例如:

复制代码
limo_status_pub 不断发布 /limo_status
limo_status_sub 不断订阅 /limo_status

这种适合持续变化的数据,比如:

  1. 小车状态
  2. 雷达数据
  3. 速度指令
  4. IMU 数据
  5. 里程计数据
  1. Service 更像是问答。
  2. 客户端发起请求,服务端返回结果。

例如:

复制代码
客户端:请让小车按照 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

它的作用是描述:

  1. 客户端要发送什么请求数据
  2. 服务端要返回什么响应数据

本案例中,客户端需要发送三个速度参数:

  1. x:前进后退速度
  2. y:左右平移速度
  3. 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
  1. msg 文件只有一段内容。
  2. 它只描述一条消息的数据结构。

但是 srv 文件有两段内容:

复制代码
请求部分
---
响应部分

例如:

复制代码
float32 x
float32 y
float32 z
---
bool success

可以这样对比:

|------|-----------|---------|------------|
| 文件类型 | 是否有 --- | 通信方式 | 作用 |
| msg | 没有 | Topic | 描述话题中传输的数据 |
| srv | 有一个 --- | Service | 描述请求和响应数据 |

所以:

  1. msg 适合单向传输数据
  2. 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:

  1. 这个服务请求里面有哪些字段
  2. 这个服务响应里面有哪些字段
  3. 字段类型是什么
  4. 字段名称叫什么

但是这个 .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_interfaces

2. 而是写在 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:

  1. 除了生成 LimoStatus.msg 对应的接口代码
  2. 还要生成 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>

这一项非常重要。

  1. 它表示当前功能包属于 ROS2 接口包。
  2. 如果忘记这一项,自定义接口可能无法正常生成或无法被其它功能包使用。

四、C++ 编写自定义 srv 服务端

4.1 服务端节点作用

接下来在 limo_learning 功能包中写一个 C++ 服务端节点:

复制代码
limo_srv_server

它的作用是:

  1. 创建 /limo_srv 服务
  2. 等待客户端发送 x、y、z 请求
  3. 收到请求后生成 Twist 速度消息
  4. 发布到 /cmd_vel 控制小车
  5. 返回 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 节点类

这个节点的主要作用是:

  1. 创建 /limo_srv 服务
  2. 接收客户端发来的 x、y、z 控制请求
  3. 把请求转换成 Twist 速度消息
  4. 发布到 /cmd_vel 控制小车
  5. 返回 success 执行结果

(2)"limo_srv_server":节点名称

复制代码
LimoSrvServer() : Node("limo_srv_server")

这里的:

复制代码
"limo_srv_server"

表示当前 ROS2 节点的名字。

启动后可以通过命令查看:

复制代码
ros2 node list

正常情况下会看到:

复制代码
/limo_srv_server

注意:

  1. LimoSrvServer 是 C++ 类名
  2. 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
);

可以理解为:

  1. 创建一个发布者 cmd_pub_
  2. 发布的消息类型是 geometry_msgs::msg::Twist
  3. 发布的话题名称是 /cmd_vel
  4. 队列长度是 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_ 是服务端类中的一个成员变量。

它的作用是:

  1. 创建并保存 /limo_srv 服务
  2. 等待客户端请求
  3. 收到请求后执行 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)
);

可以理解为:

  1. 创建一个服务端 srv_
  2. 服务类型是 limo_msgs::srv::LimoSrv
  3. 服务名称是 /limo_srv
  4. 收到客户端请求后执行 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);

这句是服务端真正发布速度消息的地方。

它的意思是:

  1. 通过 cmd_pub_ 这个发布者
  2. 把 cmd_vel 这条 Twist 速度消息
  3. 发布到 /cmd_vel 话题上

注意:

  1. cmd_vel 只是一个消息对象
  2. cmd_pub_ 是发布者对象
  3. 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;

所以这个回调函数主要做三件事:

  1. 读取客户端请求 request
  2. 发布 /cmd_vel 速度消息
  3. 填写服务端响应 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

它的作用是:

  1. 连接 /limo_srv 服务
  2. 创建请求 request
  3. 填写 x、y、z 控制数据
  4. 发送请求给服务端
  5. 等待服务端返回 response
  6. 打印 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

它的作用是:

  1. 向 /limo_srv 服务发送请求
  2. 接收服务端返回的响应

(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 通信必须先有服务端提供服务,客户端才能调用。

  1. 所以客户端一般会先判断:/limo_srv 服务是否存在
  2. 如果服务端还没启动,客户端就继续等待。

(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 并打印结果

一句话总结:

  1. LimoSrv.srv 负责定义服务格式
  2. limo_srv_client 负责发送请求
  3. 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

常见原因包括:

  1. limo_msgs 没有编译
  2. 没有 source install/setup.bash
  3. limo_learning 没有依赖 limo_msgs
  4. CMakeLists.txt 中没有 find_package(limo_msgs REQUIRED)
  5. ament_target_dependencies 中没有添加 limo_msgs
  6. 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

找不到接口,重点检查:

  1. LimoSrv.srv 文件路径是否正确
  2. 文件名是否是 LimoSrv.srv
  3. CMakeLists.txt 是否写了 "srv/LimoSrv.srv"
  4. limo_msgs 是否编译成功
  5. 当前终端是否 source install/setup.bash
  6. 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

提示找不到可执行文件,重点检查:

  1. limo_learning 是否编译成功
  2. 是否 source install/setup.bash
  3. CMakeLists.txt 是否写了 add_executable
  4. CMakeLists.txt 是否写了 install
  5. 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...

说明客户端没有找到服务端。

常见原因包括:

  1. 服务端没有启动
  2. 服务端启动失败
  3. 服务名不一致
  4. 当前终端没有 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 通信基础上的进一步扩展。

前面两篇主要讲:

  1. Topic + 官方 msg
  2. Topic + 自定义 msg

本篇主要讲:

Service + 自定义 srv

Service 通信是一种"请求-响应"通信方式。客户端先向服务端发送请求,服务端收到请求后执行对应处理逻辑,处理完成后再把结果返回给客户端。返回结果中通常会包含 success、message 等字段,用来告诉客户端本次服务调用是否成功。

不过还要补充一点:

客户端在真正发送请求前,一般会先判断:

  1. 这个服务现在存不存在?
  2. 服务端有没有启动?

因为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 控制请求 |

本章最重要的理解是:

  1. Service 使用 srv
  2. srv 文件分为 request 请求和 response 响应
  3. request 和 response 中间使用 --- 分隔
  4. 服务端负责处理请求并返回响应
  5. 客户端负责发送请求并等待响应

本章完整通信流程如下:

复制代码
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 服务通信的完整流程就跑通了。

下一篇将继续讲解:

  1. Action + 自定义 action
  2. 实现长时间任务、过程反馈和最终结果返回。
相关推荐
KuaCpp1 小时前
C++进阶(上)
linux·c++
草莓熊Lotso1 小时前
【Linux网络】深入理解 TCP 协议(一):报头设计与可靠性基石
linux·运维·服务器·c语言·网络·c++·tcp/ip
加油码1 小时前
Linux 信号详解:从 Ctrl+C 到进程异常退出,真正理解信号机制
linux·服务器·c++
Shadow(⊙o⊙)2 小时前
QT常用控件3.0,font字体设置,toolTip提示,focusPolicy焦点定位原则,中型控件StyleSheet样式表。
服务器·开发语言·前端·c++·qt
Shadow(⊙o⊙)2 小时前
QT常用控件2.0,windowOpacity窗口透明度,Cursor光标设置
开发语言·c++·qt
Lazionr2 小时前
类和对象(上):走进面向对象编程
c++
kyle~2 小时前
工业机械臂---TCP标定验收
机器人·ros2·标定
晚风叙码2 小时前
《C++面向对象进阶:static成员、友元、匿名对象与拷贝优化详解》
c++
j7~2 小时前
【C++】STL--string类--拆析解剖string以及string类的底层详解(1)
开发语言·c++·ascii编码·string类·auto和范围for