Node.js
RaspberryPi
bleno
LINEThings

LINE Things 対応デバイスをraspberry pi zero w + Node.jsで実装してみる

本エントリは、LINEBot&Clova Advent Calendar 2018 11日目の投稿です。

前日は @daitasu さんのLIFFのおともにSentryはいかが?でした。
これでLIFFを利用したアプリの開発効率がかなり上がりそうですね!私もぜひ試してみたいと思いました。

LINE Thingsを試してみたい

LINE Developer day 2018で発表されたLINE Things
スマートフォン(LINE)からBluetooth LEを使ってIoTデバイスを操作する、といったことが専用のアプリを作ることなく簡単にできるようになるとの事です。これはぜひ試さなくては!

と思ったのですが、公式のサンプルとして用意されているLINE Things Starterで対応しているデバイスを持っていませんでした。
IoTに関してど素人な私が持っているデバイスといえばLINE Simple Beaconの時に買った「raspberry pi zero w」 位しかなかったので、こいつを使って何とかしよう!ということでやってみました!

試した環境

  • デバイスに Raspbian(4.14)をインストールした「raspberry pi zero w」を使用
  • デバイス側の制御プログラムはNode.jsで記述、Bluetooth LE のライブラリはblenoを使用しました。
  • LIFFアプリは「LINE Things Starter」のliff-appのコードをそのまま利用します。

Node.jsのバージョンはv8.11.1を使用 (最新の安定板v10.x系はインストール中にOut of memoryとなってインストールできませんでした。)

事前準備

はじめに

LINE things の設定手順等は、基本的には以下のページに記載されている内容に沿って行っています。まずはこちらをご一読ください。
LINE の IoT プラットフォーム LINE Things の Developer Trial を試してみる

※ 上記設定手順の「ファームウェアの書き込み」の部分で書き込むプログラムが今回自前で実装する部分となります。

blueZのインストール

今回使用するBLEライブラリblenoが依存しているBluetoothのプロトコルスタックモジュール「blueZ」をインストールします。

sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev

LIFFアプリの作成

デモ用のLIFFアプリを作成し、公開しておきます。今回はGithub Pagesを使います。

今回作成したプログラムは、Githubの以下のリポジトリに置いてあります。
https://github.com/pierre3/line-things-sample
LINE Things Starterがら取得した「liff-app」内のファイルを以下の様に「docs」フォルダに格納しておきます。

  • line-things-sample
    • docs
      • index.html
      • liff.js
      • liff.css

リポジトリのSettingsページでGithub PagesのSourceを「master branch/docs folder 」に設定することで、docsフォルダ内が静的Webサイトとして公開されるようになります。( https://pierre3.github.io/line-things-sample/

LIFFアプリの登録

作成したLIFFアプリをBotに紐づけて登録します。
最近、LINE Developersのチャネル設定画面でLIFFを登録できる様になったのですが、この画面ではLINE Thingsで使用する際に必要なパラメータの指定ができないため使用することができません。

そこで、LINE Things対応LIFFが登録できるGithub Pagesのサイトを作りました。
https://pierre3.github.io/add-liff-web/
image.png
今回は、これを使ってLIFFアプリをBotに登録します。

  • [Channel Access Token]欄に、関連付けするBotのアクセストークンを張り付け、「Verify」をクリックします
  • 「Add LIFF」の[URL]欄にLIFFページのURLを入力し、ViewTypeでLIFF画面の表示サイズを指定します。
  • 「Add LIFF」の[Use BLE]チェックボックスにチェックを付けて[Add]をクリックします。
  • 登録に成功すると、「LIFF List」のところに登録したLIFFのURLが表示されます。(URLの下の[Copy LIFF URL]をクリックするとクリップボードにコピーできます。)

LINE Things トライアルプロダクトの作成

次に、トライアルプロダクトの作成を行います。

curl -X POST https://api.line.me/things/v1/trial/products \
-H 'Authorization: Bearer {channel access token}' \
-H 'Content-Type:application/json' \
-d '{
  "name": "{trial product name}",
  "liffId": "{LIFF APP ID}"
}'
  • {Channel Access Token} にはLIFFアプリの登録で使用したアクセストークンを設定します
  • {traial product name} には任意のプロダクト名を指定します
  • {LIFF APP ID} には、LIFFアプリの登録で取得したLIFFのURLから”line://app/”を削除した文字列を設定します

