SwitchbotのBluetoothAPIが公開されていると聞いたので、obnizと連携できるなと連携させてみました。
あんまりパーツライブラリの開発過程って書いたことないなと思って、今回はライブラリの使い方というよりは作り方を書いてみます。
SwitchbotAPIを読んでみる
SwitchbotのGithubにて、APIが公開されていました。
ちょっとわかりにくいのが、SwitchBotAPIと書いてあるのはwebのapiで、
SwitchBotAPI-BLEのほうがBluetooth APIの仕様書のようです。
手元にあるのがSwitchbot 温度計なので、温度計の仕様を見てみます。
温度計は英語名がMeterっぽいので、Meterのところを見てみます。
目次を見るとできることの全体像がわかりますね。
どうやら、ブロードキャスト(アドバタイズメント)のメッセージと、接続して行うコミュニケーションモードがあるようです
ブロードキャスト(アドバタイズメント)では
- 温湿度データを読む
ができるようです。
コミュニケーションモードでは
- ハードウェアのバージョンを読む
- ディスプレイの表示モードを変える
- ディスプレイの表示モードの状態を読む
- 温湿度データを読む
ができるようですね。
・・・ちょっとコミュニケーションモードではできることが少ない(obnizでやらずとも、アプリで設定すればいいかな)ので、
このデバイスはブロードキャストモードだけ開発してみます。
ブロードキャストモードのフォーマット
ブロードキャストモードでは、この2つができれば十分に利用ができます。
- デバイスがSwitchbot Meterであることを示すデータ
- 温湿度データ
デバイスがSwitchbot Meterであることを示すデータ
デバイスがSwitchbot Meterであることを示すデータを探すと、Advertisementの中にCompanyID(製造メーカーのコード)が入っているようです。これでSwitchbot社のデバイスだ、というところまでは特定できそうです。
写真では会社名がNordicとなっていますが、トップのReadmeにて番号が変わったことが書いてありました。 あとから会社の番号を取得したのですかね。
会社のIDだけではSwitchbotの製品であることがわかっても、どの製品かがわからないので、もうちょっと探してみます。
Service DataのByte 0にDeviceTypeがありますね。
上の方にDeviceTypeの表がありました。これらを組み合わせると一意にDeviceを特定できそうですね。
温湿度データ
こちらはServiceDataの方にまんま書いていてすぐ見つかりますね。
実装してみる
上記でドキュメントのどこにデータがあるかわかったので、それをそのまま実装します
デバイスが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);
};
実験してみる
ちゃんと動いているのが確認できました!
そのうちパーツライブラリ化して公開します。