例のラジコンをROS2+micro-ROSで動かしてみた

2023年11月24日〜12月1日に行われたAmazonブラックフライデーセールにて，RoboMaster S1にそっくりなラジコンが激安で販売されていることが𝕏（旧Twitter）上で話題になっていました．

このビッグウェーブに乗るしかないと思い，私も即ポチりました．
わずか3,000円でサスペンション付きのメカナム足回りが手に入ると思うと驚きです．

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">例のアレ(とDUALSHOCK4)買いました<br>このロボット3000円ってマジ？ <a href="https://t.co/cFAyRUA7ZV">pic.twitter.com/cFAyRUA7ZV</a></p>&mdash; Akinosuke Tsutsumi (@akki_nitkk) <a href="https://twitter.com/akki_nitkk/status/1730155025509560448?ref_src=twsrc%5Etfw">November 30, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607628/94080326-6ede-c735-8400-6fdf3b94f4e3.png)

注文した2日後には届いたので早速組み立てを行いました．
しかし，このあと学会やロボコンの大会で忙しく1ヶ月ほど寝かしてしまい...
年末にようやく時間を作って動かすことができました．

# ラジコン制御

今回はROS2とmicro-ROSを用いて足回りを動かしてみました．
上物も動かしたかったのですが，回路を取り付ける際に煩わしかったので一旦取り外しました．

使用したマイコンはM5Stack Core2です．
マイコンに給電するモバイルバッテリーを搭載するスペースを確保するのが面倒だったため，バッテリー内蔵でmicro-ROSも使えるこちらを使用しました．

始めは「RaspberryPi Zero 2W」でモータを駆動しつつカメラ積んで画像処理などしようかと考えていたのですが，そもそもROS2がスムーズに動いてくれるか怪しかったため，将来的にミニPCを搭載することにして採用を見送りました．

ソースコードはこちらです．
PlatformIOのプロジェクトになっていますのでメインの処理は`src/main.cpp`に記述されています．

https://github.com/TsutsumiAkinosuke/radicon_drive

micro-ROS関連の処理にはコメントを付けていますのでそちらを見れば何をしているかは理解できるかと思います．
本記事では制御システム概要ともう少し細かい部分を解説していきます．

---

今回は`joy_node`から`/joy`をパブリッシュし，`teleop_twist_joy`ノードで`/cmd_vel`メッセージに変換してからmicro-ROS経由でマイコン側の`radicon_node`に指令を送信しています．

![radicon_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607628/ec470aed-61f9-9028-fd65-74eb3a071b28.png)

なぜわざわざ変換する処理を挟んでいるかというと，`/joy`メッセージは`sensor_msgs/Joy`型ですが，この型はメンバとして動的なサイズのVectorを持つため，マイコン側で受信することが難しいからです．

一応，micro-ROS公式にはSequence型として動的なサイズのメッセージを受信する方法が紹介されていますが，私は正常に受信できたことがありません...

https://micro.ros.org/docs/tutorials/advanced/handling_type_memory/

そこで動的なサイズのメンバを持たない`geometry_msgs/Twist`型の`/cmd_vel`メッセージに変換してから送ることでこの問題を回避しています．

変換ノードを記述するのが面倒なので`teleop_twist_joy`を利用していますが，もちろん自前で用意してもいいです．

...と言うよりは，`teleop_twist_joy`はOPTIONボタンかPSボタンを押しながらでないとジョイスティックの入力が反映されない仕様のため，自前で用意して好みの仕様にしたほうがいいです．（単純なノードになるためROS2初学者の方は作ってみるといい勉強になるかと思います）

---

始めに，setup関数で実行しているピンの初期化やmicro-ROSのセットアップについて注意点だけ見ていきます．

```src/main.cpp

int pwm_pin[8] = {14, 26, 25, 13, 32, 27, 19, 33};

void setup(void) {

  /*中略(Lcd等のセットアップ)*/

  // PWM波生成に使用するタイマーのセットアップとピン割当て
  for(int i = 0; i < 8; i++) {
    ledcSetup(i, 20000, 8);       // i番チャンネルを20kHz・分解能8bitで初期化
    ledcAttachPin(pwm_pin[i], i); // i番チャンネルに配列pwm_pinのi番目のピンを割当て
    ledcWrite(i, 0);              // モータを停止させる
  }

  // micro-ROSをセットアップする関数
  // Wi-FI経由で通信する際の引数はSSID, パスワード, 接続先のPCのIPアドレス, ポート番号(適当でOK)
  set_microros_wifi_transports("SSID", "PASSWORD", "IP_ADDRESS", 2000);

  /*中略(micro-ROSのセットアップ)*/

  // executorを作成
  // 引数は executorの構造体, サポートのコンテクスト, ハンドル数, allocator
  // ハンドル数：マイコンで処理するtopic, service, timerなどのコールバックの数
  RCCHECK(rclc_executor_init(&executor, &support.context, 1, &allocator));

  // executorにsubscriptionを追加
  // 引数は executor, subscription, msg, コールバック関数, 呼び出し方法(?)
  RCCHECK(rclc_executor_add_subscription(&executor, &subscriber, &twist_msg, &twist_callback, ON_NEW_DATA));
}
```

まずはモータードライバ(MD)に入力するPWM波を生成するためのピン設定を行っています．
サンプリング周波数を今回使用したモータードライバの許容最大PWM周波数である20kHz，分解能を8bit(0~255)として，8つのピンそれぞれに1つのチャンネルをfor文で回して割り当てています．

