init: ROS2 learning suite
This commit is contained in:
@@ -0,0 +1,510 @@
|
||||
# 30 · Service 深度:同步 req/resp(完全指南)
|
||||
|
||||
> **目标**:吃透 ROS2 Service,涵盖 .srv 定义、Server/Client API、Future、wait_for_service、调试,学完能写工业级 Service。
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
- [1. 通信模型](#1-通信模型)
|
||||
- [2. .srv 文件定义](#2-srv-文件定义)
|
||||
- [3. Server 端(Python / C++)](#3-server-端python--c)
|
||||
- [4. Client 端(Python / C++)](#4-client-端python--c)
|
||||
- [5. 同步 vs 异步调用](#5-同步-vs-异步调用)
|
||||
- [6. wait_for_service 详解](#6-wait_for_service-详解)
|
||||
- [7. QoS + Service 名约定](#7-qos--service-名约定)
|
||||
- [8. 实战:写一个拍照 + 计算服务](#8-实战写一个拍照--计算服务)
|
||||
- [9. 调试命令](#9-调试命令)
|
||||
- [10. 常见坑](#10-常见坑)
|
||||
- [11. 在本仓库里跑](#11-在本仓库里跑)
|
||||
- [12. 进阶:异步服务端 + 回调式 client](#12-进阶异步服务端--回调式-client)
|
||||
|
||||
---
|
||||
|
||||
## 1. 通信模型
|
||||
|
||||
### 1.1 一句话
|
||||
Service 是 **同步、一对一、双向**的请求/响应。
|
||||
- 一次性调用 + 等结果
|
||||
- 几毫秒到几秒
|
||||
|
||||
```
|
||||
Client ──call(req)──> Service Server
|
||||
◀──response────
|
||||
─────────────────
|
||||
同步(阻塞),一次一答
|
||||
```
|
||||
|
||||
### 1.2 vs Topic / Action
|
||||
|
||||
| 维度 | Service | Topic | Action |
|
||||
|---|---|---|---|
|
||||
| 同步性 | **同步(等响应)** | 异步 | 异步(long) |
|
||||
| 方向 | **双向** req/resp | 单向 pub→sub | 双向 goal/fb/result |
|
||||
| 1对多 | ❌ 一对一 | ✅ | ❌ |
|
||||
| 反馈进度 | ❌ | ❌ | ✅ |
|
||||
| 可取消 | ❌ | N/A | ✅ |
|
||||
|
||||
### 1.3 何时用
|
||||
|
||||
✅ **适合**:
|
||||
- 拍照(几 ms)
|
||||
- 关节角度查询
|
||||
- 短计算(几十 ms)
|
||||
- 开关 / 触发动作
|
||||
|
||||
❌ **不适合**:
|
||||
- 周期性相机帧(用 Topic)
|
||||
- 长任务(用 Action)
|
||||
- 需要进度(用 Action)
|
||||
|
||||
---
|
||||
|
||||
## 2. .srv 文件定义
|
||||
|
||||
### 2.1 格式
|
||||
|
||||
```
|
||||
Request 字段
|
||||
---
|
||||
Response 字段
|
||||
```
|
||||
|
||||
### 2.2 标准 .srv 示例
|
||||
|
||||
```srv
|
||||
# example_interfaces/srv/AddTwoInts.srv
|
||||
int64 a # Request
|
||||
int64 b
|
||||
---
|
||||
int64 sum # Response
|
||||
```
|
||||
|
||||
### 2.3 复杂示例(多个字段)
|
||||
|
||||
```srv
|
||||
# example_interfaces/srv/SetBool.srv
|
||||
bool data
|
||||
---
|
||||
bool success
|
||||
string message
|
||||
```
|
||||
|
||||
### 2.4 本仓库用的 Service
|
||||
|
||||
`example_interfaces/srv/AddTwoInts`:
|
||||
- Request: `int64 a`, `int64 b`
|
||||
- Response: `int64 sum`
|
||||
|
||||
---
|
||||
|
||||
## 3. Server 端(Python / C++)
|
||||
|
||||
### 3.1 Python
|
||||
|
||||
```python
|
||||
import rclpy
|
||||
from rclpy.node import Node
|
||||
from example_interfaces.srv import AddTwoInts
|
||||
|
||||
class AddTwoIntsServer(Node):
|
||||
def __init__(self):
|
||||
super().__init__('add_two_ints_server_py')
|
||||
# create_service(srv_type, srv_name, callback)
|
||||
# callback 签名: callback(request, response) -> response
|
||||
self.srv = self.create_service(
|
||||
AddTwoInts,
|
||||
'add_two_ints',
|
||||
self.add_two_ints_callback,
|
||||
)
|
||||
self.get_logger().info('add_two_ints_server ready')
|
||||
|
||||
def add_two_ints_callback(self, request, response):
|
||||
response.sum = request.a + request.b
|
||||
self.get_logger().info(f'{request.a} + {request.b} = {response.sum}')
|
||||
return response # 必须 return response 对象
|
||||
|
||||
def main():
|
||||
rclpy.init()
|
||||
node = AddTwoIntsServer()
|
||||
try:
|
||||
rclpy.spin(node)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
node.destroy_node()
|
||||
rclpy.shutdown()
|
||||
```
|
||||
|
||||
### 3.2 C++
|
||||
|
||||
```cpp
|
||||
#include "rclcpp/rclcpp.hpp"
|
||||
#include "example_interfaces/srv/add_two_ints.hpp"
|
||||
|
||||
class Server : public rclcpp::Node {
|
||||
public:
|
||||
Server() : rclcpp::Node("add_two_ints_server_cpp") {
|
||||
srv_ = this->create_service<example_interfaces::srv::AddTwoInts>(
|
||||
"add_two_ints",
|
||||
[this](
|
||||
const example_interfaces::srv::AddTwoInts::Request::SharedPtr req,
|
||||
example_interfaces::srv::AddTwoInts::Response::SharedPtr res
|
||||
) {
|
||||
res->sum = req->a + req->b;
|
||||
});
|
||||
}
|
||||
private:
|
||||
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr srv_;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Client 端(Python / C++)
|
||||
|
||||
### 4.1 Python — 同步阻塞风格
|
||||
|
||||
```python
|
||||
class Client(Node):
|
||||
def __init__(self):
|
||||
super().__init__('add_two_ints_client_py')
|
||||
self.client = self.create_client(AddTwoInts, 'add_two_ints')
|
||||
|
||||
# 阻塞等服务端上线(1s 超时,循环等)
|
||||
while not self.client.wait_for_service(timeout_sec=1.0):
|
||||
self.get_logger().info('waiting for service...')
|
||||
|
||||
def call_sync(self, a, b):
|
||||
req = AddTwoInts.Request()
|
||||
req.a = a; req.b = b
|
||||
|
||||
# call_async:返回 Future,不等
|
||||
future = self.client.call_async(req)
|
||||
|
||||
# spin_until_future_complete:阻塞到 future 完成(或超时)
|
||||
rclpy.spin_until_future_complete(self, future, timeout_sec=5.0)
|
||||
|
||||
if future.result() is None:
|
||||
self.get_logger().error('service call failed')
|
||||
return None
|
||||
return future.result().sum
|
||||
```
|
||||
|
||||
### 4.2 Python — 异步回调风格
|
||||
|
||||
```python
|
||||
class AsyncClient(Node):
|
||||
def __init__(self):
|
||||
super().__init__('async_client')
|
||||
self.client = self.create_client(AddTwoInts, 'add_two_ints')
|
||||
self.client.wait_for_service()
|
||||
|
||||
def call_async(self, a, b):
|
||||
req = AddTwoInts.Request()
|
||||
req.a = a; req.b = b
|
||||
|
||||
future = self.client.call_async(req)
|
||||
|
||||
def done_cb(fut):
|
||||
result = fut.result()
|
||||
if result is not None:
|
||||
self.get_logger().info(f'result: {result.sum}')
|
||||
else:
|
||||
self.get_logger().warn('failed')
|
||||
|
||||
future.add_done_callback(done_cb)
|
||||
|
||||
def main():
|
||||
rclpy.init()
|
||||
node = AsyncClient()
|
||||
node.call_async(12, 30)
|
||||
rclpy.spin(node) # 阻塞,直到 callback 调 shutdown
|
||||
```
|
||||
|
||||
### 4.3 C++
|
||||
|
||||
```cpp
|
||||
class Client : public rclcpp::Node {
|
||||
public:
|
||||
Client() : rclcpp::Node("client") {
|
||||
client_ = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
|
||||
while (!client_->wait_for_service(std::chrono::seconds(1))) {
|
||||
RCLCPP_INFO(this->get_logger(), "waiting...");
|
||||
}
|
||||
}
|
||||
|
||||
int64_t call(int64_t a, int64_t b) {
|
||||
auto req = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
|
||||
req->a = a; req->b = b;
|
||||
|
||||
auto future = client_->async_send_request(req);
|
||||
|
||||
if (rclcpp::spin_until_future_complete(
|
||||
this->shared_from_this(), future, 5s) == rclcpp::FutureReturnCode::SUCCESS) {
|
||||
return future.get()->sum;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 4.4 关键 API 速查
|
||||
|
||||
| Python | C++ |
|
||||
|---|---|
|
||||
| `create_client(SrvType, name)` | `create_client<T>(name)` |
|
||||
| `client.wait_for_service(timeout_sec=N)` | `client->wait_for_service(1s)` |
|
||||
| `client.call_async(req)` | `client->async_send_request(req)` |
|
||||
| `future.result()` | `future.get()` |
|
||||
| `rclpy.spin_until_future_complete(node, future, timeout_sec)` | `rclcpp::spin_until_future_complete(this, future, 5s)` |
|
||||
| `future.add_done_callback(cb)` | (用 `bind`) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 同步 vs 异步调用
|
||||
|
||||
| 方式 | 适用 | 阻塞? |
|
||||
|---|---|---|
|
||||
| `call(req)` | ROS1 风格,**Python 已 deprecated** | ✅ 同步 |
|
||||
| `call_async + spin_until_future_complete` | 命令行 / 单测 / 测试 | ✅ 同步但 yield |
|
||||
| `call_async + add_done_callback` | 生产节点,主线程不能停 | ❌ 异步 |
|
||||
|
||||
**生产推荐**: `add_done_callback` 风格,主线程继续 spin 其他东西。
|
||||
|
||||
---
|
||||
|
||||
## 6. wait_for_service 详解
|
||||
|
||||
### 6.1 为什么需要
|
||||
ROS2 节点启动到 ROS 实际可达,**需要 1-2 秒 DDS discovery**。
|
||||
client 启动时 server 可能还没起,所以要先等。
|
||||
|
||||
### 6.2 用法对比
|
||||
|
||||
```python
|
||||
# ❌ 错误:永久阻塞,debug 难
|
||||
client.wait_for_service()
|
||||
|
||||
# ⚠️ 不推荐:硬超时
|
||||
client.wait_for_service(timeout_sec=5.0)
|
||||
|
||||
# ✅ 推荐:循环 + 日志
|
||||
while not client.wait_for_service(timeout_sec=1.0):
|
||||
node.get_logger().info('waiting for service...')
|
||||
```
|
||||
|
||||
### 6.3 死锁陷阱
|
||||
|
||||
如果 `wait_for_service()` 在 `__init__` 里**阻塞**,主线程 spin 没跑,DDS discovery 没动 → 永远 wait 不到。
|
||||
|
||||
**修法**: 不要在 `__init__` 阻塞,放到独立的 `wait_for_server_ready()` 方法。
|
||||
|
||||
---
|
||||
|
||||
## 7. QoS + Service 名约定
|
||||
|
||||
### 7.1 Service QoS
|
||||
默认 RELIABLE。一致即可,不用改。
|
||||
|
||||
### 7.2 命名约定
|
||||
|
||||
| 角色 | 推荐节点名 / service 名 |
|
||||
|---|---|
|
||||
| Service Server | `<verb>_server` (如 `add_two_ints_server`) |
|
||||
| Service Client | `<verb>_client` (如 `add_two_ints_client`) |
|
||||
| Service 名 | `<action>` (如 `add_two_ints`、`take_photo`) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 实战:写一个拍照 + 计算服务
|
||||
|
||||
### 8.1 .srv 定义(自定义)
|
||||
|
||||
```srv
|
||||
# my_camera/srv/TakePhoto.srv
|
||||
string filename
|
||||
---
|
||||
bool success
|
||||
int32 width
|
||||
int32 height
|
||||
string saved_path
|
||||
```
|
||||
|
||||
### 8.2 Server
|
||||
|
||||
```python
|
||||
import cv2
|
||||
from my_camera.srv import TakePhoto
|
||||
|
||||
class TakePhotoServer(Node):
|
||||
def __init__(self):
|
||||
super().__init__('take_photo_server')
|
||||
self.srv = self.create_service(
|
||||
TakePhoto, 'take_photo', self.cb)
|
||||
|
||||
def cb(self, req, resp):
|
||||
# 1) 从相机读 frame
|
||||
cap = cv2.VideoCapture(0)
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
resp.success = False
|
||||
return resp
|
||||
|
||||
# 2) 保存
|
||||
h, w = frame.shape[:2]
|
||||
path = f'/tmp/{req.filename}.jpg'
|
||||
cv2.imwrite(path, frame)
|
||||
|
||||
resp.success = True
|
||||
resp.width = w
|
||||
resp.height = h
|
||||
resp.saved_path = path
|
||||
return resp
|
||||
```
|
||||
|
||||
### 8.3 Client
|
||||
|
||||
```python
|
||||
class TakePhotoClient(Node):
|
||||
def take(self, filename):
|
||||
req = TakePhoto.Request()
|
||||
req.filename = filename
|
||||
future = self.client.call_async(req)
|
||||
rclpy.spin_until_future_complete(self, future)
|
||||
result = future.result()
|
||||
if result.success:
|
||||
print(f'saved {result.saved_path} ({result.width}x{result.height})')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 调试命令
|
||||
|
||||
```bash
|
||||
ros2 service list # 所有 service
|
||||
ros2 service type /add_two_ints # 服务类型
|
||||
ros2 service info /add_two_ints -v # 提供方节点
|
||||
ros2 service find example_interfaces/srv/AddTwoInts # 找某类型的所有 service
|
||||
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 12, b: 30}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 常见坑
|
||||
|
||||
### 10.1 Future 不 done
|
||||
|
||||
```python
|
||||
# 错:立即读 future.result()
|
||||
future = client.call_async(req)
|
||||
result = future.result() # 阻塞到死
|
||||
|
||||
# 对:spin_until_future_complete
|
||||
rclpy.spin_until_future_complete(node, future, timeout_sec=5.0)
|
||||
result = future.result()
|
||||
```
|
||||
|
||||
### 10.2 服务端 shutdown 后 client 调
|
||||
|
||||
```python
|
||||
# 服务端 rclpy.shutdown() 后,future.result() 是 None
|
||||
assert future.result() is not None, 'service unavailable'
|
||||
```
|
||||
|
||||
### 10.3 多 client 排队
|
||||
Service 是 1对1。两个 client 同时调,**第二个等第一个完成**。长任务用 Action。
|
||||
|
||||
### 10.4 wait_for_service 死锁
|
||||
|
||||
不要在 `__init__` 阻塞 wait!否则 DDS discovery 没法跑。
|
||||
|
||||
---
|
||||
|
||||
## 11. 在本仓库里跑
|
||||
|
||||
### 11.1 启 server
|
||||
```bash
|
||||
docker exec ros2_dev bash -lc "source /root/ros2_ws/install/setup.bash && ros2 launch bringup service_launch.py"
|
||||
```
|
||||
|
||||
### 11.2 调 service(新终端)
|
||||
```bash
|
||||
docker exec ros2_dev bash -lc "source /root/ros2_ws/install/setup.bash && ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts '{\"a\": 12, \"b\": 30}'"
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
response:
|
||||
example_interfaces.srv.AddTwoInts_Response(sum=42)
|
||||
```
|
||||
|
||||
### 11.3 源码
|
||||
- Server: [`src/py_srv/py_srv/add_two_ints_server.py`](../src/py_srv/py_srv/add_two_ints_server.py)
|
||||
- Client: [`src/py_srv/py_srv/add_two_ints_client.py`](../src/py_srv/py_srv/add_two_ints_client.py)
|
||||
- launch: [`src/bringup/launch/service_launch.py`](../src/bringup/launch/service_launch.py)
|
||||
- 测试: [`src/py_srv/test/test_srv.py`](../src/py_srv/test/test_srv.py)
|
||||
|
||||
### 11.4 端到端日志
|
||||
[`docker/srv_e2e.log`](../docker/srv_e2e.log)
|
||||
|
||||
---
|
||||
|
||||
## 12. 进阶:异步服务端 + 回调式 client
|
||||
|
||||
### 12.1 异步服务端
|
||||
|
||||
```python
|
||||
class AsyncServer(Node):
|
||||
"""长时间运行的服务,内部用回调推进,不阻塞主线程。"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__('async_server')
|
||||
self.srv = self.create_service(MySrv, 'async_srv', self.cb)
|
||||
|
||||
def cb(self, req, resp):
|
||||
# 启动后台任务做实际工作
|
||||
future = self._do_work(req)
|
||||
future.add_done_callback(lambda f: self._respond(f, resp))
|
||||
return resp # 先返回,后续异步填充
|
||||
|
||||
def _do_work(self, req):
|
||||
# 用 executor.spin_until_future_complete 做异步
|
||||
...
|
||||
```
|
||||
|
||||
### 12.2 回调式 client
|
||||
|
||||
```python
|
||||
def on_response(future):
|
||||
result = future.result()
|
||||
print(f'result: {result.value}')
|
||||
rclpy.shutdown()
|
||||
|
||||
future = client.call_async(req)
|
||||
future.add_done_callback(on_response)
|
||||
rclpy.spin(node)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Service vs Action 怎么选
|
||||
|
||||
| 维度 | Service | Action |
|
||||
|---|---|---|
|
||||
| 持续时间 | < 几秒 | 几秒 ~ 几小时 |
|
||||
| 反馈进度 | ❌ | ✅ Feedback |
|
||||
| 可取消 | ❌ | ✅ |
|
||||
| 适用 | 拍照、查询 | 抓取、导航 |
|
||||
|
||||
本仓库 [`py_srv`](../src/py_srv/) 是 Service demo;[`py_action_demo`](../src/py_action_demo/) 是 Action demo。
|
||||
|
||||
---
|
||||
|
||||
## 接下来读
|
||||
|
||||
| 主题 | 文档 |
|
||||
|---|---|
|
||||
| Topic 深度 | [`20-topics.md`](20-topics.md) |
|
||||
| Action 深度 | [`40-actions.md`](40-actions.md) |
|
||||
| TF2 坐标变换 | [`50-tf2.md`](50-tf2.md) |
|
||||
| 三机部署 | [`100-embedded-deployment.md`](100-embedded-deployment.md) |
|
||||
Reference in New Issue
Block a user