昨年リリースされたLINE Thingsに新たに自動通信機能が追加されました。自動通信機能とはアプリを閉じている間もBLE通信を行うことをいいます。
line developersのブログより
つまり、この機能を利用すればwebhookでデバイスのセンサーデータを送ることができるので、サーバーサイドでデータの処理を施すことができるようになります。ということは今まで以上にIoTシステムの構築の可能性が広がるということです。そこで、今回はこの機能を利用して心拍数を測定するウェアラブルデバイスを作ってみたので、紹介します。
とりあえずセンサーのデータを送信してみた
まずは、自動通信がどんなものかを試すためにセンサーのデータをサーバーに送信してそのままボットからメッセージを送信する流れで、試していきたいと思います。
LIFF&ボット側
今までのStarterプログラムはgithub pagesを使ってliffを作成していましたが、今回はwebhookを使うのでherokuとpython(Flask)でliffとボットを作成します。今回、LIFFは初回の接続に必要なだけなので、starterで使ってたフロントをそのまま使っても大丈夫です(この記事の下に掲載したGithubのリポジトリにあるLIFFにはセンサの値が見れるようにアレンジしました)。自動通信をする際にthingsのイベントを受け取ってメッセージを送信する部分は下記に載せます。
def handle_things_event(event):
if event["things"]["type"] != "scenarioResult":
return
if event["things"]["result"]["resultCode"] != "success":
app.logger.warn("Error result: %s", event)
return
username = event["source"]["userId"]
heart_rate = int.from_bytes(base64.b64decode(event["things"]["result"]["bleNotificationPayload"]), 'little')
save_date = datetime.now()
message = "Heart_rate: " + heart_rate
print("Got data: " + str(heart_rate))
line_bot_api.reply_message(event.reply_token,
TextSendMessage(text=message))
thingsにはまだイベントをパースするAPIが無いので、ここはJSONの操作でしっかりデータを送信されたかどうかを判断して、必要なデータを取り出していきます。ポイントとしてはwebhookで受け取るメッセージはbase64になっているので、デコードしてデータを可視化することです。
ハード側
用意するもの&配線
今回マイコンはmicro:bitを使用します。心拍数はseeedの心拍センサーでとっていきます。指や手首に巻くことが出来ます。さらにmicro:bitとセンサーを接続するために、micro:bit用のシールドを使います。配線はただケーブルを繋げるだけなので、特に配線図を書くほどではありませんが、参考画像を下に載せます。このセンサーはI2Cで通信を行うなのでシールドのI2C端子に接続します。
ファームウェアの書き込み
micro:bitでArduinoを動かすときは設定がいろいろ必要なので、この記事を参考にしてファームウェアを書き込みできるようにします。ファームウェアはこのサンプルにセンサーの値を受け取る部分を足すだけで、送信できます。自動通信を行うために特別な設定を加える必要はありません。センサの値を受け取る部分は心拍センサの公式ドキュメントにあるサンプルコードを参考にします。具体的なファームウェアは下記に載せます。USER_SERVICE_UUID
は先ほどliffを作成した際に生成したものを記入してください。
#include <Wire.h>
#include <BLEPeripheral.h>
#include <Adafruit_Microbit.h>
// Device Name: Maximum 20 bytes
#define DEVICE_NAME "micro:bit"
// Local Name in advertising packet: Maximum 29 bytes
#define LOCAL_NAME "BBC micro:bit"
// User Service UUID: Change this to your generated service UUID
#define USER_SERVICE_UUID "YOUR_UUID"
// User service characteristics
#define WRITE_CHARACTERISTIC_UUID "E9062E71-9E62-4BC6-B0D3-35CDCD9B027B"
#define NOTIFY_CHARACTERISTIC_UUID "62FBD229-6EDD-4D1A-B554-5C4E1BB29169"
// PSDI Service UUID: Fixed value for Developer Trial
#define PSDI_SERVICE_UUID "E625601E-9E55-4597-A598-76018A0D293D"
#define PSDI_CHARACTERISTIC_UUID "26E2B12B-85F0-4F3F-9FDD-91D114270E6E"
BLEPeripheral blePeripheral;
BLEBondStore bleBondStore;
// Setup User Service
BLEService userService(USER_SERVICE_UUID);
BLEUnsignedCharCharacteristic writeCharacteristic(WRITE_CHARACTERISTIC_UUID, BLEWrite);
BLEUnsignedCharCharacteristic notifyCharacteristic(NOTIFY_CHARACTERISTIC_UUID, BLENotify);
// Setup PSDI Service
BLEService psdiService(PSDI_SERVICE_UUID);
BLECharacteristic psdiCharacteristic(PSDI_CHARACTERISTIC_UUID, BLERead, sizeof(uint32_t) * 2);
Adafruit_Microbit_Matrix microbit;
volatile int btnAction = 0;
void setup() {
Serial.begin(115200);
Serial.println("Initializing...");
Serial.println("heart rate sensor:");
Wire.begin();
pinMode(PIN_BUTTON_A, INPUT_PULLUP);
pinMode(PIN_BUTTON_B, INPUT_PULLUP);
// Clear bond store if push button A+B for 3 secs on start up
// You can bond only one central device on each peripheral device by library restriction
if (!digitalRead(PIN_BUTTON_A) && !digitalRead(PIN_BUTTON_B)) {
delay(3000);
if (!digitalRead(PIN_BUTTON_A) && !digitalRead(PIN_BUTTON_B)) {
bleBondStore.clearData();
Serial.println("Cleared bond store");
}
}
attachInterrupt(PIN_BUTTON_A, buttonAction, CHANGE);
blePeripheral.setDeviceName(DEVICE_NAME);
blePeripheral.setLocalName(LOCAL_NAME);
blePeripheral.setBondStore(bleBondStore);
blePeripheral.setAdvertisedServiceUuid(userService.uuid());
blePeripheral.addAttribute(userService);
blePeripheral.addAttribute(writeCharacteristic);
blePeripheral.addAttribute(notifyCharacteristic);
blePeripheral.addAttribute(psdiService);
blePeripheral.addAttribute(psdiCharacteristic);
// Set callback
blePeripheral.setEventHandler(BLEConnected, blePeripheralConnectHandler);
blePeripheral.setEventHandler(BLEDisconnected, blePeripheralDisconnectHandler);
writeCharacteristic.setEventHandler(BLEWritten, writeLEDCallback);
// Set PSDI (Product Specific Device ID) value
uint32_t deviceAddr[] = { NRF_FICR->DEVICEADDR[0], NRF_FICR->DEVICEADDR[1] };
psdiCharacteristic.setValue((unsigned char *)deviceAddr, sizeof(deviceAddr));
blePeripheral.begin();
Serial.println("Ready to Connect");
microbit.begin();
}
void loop() {
BLECentral central = blePeripheral.central();
if (central && central.connected()) {
Wire.requestFrom(0xA0 >> 1, 1); // request 1 bytes from slave device
while(Wire.available()) { // slave may send less than requested
uint8_t c = Wire.read(); // receive heart rate value (a byte)
Serial.println(c, DEC); // print heart rate value
notifyCharacteristic.setValue(c);
}
delay(2000);
}
blePeripheral.poll();
}
void buttonAction() {
btnAction++;
}
void writeLEDCallback(BLECentral& central, BLECharacteristic& characteristic) {
if (writeCharacteristic.value()) {
Serial.println("ON");
microbit.show(microbit.HEART);
} else {
Serial.println("OFF");
microbit.clear();
}
}
void blePeripheralConnectHandler(BLECentral& central) {
// central connected event handler
Serial.print("Connected event, central: ");
Serial.println(central.address());
microbit.show(microbit.YES);
}
void blePeripheralDisconnectHandler(BLECentral& central) {
// central disconnected event handler
Serial.print("Disconnected event, central: ");
Serial.println(central.address());
microbit.show(microbit.NO);
}
書き込みが終わったらペアリングを行い、エラーなくLIFFが立ち上がることを確認します。もし、以前にmicro:bitでLINEThingsを使っていて、エラーが発生してLIFFが立ち上がらないときは、こちらの記事を参考にしてmicro:bitのキャッシュをクリアにしてみてください。
##いよいよ自動通信
ここまででLIFFアプリを起動している間にデバイスに接続するところまでいけたと思います。今度はここからセンサの値を自動通信で送信できるようにするためにシナリオセットという作業を行います。とはいってもただ自動通信管理APIを一発実行するだけで設定が完了します。今回のパターンだと以下のようにcurlを実行します。ここでのproductIdはUUIDを生成したときに出力されたものを使用します。
curl -X PUT https://api.line.me/things/v1/products/{productId}/scenario-set \
-H "Authorization: Bearer {channel access token}" \
-H "Content-Type: application/json" \
-d "{
"autoClose": false,
"suppressionInterval": 0,
"scenarios": [
{
"trigger": {
"type": "BLE_NOTIFICATION",
"serviceUuid": "{YOUR_SERVICE_UUID}",
"characteristicUuid": "62FBD229-6EDD-4D1A-B554-5C4E1BB29169"
},
"actions": [
]
}
]
}"
シナリオの設定が完了したらLINE Things連携画面をもう一度開き直すと、上記のシナリオが有効になります。LINEアプリを閉じると下の画像のようにセンサーの値がボットからのメッセージとして出力されたら自動通信ができています。
もう少し実用的にしてみた
実際に使ってみるとわかりますが、ここまででボットの運用をしてみると毎回通知がきて鬱陶しいです(笑)。せっかくLINEボットを使っているのだから、センサを動かしている時に聞きたい時に今の心拍数を返してくれるようにしてみたいんですよ。そこで新たにデータベースを追加します。使用するのはherokuで簡単に設定できるPostgres SQLです。herokuからAdd onして、有効化しておきましょう。そして、ターミナルを立ち上げてherokuにデプロイしたコードが入っているディレクトリに移動したら、以下のコマンドを実行して新規のデータベースを作ります。
heroku run python
>>> from app import db
>>> db.create_all()
作成したらインデックスに「現在時刻」「心拍数」を追加します。ボットにメッセージを送信したらクエリで10秒間の心拍数を取り出しその平均をメッセージとして返せるようにしました。おまけ機能として、デバイスが起動していない時に心拍数を聞いてもデバイスの接続が無いことを返すようにしてみました。実行結果が以下のようになります。
いい感じに動きました!
まとめ
今までIoTシステムを構築するためには専用のプラットフォームを用意して複数のサービスと連携する必要がありました。しかし、LINE Thingsなら通常のLINE Thingsの設定をした上で、APIを実行するだけで自動通信を簡単にできてしまいます。LINEのサービスだけでIoTシステムを完結できるので、IoT初心者、特にお仕事でWEB系をやっている方にとっては、IoTを始める敷居が低くなるような気がします。(ただBluetoothの接続を行うところで、ちょっと戸惑うかもしれません…)あとはハードウェアの知識にも左右されるので、そこからアイデアがさらに広がると思います。
今回のソースコード
こちらのレポジトリにherokuにデプロイしたコードを載せておきます。
参考
自動通信の準備をする
#linethings でバイクの盗難防止装置作ってみた
LINE Things 自動通信機能がリリースされました & 使い方紹介