Node.js + nobleを使ってMicrobitのセンサー情報を読み取る

前回、gatttoolを使ってMicrobitの各種サービス情報を取得し、Notifyイベントを使って、Temperatureサービスの温度情報を取得した。今回は、プログラムで同様の事を行ってみる。本当はBluezライブラリを使ってC/C++プログラミングとも思ったが、ちょっと敷居が高いので、まずは、nobleを使って手っ取り早く実装してみる。最終的にはMicrobitとdualshockコントローラーの間に位置してデバイス間でデータのやりとりをする必要があるので、Node.jsとnobleの構成は愛称が良いので、これでいいのかもしれない。

> npm install noble

まずは、Microbitのキャラクタリスティックスをサービスごとに一覧をダンプしてみる。

// test01.js
var noble = require('noble');

var FOUND_MICROBIT_ADDR1 = 'db:88:10:28:d8:61';

noble.on('stateChange', function(state) {
  console.log('[1.stateChange] ' + state);
  if (state === 'poweredOn') {
      noble.startScanning();
  } else {
      noble.stopScanning();
  }
});

noble.on('scanStart', function() {
  console.log('[2.scanStart]');
});

noble.on('scanStop', function() {
  console.log('[scanStop]');
});

noble.on('discover', function(peripheral) {
  console.log('[3.discover]\n' + peripheral);
  if( peripheral.address == FOUND_MICROBIT_ADDR1){
    peripheral.on('connect', function() {
      console.log('[4.connect]');
      this.discoverServices();
    });
    peripheral.on('disconnect', function() {
      console.log('[disconnect]');
    });
    peripheral.on('servicesDiscover', function(services) {
      console.log('[5.servicesDiscover]');
      for(i = 0; i < services.length; i++) {
        services[i].on('includedServicesDiscover', function(includedServiceUuids) {
          //console.log('[6.includedServicesDiscover]');
          this.discoverCharacteristics();
        });
        services[i].on('characteristicsDiscover', function(characteristics) {
          console.log('[7.characteristicsDiscover]');
          for(j = 0; j < characteristics.length; j++) {
            console.log("------------");
            console.log("  serviceid:"+characteristics[j]._serviceUuid);
            console.log("  uuid:"+characteristics[j].uuid);
            console.log("  name:"+characteristics[j].name);
            console.log("  type:"+characteristics[j].type);
            console.log("  properties:"+characteristics[j].properties);
          }
        });
        services[i].discoverIncludedServices();
      }
      
    });
    peripheral.connect();
  }
  noble.stopScanning();
});

特筆すべき点もなく簡単な実装で済んでしまう。このプログラムを実行すると、以下のようなダンプ結果になる。gatttoolと比較すると、UUIDにハイフンがなく小文字で統一されている。

> sudo node test01.js

[1.stateChange] poweredOn
[2.scanStart]
[3.discover]
{"id":"db881028d861","address":"db:88:10:28:d8:61","addressType":"random","connectable":true,"advertisement":{"localName":"BBC micro:bit [vutaz]","serviceData":[],"serviceUuids":[],"solicitationServiceUuids":[],"serviceSolicitationUuids":[]},"rssi":-62,"state":"disconnected"}
[scanStop]
[4.connect]
[5.servicesDiscover]
[7.characteristicsDiscover]
------------
  serviceid:1800 // Generic Access
  uuid:2a00
  name:Device Name
  type:org.bluetooth.characteristic.gap.device_name
  properties:read,write
------------
  serviceid:1800
  uuid:2a01
  name:Appearance
  type:org.bluetooth.characteristic.gap.appearance
  properties:read
------------
  serviceid:1800
  uuid:2a04
  name:Peripheral Preferred Connection Parameters
  type:org.bluetooth.characteristic.gap.peripheral_preferred_connection_parameters
  properties:read
[7.characteristicsDiscover]
------------
  serviceid:e95d93b0251d470aa062fa1922dfa9a8 // DFU
  uuid:e95d93b1251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,write
