masato-ka's diary

日々思ったこととか、やったことの備忘録。

1000円から始めるBLEデバイス開発、RN4020をPCから制御して使ってみる。

この記事について

 この記事では1000円代で購入出来るBLEモジュールRN4020の使い方を検証してみました。まずはシリアル通信で制御しペリフェラルとして動作させます。さらにセントラルと値をやり取りする方法についても紹介しています。

RN4020とは

RN4020はMicrochip社が製造しているBLEモジュールです。価格は1000円程度で、秋月電子通商などで購入できます。

f:id:masato-ka:20170925200254j:plain

akizukidenshi.com

また、使いやすくしたブレークアウトボード版も販売されています。(製造も秋月電子通商)今回はこちらのブレークアウトボードを利用しています。

akizukidenshi.com

RN4020はUARTインタフェースが搭載されており、シリアル通信からコマンドを入力することで、簡単にBLEデバイス(ペリフェラル動作)を実現することができます。Arduinoなどのマイコンボードなどに接続しBLEデバイスを作成することができます。また、セントラルとしても動作させることができるため、汎用的なBLEの通信モジュールとして利用することが可能です。さらにUARTインタフェースだけでなく、アナログI/O, デジタルI/Oを兼ね備えており、LEDやセンサを接続して利用することも可能です。  RN4020は専用のスクリプトを書き込むことで、BLEとI/Oを制御し、RN4020のみでBLEデバイスを構成することができます。詳細な仕様については以下のMicrosoftのデータシートを参考にしてください。また、シリアルコマンドはスクリプトのリファレンスはユーザガイドに記載されています。

  • データシート

http://ww1.microchip.com/downloads/jp/DeviceDoc/50002279A_JP.pdf

  • ユーザガイド

http://akizukidenshi.com/download/ds/microchip/70005191A_JP.pdf

RN4020をシリアル通信で制御する

 まずはじめにRN4020をUARTで制御します。今回はUSBシリアル変換を使い、PC上から制御します。マイコンなどを接続する場合も基本的に同じコマンドとなります。

接続回路

 RN4020ですが、前述のブレークアウトボードを利用する場合、シルクパターンに従って、GND, +5V, RX, TXを接続するのみです。ブレークアウトボードの場合電源が+5Vである点に注意してください。今回はUSBシリアル変換は秋月電子通商から発売されている以下のモジュールを利用しました。

akizukidenshi.com

実は上記ブレークアウトボードとこのUSBシリアル変換機のピンアサインが向かい合わせでぴったりとはまるようになっています。写真のように向かい合わせでブレットボード上に刺すとGND、電源、がお互い接続され、RXとTXはクロスに接続されるようになっています。

f:id:masato-ka:20170925200517j:plain

ですが、実際の配線は製造ロットによって変わる場合もあるので、予め確認して接続するようにしてください。接続はそれぞれのモジュールが以下の表のように対応していれば大丈夫です。お互いのRXとTXをクロスに接続することろがポイントです。

USBシリアル変換 RN4020
GND GND
+5V +5V
RX TX
TX RX

PCでの接続

 回路の接続ができたら、USBシリアル変換機とPCをUSBケーブルで接続します。WindowsであればTeraTermなどのターミナルソフトから以下のパラメータでシリアル接続します。

項目
ボーレート 115200
データビット 8bit
パリティ なし
ストップビット 1bit
フロー制御 なし
改行文字 CR

OSXの場合は以下のコマンドで接続します。

$screen /dev/tty.usbserial-A1032M5W 115200 

/dev/tty.usbserial-A1032M5Wは接続するUSBシリアル変換機ごとに変わるので予め/dev/フォルダ以下を見てそれっぽいものを確認します。

ペリフェラルとしてBLEのHeartRateサービスをアドバタイズしてみる。

ターミナルの接続が完了したら以下のようにコマンドを1行ずつ実行します。RN4020がHeartRateサービスを持ったペリフェラルとしてアドバタイズを開始するはずです。 先頭に「-」が付いている行はRN4020からの応答行です。実際には「-」は表示されていません。

+//(1)
- Echo On
SF,1//(2)
- AOK
SS,A0000000//(3)
- AOK
SR,00000000//(4)
- AOK
R,1//(5)
- Reboot
- CMD
A//(6)
  • (1) EchoをOnにしてターミナルに入力した値がエコーバックで表示されるようにする。
  • (2) デバイスの初期化を行う(デバイスの基本情報はそのまま)
  • (3) HeartRateとDeviceInformationのサービスを設定
  • (4) デバイスモードをペリフェラルに設定
  • (5) ハードウェアを再起動して設定を再読み込み
  • (6) アドバタイズ開始

 実際にBLEデバイスをLightblueなどのBLEデバイスのユーティリティアプリで見るとRN4020が見えるようになっています。  引き続き、RN4020がどういう状態になっているのか詳細を見てみましょう。まずはシリアルコンソールからLSコマンドを入力して、現在設定されているサービスとキャラクタリスクを確認してみます。そうすると以下のように返答が帰ってきます。

LS
180A
         2A25,000B,V
         2A27,000D,V
         2A26,000F,V
         2A28,0011,V
         2A29,0013,V
         2A24,0015,V
180D
         2A37,0018,V
         2A37,0019,C
         2A38,001B,V
         2A39,001D,V
END

 先頭の180Aは承認サービスとして予め定義されているDeviceInformation (180A)です。そこから一段下がってDeviceInformationのキャラクタリスティックが表示されています。また180DはHeartRateサービスとなっています。このようにSSコマンドを利用することで、承認サービスを簡単に設定することができます。

 続いてDコマンドを入力し、デバイスの詳細情報を出力します。

D
- BTA=001EC04633AB
- Name=RN33AB
- Connected=no
- Bonded=no
- Server Service=A0000000
- Features=00000000
- TxPower=4

これらの情報も別のコマンドから書き換えることができ、デバイス名やBLEとしての役割の変更(セントラル、ペリフェラルなど)を変更することが可能です。詳細はユーザマニュアルを確認してください。

キャラクタリスティクへの値の書き込み

