#サマリ
前回に引き続き、toio™️のキューブを使ったメカナムホイール制御する記事ですが、今回は下記のポイントを意識して実現した内容です。
- ブラウザで特定URLにアクセスするだけで使える
- WebBluetoothによるキューブの複数台制御
- Gamepad APIを用いて、DUALSHOCK 4およびJoy-Conで操作可能
- アナログスティック操作による真の全方位移動の実現
成果物
YouTube -> https://youtu.be/LS_c0v6lSh4#toio で作ったメカナムホイール車をブラウザから各種Gamepad(DUALSHOCK 4やJoy-Con)でアナログ操作できるようにしました。#WebBluetooth によるキューブの複数台制御とGamepad APIが技術的なポイントです。
— Tetsunori NAKAYAMA | 中山 哲法 (@tetunori_lego) January 13, 2020
詳しくは下記Qiita記事にて。https://t.co/jiV1dErfVG pic.twitter.com/ibSvGTufhP
#技術方針
##背景
前回toio™のVisual Programming環境で手早くメカナムホイール制御を実現しましたが、キー操作だけでは気持ちよく全方位移動操作できなかったこと、より複雑な制御を実現するにはメンテナンス性に課題があったこと、などから別の手段を検討しました。
その検討の中で、WebBluetoothがWindowsでも十分に使えるようになっていたこと、Gamepad APIが優秀すぎること
恥ずかしながらHTML5のGamepad APIを初めて知ったのですが、超絶便利。DualShock4をBTでPCとつないだだけで、ブラウザからほとんどの機能が使えました。これってみんな知っていることなの?
— Tetsunori NAKAYAMA | 中山 哲法 (@tetunori_lego) December 26, 2019
コチラのサイト↓で動作チェックしました。https://t.co/1zsDt9kY9y pic.twitter.com/IP00t0QvK0
などを知り、上述の目標をたて、『URLにアクセスするだけで使える簡単ツール』をJavaScriptで作成することにしました。
##システム構成
とにかく簡単に。
※当方の確認環境はWindows10 vaio VJP132, Google Chrome. 特別なBTドングル等は不要!
準備
Hardware
-
前回の記事に従ってメカナムホイールカーを組み立ててください。
- 2つのキューブの電源を入れておきます。
- "DUALSHOCK 4"か"Joy-Con"(ただしL/R両方)を用意します。事前にPCのBluetooth設定でペアリングを済ませておいてください。
Software
- こちらのツールを開きましょう。 WebBluetoothはまだ限られたブラウザでしか動作しないので、"Google Chrome"を使うことを強く推奨します。
- GamepadのPS/Homeボタンを押してください。するとすぐに認識され、ツール内のGamepadアイコンが下記の様に白くアクティブになるはずです。
- "CONNECT CUBE 1/2"ボタンを押すと1つずつキューブと接続することができます。
- 接続が完了するとツールの画面が変わります。この画像になったら操作可能です!
- なお、Head/Tailキューブがただしく配置されているか、後述のチェック手段を参考にして確認してください。
画面の説明
操作方法
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;
});
このindex
をnavigator.getGamepads()
の配列インデックスに放り込むと、各アナログスティックやボタンの状態を取得できます。
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
を求める計算式が若干面倒になります。
gArrowAngle = -1 * Math.round( 4 * Math.atan2( gIS.yAxisMove, gIS.xAxisMove) / Math.PI ) * Math.PI/4;
WebBluetooth回り
今回はnoble
に非依存の直書きの構築としました。接続のボタン押下後、こんな感じで接続からLED/Motor制御の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();
}
});
で、実際のコマンド発行はこんな感じですね。
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を接続してみたら、ほぼそのまま使えたのはあまりに衝撃的。接続系、特に無線回りが初めから担保されているのは強力な武器になる。単純にキューブのラジコン操作ができるツールがあったらいいなとも思った。サクッと作ろうかな。
- 今回、スピードの調整を入れたが、
MaxSpeed
60以上の早いスピードで長時間走行していると、レゴ側のゴムタイヤが削られてきて、ゴムのカスがたくさん出てしまった。キューブの中に入らないように気を付けないといけない。本機構の使用は個人の責任で~。
- 引き続き、toio™ならではの制御として、位置座標による自動全方位移動制御も確認してみたい。