【ROS×Python】アクション(Action)通信を実装しよう!

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

たっきん(Twitter)です!

これまでROS通信機能について、トピック(Topic)、サービス(Service)と解説をしてきました。

今回はROS通信3つ目の機能である「アクション(Action)」通信について解説していきます。

正直アクションの使用頻度は高くないと思いますが、使いどころはありますので知っていて損はないと思います。

サンプルコードを使って説明していきますので、この機会にアクションを使えるようになっておきましょう!

ROSメッセージ通信の種類

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

アクション(Action)通信

アクション(Action)通信は、サービス(Service)とトピック(Topic)を合体させたような機能になります。

サービスのように引数(目標値)と返り値(実行結果)があり、さらにアクション処理が開始してから処理が完了し、実行結果が返されるまでの途中段階の値(フィードバック)をトピックのように受信することができます。

また、アクション処理実行中に処理の中断要求を出すことで、アクション処理を中断させることもできます。

中断機能はトピックやサービスにはなく、アクション独自の機能になってきます。

アクション(Action)通信

アクション定義ファイル

アクション通信を行う際の目標値(引数)、実行結果(返り値)、フィードバックはアクション定義ファイル(.action)で定義する必要があります。

アクション定義ファイルにはGoal, Result, Feedbackの3要素を記述します。

  • Goal:
    • アクションの目標値。
      サービスでいうところのリクエスト(引数)に該当します。
  • Result:
    • アクションの実行結果。
      サービスでいうところのレスポンス(返り値)に該当します。
  • Feedback:
    • アクション実行途中の値。
      アクション独自の要素です。

各要素間は”---“で区切って記述します。

## アクション定義例

# Goal
int32 order
---
# Result
int32[] sequence
---
# Feedback
int32[] sequence

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

アクション通信プログラムを動かしてみよう!

それでは、実際にアクション通信プログラムを書いて、動作を確認してみましょう!

今回作成するのは目標値(Goal)に「区切り範囲」「為替レート」リストを渡すと、区切り範囲で計算された為替レートの「単純移動平均計算結果」を実行結果(Result)として返すアクションになります。

また、計算途中の「進捗[%]」はフィードバックとして定期的に送信します。

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

以降で紹介するサンプルコードは全てROSパッケージ化した状態でGitHubに載せています。

自分の環境で実行して挙動を確認してみたい場合は下記からクローンして使用してください。

本ページで使用するコード

アクション定義ファイル(.action)の作成

まずはアクション定義ファイルを作成しましょう。

アクション定義ファイルの拡張子は.actionになります。

今回作成するアクション定義ファイル名は「SimpleMovingAverage.action」とします。

#--------------------------------------------------
# 為替レートリストから単純移動平均を計算した結果を返す
# アクション定義
#--------------------------------------------------

##### Goal #####
# 区切り範囲
int32 window

# 為替レートリスト
float32[] price_raw_list

---
##### Result #####
# 単純移動平均計算結果
float32[] price_sma_list

---
##### Feedback #####
# 進捗[%]
int32 progress

アクション定義ファイルを新規追加した場合は忘れずにCMakeLists.txtにも追加したファイル名を追記しましょう!

・・・
・・・
・・・
set(act_files
  # "action/***.action"
  "action/SimpleMovingAverage.action"
)
・・・
・・・
・・・

アクション定義ファイルの作成はこれで完了です。

このあとにcolconビルドすることで、“SimpleMovingAverage”というアクション型が使用可能になります。

アクション提供ノード(サーバー)の作成

アクション提供ノード(サーバー)を作成していきます。

import time
import numpy as np
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from rclpy.action import ActionServer
from rclpy.action import GoalResponse
from ros2_example_msgs.action import SimpleMovingAverage