キャラクタリスティックに値を書き込んで、接続しているセントラルにデータを送信します。今回はセントラル側としてOSX上でLightblueを使います。まず今の状態のまま、LightblueからRN4020にアクセスすると、先ほどLSコマンドで見た通りのサービスとキャラクタリスティックを確認できます。キャラクタリスティックの値はまた表示されていません。

f:id:masato-ka:20170925205434p:plain

以下のコマンドでキャラクタリスティックに値を書き込みます。2A37は心拍数の値を返すコマンドのようです。予め、Lightblue上でサブスクライブしておきます。

SUW,2A37,50

上記のコマンドはキャラクタリスティック2A37に対して0x50(80)を設定する値となります。 またはハンドラに対する書き込みだと以下のようになり結果は同じになります。

SHW,0018,50

Lightblueを見ると値が書き込まれているのがわかります。このようにキャラクタリスティックに値を書き込むとサブスクライブしているセントラルに値を通知し、またReadが許可されている場合はReadしたセントラルに値を送信します。

f:id:masato-ka:20170925205443p:plain

キャラクタリスティクからの値の読み取り

続いて、セントラルから書き込まれた値を読み出してみます。同じくHeartRateサービスの2A39が書き込み可能なキャラクタリスティックになっています。lightblueから値を書き込むと以下のようにコンソールに表示されます。

-WV,001D,50.

これは001Dのハンドラ(UUIDじゃないことに注意)に0x50が書き込まれたという表示です。

ユーザ定義のBLEサービスとキャラクタリスティックを設定する。

RN4020はDeviceInforamtionやHeartRateサービスのように承認済みサービスだけでなく、ユーザが独自に定義したサービスとキャラクタリスティックを設定することができます。以下のコマンドを実行することでユーザ定義のサービスとキャラクタリスティックを設定できます。

+
- Echo On
SF,1
- AOK
SS,00000001
- AOK
PZ //(1)
- AOK
PS,11223344556677889900AABBCCDDEEFF //(2)
- AOK
PC,010203040506070809000A0B0C0D0E0F,02,05//(3)
- AOK
R,1
- Reboot
- CMD
  • (1) ユーザ定義のサービスとキャラクタリスティックを削除
  • (2) 指定のUUIDでサービスを設定
  • (3) 指定のUUIDでキャラクタリスィックをRead Only, 5byte長として設定する。

PCコマンドでキャラクタリスティックの設定をしますが、キャラクタリスティックに許可する操作(Read, Write, Notifyなど)を2つ目の引数で設定することができます。ユーザマニュアルを読むと細かく設定できるようです。よく使いそうな値は以下の表の通りかと思います。

設定名
Notify 0x10
Write(応答あり) 0x08
Write(応答なし) 0x04
Read 0x02

例えばReadとNotifyを設定する場合は上記の例だと以下のようになります。

PC,010203040506070809000A0B0C0D0E0F,12,05

キャラクタリスティックの値の読み書きについては先ほどと同じ方法で行えます。

まとめ

 RN4020を利用することで非常に簡単にBLEのペリフェラルバイスを作成できます。Arduinoなどを使って簡単な工作ができればRN4020を接続するだけでBLE機器として利用できそうです。今回はシリアル通信からの制御を中心に紹介しました。RN4020をマイコン代わりに使い、センサやLEDを直接接続して動かすこともできます。また簡単なスクリプトを利用し、他のマイコンなどと接続せずに単独のデバイスとして動かすこともできます。これらの利用方法についてもまとめ次第紹介したいと思います。

WebブラウザからBLE接続 WEB Bluetooth APIでNotificationを受け取る方法

この記事について

 この記事ではWebBluetooth API を使い、BLEデバイスからNotificationを受け取る方法を説明します。 対象とするデバイス2JCIE-BL01を利用します。デバイスから一定間隔で環境データを取得します。

WEB Bluetooth API

 WEB Bluetooth APIはWEBブラウザ上でBLEデバイスと通信を実現するJavaScriptAPIです。実装は各ブラウザごとに 違いますが2017年9月現在、各ブラウザともそんなに違いはないそうです。iOSについては今の所非対応のようですが、OSXでは問題なく使えそうです。

WEB Bluetooth APIの実装

WEB Bluetooth API を使いデバイスからNotificationを受ける実装は非常に簡単です。以下のコードで実現できます。onStartNotifyをブラウザのUIから呼び出すことで、実行することが可能です。今回ブラウザはChromeを利用しました。

var SensorServiceUUID = "0c4c3000-7700-46f4-aa96-d5e974e32a54" // WxBeacon2のSensorService UUID
var LatestDataUUID = "0c4c3001-7700-46f4-aa96-d5e974e32a54"// 最新データ取得のためのキャラクタリスティック UUID

function onStartNotify() {

    navigator.bluetooth.requestDevice(
        { acceptAllDevices:true,optionalServices:[SensorServiceUUID] } // (1)
     ) 
        .then(device => device.gatt.connect())//(2)
        .then(server => server.getPrimaryService(SensorServiceUUID))
        .then(service => service.getCharacteristic(LatestDataUUID))
        .then(characteristic =>{
            characteristic.addEventListener('characteristicvaluechanged', onRecvSensorData); //(3)
            characteristic.startNotifications();//(4)
        })

}

  • コード解説

(1) requestDeviceでBLEデバイスの検索を開始します。引数には探索するデバイスのフィルタを指定できます。今回はデバイスの指定は行わず、すべてのデバイスを受信します。また、デフォルトでは任意のサービスに接続できないため、予め接続できるサービスのUUIDをoptionalServicesで指定しておきます。

このメソッドは実行されると以下のようなダイアログがWeb ブラウザ上に表示されます。接続したいデバイスを選択し、ペア設定を押します。

f:id:masato-ka:20170924144104p:plain

(2) (1)で指定した デバイスに対して接続処理を行います。接続後、サービスの検索、キャラクタリスティックの検索と処理が 続いていきます。

(3) キャラクタリスティックを取得後、データがNotifyされた際に呼ばれるイベントリスナcharacteristicvaluechangedにコールバックを指定します。BLEデバイスからデータが通知されたらonRecvSensorDataメソッドが呼ばれるようになります。

(4) startNotifications()メソッドでデバイスからの通知が開始されます。

WxBeacon2のデータの処理を行うonRecvSensorDataは以下のようになっています。

