ROS2の非同期parameterクライアントについて

メモ:未整理の情報

ここではparameterに関するプログラムを作成する．
特にparameter clientとして非同期に取得・設定を行うAsyncParameterClientと同期的に行うSyncParameterClientを紹介する．

基本的には，宣言・値の設定・取得となり，それぞれ以下の関数となる．詳しくは以下の通り．

- 宣言
    - declare_parameter
        - パラメータの宣言
        - dashing diademataから宣言なしでは値の設定・取得ができなくなった．これにより「パラメータの名前を打ち間違っちゃって似た名前のものが複数できる」などがなくなった．
    - undeclare_parameter
        - パラメータの無効化
    - has_parameter
        - パラメータの存在確認
- 値の設定
    - set_parameters
- 値の取得
    - get_parameter
    - get_parameters

~~2019/6/13現在，[公式のAPI][]には無いようである．~~ 2023/01/21 存在を確認．
[公式のRoadmap][]に詳しく書いてある．
もしくは[公式githubのrcl_interfaces][]．

この他，parameter serviceや参照しているparameterが変更された場合に処理を行うcallback関数の機能，パラメータの権限に関するあれやこれやなどあるが，future worksで．

# 準備

```Bash:terminal
$ cd ~/ros2_studies_ws/
$ ros2 pkg create minimal_parameter_class
```

# ROS風 parameter
- src/ros1_like_parameter_async_user_main.cpp
    - param_holderに接続し，値を参照したり設定したりするnode
- src/ros1_like_parameter_sync_user_main.cpp
    - param_holderに接続し，値を参照したり設定したりするnode

## プログラム : src/ros1_like_parameter_holder_main.cpp

```c++:src/ros1_like_parameter_holder_main.cpp
#include <rclcpp/rclcpp.hpp>
#include <chrono>

int main(int argc, char * argv[]){
  using namespace std::chrono_literals;
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("param_holder");

  auto param1 = node->declare_parameter("foo",0);
  auto param2 = node->declare_parameter("bar", "ok");
  auto results = node->set_parameters({
    rclcpp::Parameter("foo",2),
    rclcpp::Parameter("bar","hello")
  });
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}
```

### 概要
パラメータを保持するnodeとして作成．
nodeを作り，保持するパラメータを準備し，spinでnodeを実行したままにする．
このnode自身がパラメータを定義し，保持・管理する(nodeがserviceとなっている)．よって，declare_parameterやget_parameter,setparametersなどはnodeが行っている(後述するclient側ではnodeがget,setしていないことに注意)．またwait_for_serviceもしていない(service元のnodeが自分なので接続待ちをしない)．

### 説明
7行目:nodeの設定．名前をparam_holderとする．
9～14行目:設定するparameterの宣言．名前だけでも良いし，デフォルト値を設定してもよい．
11～14行目:設定するparameterの準備．
15行目:parameterを持つnodeの実行．

### Parameter
set_parameters関数の中でParameterを使用している．また[公式のAPI][]にも記述があるが補足．

パラメータはキー(要素名?)と値の対にて表される．辞書型の変数．ここで，ROS2ではParameterクラスとParameterValueクラスが存在し，両方ともパラメータを表すのに使えそう．[公式のAPI][]によるとParameterValueはパラメータを保持するためのクラスで，Parameterも同じ．ただParameterクラスはget, setに関する関数テンプレートを使っていて任意のパラメータを扱えるよう．

## プログラム : src/ros1_like_parameter_async_user_main.cpp

