LoginSignup
7
5

More than 3 years have passed since last update.

WebBluetoothとGamepad APIを使ってtoioのメカナムホイールを制御する

Last updated at Posted at 2020-01-13

サマリ

前回に引き続き、toio™️のキューブを使ったメカナムホイール制御する記事ですが、今回は下記のポイントを意識して実現した内容です。

  • ブラウザで特定URLにアクセスするだけで使える
  • WebBluetoothによるキューブの複数台制御
  • Gamepad APIを用いて、DUALSHOCK 4およびJoy-Conで操作可能
  • アナログスティック操作による真の全方位移動の実現

成果物

ツールソースコードscreen_10.png


YouTube ->

技術方針

背景

前回toio™のVisual Programming環境で手早くメカナムホイール制御を実現しましたが、キー操作だけでは気持ちよく全方位移動操作できなかったこと、より複雑な制御を実現するにはメンテナンス性に課題があったこと、などから別の手段を検討しました。
その検討の中で、WebBluetoothがWindowsでも十分に使えるようになっていたこと、Gamepad APIが優秀すぎること


などを知り、上述の目標をたて、『URLにアクセスするだけで使える簡単ツール』をJavaScriptで作成することにしました。

システム構成

とにかく簡単に。
topology.png
※当方の確認環境はWindows10 vaio VJP132, Google Chrome. 特別なBTドングル等は不要!

準備

Hardware

  1. 前回の記事に従ってメカナムホイールカーを組み立ててください。
    gamepads.jpg
  2. 2つのキューブの電源を入れておきます。
  3. "DUALSHOCK 4"か"Joy-Con"(ただしL/R両方)を用意します。事前にPCのBluetooth設定でペアリングを済ませておいてください。
    gamepads.jpg

Software

  1. こちらのツールを開きましょう。 WebBluetoothはまだ限られたブラウザでしか動作しないので、"Google Chrome"を使うことを強く推奨します。
  2. GamepadのPS/Homeボタンを押してください。するとすぐに認識され、ツール内のGamepadアイコンが下記の様に白くアクティブになるはずです。
    105.png

  3. "CONNECT CUBE 1/2"ボタンを押すと1つずつキューブと接続することができます。

  4. 接続が完了するとツールの画面が変わります。この画像になったら操作可能です!
    110.png

  5. なお、Head/Tailキューブがただしく配置されているか、後述のチェック手段を参考にして確認してください。

画面の説明

screen_description.png

操作方法

カテゴリ キー操作 Gamepad操作
DUALSHOCK 4/Joy-Con
スクリーン/UIの例
全方位移動 ↑/↓/←/→組み合わせ8方向 左analog stick
(✕/B buttonを押しながらで強制的に8方向へ補正。この時フレームが赤に。)
200.png
中央(赤い点)を中心に回転 R + ←/→ 右analog stick 201.png
Head/tail(赤い点)を中心に回転 H/T + ←/→ ↑/↓ + 右analog stick 202.png
L/R後輪(赤い点)を中心に回転 D/F + ←/→ ←/→ + 右analog stick 203.png
Head/Tailの交換 Q Option/"+"ボタン Headキューブが白く光ります。
設定リセット ESC PS/Homeボタン
Gamepad選択 N/A PS/Homeボタン
速度調整(離散的) "-"/"+" L1/R1, L/Rボタン 204.png
デフォルト値は60です。
速度調整(連続的) N/A 〇 button + L2トリガー.
(Joy-Con does not support this function.)
205.png
もちろん直接スライダーをいじってもOK。

Working demo movie

YouTube -> https://youtu.be/LS_c0v6lSh4
Twitter -> https://twitter.com/tetunori_lego/status/1216532037898620928?s=20

SWのポイント

gamepad API

こんな感じで接続イベントを受け取れるので、Gamepadのindexを保存しておきます。

接続時のイベントハンドラを登録
  window.addEventListener( "gamepadconnected", ( event ) => {
    // console.log( "Gamepad connected." );
    // console.log( event.gamepad );
    gGamePadIndex = event.gamepad.index;
  });

このindexnavigator.getGamepads()の配列インデックスに放り込むと、各アナログスティックやボタンの状態を取得できます。

各種Gamepadステータスへのアクセス
    const gamePad = navigator.getGamepads()[ gGamePadIndex ];
    gamePad.axes[ GAMEPAD_LEFT_AXIS_X /* 0 */ ];
    gamePad.buttons[ GAMEPAD_BT_DOWN /* 13 */ ].value; 

今回のソースでは、window.requestAnimationFrame()毎に状態を取得しています。
なお接続はしていなくても、ペアリングしてるGamepadが接続イベント時に上がってくることがあるので、今回はすべてのIndexを内部で登録しておき、PS/Homeボタンが押された際に使用するGamepadとみなす処理を入れています。

全方位移動の制御ロジック

