芽萌丸IOT研究会 @iot
@iot
2020/11/2 11:52 更新✏

Node.jsで BLE Peripheral を作成

Node.jsで BLE Peripheral を作成するメモです。 (※BLE Peripheral とは Bluetooth Low Energy通信における周辺機器側、例えばセンサーデバイス等を指します。)

今回はPC側を周辺機器 (Peripheral) として実装し、スマホ側 (Central) から接続&操作してみますが、 PC (Peripheral) とスマホ (Central) のペアリングは不要です!

目次:

前提

Central (スマホ)

Peripheral (PC)

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は普通はセンサーデバイスのような機器を想定しているため、機密情報を取り扱うような場面はあまり無いとは思いますが、念の為。)

以上です。

Bluetooth

芽萌丸IOT研究会 @iot
芽萌丸IOT研究会@iot
芽萌丸のIOT関連アカウント。ラズパイとか色々。記事は主に @TanakaSoftwareLab が担当。
オススメ:Zattoyomiで時事ネタチェックの時間節約!