【ROS×Python】サービス(Service)通信を実装しよう!

 ※ 本記事は広告・プロモーションを含みます。
Python

たっきん(Twitter)です!

今日はROS通信の機能の1つ、「サービス(Service)」通信の機能に絞って解説していきます。

アプリケーションによっては、前回解説したトピック(Topic)通信よりも、こちらのサービス通信の使用頻度のほうが高い場合がありますので、この機会に使いこなせるようになっておきましょう!

ROSメッセージ通信の種類

  • トピック(Topic)
  • サービス(Service) ← 本記事で解説
  • アクション(Action)

サービス(Serive)通信

サービス(Serice)通信は、一言で説明するなら自分以外の他のノードの関数を呼び出すことができる機能になります。

一方向にストリーミング・メッセージを送信し続けるトピックとは異なり、単一のデータを同期的にやり取りするために使用されます。

サービス(Service)通信

サービス定義ファイル

サービス通信を行う際のリクエスト・レスポンス(引数・返り値)はサービス定義ファイル(.srv)で定義する必要があります。

記述方法はトピックのメッセージ定義ファイルと同じく、「データ型」「変数名」のペアで記述し、リクエスト・レスポンス間を”---“で区切る必要があります。

## サービス定義例

# リクエスト(引数)
int32 x
int32 y
---
# レスポンス(返り値)
int32 ans

サービス通信もトピック通信と同様にサービス定義ファイルをもとに各プログラミング言語に対応したアクセスライブラリを自動生成するため、異言語で実装されたノード間でもサービス通信が可能です。

サービス通信プログラムを動かしてみよう!

それでは、実際にサービス通信プログラムを書いて、動作を確認してみましょう!

今回作成するのは「円の半径」を渡すと、その円半径の「円周」「円面積」を返すサービスになります。

サービスを作成するには「サービス定義ファイル(.srv)」と2つのノード「サービス提供ノード(サーバー)」「サービス受領ノード(クライアント)」が必要になります。

サービス定義ファイル(.srv)の作成

まずはサービス定義ファイルを作成しましょう。

サービス定義ファイルの拡張子は.srvになります。

今回作成するサービス定義ファイル名は「Circle.srv」とします。

#--------------------------------------------------
# 円の半径から円周と円面積を返すサービス定義
#--------------------------------------------------

# 円の半径
float32 radius

---
# 円周
float32 circumference

# 円面積
float32 area

サービス定義ファイルを新規追加した場合は忘れずにCMakeLists.txtにも追加したファイル名を追記しましょう!

・・・
・・・
・・・
set(srv_files
  # "srv/***.srv"
  "srv/Circle.srv" ← 追加
)
・・・
・・・
・・・

サービス定義ファイルの作成はこれで完了です。

このあとにcolconビルドすることで、“Circle”というサービス型が使用可能になります。

サービス提供ノード(サーバー)の作成

サービス提供ノード(サーバー)を作成していきます。

import math
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from ros2_example_msgs.srv import Circle


class CircleServer(Node):
    """
    円サービスServerノード
    """

    def __init__(self) -> None:
        """
        ノードの初期化
        """
        super().__init__("circle_server")

        # ロガー取得
        self.logger = self.get_logger()

        # circleサービスserverの定義
        self.srv = self.create_service(
            Circle,
            "circle",
            self._calc_circle,
        )

    def _calc_circle(self, req, rsp):
        """
        circleサービスコールバック
        """
        # 受信requestの中身をログ出力
        self.logger.info("<リクエスト>")
        self.logger.info("  - 半径:[{:.2f}]".format(req.radius))

        # 円周の計算(直径×π)
        rsp.circumference = req.radius * 2 * math.pi
        # 円面積の計算(半径^2×π)
        rsp.area = req.radius**2 * math.pi

        self.logger.info("<レスポンス>")
        self.logger.info("  - 円周:[{:.2f}]".format(rsp.circumference))
        self.logger.info("  - 円面積:[{:.2f}]".format(rsp.area))

        return rsp