class SimpleMovingAverageServer(Node):
    """
    単純移動平均計算アクションServerノード
    """

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

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

        # smaアクションserverの定義
        self.act_srv = ActionServer(
            self,
            SimpleMovingAverage,
            "sma",
            self._execute_callback,
            goal_callback=self._goal_callback,
        )

    def _goal_callback(self, goal_request) -> GoalResponse:
        """
        アクション・Goal要求コールバック
        """
        self.logger.info("<アクション:Goal受信>")
        self.logger.info(" 区切り範囲(window):[{}]".format(goal_request.window))
        self.logger.info(" データ長:[{}]".format(len(goal_request.price_raw_list)))

        if goal_request.window < 1:
            self.logger.warn(" Goal Request: REJECT")
            self.logger.warn("  区切り範囲(window)が0以下です。")
            return GoalResponse.REJECT

        if len(goal_request.price_raw_list) < goal_request.window:
            self.logger.warn(" Goal Request: REJECT")
            self.logger.warn("  データ長が区切り範囲(window)より小さいです。")
            return GoalResponse.REJECT

        self.logger.info(" Goal Request: ACCEPT")
        return GoalResponse.ACCEPT

    def _execute_callback(self, goal_handle):
        """
        アクション実行コールバック
        """
        # アクションの実行
        self.logger.info("アクション実行中・・・")

        # Feedbackメッセージの定義
        fb_msg = SimpleMovingAverage.Feedback()
        fb_msg.progress = 0

        # Resultの定義
        result = SimpleMovingAverage.Result()

        goal = goal_handle.request
        price_len = len(goal.price_raw_list)
        prg_rate = float(100.0 / (price_len - goal.window + 1))

        # リスト位置 0〜goal.window-1 は計算不可のためNaNで埋める
        for i in range(0, goal.window - 1):
            result.price_sma_list.append(np.nan)

        # リスト位置 goal.window-1 以降で計算
        for i in range(goal.window, price_len + 1):
            # 移動平均の計算
            price_sum = 0.0
            for j in range(i - goal.window, i):
                price_sum += goal.price_raw_list[j]
            result.price_sma_list.append(price_sum / goal.window)

            # 0.5秒待機(重たい処理の代わり)
            time.sleep(0.5)

            # アクションのフィードバックの送信
            fb_msg.progress = int(prg_rate * (i - goal.window + 1))
            goal_handle.publish_feedback(fb_msg)
            self.logger.info("フィードバック送信:進捗{}%".format(fb_msg.progress))

        # アクションステータスに"成功"をセット
        goal_handle.succeed()
        self.get_logger().info("Goal成功")
        # アクション実行結果を送信
        return result


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

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

サーバー側のアクションはActionServer()クラスのインスタンスを生成することで定義します。

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

  • 第1引数:ノード
  • 第2引数:アクション型
  • 第3引数:アクション名
  • 第4引数:アクション実行コールバック関数

上記引数とは別にオプション引数として目標値(Goal)受信時に呼び出されるコールバック関数goal_callbackをセットすることもできます。

上記コードでは25~32行目でアクション定義をしており、「SimpleMovingAverageアクション型」「sma」という名前のアクションが提供されます。

# smaアクションserverの定義
self.act_srv = ActionServer(
    self,
    SimpleMovingAverage,
    "sma",
    self._execute_callback,
    goal_callback=self._goal_callback,
)

アクションの目標値(Goal)を受信したときにgoal_callbackをセットしていた場合は本関数が最初に呼び出されます。

今回はself._goal_callbackをセットしているため、この関数が実行されます。

def _goal_callback(self, goal_request) -> GoalResponse:
    """
    アクション・Goal要求コールバック
    """
    self.logger.info("<アクション:Goal受信>")
    self.logger.info(" 区切り範囲(window):[{}]".format(goal_request.window))
    self.logger.info(" データ長:[{}]".format(len(goal_request.price_raw_list)))

    if goal_request.window < 1:
        self.logger.warn(" Goal Request: REJECT")
        self.logger.warn("  区切り範囲(window)が0以下です。")
        return GoalResponse.REJECT

    if len(goal_request.price_raw_list) < goal_request.window:
        self.logger.warn(" Goal Request: REJECT")
        self.logger.warn("  データ長が区切り範囲(window)より小さいです。")
        return GoalResponse.REJECT

    self.logger.info(" Goal Request: ACCEPT")
    return GoalResponse.ACCEPT