制御方向をgArrowAngleとした時、アナログスティックのX/Y座標を下記の様にMath.atan2するだけでOKです。座標系の調整で-1かけたりもしますが。
速さについては、これもアナログスティックから得られる値にMaxSpeed設定値を掛け算してmagnitudeとして算出しています。あとは前回同様のロジックで各タイヤへの制御値を決定します。

全方位移動の制御ロジック
gArrowAngle = -1 * Math.atan2( gIS.yAxisMove, gIS.xAxisMove );

const magnitude = gMaxSpeed * 100 * Math.sqrt( gIS.xAxisMove * gIS.xAxisMove + gIS.yAxisMove * gIS.yAxisMove );

moveSpeedUpRightDownLeft = -1 * Math.round( magnitude * Math.sin( gArrowAngle + Math.PI / 4 ) );
moveSpeedUpLeftDownRight = -1 * Math.round( magnitude * Math.sin( gArrowAngle - Math.PI / 4 ) );

なお、✕/Bボタンを押しながらアナログスティックを動かすと、8方向に補正する処理が入っていますが、その場合はgArrowAngleを求める計算式が若干面倒になります。

全方位移動の制御ロジック:8方向へ強制的に補正
gArrowAngle = -1 * Math.round( 4 * Math.atan2( gIS.yAxisMove, gIS.xAxisMove) / Math.PI ) * Math.PI/4;

WebBluetooth回り

今回はnobleに非依存の直書きの構築としました。接続のボタン押下後、こんな感じで接続からLED/Motor制御のCharacteristicsまでシーケンシャルに取得してしまいます。

WebBluetoothでCube接続からCharacteristics取得まで
const SERVICE_UUID              = '10b20100-5b3b-4571-9508-cf3efcd7bbae';
const MOTOR_CHARCTERISTICS_UUID = '10b20102-5b3b-4571-9508-cf3efcd7bbae';
const LIGHT_CHARCTERISTICS_UUID = '10b20103-5b3b-4571-9508-cf3efcd7bbae';

// Scan only toio Core Cubes
const options = {
    filters: [
        { services: [ SERVICE_UUID ] },
    ],
}

navigator.bluetooth.requestDevice( options ).then( device => {
    cube.device = device;
    disableConnectCubeButton( cube );
    return device.gatt.connect();
}).then( server => {
    cube.server = server;
    return server.getPrimaryService( SERVICE_UUID );
}).then(service => {
    cube.service = service;
    return cube.service.getCharacteristic( MOTOR_CHARCTERISTICS_UUID );
}).then( characteristic => {
    cube.motorChar = characteristic;
    return cube.service.getCharacteristic( LIGHT_CHARCTERISTICS_UUID );
}).then( characteristic => {
    cube.lightChar = characteristic;
    if( ( gCubes.head !== undefined ) && ( gCubes.head.lightChar !== undefined ) && 
        ( gCubes.tail !== undefined ) && ( gCubes.tail.lightChar !== undefined ) ){
        lightHeadCube();
    }
});

で、実際のコマンド発行はこんな感じですね。

WebBluetoothでLED制御コマンド発行
const turnOnLightGreen = ( cube ) => {

    // Green light
    const buf = new Uint8Array([ 0x03, 0x00, 0x01, 0x01, 0x00, 0xFF, 0x00 ]);
    if( ( cube !== undefined ) && ( cube.lightChar !== undefined ) ){
        cube.lightChar.writeValue( buf );
    }

}

所感と考察

  • アナログスティックの全方位移動はやはり気持ちが良い。実は作る前は『アナログスティックではむしろ操作が難しく、むしろ8方向に限定した方が操作しやすいのではないか?』と思っていたのだが、実際に実装して触ってみると、人間は目で見てそのフィードバックを元に細かい調整ができるため、子どもでも操作感の違いが分かるほど簡単になった。
  • 操作をしていると、停止する際?にドリフトが発生してしまうことが多々あり、思い通りに操作できない瞬間がもどかしい。BTの通信や複数キューブ制御のタイミングのズレか?とデバッグしてみたもののそうではないっぽかった。メカナムホイールならではのメカ的な課題とかあるのかな?
  • Gamepad APIについては、本当に超優秀。最初はDUALSHOCK 4だけで実装していて、そういえばと思って隣においてあったJoy-Conを接続してみたら、ほぼそのまま使えたのはあまりに衝撃的。接続系、特に無線回りが初めから担保されているのは強力な武器になる。単純にキューブのラジコン操作ができるツールがあったらいいなとも思った。サクッと作ろうかな。
  • 今回、スピードの調整を入れたが、MaxSpeed60以上の早いスピードで長時間走行していると、レゴ側のゴムタイヤが削られてきて、ゴムのカスがたくさん出てしまった。キューブの中に入らないように気を付けないといけない。本機構の使用は個人の責任で~。
  • 引き続き、toio™ならではの制御として、位置座標による自動全方位移動制御も確認してみたい。
7
5
2

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
7
5