4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

obnizAdvent Calendar 2023

Day 6

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

Last updated at Posted at 2023-12-05

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

SwitchbotAPIを読んでみる

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

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

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

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

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

SS20231205171345.png

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

ブロードキャスト(アドバタイズメント)では

  • 温湿度データを読む
    ができるようです。

コミュニケーションモードでは

  • ハードウェアのバージョンを読む
  • ディスプレイの表示モードを変える
  • ディスプレイの表示モードの状態を読む
  • 温湿度データを読む

ができるようですね。

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

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

ブロードキャストモードでは、この2つができれば十分に利用ができます。

  • デバイスがSwitchbot Meterであることを示すデータ
  • 温湿度データ

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

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

SS20231205172652.png

写真では会社名がNordicとなっていますが、トップのReadmeにて番号が変わったことが書いてありました。 あとから会社の番号を取得したのですかね。

会社のIDだけではSwitchbotの製品であることがわかっても、どの製品かがわからないので、もうちょっと探してみます。
Service DataのByte 0にDeviceTypeがありますね。
SS20231205173100.png

上の方にDeviceTypeの表がありました。これらを組み合わせると一意にDeviceを特定できそうですね。
SS20231205173236.png

温湿度データ

こちらはServiceDataの方にまんま書いていてすぐ見つかりますね。
SS20231205173434.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以外だった場合はエラーとなります。

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化してくれます

{
  "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]
  }
}

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

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が参考になりますね。

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

(onfindだけ抜粋)


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

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

4
3
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
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?