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

本エントリは、[LINEBot&Clova Advent Calendar 2018](https://qiita.com/advent-calendar/2018/linebot) 11日目の投稿です。

前日は @daitasu さんの[LIFFのおともにSentryはいかが？](https://qiita.com/daitasu/items/7c55f5b8919432a8b584)でした。
これでLIFFを利用したアプリの開発効率がかなり上がりそうですね！私もぜひ試してみたいと思いました。

## LINE Thingsを試してみたい
[LINE Developer day 2018](https://linedevday.linecorp.com/jp/2018/)で発表された[LINE Things](https://developers.line.biz/ja/docs/line-things/)。
スマートフォン(LINE)からBluetooth LEを使ってIoTデバイスを操作する、といったことが専用のアプリを作ることなく簡単にできるようになるとの事です。これはぜひ試さなくては！

と思ったのですが、公式のサンプルとして用意されている[LINE Things Starter](https://github.com/line/line-things-starter)で対応しているデバイスを持っていませんでした。
IoTに関してど素人な私が持っているデバイスといえば[LINE Simple Beacon](https://engineering.linecorp.com/ja/blog/line-beacon-raspberry-pie-line-simple-beacon-specification/)の時に買った「raspberry pi zero w」 位しかなかったので、こいつを使って何とかしよう！ということでやってみました！

## 試した環境
- デバイスに Raspbian(4.14)をインストールした「raspberry pi zero w」を使用
- デバイス側の制御プログラムはNode.jsで記述、Bluetooth LE のライブラリは[bleno](https://www.npmjs.com/package/bleno)を使用しました。
- LIFFアプリは「LINE Things Starter」の[liff-app](https://github.com/line/line-things-starter/tree/master/liff-app)のコードをそのまま利用します。

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


## 事前準備
### はじめに
LINE things の設定手順等は、基本的には以下のページに記載されている内容に沿って行っています。まずはこちらをご一読ください。
[LINE の IoT プラットフォーム LINE Things の Developer Trial を試してみる](https://engineering.linecorp.com/ja/blog/line-things-developer-trial/)

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

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

```shell
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](https://github.com/line/line-things-starter/tree/master/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](https://qiita-image-store.s3.amazonaws.com/0/28805/de21e407-84a0-bb4d-6c8a-9687437dc910.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 トライアルプロダクトの作成
次に、[トライアルプロダクトの作成](https://developers.line.biz/ja/docs/line-things/regist-product/)を行います。

```shell
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の静的サイトでは利用できませんでした。)

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

```json
{
    "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
      - ... 

```shell
~ $ cd /line-things-sample
~/line-things-sample $ mkdir pi-zerow
~/line-things-sample $ cd pi-zerow
~/line-things-sample/pi-zerow $ npm init
```

続いてblenoをインストール

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

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

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


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

```shell
~/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を実行してみましょう！

```shell
~/line-things-sample/pi-zerow $ sudo node index.js
```
以下の様にログが表示されれば起動成功です。

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

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

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

- [LINEとLINE Thingsデバイスを連携させてみよう - LINE Things Developer Traial ハンズオン](https://qiita.com/hktechno/items/bb83cc898c75819b2664#line-%E3%81%A8-line-things-%E3%83%87%E3%83%90%E3%82%A4%E3%82%B9%E3%82%92%E9%80%A3%E6%90%BA%E3%81%95%E3%81%9B%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86)

うまく連携できれば「マイデバイス」欄に連携したデバイスのプロダクト名が表示されるので、それをタップしてLIFFアプリを起動します。
無事起動したら「Switch LED ON/OFF」をタップしてLEDとチカチカさせられたら成功です！
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.amazonaws.com/0/28805/b7a88bae-a971-78a8-80bd-bd6c0aaf49cb.gif)

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

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

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