SwitchBot温湿度計のデータを Web Bluetooth API で取得する【完結編】

この記事は以下の 2つの記事の続きで、完結編となる内容です。

1. [SwitchBot温湿度計を Web Bluetooth API でスキャンする【試行錯誤中】 - Qiita](https://qiita.com/youtoy/items/e28a10866dd0050251c0)
2. [SwitchBot温湿度計のデータを Web Bluetooth API で取得する：【未完】準備や試行錯誤のメモ - Qiita](https://qiita.com/youtoy/items/6abcc9d704aaebcf5b18)

## これまでの流れ
### やろうとしていること
これまでの記事・この記事でやろうとしていることは、以下の「[SwitchBot温湿度計](https://www.switchbot.jp/meter)」の温湿度のデータを Web Bluetooth API を使って取得する、というものです。
![SwitchBot温湿度計](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/b68c7a94-9690-9b2c-6a85-1f71ee15eee9.jpeg)

BLE による温湿度の取得自体は、検索をすると Python・Node.js・Go などで実現した事例が出てきます。しかし Web Bluetooth API を使ったものはなさそうだったので軽い気持ちで試してみようと思ったのがきっかけでした。
そして、思ったより試行錯誤が必要な内容が出てきて、その過程をメモにして残していたら記事が 3つ目となりました。

### これまでに得られた情報など
上記の 2つの記事を書いた中で、調べたり試したりして得られた情報は以下でした。

- SwitchBot温湿度計 の BLE まわりの情報
   - サービスの UUID `cba20d00-224d-11e6-9fb8-0002a5d5c51b`
   - デバイス名 `WoSensorTH `
- Web Bluetooth API関連 
   - スキャンには `navigator.bluetooth.requestLEScan` を用いる
      - 現状 Chrome で利用する場合 `chrome://flags/#enable-experimental-web-platform-features` を Enable にする必要あり
      - 筆者の環境では Mac・Windows では意図した通りに動かないことがあり Androidスマホが安定していそうだった（Androidスマホは Pixie 3）
      - PC の Chrome と Androidスマホの Chrome を連携させる仕組み（ `chrome://inspect/#devices` を開いて使うもの）を活用すると開発が効率的にできそう 

上記の「Web Bluetooth API関連」という項目の下に書いた話は、[過去の記事の 2つ目](https://qiita.com/youtoy/items/6abcc9d704aaebcf5b18)に関連する内容を書いていますので、よろしければご覧ください。

## この記事の内容を試すために必要なもの・設定等
### デバイス関連
この後に書く内容を試す場合、以下が必要になります。

- SwitchBot温湿度計
- PC
- Androidスマホ
- PC と Androidスマホをつなぐ USBケーブル

そして、PC と Androidスマホに関して、以下のアプリ・仕組みを使います。

- PC
   - Chrome
   - ローカルでサーバを立てられる何か
- Androidスマホ
   - Chrome

上記の PC の部分の「ローカルでサーバを立てられる何か」という部分は、過去の他の方の記事でも書かれているワンライナーを使うのがオススメです。
　●ワンライナーWebサーバを集めてみた - Qiita
　　https://qiita.com/sudahiroshi/items/e74d61d939f18779970d
また Androidスマホ関連の補足ですが、この後の手順を同じように進めるためには PC で Androidスマホを USBデバッグできる状態にしておく必要もあります。

なお Androidスマホの要件について、最低限、以下で Web Bluetooth に対応していると出ている Chrome が入っている必要がありそうです。
　●Can I use... Support tables for HTML5, CSS3, etc
　　https://caniuse.com/?search=webbluetooth
![WebBluetoothの対応状況.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/6a4ac845-6d28-82a2-ac48-9fae46822dd3.jpeg)

Androidスマホの Chrome について、「 `chrome://flags/#enable-experimental-web-platform-features` を Enable にして `navigator.bluetooth.requestLEScan` を動作させられるという条件」が、上記と完全に一致するかどうかは不明瞭な部分があります（※ 自分は手元にあった Pixel 3 で試して、動作させられるのを確認できているだけの状況）。

### PC・Androidスマホの Chrome関連の設定まわり
以下の内容を進める前準備として、[前回の記事の「PC の Chrome と Androidスマホの Chrome を連携させる」で書いていた内容](https://qiita.com/youtoy/items/6abcc9d704aaebcf5b18#pc-%E3%81%AE-chrome-%E3%81%A8-android%E3%82%B9%E3%83%9E%E3%83%9B%E3%81%AE-chrome-%E3%82%92%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%82%8B)ができている前提となっています。
なお全く同じ方法でなくても、とりあえず Androidスマホの Chrome のコンソール出力が確認できれば似たような流れで進めていけると思います。

## Androidスマホの Chrome で BLE のスキャンを行う
### Androidスマホの Chrome の設定を確認する
これまでの内容・前準備の確認を兼ねて、Androidスマホの Chrome のコンソール上で `navigator.bluetooth.requestLEScan` を用いたスキャンを試してみます。
再掲になりますが、その処理を実行するためには現状 `chrome://flags/#enable-experimental-web-platform-features` を Enable にする必要ありますので注意してください。

上記の準備がすんでいる PC と Androidスマホ を USB でつないだ状態にして、PC
上で Chrome を開き `chrome://inspect/#devices` を開きます。
![DevTools1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/b5cbb68c-ed77-751e-fcd0-4fa33d7558fa.jpeg)
筆者の環境では上の画像のようなページが開きました。
この中の自分の Androidスマホに該当する部分で、上の赤枠で囲んだ部分があると思います。ここに `chrome://flags/#enable-experimental-web-platform-features` を入力して「Openボタン」を押しましょう。

スマホで以下のページが開くので、Experimental Web Platform features が以下のように Enable になった状態にしてください（切り替えをした際にブラウザの再起動を求められるので、それに従って再起動してください）。
![実験的な機能をONにする.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/652f6b40-b574-2d03-1699-3c0e6bcafc01.jpeg)
なお上記の操作は、もちろん Androidスマホ側の Chrome で URL入力を行うなどして進めても問題ありません。

ちなみに、上記の Androidスマホの画面をキャプチャする際には「[scrcpy](https://github.com/Genymobile/scrcpy)」を利用しているのですが、この scrcpy も画面のミラーリングだけでなく PC からの Androidスマホの操作を行う仕組みとして利用可能です。

### Androidスマホの Chrome で簡単なスキャンを試す
ここで前回の記事で使ったソースコードに少し変更を加えた（ `navigator.bluetooth.addEventListener` の部分を追加した）以下の内容で、簡単なスキャンを試します。

```javascript
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Web Bluetooth API によるスキャン</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.css"
    />
  </head>
  <body>
    <section class="section">
      <div class="container">
        <h1 class="title">操作用ボタン</h1>
        <div class="buttons" style="margin-top: 1.5rem">
          <button
            class="button is-success is-light"
            type="button"
            onclick="onStartButtonClick()"
          >
            スキャン
          </button>
        </div>
      </div>
    </section>

    <script>
      async function onStartButtonClick() {
        await navigator.bluetooth
          .requestLEScan({
            filters: [{ services: ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] }],
            keepRepeatedDevices: true,
          })
          .then(() => {
            navigator.bluetooth.addEventListener(
              "advertisementreceived",
              (event) => {
                console.log(event);
              }
            );
          });
      }
    </script>
  </body>
</html>
```

#### PC上にローカルサーバーをたてる
前回の記事では Web上に上記の内容を書いた HTMLファイルををアップして、Androidスマホの Chrome で開きました。今回はそれと異なり、PC上でにファイルを置いた状態で Androidスマホの Chrome からアクセスできるようにします。

その準備として、PC でローカルのサーバーをたててから、上記の内容を書いた HTMLファイルをローカルのサーバ経由でアクセスできるようにしましょう。
自分の場合は、以下の Python 3.x用のワンライナーでサーバーをたてました。

```bash
python -m http.server 8000
```

PC上の Chrome で `http://localhost:8000/` のアドレスで、上記の HTMLファイルにアクセスできていれば OK です。以下のようなテキストとボタンが表示されると思います。
![Webサイトの表示.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/60b97b4b-592d-9f13-65a7-469035898071.jpeg)

#### Androidスマホの Chrome で PC の localhost にアクセスする
それでは PC の Chrome でアクセスを試した HTMLファイルを、今度は Androidスマホの Chrome で開けるようにしていきます。

PC の Chrome で `chrome://inspect/#devices` にアクセスして開いたページで、以下の赤矢印で示したボタンを押します。
![ポートフォワードの設定の準備.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/e77ace15-ae42-6b4e-a854-9a7cb52d3137.jpeg)
「Port Forwarding Setting」の画面が開くので、以下の赤線で示したような「Port」と「IP address and port」の設定を行ってください。「IP address and port」のほうは上記の PC でたてたローカルサーバーの設定に合わせたものを入れてください（※ 自分の場合は localhost:8000）。そして Port はその設定のポート番号と合わせておくのが分かりやすいと思います。
![ポートフォワードの設定.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/a1b107a2-2d56-2b8f-5ac7-8ec373a6341f.jpeg)
また、画面左下の「Enable port forwarding」にチェックを入れてから、最後に右下にある「Doneボタン」を押してください。

以下のような表示が出ていれば設定完了です。
![ポートフォワードの設定後.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/74ae51dd-d2e7-ec11-a648-3a856e6c882f.jpeg)

あとは、PC側の操作でも Androidスマホ側の操作でも良いので、Androidスマホの Chrome で `localhost:8000` を開いて、PC で表示したのと同じページにアクセスできるのを確認してください。
![Androidスマホでテスト用ページを表示.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/9744afb9-7c90-967b-c13f-afc8b02dbd7f.jpeg)

そして PC側の Chrome で、以下にある Inspect を押してウィンドウを開いておいてください。
![テスト用ページにInspect.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/8fd63906-1735-23ee-ba60-b28a9b1054b1.jpeg)
自分の場合は、以下のウィンドウが表示されました。
![PC側でAndroidスマホのデバッグ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/0a3d489f-7dc1-ef52-f850-110365e67428.jpeg)
そして、Androidスマホの Chrome上で表示されたページのスキャンボタンを押しましょう。ちなみに、PC側に表示されているボタンを押すことも可能です。

そうすると Androidスマホの Chrome側にのみ、以下の表示が出ていると思います。
ここで少し待ってみて「不明またはサポートされていないデバイス」と表示されるのを確認しましょう。そして「許可ボタン」を押してください。
![スキャンの許可.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/bb21a72e-b6e3-0d19-8845-831c4b4b2909.jpeg)
その後に、PC側のコンソール（Androidスマホの Chrome を表示しているウィンドウのコンソール）を見てみてください。

以下のように、スキャン結果が表示されていれば、ここまでの手順は OK です。
![コンソールでスキャン結果を確認.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/19b6ec19-d9eb-6bad-1261-cd8e9c312be6.jpeg)
なお、上記で折りたたまれて出力されている内容を展開すると、以下のような情報が得られていました。
![eventの内容.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/019aaefd-863a-61c7-8a5b-5c195623b585.jpeg)

### Androidスマホの Chrome で温湿度情報を取得する
#### 温湿度が含まれる場所を確認（仕様の確認）
それでは最後の仕上げを進めるため、仕様の確認を行って取り出すべき情報を特定します。

公式の仕様や他の方が書いた記事を参照してみます。
 ●Meter BLE open API · OpenWonderLabs/python-host Wiki
 　https://github.com/OpenWonderLabs/python-host/wiki/Meter-BLE-open-API

先ほど取得できていたデータの中の「serviceData」に関わる部分からバイナリデータを取り出し、その中の特定のバイト列を処理してやれば温湿度の値を得られそうです。

#### 温湿度を取り出してみる
それでは、上で書いていたソースコードに手を加えて、温湿度の出力も行ってみます。
「serviceData」に含まれる温湿度の内容を含んだバイナリデータを取得する方法をあれこれ調査していたところ、以下の別製品の事例で参考になりそうなものがありました。

●Reading Xiaomi Mi Scale data with Web Bluetooth Scanning API - DEV Community
　https://dev.to/henrylim96/reading-xiaomi-mi-scale-data-with-web-bluetooth-scanning-api-1mb9

具体的には以下の部分です。

```javascript
event.serviceData.forEach((valueDataView) => {
  console.log(valueDataView.buffer);
});
```

上で利用したソースコードに上記の内容を加えたところ、コンソールで 6バイト分のデータが出力されるのが確認できました。仕様通りのデータが得られていそうなので、あとはここに含まれるバイト列の処理を行うだけです。

[公式が提供している Node.js用のライブラリのソースコードの一部（switchbot-advertising.js の 149行目からの内容）](https://github.com/OpenWonderLabs/node-switchbot/blob/master/lib/switchbot-advertising.js#L149)を参考にして実装するのが良さそうでした。
![node-switchbot switchbot-advertising.js の 149行目からの内容](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/2e5a731b-6e82-8241-92e5-9f87fca82226.jpeg)

上記を参考に作った最終版のソースコードは以下のとおりです。
バイナリデータを扱う部分は Node.js のサンプル通りに使えない処理が含まれているため、その部分の書きかえを行っています。

```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Web Bluetooth API によるスキャン</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bulma@0.9.1/css/bulma.css"
    />
  </head>
  <body>
    <section class="section">
      <div class="container">
        <h1 class="title">操作用ボタン</h1>
        <div class="buttons" style="margin-top: 1.5rem">
          <button
            class="button is-success is-light"
            type="button"
            onclick="onStartButtonClick()"
          >
            スキャン
          </button>
        </div>
      </div>
    </section>

    <script>
      async function onStartButtonClick() {
        await navigator.bluetooth
          .requestLEScan({
            filters: [{ services: ["cba20d00-224d-11e6-9fb8-0002a5d5c51b"] }],
            keepRepeatedDevices: true,
            // active: true,
          })
          .then(() => {
            navigator.bluetooth.addEventListener(
              "advertisementreceived",
              (event) => {
                event.serviceData.forEach((valueDataView) => {
                  if (valueDataView.buffer.byteLength === 6) {
                    const buf = new Uint8Array(valueDataView.buffer);
                    // console.log(buf);
                    let byte2 = buf[2];
                    let byte3 = buf[3];
                    let byte4 = buf[4];
                    let byte5 = buf[5];

                    let temp_sign = byte4 & 0b10000000 ? 1 : -1;
                    let temp_c =
                      temp_sign * ((byte4 & 0b01111111) + byte3 / 10);
                    let humidity = byte5 & 0b01111111;

                    console.log(`温度 ${temp_c}℃、 湿度 ${humidity}％`);
                  }
                });
              }
            );
          });
      }
    </script>
  </body>
</html>
```

そして、実行結果は以下のとおりです。
![温湿度の出力.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/b825b929-dcf3-bbfd-6660-828cf6d8056b.jpeg)
SwitchBot温湿度計に表示された値と同じものをコンソールに出力できました。

## まとめ
試行錯誤を続けた結果、無事に 3つ目の記事で完結させられました！

## 参考情報のメモ
本文中には含めていないものの、調査の過程で参照したページです。

- [Web Bluetooth Scanning](https://webbluetoothcg.github.io/web-bluetooth/scanning.html)
- [SwitchBot 温湿度計の測定値を BLE Advertisement パケットから直接読み取る - Qiita](https://qiita.com/warpzone/items/11ec9bef21f5b965bce3)
- [SwitchBot温湿度計をGrafanaで可視化 - kamijin-fanta](https://blog.kamijin-fanta.info/2021/03/temperature-collector/)
- [MacでSwitchBot Meter(温度計)の値を読む | たくのこ Web](https://www.takunoko.com/blog/mac%E3%81%A7switchbot-meter%E6%B8%A9%E5%BA%A6%E8%A8%88%E3%81%AE%E5%80%A4%E3%82%92%E8%AA%AD%E3%82%80/)
- [node-switchbot/switchbot.js](https://github.com/OpenWonderLabs/node-switchbot/blob/6427e72e2d590864014635a732f4f0a65f598f00/lib/switchbot.js)
- [node-switchbot/switchbot-advertising.js](https://github.com/OpenWonderLabs/node-switchbot/blob/master/lib/switchbot-advertising.js)
- [Web Bluetooth Samples](https://googlechrome.github.io/samples/web-bluetooth/)
- [Web Bluetooth / Scanning Sample](https://googlechrome.github.io/samples/web-bluetooth/scan.html)

## 余談
なお、記事の中で少し登場した公式の Node.js 用のライブラリを利用すれば、簡単に温湿度の値を取得できそうでした。
　●OpenWonderLabs/node-switchbot
　　https://github.com/OpenWonderLabs/node-switchbot

準備も簡単で、npm を使ったセットアップは以下だけで完了です。

```bash
$ npm install @abandonware/noble
$ npm install node-switchbot
```

そして、公式ページに書かれたサンプルコードを実行するだけで、以下のように簡単にデータを取得することができました。
![公式ライブラリによるデータ取得.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50868/f9b2aa7e-9599-cfe4-b2b6-3588ffc5f4a0.jpeg)

<details><summary>なお、実行した公式のサンプルコードは以下でした（※ 表示を折りたたんでいます）。</summary><div>

```javascript
// Load the node-switchbot and get a `Switchbot` constructor object
const Switchbot = require('node-switchbot');
// Create an `Switchbot` object
const switchbot = new Switchbot();

(async () => {
  // Start to monitor advertisement packets
  await switchbot.startScan();
  // Set an event hander
  switchbot.onadvertisement = (ad) => {
    console.log(JSON.stringify(ad, null, '  '));
  };
  // Wait 10 seconds
  await switchbot.wait(10000);
  // Stop to monitor
  switchbot.stopScan();
  process.exit();
})();
```

</div></details>