本関数では受信した目標値(Goal)で処理可能か否か判断を行い、処理可能であればGoalResponse.ACCEPTを、処理不可能であればGoalResponse.REJECTをreturnします。

GoalResponse.REJECTをreturnした場合は、この時点でアクション処理中断となります。

GoalResponse.ACCEPTをreturnした場合は、第4引数のコールバック関数self._execute_callback()が呼び出され、アクション処理が実行されます。

ActionServer()定義時にgoal_callback関数のセットを省略した場合はデフォルトコールバック関数default_goal_callback()が呼び出されます。

デフォルトコールバック関数は常にGoalResponse.ACCEPTをreturnしているため、受信した目標値(Goal)で常にアクション処理を実施しても問題なければ、goal_callbackのセットは省略してしまっても良いです。

def default_goal_callback(goal_request):
    """Accept all goals."""
    return GoalResponse.ACCEPT

アクション処理のself._execute_callback()関数は55~97行目で定義しています。

アクション実行コールバック関数の処理内容をもう少し詳しく見ていきましょう!

目標値(Goal)

アクション定義ファイルで定義した目標値(Goal)にアクセスするには引数からgoal_handle.request.***のようにします。

今回の例ですと、下記のようになります。

  • 区切り範囲:goal_handle.request.window
  • 為替レートリスト:goal_handle.request.price_raw_list[]

実行結果(Result)

サービスの場合、レスポンス(返り値)はコールバック関数の引数として定義されていましたが、アクションの場合、実行結果(Result)はアクション型から定義する必要があります。

上記コードでは66~67行目で定義しています。

# Resultの定義
result = SimpleMovingAverage.Result()

変数へのアクセスはresult.***のようにします。

今回の例ですとresult.price_sma_listとなります。

上記コードでは79~83行目で移動平均を算出し、price_sma_listに結果を代入しています。

# 移動平均の計算
price_sum = 0.0
for j in range(i - goal.window, i):
    price_sum += goal.price_raw_list[j]
result.price_sma_list.append(price_sum / goal.window)

実行結果の送信はreturnを使用します。

上記コードでは96~97行目でreturnしています。

# アクション実行結果を送信
return result

実行途中の値(Feedback)

アクション独自機能の実行途中の値(Feedback)も実行結果(result)と同様にアクション型から定義する必要があります。

上記コードでは62~63行目で記述しています。

# Feedbackメッセージの定義
fb_msg = SimpleMovingAverage.Feedback()

変数へのアクセスはfb_msg.***のようにします。

今回の例ですとfb_msg.progressとなります。

そしてFeedbackの送信はgoal_handle.publish_feedback(fb_msg)と記述します。

上記コードでは88~91行目で記述しています。

# アクションのフィードバックの送信
fb_msg.progress = int(prg_rate * (i - goal.window + 1))
goal_handle.publish_feedback(fb_msg)
self.logger.info("フィードバック送信:進捗{}%".format(fb_msg.progress))

Goalステータスの設定

アクションにはもう1つサービスにはない独自の機能があり、Goalステータスを設定することができます。

設定したステータスはクライアント側へも送信されるので、クライアント側はこのステータスに応じて挙動を決めることができるようになります。

Goalステータスは以下の種類があります。

定数名内容
STATUS_UNKNOWNステータス未セット
STATUS_ACCEPTEDGoal受け入れ完了
AND
アクション未実行
STATUS_EXECUTINGアクション実行中
STATUS_CANCELINGクライアントによる
キャンセル要求受け入れ完了
STATUS_SUCCEEDEDアクション実行成功
STATUS_CANCELEDキャンセル完了
STATUS_ABORTEDクライアント要求以外による
アクション実行中止
引用:GoalStatus.html

アクション・サーバー側では下記3つのステータス更新専用の関数が用意されています。

ステータス更新関数更新後ステータス
goal_handle.succeed()STATUS_SUCCEEDED
goal_handle.canceled()STATUS_CANCELED
goal_handle.abort()STATUS_ABORTED

上記コードでは93~94行目でステータスを「成功」にセットしています。

アクション実行の成功が確定したタイミングでステータスを更新するようにしましょう!

