Switchbot 温度計をobnizに対応した話



SwitchbotのBluetoothAPIが公開されていると聞いたので、obnizと連携できるなと連携させてみました。
あんまりパーツライブラリの開発過程って書いたことないなと思って、今回はライブラリの使い方というよりは作り方を書いてみます。

## SwitchbotAPIを読んでみる

SwitchbotのGithubにて、APIが公開されていました。

ちょっとわかりにくいのが、SwitchBotAPIと書いてあるのはwebのapiで、

https://github.com/OpenWonderLabs/SwitchBotAPI

SwitchBotAPI-BLEのほうがBluetooth APIの仕様書のようです。

https://github.com/OpenWonderLabs/SwitchBotAPI-BLE



手元にあるのがSwitchbot 温度計なので、温度計の仕様を見てみます。
温度計は英語名がMeterっぽいので、Meterのところを見てみます。


目次を見るとできることの全体像がわかりますね。


![SS20231205171345.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/b7bbacf0-1be8-0dc5-cb28-9d2997bc8d62.png)


どうやら、ブロードキャスト（アドバタイズメント）のメッセージと、接続して行うコミュニケーションモードがあるようです

ブロードキャスト（アドバタイズメント）では
- 温湿度データを読む
ができるようです。

コミュニケーションモードでは
- ハードウェアのバージョンを読む
- ディスプレイの表示モードを変える
- ディスプレイの表示モードの状態を読む
- 温湿度データを読む

ができるようですね。

・・・ちょっとコミュニケーションモードではできることが少ない（obnizでやらずとも、アプリで設定すればいいかな）ので、
このデバイスはブロードキャストモードだけ開発してみます。


## ブロードキャストモードのフォーマット

ブロードキャストモードでは、この２つができれば十分に利用ができます。
- デバイスがSwitchbot Meterであることを示すデータ
- 温湿度データ

### デバイスがSwitchbot Meterであることを示すデータ

デバイスがSwitchbot Meterであることを示すデータを探すと、Advertisementの中にCompanyID（製造メーカーのコード）が入っているようです。これでSwitchbot社のデバイスだ、というところまでは特定できそうです。

![SS20231205172652.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/7d061bf5-280f-5227-012f-2783a2294825.png)

