🐙

AWS Robot Delivery Challenge の際に使用した WebUI(ブラウザーインターフェース)の話

2020/12/21に公開約9,000字

この記事はProcessing Advent Calendar 2020 15日目の記事です。
なお、この記事の公開日は24日です。公開が遅れてしまい申し訳ございません。

はじめに

今更ですが、約3ヶ月前の 2020-09-15 に開催された AWS Robot Delivery Challenge に、dot cube というサークルのメンバー3人で出場しました。
この記事では、その際に私が作成したブラウザーインターフェース(以下、WebUI )に関する話です。
この WebUI ではロボットの位置などを表示するために p5.js を使用しました。

なお、この記事中の WebUI の動画はすべてシミュレータ上で動かしたものになります。

AWS Robot Delivery Challenge とは?

以下、AWS Robot Delivery Challenge 公式ページからの引用です

若者のプログラミング技術や創作意欲を高め、これからの IT 社会を作っていく学生の皆様を支援することを目的に、「AWS Robot Delivery Challenge」を開催します。

従来のようなロボットを一から作り動かすロボコンとは違って、AWS が用意する規定のロボットを皆様が作ったアプリケーションで動かすことで、ミニチュアの街に設置されたコースを走らせ、コース各所に点在する住宅に所定の商品を届けられるかを競います。

このコンテストを見つけた高校、高専、大学、専門学校、大学院に通う学生の皆様が主役です。AWS を初めて体験するという方やアプリケーション開発の経験がそれほどない方向けに、サンプルアプリケーションとシミュレーションマップが配布され、それらを実装する方法をご紹介するオンラインセミナーも実施します。テクノロジーの世界を楽しんで学ぶことができるこの機会、奮ってご参加ください !

WebUI とは?

WebUI とは、ブラウザから AWS IoT を通じてロボットを操作するためのツールです。
運営から配布されたドキュメントやルールブックでは、「ブラウザーインターフェース」や「Web インターフェース」と記述されていました。

WebUI で使用したライブラリなど

  • AWS IoT
  • Vanilla JS
  • p5.js
  • Bootstrap4

WebUI 以外の部分に関して

ロボット側のプログラムは、サンプルプログラムを流用していますが独自に実装した機能 (ROS ノード) がいくつかあります。

ノード名 機能
commander 経路情報や位置、ロボットの状態などを管理し twugo_method に適宜移動指示を送る
twugo_method 指示された座標へ移動する
obstacle_detector ロボット周辺の障害物を検出する
planner 経路情報から最短経路を算出する
remote_console WebUIとの通信を行う

実装時、記事などを書くことを考えていなかったため、内輪ノリで命名してしまった部分があります...

WebUI の紹介

WebUI の画面構成は、以下のようになっています。
画面構成

名称 内容
Vertex 経路情報の一部。IDやチェックポイントか否かなどの情報を持つ
Edge Vertex間の接続を示す
Tolerance この円の範囲内にロボットの中心点が入った場合、ロボットが当該Vertexに到着したとみなす
ロボット ロボットの位置と向きを示す
操作パネル ロボットの操作や、各種情報を表示するためのインターフェース

実際の動作の様子

この動画は、シミュレータ上でロボットを動かしている際の WebUI の画面になります。
障害物を検出し、Edge を削除している様子などを確認できます。

仕組み

WebUIの構成
ロボットと WebUI は AWS IoT を通して通信を行っています。

主な機能

マップの表示

AWS IoT を通じて、ロボットからマップデータを受け取り表示します。
表示するマップには global_costmaplocal_costmap の2種類で、表示するマップの選択も可能です。

これらのマップ情報はそれぞれROS側の /move_base/global_costmap/costmapトピック、/move_base/local_costmap/costmap トピックに Publish されたデータを利用しています。
AWS IoT を通じて WebUI へマップ情報を送信する際には、余白部分を削除して送信しています。

ロボット位置の表示機能

ロボットの位置と向きを表示します。

ロボットの向きと位置はROS側の /odom トピックのデータをそのまま使用しています。
また、表示するロボットのサイズは表示するマップのサイズとロボット本体のサイズから算出しています。

指定した座標への GoTo 機能

何らかの理由で自立走行ができなくなった際に使用する機能です。

以下の画像のようなデフォルトの WebUI の場合、直接ロボットの移動速度(モーターの出力)をリモート操作するか、移動先の座標を数値で指定する必要があります。
しかし、直接ロボットの移動速度をリモート操作した場合、ペナルティーが加算されてしまいます。
また、移動先の座標を数値で指定する方法はあまり効率の良い方法とは言えず、入力を間違える可能性も高いです。

デフォルトの WebUI (Controller)

そこで以下の動画のように、Web UI に表示されているマップをクリックすることで移動先の座標を指定できるようにしました。

経路情報の編集機能(未実装)

こちらは、WebUI から経路情報を直接編集することで、探索によるタイムロスや衝突などの可能性がある経路の削除などを行うための機能になる予定でした。
しかし、実装が間に合いませんでした。

以下の動画では、経路情報を編集するための操作パネルを操作しています。

工夫点

ブラウザのリロードへの対応

ロボットを起動した後に WebUI を起動したりリロードを行うと、Publishする頻度(≒更新頻度)の低いデータがなかなか取得できずに、WebUI に必要な情報を表示することができない問題が発生しました。

Retain 機能がある MQTT ブローカーならば Retain 機能を使えばすぐに対応できたのですが、残念ながら AWS IoT には Retain 機能が無い[1] ようなので、ロボット側(remote_console ノード)で最後にAWS IoT へ Publish したデータを保持しておいて WebUI からの要求に応じて当該データを再度 Publish するようにしました。