# アクションステータスに"成功"をセット
goal_handle.succeed()

アクション受領ノード(クライアント)の作成

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

単純移動平均計算用の生データ配列は固定値ですが、区切り範囲(window size)はキーボード入力で任意に指定できようにしました。

import matplotlib.pyplot as plt
import rclpy
from rclpy.executors import ExternalShutdownException
from rclpy.node import Node
from rclpy.task import Future
from rclpy.action import ActionClient
from action_msgs.msg import GoalStatus
from ros2_example_msgs.action import SimpleMovingAverage


class SimpleMovingAverageClient(Node):
    """
    単純移動平均計算アクションClientノード
    """

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

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

        # smaアクションclientの定義
        self.act_cli = ActionClient(self, SimpleMovingAverage, "sma")

        # アクションServerが有効になるまで待機
        while not self.act_cli.wait_for_server(timeout_sec=1.0):
            self.logger.info("アクションServerが有効になるまで待機中・・・")

        # Future初期化
        self._goal_future = Future()
        self._result_future = Future()

        self._price_raw_list = []
        self._window = 0

    def send_action_goal_async(self, window: int, price_raw_list: list[float]) -> None:
        """
        アクション・ゴールの送信(非同期)
        """
        # smaアクションの引数
        goal_msg = SimpleMovingAverage.Goal()
        goal_msg.window = window
        for price_raw in price_raw_list:
            goal_msg.price_raw_list.append(price_raw)

        # アクションの非同期実行
        self.logger.info("<アクション:Goal送信>")
        self._goal_future = self.act_cli.send_goal_async(
            goal_msg, self._feedback_callback
        )
        self._goal_future.add_done_callback(self._goal_response_callback)

        self._price_raw_list = price_raw_list
        self._window = window

    def _feedback_callback(self, feedback) -> None:
        """
        アクション・フィードバックコールバック
        """
        self.logger.info("フィードバック受信:進捗{}%".format(feedback.feedback.progress))

    def _goal_response_callback(self, future) -> None:
        """
        アクション・Goal応答コールバック
        """
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.logger.info(" Goal拒否されました。")
            # Shutdown after receiving a result
            raise ExternalShutdownException

        self.logger.info(" Goal受理されました。")
        self._result_future = goal_handle.get_result_async()
        self._result_future.add_done_callback(self._get_result_callback)

    def _get_result_callback(self, future) -> None:
        """
        アクション・Result応答コールバック
        """
        result = future.result().result
        status = future.result().status
        if status == GoalStatus.STATUS_SUCCEEDED:
            self.logger.info("Goal成功")

            # ********** 描写 **********
            label_sma = "SMA(window:" + str(self._window) + ")"
            plt.plot(self._price_raw_list, label="Raw value")
            plt.scatter(range(len(self._price_raw_list)), self._price_raw_list)
            plt.plot(result.price_sma_list, label=label_sma)
            plt.scatter(range(len(result.price_sma_list)), result.price_sma_list)
            plt.legend()
            plt.title("Simple Moving Average(SMA)")
            plt.grid(linestyle="dashed")
            plt.show()
        else:
            self.logger.info("Goal失敗 (status: {})".format(status))

        # Shutdown after receiving a result
        raise ExternalShutdownException


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

    # 単純移動平均計算用の生データ
    price_raw_list = [
        100.0,
        102.0,
        105.0,
        112.0,
        120.0,
        122.0,
        118.0,
        110.0,
        98.0,
        88.0,
        85.0,
        90.0,
        110.0,
        125.0,
    ]

    # 区切り範囲をキーボード入力
    window = int(input("windows size: "))

    # アクション・ゴールの送信(非同期)
    mac.send_action_goal_async(window, price_raw_list)

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

クライアント側のアクションはActionClient()クラスのインスタンスを生成することで定義します。

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

  • 第1引数:ノード
  • 第2引数:アクション型
  • 第3引数:アクション名

上記コードでは25~26行目でアクション定義をしています。

このとき、「アクション型」と「アクション名」はサーバー側と同じものを指定します。

