【JavaScript 2021（2つ目）】Web Bluetooth API で PLAYBULB candle を扱う（2021年12月版、async/await での実装）

この記事は、[2021年の JavaScript のアドベントカレンダー](https://qiita.com/advent-calendar/2021/javascript) の 13日目の記事です。

2014年ごろにいくつか購入していて、たまに使っていた「[PLAYBULB candle](https://www.mipow.com/products/playbulb-candle)」。
スマホアプリから、灯る色を様々変化させたり、光り方を変化させたり（点滅させたり、ゆらめくような感じにしたり）ということができるデバイスです。

こちらは自分で作ったプログラムから BLE で扱うこともできて、昨年の3月ごろに Web Bluetooth API で扱う簡単なテストをやったりしていました。

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">今日のもくもくの成果！<br><br>PLAYBULB Candle に Web Bluetooth API で接続して、Chrome上の Webサイトのボタンから色を変える制御。<br><br> <a href="https://twitter.com/hashtag/online_temple?src=hash&amp;ref_src=twsrc%5Etfw">#online_temple</a> <a href="https://t.co/QzZ9uXlqSv">pic.twitter.com/QzZ9uXlqSv</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1235203960148881409?ref_src=twsrc%5Etfw">March 4, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

その時に参照した以下の内容をあらためて見直したり、今回用に調べた仕様の情報を見て、今回の記事を書きました。

●Control a PLAYBULB candle with Web Bluetooth
　https://codelabs.developers.google.com/codelabs/candle-bluetooth#3

## 仕組みについて
### PLAYBULB candle
今回利用する「PLAYBULB candle」は、Bluetooth　で外部から操作できます。
通常の利用方法だと、スマホ用の「‎PLAYBULB X」という以下のアプリを使って、GUI上から操作をします。
![スマホアプリ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/00211dab-b6b5-9115-09d8-b9c45115f0d9.jpeg)

そして、PLAYBULB candle は自前で実装した Bluetooth　の処理でも扱えて、冒頭に掲載した動画のように「点灯・色の変更」といったことを行うことができます。

#### 追加で調べた情報
冒頭のツイートにあったお試しには、細かな仕様に関する記述がなかったので、今回用に追加で情報をググってみました。

そうすると、ライブラリや非公式の情報っぽいものが見つかりました。

- https://github.com/danielsmith-eu/playbulb-live
- https://github.com/Phhere/Playbulb

### Web Bluetooth API の実装
Web Bluetooth API は、過去に　Qiita の記事を書いた際に「ガジェット系とブラウザの連携」をやる時によく使っています。
例えば、実装の基本的な部分について、今回の記事でも使う async/await による実装は、以下の記事を書いていたりします。

●toio を Web Bluetooth API で制御（「通知・読み出し・書き込み」を行う） - Qiita
　https://qiita.com/youtoy/items/791905964d871ac987d6

おおまかな処理は上記と同じ感じですが、「UUID　の指定」や「送信するバイナリの情報」あたりが LAYBULB candle用になる、という形になります。

#### バイナリ列についての仕様
冒頭に書いていた [Google Codelabs の⑥の部分](https://codelabs.developers.google.com/codelabs/candle-bluetooth#5)を見ると、以下のような仕様に関する記載がありました。
![エフェクトについて＠Google Codelabs.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/a0c1ced9-0749-9d99-f4e0-a6682d4aff8b.jpeg)

他にも、[Google Codelabs の⑦の部分](https://codelabs.developers.google.com/codelabs/candle-bluetooth#6)には、以下のような記載もあります。
![エフェクトについて2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/ad08e2e8-5aff-4a3a-513b-1c92de3b6616.jpeg)

また、追加で調べたページの中の一部で、以下のような記載も見つけられました。

●Playbulb/candle.md at master · Phhere/Playbulb
　https://github.com/Phhere/Playbulb/blob/master/protocols/candle.md
![エフェクトについて（別ページ）.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/048601ce-4650-f51d-b4bf-2f4053bd58b8.jpeg)

これを見ていると、Google Codelabs のページで書かれていた「no Effect」と、「単色のフラッシュエフェクト（mode 00？）」については、記載がなさそうかも、と思いました？

## いろいろなパラメータで試してみる
上記の内容をもとに、色変更と 5つのエフェクトを試すものを実装してみました。

```html
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>
    Web Bluetooth API で PLAYBULB candle を扱う
  </title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.3/css/bulma.css" />
</head>

<body>
  <section class="section">
    <div class="container">
      <h1 class="title">PLAYBULB candle との連携</h1>
      <div class="buttons" style="margin-top: 1.5rem">
        <button class="button is-success is-light" type="button" onclick="connectCandle()">
          接続＋情報取得
        </button>
        <button class="button is-danger is-light" type="button" onclick="changeColor()">
          LEDの色の変更
        </button>
        <button class="button is-info is-light" type="button" onclick="setEffect1()">
          エフェクト1
        </button>
        <button class="button is-info is-light" type="button" onclick="setEffect2()">
          エフェクト2
        </button>
        <button class="button is-info is-light" type="button" onclick="setEffect3()">
          エフェクト3
        </button>
        <button class="button is-info is-light" type="button" onclick="setEffect4()">
          エフェクト4
        </button>
        <button class="button is-info is-light" type="button" onclick="setEffect5()">
          エフェクト5
        </button>
      </div>
    </div>
  </section>

  <script>
    const CANDLE_SERVICE_UUID = 0xFF02;
    const CANDLE_DEVICE_NAME_UUID = 0xFFFF;
    const CANDLE_COLOR_UUID = 0xFFFC;
    const CANDLE_EFFECT_UUID = 0xFFFB;

    let deviceNameCharacteristic,
      ledCharacteristic,
      effectCharacteristic;

    async function connectCandle() {
      try {
        console.log("Requesting Bluetooth Device...");
        const device = await navigator.bluetooth.requestDevice({
          filters: [{ services: [CANDLE_SERVICE_UUID] }],
          optionalServices: ['battery_service'],
        });
        console.log("Connecting to GATT Server...");
        const server = await device.gatt.connect();
        console.log("Getting Service...");
        const service = await server.getPrimaryService(CANDLE_SERVICE_UUID);
        console.log("Getting Characteristic...");
        console.log(await service.getCharacteristics());
        deviceNameCharacteristic = await service.getCharacteristic(
          CANDLE_DEVICE_NAME_UUID
        );
        const deviceNameValue = await deviceNameCharacteristic.readValue();
        // console.log(deviceNameValue);
        const deviceName = (new TextDecoder('utf-8')).decode(deviceNameValue);
        console.log(deviceName);

        const batteryService = await server.getPrimaryService('battery_service');
        const batteryCharacteristic = await batteryService.getCharacteristic('battery_level');
        const batteryValue = await batteryCharacteristic.readValue();
        console.log(batteryValue.getUint8(0));

        ledCharacteristic = await service.getCharacteristic(
          CANDLE_COLOR_UUID
        );
        effectCharacteristic = await service.getCharacteristic(
          CANDLE_EFFECT_UUID
        );

      } catch (error) {
        console.log("Argh! " + error);
      }
    }

    const candle = {
      color: {
        ledOff: [0, 0, 0, 0],
        fullWhite: [255, 0, 0, 0],
        fullRed: [0, 255, 0, 0],
        fullGreen: [0, 0, 255, 0],
        // fullBlue: [0, 0, 0, 255],
        // halfBlue: [0, 0, 0, 128],
      },
      mode: {
        fade: [0x01, 0x00],
        jumpRGB: [0x02, 0x00],
        fadeRGB: [0x03, 0x00],
        candle: [0x04, 0x00],
        noEffect: [0x05, 0x00],
      },
      speed: {
        reallySlow: [0x00, 0x00],
        reallyFast: [0x01, 0x00],
        slower: [0x02, 0x00],
        faster: [0xff, 0x00],
      },
    };

    async function changeColor() {
      if (ledCharacteristic) {
        // 色変更（CANDLE_COLOR_UUID）
        await ledCharacteristic.writeValue(new Uint8Array([...candle.color.halfBlue]));
        console.log("色変更（単色）");
      }
    }

    async function setEffect1() {
      if (effectCharacteristic) {
        // キャンドルエフェクト
        const color = candle.color.fullGreen;
        const mode = candle.mode.candle;
        const speed = candle.speed.reallyFast;
        await effectCharacteristic.writeValue(new Uint8Array([...color, ...mode, ...speed]));
        console.log("キャンドルエフェクト");
      }
    }

    async function setEffect2() {
      if (effectCharacteristic) {
        // フラッシュエフェクト
        const color = candle.color.fullRed;
        const mode = [0x00, 0x00];
        const speed = [0x1f, 0x00];
        await effectCharacteristic.writeValue(new Uint8Array([...color, ...mode, ...speed]));
        console.log("フラッシュ");
      }
    }

    async function setEffect3() {
      if (effectCharacteristic) {
        // フェードエフェクト
        const color = candle.color.fullWhite;
        const mode = candle.mode.fade;
        const speed = [0x09, 0x00];
        await effectCharacteristic.writeValue(new Uint8Array([...color, ...mode, ...speed]));
        console.log("フェード");
      }
    }

    async function setEffect4() {
      if (effectCharacteristic) {
        // レインボーで点滅
        const color = [1, 0, 0, 0];
        const mode = candle.mode.jumpRGB;
        const speed = candle.speed.reallySlow;
        await effectCharacteristic.writeValue(new Uint8Array([...color, ...mode, ...speed]));
        console.log("レインボーで点滅");
      }
    }

    async function setEffect5() {
      if (effectCharacteristic) {
        // レインボーでフェード
        const color = [1, 0, 0, 0];
        const mode = candle.mode.fadeRGB;
        const speed = candle.speed.reallyFast;
        await effectCharacteristic.writeValue(new Uint8Array([...color, ...mode, ...speed]));
        console.log("レインボーでフェード");
      }
    }
  </script>
</body>

</html>
```

上記を開くと、以下のような画面になります（CSSフレームワークで「[Bulma](https://bulma.io/)」を使っています）。
![WebのUI.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/20937209-0b22-d749-bb00-9bf1d09fb49e.jpeg)

### 試してみた時の様子
上記を動かしてみた時の様子を動画にしました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ネタの仕込みを。<br><br>Web Bluetooth API で PLAYBULB candle を扱う話（デバイスは 2014年とかに買ったやつです）。 <a href="https://t.co/2HsTKBqO37">pic.twitter.com/2HsTKBqO37</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1469556293921026050?ref_src=twsrc%5Etfw">December 11, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

とりあえず、一通りの機能が動作したのを確認できました。

## おわりに
今後、自宅に 3台の PLAYBULB candle があるので、それらを連動させて動かす、というようなことをできればと思います。

<blockquote class="twitter-tweet" data-conversation="none"><p lang="ja" dir="ltr">あとで、まとめて制御というのをやりたいな。 <a href="https://t.co/brZFLM9fQ2">pic.twitter.com/brZFLM9fQ2</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1469558241671933955?ref_src=twsrc%5Etfw">December 11, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ちなみに、過去に「Web Bluetooth API で 6台のデバイス（toio）に同時接続・同時制御」というのはやったことがあるので、3台につなぐところはできるかな、と思っています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">自宅にある <a href="https://twitter.com/hashtag/toio?src=hash&amp;ref_src=twsrc%5Etfw">#toio</a> 6台の同時制御シリーズ、第2弾。<br><br>「6台同時接続＆全てに同じモーター制御を適用」という実装を Web Bluetooth API を使って書きました（実行はブラウザ上）。<br><br>以前、何度も書いていたプログラムの実装を見直してたらできてしまった（今までの実装だと、同時に 3〜4台までだった）。 <a href="https://t.co/NUFRVPEqhV">pic.twitter.com/NUFRVPEqhV</a></p>&mdash; you (@youtoy) <a href="https://twitter.com/youtoy/status/1386360362883182602?ref_src=twsrc%5Etfw">April 25, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>