```c++:src/ros1_like_parameter_async_user_main.cpp
#include <rclcpp/rclcpp.hpp>
#include <chrono>

int main(int argc, char * argv[]){
  using namespace std::chrono_literals;
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("param_async_user");
  auto param_client = std::make_shared<rclcpp::AsyncParametersClient>(node, "param_holder");
  std::stringstream ss;

  while(!param_client->wait_for_service(1s)){
    if(!rclcpp::ok()){
      RCLCPP_ERROR(node->get_logger(), "Interrupted while waiting for the service.");
      return 1;
    }
    RCLCPP_INFO(node->get_logger(), "waiting for service...");
  }

  auto parameters_future = param_client->get_parameters({"foo", "bar"});
  if(rclcpp::spin_until_future_complete(node, parameters_future) != rclcpp::executor::FutureReturnCode::SUCCESS){
    RCLCPP_ERROR(node->get_logger(), "Failed to get param list.");
    return 1;
  }
  for(auto &param : parameters_future.get()){
    ss << "\nParameter name:" << param.get_name();
    ss << "\nParameter type:" << param.get_type_name();
    ss << "\nParameter value:" << param.value_to_string();
  }
  RCLCPP_INFO(node->get_logger(), ss.str().c_str());
  ss.str("");
  ss.clear(std::stringstream::goodbit);

  auto list_future = param_client->list_parameters({"foo","bar"},10);
  if(rclcpp::spin_until_future_complete(node, list_future) != rclcpp::executor::FutureReturnCode::SUCCESS){
    RCLCPP_ERROR(node->get_logger(), "Failed to get param list.");
    return 1;
  }
  for(auto &param_name : list_future.get().names){
    ss << "\n" << param_name;
  }
  for(auto &prefix : list_future.get().prefixes){
    ss << "\n" <<  prefix;
  }
  RCLCPP_INFO(node->get_logger(), ss.str().c_str());
  ss.str("");
  ss.clear(std::stringstream::goodbit);

  auto results_future = param_client->set_parameters({
    rclcpp::Parameter("foo",3),
    rclcpp::Parameter("bar","welcome")
    //rclcpp::Parameter("bar",10) // OK:別種のものを代入することは可能
    //rclcpp::Parameter("hoge","hige") // NG:パラメータを増やすことは無理
  });
  if(rclcpp::spin_until_future_complete(node, results_future) != rclcpp::executor::FutureReturnCode::SUCCESS){
    RCLCPP_ERROR(node->get_logger(), "Failed to get param list.");
    return 1;
  }
  for(auto &result : results_future.get()){
    if(!result.successful){
      RCLCPP_ERROR(node->get_logger(), "Failed to set parameter: %s", result.reason.c_str());
    }
  }

  parameters_future = param_client->get_parameters({"foo", "bar"});
  if(rclcpp::spin_until_future_complete(node, parameters_future) != rclcpp::executor::FutureReturnCode::SUCCESS){
    RCLCPP_ERROR(node->get_logger(), "Failed to get param list.");
    return 1;
  }
  for(auto &param : parameters_future.get()){
    ss << "\nParameter name:" << param.get_name();
    ss << "\nParameter type:" << param.get_type_name();
    ss << "\nParameter value:" << param.value_to_string();
  }
  RCLCPP_INFO(node->get_logger(), ss.str().c_str());
  ss.str("");
  ss.clear(std::stringstream::goodbit);

  rclcpp::shutdown();
  return 0;
}
```

### 概要
param_holderに接続し，そのnodeが持っているパラメータを参照・変更・また参照するプログラム．
パラメータを取得・変更する場合，パラメータを保持しているnodeと接続しなければいけない．そのため，8行目のようにparameter clientとしての設定を行っている．引数は二つであり，一つ目がAsyncParametersClientの機能を持たせるnodeへのポインタ，二つ目がparameterのデータを保持しているparameter nodeの名前(serviceとなるnodeの名前)である．
また，今回はAsync(非同期)型となっているので，参照・変更を有効にするときにspin_until_future_completeを使用している．このspinによって返ってくるのはFutureオブジェクトなので，処理結果はget関数で行っている．

