Node.jsで BLE Peripheral を作成
Node.jsで BLE Peripheral※を作成するメモです。 (※BLE Peripheral とは Bluetooth Low Energy通信における周辺機器側、例えばセンサーデバイス等を指します。)
今回はPC側を周辺機器 (Peripheral) として実装し、スマホ側 (Central) から接続&操作してみますが、 PC (Peripheral) とスマホ (Central) のペアリングは不要です!
目次:
前提
Central (スマホ)
BLE通信テスト用アプリ(例:BLE Scanner)をインストール済み。
今回はこのアプリを使ってスマホをCentralとしてPeripheralにリクエストを送ります。
Peripheral (PC)
OS: Ubuntu 16.04.4 LTS
blenonpm モジュール v0.5.0
node v8.17.0
※nodeバージョンが v8 でないと bleno v0.5.0 インストール時にエラーが出るので注意して下さい!(nodeバージョンの切り替えにはnvm use を使うと便利です。)
bluez等の関連ツール群がインストール済み。未インストールなら以下を実行:
$ sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev
Peripheralを実装
プロジェクトのセットアップ
PC上でテキトーなPeripheral用プロジェクトを作成し、必要なnpmモジュールをインストールしておきます:
$ mkdir ble-example && cd ble-example
$ npm init
$ npm install --save bleno
コード
Readリクエストを受け付けるPeripheral
まずはCentralからReadリクエストが来たら時間を返すだけのシンプルなPeripheralを作ってみましょう。(「おまけ」として、Writeリクエストが来たら送信されてきたデータをログ出力する処理も付けています。)
time-peripheral.js
const bleno = require('bleno');
const { Characteristic, PrimaryService, } = bleno;
const SERVECE_UUID = 'eeee'; // テキトーなServiceUID
const CHARACTERISTIC_UUID = 'ffff'; // テキトーなCharacteristicUID
const DEVICE_NAME = 'time-prepheral'; // デバイス名
console.log(`Peripheral device: '${DEVICE_NAME}'`);
bleno.on('stateChange', (state) => {
console.log(`on -> stateChange: ${state}`);
if (state === 'poweredOn') {
// 'powerOn'の時だけAdvertise開始:
bleno.startAdvertising(DEVICE_NAME, [SERVECE_UUID]);
} else {
// それ以外の時はAdvertise終了:
bleno.stopAdvertising();
}
});
bleno.on('advertisingStart', (error) => {
console.log(`on -> advertisingStart: ${(error ? 'error ' + error : 'success')}`);
if (error) return;
// Servicesを作成してセット:
bleno.setServices(createServices());
});
bleno.on('disconnect', (clientAddress) => {
console.log(`on -> disconnect: ${clientAddress}`);
});
/**
* Servicesを作成
* @return {Array<Service>}
*/
function createServices() {
const services = [];
// Serviceを作成して追加:
services.push(createService());
return services;
}
/**
* Serviceを作成
* @return {Service}
*/
function createService() {
// Characteristic作成:
const characteristic = new Characteristic({
uuid: CHARACTERISTIC_UUID,
properties: [
'read', // read属性を付加
'write', // (おまけ) ついでにwrite属性も付加
],
// Central側からReadリクエストが来ると発火:
onReadRequest: (offset, callback) => {
const data = new Date().toLocaleString();
console.log("onReadRequest: Sending '%s' to central device.", data);
callback(Characteristic.RESULT_SUCCESS, new Buffer.from(data));
},
// (おまけ) Central側からWriteリクエストが来ると発火:
onWriteRequest: (data, offset, withoutResponse, callback) => {
console.log("onWriteRequest: data from central device:", data);
// TODO: ここでCentralから飛んできたdataを使って何かする
callback(Characteristic.RESULT_SUCCESS);
},
});
// Service作成:
const service = new PrimaryService({
uuid: SERVECE_UUID,
characteristics: [characteristic],
});
return service;
}
コードを実行:
$ sudo node time-peripheral.js
Peripheral device: 'time-peripheral'
on -> stateChange: poweredOn
on -> advertisingStart: success
## ここでCentralからの接続を待ち受けます。
それでは、作成したPeripheral(time-peripheral
)にスマホ(Central)から接続要求してみましょう。接続が成功したら、先ほど作成した Service (UUID:eeee
) の Characteristic (UUID:ffff
) にRead要求をリクエストしてみて下さい。Peripheralが以下のようなログを出力します:
Sending '2020-10-29 15:44:27' to central device.
この時、スマホのBLE通信テストアプリ上でも時刻が返ってきたと思います。
Notifyリクエストを受け付けるPeripheral
次は先程のPeripheralを少し改良し、Notifyリクエストが来たら定期的に時刻を通知するPeripheralを作ってみましょう。
time-peripheral.js のfunction createService(){...}
の部分を以下に置き換えます:
...
// 通知停止フラグ:
let stopFlag = false;
/**
* Serviceを作成
* @return {Service}
*/
function createService() {
// Characteristic作成:
const characteristic = new Characteristic({
uuid: CHARACTERISTIC_UUID,
properties: ['notify'],
// onSubscribeのupdateValueCallbackがコールされると発火
onNotify: () => {
console.log("onNotify");
},
// Central側から切断リクエストが来ると発火
onUnsubscribe: () => {
console.log("onUnsubscribe");
stopFlag = true; // サブスク停止で通知停止フラグを立てる
},
// Central側からNotifyリクエストが来ると発火
onSubscribe: (maxValueSize, updateValueCallback) => {
console.log("onSubscribe:", maxValueSize);
stopFlag = false; // サブスク開始で通知停止フラグをリセット
// 定期的に時刻を送信:
const timeId = setInterval(() => {
// 停止フラグが立っていれば定期処理を終了:
if (stopFlag) return clearInterval(timeId);
// 時刻を送信:
const data = new Date().toLocaleString();
console.log("Sending '%s' to central device.", data);
updateValueCallback(new Buffer.from(data));
}, 3000);
},
});
// Service作成:
const service = new PrimaryService({
uuid: SERVECE_UUID,
characteristics: [characteristic],
});
return service;
}
コードを実行:
$ sudo node time-peripheral.js
Peripheral device: 'time-prepheral'
on -> stateChange: poweredOn
on -> advertisingStart: success
## ここでCentralからの接続を待ち受けます。
ここでスマホ(Central)から Notify リクエストを出してみて下さい。以下のように時刻の定期通知が開始されます:
...
onSubscribe: 20
Sending '2020-10-29 17:05:14' to central device.
onNotify
Sending '2020-10-29 17:05:17' to central device.
onNotify
...
Notifyを停止するとunsubscribeが発火し一時停止します:
...
onUnsubscribe
再びNotifyをリクエストするとログ出力を再開します:
...
onSubscribe: 20
Sending '2020-10-29 17:05:39' to central device.
onNotify
Sending '2020-10-29 17:05:42' to central device.
onNotify
...
接続を切断すると、unsubscribeが発火しつつ切断されたことが分かります:
...
onUnsubscribe
on -> disconnect: xx:xx:xx:xx:xx:xx
このように、Notifyを使えばデータ変更時のみCentral側へデータを通知することができるため、ReadリクエストをCentral側からポーリングするよりも効率的です。
セキュリティ上の注意点
BLE通信は機器同士のペアリングをしなくてもCentral側から接続して通信ができてしまいます。もしPeripheral側で何らかの機密情報を取り扱うServiceを作る際にはセキュリティに十分注意する必要があります。(Peripheralは普通はセンサーデバイスのような機器を想定しているため、機密情報を取り扱うような場面はあまり無いとは思いますが、念の為。)
以上です。