🍇

かたむきコントローラーを作った話

2021/12/13に公開

要約

WebBluetooth を使って toio コアキューブ を動かしてみました。
スマホの傾き具合に応じて toio コアキューブが動きます。
ブラウザで動作するので、キューブとスマホがあれば、(スマホがネットワーク接続されていれば)どこでも動作させることができます。

このページで動作させることができます。ソースコードはこちらです。

実際に動かしている動画はこちら

https://youtu.be/mkp1n02Jn2o

動機

たとえばファミレスの待ち時間。
子供たちは退屈なので、すぐに「ゆーちゅーぶみたい」「すまほでげーむしたい」などと言ってきます。
個人的にそういう時間のつぶし方はあまり好きではありません。
ちょっとした待ち時間にYouTubeやゲームではなく、toioを使って子供たちと一緒に暇つぶしができるものを作りたいと思いました。

作ったものの説明

フレームワークはVue.js

今回、フレームワークには Vue.js を使っています。
Vue.js を使った理由は単純に今まで使ったことがなかったからで、これで Angular, React, Vue の三大フレームワークを制覇できました。(簡単に何かを作っただけで使いこなした訳ではなく、いわゆる『エンジニアの言う「完全に理解した」』の状態です)

キューブと通信する部分は CoreCube.js に集約

WebBluetooth を介して toio コアキューブと通信する部分は src/libc/CoreCube.js に集約しています。

CoreCube.jsには下記機能が実装されています。

  • WebBluetoothでの接続、切断処理
  • キャラクタリスティクスの読み出し
  • キャラクタリスティクスの書き込み
  • キャラクタリスティクスの通知ハンドラ登録

キャラクタリスティクスへの読み書きができているので、あとは技術仕様書の通信仕様を読んで気合を入れて実装すればアプリケーション側でも好きな機能が全て利用できるのですが、もうちょっと簡単に利用するために下記の処理も実装されています。

  • キューブのLEDをつける
  • キューブのモーターを回す(回しっぱなし or 指定時間だけ回す)
  • (専用マット上で)キューブを指定座標へ移動させる

今回は接続切断処理と、LEDをつける処理、単純にモーターを回す処理だけしか使っていません。

CoreCube.jsはこんな感じで使います。

// オブジェクト生成
cube1 = new coreCube();
// 接続:引数は切断時のハンドラ
cube1.connectDevice(disconnectHandler);
// キャラクタリスティクスへのハンドラ追加
cube1.addHandler("motion", motionHandler);
// 切断
cube1.disconnectDevice();

すみません。ロクな説明になっていません。。

Vueでやっていること

WebBluetoothはセキュリティのためらしいですが、ユーザー操作でしか接続開始できません。このため、画面上に接続ボタンを用意しています。
ついでに書くとiOSでは位置情報提供もユーザーの許可操作が必要で、iOSで動作するときには位置情報提供許可ボタンも表示しています。

vue-cli のひな型をそのまんま利用しているので、HelloWorld.vue にまるまる実装しています。とりあえず動かしてみたかったので、ファイル名はHelloWorldのままです。

CoreCubeオブジェクトはcreatedの時に生成しています。

https://github.com/kaz399/toio-tilt-controller/blob/c2c877ce6fae40afbda688592a9e81c9cf4c4865/src/components/HelloWorld.vue#L143-L148

  created: function () {
    console.log("Hello World!");
    console.log("This is a test program!");
    this.logMessages = "";
    this.cube = new coreCube({name: "cube1", logger: this.debugLog});
  },

接続ボタンが押されたら、実際にキューブとの接続を行います。

実はiOSにも対応

「iOSではWebBluetooth使えないじゃないか、何言ってるんだコイツ」と思われた方もいらっしゃるかと思います。それは半分正しくて、iOS上のSafari や Chrome は WebBluetoothを使うことができません。
しかし App には WebBluetooth を野良実装した勇気あるブラウザが2種類ほどあります。
かたむきコントローラーは

に対応しています。

iOS対応苦労話

最初はAndroidやWindowsのChromeで実装しましたが、そのままではWebBLEやBluefyでは動かないという謎の現象に遭遇しました。

動かないシリーズ1:WebBLEで動かない

WebBluetoothを使うとき、navigator.bluetooth.requestDevice でキューブを検索します。
Chromeでは下記のコードで動かしていました。

https://github.com/kaz399/toio-tilt-controller/blob/db777fd5c08226f805f46f3a57175d89e0091eef/src/libs/CoreCube.js#L351-L355

async connectDevice(disconnectHandler) {
      (略)
      const device = await navigator.bluetooth
        .requestDevice({
          filters: [{services: [this.SERVICE]}]
        });
      this.device = device;

しかし、これがWebBLEで動かない。this.deviceを使おうとするとproxyなんちゃらエラーとかいうのが出まくります。

上記コードはnavigator.bluetooth.requestDeviceの結果をthis.deviceに代入せず一度const deviceに代入してからthis.deviceに再代入するという謎実装していてダサいですが、そこを変更してthis.deviceに直接代入しても状況は変わりません。
ダサいコードが原因ではないようです。
(これを書くときにコードを見直して、どうしてこんな実装をしたのかと自分でも疑問に思ってしまいました。。)

ダメな理由は結局不明でしたが、回避実装を見つけることはできました。

下記が回避実装です。

https://github.com/kaz399/toio-tilt-controller/blob/3f5c88b8d8722036de4f6a528dab7404f1fc8354/src/libs/CoreCube.js#L354-L357

// ***********************************************************
// Control toio core cube with web bluetooth

let thisDevice = null;

(中略)

async connectDevice(disconnectHandler) {
      (略)
      thisDevice = await navigator.bluetooth
        .requestDevice({
          filters: [{services: [this.SERVICE]}]
        });

さらにダサさアップのコードになりました。
なんというか全然意味が分かりませんが、観測事実から
WebBLEでは「navigator.bluetooth.requestDeviceの結果をクラス変数に代入してはいけない」
ようです。

これだけではなく、characteristicsなどBLE関係のものをクラス変数にいれると謎の呪いにより正常動作しなくなってしまいます。

細かい理屈はわかりませんが、とりあえずBLE関係のものをやみくもにグローバルスコープの変数に入れておけばWebBLEでも動くようになりました。(超てきとう)

動かないシリーズ2:Bluefyで動かない

WeBBLEで入れた対応だけでは何かが不足しているようでBluefyでは動きませんでした。
こちらは単純で、BluefyではUUIDが大文字という理由だったので、UUIDをtoLowerCase()で小文字にして対応しました。

https://github.com/kaz399/toio-tilt-controller/commit/c2c877ce6fae40afbda688592a9e81c9cf4c4865#diff-c6867aac9271e3c0f4f9781916495227f1f99fe8c24c38a8717f55da914bf25fL391-R397

なんというかとにかく個別対応が面倒なので、AppleはWebBluetoothを正式にサポートしてほしいです。

本来CoreCube.jsは複数キューブに対応していたのですが、今回の雑な個別対応の結果、いまの状態では複数キューブに対応できなくなっていて残念です。
(グローバルスコープ変数に直接代入しないでハッシュで保持するなどの対応を入れればいいだけのはずなので、そのうちやりたいです)

おわりに

雑な実装ですが、とりあえずいつでもどこでも簡単にラジコン的に遊べるものができました。
これからも、ゆるい感じでキューブを使ってみたいと思います。

Discussion