ROS 1 => 2 高速移植テクニック、多分これが一番速いと思います。
ROSをお使いの皆様こんばんは。
いよいよROS 1のサ終が来月になりました。
そう、「来月」です。やばい。
まだあわわわわと言っている状況ではありません。
流石にそろそろ資産を移行しましょう。
といっても、じゃあどうすればいいんじゃ!!!すでにこっちは仕事で動かしてる案件止めれんわ!!という声が各所から聞こえてきそうなので、そのダウンタイムを最小にしつつROS 2移植する方法をご紹介しようと思います。
一応記事の執筆者本人はROS 2でtfすら怪しいタイミングにオレオレnavigationスタックをROS 2移植した実績とお仕事でおろらく2~3万行くらい?コードをROS 2移植するのをやりきった実績があります。
前提知識
ユーザーから見たROS 1/2の最大の違いはExecutorです。
DDSではありません。
詳細はこちらの記事をご覧ください。
大事なことは一つだけ
同じExecutorに乗っているTopicのPublish/SubscribeはROS 2ではプロセス間通信ではなくメモリによるプロセス内通信になります。
なので超早い。
事前に「ここが通信量多そうだな〜〜」って箇所は見積もっておきましょう。
まあ大体カメラとかLiDARとかの生データ転送してるところ見ておけばほぼそこがボトルネックだと思います。
移行手順
Step1 移行したいシステムの設計図を記述する
まず移行対象のシステムをすべて記述し、移植する範囲をはっきりさせましょう。
依存パッケージの移植に関しては一旦考えなくて良いと思います。調査することはできますが、今どきのロボット開発に必要な必要なパッケージはだいたいROS 2に揃っていますし工期をバッファを設ければなかったらなかったでそのときに考えるでなんとかなるかと思います。
そして、このときに通信量多そうなTopicを絞り込んで置きましょう。そして、それらを同じExecutorに置くということを文書化しておきましょう。
Step2 ROS 1のままROS 2ライクな記述方法に置き換える
ROS 1のAPIはとてもフリーダムです。
フリーダムすぎて、人によってコーディングスタイルが違います。
ROS 2ではcomponent指向を使うことでコーディングスタイルの自然な統一と処理の高速化を同時に実現しています。
よって、まずはROS 1のコードをROS 2ライクなROS 1のソースコードに移植してきます。
このときは一切の機能の変更はありませんので細かくPRを出すことも並行してFeature開発/Bugfixすることも可能です。
具体的な移行手順
# include <ros/ros.h>
# include <std_msgs/String.h>
int main(int argc, char** argv)
{
ros::init(argc, argv, "basic_simple_talker");
ros::NodeHandle nh;
ros::Publisher chatter_pub = nh.advertise<std_msgs::String>("chatter", 10);
ros::Rate loop_rate(10);
while (ros::ok())
{
std_msgs::String msg;
msg.data = "hello world!";
ROS_INFO("publish: %s", msg.data.c_str());
chatter_pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
}
return 0;
}
上記はよくサンプルで見るようなROS 1のソースコードかと思います。
まずはNodeを表現するクラスを作ってそこのメンバ関数などでpub/subするように書き換えて行きます。
ヘッダファイル
# include <ros/ros.h>
# include <std_msgs/String.h>
class RosPublisherComponent
{
public:
RosPublisherComponent();
private:
ros::NodeHandle nh;
ros::Publisher chatter_pub;
ros::Timer timer;
};
ソースコード(実装)
#include <ros_publisher/ros_publisher_component.hpp>
RosPublisherComponent::RosPublisherComponent()
{
chatter_pub = nh.advertise<std_msgs::String>("chatter", 10);
const auto timer_callback = [&](const ros::TimerEvent& e) {
std_msgs::String msg;
msg.data = "hello world!";
ROS_INFO("publish: %s", msg.data.c_str());
chatter_pub.publish(msg);
};
timer = nh.createTimer(ros::Duration(0.1), timer_callback);
}
ソースコード(main)
#include <ros_publisher/ros_publisher_component.hpp>
int main(int argc, char** argv)
{
ros::init(arg, argv, "publisher_node");
RosPublisherComponent();
return 0;
}
ソースコードは脳内コンパイルしているのでもしかしたら間違っているかもしれません。が、要するにクラス化していくのが重要です。
Pythonに関しても同様です。
ROS 1ライクに書けるAPIも用意されていますが、基本的にそれらは通信効率が劣悪です。
まずはすべての処理をクラス化していきましょう。
ROS 2移植を実行
このステップでは完成したコードを実際にROS 2に移植していきます。
一応ブランチを切っておけば完全にfeature開発を止めなくても理論上は可能ですが、地獄を見る可能性があるのでどうしてもな理由がなければコードフリーズをして短期間で移植を乗り切ってしまうことを推奨します。
前のステップで出てきたROS 1のソースコードをROS 2に移植するとこんな感じになります。
ヘッダファイル
# include <rclcpp/rclcpp.hpp>
# include <std_msgs/msg/string.hpp>
class RosPublisherComponent : public rclcpp::Node
{
public:
explicit RosPublisherComponent(const rclcpp::NodeOptions & options);
private:
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr chatter_pub;
rclcpp::TimerBase::SharedPtr timer;
};
ソースコード(実装)
#include <ros_publisher/ros_publisher_component.hpp>
RosPublisherComponent::RosPublisherComponent()
{
chatter_pub = create_publisher<std_msgs::msg::String>("chatter", 10);
const auto timer_callback = [&]() {
std_msgs::msg::String msg;
msg.data = "hello world!";
RCLCPP_INFO(get_logger(), "publish: %s", msg.data.c_str());
chatter_pub->publish(msg);
};
timer = create_wall_timer(100ms, timer_callback);
}
ソースコード(main)
#include <ros_publisher/ros_publisher_component.hpp>
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
rclcpp::NodeOptions options;
auto component = RosPublisherComponent(options); // ここで rclcpp::spin(RosPublisherComponent(options));ってオシャンに書こうとするとWeak pointerでcomponentのpointerが管理されているので無言で解放されるので注意
rclcpp::spin(component);
rclcpp::shutdown();
return 0;
}
前のStepと比較するとかなり機械的に書き換えられたかと思います。
これによってコードレビューの負荷を軽減したりヒューマンエラーが入る余地を大きく減らせるかと思います。
完走した感想
ROS 2移植RTA完走お疲れ様でした。
完走した感想ですが、たぶんこれが一番速いと思います。
Discussion