[7.characteristicsDiscover]
------------
  serviceid:180a // Device Information
  uuid:2a24
  name:Model Number String
  type:org.bluetooth.characteristic.model_number_string
  properties:read
------------
  serviceid:180a
  uuid:2a25
  name:Serial Number String
  type:org.bluetooth.characteristic.serial_number_string
  properties:read
------------
  serviceid:180a
  uuid:2a26
  name:Firmware Revision String
  type:org.bluetooth.characteristic.firmware_revision_string
  properties:read
[7.characteristicsDiscover]
[7.characteristicsDiscover]
------------
  serviceid:1801 // Generic Attribute
  uuid:2a05
  name:Service Changed
  type:org.bluetooth.characteristic.gatt.service_changed
  properties:indicate
[7.characteristicsDiscover]
------------
  serviceid:e95d0753251d470aa062fa1922dfa9a8 // Accelerometer
  uuid:e95dca4b251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,notify
------------
  serviceid:e95d0753251d470aa062fa1922dfa9a8
  uuid:e95dfb24251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,write
[7.characteristicsDiscover]
------------
  serviceid:e95d6100251d470aa062fa1922dfa9a8 // Temperature
  uuid:e95d9250251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,notify
------------
  serviceid:e95d6100251d470aa062fa1922dfa9a8
  uuid:e95d1b25251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,write
[7.characteristicsDiscover]
------------
  serviceid:e95df2d8251d470aa062fa1922dfa9a8 // Magnetometer
  uuid:e95dfb11251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,notify
------------
  serviceid:e95df2d8251d470aa062fa1922dfa9a8
  uuid:e95d9715251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,notify
------------
  serviceid:e95df2d8251d470aa062fa1922dfa9a8
  uuid:e95d386c251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,write
[7.characteristicsDiscover]
------------
  serviceid:e95d9882251d470aa062fa1922dfa9a8 // Button
  uuid:e95dda90251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,notify
------------
  serviceid:e95d9882251d470aa062fa1922dfa9a8
  uuid:e95dda91251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,notify
[7.characteristicsDiscover]
------------
  serviceid:e95dd91d251d470aa062fa1922dfa9a8 // LED
  uuid:e95d7b77251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,write
------------
  serviceid:e95dd91d251d470aa062fa1922dfa9a8
  uuid:e95d93ee251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:write
------------
  serviceid:e95dd91d251d470aa062fa1922dfa9a8
  uuid:e95d0d2d251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,write
[7.characteristicsDiscover]
------------
  serviceid:e95d93af251d470aa062fa1922dfa9a8 // Event Service
  uuid:e95d9775251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,notify
------------
  serviceid:e95d93af251d470aa062fa1922dfa9a8
  uuid:e95d5404251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:write
------------
  serviceid:e95d93af251d470aa062fa1922dfa9a8
  uuid:e95d23c4251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:write
------------
  serviceid:e95d93af251d470aa062fa1922dfa9a8
  uuid:e95db84c251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,notify
[7.characteristicsDiscover]
------------
  serviceid:e95d127b251d470aa062fa1922dfa9a8 // IO Pin
  uuid:e95d5899251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,write
------------
  serviceid:e95d127b251d470aa062fa1922dfa9a8
  uuid:e95db9fe251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,write
------------
  serviceid:e95d127b251d470aa062fa1922dfa9a8
  uuid:e95dd822251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:write
------------
  serviceid:e95d127b251d470aa062fa1922dfa9a8
  uuid:e95d8d00251d470aa062fa1922dfa9a8
  name:null
  type:null
  properties:read,write,notify

次に属性にアクセスしてみる。キャラクタリスティックスのオブジェクトにはreadwriteといったような属性にアクセスするAPIがある。


// reading data.
characteristic.read(function(error, data){
  console.log(data); // <Buffer 30> etc
});
// writing data.
// true : without response, false: need response
characteristic.write(data, false, function(error){
  console.log(error); // null
});
// start notification
characteristic.subscribe(function(error){
  console.log(error); // null
});
// stop notification
characteristic.unsubscribe(function(error){
  console.log(error); // null
});