def main(args: list[str] | None = None) -> None:
    # ROSの初期化
    rclpy.init(args=args)
    # ohlc_serverノードの作成
    cs = CircleServer()
    cs.get_logger().info("circle_server start!")

    try:
        # ノードの実行開始
        rclpy.spin(cs)
    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    else:
        # ROSのシャットダウン
        rclpy.shutdown()
    finally:
        # ノードの破棄
        cs.destroy_node()

サーバー側のサービスの定義はcreate_service()関数を使用します。

引数には下記を指定します。

  • 第1引数:サービス型
  • 第2引数:サービス名
  • 第3引数:コールバック関数

上記コードでは22~27行目でサービス定義をしており、「Circleサービス型」「circle」という名前のサービスが提供されます。

# circleサービスserverの定義
self.srv = self.create_service(
    Circle,
    "circle",
    self._calc_circle,
)

そして、サービスが呼び出された場合は第3引数のコールバック関数「self._calc_circle()」が呼び出されます。

コールバック関数は29~46行目で定義しています。

def _calc_circle(self, req, rsp):
    """
    circleサービスコールバック
    """
    # 受信requestの中身をログ出力
    self.logger.info("<リクエスト>")
    self.logger.info("  - 半径:[{:.2f}]".format(req.radius))

    # 円周の計算(直径×π)
    rsp.circumference = req.radius * 2 * math.pi
    # 円面積の計算(半径^2×π)
    rsp.area = req.radius**2 * math.pi

    self.logger.info("<レスポンス>")
    self.logger.info("  - 円周:[{:.2f}]".format(rsp.circumference))
    self.logger.info("  - 円面積:[{:.2f}]".format(rsp.area))

    return rsp

コールバック関数の引数はサービス定義ファイルで定義した引数と返り値になります。

  • req:サービスの引数
  • rsp:サービスの返り値

サービス引数req.radiusには、サービス受領ノード(クライアント)で設定された円半径の値が代入されていますので、円周と円面積をそれぞれ計算し、サービス返り値rsp.circumference(円周)とrsp.area(円面積)にそれぞれ計算結果を代入しています。

関数の最後には返り値のリターンreturn rspを忘れずに記入しておきましょう。

サービス受領ノード(クライアント)の作成

サービス受領ノード(クライアント)を作成していきます。

処理内容は円半径をキーボード入力で受け付けて、入力後にサービスを要求して受領した円周、円面積をログ表示する内容になっています。

import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from ros2_example_msgs.srv import Circle


class CircleClient(Node):
    """
    円サービスClientノード
    """

    def __init__(self) -> None:
        """
        ノードの初期化
        """
        super().__init__("circle_client")

        # ロガー取得
        self.logger = self.get_logger()

        # circleサービスclientの定義
        self.cli = self.create_client(Circle, "circle")

        # サービスServerが有効になるまで待機
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.logger.info("サービスServerが有効になるまで待機中・・・")

    def service_call(self, radius: float):
        """
        circleサービス呼び出し(同期)
        """
        self.logger.info("半径:[{:.2f}]".format(radius))

        # circleサービスの引数
        req = Circle.Request()
        # circleサービスの引数に半径の値を設定
        req.radius = radius

        # サービスの非同期実行
        future = self.cli.call_async(req)

        # サービスの非同期実行が完了するまで待機
        rclpy.spin_until_future_complete(self, future)

        return future.result()


def main(args: list[str] | None = None) -> None:
    # ROSの初期化
    rclpy.init(args=args)
    # circle_clientノードの作成
    cc = CircleClient()
    logger = cc.get_logger()
    logger.info("circle_client start!")

    try:
        while rclpy.ok():
            # 半径をキーボード入力
            radius = float(input("input radius: "))
            # サービスリクエストを送信
            rsp = cc.service_call(radius)
            # circleサービスの返り値をログ出力
            if rsp is not None:
                logger.info("----------")
                logger.info("円周:[{:.2f}]".format(rsp.circumference))
                logger.info("円面積:[{:.2f}]".format(rsp.area))

    except (KeyboardInterrupt, ExternalShutdownException):
        pass
    else:
        # ROSのシャットダウン
        rclpy.shutdown()
    finally:
        # ノードの破棄
        cc.destroy_node()