function onRecvSensorData(event) {//(1)

    let characteristic = event.target; //(2)
    let value = characteristic.value; //(3)

    let temp = decodeValue(value.getUint8(2), value.getUint8(1), 0.01);
    console.log(temp);
    let humid = decodeValue(value.getUint8(4),value.getUint8(3), 0.01);
    console.log(humid);
    let lumix = decodeValue(value.getUint8(6), value.getUint8(5));
    console.log(lumix);
    let uv = decodeValue(value.getUint8(8), value.getUint8(7), 0.01);
    console.log(uv);
    let atom = decodeValue(value.getUint8(10), value.getUint8(9), 0.1);
    console.log(atom);
    let noise = decodeValue(value.getUint8(12), value.getUint8(11), 0.01);
    console.log(noise);
    let disco = decodeValue(value.getUint8(14), value.getUint8(13), 0.01);
    console.log(disco);
    let heat = decodeValue(value.getUint8(16), value.getUint8(15), 0.01);
    console.log(heat);
    let battery = decodeValue(value.getUint8(18), value.getUint8(17), 0.001);
    console.log(battery);

}

function decodeValue(msb, lsb, gain){
        return ((msb << 8) + lsb) * gain
}

  • コード解説

(1) コールバックはデバイスからの通知結果を受け取るeventと言う引数を1つとります。 (2) eventからキャラクタリスティックのオブジェクトを取得します。 (3) キャラクタリスティックのvalueの値を取得します。

最終的に取得されるキャラクタリスティックの値はDataViewオブジェクトになっています。getUint8メソッドを利用することで1バイトづつデータを取得することができます。 WxBeacon2の設定を変更していなければ5分間隔でデータが出力されます。今回はコンソールログに出力しているため、開発者ツールなどから確認します。

まとめ

WEB Bluetooth APIは非常に簡単にデバイスへの接続とキャラクタリスティックに対するデータの読み書きができます。今回はNotificationの受け取りのみですが、もちろん通常のreadやwriteも可能です。プラットフォームに依存なく簡単にBLEデバイスにアクセスできます。また、WEBブラウザから実行できるという性格上、検索できるデバイスやデバイスのサービスをホワイトリスト形式で縛る、実行にはかならずユーザアクションが必要など、セキュリティ面にも気を使った仕様になっていると思いました。実際にWEBアプリケーションと組み合わせた使い方をする場合はセキュリティ含めてアプリケーションの実装上工夫が必要そうな気はしますが。  また今回Beaconの情報を取得できないか挑戦しました。WEB Bluetooth API 自体の仕様上、可能なようですが、必要な実装はChrome含めて未対応のようです。この辺りについては今後に期待でしょうか。 WEB USB API 含めてWEB ブラウザから直接ハードウェアにアクセスできるのは未来を感じます。

JavaでもBLEでIoT〜RaspberryPi Zero WからBLEデバイスにアクセスしてみる。

この記事について

 この記事ではRaspberry Pi Zero W上でJavaからBLEデバイスにアクセスする方法を記載しています。Raspberry PiからBLEインタフェースを操作する方法はNode.jsやPythonを使うやり方が多く見られます。これらのライブラリはLinuxのBlutoothスタックであるBluezのライブラリリンクして実行するか、D-BUSインタフェース経由でBuezを呼び出すことで実現されています。そのため、Bluezのインタフェースにアクセスできれば、プログラミング言語に依存せずにBLEにアクセスすることができます。  しかし、いずれの方法も一から実装すると非常に時間がかかるためすでにあるライブラリを利用する方法が良いでしょう。今回はJavaからBluezを呼び出すライブラリを調査し、利用方法をまとめましたので紹介します。サンプルコードは以下のリポジトリに置いています。Raspberry PiやRaspbianに限らずLinux上であれば動かせると思います。

github.com

JavaのBLEライブラリ

 今回調べた中で、JavaからBluezにアクセスしライブラリを呼び出すためのライブラリは以下の2つがありました。どちらもBluezをD-BUSインタフェース経由で動かしており、ライセンスはMITライセンスとなっています。この2つのライブラリの違いですが、tinybはintel-iot-devkitプロジェクトの中にあるようです。そのため、比較的作りがしっかりしていそうです。その反面、ライブラリを使うまでのコストが高く、ローカルで関連ライブラリをビルドしなければなりません。一方bluez-dbusの方は個人開発のライブラリのようです。tinybにインスパイアされたプロジェクトということですが、Mavenのセントラルリポジトリに登録されています。Maven プロジェクトからダウンロードして利用できるのが非常に使いやすく感じました。また必要なライブラリもapt-get でインストールできるため、今回はこのbluez-dbusを利用してみます。

ライブラリ名 備考
tiny C++ライブラリも同梱
bluez-dbus Maven central リポジトリに登録

Raspberry Pi Zero Wの環境構築

OSはRaspbian STRECHを利用しました。このバージョンからデフォルトインストールのBluezが5.43となります。bluez-dbusはBluez 5.43対応のため、Raspbian JESSEIなどでうまくいかない場合はBluezのバージョンを揃えることをお勧めします。

$uname -a
Linux raspberrypi 4.9.41+ #1023 Tue Aug 8 15:47:12 BST 2017 armv6l GNU/Linux

始める前にapt-getのアップデートを実施しておきます。

$sudo apt-get update & apt-get upgrade

libunixsocket-javaのインストール

bluez-dbusが唯一依存しているネイティブライブラリです。d-busのインタフェースライブラリからリンクされています。

$sudo apt-get install libunixsocket-java

Javaのインストール

JavaOracle Javaをインストールしました。Open JDKでも問題はないと思います。

$ sudo apt-get install openjdk-8-jdk

Mavenのインストール

Mavenも以下のように設定します。

$ wget http://ftp.jaist.ac.jp/pub/apache/maven/maven-3/3.5.0/binaries/apache-maven-3.5.0-bin.tar.gz
$ tar xzvf apache-maven-3.5.0-bin.tar.gz
$ sudo mv apache-maven-3.5.0 /opt/

環境変数の設定

環境変数JAVA_HOMEとPATHを設定します。~/.bashrcの末尾に以下を記載します。

$ export JAVA_HOME=$(readlink -f /usr/bin/javac | sed "s:/bin/javac::")
$ export PATH=/opt/apache-maven-3.5.0/bin:$JAVA_HOME/bin:$PATH

