ROS2のService通信で、同期的に処理したい (Python)

ROS2のService通信で、レスポンスを受け取ってから次の処理に進む、という同期的な書き方をしたかった。

call() は使いたくない

rclpyには同期版の call() メソッドがあり、これを使えば結果が返るまでブロックできる。ただし、コールバックの中から呼ぶとDead Lockを招きやすい。スピンが止まっている状態でレスポンスを待つことになり、レスポンスが永久に処理されないからである。

Making sure you're not a bot!docs.ros.org

(そのため、そもそもrclcpp(C++)には同期版のAPIが存在せず、非同期版しかサポートされていない)

では call_async() を使うとして、これをスマートに同期的な処理として書けないか。調べてみると main 関数の中で spin_until_future_complete() をゴニョゴニョする例ばかりで、呼び出し箇所とノードの実装が分離してしまい、複雑なコードでは扱いづらい。

結論: async/await とコールバックグループを使う

  • コールバックを async def で定義し、call_async() が返すFutureを await する
  • サービスクライアントを、await する側のコールバックとは別の MutuallyExclusiveCallbackGroup に入れる

await した時点でそのコールバックの実行が中断され、executorのスピンループに制御が戻る。だからスピンが止まらず、レスポンスが処理されてFutureが完了し、コールバックの続きが再開される。クライアントを別のコールバックグループに置くのは、await 中のコールバックと同じグループだとレスポンスの処理が始められないためである。

MultiThreadedExecutor は必要なく、rclpy.spin() のままでよい。

実装例

import rclpy
from rclpy.callback_groups import MutuallyExclusiveCallbackGroup
from rclpy.node import Node
from std_srvs.srv import Empty


class AwaitClient(Node):
    def __init__(self):
        super().__init__('await_client')
        self.cbgroup = MutuallyExclusiveCallbackGroup()
        # サービスクライアントは専用のコールバックグループに入れる
        self._client = self.create_client(Empty, 'delay', callback_group=self.cbgroup)
        # await する側のコールバックはデフォルトのコールバックグループのまま
        self._tmr = self.create_timer(5.0, self.timer_callback)

    async def timer_callback(self):
        self.get_logger().info('calling delay')
        # await した時点で実行が中断され、スピンループに制御が戻る
        await self._client.call_async(Empty.Request())
        self.get_logger().info('done!')


def main(args=None):
    rclpy.init(args=args)
    rclpy.spin(AwaitClient())
    rclpy.shutdown()

main 側には何の仕掛けも要らず、待ちたい箇所にそのまま await と書けるのでかなりスマートに書ける。

以下のリポジトリに、この書き方とDead Lockする書き方が並べて置かれていて分かりやすかった。

async_experiments/async_experiments/async_client.py at 68c9edbb174d5e1c53a19b883b4d0329e2360124 · m-elwin/async_experimentsAsynchronous service client experiments for ros2. Contribute to m-elwin/async_experiments development by creating an account on GitHub.GitHub

参考

Synchronous vs. asynchronous service clients — ROS 2 Documentation

Using callback groups — ROS 2 Documentation: Humble documentation