はじめに
前回記事 ではROS Workspaceを作成するところまで行いました。
今回は talker.cpp と listener.cpp を実装し、ROS2のメッセージ通信手段の1つである、「トピック」を行うことを目標とします。
環境
Ubuntu 22.04
Qt Creator 13.0.0
ROS : Humble Hawksbill
1. srcディレクトリを表示する
Qt Creator のデフォルト設定では空のディレクトリを非表示にしてしまうそうです。そのため、(特に何もしていなければ)src フォルダが表示されていないかと思います。
フィルターツリーの 「空のディレクトリを非表示にする」(Hide Empty Directories) のチェックを外すと、srcディレクトリが表示されるはずです。
2. ROS2パッケージを作成する
パッケージとはROS2コードを入れておく箱のようなものです。
Qt Creator を使用するとコマンドを使わずともパッケージ作成ができます。
ctrl+Nでファイルの新規作成が行えるので、そこから、ROS Packageを選択します。
パッケージ名を入力します。
バージョン・ライセンス表記は好きにしてください。
ビルドシステムとしてcolconを使用しているためか、ここでDependenciesに依存パッケージを書き込んでもエラーが出てパッケージ作成できませんでした。
仕方がないので、手動でCMakeLists.txtとpackage.xmlに依存パッケージを記入したいと思います。
pathも変更できるようになっていますが、srcディレクトリのままにします。
「次へ」をクリックできない場合は、一度上の参照をクリックし、表示されるウィンドウを消してからTabキーで「次へ」に移動してください。
[2024/5/27 追記]
どうやらパッケージの名前にアルファベットの大文字を使ってはいけないようです。サービス通信の実装を行っている際にエラーで怒られました。
なぜトピック通信ではエラーにならなかったのかは分かりませんが、小文字+アンダースコアで書くようにしましょう。
rosidl_adapter.parser.InvalidResourceName: 'HelloWorld' is an invalid
package name. It should have the pattern
'^(?!.*__)(?!.*_$)[a-z][a-z0-9_]*$'
CMakeLists.txtとpackage.xmlが追加されていることが確認できます。
3. 依存パッケージを記述する
3.1. CMakeLists.txt
必要なパッケージを追記します。
ここでは、C++を扱うのに必要な rclcpp (ROS Client Library for C++) および、標準メッセージ用パッケージである std_msgs を見つけてもらえるようにします。
# find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED) # 追記
find_package(std_msgs REQUIRED) # 追記
# uncomment the following section in order to fill in
# further dependencies manually.
# find_package(<dependency> REQUIRED)
3.2. package.xml
パッケージをビルド・実行するときにrclcppを使用できるよう、追記します。
<buildtool_depend>ament_cmake</buildtool_depend>
<!-- 追記ここから -->
<build_depend>rclcpp</build_depend>
<exec_depend>rclcpp</exec_depend>
<build_depend>std_msgs</build_depend>
<exec_depend>std_msgs</exec_depend>
<!-- 追記ここまで -->
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
4. トピックの実装
いよいよ本題です。
"Hello, World" を0.1秒に1回のペースで出力し続ける talker.cpp と、それを受け取る listener.cpp を作成していきます。
4.1. cppファイル作成
C++ Source File を選択します。
ファイル名とパスを指定します。
ここでは talker.cpp と listener.cpp を作成しました。
cppファイルはsrcディレクトリ内に置きます。
4.2. CMakeLists.txtの更新
CMakeLists.txtに次の内容を追記します。
add_executable(talker src/talker.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
install(TARGETS talker DESTINATION lib/${PROJECT_NAME})
add_executable(listener src/listener.cpp)
ament_target_dependencies(listener rclcpp std_msgs)
install(TARGETS listener DESTINATION lib/${PROJECT_NAME})
簡単な説明
add_executable
実行可能ファイル(ここではtalker/listener)の定義。
talker/listener
実行可能ファイルの名前。src/talker.cpp あるいは src/listener.cpp をビルドして生成される。
src/talker.cpp
talkerという名前の実行可能ファイルのソースコードが格納されているファイルパス。
ament_target_dependencies(talker rclcpp std_msgs)
ターゲット(ここではtalker/listener)が依存するパッケージやライブラリを指定。
install
ビルドされたターゲット(ここではtalker/listener)を指定されたディレクトリ(ここではlib/${PROJECT_NAME})にインストールする指示を行う。
CMakeListsでは関数を定義することもできます。
これを使用することで、cppファイルが多くなっても繰り返し同じような文を書く必要がなくなります。
function(config_executable target)
add_executable(${target} src/${target}.cpp)
ament_target_dependencies(${target} rclcpp std_msgs)
install(TARGETS ${target} DESTINATION lib/${PROJECT_NAME})
endfunction()
config_executable(talker)
config_executable(listener)
4.3. ビルド&実行ファイル設定
左下のトンカチマークからビルドをします。とはいえ、これだけではまだ実行できません。
カスタム実行構成に実行ファイルを設定します。左のバーにある「プロジェクト」から「実行」を選択します。
「実行ファイル」に、先ほど CMakeLists で設定したディレクトリ内にある実行ファイルのパスを入力します。
「作業ディレクトリ」にはsrcディレクトリを指定します。
この段階で実行してエラーが出る場合の対処法
この段階で実行すると
エラー: /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/Scrt1.o: in function `_start':
のようなエラーが現れるかもしれません。これはmain関数が存在しないことに起因して起こるようです。
int main(int argc, char *argv[])
{
std::cout << "Hello, World" << std::endl;
}
なんでもいいので、main関数を用意してあげるとエラーが発生しなくなります。
4.4. talker.cppの実装
実装に際しては、近藤 豊「ROS2ではじめよう 次世代ロボットプログラミング」(pp.74-76) を参考にしています。エラーを吐いた部分の修正を行いました。
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
class Talker : public rclcpp::Node
{
public:
explicit Talker(const std::string &topic_name) : Node("talker")
{
message_ = std::make_shared<std_msgs::msg::String>();
message_->data = "Hello, World";
auto publish_Message =
[this]() -> void
{
RCLCPP_INFO(this->get_logger(),"%s", message_->data.c_str());
publish_->publish(*message_);
};
publish_ = create_publisher<std_msgs::msg::String>(topic_name, 10);
timer_ = create_wall_timer(std::chrono::milliseconds(100), publish_Message);
}
private:
std::shared_ptr<std_msgs::msg::String> message_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publish_;
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char *argv[])
{
setvbuf(stdout, nullptr, _IONBF, BUFSIZ);
rclcpp::init(argc, argv);
auto node = std::make_shared<Talker>("chatter");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
コードざっくり解説
クラス定義
class Talker : public rclcpp::Node
ノードを作成する際には rclcpp::Node クラスを継承します。
Nodeクラスのリファレンスは ここ にあります。
コンストラクタ
explicit Talker(const std::string &topic_name) : Node("talker")
コンストラクタの定義です。Node クラス(親クラス)のコンストラクタに "talker" という文字列を渡して初期化しています。
メッセージ設定
message_ = std::make_shared<std_msgs::msg::String>();
message_->data = "Hello, World";
make_shared関数を使って、共有ポインタ (スマートポインタの一種) を作成します。
その後、"Hello, World" という文字列を代入します。message_はポインタなのでアロー演算子を使用します。
publish_Message関数オブジェクト
auto publish_Message =
[this]() -> void
{
RCLCPP_INFO(this->get_logger(),"%s", message_->data.c_str());
publish_->publish(*message_);
};
ラムダ式を使用しています。
RCLCPP_INFO()はログ表示のための関数で、これを書くとQtCreatorのアプリケーション出力にログが表示されるようになります。
publisher設定
publish_ = create_publisher<std_msgs::msg::String>(topic_name, 10);
create_publisher関数はNodeクラスのメンバ関数です。第2引数は過去のメッセージ履歴をどれだけ保存するかを指定しています。第2引数の省略はできません。(以前はできた?)
タイマー設定
timer_ = create_wall_timer(std::chrono::milliseconds(100), publish_Message);
create_wall_timer関数もNodeクラスのメンバ関数です。第1引数が実行周期、第2引数が定期的に実行したい処理(関数オブジェクト)です。
main関数
int main(int argc, char *argv[])
{
setvbuf(stdout, nullptr, _IONBF, BUFSIZ);
rclcpp::init(argc, argv);
auto node = std::make_shared<Talker>("chatter");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
main関数です。
setvbuf(stdout, nullptr, _IONBF, BUFSIZ)では標準出力のバッファリングを無効化し、即座に出力するよう設定します。
実行するときは rclcpp::spin() を使用します。
4.5. listener.cppの実装
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
class Listener : public rclcpp::Node
{
public:
explicit Listener(const std::string &topic_name) : Node("listener")
{
auto callback =
[this](const std_msgs::msg::String::SharedPtr message) -> void
{
RCLCPP_INFO(this->get_logger(),"%s", message->data.c_str());
};
subscription_ = create_subscription<std_msgs::msg::String>(topic_name, 10, callback);
};
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char *argv[])
{
setvbuf(stdout, nullptr, _IONBF, BUFSIZ);
rclcpp::init(argc, argv);
auto node = std::make_shared<Listener>("chatter");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
talker.cpp と大きく変わっているわけではないので解説は省略します。
4.6. 実行
talker.cpp と listener.cpp をどちらも実行します。
すると、Listenerが正しくデータ受信できていることが分かります。
これで、ひとまず「トピック」の実装まで終わりました。
GUI制作編