また，電源を入れてからmicro-ROSの接続が確立されるまではモータが止まっていてほしいので，念のためモータを停止させる処理を入れています．

次に，`set_microros_wifi_transports`関数に，接続するWiFiのSSIDとパスワード，そしてmicro-ROSのAgentを起動するPCのIPアドレスを渡して接続を試みます．
それぞれの引数(文字列)は各自の環境に合わせて書き換える必要があります．

接続できたらmicro-ROSのセットアップを行います．
`rclc_executor_init`関数の引数に渡すコールバックの数について，なぜかメッセージを受信できないと思ったらこの引数の値が間違っていたというミスが起こりがちなのでご注意ください．

また，subscriptionを宣言するときに，今回は`twist_callback`というコールバック関数を呼び出すよう割り当てています．

---

`/cmd_vel`メッセージを受信した際に呼び出される`twist_callback`関数の中身を見ていきます．

```src/main.cpp

// MDに入力するduty比(0~255)を格納する配列(0:右前, 1:左前, 2:左後, 3:右後)
int duty[4] = {0};

void twist_callback(const void * msgin)
{
  // 受信したメッセージを格納
  const geometry_msgs__msg__Twist * msg = (const geometry_msgs__msg__Twist *)msgin;

  // 各モータのduty比を計算
  duty[0] = msg->linear.x *  127 + msg->angular.z * 127;
  duty[1] = msg->linear.x * -127 + msg->angular.z * 127;
  duty[2] = msg->linear.x * -127 + msg->angular.z * 127;
  duty[3] = msg->linear.x *  127 + msg->angular.z * 127;

  // 各モータを回転させる
  for(int i = 0; i < 4; i++) {
    if (duty[i] > 0) {
      // Duty比が正ならモータを正転
      ledcWrite(i*2, duty[i]);
      ledcWrite(i*2+1, 0);
    } else if(duty[i] < 0) {
      // Duty比が負ならモータを逆転
      ledcWrite(i*2, 0);
      ledcWrite(i*2+1, -1*duty[i]);
    } else {
      // Duty比が0ならモータを停止
      ledcWrite(i*2, 0);
      ledcWrite(i*2+1, 0);
    }
  }
}
```

まず受信したメッセージをローカルの変数に格納しています．
基本的に受信したメッセージの内容にアクセスする際は，コールバック関数内であればこのローカルの変数，コールバック関数外であれば事前に宣言しておいたメッセージ格納用変数を利用することが多いようです．

次に足回りの各モータに入力するPWM波のDuty比を計算しています．
4輪メカナムの運動学モデルに従って，左前と左後のDuty比だけ`linear.x`の値に負のバイアスを掛けています．

また，今回は`/cmd_vel`の各値の絶対値が1を超えないという前提で計算式を記述していますが，念のため配列`duty`に格納されている値が0~255の範囲内かチェックするといった対策をしておいたほうがいいかもしれません．

参考までに，以下のように記述すれば`duty[0]`に代入される値を-254~254の範囲に制限することができます．
```
duty[0] = min(254, max(-254, msg->linear.x *  127 + msg->angular.z * 127));
```

最後にモータを回転させる処理をfor文で回して実行しています．
配列`duty`に格納されている値が正であれば正回転，負であれば逆回転させています．

`ledcWrite`関数の第2引数は正の値しか受け付けないため，`duty`に格納されている値が負の場合のみ`-1`を掛けて渡しています．

なお，今回はPWM-PWM型のMDを使用しているため`ledcWrite`関数を2回実行する形になっていますが，DIR-PWM型のMDを使用する場合は記述が変わってきますのでご注意ください．

# ラジコン回路

MDはCytron社製のMaker Driveを使用しています．
M5Stack Core2で自由にPWM波を出力できるGPIOはちょうど8つぐらいのようなので，単純な拡張でこれ以上モータを増やすのは少々難しそうです...

本体付属のバッテリーはちょうど2.54mmピッチのピンヘッダーに刺さるため，そこからMDに電力を供給させています．

![radicon_circuit.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2607628/d49afa51-552d-e713-d7e6-c3f8df5da365.png)

# 早速動かしてみた

プログラムを書き込み，回路を組んでラジコンを動かしてみました！

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">例のラジコンを <a href="https://twitter.com/hashtag/ROS2?src=hash&amp;ref_src=twsrc%5Etfw">#ROS2</a> + micro-ROSで動かしてみました！<br><br>teleop_twist_joyパッケージで/joyを/cmd_velに変換してM5Stack Core2に送っています<br><br>ソースコードの細かい部分や回路については後日Qiitaにまとめる予定です👀<br><br>▼ソースコードあります<a href="https://t.co/5ieHSDFkWq">https://t.co/5ieHSDFkWq</a> <a href="https://t.co/7doNqQK03S">pic.twitter.com/7doNqQK03S</a></p>&mdash; Akinosuke Tsutsumi (@akki_nitkk) <a href="https://twitter.com/akki_nitkk/status/1740709275213967383?ref_src=twsrc%5Etfw">December 29, 2023</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# おわりに

今後はミニPCとLiDAR積んで魔改造していこうと考えています！
USBカメラ(もしくはRealSense)あたりを取り付けて上物の照準を何かに自動追従させるとかも楽しそうです．

この記事が少しでも皆様のものづくりの役に立てれば幸いです．
もし質問等あれば気軽にお問い合わせください．