写真では会社名がNordicとなっていますが、[トップのReadme](https://github.com/OpenWonderLabs/SwitchBotAPI-BLE/blob/latest/README.md#uuid-update-notes)にて番号が変わったことが書いてありました。 あとから会社の番号を取得したのですかね。

会社のIDだけではSwitchbotの製品であることがわかっても、どの製品かがわからないので、もうちょっと探してみます。
Service DataのByte 0にDeviceTypeがありますね。
![SS20231205173100.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/ec1b3cee-a6d8-8e2b-55c5-ea0dd2bdf4fb.png)


上の方にDeviceTypeの表がありました。これらを組み合わせると一意にDeviceを特定できそうですね。
![SS20231205173236.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/5223e23e-e76a-6d3c-d57a-47f0f13e0080.png)



### 温湿度データ

こちらはServiceDataの方にまんま書いていてすぐ見つかりますね。
![SS20231205173434.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/e64f58f8-e96d-d4cb-5d6b-bdc22227cb59.png)





## 実装してみる

上記でドキュメントのどこにデータがあるかわかったので、それをそのまま実装します

### デバイスがSwitchbot Meterであることを示すデータ

obnizではble.onfindのperipheralのパラメータ、adv_dataとscan_respにブロードキャストのデータが入ってきますが、これは `number[]` です。
配列そのまま使ってもいいのですが、index番号をよく間違えるので、BleAdvBinaryAnalyzerを使っています。
これは、配列のバリデータ & パーサーです。
こういうデータが来るよ！を書いておくと、それに適合しているかを確認して、適合していたら人が読めるようにobjectに分割してくれます。


例えば、この`_deviceAdvAnalyzer`は `[0x02, 0x01, 0x06, 0x0e, 0xff, 0x69, 0x09, ?A1, ?A2, ?A3, ?A4, ?A5, ?A6, ?B1, ?B2, ?B3, ?B4, ?B5]` の形のデータが来たときだけ通過します。`?A1〜?B5`はどの値が来ても大丈夫ですが、先頭が`0x02`以外だった場合はエラーとなります。

```javascript
const _deviceAdvAnalyzer = new BleAdvBinaryAnalyzer()
  .addTarget("flag", [0x02, 0x01, 0x06])
  .groupStart("manufacture")
  .addTarget("length", [0x0e])
  .addTarget("type", [0xff])
  .addTarget("companyId", [0x69, 0x09])
  .addTargetByLength("deviceAddress", 6)
  .addTargetByLength("otherdata", 5)
  .groupEnd();
  
```

そしてこれを下記のような形でobject化してくれます
```javascript
{
  "flag": [0x02, 0x01, 0x06],
  "manufacture": {
     "length": [0x0e],
     "type": [0xff],
     "companyId": [0x69, 0x09],
     "deviceAddress": [?A1, ?A2, ?A3, ?A4, ?A5, ?A6],
     "otherdata":  [?B1, ?B2, ?B3, ?B4, ?B5]
  }
}
```

これを使って判定していきます


```javascript
const _deviceAdvAnalyzer = new BleAdvBinaryAnalyzer()
  .addTarget("flag", [0x02, 0x01, 0x06])
  .groupStart("manufacture")
  .addTarget("length", [0x0e])
  .addTarget("type", [0xff])
  .addTarget("companyId", [0x69, 0x09])
  .addTargetByLength("deviceAddress", 6)
  .addTargetByLength("otherdata", 5)
  .groupEnd();

const _deviceScanRespAnalyzer = new BleAdvBinaryAnalyzer()
  .addTarget("length", [0x09])
  .addTarget("type", [0x16])
  .addTarget("uuid", [0x3d, 0xfd])
  .addTarget("deviceType", [0x54]) // Meter
  .addTargetByLength("meter", 5);

  
console.log("start");
const obniz = new Obniz("xxxxxxxx");

obniz.onconnect = async () => {
  console.log("obniz connected");
  obniz.ble = obniz.ble!;
  await obniz.ble.initWait();

  obniz.ble.scan.onfind! = async (p: BleRemotePeripheral) => {
    const advData = _deviceAdvAnalyzer.getAllData(p.adv_data);
    const scanResp = _deviceScanRespAnalyzer.getAllData(p.scan_resp ?? []);


    if (!advData || !scanResp) return null; // not target device
    console.log("find!");
  }
  await obniz.ble.scan.startWait(undefined, {
    duplicate: false,
    duration: null,
  });
}
```

これで見つけたときだけ`find!`と出すようになりました

### 温湿度データ

パーサーを先程作ったので、ドキュメントとにらめっこしながらバイナリと戦います。
ここは[nodejsのSDK](https://github.com/OpenWonderLabs/node-switchbot)が参考になりますね。

Bit演算なので、jsでは珍しく`&`を使います。
Lintの設定によっては`&&`にしなさいとか言われてしまって、JSでbinaryを扱う少数派には厳しい世界だなと思ってます笑

（onfindだけ抜粋）
```javascript

obniz.ble.scan.onfind! = async (p: BleRemotePeripheral) => {
    const advData = _deviceAdvAnalyzer.getAllData(p.adv_data);
    const scanResp = _deviceScanRespAnalyzer.getAllData(p.scan_resp ?? []);

    if (!advData || !scanResp) return null; // not target device
    console.log("find!");

    const buf = Buffer.from([...scanResp.deviceType, ...scanResp.meter]);
    const byte2 = buf.readUInt8(2);
    const byte3 = buf.readUInt8(3);
    const byte4 = buf.readUInt8(4);
    const byte5 = buf.readUInt8(5);

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

    const data = {
      temperature: temp_c,
      humidity: byte5 & 0b01111111,
      battery: byte2 & 0b01111111,
    };

    console.log(data);
  };
```


## 実験してみる

ちゃんと動いているのが確認できました！

![IMG_1344.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227015/86d1df63-a1d4-5af2-d187-4700968fafa0.jpeg)


そのうちパーツライブラリ化して公開します。