LoginSignup
1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-12-11

この記事は、2021年の JavaScript のアドベントカレンダー の 13日目の記事です。

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

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

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

●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

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

追加で調べた情報

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

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

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 の⑥の部分を見ると、以下のような仕様に関する記載がありました。
エフェクトについて@Google Codelabs.jpg

他にも、Google Codelabs の⑦の部分には、以下のような記載もあります。
エフェクトについて2.jpg

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

●Playbulb/candle.md at master · Phhere/Playbulb
 https://github.com/Phhere/Playbulb/blob/master/protocols/candle.md
エフェクトについて(別ページ).jpg

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

いろいろなパラメータで試してみる

上記の内容をもとに、色変更と 5つのエフェクトを試すものを実装してみました。

<!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」を使っています)。
WebのUI.jpg

試してみた時の様子

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

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

おわりに

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

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

1
1
0

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