社会の窓締め忘れ通知装置 episode２

このお話は、[IoT縛りの勉強会! IoTLT vol.38 @ ウイングアーク1st](https://iotlt.connpass.com/event/82485/)でLTした内容です。
ウケ狙いでLTしたため、技術的なことは喋りませんでした、ここに書いておきます。

# LTの内容（SlideShare）
[![thumbnail](http://image.slidesharecdn.com/chuck2-181101124619/95/slide-1-1024.jpg)](https://www.slideshare.net/YoshiakiIto1/ver2-121421359)

概要は上記資料をみてください。

# プログラムの解説
## システム構成
![imagel](http://image.slidesharecdn.com/chuck2-181101124619/95/slide-14-1024.jpg)

## ハードウエア(バックル部)
![IMG_3045.jpg](https://qiita-image-store.s3.amazonaws.com/0/136795/4087ef51-cdd4-0611-b813-68df0f4b6725.jpeg)

### 材料
下記を使いました

+ ピンバッチのケース   
ちょうど大きさがミニブレッドボードにあっていました。
+ BLE Nano
https://www.switch-science.com/catalog/3444/
BLE通信ができる超小型のマイコンです、MbedでもArduinoでも開発できます
今回は慣れているArduinoでコード書きました
本体と書き込み装置があるので両方買いました

+ ミニブレッドボード
http://akizukidenshi.com/catalog/g/gP-05155/

+ ボタン電池基板取付用ホルダー
http://akizukidenshi.com/catalog/g/gP-00706/

+ リードスイッチ
http://akizukidenshi.com/catalog/g/gP-03676/
近くに磁石があると電気が通じるスイッチです

## ソフトウエア
### バックル部(arduino)

プログラムの説明
ArduinoでLINE Beaconの信号の送信処理を行います

+ hwid[]にLINE BeaconのHWIDを設定してください
+ advertising()関数：ビーコンのパケットを送信します、連続送信は行いません、ただし送信を停止します
+ setup()関数：初期化を行います、電池を持たせるために通常はLEDを消灯しています、なので電源ON時10間だけ点滅させてます
+ loop()関数：D2につけたリードスイッチがOFF（磁石が離れた＝チェックが下がった）になったらLEDを点灯します、、3秒間連続でOFFだとビーコンのパケットを送信します

```ino
#include <nRF5x_BLE_API.h>
#include "setting.h"

BLEDevice ble;
uint8_t sid[] = {0x6F, 0xFE};
uint8_t sdata[] = {0x6F, 0xFE, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x00};
uint8_t hwid[] = {0x00, 0x00, 0x00, 0x00, 0x00};  // LINE BeaconのHWID
int cnt = 0;

void advertising() {
  ble.init(); 
  ble.accumulateAdvertisingPayload(GapAdvertisingData::BREDR_NOT_SUPPORTED | GapAdvertisingData::LE_GENERAL_DISCOVERABLE);
  ble.accumulateAdvertisingPayload(GapAdvertisingData::COMPLETE_LIST_16BIT_SERVICE_IDS , sid, sizeof(sid));
  ble.accumulateAdvertisingPayload(GapAdvertisingData::SERVICE_DATA , sdata, sizeof(sdata));
  ble.setAdvertisingType(GapAdvertisingParams::ADV_NON_CONNECTABLE_UNDIRECTED);
  ble.setAdvertisingInterval(160);   // 100ms; in multiples of 0.625ms
  ble.setAdvertisingTimeout(5);      // 単位は秒
  ble.startAdvertising();
  ble.waitForEvent();
  ble.stopAdvertising();
  ble.shutdown();
  delay(150);
}
void setup() {
  Serial.begin(9600);
  NRF_POWER->DCDCEN = 0x00000001;

  pinMode(LED, OUTPUT);
  pinMode(D2, INPUT_PULLUP);

  memcpy(&sdata[3], hwid, 5);
  for (int i = 0; i < 10; i++) {  // 10秒間ウエイト
    digitalWrite(LED, HIGH);      // LED ON
    delay(500);
    digitalWrite(LED, LOW);       // LED OFF    
    delay(500);
  }
}

void loop() {
  int data = digitalRead(D2);
  if (data) {
    if (++cnt & 0x1) {
      digitalWrite(LED, HIGH);
    } else {
      digitalWrite(LED, LOW);      
    }
    if (cnt > 30 ) {        // 3秒間
      digitalWrite(LED, HIGH);
      advertising();        // チャックが開いている時だけ通信
    }
  } else {
    cnt = 0;
    digitalWrite(LED, LOW);
  }
  Serial.println(data, HEX);
  delay(100);
}
```

### サーバ(LINE BOT)
プログラムの説明
HerokuやAzure APP Service上のnodeで動作するプログラムです

+ LINE_CHANNEL_ACCESS_TOKENにLINEのアクセストークンを設定してください
+ TWILIO_CALL_URLには、後述のTwilio Functionsの/chuckcallのURLを設定してください
+ TWILIO_CALL_BODYは携帯の電話番号を設定します「+81」をつけるために「%2B81」とコードを書いています
+ function reply(event, text)関数：LINEの画面にメッセージを表示します
+ function select(id, hwid)関数：LINEの画面に選択画面を表示します、電話するを選択するとtwilio()関数を呼びます
+ app.post('/', function(request, response)関数：LINEからWebHookされます、ビーコンを検知したらselect()関数を、検知しなくなったらreply()関数を呼びます
+ function twilio()関数：Twilio Functionsの/chuckcallを呼びます

```js
var express = require('express');
var bodyParser = require('body-parser');
var request = require('request');
var app = express();

var LINE_CHANNEL_ACCESS_TOKEN = "LINEのアクセストークン";
var TWILIO_CALL_URL = "TwilioのURL";
var TWILIO_CALL_BODY = 'NumberToCall=%2B819012345678携帯の電話番号';

app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
app.use(express.static('img'));

app.set('port', (process.env.PORT || 5000));

function reply(event, text) {
    var headers = {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
    }
    console.log(headers);
    var body = {
        replyToken: event.replyToken,
        messages: [{
            type: 'text',
            text: text
        }]
    }
    console.log(body);
    var url = 'https://api.line.me/v2/bot/message/reply';
    request({
        url: url,
        method: 'POST',
        headers: headers,
        body: body,
        json: true
    }, function(error, response, bdy){
        if (!error && response.statusCode == 200) {
            console.log(bdy);
        } else {
            console.log('error: '+ response.statusCode);
        }
    });
}

function select(id, hwid) {
  var headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + LINE_CHANNEL_ACCESS_TOKEN
  }
  var body = {
    to: id,
    messages: [{
      type: 'template',
      altText: 'チャックを開けている人を発見',
      template: {
        type: "buttons",
        thumbnailImageUrl: 'https://shakainomado.azurewebsites.net/chuck.png',
        title: '近くにチャックを開けている人がいます ' + hwid,
        text: '探して、伝えてもらえませんか？\n恥ずかしいのであれば、こちらが電話で通知します\nどうします？',
        actions: [
          {
            type: 'postback',
            label: '私が伝えます',
            data: 'none'
          },
          {
            type: 'postback',
            label: '電話で教えてあげて',
            data: 'tel'
          }
        ]
      }
    }]
  }
  var url = 'https://api.line.me/v2/bot/message/push';
  request({
    url: url,
    method: 'POST',
    headers: headers,
    body: body,
    json: true
  }, function(error, response, body){
    if (!error && response.statusCode == 200) {
      //console.log(body.name);
    } else {
      console.log('error: '+ response.statusCode);
    }
  });
}

app.post('/', function(request, response) {
  console.log('post');
  console.log(request.body);
  response.sendStatus(200);

  for (var event of request.body.events) {
    console.log('event.type : ' + event.type);
    console.log('event.source : ' + event.source);
    if (event.source.type == 'user') {
      console.log('event.source.userId : ' + event.source.userId);
    }
    if (event.type == 'message') {
      console.log('message : ' + event.message.text);
    } else if (event.type == 'beacon') {
      console.log('*event.beacon.type : ' + event.beacon.type);
      console.log('*event.beacon.hwid : ' + event.beacon.hwid);
      console.log('*event.beacon.dm : ' + event.beacon.dm);
      console.log('*event.source.userId : ' + event.source.userId);
      if (event.beacon.type == 'enter') {
        select(event.source.userId, event.beacon.hwid);
      } else {
        reply(event, 'チャックを閉めたか、あるいは離れていきました ' + event.beacon.hwid);
      }
    }
    if (event.type == 'postback') {
      console.log('postback: '+ event.postback.data);
      if (event.postback.data == 'tel') {
        twilio();
      }
    }
  }
});

app.listen(app.get('port'), function() {
  console.log("Node app is running at localhost:" + app.get('port'))
});


function twilio() {
  var headers = {
    'Accept': '*/*',
    'Content-Type': 'application/x-www-form-urlencoded'
  }
  var body = TWILIO_CALL_BODY;
  var url = TWILIO_CALL_URL;
  request({
    url: url,
    method: 'POST',
    headers: headers,
    body: body,
    json: false
  });
}
```

### Twilio 
電話をかけるTwilio Functionsです、電話をかける/chuckcallを音声を返す/chuckvoiceの２つのFunctionsを用意します



#### /chuckcall(Twilio Functions)
```js
exports.handler = function(context, event, callback) {
	const client = context.getTwilioClient()
	client.calls.create({
		to: event.NumberToCall, 
		from: context.PHONE_NUMBER, 
		url:  'https://*****.twil.io/chuckvoice 
 のURL'
    }, 
	function(err, res) {
		callback(err, "OK")
	})
};
```

### /chuckvoice(Twilio Functions)
```js
exports.handler = function(context, event, callback) {
	let twiml = new Twilio.twiml.VoiceResponse();
 	twiml.say("こんにちは。ちゃっくが、あいていますよ。きをつけましょうね。", {voice : "", language: "ja-JP"});
	callback(null, twiml);
};
```

一昨年のLINE BOT AWARDSグランプリの&HAND（アンドハンド）も同じコンセプトでした
https://www.andhand-project.com/
このようなサービスが、本当に使われるようになるといいですね