有効なボタンと無効なボタンの明確化

以下の画像は、両方ともスタートボタンのみ有効になっており、他のボタンはすべて無効化されています。
ボタンの比較

このように、変更前のBootstrap4 のデフォルトのデザインでは有効なボタンと無効なボタンが分かりづらかったので無効なボタンは枠線のみにし、ボタン名に取り消し線を引くようにしました。

不要な画面更新処理の停止

p5.js はデフォルトで1秒当たりにモニタのリフレッシュレートの数だけ描画のための処理が走る[2]ようです。
しかし、これではデータの更新が無いのに再描画のための処理が走り効率が悪いので、noLoop 関数redraw 関数を用いてデータ更新時のみ描画処理を実行するようにしました。

複数 Canvas を用いたレイヤー構造による更新処理の効率化

WebUIのCanvas部分は、AWS IoT を通してデータを受け取ったときにだけ更新するようにしています。
その際、Canvas が1つしか無い場合更新されたデータ以外のデータの再描画も必要になります。

そこで、3つの Canvas を用意しデータの更新頻度などに応じて描画する Canvas を以下のように分けました。

レイヤー名 描画内容
背面レイヤー マップの描画(更新頻度:中)
中間レイヤー 経路情報の描画(更新頻度:低)
前面レイヤー ロボットの描画(更新頻度:高)

これにより、あるデータが更新されたとき、他の更新されていないデータまで再描画しなければならないといったことを減らすことができました。

レイヤー構造

ウィンドウサイズへの動的な変更への対応

デバッグで開発者ツールを使用する際、表示が崩れないようにするためにウィンドウサイズの動的な変更に対応しました。

操作パネルのドラッグ・最小化・非表示機能

以下の動画のように、一部の操作パネルをドラッグ&ドロップで自由に配置できるようにしました。

具体的な実装は以下のようになっています。
ドラッグ可能な要素は属性としてdraggable="true"を設定して、CSS でposition: absoluteを適用しています。

JavaScript では、ドラッグイベントが発生しイベントターゲットの属性にdraggable="true"が設定してあった場合、イベントターゲットの style 属性にページ全体でのマウスの位置座標からオフセットを引いたものを設定しています。

ソースコード:HTML

GitHub

browser/index.html
    <div class="container-fluid p-0 draggable-card-parent">
        <div class="card console-card" id="card-console-editor" draggable="true" hidden>
            [省略]
        </div>
    </div>
ソースコード:CSS

GitHub

browser/css/remote_console.css
.console-card {
    position: absolute;
    cursor: move;
    min-width: 300px;
    max-width: 20%;
    top: 300px;
    left: 300px;
    z-index: 20;
}

.draggable-card-parent {
    position: absolute;
    top: 0px;
    left: 0px;
    height: 0px;
    background: transparent;
}
ソースコード:JavaScript

GitHub

browser/js/remote-console.js
/**** 自由にドラッグして配置できるUIを実現するための処理 ****/
let beforeX = null;  // ドラッグ Event の一番最後の座標が(0, 0)になってしまう問題を回避するために使用
let beforeY = null;  // ドラッグ Event の一番最後の座標が(0, 0)になってしまう問題を回避するために使用
let offsetX = 0;  // ドラッグ Event 開始時の、対象 Element を基準としたマウスの位置を格納
let offsetY = 0; // ドラッグ Event 開始時の、対象 Element を基準としたマウスの位置を格納
document.addEventListener("drag", function (event) {
    if (!event.target.hasAttribute("draggable")) {
        return;
    }
    if (beforeX === null || beforeY === null) {
        event.target.style.left = String(event.pageX - offsetX) + "px";
        event.target.style.top = String(event.pageY - offsetY) + "px";
    } else {
        event.target.style.left = beforeX;
        event.target.style.top = beforeY;
    }
    beforeX = String(event.pageX - offsetX) + "px";
    beforeY = String(event.pageY - offsetY) + "px";
}, false);

document.addEventListener("dragstart", function (event) {
    if (!event.target.hasAttribute("draggable")) {
        return;
    }
    offsetX = event.offsetX;
    offsetY = event.offsetY;
    event.target.style.opacity = 0.5;  // 不透明度の設定
}, false);

document.addEventListener("dragend", function (event) {
    if (!event.target.hasAttribute("draggable")) {
        return;
    }
    beforeX = null;
    beforeY = null;
    event.target.style.opacity = 1;  // 不透明度の設定
}, false);

失敗したこと

実は、モード選択機能というものも実装していました。
本選と決勝戦では、ルールが若干異なり障害物の配置が予選と同じものになります。ただし、ルールは本選と同じです。
そこで、決勝戦のために走行経路などを最適化したものを決勝戦モードとして用意しました。

しかし、我々は決勝戦は予選と同じルールだと勘違いしたまま決勝戦まで進んでしまいました。
勘違いには、決勝戦で他のチームの走行を見てようやく気づきました...

結局、決勝戦では「本選モード」のまま走らせることになりました。
そして、実際に走行させると、Amazon S3 バケット保存してあるのマップデータをを読み込むことができず、満足に走行することができませんでした。

おわりに

Processing Advent Calendar なのに、p5.js 要素が少なくなってしまいました...
それにも関わらず、最後まで読んでいただきありがとうございます。

質問やご指摘、もっと良い実装方法などがあれば教えていただけると幸いです。

おまけ動画

再読み込みへの対応


ウィンドウサイズのリサイズへの対応


参考文献








脚注
  1. https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/mqtt.html#mqtt-differences ↩︎

  2. https://p5js.org/reference/#/p5/frameRate ↩︎

Discussion

ログインするとコメントできます