このチュートリアルではReactを使ってROSのトピックをインターネット越しにPublish/SubscribeできるWebフロントエンドを、Rowmaというシステムを使って実装していきます。
以下のキャプチャは完成図です。左がReactで作ったWebフロントエンドで右がROS側のターミナルです。
まず/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するノードです。
#!/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
を作成します。
#!/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にアクセスして以下のページが見えたら成功です。
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に接続する必要があります。
ここではConnectionManagerに接続中のロボットのUUID一覧を取得して表示するコードを書きます。なおデフォルトでは https://rowma.moriokalab.com
に設置されている公開サーバーに接続されます。
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が見えるはずです。
1.1 コード解説
最初なのでコードの解説をしておきます。なおここ以降の変更は似た感じなので解説はスキップします。
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<変数の型>(変数の初期値);
で覚えていてよいと思います。
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
に代入しています。
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関数を使うようにしましょう。
{robotUuids.length > 0 && (
robotUuids.map((uuid: string) => {
return(
<option key={uuid} value={uuid}>{uuid}</option>
)
})
)}
次にselectタグ本体です。
<select onChange={handleConnectionListChange}>
ここでonChangeはセレクトタグの中の値がクリックされたタイミングで呼ばれる関数のことで、handleConnectionListChange
関数が呼び出されています。handleConnectionListChange
関数の中では選択されたUUIDをselectedRobotUuid
に代入する処理をしています。
2. ロボットに接続してノード一覧を取得する
まず、useState
を使って選択したロボットを格納できるようにします。
const [robot, setRobot] = useState<any>(null);
次にロボットへの接続とロボット情報の取得を行うhandleConnectClicked
関数の実装です。
ここではまずrowma.connect()
関数を使ってセレクトボックスでConnectionManagerに接続し、接続状態をsetSocket
でsocket
に代入します。そしてrowma.getRobotStatus()
関数を使いロボットの情報を取得しsetRobot
でrobot
に代入します。handleRostopicChange
の中身はあとで書きます。
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に値が入るとその下のセレクトボックスが表示されるようになっています。
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ボタンを押してノード一覧のセレクトボックスが表示されたら成功です。
3. ロボットにトピックをPublishする
ではロボットへ接続する処理も済んだのでいよいよロボットにROSトピックをPublish/Subscribeする実装に移ります。
まず、上で作成したchatterのlistenrノードを起動しておきます。
rosrun chatter listener
listenerの起動ができたら一度RowmaROSを再起動します。
rosrun rowma_ros rowma
ここまでできたら次にコードを変更していきます。まずはuseState
を追加します。selectedTopicName
はセレクトボックス中から選択したトピックの名前です。
const [selectedTopicName, setSelectedTopicName] = useState<string>('');
さっきまでconsole.log
しか実装されていなかったhandleRostopicChange
関数の中身を実装します。ここではsetSelectedTopicName
を使ってセレクトボックス中から選択された値をselectedTopicName
にセットします。
次にhandlePublish
関数を実装します。ここが実際にトピックをロボットに送信する本体です。rowma.publish
関数がトピックの送信を担当する部分になります。
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">
内の一番最後に置きます。
return (
<div className="App">
(略)
{selectedTopicName && (
<button onClick={handlePublish}>
Publish
</button>
)}
</div>
);
これらを追加して保存したらブラウザを開いてlocalhost:3000を見ます。
このキャプチャの左側がReactで作ったWebページで、右側がROSの画面になります。画面上でPublishボタンを押すとlistenerノードがトピックを受け取って出力をしていることが分かります(キャプチャ上ではRowmaROSが受け取っているように見えますがちゃんとlistenerノードが受け取っています。これは開発とキャプチャの都合です。)
画面を隣同士に表示しているとローカルホストでデータをやり取りしているように見えますが、インターネット経由でデータをやり取りしているため、離れた場所でも可能です。
4. ロボットのトピックをSubscribeする
Subscribeは少しだけ複雑になっています。Subscribeを行うにはブラウザから「ロボット内に流れるトピック/xxxxをブラウザに転送せよ」という命令を選択中のロボットに送信する必要があります。
まずはTopicのインターフェイスをimportに追加しておきます。
import Rowma, { Topic } from 'rowma_js';
そして他と同じようにuseState
を追加します。これはSubscribeしたトピックを保存する配列となっています。
const [receivedTopics, setReceivedTopics] = useState<Array<string>>([]);
handleSubscribeButtonClick
内のrowma.setTopicRoute
関数は「ロボット内に流れるトピック/xxxxをブラウザに転送せよ」という命令を選択中のロボットに送信する部分です。rowma.setTopicRoute
関数の概要を以下の図に示しておきます。
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ボタンを押すたびに送ったトピックが表示されているのが見えるはずです。
これもローカルホストでやり取りしているようにも見えますが、データはブラウザ->サーバー->ロボット->サーバー->ブラウザ、というふうに流れています。
では次にROS側でtalkerノードを立ち上げましょう。ROS内でやり取りされているデータがブラウザでSubscribeできていることがわかると思います。
rosrun rowma_ros talker
まとめ
以上Rowmaを使ったROSトピックの送受信をReactで行うチュートリアルでした。
ここで作ったWebフロントエンドはasmsuechan/rowma-react-tutorialに公開しています。
何か問題を発見した方はここにコメントしたり該当するGitHubリポジトリでissueを立てたりしてください。