LINE Thingsの自動通信機能を応用して、電波が届くエリアに入ったら認証コードを送信して
LINEのLIFFを使って認証している端末に認証コードを送信するシステムを作ってみました。
認証コードはAmazon Connectを使って電話がかかってくるパターンとLINE Notifyを使って通知する2パターンを試しました。
動作デモ
LINE Thingsの自動通信を応用した認証コードシステムのデモです。Amazon Connectで電話がかかってきて、認証コードを教えてくれます。LIFFアプリで認証コードをLINE Things経由でM5Stackに渡しているデモです。#linethings #linedc #AmazonConnect #M5Stack pic.twitter.com/zq0ZBmt5LK
— がおまる@スマートスピーカーアプリ開発入門発売中! (@gaomar) May 27, 2019
アーキテクチャ
LINE Thingsの自動通信でWebhookのURLへ飛ばして、AWS LambdaからAmazon ConnectとLINE Notifyの処理を実行しています。
一連の流れ
1. M5Stackのプログラム
特に難しいことはしていません。LINE Things連携済みの携帯がIoTデバイスに近づいたら認証コードを作成して端末に送信しています。
#include <M5Stack.h>
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
// Device Name: Maximum 30 bytes
#define DEVICE_NAME "LINE Things Meetup Demo"
// あなたのサービスUUIDを貼り付けてください
#define USER_SERVICE_UUID "<あなたのサービスUUID>"
// Notify UUID: トライアル版は値が固定される
#define NOTIFY_CHARACTERISTIC_UUID "62FBD229-6EDD-4D1A-B554-5C4E1BB29169"
// PSDI Service UUID: トライアル版は値が固定される
#define PSDI_SERVICE_UUID "E625601E-9E55-4597-A598-76018A0D293D"
// LIFFからのデータ UUID: トライアル版は値が固定される
#define WRITE_CHARACTERISTIC_UUID "E9062E71-9E62-4BC6-B0D3-35CDCD9B027B"
// PSDI CHARACTERISTIC UUID: トライアル版は値が固定される
#define PSDI_CHARACTERISTIC_UUID "26E2B12B-85F0-4F3F-9FDD-91D114270E6E"
BLEServer* thingsServer;
BLESecurity* thingsSecurity;
BLEService* userService;
BLEService* psdiService;
BLECharacteristic* psdiCharacteristic;
BLECharacteristic* notifyCharacteristic;
BLECharacteristic* writeCharacteristic;
bool deviceConnected = false;
bool oldDeviceConnected = false;
bool sendFlg = false;
bool unlockFlg = false;
// 認証コード
int randNumber;
class serverCallbacks: public BLEServerCallbacks {
// デバイス接続
void onConnect(BLEServer* pServer) {
deviceConnected = true;
// 一度認証されないとコードは生成しない
if (unlockFlg) {
sendFlg = false;
unlockFlg = false;
}
};
// デバイス未接続
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
if (unlockFlg) {
sendFlg = false;
unlockFlg = false;
}
}
};
// LIFFから送信されるデータ
class writeCallback: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *bleWriteCharacteristic) {
// LIFFから来るデータを取得
std::string value = bleWriteCharacteristic->getValue();
int myNum = atoi(value.c_str());
// 認証コードと一致しているか確認
if (randNumber == myNum) {
M5.Lcd.fillScreen(GREEN);
M5.Lcd.setCursor(0, 30);
M5.Lcd.print("Unlock!");
unlockFlg = true;
}
}
};
void setup() {
Serial.begin(115200);
BLEDevice::init("");
BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT_NO_MITM);
// Security Settings
BLESecurity *thingsSecurity = new BLESecurity();
thingsSecurity->setAuthenticationMode(ESP_LE_AUTH_BOND);
thingsSecurity->setCapability(ESP_IO_CAP_NONE);
thingsSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
setupServices();
startAdvertising();
// put your setup code here, to run once:
M5.begin();
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setTextSize(2);
}
void loop() {
if (!deviceConnected && oldDeviceConnected) {
delay(500); // Wait for BLE Stack to be ready
thingsServer->startAdvertising(); // Restart advertising
oldDeviceConnected = deviceConnected;
Serial.println("Restart!");
}
// Connection
if (deviceConnected && !oldDeviceConnected) {
oldDeviceConnected = deviceConnected;
}
if (!sendFlg && deviceConnected){
sendFlg = true;
delay(5000);
// 認証コード生成
randNumber = random(1000, 10000);
char newValue[16];
sprintf(newValue, "%d", randNumber);
// 認証コードをWebhookURLに送信
notifyCharacteristic->setValue(newValue);
notifyCharacteristic->notify();
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 30);
M5.Lcd.print("Send!!");
delay(5000);
M5.Lcd.fillScreen(BLACK);
M5.Lcd.setCursor(0, 30);
M5.Lcd.print("Connected"); /*表示クリア*/
delay(50);
} else if (!deviceConnected) {
M5.Lcd.setCursor(0, 30);
M5.Lcd.print("Not Connected");
}
M5.update();
}
// サービス初期化
void setupServices(void) {
// Create BLE Server
thingsServer = BLEDevice::createServer();
thingsServer->setCallbacks(new serverCallbacks());
// Setup User Service
userService = thingsServer->createService(USER_SERVICE_UUID);
// LIFFからのデータ受け取り設定
writeCharacteristic = userService->createCharacteristic(WRITE_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_WRITE);
writeCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
writeCharacteristic->setCallbacks(new writeCallback());
// Notifyセットアップ
notifyCharacteristic = userService->createCharacteristic(NOTIFY_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_NOTIFY);
notifyCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
BLE2902* ble9202 = new BLE2902();
ble9202->setNotifications(true);
ble9202->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
notifyCharacteristic->addDescriptor(ble9202);
// Setup PSDI Service
psdiService = thingsServer->createService(PSDI_SERVICE_UUID);
psdiCharacteristic = psdiService->createCharacteristic(PSDI_CHARACTERISTIC_UUID, BLECharacteristic::PROPERTY_READ);
psdiCharacteristic->setAccessPermissions(ESP_GATT_PERM_READ_ENCRYPTED | ESP_GATT_PERM_WRITE_ENCRYPTED);
// Set PSDI (Product Specific Device ID) value
uint64_t macAddress = ESP.getEfuseMac();
psdiCharacteristic->setValue((uint8_t*) &macAddress, sizeof(macAddress));
// Start BLE Services
userService->start();
psdiService->start();
}
void startAdvertising(void) {
// Start Advertising
BLEAdvertisementData scanResponseData = BLEAdvertisementData();
scanResponseData.setFlags(0x06); // GENERAL_DISC_MODE 0x02 | BR_EDR_NOT_SUPPORTED 0x04
scanResponseData.setName(DEVICE_NAME);
thingsServer->getAdvertising()->addServiceUUID(userService->getUUID());
thingsServer->getAdvertising()->setScanResponseData(scanResponseData);
thingsServer->getAdvertising()->start();
}
2. Lambdaの処理
Amazon Connectで電話をかける処理とLINE Notifyへ送信する処理を実装しています。
const Util = require('util.js');
exports.handler = async (event) => {
const body = JSON.parse(event.body);
const thingsData = body.events[0].things.result;
if (thingsData.bleNotificationPayload) {
// LINE Thingsから飛んでくるデータを取得
const blePayload = thingsData.bleNotificationPayload;
var buffer1 = new Buffer(blePayload, 'base64');
var m5Data = buffer1.toString('ascii'); //Base64をデコード
const sendMessage = `認証コードは「${m5Data}」です。`;
// Amazon Connect送信
await Util.callMessageAction(m5Data);
// LINE Notify送信
await Util.postNotify(sendMessage);
}
};
'use strict';
const request = require('request');
const AWS = require('aws-sdk');
var connect = new AWS.Connect();
// あなたのトークンを設定してください
const headers = {
Authorization: `Bearer ${process.env.NOTIFY_TOKEN}`,
'Content-Type': 'application/x-www-form-urlencoded'
};
// 電話をかける処理
module.exports.callMessageAction = async function callMessageAction(message) {
return new Promise(((resolve, reject) => {
// 1桁ずつ読ませるために分けている
const no1 = message.substr(0, 1);
const no2 = message.substr(1, 1);
const no3 = message.substr(2, 1);
const no4 = message.substr(3, 1);
// Attributesに発話する内容を設定
var params = {
Attributes: {"message": `<speak><break time='2s' />認証コードは、<prosody rate="65%"><say-as interpret-as="digits">${no1}</say-as>、<say-as interpret-as="digits">${no2}</say-as>、<say-as interpret-as="digits">${no3}</say-as>、<say-as interpret-as="digits">${no4}</say-as></prosody>です。</speak>`},
InstanceId: process.env.INSTANCEID,
ContactFlowId: process.env.CONTACTFLOWID,
DestinationPhoneNumber: process.env.PHONENUMBER,
SourcePhoneNumber: process.env.SOURCEPHONENUMBER
};
// 電話をかける
connect.startOutboundVoiceContact(params, function(err, data) {
if (err) {
console.log(err);
reject();
} else {
resolve(data);
}
});
}));
};
// LINE Notifyへ送信
module.exports.postNotify = async function postNotify(postMessage) {
// オプションを定義
const jsonData =
{
'message': postMessage
};
const options = {
url: 'https://notify-api.line.me/api/notify',
method: 'POST',
headers: headers,
form: jsonData
};
return new Promise(function (resolve, reject) {
request(options, function (error, response, body) {
if (!error) {
console.log(body.name);
resolve(true);
} else {
console.log('error: ' + response + body);
resolve(true);
}
});
});
};
3. Amazon Connect側の設定
Attributesのmessageの内容をAmazon Connectの問い合わせフローで発話するように設定しています。
全体のフローは以下の通り。問い合わせ属性の設定
部分でmessageの内容を取得します。
聞き逃しがないように認証コードは2回ループさせています。
詳細は下記サイトをご確認ください。
https://dev.classmethod.jp/cloud/aws/ask-linebot-to-call-from-amazon-connect-api/
問い合わせ属性の設定は以下の通り。宛先キーと属性にmessage
を設定しています。
4. LINE Notifyの設定
こちらからトークンを発行します。
https://notify-bot.line.me/my/
5. LIFFアプリの設定
LIFF側は今回Vue.jsでやってみました。
プログラムはのサンプルはこちらで公開しています。
index.htmlでLIFFのSDKを設定しています。UIライブラリはVuetifyを使っています。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script src="https://d.line-scdn.net/liff/1.0/sdk.js"></script>
<title>linethings-meetup-demo</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Material+Icons">
</head>
<body>
<noscript>
<strong>We're sorry but linethings-meetup-demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
実際の画面はこちら
<template>
<v-app id="inspire">
<v-container>
<v-layout
text-xs-center
wrap
>
<v-flex mb-4>
<h1 class="display-2 font-weight-bold mb-3">
認証画面
</h1>
</v-flex>
<v-flex xs12>
<v-form>
<v-container>
<v-layout row wrap>
<v-text-field
v-model="code"
@keyup.enter="say"
@keypress="setCanSubmit"
outline
label="認証コードを入力してください"
></v-text-field>
</v-layout>
</v-container>
</v-form>
</v-flex>
<v-flex xs12>
{{this.bleStatus}}
</v-flex>
</v-layout>
</v-container>
</v-app>
</template>
<script>
export default {
data () {
return {
USER_SERVICE_UUID: '<あなたのサービスUUID>',
LED_CHARACTERISTIC_UUID: 'E9062E71-9E62-4BC6-B0D3-35CDCD9B027B', /* トライアルは固定値 */
bleConnect: false,
canSubmit: false,
bleStatus: '',
state: false,
characteristic: '',
code: '',
user: {
image: '',
userId: ''
}
}
},
methods: {
setCanSubmit () {
this.canSubmit = true
},
say () {
this.sendData()
},
sendData () {
this.bleStatus = `送信:${this.code}`
let ch_array = this.code.split("");
for(let i = 0; i < 16; i = i + 1){
ch_array[i] = (new TextEncoder('ascii')).encode(ch_array[i]);
}
this.characteristic.writeValue(new Uint8Array(ch_array)
).catch(error => {
this.bleStatus = error.message
})
},
// BLEが接続できる状態になるまでリトライ
liffCheckAvailablityAndDo: async function (callbackIfAvailable) {
try {
const isAvailable = await liff.bluetooth.getAvailability();
if (isAvailable) {
callbackIfAvailable()
} else {
// リトライ
this.bleStatus = `Bluetoothをオンにしてください。`
setTimeout(() => this.liffCheckAvailablityAndDo(callbackIfAvailable), 10000)
}
} catch (error) {
alert('Bluetoothをオンにしてください。')
}
},
// サービスとキャラクタリスティックにアクセス
liffRequestDevice: async function () {
const device = await liff.bluetooth.requestDevice()
await device.gatt.connect()
const service = await device.gatt.getPrimaryService(this.USER_SERVICE_UUID)
service.getCharacteristic(this.LED_CHARACTERISTIC_UUID).then(characteristic => {
this.characteristic = characteristic
this.bleConnect = true
this.bleStatus = `デバイスに接続しました!`
}).catch(error => {
this.bleConnect = true
this.bleStatus = `デバイス接続に失敗=${error.message}`
})
},
initializeLiff: async function(){
await liff.initPlugins(['bluetooth']);
this.liffCheckAvailablityAndDo(() => this.liffRequestDevice())
// プロフゲット
const profile = await liff.getProfile()
this.user.image = profile.pictureUrl
this.user.userId = profile.userId
}
},
mounted: function () {
liff.init(
() => this.initializeLiff()
)
}
}
</script>
まとめ
認証コードはM5Stackで保持しているので、ちょっとだけセキュアです。
一度認証させると次回接続時は別のIDが生成されるようにもしています。一種のワンタイムパスワードみたいなものですね。
LINE Thingsを使えば、様々な連携をすることができます。是非試してみてください!
システム化のご検討やご相談は弊社までお問い合わせください。
https://i-enter.co.jp/contact/