WebブラウザからBLE接続 WEB Bluetooth APIでNotificationを受け取る方法
この記事について
この記事ではWebBluetooth API を使い、BLEデバイスからNotificationを受け取る方法を説明します。 対象とするデバイスは2JCIE-BL01を利用します。デバイスから一定間隔で環境データを取得します。
WEB Bluetooth API
WEB Bluetooth APIはWEBブラウザ上でBLEデバイスと通信を実現するJavaScriptのAPIです。実装は各ブラウザごとに 違いますが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 ブラウザ上に表示されます。接続したいデバイスを選択し、ペア設定を押します。
(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上であれば動かせると思います。
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のインストール
JavaはOracle 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/
実はこのライブラリの読み込みにかなりはまりました。
これで実行の準備が整いました。実行にはsudoをつけて実行します。Raspbian STRECH + Bluez 5.43ではD-BUSにBluezのD-BUSインタフェースに接続するのにbluetoothユーザグループかルート権限が必要になります。 以下のフォーラムのようにpiユーザにユーザグループを追加すればsudoは必要なくなるはずです。(私はやってみましたが結局sudoが必要でした。。。)
$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を対象としました。次にサンプルソースの説明を記載します。
サンプルコードの解説
サンプルコードの全文はGitHubで確認してください。やっつけで書いたので例外処理などあれな部分も含まれています。 サンプルコードの処理は全部で大きく以下の3つの部分に分かれています。
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のデータを取得する。
この記事で紹介すること
この記事ではMacOSXのBluetoothライブラリである、CoreBluethoothをPythonから呼び出し、WxBeacon2(OMRON 2JCIE-BL01)から環境データを取得する方法について解説しています。 CoreBlutoothはObjectiveCのライブラリのため、PythonとObjectiveCのブリッジライブラリのPyObjCを利用しています。 以下の順番で説明します。
- WxBeacon2の概要+BLEの基礎
- PyObjeCのインストール
- WxBeacon2の読み出しプログラム
WxBeacon2
前回の記事PythonからBLEを制御するライブラリの調査でも記載しましたが、WxBeacon2はウェザーニュースから販売されているBLEインタフェースの環境センサです。OMRONの2JCIE-BL01とほぼ同等の製品になります。外観は以下のようになっており、WxBeacon2にはウェザーニュースさんのロゴがプリントされているのが特徴です。
センサとして取得できる項目は以下の通りです、
センサ名 | 単位 |
---|---|
温度センサ | ℃ |
湿度センサ | % |
気圧センサ | hPa |
照度センサ | lum |
紫外線センサ | ? |
騒音センサ(マイク) | dB |
またこれ以外の値としてバッテリーの値が取得できます。 WxBeacon2はBLEにおいて、ペリフェラルとして動作します。またはBeaconのブロードキャスターとしても動作します。今回はWxBeacon2をBLEペリフェラルとして接続します。 話は脱線しますが、BLEの通信について少し記載しておきます。
BLEの通信について
BLEの通信ではセントラルとペリフェラルという2つの役割があります。BLEを使うユースケースの例としてスマートフォンと活動量計を考えます。たいていの場合、スマートフォンがセントラル、活動量計がペリフェラルとなります。
セントラル
親機のようなイメージ。ペリフェラルを発見後接続要求を送り、データをやりとりを行う。また、ペリフェラルへNotificationの依頼を送れる。
ペリフェラル
アドバタイズと呼ばれるデータを送信しラントラルに発見させます。 ペリフェラルの機能は大分類のサービスと小分類のキャラクタリスティックで定義されています。さらに個々のキャラクタリスティックのデータに対してRead, Write, Notifyのどの操作が可能か定義されてます。キャラクタリスティックはデータが格納されているアドレスと考えるとすんなりわかります。
セントラルとペリフェラルの通信の流れ
通信は以下のような流れになります。
- ペリフェラルがアドバタイズと呼ばれるデータをブロードキャストする。
- ペリフェラルのアドバタイズを受信したセントラルが接続を要求する。
- ペリフェラルに接続したセントラルがペリフェラルのサービスとキャラクタリスティックを検索
- 目的のキャラクタリスティックにRead Write Notifyなどの処理を行う。
PyObjCのインストール
PyObjCはPythonから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からデータを取得するまでのコードを解説します。以下にソースコードを置いています。
最初に必要なモジュールをインポートします。
#!/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の詳細については以下の書籍をお勧めします。今回の実装でも一部参考にさせていただきました。
- 作者: 堤修一,松村礼央
- 出版社/メーカー: ソシム
- 発売日: 2015/03/23
- メディア: 単行本
- この商品を含むブログを見る
その他参考
WxBeaconのデータのパース方法
PyObjCとCoreBluetooth
PythonからBLEを制御するライブラリの調査
はじめに
この記事ではPythonのBLE制御ライブラリに調査を行った結果をまとめています。2017年のMaker Fair Tokyoのウェザーニュースブースにて、WxBeacon2というBLEの環境センサを購入しました。このデバイスはOMRONの2JCIE-BL01まんまの代物です。本家は加速度センサが入っているようですが、ブースの方曰くこちらは入っていないかもとのこと。
しばらくは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.認証トークンのキャッシュ化
認証トークンの取得とセット
そもそもリクエストへの認証トークンの設定は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の設定と実行
- RestClient.java
@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を実装しない簡単な方法があるかもしれません。
Konashiを使ってみた
ユカイ工学さんのKonashiで遊んでみた。
http://konashi.ux-xu.com/
はじめてのObjective-Cつらい。。。グラフ書くのにCore-Plotというライブラリを利用したんだけでも、
ただ折れ線グラフ書くだけで心が折れそうに。
説明:
iOSシミュレータでKonashiに接続後、iOS側からデジタルIOを制御してます。
その後圧力センサの値をAD変換で取り込んでグラフに出力しています。
何作ろうかな。