# smaアクションclientの定義
self.act_cli = ActionClient(self, SimpleMovingAverage, "sma")

28~30行目はアクション・サーバーが起動するまで待機する処理になります。

# アクションServerが有効になるまで待機
while not self.act_cli.wait_for_server(timeout_sec=1.0):
    self.logger.info("アクションServerが有効になるまで待機中・・・")

アクション呼び出し(Goal送信)は49~53行目になります。

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

# アクションの非同期実行
self.logger.info("アクション:Goal送信")
self._goal_future = self.act_cli.send_goal_async(
    goal_msg, self._feedback_callback
)

第2引数はFeedbackのコールバック関数を設定します。

こうすることで、Feedbackを受信する度にコールバック関数が呼び出されるようになります。

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

def _feedback_callback(self, feedback) -> None:
    """
    アクション・フィードバックコールバック
    """
    self.logger.info("フィードバック受信:進捗{}%".format(feedback.feedback.progress))

send_goal_async()はサービスのcall_async()に相当する関数になります。

この関数も非同期処理となるるため、サービスと同じくfutureが返されるので、

非同期でアクション処理結果を受け取ることになります。

54行目ではサーバー側でGoalResponseACCEPT or REJECT)がreturnされた際に呼び出されるコールバック関数self._goal_response_callbackをセットしています。

self._goal_future.add_done_callback(self._goal_response_callback)

self._goal_response_callbackコールバック関数の定義は65~77行目になります。

def _goal_response_callback(self, future) -> None:
    """
    アクション・Goal応答コールバック
    """
    goal_handle = future.result()
    if not goal_handle.accepted:
        self.logger.info(" Goal拒否されました。")
        # Shutdown after receiving a result
        raise ExternalShutdownException

    self.logger.info(" Goal受理されました。")
    self._result_future = goal_handle.get_result_async()
    self._result_future.add_done_callback(self._get_result_callback)

REJECTがreturnされた場合は、このまま処理を終了します。

ACCEPT がreturnされた場合は、サーバー側はアクション処理を実行するため、実行結果(Result)を非同期で受け取るためのself._result_futureとそのコールバック関数self._get_result_callbackをセットします。

self._get_result_callbackコールバック関数の定義は79~102行目になります。

def _get_result_callback(self, future) -> None:
    """
    アクション・Result応答コールバック
    """
    result = future.result().result
    status = future.result().status
    if status == GoalStatus.STATUS_SUCCEEDED:
        self.logger.info("Goal成功")

        # ********** 描写 **********
        label_sma = "SMA(window:" + str(self._window) + ")"
        plt.plot(self._price_raw_list, label="Raw value")
        plt.scatter(range(len(self._price_raw_list)), self._price_raw_list)
        plt.plot(result.price_sma_list, label=label_sma)
        plt.scatter(range(len(result.price_sma_list)), result.price_sma_list)
        plt.legend()
        plt.title("Simple Moving Average(SMA)")
        plt.grid(linestyle="dashed")
        plt.show()
    else:
        self.logger.info("Goal失敗 (status: {})".format(status))

    # Shutdown after receiving a result
    raise ExternalShutdownException

上記処理内ではまず最初にサーバー側で設定されたGoalステータスをチェックして、「成功」がセットされていた場合のみ、為替レートとサーバーから受け取った単純移動平均計算結果をグラフ描写しています。

さて、クライアント側ではいくつかコールバック関数が出てきましたので少し整理していきましょう。

各コールバック関数の呼び出しタイミングは下記の通りとなります。

コールバック関数呼び出しタイミング
self._goal_response_callback()目標値(Goal)要求に対する応答をサーバーから受信したとき
self._feedback_callback()サーバーからフィードバック・メッセージを受信したとき
self._get_result_callback()実行結果(Result)をサーバーから受信したとき

実行してみる

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

今回はクライアントから送信された目標値(Goal)がサーバー側でACCEPT される場合とREJECTされる場合の2パターンを実施してみます。

“ACCEPT”される場合

クライアント側のキーボード入力で区切り範囲:”3″を入力した場合の実行結果です。

