概要
ロボットの開発をしているとC++/pythonあたりを使った開発が多いと思いますが、独立してからweb系の方とちょいちょい関わるようになり、最近その辺も少しずつ勉強しております。
まずはフロントエンドの方から勉強から始めてみようと思い、最近流行っているTypescript+Reactで超シンプルなトピックをリアルタイムにプロットするUIを作ってみました。
フロントエンドに入門したいなと思っているロボットエンジニアの方の参考になればと思っています。
初心者で間違って理解している部分もあると思いますので、気づいた方はご指摘・ご意見頂けると有難いです。
ちなみにROS1想定です。
Typescript + React
フロント界隈ではいろいろなフレームワークが使われているようですが、現在世界的によく使われているのはReactのようです。
https://www.publickey1.jp/blog/21/state_of_javascript_2020reactexpressjest24000.html
Typescriptはプログラム言語でjavascriptにトランスパイルを行って実行するのですが、javascriptにはこのようなトランスパイル言語もいくつかあり、Typescriptはその中で最も使われている言語です。
なんとなく両方を触った感覚で言うと、Typescriptはjavascriptに型を与えるという意味で、python3のtype annotation/mypyみたいな感じで、
Reactはロボット系の人にとってイメージしやすいのはQtかなと思います(言語、書き方とか全然違うけど、stateとかpropertyとかのUIの概念が似てると思った)。
開発ツール
フロントエンドの最初のつまづきポイントと思っているのが、開発ツールの種類の多さだと思います。
以下に今回、フロントで使用する開発ツールの種類と今回使用したツールについてまとめておきます。
パッケージマネージャ
pythonでいうところのpip/poetry、ROSでいうところのvcstoolにあたるものです。
jsではnpmとyarnがよく使われています。yarnの方が高機能らしいですが、今回はnpmを使っています。
パッケージの情報はpackage.json
に保存されます。
モジュールバンドラ
コンパイラ言語におけるmakeに近いような役割のツールです。
フロントエンドではhtmlやjavascript、cssなどいろんな種類のファイルが使われますが、ファイルが多くなるとウェブサーバのファイル転送が重くなるので、ファイルを一つにまとめたり、よく使うタスクを定義したりという役割を持っています。
webpackというツールがよく使われているようなので、こちらを採用しました。
webpack.config.js
という設定ファイルでwebpackの設定を行います。
トランスパイラ
Typescriptで書かれたコードをjavascriptに変換するツールです。
トランスパイルそのものの設定をtsconfig.json
に記述し、実際に行うトランスパイル処理に関してはwebpack.config.js
に記述します。
使用ライブラリ
roslibjs
ROSで通信を行うためのライブラリです。
今回、webpackのバージョンをwebpack5にしたのですが、webpack5とroslibjsの相性が悪く、以下のような問題が発生します。
https://github.com/RobotWebTools/roslibjs/issues/382
これを解決するために、根本の問題となっているパッケージであるwebworkifyをnpmサーバ経由のものではなく、リポジトリ内に含めた修正されたものを参照するようにしました。
plotly
jsのプロットツールです。
他にもChart.jsやhighchartsなどのプロットツールがあるのですが、インタラクティブに拡大したり、点の値を参照したりする機能が豊富そうなのでplotlyを選びました。
さらにplotlyにはReactでコンポーネントとして簡単に使えるreact-plotly.jsというのがあり、今回はそれを用いました。
topicplot
以上のようなツール・ライブラリを用いて、topicplot という名前でリポジトリを作成しました。
ファイル構成は以下のようになっています。
├── .gitignore
├── CMakeLists.txt
├── README.md
├── package-lock.json
├── package.json
├── package.xml
├── public
├── src
│ ├── app
│ │ ├── app.tsx
│ │ └── components
│ │ └── LinePlot.tsx
│ └── templates
│ └── index.html
├── tsconfig.json
├── webpack.config.js
└── webworkify
├── LICENSE
├── index.js
├── package.json
└── readme.markdown
トピックからグラフ描画
topicplotを実際に動かしてみます。
まずはROSのメッセージをwebsocketに流すために以下を実行します。
roslaunch rosbridge_server rosbridge_websocket.launch
次にtopicplotを実行します。topicplotのディレクトリに移動して、以下を実行します。
npm start
ブラウザが立ち上がって、初期に設定した適当なプロットが表示されます。
別のコンソールで以下を実行します。現状描画できるトピック名と型は固定なので注意して下さい。
rostopic pub /example std_msgs/Int32 "data: 9" -r 1
以下のようにリアルタイムで描画が更新されます。
実装
メインのプロット部分のコンポーネントです。ROSのメッセージを受け取って、グラフの更新を行っています。
import React from 'react';
import Plot from 'react-plotly.js';
import { Ros, Topic, Message } from 'roslib';
interface LineData {
x: number[]
y: number[]
mode: string
}
interface LineLayout {
datarevision: number
title: string
}
interface LineState {
line: LineData
layout: LineLayout
revision: number
}
export class LinePlot extends React.Component<{}, LineState> {
ros = new Ros({
url: 'ws://localhost:9090'
});
constructor(props: {}) {
super(props)
this.state = {
line: {
x: [1, 2, 3],
y: [2, 6, 3],
mode: 'lines+markers',
},
layout: {
datarevision: 0,
title: 'A Line Plot'
},
revision: 0,
};
this.updateGraph = this.updateGraph.bind(this);
}
componentDidMount() {
try {
this.ros.on('connection', () => {
console.log('[INFO]: connection');
});
this.ros.on('error', err => {
console.log('[ERROR]: ' + err);
});
this.ros.on('close', () => {
console.log('[INFO]: close');
});
const listener = new Topic({
ros: this.ros,
name: '/example',
messageType: 'std_msgs/Int32'
});
listener.subscribe(message => {
this.updateGraph(message['data']);
});
} catch (e) {
console.log('[ERROR]: ' + e);
}
}
updateGraph(val: number) {
const { line, layout } = this.state;
line.x.push(line.x[line.x.length - 1] + 1);
line.y.push(val);
if (line.x.length >= 20) {
line.x.shift();
line.y.shift();
}
this.setState({ revision: this.state.revision + 1 });
layout.datarevision = this.state.revision + 1;
}
render() {
return (
<Plot
data={[
this.state.line,
]}
layout={this.state.layout}
revision={this.state.revision}
/>
);
}
}
ToDo
最低限のプロットしかできないのでまだまだ改善点が多いです。
- 固定のトピックではなく任意のトピックのメンバを指定できるようにする(rqt_plotみたいな感じ)
- トピックの一覧を見れる
- float/int/uint/boolなどの型に対応する
- 複数のトピックを描画できる
- 複数の変数を指定できる
- 凡例を追加する
- 複数のグラフを作成できる
などなど。
まとめ
UIをブラウザ上で動かせると、異なるOSのPCやスマホでUIが見れて便利なので、ロボット系では有用だと思ってます。
今回作ったプロットツールを少しずつ改良しながら勉強を進めていきたいと思います。
2023/5/5追記
グラフ描画をrechartsを使用し、Reactのクラスではなく、hooksを使用して作るように書き直しました。
https://github.com/neka-nat/topicplot