BLE Nano を光センサー CdS と組み合わせる

 前回までは BLE Nano をそのまま USB のライター DAPLink にさして使っていましたが、このままだと他のセンサー類と組み合わせることができないので、 BLE Nano をブレッドボードにさして使ってみます。まずは BLE Nano をブレッドボードにさして動作させられることを確認し、その後実際のセンサーと組み合わせて値を取得してみます。

ブレッドボードで配線

 写真だとちょっと分かりづらいですが、今回は下記のように配線しました。基本的には BLE Nano をブレッドボードにさして、DAPLink の対応するピンとオス-オスのジャンパーワイヤーで接続しています。

f:id:akanuma-hiroaki:20170918144703j:plain:w450 f:id:akanuma-hiroaki:20170918144714j:plain:w450

 今回は下記のピンを接続しました。

  • SWCLK
  • SWDIO
  • VIN
  • GND
  • TXD
  • RXD

 シリアル接続でのデバッグ等を行わなければ TXD, RXD は不要かもしれません。

 また、今回は LED と抵抗を接続して、前回のプログラムで BLE Nano のオンボードLEDを点滅させていたところを、ブレッドボード上の LED を点滅させてみます。 BLE Nano の P0_4 ピンから 1kΩ の抵抗を経由して LED のアノード側に接続し、カソード側と BLE Nano の GND ピンに接続しています。

ソースコードを変更してコンパイル&実行

 前回実装したコードの LED のピンの指定を下記のように変更します。

DigitalOut led(LED1, 0);
 ↓
DigitalOut led(P0_4, 0);

 そして mbed compile して hex ファイルを BLE Nano に転送すると、ブレッドボード上の LED が点滅を開始し、 LightBlue などでは前回同様にデバイスが検知され、 Characteristic の値が確認できます。

ボタン電池で動かす

 開発・デバッグ時は DAPLink に接続しているので電源もそこから取っていますが、実際に何か BLE デバイスを作成した時には単体で動くようにする必要があるので、ボタン電池で動かしてみます。配線は下記の写真のようにしました。

f:id:akanuma-hiroaki:20170923015011j:plain:w450

 DAPLink の VIN と GND にさしていたピンをそれぞれボタン電池ホルダーの + 側と - 側に繋いで、それ以外のコードを取り外しています。今回はスイッチなどはつけてないので、ボタン電池をつけるとすぐにプログラムが動き始めます。 LightBlue などで接続してみると今まで同様に動作していることが確認できます。

実際のセンサーと組み合わせる

 先ほどまではダミーの値をセンサー値として使用していましたが、今度は実際のセンサーと組み合わせてみます。今回は下記の光センサー CdSセルを使用してみました。

www.aitendo.com

 これを 4.7kΩの抵抗と組み合わせて下記の写真のように配線しました。

f:id:akanuma-hiroaki:20170923015435j:plain:w450 f:id:akanuma-hiroaki:20170923015458j:plain:w450

 コードも実際のセンサー値を使用するように変更します。 main.cpp は下記のようにしました。

#include "mbed.h"
#include "ble/BLE.h"
#include "LuxService.h"

Serial pc(USBTX, USBRX);
DigitalOut led(P0_4, 0);
AnalogIn cds(P0_5);

const static char DEVICE_NAME[] = "LUXSample";
static const uint16_t uuid16_list[] = {LuxService::LUX_SERVICE_UUID};

static volatile bool triggerSensorPolling = false;

unsigned short luxValue;
const int threshold = 500;

LuxService *luxServicePtr;

Ticker ticker;

BLE ble;

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
  BLE::Instance().gap().startAdvertising();
}

void periodicCallback(void)
{
  luxValue = cds.read_u16();
  if (luxValue >= threshold) {
    led = 0;
  } else {
    led = 1;
  }

  triggerSensorPolling = true;
}

void onBleInitError(BLE &ble, ble_error_t error)
{
}

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
  BLE& ble = params->ble;
  ble_error_t error = params->error;

  if (error != BLE_ERROR_NONE) {
    onBleInitError(ble, error);
    return;
  }

  if (ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
    return;
  }

  ble.gap().setDeviceName((const uint8_t *) DEVICE_NAME);
  ble.gap().onDisconnection(disconnectionCallback);

  uint16_t initialValueForLUXCharacteristic = 100;
  luxServicePtr = new LuxService(ble, initialValueForLUXCharacteristic);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
  ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
  ble.gap().setAdvertisingInterval(1000);
  ble.gap().startAdvertising();
}

int main(void)
{
  pc.baud(115200);
  pc.printf("Starting BLE_LUX Sample...\r\n");

  ticker.attach(periodicCallback, 1);

  pc.printf("Initializing BLE Controller...\r\n");
  ble.init(bleInitComplete);
  pc.printf("Initialized.\r\n");

  while (ble.hasInitialized() == false) { /* spin loop */ }

  pc.printf("Starting loop...\r\n");
  while (true) {
    if (triggerSensorPolling && ble.getGapState().connected) {
      triggerSensorPolling = false;
      pc.printf("%u\r\n", luxValue);
      luxServicePtr->updateLuxValue(luxValue);
    } else {
      ble.waitForEvent();
    }
  }
}

 今までダミーの値を生成していた部分を削除し、実際のセンサーデータを読み出す処理を追加しています。 CdSセルからのデータはアナログ入力として受け取ります。また、今までは一秒間隔で点滅させていたLEDを、センサーデータが閾値より小さかったら(暗かったら)点灯し、大きかったら(明るかったら)消すという処理にしています。

void periodicCallback(void)
{
  luxValue = (cds.read_u16() & 0xffff);
  if (luxValue >= threshold) {
    led = 0;
  } else {
    led = 1;
  }

  triggerSensorPolling = true;
}

 LuxService.h の方も全体を掲載しておきますが、前回との変更は受け取るセンサー値の型を unsigned short に変更しただけです。

#ifndef __BLE_LUX_SERVICE_H__
#define __BLE_LUX_SERVICE_H__

class LuxService {
public:
  const static uint16_t LUX_SERVICE_UUID = 0xA000;
  const static uint16_t LUX_VALUE_CHARACTERISTIC_UUID = 0xA001;

  LuxService(BLE &_ble, uint16_t initialValueForLUXCharacteristic) :
    ble(_ble), luxValue(LUX_VALUE_CHARACTERISTIC_UUID, &initialValueForLUXCharacteristic, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY)
  {
    GattCharacteristic *charTable[] = {&luxValue};
    GattService luxService(LUX_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.gattServer().addService(luxService);
  }

  void updateLuxValue(unsigned short newValue) {
    ble.gattServer().write(luxValue.getValueHandle(), (uint8_t *)&newValue, sizeof(bool));
  }

private:
  BLE &ble;
  ReadOnlyGattCharacteristic<uint16_t> luxValue;
};

#endif

 これで実際のセンサーの計測値をBLEで受け取ることができるようになります。 LightBlue で見てみると下記のように値が取得できます。

f:id:akanuma-hiroaki:20170923134018p:plain:w300

まとめ

 BLE Nano とボタン電池を使うことで簡単にワイヤレスで動作するデバイスが試作できました。どんなセンサーと組み合わせて何をするかはアイディア次第だと思いますので、日常的に役に立ちそうなものを考えてプロトタイプを作ってみたいと思います。

 ちなみに BLE Nano は日本でも V2 が発売されましたね。スペックも上がってるようですし、 DAPLink のバージョンも 1.0 から 1.5 に上がっているようです。DAPLink 1.0 でも BLE Nano V2 が使えるのかなどは気になるところです。

mag.switch-science.com

BLE Nano で Advertisement & カスタムサービス実装

 前回はとりあえずサンプルコードで BLE Nano を iBeacon として動かしましたが、今回は Peripheral として Advertisement を送信する例と、カスタムサービスを定義して Central との通信を行う例を実装してみたいと思います。

Advertisement 送信

 まずは Advertisement に独自の情報を載せて送信してみます。下記サンプルコードをベースにしています。

BLE_GAP_Example - a mercurial repository | Mbed

 ただ上記サンプルを mbed import でインポートしてビルドしたところエラーになってしまったので、新規にプロジェクトを作って実装してみることにしました。

 まずは mbed new でプロジェクトを作成します。デフォルトだと mbed os 5 向けとして作成されますが、 BLE Nano では mbed os 5 に対応していないようなので、 mbed os 2 向けとして作成するために、 --mbedlib オプションをつけて作成します。

[vagrant@localhost vagrant]$ mbed new BLE_GAP_EXAMPLE --mbedlib                                                       
[mbed] Creating new program "BLE_GAP_EXAMPLE" (git)                                                                   
[mbed] Adding library "mbed" from "https://mbed.org/users/mbed_official/code/mbed/builds" at latest revision in the current branch                                                                                                          
[mbed] Updating reference "mbed" -> "https://mbed.org/users/mbed_official/code/mbed/builds/tip"                       
[mbed] Couldn't find build tools in your program. Downloading the mbed 2.0 SDK tools...                               
[vagrant@localhost vagrant]$ 
[vagrant@localhost vagrant]$ cd BLE_GAP_EXAMPLE/
[vagrant@localhost BLE_GAP_EXAMPLE]$            
[vagrant@localhost BLE_GAP_EXAMPLE]$ ls         
mbed  mbed.bld  mbed_settings.py                

 とりあえず toolchain と target を指定しておきます。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed detect                                     
                                                                                     
[mbed] Detected RBLAB_BLENANO, port /dev/ttyACM0, mounted /run/media/vagrant/DAPLINK 
[mbed] Supported toolchains for RBLAB_BLENANO                                        
+--------+-----------+-----------+-----+---------+-----+                             
| Target | mbed OS 2 | mbed OS 5 | ARM | GCC_ARM | IAR |                             
+--------+-----------+-----------+-----+---------+-----+                             
+--------+-----------+-----------+-----+---------+-----+                             
Supported targets: 0                                                                 
                                                                                     
[vagrant@localhost BLE_GAP_EXAMPLE]$                                                 
[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed toolchain GCC_ARM                          
[mbed] GCC_ARM now set as default toolchain in program "BLE_GAP_EXAMPLE"             
[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed target RBLAB_BLENANO                       
[mbed] RBLAB_BLENANO now set as default target in program "BLE_GAP_EXAMPLE"          

 ライブラリを更新します。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed deploy
[mbed] Updating library "mbed" to branch tip
[mbed] Downloading library build "675da3299148" (might take a minute)
[mbed] Unpacking library build "675da3299148" in "/vagrant/BLE_GAP_EXAMPLE/mbed"
[mbed] Updating the mbed 2.0 SDK tools...

 BLE の Firmware 開発をするには mbed の BLE API が必要なので、 mbed add でライブラリを追加します。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed add http://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_API/
[mbed] Adding library "BLE_API" from "https://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_API" at latest revision in the current branch
[mbed] Updating reference "BLE_API" -> "https://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_API/#65474dc93927"

 nRF51822 のライブラリも必要なので mbed add しておきます。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed add https://mbed.org/teams/Nordic-Semiconductor/code/nRF51822/
[mbed] Adding library "nRF51822" from "https://mbed.org/teams/Nordic-Semiconductor/code/nRF51822" at latest revision in the current branch
[mbed] Updating reference "nRF51822" -> "https://mbed.org/teams/Nordic-Semiconductor/code/nRF51822/#c90ae1400bf2"

 そして main.cpp はサンプルコードを参考に下記のように実装します。

#include "mbed.h"
#include "ble/BLE.h"

BLE ble;

const static char DEVICE_NAME[] = "SampleDevice";
const static uint8_t AdvData[] = {"SampleAdvertisement"};

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
  BLE::Instance().gap().startAdvertising();
}

void onBleInitError(BLE &ble, ble_error_t error)
{
  (void) ble;
  (void) error;
}

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
  BLE &ble          = params->ble;
  ble_error_t error = params->error;

  if (error != BLE_ERROR_NONE) {
    onBleInitError(ble, error);
    return;
  }

  if (ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
    return;
  }

  ble.gap().setDeviceName((const uint8_t *) DEVICE_NAME);
  ble.gap().onDisconnection(disconnectionCallback);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
  ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::MANUFACTURER_SPECIFIC_DATA, AdvData, sizeof(AdvData));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *) DEVICE_NAME, sizeof(DEVICE_NAME));

  ble.gap().setAdvertisingInterval(100);
  ble.gap().startAdvertising();
}

int main(void)
{
  ble.init(bleInitComplete);

  while (true) {
    ble.waitForEvent();
  }
}

 今回独自のデータとして使用するのは下記のデバイス名とアドバタイズデータです。

const static char DEVICE_NAME[] = "SampleDevice";
const static uint8_t AdvData[] = {"SampleAdvertisement"};

 bleInitComplete() の中で上記データを Advertisement に設定しています。

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::MANUFACTURER_SPECIFIC_DATA, AdvData, sizeof(AdvData));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *) DEVICE_NAME, sizeof(DEVICE_NAME));

 これをコンパイルして BLE Nano にコピーします。