(※こちらも、LIFFアプリ登録で使用したページで登録できるようにしたかったのですが、こっちのAPIはクロスドメインのアクセスを許可していないようで、Github Pagesの静的サイトでは利用できませんでした。)

トライアルプロダクトの作成に成功すると、以下の情報が返却されてきます。

{
    "id":6477170151163308299,
    "name":"pierre3/line-things-sample",
    "type":"BLE",
    "channelId":1570607058,
    "actionUri":"line://app/1570607058-xNE0NQZZ",
    "serviceUuid":"7d0fdbdc-0c2d-46bb-99c8-45bf2431e92b",
    "psdiServiceUuid":"e625601e-9e55-4597-a598-76018a0d293d",
    "psdiCharacteristicUuid":"26e2b12b-85f0-4f3f-9fdd-91d114270e6e"
}

ここで取得したUUIDがあとで必要になるので忘れずに保存しておきましょう。

LIFFアプリ内のService UUIDを書き換える

リポジトリの「docs」フォルダ追加したliff.jsを開き、USER_SERVICE_UUIDを先ほど取得したServiceUuidの値に書き換えます。

liff.js
// User service UUID: Change this to your generated service UUID
const USER_SERVICE_UUID         = '7d0fdbdc-0c2d-46bb-99c8-45bf2431e92b'; // <--ここを置き換える
// User service characteristics
const LED_CHARACTERISTIC_UUID   = 'E9062E71-9E62-4BC6-B0D3-35CDCD9B027B'; // <-- ここと
const BTN_CHARACTERISTIC_UUID   = '62FBD229-6EDD-4D1A-B554-5C4E1BB29169'; // <-- ここはデバイス側で合わせる

// PSDI Service UUID: Fixed value for Developer Trial
const PSDI_SERVICE_UUID         = 'E625601E-9E55-4597-A598-76018A0D293D'; // <-- ここと
const PSDI_CHARACTERISTIC_UUID  = '26E2B12B-85F0-4F3F-9FDD-91D114270E6E'; // <-- ここは固定値

//...

ここまでで、ようやく準備完了です。

デバイス側の処理をNode.jsで実装する

いよいよデバイス側の実装に取り掛かります。
リポジトリに「pi-zerow」のフォルダを作って、そこに実装することにしましょう。

  • line-things-sample
    • docs
      • index.html
      • liff.js
      • liff.css
    • pi-zerow
      • ...
~ $ cd /line-things-sample
~/line-things-sample $ mkdir pi-zerow
~/line-things-sample $ cd pi-zerow
~/line-things-sample/pi-zerow $ npm init

続いてblenoをインストール

~/line-things-sample/pi-zerow $ npm install --save bleno

LチカするためのGPIOライブラリをonoffをインストール

~/line-things-sample/pi-zerow $ npm install --save onoff

index.jsを作成。このファイルにBLE制御のコードを書いていきます。

~/line-things-sample/pi-zerow $ touch index.js

おおよそ以下の手順で処理を行います。

  • 各種UUIDの設定
  • 依存パッケージの準備
  • サービスの作成・登録
    • Characteristicの作成
      • ユーザーサービスのCharacteristic
      • デバイス特定用サービスの Characteristic
    • アドバタイジング開始
    • サービスの登録

全体のソースコードは以下を参照ください。
https://github.com/pierre3/line-things-sample/blob/master/pi-zerow/index.js

