たっきん(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_ACCEPTED | Goal受け入れ完了 AND アクション未実行 |
STATUS_EXECUTING | アクション実行中 |
STATUS_CANCELING | クライアントによる キャンセル要求受け入れ完了 |
STATUS_SUCCEEDED | アクション実行成功 |
STATUS_CANCELED | キャンセル完了 |
STATUS_ABORTED | クライアント要求以外による アクション実行中止 |
アクション・サーバー側では下記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行目ではサーバー側でGoalResponse
(ACCEPT
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)をサーバーから受信したとき
- goal_response_callback()
コメント