[vagrant@localhost BLE_GAP_EXAMPLE]$ mbed compile
Building project BLE_GAP_EXAMPLE (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: env
Scan: mbed
Compile [100.0%]: main.cpp
Link: BLE_GAP_EXAMPLE
Elf2Bin: BLE_GAP_EXAMPLE
+-----------+-------+-------+------+
| Module    | .text | .data | .bss |
+-----------+-------+-------+------+
| Fill      |   112 |     3 |   31 |
| Misc      | 33115 |   141 | 1189 |
| Subtotals | 33227 |   144 | 1220 |
+-----------+-------+-------+------+
Allocated Heap: 2728 bytes
Allocated Stack: 2048 bytes
Total Static RAM memory (data + bss): 1364 bytes
Total RAM memory (data + bss + heap + stack): 6140 bytes
Total Flash memory (text + data + misc): 33371 bytes

Image: ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_GAP_EXAMPLE.hex
[vagrant@localhost BLE_GAP_EXAMPLE]$ 
[vagrant@localhost BLE_GAP_EXAMPLE]$ cp ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_GAP_EXAMPLE.hex /run/media/vagrant/DAPLINK/
[vagrant@localhost BLE_GAP_EXAMPLE]$ 

 すると Advertisement の送信が始まりますので、 iOS アプリの LightBlue などで確認すると、下記のように表示されます。

f:id:akanuma-hiroaki:20170917120410p:plain:w300 f:id:akanuma-hiroaki:20170917120439p:plain:w300

 Manufacturer Data には “SampleAdvertisement” という文字列が16進数変換されたものが表示されています。

独自サービス実装

 mbed の BLE API では下記ページに記載されている BLE Service はサポートされていますので、すぐに利用することができます。

BLE services supported on mbed - | Mbed

 例えば Heart Rate Service を使うサンプルに下記のようなものがあります。

BLE_HeartRate - a mercurial repository | Mbed

 また、カスタムのサービスを実装している例として下記のようなサンプルがあります。

BLE_Button - a mercurial repository | Mbed

 これらを参考にしてカスタムサービスを使う例を実装してみました。照度センサーからの値を扱うケースを想定して、 LuxService を実装して利用する例です。まず LuxService.h を下記のような内容で実装します。

#ifndef __BLE_LUX_SERVICE_H__
#define __BLE_LUX_SERVICE_H__

class LuxService {
public:
  const static uint16_t LUX_SERVICE_UUID = 0xA000;
  const static uint16_t LUX_VALUE_CHARACTERISTIC_UUID = 0xA001;

  LuxService(BLE &_ble, uint16_t initialValueForLUXCharacteristic) :
    ble(_ble), luxValue(LUX_VALUE_CHARACTERISTIC_UUID, &initialValueForLUXCharacteristic, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY)
  {
    GattCharacteristic *charTable[] = {&luxValue};
    GattService luxService(LUX_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.gattServer().addService(luxService);
  }

  void updateLuxValue(uint16_t newValue) {
    ble.gattServer().write(luxValue.getValueHandle(), (uint8_t *)&newValue, sizeof(bool));
  }

private:
  BLE &ble;
  ReadOnlyGattCharacteristic<uint16_t> luxValue;
};

#endif

 照度の値を扱うため、 private の変数として読み取り専用の Characteristic を luxValue として定義します。

private:
  BLE &ble;
  ReadOnlyGattCharacteristic<uint16_t> luxValue;

 コンストラクタでその Characteristic を初期化します。 Characteristic の値が更新された時に Notification を送れるように、初期化時に GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY を渡しています。そして Service 初期化時に Characteristic を登録します。 最後に Service を GATT Server に登録します。

  LuxService(BLE &_ble, uint16_t initialValueForLUXCharacteristic) :
    ble(_ble), luxValue(LUX_VALUE_CHARACTERISTIC_UUID, &initialValueForLUXCharacteristic, GattCharacteristic::BLE_GATT_CHAR_PROPERTIES_NOTIFY)
  {
    GattCharacteristic *charTable[] = {&luxValue};
    GattService luxService(LUX_SERVICE_UUID, charTable, sizeof(charTable) / sizeof(GattCharacteristic *));
    ble.gattServer().addService(luxService);
  }

 さらに照度の値の更新用のメソッドとして void updateLuxValue(uint16_t newValue) を実装しておきます。

  void updateLuxValue(uint16_t newValue) {
    ble.gattServer().write(luxValue.getValueHandle(), (uint8_t *)&newValue, sizeof(bool));
  }

 では LuxService を利用する main.cpp の実装です。コード全体は下記のようになります。

#include "mbed.h"
#include "ble/BLE.h"
#include "LuxService.h"

DigitalOut led(LED1, 0);

const static char DEVICE_NAME[] = "LUXSample";
static const uint16_t uuid16_list[] = {LuxService::LUX_SERVICE_UUID};

static volatile bool triggerSensorPolling = false;

uint16_t luxValue = 100;
static volatile bool riseValue = true;

LuxService *luxServicePtr;

Ticker ticker;

BLE ble;

void disconnectionCallback(const Gap::DisconnectionCallbackParams_t *params)
{
  BLE::Instance().gap().startAdvertising();
}

void periodicCallback(void)
{
  led = !led;
  triggerSensorPolling = true;
}

void onBleInitError(BLE &ble, ble_error_t error)
{
}

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)
{
  BLE& ble = params->ble;
  ble_error_t error = params->error;

  if (error != BLE_ERROR_NONE) {
    onBleInitError(ble, error);
    return;
  }

  if (ble.getInstanceID() != BLE::DEFAULT_INSTANCE) {
    return;
  }

  ble.gap().setDeviceName((const uint8_t *) DEVICE_NAME);
  ble.gap().onDisconnection(disconnectionCallback);

  uint16_t initialValueForLUXCharacteristic = 100;
  luxServicePtr = new LuxService(ble, initialValueForLUXCharacteristic);

  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS, (uint8_t *)uuid16_list, sizeof(uuid16_list));
  ble.gap().accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LOCAL_NAME, (uint8_t *)DEVICE_NAME, sizeof(DEVICE_NAME));
  ble.gap().setAdvertisingType(GapAdvertisingParams::ADV_CONNECTABLE_UNDIRECTED);
  ble.gap().setAdvertisingInterval(1000);
  ble.gap().startAdvertising();
}

int main(void)
{
  ticker.attach(periodicCallback, 1);

  ble.init(bleInitComplete);

  while (ble.hasInitialized() == false) { /* spin loop */ }

  while (true) {
    if (triggerSensorPolling && ble.getGapState().connected) {
      triggerSensorPolling = false;

      if (riseValue) {
        luxValue++;
      } else {
        luxValue--;
      }

      if (luxValue == 200) {
        riseValue = false;
      } else if (luxValue == 100) {
        riseValue = true;
      }

      luxServicePtr->updateLuxValue(luxValue);
    } else {
      ble.waitForEvent();
    }
  }
}

 カスタムサービスに関連するところをピックアップして簡単に説明します。まずは先ほど実装した LuxService を include します。

#include "LuxService.h"

 UUID のリストは LuxService で定義されている UUID を使用して作成します。

static const uint16_t uuid16_list[] = {LuxService::LUX_SERVICE_UUID};

 bleInitComplete の中で LuxService を初期化します。

  uint16_t initialValueForLUXCharacteristic = 100;
  luxServicePtr = new LuxService(ble, initialValueForLUXCharacteristic);

 あとは main() の中で、センサーから取得した値(今回は擬似的に変化させている値)で LuxService の値を更新します。

      luxServicePtr->updateLuxValue(luxValue);

 そしてコンパイルして BLE Nano に hex ファイルをコピーし、動作を確認してみます。 LightBlue で確認すると下記のようにデバイスがリストに表示されます。

f:id:akanuma-hiroaki:20170917235501p:plain:w300

 デバイスを選択して接続すると、プロパティに Read と Notify が表示されています。

f:id:akanuma-hiroaki:20170917235622p:plain:w300

 Characteristic を選択して Notification の Listen を開始すると、毎秒照度の値が更新される度に値が表示されていきます。

f:id:akanuma-hiroaki:20170917235743p:plain:w300

まとめ

 ひとまずサンプルコードを頼りに見よう見まねで Advertisement の送信と、カスタムサービスを利用する例を実装してみました。細かいところはまだ理解できていないですが、とりあえず動かせるということはわかったので、詳細は順次確認しつつ、実際にセンサーと組み合わせて処理ができるようにしてみたいと思います。

BLE Nano を mbed で iBeacon 化する

 前回 BLE Nano で Lチカまでやったので、今回は BLE Nano を iBeacon 化して、iOS アプリから検知してみたいと思います。

 また、開発環境としては前回の記事で、 Vagrant で構築した VM からだと mbed import の途中で止まってしまったと書きましたが、 Mac 上の環境と同様で時間がかかるものの、待てば正常に進んだので、今回は Vagrant で構築した CentOS 環境から mbed cli で実行します。

サンプルコードを動かす

 ひとまず動かしてみるだけであれば公開されているサンプルコードをインポートしてそのままコンパイルすれば動かせてしまいます。まずはインポート。

[vagrant@localhost vagrant]$ mbed import https://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_iBeacon/
[mbed] Importing program "BLE_iBeacon" from "https://mbed.org/teams/Bluetooth-Low-Energy/code/BLE_iBeacon" at latest revision in the current branch
[mbed] Adding library "BLE_API" from "https://developer.mbed.org/teams/Bluetooth-Low-Energy/code/BLE_API" at rev #65474dc93927
[mbed] Adding library "mbed" from "https://mbed.org/users/mbed_official/code/mbed/builds" at rev #abea610beb85
[mbed] Downloading library build "abea610beb85" (might take a minute)
[mbed] Unpacking library build "abea610beb85" in "/vagrant/BLE_iBeacon/mbed"
[mbed] Adding library "nRF51822" from "https://mbed.org/teams/Nordic-Semiconductor/code/nRF51822" at rev #c90ae1400bf2
[mbed] Adding library "shields/TARGET_ST_BLUENRG" from "https://developer.mbed.org/teams/ST/code/X_NUCLEO_IDB0XA1" at rev #fa98703ece8e
[mbed] Couldn't find build tools in your program. Downloading the mbed 2.0 SDK tools...

 続けて toolchain と target を指定してからコンパイルしてみます。

[vagrant@localhost vagrant]$ cd BLE_iBeacon/                                        
[vagrant@localhost BLE_iBeacon]$                                                    
[vagrant@localhost BLE_iBeacon]$ mbed detect                                        
                                                                                    
[mbed] Detected RBLAB_BLENANO, port /dev/ttyACM0, mounted /run/media/vagrant/DAPLINK
[mbed] Supported toolchains for RBLAB_BLENANO                                       
+--------+-----------+-----------+-----+---------+-----+                            
| Target | mbed OS 2 | mbed OS 5 | ARM | GCC_ARM | IAR |                            
+--------+-----------+-----------+-----+---------+-----+                            
+--------+-----------+-----------+-----+---------+-----+                            
Supported targets: 0                                                                
                                                                                    
[vagrant@localhost BLE_iBeacon]$                                                    
[vagrant@localhost BLE_iBeacon]$ mbed toolchain GCC_ARM                             
[mbed] GCC_ARM now set as default toolchain in program "BLE_iBeacon"                
[vagrant@localhost BLE_iBeacon]$ mbed target RBLAB_BLENANO                          
[mbed] RBLAB_BLENANO now set as default target in program "BLE_iBeacon"             
[vagrant@localhost BLE_iBeacon]$                                                    
[vagrant@localhost BLE_iBeacon]$ mbed compile                                       
Building project BLE_iBeacon (RBLAB_BLENANO, GCC_ARM)                               
Scan: .                                                                             
Scan: mbed                                                                          
Scan: env                                                                           
Compile [  1.8%]: BLE.cpp                                                           
〜〜〜中略〜〜〜
Compile [100.0%]: nRF5xn.cpp
Link: BLE_iBeacon
/usr/local/lib/gcc-arm-none-eabi-4_9-2015q3/bin/../lib/gcc/arm-none-eabi/4.9.3/../../../../arm-none-eabi/lib/armv6-m/crt0.o: In function `_start':
(.text+0x52): undefined reference to `__wrap_exit'
./mbed/abea610beb85/TARGET_RBLAB_BLENANO/TOOLCHAIN_GCC_ARM/retarget.o: In function `__cxa_pure_virtual':
retarget.cpp:(.text.__cxa_pure_virtual+0x4): undefined reference to `__wrap_exit'
./mbed/abea610beb85/TARGET_RBLAB_BLENANO/TOOLCHAIN_GCC_ARM/libmbed.a(mbed_error.o): In function `error':
mbed_error.c:(.text.error+0x10): undefined reference to `__wrap_exit'
collect2: error: ld returned 1 exit status
[ERROR] /usr/local/lib/gcc-arm-none-eabi-4_9-2015q3/bin/../lib/gcc/arm-none-eabi/4.9.3/../../../../arm-none-eabi/lib/armv6-m/crt0.o: In function `_start':
(.text+0x52): undefined reference to `__wrap_exit'
./mbed/abea610beb85/TARGET_RBLAB_BLENANO/TOOLCHAIN_GCC_ARM/retarget.o: In function `__cxa_pure_virtual':
retarget.cpp:(.text.__cxa_pure_virtual+0x4): undefined reference to `__wrap_exit'
./mbed/abea610beb85/TARGET_RBLAB_BLENANO/TOOLCHAIN_GCC_ARM/libmbed.a(mbed_error.o): In function `error':
mbed_error.c:(.text.error+0x10): undefined reference to `__wrap_exit'
collect2: error: ld returned 1 exit status

