nobleで複数のBLEデバイスを扱う際のちょい足しレシピ

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、せーのです。 今日はNode.jsでBLEデバイスを扱うモジュール[noble]で複数のBLEデバイスを扱う際のコツをご紹介します。

現象

Raspberry PiやBX1等のIoTゲートウェイでBLEデバイスを扱う際にnobleは非常に便利なのですぐに使うのですが、先日OMRONの顔認識センサー、HVC-C1Bを複数台BX1につなぐソリューションを作っていまして、どうにも繋がらない現象が起きました。具体的には2台目のHVC-C1Bをつなぎに行くと1台目の接続が切れるのです

hvc-c1b_to_bx1_1

原因

色々な実験や検証、また識者の方に相談したところ、BX1を製造しているぷらっとホームの松下さん

BLEの仕様なのかちょっと不明なのですが、discoverCharacteristics (node-sensortagではconnectAndSetUpにwrap)とstartScan(node-sensortagではdiscoverにwrap)が同時に使えないので、discoverで見つけたらいったんstopDiscoverして、Characteristicsを取得した後、startScanするという感じになりました。

という大変貴重な情報を提供してくださいました。またご自身でもnobleのラッパーモジュール、noble-deviceを使ってTI Sensorのモジュールを複数connectする方法をアップしてくれました。

noble-deviceでそうならnobleでもきっとそうなはず、、、

解決

ということで1台目をscannningし、discoverしたらCharacteristicsを見つけるまで新たなscanningを止めてみました。

noble.on('discover', function(peripheral) {
    	var uuid = peripheral.uuid;
    	var localName = peripheral.advertisement.localName;

	
	if (localName != null && myUUIDs.indexOf(localName) >= 0 ) {
		noble.stopScanning();       // <= ここで一旦SCANを止める
		cnt ++;
		
		setTimeout(function() {
			connect_hvcc(peripheral);
		}, 1000);
	}
});
function connect_hvcc(peripheral) {
	var uuid = peripheral.uuid;

	peripheral.connect(function(err) {

		......

		
		peripheral.on('disconnect', function() {
			logger.info('disconnect... : uuid=' + uuid);
			logger.info('start scanning...');
			on_response = null;
			noble.startScanning([], false);
		});

		peripheral.discoverServices([], function(err, services) {
			service = _.find(services, function(s) {return s.uuid === service_uuid});
			logger.debug('service: ' + service);
			service.discoverCharacteristics([], function(err,chars) {
				rx_buf[uuid] = new Buffer(0);
				rx[uuid] = _.find(chars, function(c) {return c.uuid === rx_char_uuid});
				tx[uuid] = _.find(chars, function(c) {return c.uuid === tx_char_uuid});

				rx[uuid].notify(true);
				rx[uuid].on('read', on_read);

				logger.info('peripheral.discoverServices uuid: ' + uuid);

				if (cnt < myUUIDs.length){
					noble.startScanning([], true);  //<= ここで再びSCANを開始する
				}
				setTimeout(start_hvcc, 1000, uuid);
			});
		});
	});
}

これが見事ビンゴ!複数のHVC-C1BをConnectすることに成功しました。松下さん、ありがとうございます!

※上のコードの例ではConnectさせるBLEデバイスをPeripheral IDでフィルタリングしています。ただつなぐだけならもっとシンプルでいいです。

ですが新たな問題が発生しました。

新たな問題

HVC-C1BはPUSH型ではなくPULL型のデバイスとなります。つまり「これこれのデータをちょうだい」というコマンドをHVC-C1Bに投げるとそれに対するレスポンスという形で結果コマンドが返ります。そしてHVC-C1Bに投げるCharacteristicsとHVC-C1Bからのコマンドを受け取るCharacteristicsは別です。上のコードで言えばtxがHVC-C1Bにコマンドを投げる方、rxがコマンドを受け取る方、です。

それぞれperipheral IDで連想配列の形にしていて、個別に動くようにしています。また必要な関数は全て引数に[uuid]という変数を付けて、どのデバイスのデータを投げているのか、また受け取っているのかを区別しています。ただここで問題になるのが上のコードの

rx[uuid].notify(true);
rx[uuid].on('read', on_read);

の部分です。
ここはBLEデバイスからのデータを受信(read)した時にこの関数(on_read)を動かしなさい、という設定の部分ですが、元々のnobleのreadの部分は

this._bindings.on('read', this.onRead.bind(this));

......

Noble.prototype.onRead = function(peripheralUuid, serviceUuid, characteristicUuid, data, isNotification) {
  var characteristic = this._characteristics[peripheralUuid][serviceUuid][characteristicUuid];

  if (characteristic) {
    characteristic.emit('data', data, isNotification);

    characteristic.emit('read', data, isNotification); // for backwards compatbility
  } else {
    this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ', ' + characteristicUuid + ' read!');
  }
};

となっています。せっかくonReadの時点では各IDを持ってきているのですが、readとしてイベント化する時に外しちゃっているんですね。これでは受信する関数にどのデバイスからきたデータなのかを引き継げません。さて、どうしましょう。

解決

これは簡単ですね。引数でPeripheral IDを持ってきているのですからそれをemitに突っ込んであげればいいでしょう。

Noble.prototype.onRead = function(peripheralUuid, serviceUuid, characteristicUuid, data, isNotification) {
  var characteristic = this._characteristics[peripheralUuid][serviceUuid][characteristicUuid];

  if (characteristic) {
    characteristic.emit('data', data, isNotification, peripheralUuid);

    characteristic.emit('read', data, isNotification, peripheralUuid); // for backwards compatbility
  } else {
    this.emit('warning', 'unknown peripheral ' + peripheralUuid + ', ' + serviceUuid + ', ' + characteristicUuid + ' read!');
  }
};

これで引き継ぐ側のコードをこうしてあげます。

rx[uuid].notify(true);
rx[uuid].on('read', on_read);
function on_read(data, isNotification, uuid) {
	logger.debug("on_read: uuid-" + uuid);
	rx_buf[uuid] = Buffer.concat([rx_buf[uuid], data]);
	.......

これでどのperipheral IDから来たデータかはっきりしました。上の例では「顔認識データを投げる」「顔認識データを受け取る」という2つのサービスしかないのでperipheral IDのみで区別しましたが、他にも色々なサービスのあるBLEデバイスであればserviceUuid, characteristicUuidも引数に加えると良いかと思います。

ちなみにきたBufferをconcatしているのはBLE通信を規定しているGATTという仕様で、データ通信のやり取りの単位(ATT_MTUと言います)は23byteと決まっているからです。23byteのうちヘッダを抜くと大体データは20byteずつ送られてきます。つまり20byte以上の通信データが来る場合は複数回に分けてくるのでconcatでつなげているのですね。

まとめ

いかがでしたでしょうか。まとめるとnobleで複数のBLEデバイスを扱う時は

  • discoverしたらcharacteristicsを見つけるまで次のscanningをしない
  • 各characteristicsはperipheral IDで連想配列を作って個別に管理する
  • readメソッドの引数にperipheral IDを増やす

というちょい足しレシピでうまく行きました。
BLE関係はなかなか難しいですが、一つ一つ読み解いていけばなんとかなります。頑張って行きましょう!

参考サイト