各種UUIDの設定

まずはデバイスとLIFFアプリとでお互いを認識するために必要なUUIDを設定します。

index.js
const USER_SERVICE_UUID = '7d0fdbdc-0c2d-46bb-99c8-45bf2431e92b';
const WRITE_CHARACTERISTIC_UUID = 'E9062E71-9E62-4BC6-B0D3-35CDCD9B027B';
const NOTIFY_CHARACTERISTIC_UUID = '62FBD229-6EDD-4D1A-B554-5C4E1BB29169';

const PSDI_SERVICE_UUID ='E625601E-9E55-4597-A598-76018A0D293D';
const PSDI_CHARACTERISTIC_UUID = '26E2B12B-85F0-4F3F-9FDD-91D114270E6E';
  • USER_SERVICE_UUID
    トライアルプロダクトの作成で取得したServiceUuidを指定
  • WRITE_CHARACTERISTIC_UUID
    LIFFアプリ(liff.js)のLED_CHARACTARISTIC_UUIDと同じ値を設定
  • NOTIFY_CHARACTERISTIC_UUID
    LIFFアプリ(liff.js)のBTN_CHARACTARISTIC_UUIDと同じ値を設定
  • PSDI_SERVICE_UUID
    トライアルプロダクトの作成で取得したpsidServiceUuidを指定
  • PSDI_CHARACTERISTIC_UUID
    トライアルプロダクトの作成で取得したpsidCharacteristicUuidを指定

依存パッケージの取得

BLE制御用のパッケージblenoと、Lチカ用のパッケージonOffを参照します。

index.js
const DEVICE_NAME = 'line-things-device';

const bleno = require('bleno');
const onoff = require('onoff');

const Characteristic = bleno.Characteristic;
const PrimaryService = bleno.PrimaryService;
const Gpio = onoff.Gpio;

サービスの作成

LINE Things 対応のデバイスを作るには、次の2種類のサービスを用意する必要があります。

  • デバイス特定用サービス
    LIFFがLINE Things対応デバイスを特定するのためのサービス。ここで設定されたService UUIDとCharacteristic UUIDを見てLINE Things対応デバイスか否かを判断します。
  • ユーザーサービス
    ユーザーが実現したいサービスはこちらに記述します。トライアルプロダクトの作成時に取得したService UUIDを指定する必要があります。

Characteristicの作成

ユーザーサービスのCharacteristic

LIFF画面の「LED ON/OFF」ボタンがタップされた際の通知をwriteCharcteristicで定義します。
「Switch LED ON」をタップすると 「value=1」が、「Switch LED OFF」をタップすると「value=0」が onWriteRequestイベントに通知されます。
タップする度に0、1が交互に飛んでくるので、それをLEDをつなげたGPIO(ここではGPIO 12ピン)に設定することでLEDをON/OFFさせるようにしています。

index.js
const writeCharacteristic = new Characteristic({
    uuid: WRITE_CHARACTERISTIC_UUID,
    properties: ['write'],
    onWriteRequest: (data, offset, withoutResponse, callback) => {
        console.log(`onOff = ${data[0]}`)

        var led = new Gpio(12,"out");     
        led.writeSync(data[0]);          
        callback(Characteristic.RESULT_SUCCESS);
    }
});

次にデバイス側のアクションをnotifyCharacteristicで定義します。
プロパティは 'notify'を使用します。
notifyでは購読開始(onSubscribe)時に通知用のCallback関数を受け取ります。デバイス側からの通知は、このCallback関数を使って行います。

今回は、プッシュボタン等アクションを起こせるパーツが何もなかったので、setInterval()関数で0、1を交互に送信するようにしています。

index.js
let onOff = 0;
const notifyCharacteristic = new Characteristic({
    uuid: NOTIFY_CHARACTERISTIC_UUID,
    properties: ['notify'],
    onSubscribe: (maxSize, callback) => {
        console.log('subscribe');
        setInterval(function() {
            onOff ^= 1;
            callback(new Buffer([onOff]));
          }, 2000);
     }
});
PSDIサービスのCharacteristic

