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

OpenBlocks IoT BX1 で SensorTag のデータを AWS IoT に送信する

 ぷらっとホームの IoT ゲートウェイ OpenBlocks IoT BX1 を試す機会があったので、SensorTag のデータを BX1 から AWS IoT に送信する処理を試してみました。

f:id:akanuma-hiroaki:20170812204308j:plain:w300

openblocks.plathome.co.jp

初期設定

 まずは BX1 の初期設定を行います。付属のUSBケーブルで BX1 と MacBook Pro を接続すると BX1 が起動します。

f:id:akanuma-hiroaki:20170812204611j:plain:w300

 初期状態ではAPとして起動するので、 iotfamily_シリアル番号 となっているAPに初期パスワードで接続します。

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

 APに接続したらブラウザから https://192.168.254.254:4430 にアクセスすると初回は使用許諾画面や管理者アカウント設定画面が表示されるので、許諾への同意やアカウント設定を行います。

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

 次にネットワークの設定です。BX1 はSimを挿して3G回線に接続することもできますが、まずは自宅のLANに接続してみます。LANに接続する場合には使用モードを「クライアントモード」に設定し、接続するAPの情報や、IPアドレス等を設定します。

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

 ネットワーク設定を変更すると BX1 の再起動が求められますので、「メンテナンス」メニューから再起動を行います。再起動後は先ほど設定したIPアドレスでアクセスできるようになりますので、MacBookの接続先APをいつもの自宅のLANのAPに戻し、 https://192.168.10.100:4430 でアクセスします。

 そして時刻の設定を行えば最低限の設定は完了です。

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

BLEデバイスの接続設定

 BX1 では IoT デバイスとして Bluetooth インタフェースを標準でサポートしており、 SensorTag はサポート対象デバイスにも含まれています。

OpenBlocks IoT BX1 対応センサー/デバイス
http://openblocks.plathome.co.jp/products/obs_iot/bx1/sensor_dev/index.html

 まずは BT サービスを起動します。「サービス」メニューから BT の使用設定を「使用する」に変更して保存します。

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

 「BT関連」等のタブが追加されますので、「BT関連」タブを選択し、 SensorTag の Advertising を開始してから「BLEデバイス検出」の 検出 ボタンをクリックします。すると SensorTag が検出されますので、「使用設定」にチェックを入れて保存します。

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

 すると一覧に SensorTag が追加されます。   f:id:akanuma-hiroaki:20170812212651p:plain

 「BT編集」タブでは各デバイスのメモの編集等が行えますので、わかりやすいようにメモを登録しておきます。

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

データ収集設定

 BX1 はデータ収集機能を持っていて、 Web UI から設定することが可能です。収集したデータをクラウドに送信することもできますが、まずはBLEデバイスからのデータ収集ができるところまでを確認してみます。

 「サービス」メニューの「基本」タブから、データ収集と PD Handler BLE を「使用する」に設定して保存します。

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

 すると「収集設定」タブが追加されます。各種クラウドサービスへの送信の設定が行えますが、まずは BX1 で SensorTag の情報が取得できているかを確認するため、「本体内(local)」のみ「使用する」に設定します。また、画面下部のデバイス情報送信設定にて、 SensorTag の送信対象を「送信する」に設定し、送信先設定で「local」のみにチェックを入れて保存します。

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

 正しく設定されていれば SensorTag からの値の取得が始まり、「収集ログ」タブで pd-handler-stdout.log を確認すると、センサーデータが収集されていることが確認できます。

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

 また、「データ表示」タブでは、収集しているデータをグラフやテーブル表示で確認することができます。

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