クライアント側のサービスの定義はcreate_client()関数を使用します。

引数には下記を指定します。

  • 第1引数:サービス型
  • 第2引数:サービス名

上記コードでは21~22行目でサービス定義をしています。

このとき、「サービス型」と「サービス名」はサーバー側と同じものを指定します。

# circleサービスclientの定義
self.cli = self.create_client(Circle, "circle")

24~26行目はサービス・サーバー側が起動するまで待機します。

# サービスServerが有効になるまで待機
while not self.cli.wait_for_service(timeout_sec=1.0):
    self.logger.info("サービスServerが有効になるまで待機中・・・")

サービス呼び出しは34~40行目になります。

call_async(req)が非同期での呼び出しとなります。

# circleサービスの引数
req = Circle.Request()
# circleサービスの引数に半径の値を設定
req.radius = radius

# サービスの非同期実行
future = self.cli.call_async(req)

ROSのサービスは基本的に非同期処理となることを覚えておきましょう。

非同期処理のcall_async(req)はサービス側の処理が終わっていなかったとしても即座に返り値をfutureで返します。

そして、futureの状態を定期的にチェックし、サービス処理が完了したタイミングで返り値を受け取ることができます。

そうすることによって、メイン処理を妨げることなくサービスの返り値を受け取ることができるようになります。

43行目のrclpy.spin_until_future_complete()でサービス処理が完了するまで待機します。

待機処理を入れることで、45行目のreturn future.result()で返り値が取得済であることが保証されます。

# サービスの非同期実行が完了するまで待機
rclpy.spin_until_future_complete(self, future)

return future.result()

気づいた方もいるかもしれませんが、call_async(req)rclpy.spin_until_future_complete()の並びで間接的にサービスの同期処理を実現しています。

つまりこの方法は「待ち」によってメイン処理を妨げる要因になるため、好ましい実装方法とは言えません。

クラスのコンストラクタのように、最初の1回しか呼び出されない初期化処理等では使用しても問題ないと思いますが、メインルーチン処理内では使用しないようにしましょう。

僕が推奨するサービス非同期通信実装方法は次のページで紹介しますね!

実行してみる

サーバー側とクライアント側を実行してみます。

クライアント側のキーボード入力で半径:”5″を入力すると、半径5の円周と円面積が返されます。

<サーバー側>
$ ros2 run ros2_example circle_server
[circle_server]: circle_server start!
[circle_server]: <リクエスト>
[circle_server]:   - 半径:[5.00]
[circle_server]: <レスポンス>
[circle_server]:   - 円周:[31.42]
[circle_server]:   - 円面積:[78.54]
<クライアント側>
$ ros2 run ros2_example circle_client
circle_client start!
input radius: 5 ← キーボード入力
[circle_client]: 半径:[5.00]
[circle_client]: ----------
[circle_client]: 円周:[31.42]
[circle_client]: 円面積:[78.54]

ただし、今回作成したプログラムはrclpy.spin_until_future_complete()「待ち」を使った好ましくない実装例になります。

さらに、rclpy.spin_until_future_complete()rclpy.spin()実行中だと使用できません。

rclpy.spin_until_future_complete()rclpy.spin()もどちらもspin処理となるのですが、spin処理の多重実行はできないためです。

多重実行した場合、コールバック関数の呼び出しがブロックされることがあり、デットロックが発生する場合があります。

ではどのようにクライアント側のサービス処理を実装するのが好ましいか?

次のページで推奨実装方法を紹介していきます。

スポンサーリンク

コメント

タイトルとURLをコピーしました