サンプルコードのビルド

まずはじめにサンプルコードの実行方法を説明します。https://github.com/masato-ka/bluez-dbus-sample.gitをクローンしてください。 クローン後、チェックアウトされたフォルダに入りmavenを使いビルドします。

$git clone https://github.com/masato-ka/bluez-dbus-sample.git
$cd bluez-dbus-sample
$mvn package

ビルド完了後、libunixsocket-javaのライブラリをコピーします。これはbluez-javaライブラリの仕様によるもので、このライブラリは Javaの実行パスからlibフォルダを探して、libunix-javaを読み込みます。もし存在しなければクラスパスからライブラリを読み込むのですが依存jarの一つであるmatthew.jarにlibunix-java.soが含まれていて読み込みます。しかしこのsoはRaspberry Pi Zero wでは動かないので、あらかじめ実行パスにライブラリをコピーしておきます。

$cd bluez-dbus-sample
$mkdir lib
$cp /usr/lib/jni/libunix-java.so ./lib/

実はこのライブラリの読み込みにかなりはまりました。

github.com

これで実行の準備が整いました。実行にはsudoをつけて実行します。Raspbian STRECH + Bluez 5.43ではD-BUSにBluezのD-BUSインタフェースに接続するのにbluetoothユーザグループかルート権限が必要になります。 以下のフォーラムのようにpiユーザにユーザグループを追加すればsudoは必要なくなるはずです。(私はやってみましたが結局sudoが必要でした。。。)

raspberrypi.stackexchange.com

$cd ~/bluez-dbus-sample
$sudo java -jar target/bluz-sample-0.0.1-SNAPSHOT-jar-with-dependencies.jar
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
-----------------HCI Interface---------------------
hci0
-----------------Scan BLE Device---------------------
-----------------Result BLE Device----------------
EnvSensor-BL01
ID115
Choose device:EnvSensor-BL01 //ここで検索したいデバイス名を入力する。
-----------------Characteristics----------------
00002a05-0000-1000-8000-00805f9b34fb
00002a29-0000-1000-8000-00805f9b34fb
00002a24-0000-1000-8000-00805f9b34fb
00002a25-0000-1000-8000-00805f9b34fb
00002a27-0000-1000-8000-00805f9b34fb
00002a26-0000-1000-8000-00805f9b34fb
0c4c3001-7700-46f4-aa96-d5e974e32a54
0c4c3002-7700-46f4-aa96-d5e974e32a54
0c4c3003-7700-46f4-aa96-d5e974e32a54
0c4c3004-7700-46f4-aa96-d5e974e32a54
0c4c3005-7700-46f4-aa96-d5e974e32a54
0c4c3006-7700-46f4-aa96-d5e974e32a54
0c4c3011-7700-46f4-aa96-d5e974e32a54
0c4c3012-7700-46f4-aa96-d5e974e32a54
0c4c3013-7700-46f4-aa96-d5e974e32a54
0c4c3014-7700-46f4-aa96-d5e974e32a54
0c4c3015-7700-46f4-aa96-d5e974e32a54
0c4c3016-7700-46f4-aa96-d5e974e32a54
0c4c3017-7700-46f4-aa96-d5e974e32a54
0c4c3018-7700-46f4-aa96-d5e974e32a54
0c4c3019-7700-46f4-aa96-d5e974e32a54
0c4c301a-7700-46f4-aa96-d5e974e32a54
0c4c3031-7700-46f4-aa96-d5e974e32a54
0c4c3032-7700-46f4-aa96-d5e974e32a54
0c4c3033-7700-46f4-aa96-d5e974e32a54
0c4c3034-7700-46f4-aa96-d5e974e32a54
0c4c3041-7700-46f4-aa96-d5e974e32a54
0c4c3042-7700-46f4-aa96-d5e974e32a54

実行すると上記のようにHCIインタフェース名、検出されたデバイス名、キャラクタリスティックのUUIDが出力されます。今回は前回の記事でも紹介したOMRONの2JCIE-BL01を対象としました。次にサンプルソースの説明を記載します。

masato-ka.hatenablog.com

サンプルコードの解説

サンプルコードの全文はGitHubで確認してください。やっつけで書いたので例外処理などあれな部分も含まれています。 サンプルコードの処理は全部で大きく以下の3つの部分に分かれています。

  1. DeviceManagerオブジェクトの取得
  2. BLEデバイスのスキャン
  3. BLEデバイスへの接続とキャラクタリスティックの取得

pom.xmlへblues-dbusを依存関係として追加します。

<dependency>
        <groupId>com.github.hypfvieh</groupId>
        <artifactId>bluez-dbus</artifactId>
        <version>0.0.2</version>
</dependency>

1 DeviceManagerオブジェクトの取得

try {
    DeviceManager.createInstance(false);
} catch (DBusException e1) {
    System.out.println("failed create instance caused by D-BUS.");
}
DeviceManager deviceManager = DeviceManager.getInstance();
  • (1) DeviceManagerのインスタンスを作成しています。DeviceManager::createInstance(Boolean)でtrueを設定するとD-BUSのログインセッションを利用し、falseを設定するとシステム管理用セッションを利用します。(ライブラリのコメントから解釈しています。)

2 BLEデバイスのスキャン

List<BluetoothAdapter> result = deviceManager.getAdapters();
BluetoothAdapter bluetoothAdaptor = result.get(0); //(1)
System.out.println("-----------------HCI Interface---------------------");
result.stream().map(e->e.getDeviceName()).forEach(System.out::println);
System.out.println("-----------------Scan BLE Device---------------------");
bluetoothAdaptor.startDiscovery();//(2)
Thread.sleep(5000);
bluetoothAdaptor.stopDiscovery();//(3)
  • (1) HCIデバイスのオブジェクトを取得します。今回は最初に見つかったHCIインタフェースを利用します。複数インタフェースがある場合(BLEドングルを刺しているなど)は選ぶ必要があります。
  • (2) デバイスの検索を開始します。
  • (3) 5秒待った後デバイスの検索を停止します。

3 BLEデバイスへの接続とキャラクタリスティックの取得