### 説明
8行目:AsyncParametersClientとして設定．二つ目の引数で参照するparameter nodeの名前を渡している．
19行目:取得するparameterの設定．キーで指定．
20～23行目:spinによってparameter取得を実行．できなかったら(rclcpp::executor::FutureReturnCode::SUCCESSでなかったら)エラー処理となっている．
24～28行目:spinによって返ってくるのはFutureオブジェクト．よってparameterの取得結果(処理結果)はget関数で得る．複数のparameterを取得しているのでfor文で回している．
33～46行目:parameterのlist．詳細情報？33行目の二つ目の引数10は深さで，対象parameterの名前を深さ10まで取得するらしい．
48～62行目:paramter設定．値を変えることは可能(49,50行目)．また値の種類を超えて変えることも可能(51行目)．しかし新しいキーを設定することはできない模様(エラーにはならない)．
64～76行目:parameter取得．

## プログラム : src/ros1_like_parameter_sync_user_main.cpp

```c++:src/ros1_like_parameter_sync_user_main.cpp
#include <rclcpp/rclcpp.hpp>
#include <chrono>

int main(int argc, char * argv[]){
  using namespace std::chrono_literals;
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("param_sync_user");
  auto param_client = std::make_shared<rclcpp::SyncParametersClient>(node, "param_holder");
  std::stringstream ss;

  while(!param_client->wait_for_service(1s)){
    if(!rclcpp::ok()){
      RCLCPP_ERROR(node->get_logger(), "Interrupted while waiting for the service.");
      return 1;
    }
    RCLCPP_INFO(node->get_logger(), "waiting for service...");
  }

  auto parameters_get_results = param_client->get_parameters({"foo", "bar"});
  for(auto &param : parameters_get_results){
    ss << "\nParameter name:" << param.get_name();
    ss << "\nParameter type:" << param.get_type_name();
    ss << "\nParameter value:" << param.value_to_string();
  }
  RCLCPP_INFO(node->get_logger(), ss.str().c_str());
  ss.str("");
  ss.clear(std::stringstream::goodbit);

  auto list_results = param_client->list_parameters({"foo","bar"},10);
  for(auto &param_name : list_results.names){
    ss << "\n" << param_name;
  }
  for(auto &prefix : list_results.prefixes){
    ss << "\n" <<  prefix;
  }
  RCLCPP_INFO(node->get_logger(), ss.str().c_str());
  ss.str("");
  ss.clear(std::stringstream::goodbit);

  auto parameter_set_results = param_client->set_parameters({
    rclcpp::Parameter("foo",3),
    rclcpp::Parameter("bar","welcome")
    //rclcpp::Parameter("bar",10) // OK:別種のものを代入することは可能
//    rclcpp::Parameter("hoge","hige") // NG:パラメータを増やしても無効?
  });
  for(auto &result : parameter_set_results){
    if(!result.successful){
      RCLCPP_ERROR(node->get_logger(), "Failed to set parameter: %s", result.reason.c_str());
    }
  }

  parameters_get_results = param_client->get_parameters({"foo", "bar"});
  for(auto &param : parameters_get_results){
    ss << "\nParameter name:" << param.get_name();
    ss << "\nParameter type:" << param.get_type_name();
    ss << "\nParameter value:" << param.value_to_string();
  }
  RCLCPP_INFO(node->get_logger(), ss.str().c_str());
  ss.str("");
  ss.clear(std::stringstream::goodbit);

  rclcpp::shutdown();
  return 0;
}
```

### 概要
Sync(同期)型のプログラム．Async型と異なり，spin関数を用いなくても，get_parameters, set_parametersなどを実行したらすぐに取得・設定が行われる．そのためクラス化したnodeの場合にはSync型を使用することになる(コンストラクタの中でパラメータの取得・設定を行うためにはAsyncでは無理っぽい)．
また(Async型と同様に)get_parameter(get_parameters)で得られた返り値は型や名前など色々な情報を含む．パラメータの値そのものを得るためには，param.value_to_string()などの関数を使用する．代表的なものは以下の通り．その他は[公式のAPI][]のrclcpp::Parameterを参照．

