Web BluetoothによるU.F.O. SAの制御

# はじめに

せっかくUFOSAを購入したのでBluetoothで制御してみました。
WebブラウザってなんでもできるからBluetoothも使えるんじゃないかと調べてみたら、やはりWeb Bluetoothがありました。

UFOSAのBluetooth通信の仕様やWeb Bluetoothを用いた実装方法を解説していきます。

## UFOSAとは

U.F.O. SA とは、A10サイクロンSAと同様にBluetoothに対応した性家電です。
ハードウェアの性能はもちろん、動画・音声に連動することもできるハイテクマシン！

専用無線アダプタをPCに差し込んで、CSVファイルと動画ファイルをVORZE Playerで再生すると、動画に連動してUFOSAが動きま…せんでした。非公式プレイヤーなら動きました。（VORZEェ…）
CSVファイル(play file)の仕様は「PLEASE READ FIRST」PDFファイルにあります。
https://vorzeinteractive.com/download

なお専用無線アダプタは別売になりました。（VORZEェ…）

また、スマホからもVorze ControllerでUFOSAを操作できます。（できませんでした。VORZEェ…）

# デモ

UFOSAに接続して回転方向とスピードを操作できるシンプルなコントローラーを作りました。

https://yotto4.github.io/WebSaController/

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">UFOSAを操作するシンプルなコントローラーを公開しました！<br><br>Web Bluetooth対応ブラウザで利用可能です<a href="https://t.co/i6uwTVUx4f">https://t.co/i6uwTVUx4f</a> <a href="https://t.co/YDaQfQDqFK">pic.twitter.com/YDaQfQDqFK</a></p>&mdash; ヨット (@yotto_) <a href="https://twitter.com/yotto_/status/1259133931367985152?ref_src=twsrc%5Etfw">May 9, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


# 動作環境

- UFOSA
- ZenFone 5Z (Android 10, Chrome 81.0.4044.138)
- ZenFone 2 (Android 6.0.1, Chrome 81.0.4044.138)

PCにBluetoothがなかったので、スマホのみ動作確認しました。
A10サイクロンSAは動作確認していませんが、ネットの情報をもとに対応したのでヨシ！

# 用意するもの

- UFOSA または A10サイクロンSA
- Bluetoothが使えるスマホ、PCなど
- Web Bluetoothに対応したブラウザ

Web Bluetoothに対応しているのは一部ブラウザのみです。
https://caniuse.com/#feat=mdn-api_bluetooth
Chrome for Androidは対応していますが、iOS Safariが未対応なのはつらい！

専用無線アダプタ(別売)は今回使用しませんが、他のツールでは大変お世話になるので一緒に購入するのがおすすめです。

# 実装

## UFOSAに接続

```javascript
const SA_SERVICE_UUID = '40ee1111-63ec-4b7f-8ce7-712efd55b90e';
const SA_CHARACTERISTIC_UUID = '40ee2222-63ec-4b7f-8ce7-712efd55b90e';

// デバイス選択画面
const device = await navigator.bluetooth.requestDevice({
  filters: [
    { services: [SA_SERVICE_UUID] }
  ]
});

// 選択したデバイスに接続する
const server = await device.gatt.connect();
const service = await server.getPrimaryService(SA_SERVICE_UUID);
const characteristic = await service.getCharacteristic(SA_CHARACTERISTIC_UUID);
```

`requestDevice`でBluetoothデバイス一覧が画面に表示され、画面上で選択できます。

UFOSAの電源が入っていて未接続かつ回転スピード0のとき接続可能です。
Bluetoothで「UFOSA」という名前で堂々と公開してるのでわかりやすいですね！