デバイス特定用サービスのCharacteristicをpsdiCharacteristicで定義します。
プロパティは'read'を使用します。
LIFF側からreadのリクエストがあった場合にCharacteristic.RESULT_SUCCESSで応答します。

index.js
const psdiCharacteristic = new Characteristic({
    uuid: PSDI_CHARACTERISTIC_UUID,
    properties: ['read'],
    onReadRequest: (offset, callback) => {
        console.log('PSDI read');
        const result = Characteristic.RESULT_SUCCESS;
        const data = new Buffer.from('PSDI read');
        callback(result, data);
    }
});

アドバタイジングの開始

blenoのstateChangeイベントで、Bluetoothのインターフェースが起動したタイミングでアドバタイジングを開始するようにします。
startAdvertiging()関数には、デバイス名とサービスUUIDを渡します。
サービスUUIDは配列で複数指定できるのですが、USER_SERVICE_UUIDとPSDI_SERVICE_UUIDの両方を渡すと、「31バイトを超えた情報は送れないよ」とエラーとなってしまいました。
ここではUSER_SERVICE_UUIDのみを設定するのが正解のようです。(PSDI_SEVICE_UUIDでも試しましたがだめでした)

index.js
bleno.on('stateChange', (state) => {
    console.log(`on -> stateChange: ${state}`);
    if (state === 'poweredOn') {
        bleno.startAdvertising(DEVICE_NAME, [USER_SERVICE_UUID]);
    } else {
        bleno.stopAdvertising();
    }
});

サービスの登録

blenoのadvertisingStartイベントでユーザーサービスとPSDIサービスを作成します。
それぞれ、対応するService UUIDとCharacteristicを指定します。

作成したサービスをsetServices()関数で登録して完了です!

index.js
bleno.on('advertisingStart', (error) => {
    console.log(`on -> advertisingStart: ${(error ? 'error ' + error : 'success')}`);
    if(error) return;

    const userService = new PrimaryService({
        uuid: USER_SERVICE_UUID,
        characteristics: [writeCharacteristic,notifyCharacteristic]
    }); 
    const psdiService = new PrimaryService({
        uuid: PSDI_SERVICE_UUID,
        characteristics: [psdiCharacteristic]
    });

    bleno.setServices([userService,psdiService]);
});

デバイス側のプログラムを実行する

それでは、作成したプログラムindex.jsを実行してみましょう!

~/line-things-sample/pi-zerow $ sudo node index.js

以下の様にログが表示されれば起動成功です。

~/line-things-sample/pi-zerow $ sudo node index.js
bleno - line-things-device
on -> stateChange: poweredOn
on -> advertisingStart: success

LINEとLINE Thingsデバイスを連携させる

以下に記載されている手順に従ってデバイスの連携を行います。

うまく連携できれば「マイデバイス」欄に連携したデバイスのプロダクト名が表示されるので、それをタップしてLIFFアプリを起動します。
無事起動したら「Switch LED ON/OFF」をタップしてLEDとチカチカさせられたら成功です!
ezgif.com-video-to-gif.gif

ちゃんとボタンをタップするたびにLEDが「ON」/「OFF」されました!
LIFF アプリ内のButton Stateも定期的に「Pressed」/「Released」と切り替わることが確認できます。

おわりに

今回、BLEなど前提となる知識がほとんどない状態で始めましたが、ハンズオンの記事やLINE Things Starterのソースコード等を参考にさせて頂き、何とか動かすことができました。
また、BLE制御に使用したパッケージblenoもシンプルで直感的に使用することができました。

今回はraspberry pi とNode.jsでしたが、BLEの要件さえ満たしていれば実装方法によらず対応デバイスを実装することができそうですね。