[mbed] ERROR: "/usr/local/pyenv/versions/anaconda2-4.4.0/bin/python" returned error code 1.
[mbed] ERROR: Command "/usr/local/pyenv/versions/anaconda2-4.4.0/bin/python -u /vagrant/BLE_iBeacon/.temp/tools/make.py -t GCC_ARM -m RBLAB_BLENANO --source . --build ./BUILD/RBLAB_BLENANO/GCC_ARM" in "/vagrant/BLE_iBeacon"
---

 コンパイル自体は成功しましたが、ライブラリをリンクするところでエラーになってしまいました。調べたところ下記のページの情報をみつけました。

https://developer.mbed.org/forum/bugs-suggestions/topic/27426/?page=1#comment-52739

 mbed.bld で指定されているビルドが古いということなので、上記ページにあるように変更してみます。

[vagrant@localhost BLE_iBeacon]$ cat mbed.bld 
https://mbed.org/users/mbed_official/code/mbed/builds/abea610beb85
 ↓
[vagrant@localhost BLE_iBeacon]$ cat mbed.bld
https://mbed.org/users/mbed_official/code/mbed/builds/4eea097334d6

 そして mbed deploy で更新します。

[vagrant@localhost BLE_iBeacon]$ mbed deploy
[mbed] Updating library "BLE_API" to rev #65474dc93927
[mbed] Updating library "mbed" to rev #4eea097334d6
[mbed] Downloading library build "4eea097334d6" (might take a minute)
[mbed] Unpacking library build "4eea097334d6" in "/vagrant/BLE_iBeacon/mbed"
[mbed] Updating library "nRF51822" to rev #c90ae1400bf2
[mbed] Updating library "shields/TARGET_ST_BLUENRG" to rev #fa98703ece8e
[mbed] Updating the mbed 2.0 SDK tools...

 そしてコンパイル。