ServiceにはSAシリーズ独自のUUIDが使われています。こちらの記事が大変参考になりました！
[Bluetooth Low EnergyによるA10サイクロンSAの制御](https://qiita.com/tnayuki/items/d81609454cb87534e681)
`filters:`でそのServiceを指定することで、SAシリーズのみが一覧に表示されてわかりやすくなるのと同時に、接続後に`getPrimaryService`でそのServiceが使えるようになります。


## 回転する

```javascript
const direction = 0; // 0 or 1
const speed = 15;  // 0~100
const SA_DEVICE_ID_BYTE = {
  CycSA: 0x01,
  UFOSA: 0x02,
};
const idByte = SA_DEVICE_ID_BYTE[device.name];
const directionAndSpeed = (direction << 7) | speed;
const value = new Uint8Array([idByte, 0x01, directionAndSpeed]);

await characteristic.writeValue(value);
```

接続した直後は回転スピード0の状態ですが、ある3バイトを書き込むことで回転スピードを変更できます。

例えば[0x02, 0x01, 0x0f]を書き込むとUFOSAが時計回りにスピード15%で回転します。

0バイト目はほぼ固定、
1バイト目は固定です。
2バイト目の先頭1ビットは回転方向、残り7ビットは回転スピードです。
回転方向は0または1、スピードは0〜100です。（動画連動のCSVファイルと同じですね）


## 切断イベント

```javascript
device.addEventListener('gattserverdisconnected', onDisconnect);

function onDisconnect() {
  alert('disconnected');
}
```

BluetoothをオフにしたときやUFOSAの電源を切ると切断されます。
切断後に再度接続できるように実装すると使いやすくなるでしょう。

# 注意点

実装中につまづいてしまった点と解決方法を紹介します。

## HTTPサイトでWeb Bluetoothが使えない

html, jsファイルを作ってスマホで開くためにHTTPサーバを建てると思いますが、残念ながらHTTPSでないとWeb Bluetoothが使えません。
ただしローカルのファイルならOKなので、スマホにファイルを入れて開けばOKです。

localhostならHTTPSでなくてもWeb Bluetoothが使えます。
AndroidのUSBデバッグを有効にして、PCのChromeまたはEdgeで chrome://inspect の「Port forwarding...」を利用すると、Android ChromeからlocalhostでPCのHTTPサーバにアクセスできます。

ローカル サーバーへのアクセス
https://developers.google.com/web/tools/chrome-devtools/remote-debugging/local-server?hl=ja

## 0バイト目はデバイスID

UFOSAに対して[0x01, 0x01, 0x10]を書き込むとエラーになりました。
0バイト目はデバイスによって変える必要があり、
UFOSAなら[0x02, 0x01, 0x10]で書き込みに成功します。

`device.name`がCycSAなら0x01、UFOSAなら0x02にすると良いでしょう。

ServiceのUUIDでググって見事にヒットしたこちらの記事が大変参考になりました！
[Vorze SA Series (Cyclone A10 SA, UFO SA)](https://github.com/buttplugio/stpihkal/blob/master/stpihkal/protocols/vorze-sa.md)

## writeValue中にwriteValueできない

`writeValue`が完了するまでおよそ0.09秒前後かかりました。

`writeValue`が完了する前に再度`writeValue`を呼び出すとエラーになり、元のスピードのまま回転し続けてしまいます。

```javascript:NG
characteristic.writeValue(value1);
characteristic.writeValue(value2); // まだvalue1を書き込み中なのでエラー、回転スピードはvalue1のまま
```

前の書き込みが完了するまで待つ必要があります。

```javascript:OK
await characteristic.writeValue(value1); // 書き込み完了まで待つ
await characteristic.writeValue(value2); // 完了後に書き換え、回転スピードはvalue2
```

ただし順番待ちを実装すると待機列が長いときに遅延が大きくなってしまいますので、中間は無視して最後尾のみ書き込むと良いと思います。
デモのソースコードでは少し複雑になりましたがそのように実装しました。

## イベントが2重登録される

これはJavaScript全般に言えることですね。

接続後に`addEventListener`で切断イベントを登録していますが、切断して再接続すると切断イベントが2重に登録されてしまい、切断したときにアラートが2回表示されてしまいます。

```javascript:NG
connectButton.addEventListener('click', async () => {

  // UFOSAに接続（略）

  device.addEventListener('gattserverdisconnected', () => {
    alert('disconnected');
  });
});
```

関数を毎回生成するのではなく、関数を一度だけ生成するようにすれば、同じ関数なので2重に登録されません。

```javascript:OK
connectButton.addEventListener('click', async () => {

  // UFOSAに接続（略）

  device.addEventListener('gattserverdisconnected', onDisconnect);
});

function onDisconnect() {
  alert('disconnected');
}
```


# おまけ：nls_command

```javascript
const descriptor = await characteristic.getDescriptor('gatt.characteristic_user_description');
const description = await descriptor.readValue();
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(description);
```

これを実行すると、「nls_command」という文字列が得られます。
我々が書き込んでいる3バイトはnlsコマンドと呼べばいいんでしょうか？
ブランド名のVORZEでも良かった気がしますが、nls専売の意思が感じられます。所在地同じですし。


# おわりに

実装するにあたり、以下の参考URLが大変参考になりました！

完成したのはただのコントローラーですが、これを応用すれば連動する動画・音声プレイヤーはもちろん、連動するゲームなんかも作れるのではないでしょうか？

ソースコードはGitHubで公開しています。
ライセンスは[WTFPL](https://github.com/yotto4/WebSaController/blob/master/LICENSE)なのでどうぞお好きにお使いやがれクソッタレください。
https://github.com/yotto4/WebSaController


# 参考URL

Web Bluetooth Samples
https://googlechrome.github.io/samples/web-bluetooth/

Web Bluetooth
https://webbluetoothcg.github.io/web-bluetooth/

Bluetooth Low EnergyによるA10サイクロンSAの制御
https://qiita.com/tnayuki/items/d81609454cb87534e681

Vorze SA Series (Cyclone A10 SA, UFO SA)
https://github.com/buttplugio/stpihkal/blob/master/stpihkal/protocols/vorze-sa.md