Temperatureサービスからは温度、Magnetometerサービスからは向きN極からの角度を取得する。
注意点としては、discoverSomeServicesAndCharacteristicsにサービスとキャラクタリスティクスのUUID配列を渡す必要があるが、複数のサービスの属性を取得する場合でも、引数の配列に該当するサービスとキャラクタを追加して一度に実行する必要がある。分けて実行すると、前回コールしたものが取り消されてしまうので注意する。

// test02.js
var noble = require('noble');

var FOUND_MICROBIT_ADDR1 = 'db:88:10:28:d8:61';

// Temperature + Magnetometer
// characteristicsが混在する場合、それぞれのservicesも配列に追加されていないといけない
var service_uuids = ["e95d6100251d470aa062fa1922dfa9a8", "e95df2d8251d470aa062fa1922dfa9a8"];
var character_uuids = ["e95d9250251d470aa062fa1922dfa9a8", "e95dfb11251d470aa062fa1922dfa9a8", "e95d9715251d470aa062fa1922dfa9a8"];

var subscribeCharacteristics = function( peripheral, servs, chars, callback ){
  peripheral.discoverSomeServicesAndCharacteristics(
    servs, chars, function(error, services, characteristics){
      console.log("services = "+services.length);
      console.log("characteristics = "+characteristics.length);
      for(var ic = 0; ic < characteristics.length; ic++){
        characteristics[ic].on('data', function(data, isNotification){
            callback( null, this._serviceUuid, this.uuid, data );
        });
        characteristics[ic].subscribe(function(error){
            callback( error, "", "", null );
        });
      }
  });
}

noble.on('stateChange', function(state) {
  console.log('[1.stateChange] ' + state);
  if (state === 'poweredOn') {
      noble.startScanning();
  } else {
      noble.stopScanning();
  }
});

noble.on('scanStart', function() {
  console.log('[2.scanStart]');
});

noble.on('scanStop', function() {
  console.log('[scanStop]');
});

noble.on('discover', function(peripheral) {
  if( peripheral.address == FOUND_MICROBIT_ADDR1){

    peripheral.on('connect', function() {
      subscribeCharacteristics(this, service_uuids, character_uuids,
        function( error, serviceid, characterid, data ){
          console.log("error:"+error);
          console.log("serviceid:"+serviceid);
          console.log("characterid:"+characterid);
          console.log(data);
      });
    });

    peripheral.on('disconnect', function() {
    });

    peripheral.connect();
  }
  noble.stopScanning();
});

characteristics.subscribe()を実行すると、指定した属性の通知がオンになり、各属性の通知周期ごとにcharacteristics.on("data", function())がコールされる。複数の属性に対してオンすると、混在した状態で"data"イベントが発火することになる。以下の例だと、Temperatureの通知が1回(一番最後のログ)に対して、Magnetometerの通知が何度も実行されていることが分かる。

> sudo node test02.js
:
serviceid:e95df2d8251d470aa062fa1922dfa9a8
characterid:e95dfb11251d470aa062fa1922dfa9a8
<Buffer 97 6f 23 e4 fb a6>
error:null
serviceid:e95df2d8251d470aa062fa1922dfa9a8
characterid:e95d9715251d470aa062fa1922dfa9a8
<Buffer fc 00>
error:null
serviceid:e95df2d8251d470aa062fa1922dfa9a8
characterid:e95dfb11251d470aa062fa1922dfa9a8
<Buffer 8b 71 eb e4 b7 a9>
error:null
serviceid:e95df2d8251d470aa062fa1922dfa9a8
characterid:e95d9715251d470aa062fa1922dfa9a8
<Buffer fd 00>
error:null
serviceid:e95d6100251d470aa062fa1922dfa9a8
characterid:e95d9250251d470aa062fa1922dfa9a8
<Buffer 22>
: