ROS2+ラズパイ(+マイコン)でロボットを無線操作

# 概要
### 初めに
ロボットを無線操作する方法は様々あると思いますが、せっかくROS2を勉強したので、ROS2で完結する通信方法を調べました。分かったことを記事にまとめます。
実戦投入はまだなので、参考程度に見てもらえればと思います。
### 大まかな手順
　最初に、ルーターを使って無線LANネットワークを構成し、PCとラズパイ間で無線通信を行います。次に、PC側の実装を行います。ジョイコンの入力をROS2で取得してtopicをpublishします。最後に、ラズパイ側でtopicをsubscribeしてマイコン側に指令を送ります。micro-ROSを用いることで、マイコンを一つのROS2 nodeとみなして通信環境を構成することができます。
※ラズパイのピンを使ってロボットを操作する場合はmicro-ROSの話題は読み飛ばしてください。
# 動作環境・バージョン
### 動作環境
Ubuntu22.04　：デュアルブートした実機PC
VScode　：PCとラズパイのコーディングは基本的にVScodeで行った
Raspberrypi 4 model B　
nucleo-f767zi　：現在弊部で一般的に用いられているマイコン
Archer C6　：無線LANルーター
logicool gamepad F310　：ジョイコントローラー
### バージョン
ROS2 : humble　：現在弊部で推奨されているバージョン
micro-ROS：humble　：ROS2のバージョンと同じものを
cubeIDE ：1.13.2　：2023/12/20現在最新バージョン
raspiのOS: Ubuntu server 22.04 (64bit) 　：desktopでもいいが、軽いのでこちらを選択

# コンテンツ
## PCとラズパイの通信
　PCとラズパイ間の通信はROS2を場合、基本的には同じwifiに接続していれば可能です。が、念のために`ping`で疎通確認を行いました。`ifconfig`でPCとラズパイのIPアドレスを調べて双方向に`ping`が通ることを確認しました。
![Screenshot from 2023-12-19 20-48-22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3338511/03fcb5c9-64d8-79c4-b555-38850ffe1209.png)

　さらに今回は、ラズパイに無線LANだけでなく、有線LANも繋いでLidarと通信を行いました。その際、無線LANネットワークと有線LANネットワークが混線しないように固定IPアドレスを設定する必要がありました。
　LidarのIPアドレスはデフォルトでは`192.168.0.10`ですが、dhcpでラズパイに割り振られたＩＰアドレスが`192.168.0.~`だったためにサブネットが被ってしまっていることが問題です。
　固定IPはルーターの管理者画面から設定できます。使っているルーターによって管理者画面のUIは異なると思いますが概ね同じような設定ができるはずです。 
私の使っているルーターの場合はブラウザの検索バーに`http://tplinkwifi.net`と入力することで、管理者画面に行くことができました。
![スクリーンショット 2023-12-19 210427.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3338511/a0660b13-d4fe-d933-6c27-eb0f24d35328.png)
  アドレス予約の箇所から使いたい端末のMACアドレスとそれに割り当てたいIPアドレスを設定します。
  サブネットがどうとか言うエラーがでた場合は`ネットワーク`→`LAN`からルーター自体のサブネットを設定することで解決できます。

## PC側の実装
### ジョイコンの入力をROS2で取得する
　ジョイコンの入力をROS2で取得するのは簡単で、ROS2に標準で搭載されている`joy_node`を用いればよいです。ターミナルから起動する場合は`ros2 joy joy_node`で起動できます。PCにジョイコンを差していればjoy nodeを起動した段階で`sensor_msgs/msg/Joy`型のメッセージがpublishされ始めます

 ![Screenshot from 2023-12-19 20-40-33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3338511/3b57326e-d1bb-0c80-e9d5-45f0bb2359b8.png)

### モータの制御値をpublishする
　joy型のメッセージそのままではロボットを動かすのには不都合が大きいので、これを`geometry_msgs/msg/Twist`型のメッセージに変換しましょう。

```joy_transrate_node.cpp
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
#include "geometry_msgs/msg/twist.hpp"
# include <bits/stdc++.h>
#include "sensor_msgs/msg/joy.hpp"

using namespace std::chrono_literals;

class JoyTransrateNode : public rclcpp::Node {
public:
    std::string type_;
    double lx_;
    double az_;

    JoyTransrateNode() : Node("joy_transrate_node") {

        //使用するパラメータの宣言
        declare_parameter("robot_type", "default");
        declare_parameter("max_lx", -1.0);
        declare_parameter("max_az", -1.0);

        //パラメータの取得
        type_ = get_parameter("robot_type").as_string();
        lx_ = get_parameter("max_lx").as_double();
        az_ = get_parameter("max_az").as_double();

        //パラメータの確認
        RCLCPP_INFO(this->get_logger(), "robot type:%s\r\n",type_.c_str());
        RCLCPP_INFO(this->get_logger(), "max lx:%f\r\n",lx_);
        RCLCPP_INFO(this->get_logger(), "max az:%f\r\n",az_);
  
        publisher_ = this->create_publisher<geometry_msgs::msg::Twist>("cmd_vel", 10);

        auto topic_callback = [this](const sensor_msgs::msg::Joy &msg) -> void {

            auto message = geometry_msgs::msg::Twist();

            //最大値に-1~1を乗算
            message.linear.x = lx_ * msg.axes[1];
            message.angular.z = az_ * msg.axes[0];

            //十字キーでの操作を上書き
            if(msg.axes[5] != 0){
                message.linear.x = lx_ * msg.axes[5];
            }
            if (msg.axes[4] != 0)
            {
                message.angular.z = az_ * msg.axes[4];
            }
            
            this->publisher_->publish(message);
        }; 

        subscription_ = this->create_subscription<sensor_msgs::msg::Joy>
                ("joy", 10, topic_callback);
    }
private:
    rclcpp::Subscription<sensor_msgs::msg::Joy>::SharedPtr subscription_;
    rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char *argv[]) {
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<JoyTransrateNode>());
    rclcpp::shutdown();
    return 0;
}
```

コントローラーによってキーの対応関係が若干異なるそうなので、注意して下さい。
上のコードでは差動二輪ロボットを想定してキー入力をx方向の速度とz軸周りの角速度に変換しています。

必ずしも必要ではありませんが、今回はTwist型の速度司令をさらに各モータの制御値に変換するnodeを作成しました。

```diff_drive_node.cpp
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
#include "geometry_msgs/msg/twist.hpp"
#include "drive_msgs/msg/diff_drive.hpp"
# include <bits/stdc++.h>

using namespace std::chrono_literals;

class DiffDriveNode : public rclcpp::Node {
public:

    std::string type_;
    std::double_t rad_;
    std::double_t dis_;

    //cmd_velからモータの制御値を計算
    void diffDrive(float cmd[2],float lx,float az){ //cmd[0]:右輪rps,cmd[1]:左輪rps,lx:m/s,az:rad/s

        cmd[0] = (lx + dis_*az/2)/(2.0*M_PI*rad_);
        cmd[1] = (lx - dis_*az/2)/(2.0*M_PI*rad_);
        
    }

    DiffDriveNode() : Node("diff_drive_node") {
        //使用するパラメータの宣言
        declare_parameter("robot_type", "default");
        declare_parameter("wheel_radious", -1.0);
        declare_parameter("wheel_distance", -1.0);

        //パラメータの取得
        type_ = get_parameter("robot_type").as_string();
        rad_ = get_parameter("wheel_radious").as_double();
        dis_ = get_parameter("wheel_distance").as_double();

        //パラメータの確認
        RCLCPP_INFO(this->get_logger(), "robot type:%s\r\n",type_.c_str());
        RCLCPP_INFO(this->get_logger(), "wheel radious:%f\r\n",rad_);
        RCLCPP_INFO(this->get_logger(), "wheel distance:%f\r\n",dis_);
        
        publisher_ = this->create_publisher<drive_msgs::msg::DiffDrive>("cmd_motor", 10);

        auto topic_callback = [this](const geometry_msgs::msg::Twist &msg) -> void {

            auto message = drive_msgs::msg::DiffDrive();
            float ans[] = {0,0}; //ans[0]:右輪rps,ans[1]:左輪rps
            diffDrive(ans, msg.linear.x, msg.angular.z);
            
            message.name = "differencial drive";
            message.m1 = ans[0];
            message.m2 = ans[1];

            this->publisher_->publish(message);
        }; 

        subscription_ = this->create_subscription<geometry_msgs::msg::Twist>
                ("cmd_vel", 10, topic_callback);
    }
private:
    rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr subscription_;
    rclcpp::Publisher<drive_msgs::msg::DiffDrive>::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char *argv[]) {
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<DiffDriveNode>());
    rclcpp::shutdown();
    return 0;
}

```

このコードでは各モータの速度目標値を計算しカスタムメッセージに格納しています。カスタムメッセージは以下のようなものを作成しました。
```DiffDrive.msg
string name
float64 m1 #右輪rps
float64 m2 #左輪rps
```

## ロボット側の実装
### ラズパイでROS2 topicを中継
　次に、ラズパイでPCから送られてきたモータ制御値のtopicを中継します。今回はラズパイにubuntu serverを入れて、そこにROS2をインストールしているので普通のPCと同様にROS2を扱えます。ラズパイの詳しいセットアップ方法は[この記事](https://qiita.com/N622/items/cd41c09817d13ac7035c)を参考にしてください。
 

```raspi_node.cpp

#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
#include "geometry_msgs/msg/twist.hpp"
#include "sensor_msgs/msg/laser_scan.hpp"
#include "drive_msgs/msg/diff_drive.hpp"

using namespace std::chrono_literals;

class RaspiNode : public rclcpp::Node {
public:
    RaspiNode() : Node("raspi_node") {
        
        publisher_ = this->create_publisher<drive_msgs::msg::DiffDrive>("/cmd_ras", 10);

        auto topic_callback = [this](const drive_msgs::msg::DiffDrive &msg) -> void {

            auto message = drive_msgs::msg::DiffDrive();

            message.name = msg.name;
            message.m1 = msg.m1;
            message.m2 = msg.m2;

            this->publisher_->publish(message);

            RCLCPP_INFO(this->get_logger(), "I'm hearing\r\n");
        }; 

        subscription_ = this->create_subscription<drive_msgs::msg::DiffDrive>("/cmd_motor", 10,topic_callback);

    }
private:
    rclcpp::Subscription<drive_msgs::msg::DiffDrive>::SharedPtr subscription_;
    rclcpp::TimerBase::SharedPtr timer_;
    rclcpp::Publisher<drive_msgs::msg::DiffDrive>::SharedPtr publisher_;
};

int main(int argc, char *argv[]) {
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<RaspiNode>());
    rclcpp::shutdown();
    return 0;
}

```


### マイコンの実装
　ラズパイのGPIOピンを使う場合は必要ありませんが、マイコンを使いたい場合はさらにmicro-ROSを使ってモータ制御値のtopicをマイコン側で受け取れるようにする必要があります。今回用いたマイコンはstm32 f767ziで、cubeIDEを使いました。
 以下、cubeIDEでの実装を紹介します。
 micro-ROSの導入やカスタムメッセージの使い方などについては[こちらの記事](https://qiita.com/hirekatsu0523/items/492f52c9d697dac1eb29)を参考にさせて頂きました。
 micro-ROSの準備ができたら、次にsubscriberを作成します。
 ```c
 //メッセージを受け取る変数の宣言
 drive_msgs__msg__DiffDrive sub;
 
//subscriberの作成
    RCCHECK(rclc_subscription_init_default(
      &subscriber,
      &node,
      ROSIDL_GET_MSG_TYPE_SUPPORT(drive_msgs, msg, DiffDrive),
      "/cmd_ras"));
// エグゼキューターの作成。三番目の引数はextecuterに登録するコールバック関数の数。
RCCHECK(rclc_executor_init(&executor, &support.context, 2, &allocator));

// エグゼキューターにサービスを追加
RCCHECK(rclc_executor_add_subscription(
      &executor, &subscriber, &sub,
      &subscription_callback, ON_NEW_DATA));
 ```
 subscribeのコールバック関数の中でモータを回す処理を書きます。今回は通信周りについての記事なので具体的なモータを回す実装については省略します。
```c
void subscription_callback(const void * msgin)
{
	 // Cast received message to used type
	  const drive_msgs__msg__DiffDrive * sub = (const drive_msgs__msg__DiffDrive *)msgin;

      //デバッグ用の処理
	  char hearing[] = "I'm hearing from f7";
	  rosidl_runtime_c__String__assignn(&pub.data, hearing, sizeof(hearing));
	  RCSOFTCHECK(rcl_publish(&publisher, &pub, NULL));

	  run_motor(sub->m1,sub->m2); //適宜モータを回す処理を実装してください
}
```
## 動作確認
PC側で以下のnodeを起動します
 - joy_node
 - joy_transrate_node
 - diff_drive_node

ラズパイ側では
- raspi_node　を起動し、

`ros2 run micro_ros_agent micro_ros_agent serial -b 115200 --dev /dev/ttyACM0
`
でmicro-ROS　agentを起動して下さい。但し、使っているシリアルポートによってttyACM以下を書き換える必要があるかもしれません。
マイコンに給電して、適切なハードウェアを用意すればロボットを無線操作できるはずです。
# 終わりに
　今回はROS2とラズパイ、stmマイコンを使ってロボットを無線操作したときの実装を紹介しました。実際に使ってみた感想としては遅延が結構あって、これを改善しないと実戦投入は難しいかな、といったところでした。コントローラーの相性で遅延が発生することもあるそうなので、今後色々試してみて遅延を解消していきたいと思います。

# 参考文献
- [「続」stm32でmicro-ROSを使う](https://qiita.com/hirekatsu0523/items/492f52c9d697dac1eb29)
- [複数のRaspberry Pi(Ubuntu20.04)を有線接続し、さらにPCを無線で接続するネットワーク設定](https://qiita.com/gtkr/items/dcdf55faf8e599d4b8ea)
- [ルーターを利用してIPアドレスを固定で割り振る方法](https://qa.elecom.co.jp/faq_detail.html?id=3126#:~:text=Q%EF%BC%8E%E3%80%90Wi-Fi%E3%83%AB%E3%83%BC%E3%82%BF%E3%83%BC%E3%80%91%E3%83%AB%E3%83%BC%E3%82%BF%E3%83%BC%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6IP%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%82%92%E5%9B%BA%E5%AE%9A%E3%81%A7%E5%89%B2%E3%82%8A%E6%8C%AF%E3%82%8B%E6%96%B9%E6%B3%95%20%E3%83%AB%E3%83%BC%E3%82%BF%E3%83%BC%E3%81%AE%E3%80%90%E7%AE%A1%E7%90%86%E7%94%BB%E9%9D%A2%E3%80%91%E3%81%8B%E3%82%89%EF%BC%BB%E8%A9%B3%E7%B4%B0%E8%A8%AD%E5%AE%9A%EF%BC%88%E4%B8%8A%E7%B4%9A%E8%80%85%E5%90%91%E3%81%91%EF%BC%89%EF%BC%BD%E3%82%92%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%81%97%E3%81%BE%E3%81%99%E3%80%82%20%E5%B7%A6%E3%81%AE%E3%83%A1%E3%83%8B%E3%83%A5%E3%83%BC%E3%83%AA%E3%82%B9%E3%83%88%E3%81%8B%E3%82%89%EF%BC%BBLAN,%E5%81%B4%E8%A8%AD%E5%AE%9A%EF%BC%BD%E3%82%92%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%E3%81%97%E3%81%BE%E3%81%99%E3%80%82%20%EF%BC%BB%E5%9B%BA%E5%AE%9ADHCP%E3%83%AA%E3%83%BC%E3%82%B9%E3%82%92%E6%9C%89%E5%8A%B9%EF%BC%BD%E3%81%AB%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%82%92%E3%81%A4%E3%81%91%E3%81%BE%E3%81%99%E3%80%82%20IP%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%82%92%E5%89%B2%E3%82%8A%E5%BD%93%E3%81%A6%E3%81%9F%E3%81%84%E7%AB%AF%E6%9C%AB%E3%81%AEMAC%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%82%92%E5%85%A5%E5%8A%9B%E3%81%97%E3%81%BE%E3%81%99%E3%80%82)