Help us understand the problem. What is going on with this article?

LINE Thingsでウェアラブルデバイスを作ってみた

昨年リリースされたLINE Thingsに新たに自動通信機能が追加されました。自動通信機能とはアプリを閉じている間もBLE通信を行うことをいいます。
about-auto-communication-01.jpg
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端子に接続します。
2019-05-10_00-37-37_320.jpeg

ファームウェアの書き込み

micro:bitでArduinoを動かすときは設定がいろいろ必要なので、この記事を参考にしてファームウェアを書き込みできるようにします。ファームウェアはこのサンプルにセンサーの値を受け取る部分を足すだけで、送信できます。自動通信を行うために特別な設定を加える必要はありません。センサの値を受け取る部分は心拍センサの公式ドキュメントにあるサンプルコードを参考にします。具体的なファームウェアは下記に載せます。USER_SERVICE_UUIDは先ほどliffを作成した際に生成したものを記入してください。

sens.ino
#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アプリを閉じると下の画像のようにセンサーの値がボットからのメッセージとして出力されたら自動通信ができています。

2019-05-15_01-19-36_000.png

もう少し実用的にしてみた

実際に使ってみるとわかりますが、ここまででボットの運用をしてみると毎回通知がきて鬱陶しいです(笑)。せっかくLINEボットを使っているのだから、センサを動かしている時に聞きたい時に今の心拍数を返してくれるようにしてみたいんですよ。そこで新たにデータベースを追加します。使用するのはherokuで簡単に設定できるPostgres SQLです。herokuからAdd onして、有効化しておきましょう。そして、ターミナルを立ち上げてherokuにデプロイしたコードが入っているディレクトリに移動したら、以下のコマンドを実行して新規のデータベースを作ります。

heroku run python
>>> from app import db
>>> db.create_all()

作成したらインデックスに「現在時刻」「心拍数」を追加します。ボットにメッセージを送信したらクエリで10秒間の心拍数を取り出しその平均をメッセージとして返せるようにしました。おまけ機能として、デバイスが起動していない時に心拍数を聞いてもデバイスの接続が無いことを返すようにしてみました。実行結果が以下のようになります。
2019-05-15_01-19-18_000.png
いい感じに動きました!

まとめ

今までIoTシステムを構築するためには専用のプラットフォームを用意して複数のサービスと連携する必要がありました。しかし、LINE Thingsなら通常のLINE Thingsの設定をした上で、APIを実行するだけで自動通信を簡単にできてしまいます。LINEのサービスだけでIoTシステムを完結できるので、IoT初心者、特にお仕事でWEB系をやっている方にとっては、IoTを始める敷居が低くなるような気がします。(ただBluetoothの接続を行うところで、ちょっと戸惑うかもしれません…)あとはハードウェアの知識にも左右されるので、そこからアイデアがさらに広がると思います。

今回のソースコード

こちらのレポジトリにherokuにデプロイしたコードを載せておきます。

参考

自動通信の準備をする
#linethings でバイクの盗難防止装置作ってみた
LINE Things 自動通信機能がリリースされました & 使い方紹介

K_M95
IBM Champion 2020 / LINE API Expert 2020
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away