はじめに
BLEのこと全然知らないけど、noble(nodejsのBLEライブラリ)とnode-linkig(Linkingデバイス用ライブラリ)があるから、Linkingデバイスが簡単に使えるね!
そんなふうに考えていた時期が自分にもありました。
以下、node-red-contrib-linking-deviceの開発中にいろいろと試行錯誤した記録です。Raspberry Pi3とLinkingデバイスでの話なので、他の組み合わせでは違う場合もあるかと思いますが、検索してみるとよくある話のようにも思われます。
なお、Bluetoothプログラムは初めてなので、誤解してるところなどあればコメントいただけるとありがたいです。
用語説明:
- BLE: Bluetooth Low Energy.
- Linkingデバイス: ドコモが主導してるっぽいいわゆるIoTなBLEデバイス。無線温度センサーが1600円で買える。基本はAndroid, iOS用だけどLinuxからも使える。
- noble: nodejsのBLEライブラリ
- node-linking: Linkingデバイスを利用するための、nodejsで書かれたライブラリ。nobleを使用。
- node-red-contrib-linking-device: 更にそれをnode-redで簡単に使うためのカスタムノード。自作。
スキャン状態でデバイスに接続しようとするとスキャンが勝手に停止する
まず最初の洗礼がこれ。どうやら、そういうものらしい。
参考: noble: Adapter specific known issues
参考: nobleで複数のBLEデバイスを扱う際のちょい足しレシピ
つまり、接続処理とスキャンの排他処理を行う必要が出てきました。その際、node-linkingではスキャン系処理がビーコン受信とデバイス発見の用途別に2つに別れていて、両方をいっぺんに行うのに適してなかったため、スキャン処理はnobleを直接呼び出す形に変更。
なお、いったん接続が確率した状態でスキャンを再開するのは問題ない模様。
スキャン停止に10秒程度かかる
なぜこんなにかかるか不明。ちなみにLinkingデバイスへの接続自体も10秒程度かかるので、合計20秒となり使いにくいことこの上ない。
追記:後で試してみると、スキャンの停止はほぼ一瞬でした。開発途中の単なる不具合だったのかも。
じゃあ、面倒な接続なんかやめて原則スキャンだけでセンサーデータ読み込む形で使おうと思ったら...
Linkingデバイスは接続しておいたほうが電池が長持ち
温度センサーShizuku THAのドキュメントを見ると、
電池寿命は、BLE 接続状態で 500 日程度、接続してセンサー情報を取得すると 170 日程度、
Linking Beacon で利用の場合は電池寿命 100 日程度になります。
とあり、スキャン状態でデータを取るより、接続して取ったほうが倍近く電池が長持ち。
じゃあ、原則スキャンは最初だけデバイス発見のために使い、あとは常時接続状態で使おうと思ったら...
接続状態を維持するのは簡単ではない
温度センサーTukeruで開発を行っていましたが、2階に置いたデバイスと1階天井付近に置いたRaspiとの接続の確立が不安定。
接続に失敗する場合のnobleの動作も様々で:
- だいたいはタイムアウトエラーが返ってくる
- 接続リクエストの結果が永遠に戻ってこないケースもある。どうやら、自分でもタイムアウト検知してdisconnectしないとだめらしい
- 接続を数分待たされた挙句、nobleのSmp.handlePairingResponse()内部で例外を起こすケースもある(これはnode-redごと落ちるのできつい)
- リトライしたら、nobleが"Already connected"のエラーを返してくる。上位層のnode-linkingは未接続と思ってるため回復手段がなくタチが悪い。
やっと接続できたとしても、30秒程度で勝手に切れたりする。タイムアウト処理やらリトライ処理やら、しかも接続中はスキャンとの排他処理まで必要となると状態管理が面倒で、さらにデバイスが複数台になった際にまともにうごくかなといやな気分になる。
接続状態が良好でも、デバイスへのリクエストは排他制御が必要
Linkingデバイスに接続後、temperature, humidity, batteryのそれぞれのデータ送信開始のリクエストを立て続けに送ると3つのうち2つが失敗する。2つをリトライしたらまた1つ失敗。最後の1つをリトライしてようやく完了という動作になる。
デバイスのリクエスト部分にも排他処理を行うように対応が必要です。
接続中のデバイスはビーコンを出さないので、接続しているデバイスをきちんと管理する
接続中のデバイスはビーコンを出さなくなるため、スキャンしても検知できなくなります。プログラム側で接続中のデバイスをきちんと把握し、かつプログラム終了時等にはきちんと接続を解除する必要があります。
プログラムの管理外で接続しっぱなしの状態になった場合、スキャン結果にも出てこないのでデバイスを見失ってしまいます。
数メートル以上離れたデバイスに接続して使うのはあきらめたほうが良いかも
距離が1mのときは秒単位の間隔で来ているビーコンデータも、距離が5mかそれ以上離れると1分に1回程度しか受信できなくなります。もちろん接続なんて無理。
家の各部屋にセンサー置くとなると5mは離れるわけで、接続はあきらめてスキャンだけで使うしかなさそうです。
なお、スキャンだけで使う場合でも、双方向の通信が必要になるアクティブスキャンが少なくとも一回は成功する必要があり、やはりそれなりに近い距離で通信する必要があります。
近接してるとビーコンデータが大量に来過ぎる
Linkingデバイスの製品仕様では、Advertising interval: 800msなので、受信状態が良好だとこんな感じでバンバン来ます。気温のデータなんてこんな高頻度で来られても困る。
2018/8/14 10:22:52node: 8bb367b1.568578
Tukeru_th0164136 temperature : msg.payload : Object
{ device: "Tukeru_th0164136", service: "temperature", value: 27.625 }
2018/8/14 10:22:53node: 8bb367b1.568578
Tukeru_th0164215 humidity : msg.payload : Object
{ device: "Tukeru_th0164215", service: "humidity", value: 59.625 }
2018/8/14 10:22:53node: 8bb367b1.568578
Tukeru_th0164215 humidity : msg.payload : Object
{ device: "Tukeru_th0164215", service: "humidity", value: 59.625 }
2018/8/14 10:22:53node: 8bb367b1.568578
Tukeru_th0164136 temperature : msg.payload : Object
{ device: "Tukeru_th0164136", service: "temperature", value: 27.625 }
2018/8/14 10:22:53node: 8bb367b1.568578
Tukeru_th0164136 temperature : msg.payload : Object
{ device: "Tukeru_th0164136", service: "temperature", value: 27.625 }
2018/8/14 10:22:55node: 8bb367b1.568578
Tukeru_th0164215 humidity : msg.payload : Object
{ device: "Tukeru_th0164215", service: "humidity", value: 59.625 }
node-redはdelayノードで流量制限もできるけど、試したところちょっと使いにくいので、自分のカスタムノード中でビーコンデータの出力頻度を調整できるように node-rate-limiter を使って対応しました。
結局nobleに手を出すことになる
https://github.com/noble/noble のPull requestみると、1年前から30件以上たまってる状態で、メンテナンスが停滞気味の模様。
jroberson氏のmergedブランチを使う
溜まっているPRを独自にまとめて、Promise対応などごりごりと変更を入れた jrobeson 氏のブランチをベースに使うことにします(なお、これはLinux以外の動作は未サポートの模様)。
nobleのデバッグログをONにする
export DEBUG=noble
してからプログラムを実行すると、nobleが詳細レベルのログを出すようになります。これでもごく一部のログで、export DEBUG=*
するとすごい量のログが出ます。
node-linkingもちょっといじる
- 接続中に再度接続リクエストしても、エラーではなく成功扱いとする
- 接続リクエストしたら、noble側から"Already connected"エラーが返ってきた際のワークアラウンド。
RSSIの値を随時更新する
電波の強さを表すRSSIの値が表示されてると設置の目安となり便利です。
デバイスから来たビーコンデータは、nobleのperipheralオブジェクトにて随時渡されるので、ここからRSSIの値をつまんでくることにしました。
peripheral {
_noble:
Noble {
initialized: true,
address: 'b8:27:eb:1e:b5:88',
_state: 'poweredOn',
_bindings:
NobleBindings {
_state: 'poweredOn',
_addresses: [Object],
_addresseTypes: [Object],
_connectable: [Object],
_pendingConnectionUuid: null,
_connectionQueue: [],
_handles: {},
_gatts: {},
_aclStreams: {},
_signalings: {},
_hci: [Object],
_gap: [Object],
_events: [Object],
_eventsCount: 21,
onSigIntBinded: [Function: bound ],
_scanServiceUuids: [Array] },
_peripherals: { f636e499db54: [Circular] },
_services: { f636e499db54: {} },
_characteristics: { f636e499db54: {} },
_descriptors: { f636e499db54: {} },
_discoveredPeripheralUUids: [ 'f636e499db54' ],
_events: { warning: [Function: bound ], newListener: [Function: bound ] },
_eventsCount: 2,
_allowDuplicates: true },
id: 'f636e499db54',
uuid: 'f636e499db54',
address: 'f6:36:e4:99:db:54',
addressType: 'random',
connectable: true,
advertisement:
{ localName: 'Tukeru_th0164136',
txPowerLevel: -96,
manufacturerData: <Buffer e2 02 00 02 81 28 15 d3>,
serviceData: [],
serviceUuids: [ 'b3b3690150d34044808d50835b13a6cd' ],
solicitationServiceUuids: [],
serviceSolicitationUuids: [] },
rssi: -99,
services: null,
state: 'disconnected' }
アクティブ・パッシブスキャンの切り替えに対応
noble該当リンク: https://github.com/noble/noble/issues/556#issuecomment-282377457
スキャンにも実は二種類あり、受信のみのpassiveと送受信が必要になるactiveがあるとの事(参考:[Bluetooth Low Energyのアドバータイズとスキャン] (https://fielddesign.jp/technology/ble/blespec_advertise/))。
nobleはactiveスキャン固定で、これだとデバイス側から継続的に応答を返す必要があり無駄に電池を消費する。passiveスキャンのほうが電池持つのではと試してみる。
- https://github.com/noble/noble/issues/701 を参考にして、nobleのソースを修正しactive/passiveスキャンモードの指定ができるように対応
- それだけでは、passiveスキャンのイベントが一切通知されなかった。ソースを見たところlib/hci-socket/gap.js最後尾に、パッシブスキャン時のデータをフィルタリング処理する箇所があったのではずす。
その上で、上位層のnode-red-contrib-linking-deviceで以下の対応
- 最初はactiveスキャンを行う
- 最後にデバイスが見つかってから60秒たったら、passiveスキャンに切り替え
- 不明デバイスが見つかったら、activeスキャンに再度切り替え
最初、1, 2だけでもいいかなと思ったのですが、Pochiru(eco)という製品の場合、ボタンを押したときだけビーコンを発信する仕様だったので3.が必要になりました。passiveスキャンの受信だけではデバイス名が取れないので、各デバイスに対して最低一回はactiveスキャンを行い、名前を取得する必要があります。