6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-01

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

以下のキャプチャは完成図です。左がReactで作ったWebフロントエンドで右がROS側のターミナルです。
Peek 2020-05-08 22-50.gif
まず/chatterトピックをSubscribeしてWebブラウザでトピックを受け取ります。そして/chatterトピックをWebブラウザからロボットに対してPublishします。ローカルホストでやり取りしているように見えますがちゃんとインターネット経由でデータを送受信しています。

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

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

Rowmaとは

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

RowmaにはJavaScript SDKがあるのでこのSDKを使って実装していきます。なおTypeScript対応です。

準備するもの

  • インターネットに繋がったROSが動くPC
  • インターネットに繋がったnodejsがインストールされたPC

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

参考: React 開発環境構築

(準備) /chatterノードを作成

まず最初の準備としてここではシンプルな/chatterノードを作成します。以下を実行してchatterパッケージを作成します。このノードのベースはWriting a Simple Publisher and Subscriber (Python)です。

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するノードです。

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を作成します。

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()

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

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がインストールされます。

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

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

rosrun rowma_ros rowma

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

$ 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/ディレクトリが作成されます。プロジェクトの作成場所はどこでも構いません。

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

完了したら実行します。

cd rowma-react-tutorial
npm start

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

image.png

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

rowma_jsのインストール

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

npm i rowma_js

1. ConnectionManagerへの接続

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

image.png

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

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

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

1.1 コード解説

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

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の使い方は最初は

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

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


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に代入しています。


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関数を使うようにしましょう。

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

次にselectタグ本体です。

App.tsx
      <select onChange={handleConnectionListChange}>

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

2. ロボットに接続してノード一覧を取得する

まず、useStateを使って選択したロボットを格納できるようにします。

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

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

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

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に値が入るとその下のセレクトボックスが表示されるようになっています。

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

3. ロボットにトピックをPublishする

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

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

rosrun chatter listener

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

rosrun rowma_ros rowma

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

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

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

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

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">内の一番最後に置きます。

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

4. ロボットのトピックをSubscribeする

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

image (1).png

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

import Rowma, { Topic } from 'rowma_js';

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

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

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

image (6).png

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

  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>領域を追加します。

      {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

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

rosrun rowma_ros talker

Peek 2020-05-08 23-01.gif

まとめ

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

ここで作ったWebフロントエンドはasmsuechan/rowma-react-tutorialに公開しています。

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

6
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?