[vagrant@localhost BLE_iBeacon]$ mbed compile
Building project BLE_iBeacon (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: mbed
Scan: env
Compile [  1.8%]: BLE.cpp
〜〜〜中略〜〜〜
Compile [100.0%]: nRF5xn.cpp
[Warning] nRF5xGap.h@223,93: 'void mbed::Ticker::attach_us(T*, M, timestamp_t) [with T = nRF5xGap; M = void (nRF5xGap::*)(); timestamp_t = long unsigned int]' is deprecated (declared at ./mbed/4eea097334d6/drivers/Ticker.h:121): The attach_us function does not support cv-qualifiers. Replaced by attach_us(callback(obj, method), t). [since mbed-os-5.1] [-Wdeprecated-declarations]
Link: BLE_iBeacon
Elf2Bin: BLE_iBeacon
+-----------+-------+-------+------+
| Module    | .text | .data | .bss |
+-----------+-------+-------+------+
| Fill      |   111 |     3 |   28 |
| Misc      | 32560 |   141 | 1124 |
| Subtotals | 32671 |   144 | 1152 |
+-----------+-------+-------+------+
Allocated Heap: 2800 bytes
Allocated Stack: 2048 bytes
Total Static RAM memory (data + bss): 1296 bytes
Total RAM memory (data + bss + heap + stack): 6144 bytes
Total Flash memory (text + data + misc): 32815 bytes

Image: ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_iBeacon.hex

 成功して hex ファイルが作成されました。ちなみにこの時点での最新のビルドは a330f0fddbec でした。

https://developer.mbed.org/users/mbed_official/code/mbed//builds/a330f0fddbec

 ですがこのビルドを指定すると下記のようなエラーになってしまったので、 4eea097334d6 で実行しています。

[vagrant@localhost BLE_iBeacon]$ mbed compile
Building project BLE_iBeacon (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: mbed
Scan: env
Compile [  1.8%]: BLE.cpp
[Warning] toolchain.h@23,2: #warning toolchain.h has been replaced by mbed_toolchain.h, please update to mbed_toolchain.h [since mbed-os-5.3] [-Wcpp]
Compile [  3.6%]: BLEInstanceBase.cpp
Compile [  5.5%]: DiscoveredCharacteristic.cpp
Compile [  7.3%]: GapScanningParams.cpp
Compile [  9.1%]: DFUService.cpp
Compile [ 10.9%]: UARTService.cpp
Compile [ 12.7%]: URIBeaconConfigService.cpp
Compile [ 14.5%]: main.cpp
[Fatal Error] iBeacon.h@19,26: core_cmInstr.h: No such file or directory
[ERROR] In file included from ./main.cpp:18:0:
./BLE_API/ble/services/iBeacon.h:19:26: fatal error: core_cmInstr.h: No such file or directory
 #include "core_cmInstr.h"
                          ^
compilation terminated.

[mbed] ERROR: "/usr/local/pyenv/versions/anaconda2-4.4.0/bin/python" returned error code 1.
[mbed] ERROR: Command "/usr/local/pyenv/versions/anaconda2-4.4.0/bin/python -u /vagrant/BLE_iBeacon/.temp/tools/make.py -t GCC_ARM -m RBLAB_BLENANO --source . --build ./BUILD/RBLAB_BLENANO/GCC_ARM" in "/vagrant/BLE_iBeacon"
---

 それではBLE Nano に hex ファイルをコピーして動作確認してみます。

[vagrant@localhost BLE_iBeacon]$ mbed detect

[mbed] Detected RBLAB_BLENANO, port /dev/ttyACM0, mounted /run/media/vagrant/DAPLINK
[mbed] Supported toolchains for RBLAB_BLENANO
+--------+-----------+-----------+-----+---------+-----+
| Target | mbed OS 2 | mbed OS 5 | ARM | GCC_ARM | IAR |
+--------+-----------+-----------+-----+---------+-----+
+--------+-----------+-----------+-----+---------+-----+
Supported targets: 0

[vagrant@localhost BLE_iBeacon]$ 
[vagrant@localhost BLE_iBeacon]$ cp ./BUILD/RBLAB_BLENANO/GCC_ARM/BLE_iBeacon.hex /run/media/vagrant/DAPLINK/
[vagrant@localhost BLE_iBeacon]$ 

 BLE Nano 側での作業は一旦終了で、 iPhone 側で iBeacon を検知するために下記アプリをインストールします。

Locate Beacon

Locate Beacon

  • Radius Networks
  • ユーティリティ
  • 無料

 アプリを起動して下記のような内容で検知対象UUIDを登録します。

f:id:akanuma-hiroaki:20170915092148p:plain:w300

 すると下記のように iBeacon が検知されます。

f:id:akanuma-hiroaki:20170915092349p:plain:w300f:id:akanuma-hiroaki:20170915092356p:plain:w300

サンプルコードの内容

 サンプルコードの内容について自分が理解した範囲で説明してみます。ちなみに mbed の BLE API のリファレンスは下記にあります。

BLE_API | Mbed

 まずは main() の中で ble.init() で BLE Controller を初期化します。 BLE API を使う場合にはまずこれをやります。引数には初期化完了時に実行されるコールバックメソッドを指定します。

ble.init(bleInitComplete);

 コールバックメソッドには BLE::InitializationCompleteCallbackContex のインスタンスが渡されます。

void bleInitComplete(BLE::InitializationCompleteCallbackContext *params)

 コールバックメソッドの中で iBeacon クラスを new することで BLE Advertisement を iBeacon として送るためのセットアップが行われます。uuid, majorNumber, minorNumber, txPower はとりあえずサンプルコードのデフォルト値をそのまま使います。

const uint8_t uuid[] = {0xE2, 0x0A, 0x39, 0xF4, 0x73, 0xF5, 0x4B, 0xC4,
                        0xA1, 0x2F, 0x17, 0xD1, 0xAD, 0x07, 0xA9, 0x61};
uint16_t majorNumber = 1122;
uint16_t minorNumber = 3344;
uint16_t txPower     = 0xC8;
iBeacon *ibeacon = new iBeacon(ble, uuid, majorNumber, minorNumber, txPower);

 Advertisement の送信間隔を設定し、送信を開始します。

ble.gap().setAdvertisingInterval(1000); /* 1000ms. */
ble.gap().startAdvertising();

 main() の中では BLE Controller の初期化が終わるまで空のループを回して待ちます。

while (!ble.hasInitialized()) { /* spin loop */ }

 初期化が終わったら無限ループを回し、イベントが発生するまで待機を繰り返します。

while (true) {
    ble.waitForEvent(); // allows or low power operation
}

まとめ

 とりあえず iBeacon の Advertisement を送信するだけならサンプルコードをそのまま使ってとても簡単に実行することができてしまいました。次は色々なセンサー類と組み合わせるためにも、Peripheralとして動作できるように挑戦してみたいと思います。

 また、環境面では mbed cli だと Web IDE と比べてやはりライブラリとの依存関係などで悩まされることは多くなりそうです。すぐ解決できる場合は良いのですが、困った時はあまり悩まず Web IDE に切り替えて使うのが良さそうです。

BLE Nano と mbed CLI で Lチカしてみる

※USB接続の書き込み用ボードとして MK20 がセットになったものと DAPLink がセットになったものがあります。当初 MK20 と書いていましたが、私が購入したものは DAPLink でしたので、修正しました。(2017/09/18)

 BLE デバイスの Firmware 開発に興味があって入門用のものを探したところ、 BLE Nano というのがみつかったので購入しました。

BLE Nano — RedBear

f:id:akanuma-hiroaki:20170910001301j:plain:w450

 BLE Nano 単体のものと、USB接続の書き込み用ボード MK20 もしくは DAPLink がセットになったキットがありますが、初めて買う場合は書き込み用ボードも必要なのでキットを買うことになります。

www.switch-science.com

 スイッチサイエンスでは在庫切れだったのですが、運よくツクモのロボット王国で購入できました。

 業界最小と謳っているだけのことはあり、かなり小さいです。実際にデバイスに搭載されるモジュール部分だけであれば100円硬貨と同じぐらいです。

f:id:akanuma-hiroaki:20170910001752j:plain:w450

 今回はチュートリアルに沿ってオンラインの開発環境で試した後、 mbed CLI でターミナル上から開発できるようにしてみたいと思います。

設定&チュートリアル

 BLE Nano のチュートリアルは下記ページに公開されています。

Getting Started - nRF51822 — RedBear

 ハードウェアセットアップの説明では MK20 と BLE Nano にピンヘッダをはんだ付けすることになっていますが、私が買ったキットは DAPLink とのセットで、すでにはんだ付けされていました。また、MK20 側のホールが余るような説明になっていますが、DAPLink 側のホールは余っていませんでした。

f:id:akanuma-hiroaki:20170910003341j:plain:w450

f:id:akanuma-hiroaki:20170910003411j:plain:w450

 BLE Nano の開発環境としては、 GCC、 mbed、 Arduino IDE に対応しています。手軽に使えそうなのは mbed なので、今回は mbed で試してみたいと思います。チュートリアルページで紹介されている mbed のリンク先ではアカウント登録はできないようで、下記ページから登録しました。

developer.mbed.org

 アカウント登録が終わったら、BLE Nano + DAPLink をUSBポートに挿すと、USBドライブとして認識されます。私の環境では /Volumes/DAPLINK として認識されていました。ドライブの中に含まれている MBED.HTM をブラウザで開きます。

f:id:akanuma-hiroaki:20170910011134p:plain

 RedBearLab BLE Nano の画面が開きますので、画面右上の Compiler メニューをクリックするとオンラインの開発環境にアクセスでき、下記のような画面になります。

f:id:akanuma-hiroaki:20170910004607p:plain

 画面右上の RedBearLab BLE Nano と表示されているところをクリックすると対象のプラットフォームを選択できますが、すでに BLE Nano が選択されているのでそのままで問題ないと思います。

 また、画面左上の インポート メニューから、公開されているリポジトリのプログラムをインポートすることができます。

f:id:akanuma-hiroaki:20170910005602p:plain

 チュートリアルでは mbed_blinky を検索して選択していますが、検索ではチュートリアルと同じものがみつからなかったので、直接下記URLを指定する形でインポートしました。

https://developer.mbed.org/teams/mbed/code/mbed_blinky/

 無事にインポートされると下記のようにファイルリストが表示されます。

f:id:akanuma-hiroaki:20170910005938p:plain

 main.cpp をダブルクリックして内容を表示し、チュートリアルにあるように、 myled の値を P0_19 に変更します。

f:id:akanuma-hiroaki:20170910010036p:plain

 そして画面上部の コンパイル メニューをクリックするとコンパイルが実行され、 mbed_blinky_RBLAB_BLENANO.hex というファイルがビルドされますので、 /Volumes/DAPLINK にコピーすると BLE Nano にプログラムが書き込まれ、実行されます。

f:id:akanuma-hiroaki:20170910011952j:plain:w450

mbed CLI

 チュートリアルで紹介されているのはここまでですが、オフライン環境でも開発できて CLI から使える mbed CLI というのがあったので、こちらで同様のことを行ってみたいと思います。私としては vim から編集できて、コマンドラインからコンパイル等も行う方が好きなので、良さそうであればこちらのやり方でやっていきたいと思います。

 開発環境の設定については下記ページで紹介されていますので、紹介されている通りに手順を実行します。 Python, Git, Mercurial, GCC をインストールした後で mbed CLI をインストールする形になります。私の場合は Mac 環境なので、後者のページの手順を実行しました。

mbed オフラインの開発環境 | Mbed

mbed CLI (コマンドライン・インタフェース)を Mac OS X で使ってみる | Mbed

 必要なもののインストールが終わったら作業ディレクトリに移動して、オンラインの開発環境でやったのと同じように、公開されているプログラムをインポートします。

mbed_cli  $ mbed import https://developer.mbed.org/teams/mbed/code/mbed_blinky/
[mbed] Importing program "mbed_blinky" from "https://developer.mbed.org/teams/mbed/code/mbed_blinky" at latest revision in the current branch
[mbed] Adding library "mbed" from "https://mbed.org/users/mbed_official/code/mbed/builds" at rev #a97add6d7e64
[mbed] Downloading library build "a97add6d7e64" (might take a minute)
[mbed] Unpacking library build "a97add6d7e64" in "/Users/akanuma/workspace/mbed_cli/mbed_blinky/mbed"
[mbed] Couldn't find build tools in your program. Downloading the mbed 2.0 SDK tools...
[mbed] Auto-installing missing Python modules...

 上記の例だと mbed_blinky というディレクトリが作成されているのでその中に入ります。

mbed_cli  $ cd mbed_blinky/
mbed_blinky  $ 
mbed_blinky  $ ls -ltr
total 24
-rw-r--r--  1 akanuma  staff    66 Sep  9 23:14 mbed.bld
-rw-r--r--  1 akanuma  staff   168 Sep  9 23:14 main.cpp
drwxr-xr-x  4 akanuma  staff   136 Sep  9 23:17 mbed
-rw-r--r--  1 akanuma  staff  1329 Sep  9 23:18 mbed_settings.py

 mbed detect コマンドを実行すると、接続されているデバイスを検出してくれるので実行してみます。

mbed_blinky  $ mbed detect
Traceback (most recent call last):
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/detect_targets.py", line 25, in <module>
    from tools.options import get_default_options_parser
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/options.py", line 21, in <module>
    from tools.toolchains import TOOLCHAINS
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/toolchains/__init__.py", line 29, in <module>
    from tools.config import Config
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/config/__init__.py", line 29, in <module>
    from tools.arm_pack_manager import Cache
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/arm_pack_manager/__init__.py", line 2, in <module>
    from bs4 import BeautifulSoup
ImportError: No module named bs4
[mbed] ERROR: "/usr/local/opt/python/bin/python2.7" returned error code 1.
[mbed] ERROR: Command "/usr/local/opt/python/bin/python2.7 -u /Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/detect_targets.py" in "/Users/akanuma/workspace/mbed_cli/mbed_blinky"
---

 bs4 というモジュールがないということなので、 pip でインストールします。

mbed_blinky  $ pip install beautifulsoup4
Collecting beautifulsoup4
  Downloading beautifulsoup4-4.6.0-py2-none-any.whl (86kB)
    100% |████████████████████████████████| 92kB 2.5MB/s 
Installing collected packages: beautifulsoup4
Successfully installed beautifulsoup4-4.6.0
mbed_blinky  $ 
mbed_blinky  $ mbed detect                                                                                                                                                                                                                    
Traceback (most recent call last):
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/detect_targets.py", line 25, in <module>
    from tools.options import get_default_options_parser
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/options.py", line 21, in <module>
    from tools.toolchains import TOOLCHAINS
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/toolchains/__init__.py", line 29, in <module>
    from tools.config import Config
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/config/__init__.py", line 29, in <module>
    from tools.arm_pack_manager import Cache
  File "/Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/arm_pack_manager/__init__.py", line 20, in <module>
    from fuzzywuzzy import process
ImportError: No module named fuzzywuzzy
[mbed] ERROR: "/usr/local/opt/python/bin/python2.7" returned error code 1.
[mbed] ERROR: Command "/usr/local/opt/python/bin/python2.7 -u /Users/akanuma/workspace/mbed_cli/mbed_blinky/.temp/tools/detect_targets.py" in "/Users/akanuma/workspace/mbed_cli/mbed_blinky"
---

 今度は fuzzywuzzy がないということなのでこちらもインストールします。

mbed_blinky  $ pip install fuzzywuzzy
Collecting fuzzywuzzy
  Downloading fuzzywuzzy-0.15.1-py2.py3-none-any.whl
Installing collected packages: fuzzywuzzy
Successfully installed fuzzywuzzy-0.15.1
mbed_blinky  $ 
mbed_blinky  $ mbed detect                                                                                                                                                                                                                    

[mbed] Detected RBLAB_BLENANO, port /dev/tty.usbmodem1412, mounted /Volumes/DAPLINK
[mbed] Supported toolchains for RBLAB_BLENANO
+--------+-----------+-----------+-----+---------+-----+
| Target | mbed OS 2 | mbed OS 5 | ARM | GCC_ARM | IAR |
+--------+-----------+-----------+-----+---------+-----+
+--------+-----------+-----------+-----+---------+-----+
Supported targets: 0

mbed_blinky  $ 

 Detected RBLAB_BLENANO ということで、 BLE Nano が検出されました。表の中と、 Supported targets に何も表示されていないのが気になるところですがひとまず進みます。続いてツールチェーン(コンパイラ)とターゲットを指定します。

mbed_blinky  $ mbed toolchain GCC_ARM
[mbed] GCC_ARM now set as default toolchain in program "mbed_blinky"
mbed_blinky  $ 
mbed_blinky  $ mbed target RBLAB_BLENANO
[mbed] RBLAB_BLENANO now set as default target in program "mbed_blinky"

 それではコンパイルしてみます。 mbed compile を実行するだけです。

mbed_blinky  $ mbed compile
Building project mbed_blinky (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: env
Scan: mbed
Compile [100.0%]: main.cpp
Link: mbed_blinky
Elf2Bin: mbed_blinky
+-----------+-------+-------+------+
| Module    | .text | .data | .bss |
+-----------+-------+-------+------+
| Fill      |    20 |     0 |   15 |
| Misc      |  8389 |   124 |  301 |
| Subtotals |  8409 |   124 |  316 |
+-----------+-------+-------+------+
Allocated Heap: 3648 bytes
Allocated Stack: 2048 bytes
Total Static RAM memory (data + bss): 440 bytes
Total RAM memory (data + bss + heap + stack): 6136 bytes
Total Flash memory (text + data + misc): 8533 bytes

Image: ./BUILD/RBLAB_BLENANO/GCC_ARM/mbed_blinky.hex

 コンパイルが実行され、 ./BUILD/RBLAB_BLENANO/GCC_ARM/mbed_blinky.hex というファイルが作成されました。これを BLE Nano にコピーします。

mbed_blinky  $ cp ./BUILD/RBLAB_BLENANO/GCC_ARM/mbed_blinky.hex /Volumes/DAPLINK/

 コピーが完了すると BLE Nano でプログラムが実行されます。

 最初は Vagrant で Linux の VM 上に mbed CLI の環境を作ってみたのですが、インポートしようとすると [mbed] Downloading library build "a97add6d7e64" (might take a minute) で止まってしまって進まなかったので、Macに直接環境を作りました。

デバッグ環境の設定

 前述の mbed CLI の参考サイトにデバッグ方法も紹介されていたので環境設定をしてみました。紹介されている方法の中でMacで使えるのは Eclipse と pyOCD でのデバッグなので、その方法を試してみました。

 まずは Eclipse IDE for C/C++ Developers をダウンロードして解凍し、 Eclipse.app を Applications ディレクトリに移動して起動します。

 次は GNU ARM Eclipse plugin をインストールということなんですが、前述のページで紹介されているURLは Deprecated ということで、色々調べたところ、 GNU ARM plugin は GNU MCU plugin に変わっているようで、下記のサイトでインストール方法が紹介されていました。

gnu-mcu-eclipse.github.io

 Marketplace で検索してインストールするやり方ではエラーになってしまったので Install New Software から下記のURLを指定してインストールしました。

http://gnu-mcu-eclipse.netlify.com/v4-neon-updates/

 PyOCD はインストール済みでした。

mbed_cli  $ pip install pyocd
Requirement already satisfied: pyocd in /usr/local/lib/python2.7/site-packages
Requirement already satisfied: websocket-client in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: enum34 in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: intelhex in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: six in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: future in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: hidapi in /usr/local/lib/python2.7/site-packages (from pyocd)
Requirement already satisfied: setuptools>=19.0 in /usr/local/lib/python2.7/site-packages (from hidapi->pyocd)

 GDB がインストールされていなかったので brew でインストールします。

mbed_blinky  $ brew install gdb

 環境の準備ができたら --profile debug オプションをつけてプログラムをコンパイルします。

mbed_blinky  $ mbed compile --profile debug
Building project mbed_blinky (RBLAB_BLENANO, GCC_ARM)
Scan: .
Scan: env
Scan: mbed
Compile [100.0%]: main.cpp
Link: mbed_blinky
Elf2Bin: mbed_blinky
+-----------+-------+-------+------+
| Module    | .text | .data | .bss |
+-----------+-------+-------+------+
| Fill      |    21 |     0 |   15 |
| Misc      |  8604 |   124 |  301 |
| Subtotals |  8625 |   124 |  316 |
+-----------+-------+-------+------+
Allocated Heap: 3648 bytes
Allocated Stack: 2048 bytes
Total Static RAM memory (data + bss): 440 bytes
Total RAM memory (data + bss + heap + stack): 6136 bytes
Total Flash memory (text + data + misc): 8749 bytes

Image: ./BUILD/RBLAB_BLENANO/GCC_ARM/mbed_blinky.hex

 そして Eclipse からデバッグ設定をします。Eclipse での設定も前述の mbed オフラインの開発環境 | Mbed のページに書かれています。

 「Run」メニューの 「Debug Configurations…」からデバッグ設定をします。GDB PyOCD Debugging の下に新しい設定を作成し、 C/C++ Application に先ほどコンパイルして作成されたファイルを指定します。

f:id:akanuma-hiroaki:20170910145301p:plain

 「Debugger」タブで pyOCD の Executable に pyocd-gdbserver のパスを設定し、GDB の Executable に gdb のパスを設定します。

f:id:akanuma-hiroaki:20170910145520p:plain

 ここまで設定したら設定を保存して Debug ボタンでデバッグを開始します。すると Eclipse の Debug Perspective が表示され、実行状況が見られるようになります。

f:id:akanuma-hiroaki:20170910145637p:plain

 試した限りではまだ実行中のコードを見ることができていないので、今後その辺りは調べてみようと思います。

まとめ

 もともと Web 系のエンジニアだった自分としては BLE の Firmware 開発は敷居が高かったのですが、 BLE Nano と mbed で、ひとまず動かしてみるぐらいまでは手軽にできました。また、 mbed CLI でオフラインでターミナルから開発することができるので、 vim でコードを書いたり、Git でバージョン管理もやりやすそうなのですが、私の環境では mbed CLI だとプログラムのインポートや新規作成に10分以上かかってしまっていたので、初期に色々試してみる場合はオンラインの開発環境の方がやりやすいかもしれません。まだ単体で動かしているだけですが、今後色々なセンサーと組み合わせて動かせるようにしていきたいと思います。

OpenBlocks IoT BX1 から Ruby で BLE デバイスにアクセスする

 以前の記事で OpenBlocks IoT BX1 の WebUI から設定を行うことで、 SensorTag のデータ取得を行いました。

blog.akanumahiroaki.com

 SensorTag は BX1 のサポート対象になっているため、 WebUI からの設定のみでセンサーデータを読み取ることができていましたが、サポート対象外の BLE デバイスの場合にはデータを取得するための処理を自前で実装する必要があります。そこで今回は Ruby のスクリプトから SensorTag のデータを読み取ってみたいと思います。

ssh設定

 まずは作業を行いやすいように、 BX1 に ssh でログインできるようにします。工場出荷状態から作業をしたとすると、上記の記事の「初期設定」の項目の内容を実施し、 Wi-Fi 経由でアクセスできるようにします。

 初期設定が終わり再起動が完了したら再び WebUI にアクセスし、「システム」メニューの「フィルター」タブで「SSH」のラジオボタンを「有効」にします。また、再起動後も設定が保持されるように、「再起動後もフィルタ解放設定を有効にする」にチェックを入れて保存します。

f:id:akanuma-hiroaki:20170903224117p:plain

 「SSH関連」タブで ssh の詳細設定を行うことができますが、今回はとりあえずのお試しということで、特に設定は変更せず、 root ログインも許可します。実際の本番運用時にはセキュリティ的に問題ないように設定を変更する必要があるかと思います。

f:id:akanuma-hiroaki:20170903224319p:plain

 root ユーザのパスワードは「パスワード」タブで設定できます。

f:id:akanuma-hiroaki:20170903224517p:plain

 これで Wi-Fi 経由で ssh ログインができるようになります。

$ ssh root@192.168.10.100
root@192.168.10.100's password: 
Linux obsiot.example.org 3.10.17-poky-edison #1 SMP PREEMPT Thu Jun 1 16:35:38 JST 2017 i686

The programs included with the Debian GNU/Linux system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Debian GNU/Linux comes with ABSOLUTELY NO WARRANTY, to the extent
permitted by applicable law.
Last login: Sun Sep  3 22:47:04 2017 from 192.168.10.8

デフォルトの BlueZ バージョンでのアクセス確認

 BX1 にはデフォルトで BlueZ がインストールされていますが、バージョンは少し古く、 5.23 です。

root@obsiot:~# apt-cache show bluez
Package: bluez
Source: bluez (5.23-2)
Version: 5.23-2+b1
Installed-Size: 3043
Maintainer: Debian Bluetooth Maintainers <pkg-bluetooth-maintainers@lists.alioth.debian.org>
Architecture: i386
Replaces: bluez-audio (<= 3.36-3), bluez-input, bluez-network, bluez-serial, bluez-utils (<= 3.36-3), udev (<< 170-1)
Depends: libc6 (>= 2.15), libdbus-1-3 (>= 1.1.1), libglib2.0-0 (>= 2.28.0), libreadline6 (>= 6.0), libudev1 (>= 196), init-system-helpers (>= 1.18~), kmod, udev (>= 170-1), lsb-base, dbus
Conflicts: bluez-audio (<= 3.36-3), bluez-utils (<= 3.36-3)
Breaks: udev (<< 170-1)
Description-en: Bluetooth tools and daemons
 This package contains tools and system daemons for using Bluetooth devices.
 .
 BlueZ is the official Linux Bluetooth protocol stack. It is an Open Source
 project distributed under GNU General Public License (GPL).
Description-md5: ef25d6a9f4a57e78f32faa7b58ef4e59
Multi-Arch: foreign
Homepage: http://www.bluez.org
Tag: admin::hardware, admin::kernel, hardware::TODO, implemented-in::c,
 protocol::TODO, role::program, use::driver, use::synchronizing
Section: admin
Priority: optional
Filename: pool/main/b/bluez/bluez_5.23-2+b1_i386.deb
Size: 750516
MD5sum: 6f731b661885f89ce75d9204a0ca7a8a
SHA1: 502e7f2d1d99b615313e846fd07177d1cc763e2f
SHA256: 3ab85fc151b51dfe91830dd546554fe739dbd7ac069c091eccb39cce88d9a79f

 apt-get でインストールできるのはこれが最新らしいのですが、執筆時の BlueZ の最新バージョンは 5.46 ですので、少し開きがあります。

 また、 bluetoothd は –experimental オプションなしで起動しています。

root@obsiot:~# ps aux | grep bluetoothd | grep -v grep
root      1498  0.0  0.1   5236  1804 ?        S    23:03   0:00 /usr/sbin/bluetoothd

 –experimental オプション付きで起動するには、 /etc/init.d/bluetooth を編集して、 SSD_OPTIONS の行の内容を下記のように変更します。

SSD_OPTIONS="--oknodo --quiet --exec $DAEMON -- $NOPLUGIN_OPTION"
 ↓
SSD_OPTIONS="--oknodo --quiet --exec $DAEMON -- $NOPLUGIN_OPTION --experimental"

 変更したら bluetoothd を再起動すると、 –experimental オプション付きで起動します。

root@obsiot:~# /etc/init.d/bluetooth restart
Stopping bluetooth: /usr/sbin/bluetoothd.
Starting bluetooth: bluetoothd.
root@obsiot:~# 
root@obsiot:~# ps aux | grep bluetoothd
root     18692  0.3  0.1   5236  1812 ?        S    23:51   0:00 /usr/sbin/bluetoothd --experimental
root     18710  0.0  0.0   5748   832 pts/1    S+   23:51   0:00 grep bluetoothd

 そしてデフォルトでは Bluetooth コントローラは有効になっていません。

root@obsiot:~# rfkill list
0: phy0: Wireless LAN
        Soft blocked: no
        Hard blocked: no
1: brcmfmac-wifi: Wireless LAN
        Soft blocked: no
        Hard blocked: no
2: bcm43xx Bluetooth: Bluetooth
        Soft blocked: yes
        Hard blocked: no

 この状態で bluetoothctl を使用しても下記のようにエラーになります。

root@obsiot:~# bluetoothctl
[bluetooth]# power on
No default controller available

 有効にするためには下記コマンドを実行します。

root@obsiot:~# bluetooth_rfkill_event &
root@obsiot:~# rfkill unblock bluetooth

 実行時の出力は下記のようになります。

root@obsiot:~# bluetooth_rfkill_event &
[1] 3752
root@obsiot:~# 1504361980.098955: idx 2 type 2 op 0 soft 1 hard 0

root@obsiot:~# rfkill unblock bluetooth
1504362012.636847: idx 2 type 2 op 2 soft 0 hard 0
root@obsiot:~# execute brcm_patchram_plus --use_baudrate_for_download --no2bytes --enable_fork --enable_lpm --enable_hci --baudrate 3000000 --patchram /etc/firmware/bcm43341.hcd --bd_addr 98:4F:EE:04:B3:24 --scopcm 1,0,0,0,0,0,0,0,0,0 /dev/ttyMFD0
Done setting line discipline
1504362013.132510: idx 3 type 2 op 0 soft 0 hard 0

 rfkill については下記サイトで説明されています。無線デバイスのON/OFFを行うためのコマンドになります。

3.12. RFKill

 実行後は下記のように hci0 のブロックが解除されて、使用できるようになっています。

root@obsiot:~# rfkill list
0: phy0: Wireless LAN
        Soft blocked: no
        Hard blocked: no
1: brcmfmac-wifi: Wireless LAN
        Soft blocked: no
        Hard blocked: no
2: bcm43xx Bluetooth: Bluetooth
        Soft blocked: no
        Hard blocked: no
3: hci0: Bluetooth
        Soft blocked: no
        Hard blocked: no

 これで bluetoothctl 等で BLE デバイスにアクセスできるようになりますので、 bluetoothctl から SensorTag に接続してみます。

root@obsiot:~# bluetoothctl
[NEW] Controller 98:4F:EE:04:B3:24 BlueZ 5.23 [default]
[NEW] Device CC:78:AB:7F:65:87 CC2650 SensorTag
[bluetooth]# scan on
Discovery started
[CHG] Controller 98:4F:EE:04:B3:24 Discovering: yes
[NEW] Device 62:BA:E7:35:DB:E8 62-BA-E7-35-DB-E8
[NEW] Device 34:36:3B:C7:FB:E9 34-36-3B-C7-FB-E9
[CHG] Device 62:BA:E7:35:DB:E8 RSSI: -41
[CHG] Device CC:78:AB:7F:65:87 RSSI: -46
[bluetooth]# devices
Device CC:78:AB:7F:65:87 CC2650 SensorTag
Device 62:BA:E7:35:DB:E8 62-BA-E7-35-DB-E8
Device 34:36:3B:C7:FB:E9 34-36-3B-C7-FB-E9
[bluetooth]# scan off
[bluetooth]# connect CC:78:AB:7F:65:87
Attempting to connect to CC:78:AB:7F:65:87
[CHG] Device CC:78:AB:7F:65:87 Connected: yes
Connection successful
[CHG] Device CC:78:AB:7F:65:87 UUIDs:
        00001800-0000-1000-8000-00805f9b34fb
        00001801-0000-1000-8000-00805f9b34fb
        0000180a-0000-1000-8000-00805f9b34fb
        0000180f-0000-1000-8000-00805f9b34fb
        0000ffe0-0000-1000-8000-00805f9b34fb
        f000aa00-0451-4000-b000-000000000000
        f000aa20-0451-4000-b000-000000000000
        f000aa40-0451-4000-b000-000000000000
        f000aa64-0451-4000-b000-000000000000
        f000aa70-0451-4000-b000-000000000000
        f000aa80-0451-4000-b000-000000000000
        f000ac00-0451-4000-b000-000000000000
        f000ccc0-0451-4000-b000-000000000000
        f000ffc0-0451-4000-b000-000000000000
[bluetooth]# info CC:78:AB:7F:65:87
Device CC:78:AB:7F:65:87
        Name: CC2650 SensorTag
        Alias: CC2650 SensorTag
        Paired: no
        Trusted: no
        Blocked: no
        Connected: yes
        LegacyPairing: no
        UUID: Generic Access Profile    (00001800-0000-1000-8000-00805f9b34fb)
        UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb)
        UUID: Device Information        (0000180a-0000-1000-8000-00805f9b34fb)
        UUID: Battery Service           (0000180f-0000-1000-8000-00805f9b34fb)
        UUID: Unknown                   (0000ffe0-0000-1000-8000-00805f9b34fb)
        UUID: Vendor specific           (f000aa00-0451-4000-b000-000000000000)
        UUID: Vendor specific           (f000aa20-0451-4000-b000-000000000000)
        UUID: Vendor specific           (f000aa40-0451-4000-b000-000000000000)
        UUID: Vendor specific           (f000aa64-0451-4000-b000-000000000000)
        UUID: Vendor specific           (f000aa70-0451-4000-b000-000000000000)
        UUID: Vendor specific           (f000aa80-0451-4000-b000-000000000000)
        UUID: Vendor specific           (f000ac00-0451-4000-b000-000000000000)
        UUID: Vendor specific           (f000ccc0-0451-4000-b000-000000000000)
        UUID: Vendor specific           (f000ffc0-0451-4000-b000-000000000000)
        Modalias: bluetooth:v000Dp0000d0110

 また、 Ruby の環境を構築し、 irb で dbus からアクセスしてみます。Ruby 環境の構築や dbus でのアクセス方法等はこちらをご参照ください。

Raspberry Pi + RubyでLチカ - Tech Blog by Akanuma Hiroaki

Raspberry Pi 3でD-BusからBLEデバイスにアクセスする - Tech Blog by Akanuma Hiroaki

irb(main):038:0* device.GetAll('org.bluez.Device1')
/root/sensortag_sample/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/message.rb:129: warning: constant ::Fixnum is deprecated
=> [{"Address"=>"CC:78:AB:7F:65:87", "Name"=>"CC2650 SensorTag", "Alias"=>"CC2650 SensorTag", "Paired"=>false, "Trusted"=>false, "Blocked"=>false, "LegacyPairing"=>false, "RSSI"=>-45, "Connected"=>true, "UUIDs"=>["00001800-0000-1000-8000-00805f9b34fb", "00001801-0000-1000-8000-00805f9b34fb", "0000180a-0000-1000-8000-00805f9b34fb", "0000180f-0000-1000-8000-00805f9b34fb", "0000ffe0-0000-1000-8000-00805f9b34fb", "f000aa00-0451-4000-b000-000000000000", "f000aa20-0451-4000-b000-000000000000", "f000aa40-0451-4000-b000-000000000000", "f000aa64-0451-4000-b000-000000000000", "f000aa70-0451-4000-b000-000000000000", "f000aa80-0451-4000-b000-000000000000", "f000ac00-0451-4000-b000-000000000000", "f000ccc0-0451-4000-b000-000000000000", "f000ffc0-0451-4000-b000-000000000000"], "Modalias"=>"bluetooth:v000Dp0000d0110", "Adapter"=>"/org/bluez/hci0"}]
irb(main):039:0> 
irb(main):040:0* device.subnodes
=> []

 BlueZ のバージョンが古いせいか、もしくは –enable-experimental オプション付きでビルドされていないせいか、 SensorTag 等の BLE デバイスにアクセスしても、接続はできるものの、ServicesResolved プロパティも存在せず、 GATT サービスにアクセスすることができませんでした。そこで、パッケージインストールされているデフォルトの bluez をアンインストールし、ソースからビルドしてインストールすることにします。

BlueZ バージョンアップ

 まずはインストール済みの BlueZ をアンインストールします。下記を実行すると openblocks-iot-webui パッケージもアンインストールされ、 WebUI が使えなくなりますのでご注意ください。

root@obsiot:~# sudo apt-get --purge remove bluez
Reading package lists... Done
Building dependency tree       
Reading state information... Done
The following packages were automatically installed and are no longer required:
  arping bind9-host bluez-hcidump cu curl dns-root-data dnsmasq dnsmasq-base dnsutils expect git git-man hostapd iotop isc-dhcp-server libapparmor1 libb-hooks-endofscope-perl libbind9-90 libbluetooth3 libc-ares2 libclass-load-perl
  libclass-singleton-perl libcurl3 libcurl3-gnutls libdatetime-locale-perl libdatetime-perl libdatetime-timezone-perl libdns100 libdrm-intel1 libdrm-nouveau2 libdrm-radeon1 libdrm2 libelf1 libelfg0 liberror-perl libfontenc1 libgc1c2
  libgl1-mesa-dri libgl1-mesa-glx libglapi-mesa libglib2.0-bin libglib2.0-dev libice6 libisc95 libisccc90 libisccfg90 libjansson4 libjson-c-dev libjson0 libjson0-dev liblist-allutils-perl liblist-moreutils-perl libllvm3.5
  liblockfile-bin liblockfile1 liblua5.1-0 liblua5.1-0-dev liblwres90 libmodule-implementation-perl libmodule-runtime-perl libmysqlclient18 libnamespace-clean-perl libnet1 libnetfilter-conntrack3 libnl-route-3-200 libonig2
  libpackage-stash-perl libpackage-stash-xs-perl libparams-classify-perl libparams-validate-perl libpciaccess0 libpcre3-dev libpcrecpp0 libperl4-corelibs-perl libpq5 libpython-stdlib libpython2.7-minimal libpython2.7-stdlib libqdbm14
  librtmp1 libsm6 libsqlite3-0 libssh2-1 libsub-exporter-progressive-perl libsub-identify-perl libsub-name-perl libtcl8.6 libtk8.6 libtool-bin libtry-tiny-perl libtxc-dxtn-s2tc0 libutempter0 libv8-3.14.5 libvariable-magic-perl
  libx11-xcb1 libxaw7 libxcb-dri2-0 libxcb-dri3-0 libxcb-glx0 libxcb-present0 libxcb-shape0 libxcb-sync1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxft2 libxi6 libxinerama1 libxmu6 libxmuu1 libxrandr2 libxrender1 libxshmfence1
  libxss1 libxt6 libxtst6 libxv1 libxxf86dga1 libxxf86vm1 lockfile-progs lrzsz lsof lua-json lua-lpeg lua5.1 minicom mysql-common nginx nkf nodejs nodejs-legacy ntpdate openblocks-iot-wstunnel pd-adder pd-emitter-lite pd-handler-ble
  pd-handler-conf pd-handler-plc pd-handler-uart pd-subscriber ph-nodered-packs ph-supervisor-nodered php-auth-sasl php-gettext php-http-request php-mail php-mail-mime php-net-dime php-net-smtp php-net-socket php-net-url php-pear
  php-screw php-soap php5-cgi php5-cli php5-common php5-fpm php5-json php5-readline pkg-config pppconfig python python-meld3 python-messaging python-minimal python-nose python-pkg-resources python-serial python-six python2.7
  python2.7-minimal rsync shellinabox spp-assist sqlite3 sshpass sudo supervisor tcl-expect tcl8.6 tk8.6 x11-common x11-utils xbitmaps xterm
Use 'apt-get autoremove' to remove them.
The following packages will be REMOVED:
  bluez* openblocks-iot-webui*
0 upgraded, 0 newly installed, 2 to remove and 90 not upgraded.
After this operation, 3982 kB disk space will be freed.
Do you want to continue? [Y/n] Y
(Reading database ... 45677 files and directories currently installed.)
Removing openblocks-iot-webui (2.1.1-10) ...
Purging configuration files for openblocks-iot-webui (2.1.1-10) ...
dpkg: warning: while removing openblocks-iot-webui, directory '/var/webui/docroot' not empty so not removed
dpkg: warning: while removing openblocks-iot-webui, directory '/var/webui/config' not empty so not removed
dpkg: warning: while removing openblocks-iot-webui, directory '/var/webui/sound' not empty so not removed
Removing bluez (5.23-2+b1) ...
Stopping bluetooth: /usr/sbin/bluetoothd.
Purging configuration files for bluez (5.23-2+b1) ...
Processing triggers for dbus (1.8.20-0+deb8u1) ...
Processing triggers for man-db (2.7.0.2-5) ...

 下記コマンドで BlueZ のビルドに必要なライブラリをインストールします。

root@obsiot:~# apt-get install libdbus-1-dev libdbus-glib-1-dev libglib2.0-dev libical-dev libreadline-dev libudev-dev libusb-dev

 BlueZ のソースを取得して展開します。

root@obsiot:~# wget http://www.kernel.org/pub/linux/bluetooth/bluez-5.46.tar.xz
root@obsiot:~# xz -dv bluez-5.46.tar.xz 
root@obsiot:~# tar -xf bluez-5.46.tar 
root@obsiot:~# cd bluez-5.46

 そして下記コマンドでビルドしてインストールします。

root@obsiot:~/bluez-5.46# ./configure --enable-experimental --disable-systemd
root@obsiot:~/bluez-5.46# make
root@obsiot:~/bluez-5.46# make install

 インストールが終了したら一度 BX1 を再起動し、とりあえず手動で bluetoothd を起動してみます。

root@obsiot:~# /usr/local/libexec/bluetooth/bluetoothd --experimental &
[1] 1657

 そして bluetoothctl で接続してみます。

root@obsiot:~# bluetoothctl
[NEW] Controller 98:4F:EE:04:B3:24 BlueZ 5.46 [default]
[NEW] Device CC:78:AB:7F:65:87 CC2650 SensorTag
Agent registered
[bluetooth]# connect CC:78:AB:7F:65:87
Attempting to connect to CC:78:AB:7F:65:87
[CHG] Device CC:78:AB:7F:65:87 Connected: yes
Connection successful
[NEW] Primary Service
        /org/bluez/hci0/dev_CC_78_AB_7F_65_87/service0008
        00001801-0000-1000-8000-00805f9b34fb
        Generic Attribute Profile
[NEW] Primary Service
        /org/bluez/hci0/dev_CC_78_AB_7F_65_87/service0009
        0000180a-0000-1000-8000-00805f9b34fb
        Device Information
[NEW] Characteristic
        /org/bluez/hci0/dev_CC_78_AB_7F_65_87/service0009/char000a
        00002a23-0000-1000-8000-00805f9b34fb
        System ID
〜〜〜中略〜〜〜
[NEW] Characteristic
        /org/bluez/hci0/dev_CC_78_AB_7F_65_87/service0063/char006f
        f000ffc4-0451-4000-b000-000000000000
        Vendor specific
[NEW] Descriptor
        /org/bluez/hci0/dev_CC_78_AB_7F_65_87/service0063/char006f/desc0071
        00002902-0000-1000-8000-00805f9b34fb
        Client Characteristic Configuration
[NEW] Descriptor
        /org/bluez/hci0/dev_CC_78_AB_7F_65_87/service0063/char006f/desc0072
        00002901-0000-1000-8000-00805f9b34fb
        Characteristic User Description
[CHG] Device CC:78:AB:7F:65:87 ServicesResolved: yes
[CHG] Device CC:78:AB:7F:65:87 Name: SensorTag 2.0
[CHG] Device CC:78:AB:7F:65:87 Alias: SensorTag 2.0
[CC2650 SensorTag]# 

 バージョンアップ前とは違って、サービスの内容まで取得でき、 ServicesResolved プロパティも表示され、 yes となっています。

 irb からアクセスしてみても下記のようにサービスが取得できるようになっています。

irb(main):024:0* device.GetAll('org.bluez.Device1')
/root/sensortag_sample/vendor/bundle/ruby/2.4.0/gems/ruby-dbus-0.13.0/lib/dbus/message.rb:129: warning: constant ::Fixnum is deprecated
=> [{"Address"=>"CC:78:AB:7F:65:87", "Name"=>"SensorTag 2.0", "Alias"=>"SensorTag 2.0", "Paired"=>false, "Trusted"=>false, "Blocked"=>false, "LegacyPairing"=>false, "Connected"=>true, "UUIDs"=>["00001800-0000-1000-8000-00805f9b34fb", "00001801-0000-1000-8000-00805f9b34fb", "0000180a-0000-1000-8000-00805f9b34fb", "0000180f-0000-1000-8000-00805f9b34fb", "0000ffe0-0000-1000-8000-00805f9b34fb", "f000aa00-0451-4000-b000-000000000000", "f000aa20-0451-4000-b000-000000000000", "f000aa40-0451-4000-b000-000000000000", "f000aa64-0451-4000-b000-000000000000", "f000aa70-0451-4000-b000-000000000000", "f000aa80-0451-4000-b000-000000000000", "f000ac00-0451-4000-b000-000000000000", "f000ccc0-0451-4000-b000-000000000000", "f000ffc0-0451-4000-b000-000000000000"], "Modalias"=>"bluetooth:v000Dp0000d0110", "Adapter"=>"/org/bluez/hci0", "ServicesResolved"=>true}]
irb(main):025:0> 
irb(main):026:0* device.subnodes
=> ["service0008", "service0009", "service001c", "service0022", "service002a", "service0032", "service003a", "service0042", "service004a", "service004f", "service0054", "service005b", "service0063"]
irb(main):027:0> 

Ruby スクリプトからのアクセス

 以前 Raspberry Pi からの SensorTag のデータ取得について書いた時に作成した Ruby のスクリプトを使ってデータを取得してみたいと思います。

blog.akanumahiroaki.com

 コードはこちらにも公開しています。

github.com

 sensortag.rb を実行すると、各センサーのデータをそれぞれログファイルに出力します。

root@obsiot:~/sensortag_sample# bundle exec ruby sensortag.rb 

 下記は温度と湿度のセンサーデータの例です。

root@obsiot:~/sensortag_sample# tail -f logs/humidity.log 
I, [2017-09-04T02:11:52.410172 #1787]  INFO -- : temp: 32.1875 hum: 57.23876953125
I, [2017-09-04T02:11:53.422242 #1787]  INFO -- : temp: 32.1875 hum: 57.23876953125
I, [2017-09-04T02:11:54.435289 #1787]  INFO -- : temp: 32.19757080078125 hum: 57.16552734375
I, [2017-09-04T02:11:55.447590 #1787]  INFO -- : temp: 32.19757080078125 hum: 57.16552734375
I, [2017-09-04T02:11:56.460004 #1787]  INFO -- : temp: 32.2076416015625 hum: 57.16552734375
I, [2017-09-04T02:11:57.472740 #1787]  INFO -- : temp: 32.2076416015625 hum: 57.16552734375
I, [2017-09-04T02:11:58.417763 #1787]  INFO -- : temp: 32.2076416015625 hum: 57.16552734375
I, [2017-09-04T02:11:59.430025 #1787]  INFO -- : temp: 32.2076416015625 hum: 57.16552734375
I, [2017-09-04T02:12:00.442741 #1787]  INFO -- : temp: 32.227783203125 hum: 57.16552734375
I, [2017-09-04T02:12:01.455942 #1787]  INFO -- : temp: 32.227783203125 hum: 57.16552734375
I, [2017-09-04T02:12:02.467395 #1787]  INFO -- : temp: 32.227783203125 hum: 57.16552734375

 これで Ruby のスクリプトからでも BLE デバイスのセンサーデータが取得できるようになりました。

ファクトリーリセット

 色々と試行錯誤する中で、何度か工場出荷状態に戻したいということがあり、はじめは結構手こずったので、参考までにファクトリーリセットの手順を紹介しておきます。ベースは下記製品サイトに掲載されている手順になります。

openblocks.plathome.co.jp

 この手順のままやると、ストレージ併用モードを解除して再起動した段階でネットワーク接続等の情報も消えてしまいますので、無線LAN環境で使っている場合にはその設定を行う手順が必要になってきます。

 まずは手順通りにストレージ併用モードを解除して再起動します。

root@obsiot:~# e2label /dev/mmcblk0p10 ""
root@obsiot:~# reboot

 私の環境はバージョン8 (jessie / FW 2.x系)モデルだったので、下記コマンドでファイルシステムを構築します。

root@obsiot:~# yes | mkfs -t ext4 -L DEBIAN /dev/mmcblk0p10

 構築したファイルシステムをマウントします。

root@obsiot:~# mount /dev/mmcblk0p10 /mnt

 そしてここで工場出荷用データを取得できるように、Wi-Fiの設定を行います。

root@obsiot:~# wpa_passphrase MY_AP_SSID MY_AP_PASSWORD >> /etc/wpa_supplicant/wpa_supplicant.conf
root@obsiot:~# chmod 600 /etc/wpa_supplicant/wpa_supplicant.conf

 /etc/network/interface のWi-Fiインタフェース設定を下記のように設定します。 BX1 からインターネットにアクセスできれば良いので、 DHCP でIPアドレスを取得し、 Wi-Fi アクセス設定は先ほどの設定ファイル wpa_supplicant.conf から読み込む形です。

auto wlan0
iface wlan0 inet dhcp
wpa-conf /etc/wpa_supplicant/wpa_supplicant.conf

#auto wlan0
#iface wlan0 inet static
#     address 192.168.1.17
#     network 192.168.1.0
#     netmask 255.255.255.0
#     broadcast 192.168.1.255
#     gateway 192.168.1.1

 設定を有効にするため一旦 Wi-Fi のネットワークインタフェースを停止し、再度起動します。

root@obsiot:~# ifdown wlan0
root@obsiot:~# ifup wlan0

 設定が正しいのに IP アドレスが正しく取得できていない場合は、再度停止・起動を行ってみると正しく取得できることがあります。

 ネットワーク接続が有効になったら下記コマンドで工場出荷用データを取得します。執筆時の最新バージョンは Kernel 3.10.17-101対応 Ver.2.1.1-10 でした。

root@obsiot:~# wget http://ftp.plathome.co.jp/pub/BX1/jessie/3.10.17-101/obsiot_userland_2.1.1-10_20170602.tgz

 そしてデータを展開して再起動します。

root@obsiot:~# tar xzf /root/obsiot_userland_2.1.1-10_20170602.tgz -C /mnt 2> /dev/null
root@obsiot:~# umount /mnt
root@obsiot:~# reboot

 すると工場出荷時の状態で起動し、Wi-Fiインタフェースもアクセスポイントモードで起動してきますので、 iotfamily_シリアル番号 という SSID でアクセスできるようになります。

 下記サイトも参考にさせていただきました。

dev.classmethod.jp

まとめ

 IoT ゲートウェイはそれぞれの製品によって特徴があり、採用されているOSによっても違いが出て来ます。また、Raspberry Pi などとは違った癖があるように思いますし、情報もあまり多くはないと思いますので、細かいことは色々と調べる必要があります。 BX1 のように WebUI から設定できるようになっている場合でも、サポートされていないデバイスを使おうと思うと Linux 上で動く BLE アプリを自前開発する必要がありますので、結構ハードルは高いように感じています。そして今回はとりあえず BLE デバイスのデータを取得するところまでということで、 WebUI も使えないままにしてしまっていますし、さらに最終的にはデータをどこかに送信しないと意味がないので、 その辺りも今後やってみたいと思います。

 今回はこちらも参考にさせていただきました。

openblocks.plathome.co.jp

Intel Edison から JRuby で Google Calendar のスケジュールを Amazon Polly に喋らせる

 Edison 上で Java の既存ライブラリを Ruby から使ってみたいというケースがあったので、 JRuby を試してみました。前回のスケジュールリマインダーの Amazon Polly へのアクセス部分を JRuby から AWS SDK for Java を使う形に変更してみます。

jruby.org

JRuby インストール

 まずは JRuby をインストールします。 JRuby のサイトから最新のバイナリをダウンロードして展開します。 2017/08/31 時点では Ruby2.3 互換の 9.1.12.0 が最新でした。

# wget https://s3.amazonaws.com/jruby.org/downloads/9.1.12.0/jruby-bin-9.1.12.0.tar.gz
# gunzip jruby-bin-9.1.12.0.tar.gz
# tar xf jruby-bin-9.1.12.0.tar

 展開したバイナリの bin ディレクトリにパスを通すために、 ~/.profile に下記を追記します。 bin ディレクトリのパスは実際の展開先のパスに置き換えてください。

export PATH=$PATH:/path/to/jruby-9.1.12.0/bin

 jruby コマンドにパスが通っていることを確認します。

# jruby -v
jruby 9.1.12.0 (2.3.3) 2017-06-15 33c6439 Java HotSpot(TM) Client VM 25.40-b25 on 1.8.0_40-b25 +jit [linux-i386]

bundler 使用設定

 JRuby から bundler を使うための設定をします。まずは JRuby で bundler をインストールします。 jruby -S の後にコマンドをつなげると JRuby としてのコマンド実行になります。

# jruby -S gem install bundler
Fetching: bundler-1.15.4.gem (100%)
Successfully installed bundler-1.15.4
1 gem installed

 無事にインストールされたので bundle install を実行します。

# jruby -S bundle install --path vendor/bundle

AWS SDK for Java で Amazon Polly へアクセス

 JRuby を使うための環境ができたので、 AWS SDK for Java を使って Amazon Polly へアクセスするコードを用意します。下記にコード全体(notifier_jruby.rb)を掲載します。

require 'bundler/setup'
require 'open3'
require 'digest/md5'

require 'java'
require 'aws-java-sdk-1.11.185.jar'
require 'commons-logging-1.2.jar'
require 'commons-codec-1.9.jar'
require 'httpcore-4.4.6.jar'
require 'httpclient-4.5.3.jar'
require 'jackson-core-2.9.0.jar'
require 'jackson-databind-2.9.0.jar'
require 'jackson-annotations-2.9.0.jar'

java_import 'java.lang.System'
java_import 'java.nio.file.Files'
java_import 'java.nio.file.StandardCopyOption'
java_import 'com.amazonaws.auth.BasicAWSCredentials'
java_import 'com.amazonaws.auth.AWSStaticCredentialsProvider'
java_import 'com.amazonaws.services.polly.AmazonPollyClientBuilder'
java_import 'com.amazonaws.services.polly.model.SynthesizeSpeechRequest'

class Notifier
  VOICE_DIR = '/work/schedule_reminder/voices'

  REGION            = 'us-east-1'
  ACCESS_KEY_ID     = ENV['ACCESS_KEY_ID']
  SECRET_ACCESS_KEY = ENV['SECRET_ACCESS_KEY']
  VOICE_ID_JP       = 'Mizuki'

  def initialize
    aws_credentials = BasicAWSCredentials.new(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    aws_credentials_provider = AWSStaticCredentialsProvider.new(aws_credentials)
    @client = AmazonPollyClientBuilder.standard.withCredentials(aws_credentials_provider).withRegion(REGION).build
  end

  def speech(text)
    text_hash = Digest::MD5.hexdigest(text)
    target_file = "#{VOICE_DIR}/#{text_hash}"

    synthesize(text, target_file)
    convert(target_file)
    play(target_file)
  end

  def synthesize(text, target_file)
    speech_request = SynthesizeSpeechRequest.new.withText(text).withVoiceId(VOICE_ID_JP).withOutputFormat('mp3')
    speech_result = @client.synthesizeSpeech(speech_request)
    file = java.io.File.new("#{target_file}.mp3")
    Files.copy(speech_result.getAudioStream, file.toPath, StandardCopyOption.valueOf('REPLACE_EXISTING'))
  end

  def convert(target_file)
    Open3.capture3("mpg123 -w #{target_file}.wav #{target_file}.mp3")
  end

  def play(target_file)
    Open3.capture3("aplay #{target_file}.wav")
  end
end

 まずは使用する jar ファイルをダウンロードして ./lib ディレクトリに配置し、 CLASSPATH環境変数にそのディレクトリのパスを設定した上で、読み込むために各 jar ファイルを require します。

require 'java'
require 'aws-java-sdk-1.11.185.jar'
require 'commons-logging-1.2.jar'
require 'commons-codec-1.9.jar'
require 'httpcore-4.4.6.jar'
require 'httpclient-4.5.3.jar'
require 'jackson-core-2.9.0.jar'
require 'jackson-databind-2.9.0.jar'
require 'jackson-annotations-2.9.0.jar'

 そしてJavaのクラスを import するために java_import を使用します。

java_import 'java.lang.System'
java_import 'java.nio.file.Files'
java_import 'java.nio.file.StandardCopyOption'
java_import 'com.amazonaws.auth.BasicAWSCredentials'
java_import 'com.amazonaws.auth.AWSStaticCredentialsProvider'
java_import 'com.amazonaws.services.polly.AmazonPollyClientBuilder'
java_import 'com.amazonaws.services.polly.model.SynthesizeSpeechRequest'

 初期化時に AWS SDK for Java を使って Amazon Polly のクライアントインスタンスを生成します。今回はとりあえずで BasicAWSCredentials で直接 ACCESS_KEY_IDSECRET_ACCESS_KEY を指定していますが、こちらにあるような認証情報プロバイダを使ったやり方の方が望ましいかと思います。

  def initialize
    aws_credentials = BasicAWSCredentials.new(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    aws_credentials_provider = AWSStaticCredentialsProvider.new(aws_credentials)
    @client = AmazonPollyClientBuilder.standard.withCredentials(aws_credentials_provider).withRegion(REGION).build
  end

 そして Polly へのアクセス部分も AWS SDK for Java を使う形に変更します。 Java の Polly クライアントでは直接保存先ファイルを指定できないので、 AudioStream を取り出して、 java.nio.file.Filescopy メソッドを使用してファイルに保存しています。

 また、 File クラスは Ruby の File クラスとクラス名が重複してしまうので、パッケージ名込みでクラスを指定しています。

  def synthesize(text, target_file)
    speech_request = SynthesizeSpeechRequest.new.withText(text).withVoiceId(VOICE_ID_JP).withOutputFormat('mp3')
    speech_result = @client.synthesizeSpeech(speech_request)
    file = java.io.File.new("#{target_file}.mp3")
    Files.copy(speech_result.getAudioStream, file.toPath, StandardCopyOption.valueOf('REPLACE_EXISTING'))
  end

実行

 今までの notifier.rb の代わりに notifier_jruby.rb を reminder.rb から require するように変更した上で、下記のように CLASSPATH 指定と一緒に実行します。

# CLASSPATH='./lib' jruby -S bundle exec jruby reminder.rb

 もちろん CLASSPATH は export であらかじめ設定しておいてもOKです。

# export CLASSPATH='./lib'
# jruby -S bundle exec jruby reminder.rb

まとめ

 今回はとりあえず試してみるということで、例外処理等は考慮していませんが、Rubyメインで実装しつつ、限定的にJavaのライブラリを使いたいようなケースでは手軽にJavaのクラスを使うことができるので便利そうです。ただ例外処理や依存するライブラリの読み込み、バグ発生時のデバッグのことを考えると、本番で使用するにはなかなかハードルは高そうにも思いました。

 あとは今回はパフォーマンス面は測定しませんでしたが、JVM上での実行となることで同じ Ruby のコードでもパフォーマンスは向上する可能性もあると思いますので、パフォーマンス改善のための選択肢としても可能性はありそうかなと思っています。

 今回のコードはこちらにも公開していますのでよろしければご参照ください。

github.com

Intel Edison から Google Calendar のスケジュールを Amazon Polly で喋らせる

 Intel Edison の環境を触れる機会があったので、Edison上でスケジュールリマインダーを作ってみました。

software.intel.com

やったこととしては、

  1. Google Calendar API でスケジュール情報を取得
  2. 開始10分前の予定があれば Amazon Polly で音声ファイル作成
  3. 音声ファイルを mp3 から wav に変換して再生

という感じで、構成としては簡単ですが下記の図のような形です。

f:id:akanuma-hiroaki:20170819111340p:plain

Google Calendar API の有効化

 Google の API を使用するには、まず該当の API を有効化する必要があります。

 まずは GCP のコンソールにアクセスし、プロジェクトを作成します。

f:id:akanuma-hiroaki:20170821074959p:plain:w450

 GCP のダッシュボードが表示されますので、「APIの概要」をクリックします。

f:id:akanuma-hiroaki:20170821075254p:plain:w450

 すると API のダッシュボードが表示されますが、初回はまだ有効なAPIがないので、下記のような表示になります。左メニューの「ライブラリ」をクリックして API のリスト画面に遷移します。

f:id:akanuma-hiroaki:20170821075449p:plain:w450

 G Suite APIs の中の Calendar API を選択します。

f:id:akanuma-hiroaki:20170821075549p:plain:w450

 API についての説明が表示されますので、 有効にする をクリックします。

f:id:akanuma-hiroaki:20170821075842p:plain:w450

 次に 認証情報を作成 ボタンを押して、認証情報の作成画面に遷移します。

f:id:akanuma-hiroaki:20170821075943p:plain:w450

 下記のように選択して、 必要な認証情報 ボタンをクリックします。

f:id:akanuma-hiroaki:20170821080218p:plain:w450

 クライアントIDの名前を決めて クライアントIDの作成 ボタンをクリックします。

f:id:akanuma-hiroaki:20170821080315p:plain:w450

 メールアドレスとサービス名を入力して 次へ をクリックします。

f:id:akanuma-hiroaki:20170821080417p:plain:w450

 作成された認証情報を ダウンロード をクリックして取得したら、 完了 をクリックしてひとまず GCP コンソールからの作業は終了です。

Google Calendar API でスケジュール情報取得

 では Google Calendar API でスケジュール情報を取得します。Google API は OAuth2 での認証が複雑ですが、認証処理については下記サイトを参考にそのまま実装しています。

Ruby Quickstart  |  Google Calendar API  |  Google Developers

 ただ、私の環境では実行すると下記のようなエラーが出ました。

/home/root/.local/lib/ruby/2.2.0/net/http.rb:923:in `connect': SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed (Faraday::SSLError)
        from /home/root/.local/lib/ruby/2.2.0/net/http.rb:923:in `block in connect'
        from /home/root/.local/lib/ruby/2.2.0/timeout.rb:73:in `timeout'
        from /home/root/.local/lib/ruby/2.2.0/net/http.rb:923:in `connect'
        from /home/root/.local/lib/ruby/2.2.0/net/http.rb:863:in `do_start'
        from /home/root/.local/lib/ruby/2.2.0/net/http.rb:852:in `start'
        from /home/root/.local/lib/ruby/2.2.0/net/http.rb:1375:in `request'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/faraday-0.12.2/lib/faraday/adapter/net_http.rb:80:in `perform_request'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/faraday-0.12.2/lib/faraday/adapter/net_http.rb:38:in `block in call'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/faraday-0.12.2/lib/faraday/adapter/net_http.rb:85:in `with_net_http_connection'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/faraday-0.12.2/lib/faraday/adapter/net_http.rb:33:in `call'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/faraday-0.12.2/lib/faraday/request/url_encoded.rb:15:in `call'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/faraday-0.12.2/lib/faraday/rack_builder.rb:141:in `build_response'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/faraday-0.12.2/lib/faraday/connection.rb:386:in `run_request'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/faraday-0.12.2/lib/faraday/connection.rb:186:in `post'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/signet-0.7.3/lib/signet/oauth_2/client.rb:960:in `fetch_access_token'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/signet-0.7.3/lib/signet/oauth_2/client.rb:998:in `fetch_access_token!'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/googleauth-0.5.3/lib/googleauth/signet.rb:69:in `fetch_access_token!'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/googleauth-0.5.3/lib/googleauth/user_authorizer.rb:178:in `get_credentials_from_code'
        from /home/root/work/schedule_reminder/vendor/bundle/ruby/2.2.0/gems/googleauth-0.5.3/lib/googleauth/user_authorizer.rb:200:in `get_and_store_credentials_from_code'
        from test.rb:30:in `authorize'
        from test.rb:39:in `<main>'
root@edison:~/work/schedule_reminder# 

 これはSSLアクセスの際に証明書が見つからないことによるエラーのようです。色々と調べた結果、下記サイトの情報をみつけました。

github.com

 上記サイトで紹介されているように、証明書のパスを下記のように設定することで解決しました。

cert_path = Gem.loaded_specs['google-api-client'].full_gem_path+'/lib/cacerts.pem'
ENV['SSL_CERT_FILE'] = cert_path

 あとは Calendar の API を呼んでスケジュールを取得します。イベントのリストを取得する API では開始時間の上限(いつより前に開始したか)や終了時間の下限(いつより後に終了するか)、ソート順を条件に指定できますので、下記のような条件を指定しています。

  • 開始時間上限:現在時刻から10分後
  • 終了時間下限:現在時刻
  • ソート順:開始時間の昇順

 ただこれだと今実行中の予定(開始済みでまだ終了していない予定)を除外することができないので、自前で除外処理を実装しています。

 ここまでを一つのクラスとして実装していて、コードの全体は下記のようになります。

require 'bundler/setup'

require 'google/apis/calendar_v3'
require 'googleauth'
require 'googleauth/stores/file_token_store'

require 'date'
require 'fileutils'
require 'active_support'
require 'active_support/core_ext'

class Calendar
  OOB_URI             = 'urn:ietf:wg:oauth:2.0:oob'
  APPLICATION_NAME    = 'ScheduleReminder'
  CLIENT_SECRETS_PATH = 'client_secret.json'
  CREDENTIALS_PATH    = File.join(Dir.home, '.credentials', "schedule_reminder.yaml")
  SCOPE               = Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY
  DEFAULT_TIME_RANGE  = 10.minutes

  def initialize(calendar_ids)
    cert_path = Gem.loaded_specs['google-api-client'].full_gem_path+'/lib/cacerts.pem'
    ENV['SSL_CERT_FILE'] = cert_path

    # Initialize the API
    @service = Google::Apis::CalendarV3::CalendarService.new
    @service.client_options.application_name = APPLICATION_NAME
    @service.authorization = authorize

    @calendar_ids = calendar_ids
  end

  def authorize
    FileUtils.mkdir_p(File.dirname(CREDENTIALS_PATH))

    client_id   = Google::Auth::ClientId.from_file(CLIENT_SECRETS_PATH)
    token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_PATH)
    authorizer  = Google::Auth::UserAuthorizer.new(client_id, SCOPE, token_store)
    user_id     = 'default'
    credentials = authorizer.get_credentials(user_id)

    if credentials.nil?
      url = authorizer.get_authorization_url(base_url: OOB_URI)
      puts "Open the following URL in the browser and enter the resulting code after authorization"
      puts url
      code = gets
      credentials = authorizer.get_and_store_credentials_from_code(user_id: user_id, code: code, base_url: OOB_URI)
    end

    credentials
  end

  def list_events(max_results, time_range = DEFAULT_TIME_RANGE, exclude_already_started = true)
    now = DateTime.now
    time_min = now.iso8601
    time_max = (now + time_range).iso8601
    events = []
    @calendar_ids.each do |calendar_id|
      response = @service.list_events(
                   calendar_id,
                   max_results:   max_results,
                   single_events: true,
                   order_by:      'startTime',
                   time_min:      time_min,
                   time_max:      time_max
                 )
      events.concat(response.items)
    end

    events.sort! do |event_a, event_b|
      event_a.start.date_time <=> event_b.start.date_time
    end

    return events unless exclude_already_started

    events.select do |event|
      event.start.date_time >= now
    end
  end
end

Amazon Polly で音声ファイルを作成

 Google Calendar から取得した予定のタイトルを喋る音声ファイルを生成するため、 Amazon Polly にアクセスします。まずは Edison 上で日本語を扱えるようにロケールを設定します。下記サイトを参考にそのまま実行しました。(実行結果については割愛)

qiita.com

 それでは Polly にアクセスします。今回は AWS の Ruby SDK を使っていますので、事前に ACCESS_KEY と SECRET_ACCESS_KEY を取得して環境変数に設定し、それを認証情報として使用します。

  ACCESS_KEY_ID     = ENV['ACCESS_KEY_ID']
  SECRET_ACCESS_KEY = ENV['SECRET_ACCESS_KEY']

  def initialize
    Aws.config.update({
      region:      REGION,
      credentials: Aws::Credentials.new(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    })

    @client = Aws::Polly::Client.new
  end

 あとは音声ファイル生成のAPIを呼び出せばOKです。 Polly では言語ごとに使用できる音声が決まっていて、日本語では今のところ Mizuki というIDの女性の音声のみ使用できます。また、直接 Web API を呼び出すと結果は音声ストリームとして返ってきますが、SDKからのアクセスであれば、出力先をファイルとして指定できるので、ストリームの処理を行う必要がなく手軽に使えます。

音声ファイルの変換と再生

 Amazon Polly では音声ファイルの出力形式として mp3, ogg_vorbis, pcm が指定できますが、 Edison で用意されているオーディオプレイヤーの aplay では、 wav 形式のファイルしか再生できません。そこで Polly からは mp3 として音声ファイルを出力し、それを mpg123 というツールを使って wav に変換します。

mpg123, Fast MP3 Player for Linux and UNIX systems

 mpg123 でそのまま mp3 を再生したかったのですが、再生しようとするとライブラリが不足していてエラーになってしまい、すんなり解決できなかったのでとりあえず変換してしまうことにしました。

  def convert(target_file)
    Open3.capture3("mpg123 -w #{target_file}.wav #{target_file}.mp3")
  end

 変換後の wav ファイルを aplay で実行すれば、再生することができます。

  def play(target_file)
    Open3.capture3("aplay #{target_file}.wav")
  end

 Polly からの音声ファイル取得と、wav への変換、再生までを一つのクラスとして実装しています。コードの全体は下記のようになっています。

require 'bundler/setup'
require 'aws-sdk'
require 'open3'
require 'digest/md5'

class Notifier
  VOICE_DIR = '/home/root/work/schedule_reminder/voices'

  REGION            = 'us-east-1'
  ACCESS_KEY_ID     = ENV['ACCESS_KEY_ID']
  SECRET_ACCESS_KEY = ENV['SECRET_ACCESS_KEY']
  VOICE_ID_JP       = 'Mizuki'

  def initialize
    Aws.config.update({
      region:      REGION,
      credentials: Aws::Credentials.new(ACCESS_KEY_ID, SECRET_ACCESS_KEY)
    })

    @client = Aws::Polly::Client.new
  end

  def speech(text)
    text_hash = Digest::MD5.hexdigest(text)
    target_file = "#{VOICE_DIR}/#{text_hash}"

    synthesize(text, target_file)
    convert(target_file)
    play(target_file)
  end

  def synthesize(text, target_file)
    resp = @client.synthesize_speech({
      response_target: "#{target_file}.mp3",
      output_format:   'mp3',
      voice_id:        VOICE_ID_JP,
      text:            text
    })
  end

  def convert(target_file)
    Open3.capture3("mpg123 -w #{target_file}.wav #{target_file}.mp3")
  end

  def play(target_file)
    Open3.capture3("aplay #{target_file}.wav")
  end
end

リマインダーを毎分実行

 ここまででスケジュール情報の取得と音声ファイルの生成、再生の準備はできたので、あとは一分おきにこれらを実行するようにします。取得したスケジュール情報をローカルでDBに取り込んだりすれば、再起動した時や cron での実行にも柔軟に対応できそうですが、今回はそこまでやると手間がかかってしまうので、起動したら60秒スリープで無限ループを回して、一度リマインド済みの予定はIDを記録しておいて、複数回通知されないようにしました。再起動時に直近の予定が再度リマインドされてしまうことは許容しています。

 コードの全体は下記の通りです。

require 'bundler/setup'
require 'active_support'
require 'active_support/core_ext'
require './calendar.rb'
require './notifier.rb'

class Reminder
  MAX_RESULTS        = 10
  TIME_RANGE         = 10.minutes
  SPEECH_TEMPLATE_JP = '間も無く、%sが始まる時間です。'
  CALENDAR_IDS       = ['XXXXXXXXXXXXXXXXXXXXXXXX']

  def initialize
    @calender = Calendar.new(CALENDAR_IDS)
    @notifier = Notifier.new
    @reminded_event_ids = []
    @log = Logger.new('logs/reminder.log')
    @log.debug('Initialized Reminder.')
  end

  def remind
    events = @calender.list_events(MAX_RESULTS, TIME_RANGE)

    if events.empty?
      @log.debug('No events found.')
      return
    end

    event = events.first
    start = event.start.date || event.start.date_time

    if @reminded_event_ids.include?(event.id)
      @log.debug("AlreadyReminded: #{start} - #{event.summary} id: #{event.id}")
      return
    end

    @notifier.speech(SPEECH_TEMPLATE_JP % event.summary)
    @reminded_event_ids << event.id
    @log.info("Reminded: #{start} - #{event.summary} id: #{event.id}")
  end
end

if $0 == __FILE__
  reminder = Reminder.new
  loop do
    reminder.remind
    sleep(1.minute)
  end
end

 これをバックグラウンド実行しておけば、次の予定の10分前になると音声での通知が行われ、下記のようにログに出力されます。

D, [2017-08-20T22:25:50.281798 #695] DEBUG -- : Initialized Reminder.
D, [2017-08-20T22:25:51.838564 #695] DEBUG -- : No events found.
D, [2017-08-20T22:26:52.503386 #695] DEBUG -- : No events found.
I, [2017-08-20T22:27:59.064125 #695]  INFO -- : Reminded: 2017-08-21T07:30:00+09:00 - 開発部定例ミーティング id: 25rq8i063ao99thjissss8i211
D, [2017-08-20T22:28:59.827868 #695] DEBUG -- : AlreadyReminded: 2017-08-21T07:30:00+09:00 - 開発部定例ミーティング id: 25rq8i063ao99thjissss8i211
D, [2017-08-20T22:30:00.609354 #695] DEBUG -- : AlreadyReminded: 2017-08-21T07:30:00+09:00 - 開発部定例ミーティング id: 25rq8i063ao99thjissss8i211

まとめ

 Intel Edison については先日残念ながら販売が終了になることが明らかになりました。

hackaday.com

 Edison を利用しているデバイスは多そうなので、影響も大きそうですが、まだしばらくは Edison を触る機会もあるのではないかと思います。

 また、 Google Calendar API や Amazon Polly API は認証部分さえクリアしてしまえば実際の機能を利用する API は手軽に使えるので、色々なことに活用できそうな気がしています。特に Polly については Lex との組み合わせも色々とできそうなので、また遊んでみたいと思います。

 今回のコードは下記リポジトリに公開してありますので、よろしければご参照ください。

github.com