List<BluetoothDevice> devicies = deviceManager.getDevices(); //(1)
devicies.stream().map(e->e.getName()).forEach(System.out::println);
BufferedReader buffredReader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("Choose device:");
String deviceName = buffredReader.readLine();
System.out.println("-----------------Characteristics----------------");
for(BluetoothDevice bluetoothDevice : devicies){
    if(bluetoothDevice.getName().equals(deviceName)){
        try{
            bluetoothDevice.connect();//(2)
                bluetoothDevice.refreshGattServices();
                List<BluetoothGattService> servicies = bluetoothDevice.getGattServices();//(3)
                for(BluetoothGattService service : servicies){    
                        List<BluetoothGattCharacteristic> characteristics  = service.getGattCharacteristics();//(4)
                    characteristics.stream().map(e->e.getUuid()).forEach(System.out::println);
                }
            }catch(DBusExecutionException e){
                 System.out.println("FailedConnection");
                 e.printStackTrace();
            }
       }
}
  • (1) 検索したデバイスを取得します。
  • (2) デバイスへの接続を行います。
  • (3) 接続が完了したデバイスからサービスを取得します。
  • (4) すべてのサービスからキャラクタリスティックを出力しています。

まとめ

BLEを扱えるJavaのライブラリを探していましたがMavenから追加できてなんとなく動くものが見つかりました。まだまだ発展の余地ありなライブラリですが、ちょっと遊ぶのには良さそうです。またLinux環境のみで開発するのは辛いのでOSXでも動くといいのですが。BLEについては基本的にプラットフォームに依存するためOSを超えての移植性は難しそうです。LinuxであればBluezになりますがOSXではCoreBluetoothをラップする必要があります。WIndowsは。。。Node.jsのnobleであれば全てのプラットフォームに対応できるようです。

PyObjCとからCoreBluetoothを呼び出しWx2Beaconのデータを取得する。

この記事で紹介すること

この記事ではMacOSXBluetoothライブラリである、CoreBluethoothをPythonから呼び出し、WxBeacon2(OMRON 2JCIE-BL01)から環境データを取得する方法について解説しています。 CoreBlutoothはObjectiveCのライブラリのため、PythonとObjectiveCのブリッジライブラリのPyObjCを利用しています。 以下の順番で説明します。

  1. WxBeacon2の概要+BLEの基礎
  2. PyObjeCのインストール
  3. WxBeacon2の読み出しプログラム

WxBeacon2

前回の記事PythonからBLEを制御するライブラリの調査でも記載しましたが、WxBeacon2はウェザーニュースから販売されているBLEインタフェースの環境センサです。OMRONの2JCIE-BL01とほぼ同等の製品になります。外観は以下のようになっており、WxBeacon2にはウェザーニュースさんのロゴがプリントされているのが特徴です。

f:id:masato-ka:20170912231711j:plain
WxBeacon2(2JCIE-BL01)

センサとして取得できる項目は以下の通りです、

センサ名 単位
温度センサ
湿度センサ %
気圧センサ hPa
照度センサ lum
紫外線センサ ?
騒音センサ(マイク) dB

またこれ以外の値としてバッテリーの値が取得できます。 WxBeacon2はBLEにおいて、ペリフェラルとして動作します。またはBeaconのブロードキャスターとしても動作します。今回はWxBeacon2をBLEペリフェラルとして接続します。 話は脱線しますが、BLEの通信について少し記載しておきます。

BLEの通信について

BLEの通信ではセントラルとペリフェラルという2つの役割があります。BLEを使うユースケースの例としてスマートフォン活動量計を考えます。たいていの場合、スマートフォンがセントラル、活動量計ペリフェラルとなります。

セントラル

親機のようなイメージ。ペリフェラルを発見後接続要求を送り、データをやりとりを行う。また、ペリフェラルへNotificationの依頼を送れる。

ペリフェラル

アドバタイズと呼ばれるデータを送信しラントラルに発見させます。 ペリフェラルの機能は大分類のサービスと小分類のキャラクタリスティックで定義されています。さらに個々のキャラクタリスティックのデータに対してRead, Write, Notifyのどの操作が可能か定義されてます。キャラクタリスティックはデータが格納されているアドレスと考えるとすんなりわかります。

セントラルとペリフェラルの通信の流れ

通信は以下のような流れになります。

  1. ペリフェラルがアドバタイズと呼ばれるデータをブロードキャストする。
  2. ペリフェラルのアドバタイズを受信したセントラルが接続を要求する。
  3. ペリフェラルに接続したセントラルがペリフェラルのサービスとキャラクタリスティックを検索
  4. 目的のキャラクタリスティックにRead Write Notifyなどの処理を行う。

f:id:masato-ka:20170912231504p:plain
BLEセントラルとペリフェラルの通信手順

PyObjCのインストール

PyObjCPythonからOSXのObjectiveCのAPIへアクセスするためのブリッジです。

インストール方法

pipからインストールできます。私の環境OSX 10.11.4 El Capitanではlibffiを入れ、パスを通す必要がありました。

$brew install pkg-config libffi
$export PKG_CONFIG_PATH=/usr/local/Cellar/libffi/3.0.13/lib/pkgconfig/
$pip install pyobjc

ソースコード

PyObjCを利用してCoreBluetoothを呼び出し、WxBeacon2からデータを取得するまでのコードを解説します。以下にソースコードを置いています。

github.com

最初に必要なモジュールをインポートします。

#!/usr/bin/env python
# encoding: utf-8

import struct

from Foundation import *
from PyObjCTools import AppHelper

from Foundation import *にてCoreBluetoothを含むObjectiveCのライブラリを読み込みます。from PyObjeCTools import AppHelperでアプリケーションを実行する ヘルパー関数を読み込みます。

次にアプリケーションのメイン関数部分です。

if "__main__" == __name__:
    central_manager = CBCentralManager.alloc()#(1)
    central_manager.initWithDelegate_queue_options_(BleClass(), None, None)#(2)
    AppHelper.runConsoleEventLoop()#(3)
  • (1) CBCentralManagerのインスタンスを取得
  • (2) (1)で取得したインスタンスを初期化する。その際、コールバックを設定しているBleClassを設定
  • (3) アプリケーションを実行すうスレッドを起動する。
wx2_service = CBUUID.UUIDWithString_(u'0C4C3000-7700-46F4-AA96-D5E974E32A54')
wx2_characteristic_data = CBUUID.UUIDWithString_(u'0C4C3001-7700-46F4-AA96-D5E974E32A54')

class BleClass(object):

    def centralManagerDidUpdateState_(self, manager):
        self.manager = manager
        manager.scanForPeripheralsWithServices_options_(None,None) #(1)

    def centralManager_didDiscoverPeripheral_advertisementData_RSSI_(self, manager, peripheral, data, rss): #(2)
        self.peripheral = peripheral
        if '8A783AEE-4277-4C3F-8382-ABFA4F6DB8B6' in repr(peripheral.UUID):
            print 'DeviceName' + peripheral.name()
            manager.connectPeripheral_options_(peripheral, None)
            manager.stopScan()


    def centralManager_didConnectPeripheral_(self, manager, peripheral): #(3)
        print repr(peripheral.UUID())
        peripheral.setDelegate_(self)
        self.peripheral.discoverServices_([wx2_service])
        
    def peripheral_didDiscoverServices_(self, peripheral, services): #(4)
        self.service = self.peripheral.services()[0]
        self.peripheral.discoverCharacteristics_forService_([wx2_characteristic_data], self.service)

    def peripheral_didDiscoverCharacteristicsForService_error_(self, peripheral, service, error): #(5)

        for characteristic in self.service.characteristics():
            if characteristic.properties() == 18:
                peripheral.readValueForCharacteristic_(characteristic)
                break

    def peripheral_didWriteValueForCharacteristic_error_(self, peripheral, characteristic, error):
        print 'In error handler'
        print 'ERROR:' + repr(error)

    def peripheral_didUpdateNotificationStateForCharacteristic_error_(self, peripheral, characteristic, error):
        print "Notification handler"

    def peripheral_didUpdateValueForCharacteristic_error_(self, peripheral, characteristic, error):#(6)
        print repr(characteristic.value().bytes().tobytes())
        value = characteristic.value().bytes().tobytes()

        temp = decode_value(value[1:3],0.01)
        print 'temprature:' + str(temp)

        humid = decode_value(value[3:5],0.01)
        print 'humidity:' + str(humid)

        lum = decode_value(value[5:7])
        print 'lumix:' + str(lum)

        uvi = decode_value(value[9:7], 0.01)
        print 'UV index:' + str(uvi)

        atom = decode_value(value[9:11], 0.1)
        print 'Atom:' + str(atom)

        noise = decode_value(value[11:13], 0.01)
        print 'Noise:' + str(noise)

        disco = decode_value(value[13:15], 0.01)
        print 'Disco:' + str(disco)

        heat = decode_value(value[15:17], 0.01)
        print 'Heat:' + str(heat)
        
        batt = decode_value(value[17:19],0.001)
        print 'Battery:' + str(batt)
  • (1) アプリケーション起動後、Bluetoothモジュールの初期化が完了したらペリフェラルのアドバタイズをスキャンするモードになります。
  • (2) ペリフェラルが見つかったらUUIDと比較を行い、目的のペリフェラルか調べます。接続処理とスキャンを停止しています。
  • (3) ペリフェラルとの接続が呼ばれたらこのメソッドが呼ばれます。続いてサービスの検索を実行しています。
  • (4) サービスが見つかったら続けて、キャラクタリスティックの検索を行います。
  • (5) 検索したキャラクタリスティックが見つかったらデータの取得要求を行います。
  • (6) データを受信したら呼ばれるメソッドです。データは必ずバイト列で送られてくるので、データを分解し各センサの値に変換します。

以下のメソッドでWxBeacon2のデータをデコードします。

#Decoding sensor value from Wx2Beancon Data format.
def decode_value(value, multi=1.0):
    if(len(value) != 2):
        return None
    lsb,msb = struct.unpack('BB',value)
    result = ((msb << 8) + lsb) * multi
    return result

まとめ

今回はCoreBluetoothをPythonから呼び出し、BLEからデータを読み出してみました。CoreBluetooth自体がわかりやすいAPIになっているため、比較的に簡単に実装することができました。 BLEの基礎やCoreBluetoothの詳細については以下の書籍をお勧めします。今回の実装でも一部参考にさせていただきました。

iOS×BLE Core Bluetoothプログラミング

iOS×BLE Core Bluetoothプログラミング

その他参考

PythonからBLEを制御するライブラリの調査

はじめに

この記事ではPythonのBLE制御ライブラリに調査を行った結果をまとめています。2017年のMaker Fair Tokyoウェザーニュースブースにて、WxBeacon2というBLEの環境センサを購入しました。このデバイスはOMRONの2JCIE-BL01まんまの代物です。本家は加速度センサが入っているようですが、ブースの方曰くこちらは入っていないかもとのこと。

OMRON環境センサ

WxBeacon2

しばらくはiPhoneから接続して専用アプリで遊んでみました。せっかくのなので、自分でアプリを作って遊んでみることにしました。今回はRaspberry Pi Zero WとMac OSX上で動作するアプリケーションを作成してみようと考えてみます。実装に利用する言語はPythonを選択しました。PythonのBLEライブラリを検索するといくつか種類が出てきたので、目に付いたものを調査してみました。

Python用BLEライブラリの現状

Python用のBLEライブラリを各プロジェクトのページから調べた結果は以下の表の通りです。4ライブラリを調査しました。

ライブラリ名 ライセンス 対応OS Python 機能 備考
pybluez GPL-v2 Linux,
Mac,
Windows
2.7系,3系 セントラルの機能 El Capitan or Sierraで動作し無さそう
bluepy GPL-v2 Linux 2.7系 3.3 3.4系 セントラルの機能
pyGATT Apache2.0
MITライセンス
Linux, Windows 2.7系 BGAPIのラッパー(Windows, Linuxのみ)
bluezのgatttoolラッパ(Linuxのみ)
WindowsではBLEが使えない?
lightblue GPL v3 Linux,
Mac
2.7系,3系 セントラルの動作のみ メンテされていない。
OSXに関しては10.4時代で更新停止している。
BluefruitLE MITライセンス Linux(Bluez),
Mac(CoreBluetooth)
2.7系 BLEのセントラルの動作のみ

以下に各ライブラリに対する所感を記載します。

pybluez

 一番使われていそうなpybluezですが、その名の通り、Linuxの標準BluetoothスタックのBluezのラッパのようです。Macにインストールした場合はlightblueと言う別のライブラリ経由でOSXのCoreBluetoothライブラリを利用しています。ですがEl CapitanやSierraではインストールに失敗します。リポジトリ上は修正されているようですが、そもそも依存しているlightblueがOSX10.4時代から更新されていないようなので使えないでしょう。またBluezとリンクして動くため、GPLライセンスとなってしまいます。ホビーの場合問題ないですが、個人的に好きではありません。

bluepy

BluezをベースにしたGPL-v2ライセンス。Linuxのみ対応しているがPython3系にも対応している。2017年9月現在もリポジトリに変化あり。開発自体は継続中の模様。

pyGATT

 次にpyGATTはOSX非対応のため、要件から外れます。Bluezのgatttoolコマンドのラッパのようです。WindowではBGAPIというBluetoothシリアルのAPIを提供するようです。特定メーカのドングルでないとダメそう。

lightblue

lightblueはpyblueのMac対応に使われているライブラリです。が、メンテされておらず、前述の通り最新のOSXには対応していません。

BluefruitLE

最後に残ったBluefruitLEですが、更新もされており、上記の中では一番筋がいいように思えます。Windowsも動けば尚良しですが、今回の要件からは外れるので問題ありません。

まとめ

PythonでBLEを制御するライブラリをまとめてみました。どのライブラリもBLEの制御はOS依存のライブラリをラップしているため、すべてのプラットフォームで共通するライブラリは難しそうです。またLinuxのBluez自体がGPLライセンスのため、コピーレフトに敏感な場合は注意が必要そうです。Windows環境を無視すればBluefruitLEが一番筋が良さそう?Macについてはpyobjc経由でCoreBluetoothを叩いているようでした。自分でもpyobjcからBLE制御してみましたが、あっさりと動いたので、個別のOSごとに対応するのは簡単そうです。同じコードで動かすのは骨が折れそうです。

RestTemplateでトークン認証なAPIにアクセスした時のトークン有効期限切れに対応してみた。

この記事の概要

 この記事はSpringBoot 1.5.6でRestTemplateを使い、認証トークンの有効期限切れに対応した際の記録を紹介しています。SpringBootとSpringを熟知しているわけではないので内容に多分の誤解がふくまれているかもしれません。  トークン認証が必要なAPIにアクセスする場合に、事前に何らかの方法で認証トークンを取得し、リクエストヘッダにトークンをセットしてAPIにアクセスするの方法が一般的です。また、多くのAPIで認証トークンの漏洩などによる不正利用に対応するため、認証トークンには有効期限が設けられています。  そのため、RestTemplateでAPIにアクセスする直前に都度認証トークンを取得する必要があります。しかし、APIサーバへのリクエストが遅い場合(ネットワーク的に遠くにあるなど)2回アクセスに時間がかかりパフォーマンスを落としてしまいます。そこで、トークンをキャッシュに保存しておき、使い回す方法が考えられます。  しかし、この場合、キャッシュに保存された有効期限を考慮しなければ、期限切れのトークンでアクセスし、リクエストが受け付けられない事態が発生します。 この記事ではトークンの有効期限を考慮せずに、キャッシュに保存したトークンを使い回すため、アクセス時に認証エラーが返ってきた場合に再度トークンを取得し、リクエストをリトライする方法を紹介します。

キャッシュ事態に有効期限を設ける方法も考えられますが、Spring Cacheの利用を考えたばい、キャッシュに有効期限を設定できるかはキャッシュに利用する製品に依存します。(Redisを利用する場合はできるようです)

以下の3つを順を追って説明します。

1.認証トークンの取得とセット

2.認証トークンのキャッシュ化

3.トークン有効期限切れのリクエストリトライ

認証トークンの取得とセット

そもそもリクエストへの認証トークンの設定はRestTemplateのClientHttpRequestInterceptorインタフェースを実装したInterceptorの中で行います。実際のTokenの取得処理はTokenManagerクラスを作り、そちらで取得を行います。

  • ClientHttpRequestInterceptorを実装したRequestTokenInterceptor(ソース1)
@Component
public class RequestTokenInterceptor implements ClientHttpRequestInterceptor {

    private BTokenManager tokenManager;
    
    public TokenInterceptor(TokenManager tokenManager){
        this.tokenManager = tokenManager;
    }
    
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {

        request.getHeaders().add("Authorization", "Bearer "+tokenManager.getAuthenticationToken());//(1)
        
        ClientHttpResponse response = null;
        
        response = execution.execute(request, body);
        
        return response;
    }

}

(1)リクエストの ヘッダにTokenManagerから取得したトークンをBearerトークンとして設定する。

また、この時、TokenManagerはAPIに対して何らかの認証処理を行い、API Tokenを取得しています。

  • TokenManager(ソース2)
@Component
public class TokenManager {
; 
    @Value("${bing.auth.url:}")
    private String authUrl;
    
    private RestTemplate restTemplate;
    
    public TokenManager(RestTemplateBuilder restTemplateBuilder, BasicAuthRequestInterceptor interceptors){
        restTemplate = restTemplateBuilder.additionalInterceptors(interceptors).build();//(1)
    }
    
    public String getAuthenticationToken(){
        URI uri = UriComponentsBuilder.fromUriString(authUrl).build().toUri();  
        String result = restTemplate.postForObject(uri, null, String.class);//(2)
        return result;
    }
    
}

(1) BasicAuthのためのヘッダを設定するInterceptorを認証処理用のRestTemplateに設定する。今回はToken取得処理の認証情報の設定もInterceptorを利用。 (2) getAuthenticationTokenメソッドが実行されると認証URIにPOSTリクエストを発行し、トークンを取得してからStringとして返す。

この方法ですと、APIにアクセスするたびに、認証リクエストが発行され、1回余分にリクエストが実行されることになります。

認証トークンのキャッシュ化

