ROS x GUIの備忘録

About

こういうことをしたので,やったことをメモしておく.

スクリーンショット

デモURL
部内Wikiに投稿した内容のほぼまんまである.大目に見てください

なにがしたいのか

思想として,

  1. ROS側に指令値を送信できること
  2. ROS側の状態をreactiveに可視化できること
  3. iPadの特性をできるだけ活かしたUI設計にすること
  4. UIのフロント部分を簡潔かつ簡単に記述できること

の4点があった.ROSを組み込む場合におけるGUIの開発では,

  • Linuxネイティブ1
    • GTK3
  • マルチプラットフォーム(PC)
    • Qt
    • Pythonを用いた様々(tkinter, simpleGUI, niceGUI, etc)
  • マルチプラットフォーム(PC, モバイル)
    • Web(Electron含む)
    • Unity
  • モバイルプラットフォーム2
    • Kotlin (Android)
    • Java (Android)
    • Swift (iOS)
    • Objective-C (iOS) 3

が定石であろう.今回はiPadで操作できるという条件があったのと,メンテナンス性及び学習コストの観点からWebを選択した.

Pros & Cons

Webの何がいいか

  • 開発がしやすい
    • ベースがHTMLなのでデザイン系はほんとうにどうにでもなる
    • フレームワークと合わせることでバックエンドのことは気にしなくてもよくなる
    • 「GUIの開発あるある」の面倒事が一切ない
      • 謎のGUIエディタでゴニョゴニョするとか
      • イベント駆動めんどくせーとか
      • ビルドおっもとか
      • そういうのがゼロ
    • ホットリロードがラク
      • 後述
  • 驚異のマルチプラットフォーム性
    • ブラウザさえあればどんなデバイスでも動く
      • 罠はあるが
      • Google Pixel Watchでもうごきましたよ()
  • テンションが上がる
    • 思ったUIを思ったとおりにつくれる(小並感)
      • 笑った?でも大事よこういうの

何がだめか

  • ROS側の起動などがやや面倒くさい
    • ROSbridge Serverなどを起動しなければならない
    • 接続に失敗した場合サーバの再起動など煩雑な手間が必要となる
  • 接続の信頼性がネイティブUIよりも低い
    • ネイティブでUIを構築した場合にはない処理が行われたときの信頼性が大幅に劣る
      • ブラウザ上でのリロード
      • 回線が切断されてからの再接続
  • 簡単な処理ならPythonで組んだほうがラク
    • という説もある.俺はTkinter特有の書き方があまり得意ではないので人によると思う

使用技術の選定

というワケで,賛否両論はあると思うがGUIをWebで構築することに踏み切った.今回使用した技術が以下の通り.

ROS関連

Web関連

やりたいことは以下のような感じである.

通信のイメージ

ROS側

Robot Web Tools様が偉大なる仕事をしてくださっているおかげで我々がすることはrosbridge_serverを起動することだけである.したがって

1
2
sudo apt install -y ros-humble-rosbridge-suite
ros2 launch rosbridge_server rosbridge_websocket_launch.xml

以上である.

Web側の実装

ここから実装していくわけだが,備忘録兼チュートリアル記事ということなので簡単に「topicをpubするボタン」と「subした値をreactiveに表示する部分」の2つに絞って書き方の雛形的なものをここに残しておく.

依存関係のインストール

まずは依存環境をインストールする.以下のスクリプトで最速で最新安定版のnodejsをインストールできる.

1
2
3
4
5
6
sudo apt update
sudo apt install -y nodejs npm
sudo npm -g install n
sudo n stable
sudo apt purge nodejs npm
sudo apt autoremove

viteでプロジェクトをつくる.ROSと深く関連付けて管理する場合は適当にROSのパッケージを作成しその中などでやるといいと思う.

1
npm create vite@latest

とすると色々聞かれるので,プロジェクト名を入力し,その後Reactを選択:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
? Select a framework: » - Use arrow-keys. Return to submit.
    Vanilla
    Vue
>   React
    Preact
    Lit
    Svelte
    Solid
    Qwik
    Others

TypeScriptを選択:

1
2
3
4
5
? Select a variant: » - Use arrow-keys. Return to submit.
>   TypeScript
    TypeScript + SWC
    JavaScript
    JavaScript + SWC

するとよい.

1
2
cd プロジェクト名
npm i

で依存関係がインストールされる.
その後はUIフレームワークのmuiをインストールするために

