Files

py_srv — Python Service 请求-响应

ROS2 三种通信模式第二种:Service(服务)。同步、一问一答。

预计学习时间:1-2 小时。

前置知识:读完 py_pubsub 的 README(知道节点和话题是什么)。可选:看完 cpp_pubsub(Service Client/Server 也有 C++ 版,但跟 Python 几乎一一对应)。


这是什么?

Service 是 ROS2 的"打电话"模式:

  • Client 发请求(Request),等回应
  • Server 收到请求,处理完返回响应(Response)
  • 同步的(等结果才能干别的)

对比三种通信模式:

模式 用途 是否同步 是否可取消
Topic 广播消息(传感器数据流) 异步
Service 一次性"问-答"(算数学、查数据) 同步
Action 长任务(导航、机械臂运动) 异步

生活化例子:

  • Topic = 广播体操,大家都能听到
  • Service = 打电话问"12+30=?",对方必须回答
  • Action = 点外卖,可以看到骑手位置 + 可以取消订单

🎯 学完之后你能做什么?

  1. 写 Service Server(同步处理请求)
  2. 写 Service Client(用 call_async 异步发 + spin 等结果)
  3. rclpy.spin_until_future_complete 等待异步结果
  4. 手动用 ros2 service call 测试服务
  5. 理解为什么不能在 callback 里阻塞

📁 文件结构

src/py_srv/
├── py_srv/
│   ├── add_two_ints_server.py         # 服务端(a + b = sum)
│   └── add_two_ints_client.py         # 客户端
├── launch/srv_launch.py               # 一键启动 server + client
├── test/
│   ├── conftest.py
│   ├── test_srv_server.py             # 服务端单元测试
│   ├── test_srv_client.py             # 客户端单元测试
│   └── test_srv_roundtrip.py          # 端到端测试
└── setup.py

关键服务类型:example_interfaces/srv/AddTwoInts(ROS2 自带的标准接口)

# srv 文件格式(本包用现成的)
int64 a
int64 b
---
int64 sum

--- 上面是 Request,下面是 Response


🚀 跑起来

启动 Server

终端 1(容器内):

source /opt/ros/humble/setup.bash
source /root/ros2_ws/install/setup.bash

ros2 run py_srv add_two_ints_server

预期输出:

[INFO] [add_two_ints_server]: AddTwoIntsServer ready: service="add_two_ints"

启动 Client(另开终端)

终端 2:

source /opt/ros/humble/setup.bash
source /root/ros2_ws/install/setup.bash

ros2 run py_srv add_two_ints_client a:=12 b:=30

预期输出:

[INFO] [add_two_ints_client]: result: 12 + 30 = 42

手动调用服务(不开 Client)

ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 12, b: 30}"

预期输出:

response: example_interfaces.srv.AddTwoInts_Response(sum=42)

📖 Server 代码

class AddTwoIntsServer(Node):
    def __init__(self):
        super().__init__('add_two_ints_server')
        
        # create_service(消息类型, 服务名, 回调)
        self.service_ = self.create_service(
            AddTwoInts, 'add_two_ints', self._handle_request)
    
    def _handle_request(self, request, response):
        """处理请求的回调。
        
        注意签名: (request, response) -> response
        必须原地修改 response,最后返回它。
        """
        try:
            response.sum = request.a + request.b    # 处理逻辑
            self.get_logger().info(
                f'{request.a} + {request.b} = {response.sum}')
        except Exception as exc:
            self.get_logger().error(f'处理失败: {exc}')
            response.sum = 0
        return response  # 必须返回

回调签名:(request, response) -> response,原地修改 response 然后返回。


📖 Client 代码(异步模式)

class AddTwoIntsClient(Node):
    def __init__(self):
        super().__init__('add_two_ints_client')
        
        # create_client(消息类型, 服务名)
        self._client = self.create_client(AddTwoInts, 'add_two_ints')
    
    def call_once(self, a, b, timeout_sec=5.0):
        """异步调用 + spin 等结果"""
        # 1) 等服务可用(必须轮询!)
        if not self.wait_for_service_ready(timeout_sec):
            self.get_logger().warn('service not available')
            return None
        
        # 2) 构造请求
        request = AddTwoInts.Request()
        request.a = a
        request.b = b
        
        # 3) 异步发请求(立刻返回,不等结果)
        future = self._client.call_async(request)
        
        # 4) spin 直到 future 完成(或超时)
        rclpy.spin_until_future_complete(self, future, timeout_sec=timeout_sec)
        
        # 5) 拿结果
        if not future.done():
            self.get_logger().warn('调用超时')
            return None
        return future.result().sum

call_async 特点:发完请求立刻返回 future,你可以干别的事,最后再 spin_until_future_complete 阻塞等结果。

wait_for_service_ready 轮询模式:

def wait_for_service_ready(self, timeout_sec=5.0):
    deadline = time.time() + timeout_sec
    while time.time() < deadline:
        if self._client.service_is_ready():
            return True
        time.sleep(0.05)  # 让出 CPU,下次再查
    return False

⚠️ 为什么不能在 __init__ / callback 里阻塞等待?

spin 是单线程的:它需要反复执行 callback 才能让系统工作。如果在 callback 里阻塞,spin 就被卡住,其他 callback 全都执行不了

真实场景的"死锁":

# ❌ 错误写法
class BadClient(Node):
    def _on_timer(self):
        # 这是订阅者回调(在 spin 里执行)
        result = self._client.call(request)  # ❌ 阻塞等结果
        # call 内部要 spin,才能把 response callback 跑起来
        # 但我们正在 spin 里 → 永远等不到 response callback 完成
        # → 死锁!节点卡死

正确写法:异步(把响应处理放到 callback,而不是当前函数里阻塞等):

# ✅ 正确写法
class GoodClient(Node):
    def _on_timer(self):
        future = self._client.call_async(request)
        # 立刻返回,不等结果
        future.add_done_callback(self._response_callback)
    
    def _response_callback(self, future):
        # 这个 callback 由 spin 在将来某个时刻调用
        result = future.result()
        # ...处理 result

规律:任何在 callback / __init__ / spin 里的代码,都不能"同步等一个需要 spin 才会发生的事"


🧪 跑测试

colcon test --packages-select py_srv
colcon test-result --all --verbose

预期:py_srv: pytest 7/7 ✓ 全部通过。

测试覆盖:

  • 服务端:节点名 / 服务名 / 处理正负零(3 个)
  • 客户端:能调用服务(1 个)
  • 端到端:同进程跑 server + client 互调(2 个)

🔧 自己写 Service

  1. 定义 .srv 文件:

    # srv/MyService.srv
    string name
    int32 count
    ---
    bool success
    string message
    

    (放在 srv/ 目录下)

  2. setup.py 加一行(data_files):

    data_files=[
        (...),
        (os.path.join('share', PACKAGE_NAME, 'srv'), glob('srv/*.srv')),
    ]
    
  3. 重编译:colcon build

  4. :

    from my_pkg.srv import MyService
    

📚 深入学习


⏭️ 下一个包

学完这个,继续学 py_action_demo — 学习 Action(带进度回调 + 可取消的长任务)。


📍 学习路径导航

⏮ 上一个 🏠 当前位置 ⏭ 下一个
py_pubsub — Python Topic py_srv — Python Service py_action_demo — Python Action

📍 完整 12 包学习顺序见 主 README