そこで、Spring Cacheの機能を使い、認証トークン取得メソッドの結果をキャッシュし、2回目以降のリクエストはキャッシュを利用するようにします。変更点はTokenManagerのみで、以下のように getAuthenticationTokenメソッドに@Cachableアノテーションを設定します。Spring Cacheを有効にするため、別途クラスの先頭に@CacheEnableを付与する必要があります。

  • トークン取得処理のメソッドのキャッシュ対応(ソース3)
@Cachable("authToken")   
public String getAuthenticationToken(){
    URI uri = UriComponentsBuilder.fromUriString(authUrl).build().toUri();  
    String result = restTemplate.postForObject(uri, null, String.class);//(2)
    return result;
}
    

これで、2回目のアクセス以降はキャッシュからトークンが返され、認証リクエストは実行されなくなります。ですが前述の通り、キャッシュに登録されているトークンの有効期限が切れた場合にAPIサーバからエラーで弾き返されることになります。APIサーバの使用にもよりますが、多くの場合401や403が帰ってくるかと思います。

トークン有効期限切れのリクエストリトライ

続いて、本題のリクエストリトライの方法です。まずTokenManagerにキャッシュをクリアするメソッドを追加します。以下のメソッドをTokenManagerに追加します。 特に処理はないですが、@CacheEvictアノテーションで指定したキャッシュが削除されます。

  • キャッシュクリアするメソッド(ソース4)
@CacheEvict(value="authToken")
public void clearCacheTokenInfo() {
    log.info("Rfresh token cache.");
}

APIへのリクエストが実行された時、RequestTokenInterceptor::interceptメソッドの中で40xのエラーを検知して、キャッシュをクリアし、新しく Tokenを取得して再度、リクエストを投げます。具体的には上記ソース1を以下のソース5のように修正します。

@Component
public class RequestTokenInterceptor implements ClientHttpRequestInterceptor {

    private BTokenManager tokenManager;
    
    public TokenInterceptor(TokenManager tokenManager){
        this.tokenManager = tokenManager;
    }
    
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {

        request.getHeaders().add("Authorization", "Bearer "+tokenManager.getAuthenticationToken());
        
        ClientHttpResponse response = null;
        
        response = execution.execute(request, body);
        if(response.getStatusCode()==HttpStatus.FORBIDDEN || response.getStatusCode()==HttpStatus.UNAUTHORIZED){//(1)
                tokenManager.clearCacheTokenInfo();//(2)
                List<String> token = new ArrayList<>();
                token.add("Bearer "+tokenManager.getAuthenticationToken());
                request.getHeaders().put("Authorization", token);//(3)
                response = execution.execute(request, body);(4)
        }
        return response;
    }

}

(1) responseのステータスコードを確認して、FORBIDDENまたはUNAUTHORIZEDが帰って来ればリクエストリトライの処理を実行する。トークンエクスパイアの際の実際の挙動については各APIの仕様を調べる必要がある。 (2) リクエストリトライの処理の最初に現在のキャッシュをクリアする。 (3) 今回はAUthorizationヘッダを上書きするため、HttpHeaders::addではなく、HttpHeaders::putを使い値を上書きする。また、この時、TokenManager::getAuthenticationTokenを実行することで、再度トークンがキャッシュされる。 (4) リクエストを実行し、新しい認証トークンを使いAPIにアクセスする。

あとはAPIにアクセスするためのRestTemplateにRestTemplateBuilder経由でinterceptorを追加すれば、トークンの有効期限切れを機にすることなく、透過的にAPIにアクセスすることができます。

まとめ

そもそも認証トークンを一定期間キャッシュで保持するのは一般的なのか知りたいところです。今回の方法ではトークンの有効期限切れを避けるため、キャッシュの残存時間の設定を調整する必要がないのがミソでしょうか。

RestTemplateでレスポンスヘッダ内のContent-Typeを変更する。

この記事で紹介すること

この記事ではSpringのRestTemplateで受け取ったレスポンスのContent-Typeを変更し、任意のHttpMessageConverterを実行させる方法を考えたのでメモとして記載します。この記事はQiitaに投稿した記事をサンプルとして転載しています。

外部のAPIサーバを利用した際に、JSONフォーマットのbodyにContent-Type: text/plainで帰ってくることがありました。レスポンスのJSONに対応したBeanに値をバインドさせようとしてもContent-Typeがtext/plainのため、期待したHttpMessageConverterにマッピングされません。 そこで、RestTemplateにレスポンスのヘッダを変更するClientHttpRequestInterceptorを登録し解決しました。

ClientHttpRequestInterceptorの実装とRestTemplateBuilderの利用方法は以下のサイトを参考にしました。

またBing Speech API を対象に今回の内容を実装したものを以下のリポジトリに置いています。認証などの要素も入っているため、今回の説明とは多少異なります。 https://github.com/masato-ka/speech-api-proxy-service

ClientHttpRequestInterceptorの実装

  • ResponseHeaderInterceptor.java
@Component
public class ResponseHeaderInterceptor implements ClientHttpRequestInterceptor {
    
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {       
        ClientHttpResponse response = execution.execute(request, body);//(1)
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);//(2)
        return response;
    }

}

(1)実際のリクエスト実行処理

(2)リクエスト実行後、レスポンスオブジェクトのContentTypeをapplication/jsonで上書きします。

RestTemaplateの設定と実行

@Component
public class RestClient {
        
        private final String apiUri = "https://hoge.com/api/v1/foo"
        private final RestTemplate restTemplate;
    
    public RestClient(RestTemplateBuilder restTemplateBuilder,
              ResponseHeaderInterceptor interceptor){//(1)
        restTemplate = restTemplateBuilder.additionalInterceptors(interceptor).build();//(2)
    }
    
    public Result request(){

        URI targetUri = UriComponentsBuilder
                                      .fromUriString(apiUri)
                                      .build()
                                      .toUri();
        
        Result result = restTemplate.getForObject(targetUri, Result.class);//(3)
        return result;
    }
    
}

(1) ResponseHeaderInterceptorをInjection

(2) RestTemplateBuilderを通してResponseHeaderInterceptorをRestTemaplateに設定

(3) リクエストを実行、この際にResponseHeaderInterceptorの処理がフックされる

まとめ

そもそもサービス側がContent-Typeを正しく返してくれればいいのですが、コントロールできない他者のサービスだったため、このような方法をとりました。  そもそもInterceptorを実装しない簡単な方法があるかもしれません。