<サーバー側>
$ ros2 run ros2_example sma_server
[sma_server]: sma_server start!
[sma_server]: <アクション:Goal受信>
[sma_server]:  区切り範囲(window):[3]
[sma_server]:  データ長:[14]
[sma_server]:  Goal Request: ACCEPT
[sma_server]: アクション実行中・・・
[sma_server]: フィードバック送信:進捗8%
[sma_server]: フィードバック送信:進捗16%
[sma_server]: フィードバック送信:進捗25%
[sma_server]: フィードバック送信:進捗33%
[sma_server]: フィードバック送信:進捗41%
[sma_server]: フィードバック送信:進捗50%
[sma_server]: フィードバック送信:進捗58%
[sma_server]: フィードバック送信:進捗66%
[sma_server]: フィードバック送信:進捗75%
[sma_server]: フィードバック送信:進捗83%
[sma_server]: フィードバック送信:進捗91%
[sma_server]: フィードバック送信:進捗100%
[sma_server]: Goal成功
<クライアント側>
$ ros2 run ros2_example sma_client_async
[sma_client_async]: sma_client start!
windows size: 3 ← キーボード入力
[sma_client_async]: <アクション:Goal送信>
[sma_client_async]:  Goal受理されました。
[sma_client_async]: フィードバック受信:進捗8%
[sma_client_async]: フィードバック受信:進捗16%
[sma_client_async]: フィードバック受信:進捗25%
[sma_client_async]: フィードバック受信:進捗33%
[sma_client_async]: フィードバック受信:進捗41%
[sma_client_async]: フィードバック受信:進捗50%
[sma_client_async]: フィードバック受信:進捗58%
[sma_client_async]: フィードバック受信:進捗66%
[sma_client_async]: フィードバック受信:進捗75%
[sma_client_async]: フィードバック受信:進捗83%
[sma_client_async]: フィードバック受信:進捗91%
[sma_client_async]: フィードバック受信:進捗100%
[sma_client_async]: Goal成功

アクション処理の実行が終了すると下図のように単純移動平均(SMA)計算結果が描写されます。

“REJECT”される場合

クライアント側のキーボード入力で区切り範囲:”-1″を入力した場合の実行結果です。

こちらはGoal受信時にサーバーは拒否応答するので、アクション処理は実施されません。

<サーバー側>
$ ros2 run ros2_example sma_server
[sma_server]: sma_server start!
[sma_server]: <アクション:Goal受信>
[sma_server]:  区切り範囲(window):[-1]
[sma_server]:  データ長:[14]
[sma_server]:  Goal Request: REJECT
[sma_server]:   区切り範囲(window)が0以下です。
<クライアント側>
$ ros2 run ros2_example sma_client_async
[sma_client_async]: sma_client start!
windows size: -1 ← キーボード入力
[sma_client_async]: <アクション:Goal送信>
[sma_client_async]:  Goal拒否されました。

まとめ

ROSアクション(Action)

  • アクション提供ノード(サーバー)
    • ActionServer()でアクション・サーバー定義
      • 第1引数:ノード
      • 第2引数:アクション型
      • 第3引数:アクション名
      • 第4引数:コールバック関数
      • オプション引数:目標値(Goal)受信コールバック関数
    • 目標値(Goal)受信コールバック関数のreturn値
      • GoalResponse.REJECT(アクション処理中止)
      • GoalResponse.ACCEPT(アクション処理実施)
    • フィードバックの送信
      • goal_handle.publish_feedback(fb_msg)
  • アクション受領ノード(クライアント)
    • ActionClient()でアクション・クライアント定義
      • 第1引数:ノード
      • 第2引数:アクション型
      • 第3引数:アクション名
    • send_goal_async()で目標値の送信(非同期アクション実行)
    • 各種コールバック関数と呼び出しタイミング
      • goal_response_callback()
        • 目標値(Goal)要求に対する応答をサーバーから受信したとき
      • feedback_callback()
        • サーバーからフィードバック・メッセージを受信したとき
      • get_result_callback()
        • 実行結果(Result)をサーバーから受信したとき
スポンサーリンク

コメント

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