3G回線への接続設定

 ここまでで SensorTag のデータが収集できていることは確認できたので、AWS IoT へのデータ送信を行いたいと思いますが、以前の記事同様に SORACOM Beam 経由で送信してみたいと思いますので、まずは3G回線への接続設定を行いたいと思います。

 一旦 BX1 をシャットダウンして SORACOM Air Sim を挿した後で起動し、再度管理画面にアクセスします。

 3G回線に接続する場合は Wireless LAN の設定をAPモードにする必要がありますので、SSIDやパスフレーズ等を設定し直します。

 モバイル回線設定では SORACOM Air の APN やユーザ名、パスワード等を設定して保存し、再起動します。

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

 再起動後はAPモードでの起動になりますので、MacBook から先ほど設定したAPへ接続した後、 https://192.168.254.254:4430 で管理画面にアクセスします。ネットワークメニューの「状態」タブを見ると、3G回線への接続が確認できます。

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

データ送信設定

 それでは SensorTag のデータを AWS IoT へ Publish するように設定します。 BX1 では AWS IoT 用の送信設定も可能ですが、今回は SORACOM Beam 経由で送信するため、 MQTT の送信設定を使用します。 サービスメニューの「収集設定」タブから、MQTTサーバを「使用する」に設定します。送信先ホストやポートは SORACOM Beam の情報を設定します。トピックプレフィックスには任意の文字列を設定します。

 また、デバイス情報送信設定の方でも、送信先設定で「MQTT」にチェックを入れます。ここで表示される ユニークID と、先ほどの トピックプレフィックス/ で繋いだものが、 Publish 先のトピックとして使用されます。

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

 ここまでの設定を保存すると、 SensorTag の情報が MQTT サーバに Publish されるようになります。AWS IoT のコンソールから該当のトピックに Subscribe してみると、下記のようにセンサーデータが Publish されていることが確認できます。

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

まとめ

 SensorTag は BX1 でサポートされているということもあり、コードを書くことなく手軽にセンサーデータをクラウドに送信することができました。今回は SORACOM Beam 経由にするために MQTTサーバへの送信にしましたが、直接 AWS IoT に送信するのであれば、送信設定で Shadow のフォーマットにも対応できるようです。また、非対応デバイスでも収集用のアプリを実装すればデータ収集ができるようですので、今後試してみたいと思います。

SensorTag のデータを Amazon Polly で読み上げる

 前回の記事では SensorTag で取得した値を AWS IoT に送信して、照度の値によって LED を点灯したり、SNSからメールを送信したりしてみましたが、今回はさらに Polly で照度の値を読み上げる音声ファイルを生成し、Raspberry Pi で再生する処理を追加してみたいと思います。

全体構成

 全体の構成は下記のようになります。

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

 前回と違うのは図の右下の赤線で囲んだ部分で、Lambda、Polly、s3との連携が追加になっています。

 処理の流れとしては、 AWS IoT Rule で SNS によるメール送信を行なっていた部分の Action に Lambda の Function 呼び出しを追加し、その Function の中から Polly のAPIを呼び出して照度の値を読み上げる mp3 ファイルを生成して s3 に格納し、 presigned URL を生成して AWS IoT に Publish します。Raspberry Pi 側では新たに音声ファイルの presigned URL が Publish されるトピックにも Subscribe しておき、 presigned URL を受け取った場合はそこから mp3 をダウンロードして再生します。

Raspberry Pi での音声ファイルの再生

 まずは下記ページを参考に Raspberry Pi 上で音声ファイルの再生を試してみます。

qiita.com

 Raspberry Pi のイヤホンジャックにヘッドフォンを挿し、下記コマンドを実行してみると、すんなり再生されました。

pi@raspberrypi:~ $ aplay /usr/share/sounds/alsa/Front_Center.wav
Playing WAVE '/usr/share/sounds/alsa/Front_Center.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono

 下記のようなテスト用のコマンドも用意されているようです。

pi@raspberrypi:~ $ speaker-test -t sine -f 1000

speaker-test 1.0.28

Playback device is default
Stream parameters are 48000Hz, S16_LE, 1 channels
Sine wave rate is 1000.0000Hz
Rate set to 48000Hz (requested 48000Hz)
Buffer size range from 512 to 65536
Period size range from 512 to 65536
Using max buffer size 65536
Periods = 4
was set period_size = 16384
was set buffer_size = 65536
 0 - Front Left
