ROSトピックをインターネット経由で送受信できるWebフロントエンドの作成チュートリアル(React + ROS + Rowma)

このチュートリアルではReactを使ってROSのトピックをインターネット越しにPublish/SubscribeできるWebフロントエンドを、**Rowma**というシステムを使って実装していきます。

以下のキャプチャは完成図です。**左がReactで作ったWebフロントエンドで右がROS側のターミナル**です。
![Peek 2020-05-08 22-50.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/1aab99d2-1964-573a-af60-dcd2fcf1c731.gif)
まず/chatterトピックをSubscribeして**Webブラウザでトピックを受け取ります**。そして/chatterトピックを**Webブラウザからロボットに対してPublish**します。ローカルホストでやり取りしているように見えますがちゃんとインターネット経由でデータを送受信しています。

このチュートリアルにあたってROSに関する知識はあった方がいいですが、Reactの方は無くても大丈夫です。React部分のフォローアップはなるべくするようにします。

ここではReactとは何か？の部分については特に触れませんので知らない人は適当に調べてみてください。雑に言うと**HP作成JavaScriptライブラリ**です。

## Rowmaとは
WebブラウザとROSのつなぎ込みの部分には[Rowma](https://github.com/rowma/rowma)というシステムを使います。
詳しくは[Rowma: ROSロボットネットワーク化システム](https://qiita.com/asmsuechan/items/5f0fe8648ddfca1e5934)で紹介しているのですが、簡単に触れておきます。Rowmaは**ROSベースロボットのネットワーク化を実現するOSS**です。具体的に言うとRowmaネットワークに接続しているロボットやアプリケーション(PCやスマホ、プログラム等)同士で相互にインターネット経由でデータをやり取りできるシステムです。

Rowmaには[JavaScript SDK](https://github.com/rowma/rowma_js)があるのでこのSDKを使って実装していきます。なお**TypeScript対応**です。

## 準備するもの
* インターネットに繋がったROSが動くPC
* インターネットに繋がったnodejsがインストールされたPC

ROSが動くPCとReact開発するPCは同じでも問題ありません。なおReact開発に必要なnodejsのインストールは各自でお願いします。

参考: [React 開発環境構築](https://qiita.com/rspmharada7645/items/25c496aee87973bcc7a5)

## (準備) /chatterノードを作成
まず最初の準備としてここではシンプルな/chatterノードを作成します。以下を実行して`chatter`パッケージを作成します。このノードのベースは[Writing a Simple Publisher and Subscriber (Python)](http://wiki.ros.org/ROS/Tutorials/WritingPublisherSubscriber%28python%29)です。

```bash
cd ~/catkin_ws/src
catkin_create_pkg chatter std_msgs rospy
cd ~/catkin_ws/src/chatter
mkdir scripts
```

`~/catkin_ws/src/chatter/scripts/talker`を作成します。1秒に1回/chatterに文字列をPublishするノードです。

```python:talker
#!/usr/bin/env python
import rospy
from std_msgs.msg import String

def talker():
    pub = rospy.Publisher('chatter', String, queue_size=10)
    rospy.init_node('talker', anonymous=True)
    rate = rospy.Rate(1)
    while not rospy.is_shutdown():
        hello_str = "hello world %s" % rospy.get_time()
        rospy.loginfo(hello_str)
        pub.publish(hello_str)
        rate.sleep()

if __name__ == '__main__':
    talker()
```

次に`~/catkin_ws/src/chatter/scripts/listener`を作成します。

```python:listener
#!/usr/bin/env python
import rospy
from std_msgs.msg import String

def callback(data):
    rospy.loginfo(rospy.get_caller_id() + "I heard %s", data.data)

def listener():
    rospy.init_node('listener', anonymous=True)
    rospy.Subscriber("chatter", String, callback)
    rospy.spin()

if __name__ == '__main__':
    listener()
```

最後に実行権限を付けます。

```bash
chmod +x ~/catkin_ws/src/chatter/scripts/talker
chmod +x ~/catkin_ws/src/chatter/scripts/listener
```

これで完了です。

## RowmaROSのインストールと起動
最初にROS用PCでRowma用ノードを立ち上げておきます。

RowmaROSはRowmaネットワークに接続するためのROSパッケージとなります。起動するだけでワークスペースの情報をサーバーに送信して接続を待ち受けます。

なお、デフォルトだと公開サーバーにロボットを登録するため注意が必要です。

ROS用PCで以下を実行してパッケージをインストールします。ワークスペースのパスにrowma_rosがインストールされます。

```bash
python <(curl "https://raw.githubusercontent.com/rowma/rowma_ros/master/install.py" -s -N)
```

インストールができたらノードを起動します。

```bash
rosrun rowma_ros rowma
```

実行が正しく成功すると以下のような出力が見えると思います。

```bash
$ rosrun rowma_ros rowma
(中略)
rowma_ros version 0.0.3
connection established
Your UUID is: xxxx-xxxx-xxxx
```

このUUIDがロボットのRowmaネットワーク上の識別子となります。あとで使います。

ではこのノードは起動したままでReactを使ったWebフロントエンド実装作業を行います。

## 新規Reactプロジェクトの作成
`create-react-app`というツールを使って新規プロジェクトを作成します。以下を実行してください。実行すると`rowma-react-tutorial/`ディレクトリが作成されます。プロジェクトの作成場所はどこでも構いません。

```bash
npx create-react-app rowma-react-tutorial --template typescript --use-npm
```

完了したら実行します。

```bash
cd rowma-react-tutorial
npm start
```

ブラウザでlocalhost:3000にアクセスして以下のページが見えたら成功です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/2524683c-b68b-5591-566c-864c988110cb.png)

ref: https://create-react-app.dev/docs/adding-typescript/

## rowma_jsのインストール
RowmaのJS用SDKであるrowma_jsをインストールします。rowma-react-tutorialディレクトリにて以下を実行します。

```bash
npm i rowma_js
```

## 1. ConnectionManagerへの接続
やっとコーディングを開始します。ここではReactから**ConnectionManager**というWebSocketサーバーに接続をします。RowmaではConnectionManagerが中心となってロボットを接続したりロボットに対してデータを送信したりします。そこでロボットに対してデータを送信するため、まずはConnectionManagerに接続する必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/e54a5973-5766-a0cb-af50-cec5afba85fd.png)


ここではConnectionManagerに接続中のロボットのUUID一覧を取得して表示するコードを書きます。なおデフォルトでは `https://rowma.moriokalab.com` に設置されている公開サーバーに接続されます。

App.tsxを以下のように変更します。(以下はApp.tsx全文)

```jsx:App.tsx
import React, { useEffect, useState } from 'react';
import './App.css';
import Rowma from 'rowma_js';

function App() {
  const [rowma, setRowma] = useState<any>(null);
  const [robotUuids, setRobotUuids] = useState<Array<string>>([]);
  const [selectedRobotUuid, setSelectedRobotUuid] = useState<string>('');

  useEffect(() => {
    const _rowma = new Rowma();
    setRowma(_rowma);

    _rowma.currentConnectionList().then((connList: any) => {
      setRobotUuids(connList.data.map((robot: any) => robot.uuid));
    })
  }, [])

  const handleConnectionListChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
    setSelectedRobotUuid(event.target.value)
  }

  return (
    <div className="App">
      <select onChange={handleConnectionListChange}>
        <option value=''>{''}</option>
        {robotUuids.length > 0 && (
          robotUuids.map((uuid: string) => {
            return(
              <option key={uuid} value={uuid}>{uuid}</option>
            )
          })
        )}
      </select>
    </div>
  );
}

export default App;
```

ブラウザを見るとこのようになっておりセレクトボックス内に接続中のロボットのUUIDが見えるはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/dfdc740d-ff21-7eaf-154f-548e2c362af2.png)

### 1.1 コード解説
最初なのでコードの解説をしておきます。なおここ以降の変更は似た感じなので解説はスキップします。

```jsx:App.tsx
  const [rowma, setRowma] = useState<any>(null);
  const [robotUuids, setRobotUuids] = useState<Array<string>>([]);
  const [selectedRobotUuid, setSelectedRobotUuid] = useState<string>('');
```

まずこの4行です。これらは共通して`useState`関数を呼び出していますね。これはReactの**ステートフック関数**と呼ばれるもので、コンポーネント中の状態を管理してくれるものです。

これは分かってる人が分かってる人に対してする説明なので、よくわからないという人は**Reactで使う変数のようなもの**だと捉えておいてください。この4行は**変数の宣言**というわけです。

これを使うと何が良いのかというと、**変数の中身が書き換わると自動で表示も書き換わる**ということです。

`useState`の使い方は最初は

```js
const[変数名, 変数に値を代入する関数の名前] = useState<変数の型>(変数の初期値);
```

で覚えていてよいと思います。

---

```jsx:App.tsx
  useEffect(() => {
    const _rowma = new Rowma();
    setRowma(_rowma);

    _rowma.currentConnectionList().then((connList: any) => {
      setRobotUuids(connList.data.map((robot: any) => robot.uuid));
    })
  }, [])
```

次はこの`useEffect`から始まる部分です。これはReact Hooksと呼ばれるもので**Reactコンポーネントのライフサイクルイベントを検知して処理を行えるもの**です。

はい、これも分かってる人が分かってる人に対してする説明です。分からない人は**ページがロードされた時、変数の値が更新された時、ページ移動が起こる時に実行される部分**くらいのものだと捉えておきましょう。

ここでようやく`rowma_js`の関数が出てきました。最初は`currentConnectionList()`です。

これは名前通り現在ConnectionManagerに接続しているロボットの一覧を取得する関数となっていて、ここではuseEffectの中で呼ばれているのでページがロードされたタイミングでロボット一覧を`robotUuids`に代入しています。

---

```jsx:App.tsx
  const handleConnectionListChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
    setSelectedRobotUuid(event.target.value)
  }

  return (
    <div className="App">
      <select onChange={handleConnectionListChange}>
        <option value=''>{''}</option>
        {robotUuids.length > 0 && (
          robotUuids.map((uuid: string) => {
            return(
              <option key={uuid} value={uuid}>{uuid}</option>
            )
          })
        )}
      </select>
    </div>
  );
```

残りの部分です。残りは主にセレクトタグを表示する部分です。

ここで大事なのが以下の部分になります。これは`robotUuids`配列の中の値を全てセレクトボックスの中に表示するためのループです。こういう場合普通のforループやforEach関数を使うとうまくいかない場合が多いので**map関数を使う**ようにしましょう。

```jsx:App.tsx
        {robotUuids.length > 0 && (
          robotUuids.map((uuid: string) => {
            return(
              <option key={uuid} value={uuid}>{uuid}</option>
            )
          })
        )}
```

次にselectタグ本体です。

```jsx:App.tsx
      <select onChange={handleConnectionListChange}>
```

ここでonChangeは**セレクトタグの中の値がクリックされたタイミングで呼ばれる関数**のことで、`handleConnectionListChange`関数が呼び出されています。`handleConnectionListChange`関数の中では選択されたUUIDを`selectedRobotUuid`に代入する処理をしています。

## 2. ロボットに接続してノード一覧を取得する
まず、`useState`を使って選択したロボットを格納できるようにします。

```jsx:App.tsx
  const [robot, setRobot] = useState<any>(null);
```

次にロボットへの接続とロボット情報の取得を行う`handleConnectClicked`関数の実装です。

ここではまず`rowma.connect()`関数を使ってセレクトボックスでConnectionManagerに接続し、接続状態を`setSocket`で`socket`に代入します。そして`rowma.getRobotStatus()`関数を使いロボットの情報を取得し`setRobot`で`robot`に代入します。`handleRostopicChange`の中身はあとで書きます。

```jsx:App.tsx
  const handleConnectClicked = () => {
    rowma.connect(selectedRobotUuid)
    rowma.getRobotStatus(selectedRobotUuid).then((res: any) => {
      setRobot(res.data)
    })
  }

  const handleRostopicChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
    console.log(event)
  }
```

`</select>`の下にbuttonとdivを追加します。Connectボタンはクリックされると`handleConnectClicked`関数が呼び出されてロボットへの接続が行われ、robotに値が入るとその下のセレクトボックスが表示されるようになっています。

```jsx:App.tsx
  return (
    <div className="App">
      <select onChange={handleConnectionListChange}>
        (略)
      </select>
      <button
        disabled={selectedRobotUuid === ''}
        onClick={handleConnectClicked}
      >
        Connect
      </button>
      <div>
        {robot && robot.rostopics.length > 0 && (
      	  <select onChange={handleRostopicChange}>
      	    <option value=''>{''}</option>
            {robot.rostopics.map((node: string) => {
      	      return(
      	        <option key={node} value={node}>{node}</option>
      	      )
      	    })}
      	  </select>
        )}
      </div>
    </div>
  );
```

上を実装してlocalhost:3000にアクセスし、自分のロボットのUUIDを選択してConnectボタンを押してノード一覧のセレクトボックスが表示されたら成功です。

![Peek 2020-05-08 11-23.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/bd4874ac-14fa-9d8d-e77a-65fb3cf52cc6.gif)

## 3. ロボットにトピックをPublishする
ではロボットへ接続する処理も済んだのでいよいよロボットにROSトピックをPublish/Subscribeする実装に移ります。

まず、上で作成したchatterのlistenrノードを起動しておきます。

```bash
rosrun chatter listener
```

listenerの起動ができたら一度RowmaROSを再起動します。

```bash
rosrun rowma_ros rowma
```

ここまでできたら次にコードを変更していきます。まずは`useState`を追加します。`selectedTopicName`はセレクトボックス中から選択したトピックの名前です。

```jsx:App.tsx
  const [selectedTopicName, setSelectedTopicName] = useState<string>('');
```

さっきまで`console.log`しか実装されていなかった`handleRostopicChange`関数の中身を実装します。ここでは`setSelectedTopicName`を使ってセレクトボックス中から選択された値を`selectedTopicName`にセットします。

次に`handlePublish`関数を実装します。ここが実際にトピックをロボットに送信する本体です。`rowma.publish`関数がトピックの送信を担当する部分になります。

```jsx:App.tsx
  const handleRostopicChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
    setSelectedTopicName(event.target.value)
  }

  const handlePublish = () => {
    const currentTime = new Date()
    const msg = { "data": `[${currentTime.toUTCString()}] Topic from browser!` }
    rowma.publish(selectedRobotUuid, selectedTopicName, msg)
  }
```

`{selectedTopicName && (`から`)}`の5行を`<div className="App">`内の一番最後に置きます。

```jsx:App.tsx
  return (
    <div className="App">
      (略)
      {selectedTopicName && (
        <button onClick={handlePublish}>
          Publish
        </button>
      )}
    </div>
  );
```

これらを追加して保存したらブラウザを開いてlocalhost:3000を見ます。

このキャプチャの**左側がReactで作ったWebページで、右側がROSの画面**になります。画面上でPublishボタンを押すとlistenerノードがトピックを受け取って出力をしていることが分かります(キャプチャ上ではRowmaROSが受け取っているように見えますがちゃんとlistenerノードが受け取っています。これは開発とキャプチャの都合です。)

画面を隣同士に表示しているとローカルホストでデータをやり取りしているように見えますが、**インターネット経由でデータをやり取りしているため、離れた場所でも可能**です。

![Peek 2020-05-08 11-52.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/58dab300-86ee-1f7d-2e3c-b945c9ad8c51.gif)

## 4. ロボットのトピックをSubscribeする
Subscribeは少しだけ複雑になっています。Subscribeを行うにはブラウザから「ロボット内に流れるトピック/xxxxをブラウザに転送せよ」という命令を選択中のロボットに送信する必要があります。

![image (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/af232e0b-af84-8b53-c410-5a7030c084b3.png)

まずはTopicのインターフェイスをimportに追加しておきます。

```jsx
import Rowma, { Topic } from 'rowma_js';
```

そして他と同じように`useState`を追加します。これはSubscribeしたトピックを保存する配列となっています。

```jsx
  const [receivedTopics, setReceivedTopics] = useState<Array<string>>([]);
```

`handleSubscribeButtonClick`内の`rowma.setTopicRoute`関数は「ロボット内に流れるトピック/xxxxをブラウザに転送せよ」という命令を選択中のロボットに送信する部分です。`rowma.setTopicRoute`関数の概要を以下の図に示しておきます。

![image (6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/e2755ffe-f42d-8ae4-78ab-3b5dd25a8daf.png)

`handleTopicArrival`はトピック受信時に実行される関数です。ここでは`receiveTopics`配列に受信したトピックをpushしています。なお`useState`で作った配列のpushは以下のように行うので気をつけてください。

```jsx
  const handleSubscribeButtonClick = () => {
    rowma.setTopicRoute(selectedRobotUuid, 'application', rowma.uuid, selectedTopicName);
    rowma.subscribe(selectedTopicName, handleTopicArrival)
  }

  const handleTopicArrival = (event: Topic) => {
    setReceivedTopics(topics => [...topics, JSON.stringify(event.msg)])
  }
```

次に要素を追加します。Publishボタンの隣に`<button>`要素と受信したトピックをそのまま表示する`<div>`領域を追加します。

```jsx
      {selectedTopicName && (
        <div>
          <button onClick={handlePublish}>
            Publish
          </button>
          <button onClick={handleSubscribeButtonClick}>
            Start Subscribe
          </button>
        </div>
      )}
      {receivedTopics.map((topic: string, index: number) => (
        <div key={index}>
          <span>{topic}</span>
        </div>
      ))}
```

実装が終わったら以下のキャプチャのようになります。Publishボタンを押すたびに送ったトピックが表示されているのが見えるはずです。

これもローカルホストでやり取りしているようにも見えますが、データはブラウザ->サーバー->ロボット->サーバー->ブラウザ、というふうに流れています。

![Peek 2020-05-08 17-49.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/0ce7698b-b1c1-6cbb-aed3-f9d772f715de.gif)

では次にROS側でtalkerノードを立ち上げましょう。ROS内でやり取りされているデータがブラウザでSubscribeできていることがわかると思います。

```bash
rosrun rowma_ros talker
```

![Peek 2020-05-08 23-01.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131581/6ce0c9df-8054-6147-a4d6-4273c05f6985.gif)


## まとめ
以上Rowmaを使ったROSトピックの送受信をReactで行うチュートリアルでした。

ここで作ったWebフロントエンドは[asmsuechan/rowma-react-tutorial](https://github.com/asmsuechan/rowma-react-tutorial)に公開しています。

何か問題を発見した方はここにコメントしたり該当するGitHubリポジトリでissueを立てたりしてください。