- param.as_bool()
- param.as_int()
- param.as_double()
- param.as_string()

またAsync型との違いは以下のとおり

- spin_until_future_completeがなく，get_parameters, set_parametersがすぐに実行される
- 結果がFutureオブジェクトでなく，処理結果そのままとなる．そのためAsyncでは返り値.get()で処理結果を得ていたが，Syncでは返り値をそのまま使用している(19,20行目，29,30行目，40,41行目，52,53行目)．



# package.xmlとCMakeLists.txt

```xml::package.xml
<package format="3">
  <build_depend>rclcpp></build_depend>
  <test_depend>rclcpp></test_depend>
  <exec_depend>rclcpp></exec_depend>
```

```CMakeLists.txt
find_package(rclcpp REQUIRED)

add_executable(minimal_parameter_holder_test
  src/minimal_parameter_holder_main.cpp
  src/minimal_parameter_holder.cpp
)
ament_target_dependencies(minimal_parameter_holder_test
  rclcpp
)

add_executable(ros1_like_param_async_user_test
  src/ros1_like_parameter_async_user_main.cpp
)
ament_target_dependencies(ros1_like_param_async_user_test
  rclcpp
)

add_executable(ros1_like_param_sync_user_test
  src/ros1_like_parameter_sync_user_main.cpp
)
ament_target_dependencies(ros1_like_param_sync_user_test
  rclcpp
)

install(TARGETS
  minimal_parameter_holder_test
  ros1_like_param_async_user_test
  ros1_like_param_sync_user_test
  DESTINATION lib/${PROJECT_NAME}
)
```

# ビルド・実行
## ビルド

```Bash:terminal
$ cd ~/ros2_studies_ws/
$ colcon build --symlink-install --packages-select minimal_parameter
$ . install/setup.bash
```

## 実行
param_holderとuser用のterminalを起動．
param_holder用はROS1でもクラス化したものでも結果は変わらないので，ここではROS1風を実行．
またuser用はAsyncでもSyncでも結果は変わらないので，ここではAsyncを実行．

```Bash:terminal1
$ cd ~/ros2_studies_ws/
$ . install/setup.bash
$ ros2 run minimal_parameter ros1_like_param_holder_test
```
```Bash:terminal2
$ cd ~/ros2_studies_ws/
$ . install/setup.bash
$ ros2 run minimal_parameter ros1_like_param_async_user_test
```

terminal2に出てくるメッセージを見て考える．
またterminal1のparam_holderを終了させないまま，async_userを2回，3回実行させると結果はどうなるか？


# メモ

parameterはserviceを提供する側とサービスを提供されるclient側がある．parameterの値を保持し他に与えるnodeはserviceとなり，それを参照したり値を変更したりするものがclientとなる．clientが他のnodeの場合，「serviceを行うnodeはだれか?」という名前の情報が必要である．一方で，自身で自身のparameterを使用する場合(つまりservice=clientとなる場合)，別に名前を知る必要はなく，自分のparameterにアクセスすればよいだけである．このことから，service≠clientの場合，clientはAsyncClientsParameterとしてparameter clientの機能を持ち，serviceに接続しなければいけない．一方，service=clientの場合，自分のparameterにアクセスするのでnode->get_parameterやthis->parameter，はてはget_parameterのみでアクセスできる．

つまり同一nodeの場合，「他のノードのparameter機能に接続してパラメータをいじる」ではなく，「nodeのパラメータに直接アクセス」になる．よってこの場合，wait_for_serviceしなくていいんじゃなかろうか．いいんです．



[公式のAPI]:http://docs.ros2.org/dashing/api/rclcpp/index.html
[公式のRoadmap]:https://index.ros.org/doc/ros2/Releases/Release-Dashing-Diademata/
[公式githubのrcl_interfaces]:https://github.com/ros2/rclcpp/blob/master/rclcpp/include/rclcpp/node.hpp#L371