Time per period = 1.409599
 0 - Front Left
Time per period = 2.563393

 また、amixer というコマンドで現在のデバイスの設定の確認や変更ができます。

pi@raspberrypi:~ $ amixer info
Card default 'ALSA'/'bcm2835 ALSA'
  Mixer name    : 'Broadcom Mixer'
  Components    : ''
  Controls      : 6
  Simple ctrls  : 1
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ amixer scontrols
Simple mixer control 'PCM',0
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ amixer scontents
Simple mixer control 'PCM',0
  Capabilities: pvolume pvolume-joined pswitch pswitch-joined
  Playback channels: Mono
  Limits: Playback -10239 - 400
  Mono: Playback -2000 [77%] [-20.00dB] [on]
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ amixer controls
numid=3,iface=MIXER,name='PCM Playback Route'
numid=2,iface=MIXER,name='PCM Playback Switch'
numid=1,iface=MIXER,name='PCM Playback Volume'
numid=5,iface=PCM,name='IEC958 Playback Con Mask'
numid=4,iface=PCM,name='IEC958 Playback Default'

 下記のようにハードウェアの情報を確認することもできます。

pi@raspberrypi:~ $ cat /proc/asound/modules 
 0 snd_bcm2835
pi@raspberrypi:~ $ 
pi@raspberrypi:~ $ cat /proc/asound/cards
 0 [ALSA           ]: bcm2835 - bcm2835 ALSA
                      bcm2835 ALSA
pi@raspberrypi:~ $ 

 aplay は mp3 ファイルは再生できないので、 mpg321 を使ってみます。下記コマンドでインストールします。

pi@raspberrypi:~ $ sudo apt-get install mpg321

 下記のように mp3 ファイルを指定することで再生することができます。

pi@raspberrypi:~/sounds $ mpg321 mondo_01.mp3

Polly と連携するための Lambda Function

 まずは Lambda Function の全文を掲載しておきます。

import json
import boto3
from boto3 import Session
from boto3 import resource
from contextlib import closing

REGION         = 'ap-northeast-1'
POLLY_REGION   = 'us-east-1'
BUCKET_NAME    = 'hiroaki.akanuma.iot'
FILE_NAME_BASE = 'voices/%s_lux.mp3'
SPEECH_BASE    = '現在の照度は、%sルクスです。'
AWS_IOT_TOPIC  = '/iot/sensor_tag/voices'

session = Session(region_name = POLLY_REGION)
polly   = session.client('polly')
s3      = resource('s3')
bucket  = s3.Bucket(BUCKET_NAME)
iot     = boto3.client('iot-data')

def synthesize_speech(lux):
    speech_text = SPEECH_BASE % lux

    response = polly.synthesize_speech(
        Text         = speech_text,
        OutputFormat = 'mp3',
        VoiceId      = 'Mizuki'
    )
    
    return response['AudioStream']

def put_to_s3(audio_stream, file_name):
    with closing(audio_stream) as stream:
        bucket.put_object(
            Key         = file_name,
            Body        = stream.read(),
            ContentType = 'audio/mpeg'
        )

def generate_presigned_url(file_name):
    return boto3.client('s3').generate_presigned_url(
        ClientMethod = 'get_object',
        Params       = {'Bucket' : BUCKET_NAME, 'Key' : file_name},
        ExpiresIn    = 3600,
        HttpMethod   = 'GET'
    )

def publish_to_iot(speech_url):
    iot.publish(
        topic = AWS_IOT_TOPIC,
        qos   = 0,
        payload = json.dumps({'speech_url': speech_url})
    )