1
npm install @mui/material @emotion/react @emotion/styled

とする.

1
npm run dev

して表示されたlocal IPをブラウザで開き,正常に画面が表示されることを確認できたらApp.tsxの内容を

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { useState } from 'react'
import './App.css'

function App() {
  return (
    <>
    </>
  )
}

export default App

ここまで減らしてよい.

前提知識

Reactの諸々はここでは省く.よさげな日本語のチュートリアルを見つけたので各自参照されたい.

ROSオブジェクトの生成

上の方で

1
2
3
4
5
import ROSLIB from "roslib";

const ros = new ROSLIB.Ros({
  url: "ws://[ROSを走らせるPCのローカルIP]:9090",
});

としてrosインスタンスを生成しておく.

TopicのPub

基本的には

1
2
3
4
5
const Topicのインスタンス名 = new ROSLIB.Topic({
  ros: ros,
  name: "/Topic名",
  messageType: "Topicの型",
});

としてTopicをインスタンス化する.例えば

1
2
3
4
5
const msg = new ROSLIB.Topic({
  ros: ros,
  name: "/msg",
  messageType: "std_msgs/String",
});

といった形である.これを

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function App() {
  const [message, setMessage] = useState("");

  const handleClick = () => {
    const msg = new ROSLIB.Message({
      data: "hello, world",
    });
    topic.publish(msg);
    setMessage("sent");
  };

  return (
    <>
      <button onClick={handleClick}>PUBLISH</button>
      {msg}
    </>
  )
}

とすると押すとPubするボタンの完成である.簡単でしょ?

TopicのSub

ROSのsubという仕組みとReactのreactive componentの相性は最高である.以下のように書くと

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
function App() {
  const [msg, setMsg] = useState(0);
  useEffect(() => {
    const listener = new ROSLIB.Topic({
      ros: ros,
      name: "/topic名",
      messageType: "topicの型",
    });
    listener.subscribe((message: any) => {
      setMsg(messsage.data[0]);
    });
  }, []);
  return (
    <>
      {msg}
    </>
  )
}

指定したtopicをsubすると表示に反映されるようになる.

Exercise

topicをpubするボタンとそのtopicをsubするボタンをつくり,pubしたtopicの内容が反映されることを確認せよ.pubする内容をフォームなどを用いてreactiveに変化させるとなおよい.

解答例

おまけ - iOS Safari向けのもろもろ

画面全体のスクロール禁止

どこから持ってきたか忘れてしまったが,ScrollLock.tsxを作成して

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import React, { useEffect, useCallback } from "react";

const ScrollLock= React.memo(() => {
  useEffect(() => {
    document.addEventListener("touchmove", scrollNo, { passive: false });

    return () => {
      document.removeEventListener("touchmove", scrollNo);
    };
  }, []);
  const scrollNo = useCallback((e: { preventDefault: () => void; }) => {
    e.preventDefault();
  }, []);

  return <></>;
});

export default ScrollLock;

とし,任意の箇所で

1
<ScrollLock />

を挿入するとその場所でのスクロールが禁止される.

フルスクリーンPWA化4

src/index.html<head></head>内に

1
2
3
4
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <meta name="apple-mobile-web-app-title" content="アプリ名">
    <link rel="apple-touch-icon" href="/icon.png"> <!--アイコン画像を指定-->

としてやるとiPadのSafariで「ホーム画面に追加」したときの動作をアプリのようにすることができる.

ここからは?

  • muiには様々なコンポーネントがあるため様々なWeb的表現を駆使したUIをROSにつなぐことが可能である.
  • Robot Web Toolsは他にも様々なWeb用ROSライブラリを提供しているため,UI上に3Dモデルを表示することなども可能である.
    • 安定性は保証できないが…

いかがでしたか?

(略)

参考文献


  1. ネイティブであるかどうかはそのディストリビューションのDesktop environmentに依存する.多くのUbuntuユーザはデフォルトであるGNOMEを用いていることからこのような表記を行っており,例えばKDEを用いているユーザにとってのネイティブツールキットはQtである. ↩︎

  2. これらのプラットフォームにおいてはROSをネイティブサポートしていないため,必然的にWebインタフェースのフロントエンドとして振る舞わせることになる. ↩︎

  3. iOS向けのアプリをビルドするにはmacOSを搭載したマシンが必要である. ↩︎

  4. 厳密にはPWAではないが,便宜上このような書き方をしている. ↩︎

comments powered by Disqus