def lambda_handler(event, context):
    lux = event['lux']
    
    audio_stream = synthesize_speech(lux)

    file_name = FILE_NAME_BASE % lux

    put_to_s3(audio_stream, file_name)

    presigned_url = generate_presigned_url(file_name)

    publish_to_iot(presigned_url)

    return presigned_url

 event に照度の値が入ってくるのでまずはそれを取り出し、読み上げ用のテキストを作成したらそれを Polly の synthesize_speech メソッドに渡して、結果を AudioStream として取得します。出力フォーマットには mp3 を指定し、日本語での読み上げなので VoiceId には Mizuki を指定しています。レスポンスの中にはメタデータ等も含んでいますが、今回は使用しなかったので、 AudioStream のみを取り出しています。ちなみに Polly はまだ東京リージョンでは使用できないので、 Polly のクライアント取得時のリージョンにバージニアを指定しています。

 次に AudioStream から音声データを取得し、それを s3 に格納します。ファイル名は読み上げている照度の値を使って、それぞれの値ごとの音声ファイルを作成します。

 そして Raspberry Pi から認証なしで s3 上の音声ファイルにアクセスできるように、 presigned URL を発行して、その URL を音声ファイルの URL のやりとり用の AWS IoT トピックに Publish しています。AWS IoT Things の Shadow 更新用とは別のトピックになります。

 この Lambda Function を Rule の Action として追加します。

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

Raspberry Pi 側での読み上げ

 Raspberry Pi 側では音声ファイルのURLのやりとり用のトピックに Subscribe して、音声ファイルを読み上げる処理を追加します。

SPEECH_TOPIC = '/iot/sensor_tag/voices'
SOUNDS_DIR = '/home/pi/sounds/voices'

def run_speech_thread(log)
  log.info("Running speech thread.")
  Thread.new do
    begin
      MQTT::Client.connect(host: BEAM_URL) do |client|
        client.subscribe(SPEECH_TOPIC)
        log.info("Subscribed to the topic: #{SPEECH_TOPIC}")

        client.get do |topic, json|
          speech_url = JSON.parse(json)['speech_url']
          speech_uri = URI.parse(speech_url)
          speech_file = speech_uri.path.split('/').last
          speech_file_path = "#{SOUNDS_DIR}/#{speech_file}"

          unless File.exist?(speech_file_path)
            log.info("Opening URL: #{speech_url}")
            open(speech_url) do |file|
              open(speech_file_path, 'w+b') do |out|
                out.write(file.read)
              end
            end
          end

          log.info("Speaking: #{speech_file_path}")
          Open3.capture3("mpg321 #{speech_file_path}")
        end
      end
    rescue => e
      log.error(e.backtrace.join("\n"))
    end
  end
end

 MQTT でトピックに Subscribe して client.get するとそこで待ち受けるようになるため、とりあえず今回は今までの Shadow 更新用の処理とは別スレッドで音声ファイル用トピックに Subscribe して処理を行うようにしました。トピックからメッセージを受信したら、音声ファイルのURLをパースして、同名のファイルがローカルにない場合は s3 からダウンロードします。そして mpg321 コマンドを実行して音声ファイルを再生しています。

動作確認

 Lambda Function の保存や Rule での Action の追加を行ったら、Subscribeします。

$ sudo bundle exec ruby subscribe.rb

 そして SensorTag のデータの Publish を開始します。

$ sudo bundle exec ruby publish.rb

 すると、照度の値によって照明の ON/OFF が切り替わるタイミングで、音声ファイルも再生され、照度が読み上げられることになります。

まとめ

 Polly を使うことで手軽に音声ファイルを作成することができ、生成されたファイルをローカルに保存して繰り返し使うこともできますので、上手く使えばリーズナブルに音声を使ったサービスを作ることができます。今回は AWS SDK の認証情報をデバイス上におきたくなかったので、 SORACOM Beam 経由の AWS IoT の Rule から使うやり方をとりましたが、 AWS SDK から直接 Polly を使えばもっと直接的に手軽に音声ファイルを利用できると思います。

 今回のコードは下記リポジトリにも公開しています。

github.com

 ちなみに今回は下記ページを参考にさせていただきました。

qiita.com