概要
LINEThingsを使ってLINEとデバイスを連携させ、O2Oマーケティングの一例を再現してみる。今回は、M5Stack CoreInkという通信機能を持った電子ペーパーを値札として用い、LINEでアンケートに答えるとその値札の表示を変更し、購買を促す(本当かな?)といったものを作成した。
※注: 筆者がガス欠で説明不十分な点があるかと思います(2020/12/11)
やったこと
- LINEThingsを用いて、LINEとM5Stack CoreInkを連携
- LIFF上でアンケートに答えてもらったら、値札の表示を変更する
M5 CoreInkを電子値札のようにしてみた。 pic.twitter.com/xr9Qbo7Fq8
— ぱぱでぃーの (@TransFat06g) December 11, 2020
動作環境
- maacOS Catalina ver.10.15.3
- Python 3.7.0
- python ライブラリ
- line-bot-sdk==1.16.0
- resspnder==1.3.2
うまくできなかったこと
- M5Stack CoreInk側からLINE(スマホ)との接続を切断できなかった(すぐ再接続される)
説明しないこと
- LINE BOTの作成方法
- M5Stack CoreInkのsetup方法
LINE Thingsとは
LINE Thingsは、LINEを介して、チャネルとBluetooth® Low Energy対応デバイスを連携し、操作を可能にするIoTプラットフォームです。
https://developers.line.biz/ja/docs/line-things/about-line-things/
M5Stack CoreInkとは
ざっと説明すると、WifiやBluetooth通信機能を備えた電子ペーパーデバイスです。
下記のページに詳細な説明があります。よければ、参考にしてください。 - [スイッチサイエンス販売ページ](https://www.switch-science.com/catalog/6735/)この記事でいうO2Oマーケティングとは
**オンラインでの施策を、オフラインに反映させ、顧客の購買行動を促進させるマーケティンング手法。**とします。
本当は、参考ページのようなオフライン、オンライン両方で相互誘導できるようなマーケティング手法のことを言うのだと思います。。。(ざっくり認識です。)
今回の記事で具体例を挙げてみると、服屋さんに来店した顧客に、LINE上で回答できるアンケートを配信する。そのアンケートに回答すると、店舗にある値札が値引きされたり、おすすめのコーディネートなどが表示されたりする。それによって、顧客の購買意欲促進を狙う。(て感じなのかな...)
参考ページ:
フロー図
大体のフローはこんな感じです。
①今回作成するLINEBOTを友達登録している人のLINEと、値札(M5Stack CoreInk)がbluetooth接続される
②アンケートを送信する
③アンケートに回答する
④値札の表示が変更される
手順
1. LINEBOTを作成する
適宜、LINEBOTを作成してください。
参考ページ:
2. Webhookされるサーバーを作成する
今回はLINEThingsを用いて、デバイスから送信されたメッセージに合わせて、利用者に送信するメッセージを変更する必要があります。ですので、メッセージを受け取り、処理をする部分を担うサーバーを作成します。
公式SDK
Messaging API対応のLINE公式SDKはこちらです。下記の中でお好きな言語を選択してください。
- Java
- PHP
- Go
- Perl
- Ruby
- Python
- Node.js
私は、Pythonを用い、サーバー側の処理を作成しました。
ソースコードはこちら
今回特筆すべき部分としては、以下の処理で、M5 CoreInkからメッセージを受信した際に実行されるメソッドです。
#---略---
###
# deviceからメッセージ受信時
###
@handler.add(ThingsEvent)
def handle_message(event):
if event.things is None:
print(event)
return
if event.things.type != "scenarioResult":
print(event)
return
if event.things.result.result_code != "success":
print(event)
return
# 届いたメッセージをdecode
message = base64.b64decode(event.things.result.ble_notification_payload).decode()
if message[0] == "S":
# LIFFアンケートページをトーク画面に返信する。
liff_url = "https://liff.line.me/1655338407-PZBX17Wz"
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text="今アンケートに答えると割引チャンス!\n" + liff_url)
)
else:
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text="対応していません")
)
#---略---
3. LINEBotと2で作成したサーバーを接続する
ここでは、
参考ページ:
4. LINEBotでLINEThingsを使えるようにする
下記記事がめちゃくちゃ詳細に記載してくれているので、おすすめです。
参考資料:
5. M5Stack CoreInkがLINEやサーバーと接続するための処理を追加する
ソースコードが割と長いため、折りたたんでいます。
LINEやサーバーとの接続時に使用するソースコード
#include <BLEServer.h>
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <M5CoreInk.h>
Ink_Sprite InkPageSprite(&M5.M5Ink);
// Device Name: Maximum 30 bytes
#define DEVICE_NAME "Trial M5Stack"
// User service UUID: Change this to your generated service UUID
#define USER_SERVICE_UUID "{USER_SERVICE_UUID}"
// User service characteristics
#define WRITE_CHARACTERISTIC_UUID "{WRITE_CHARACTERISTIC_UUID}"
#define NOTIFY_CHARACTERISTIC_UUID "{NOTIFY_CHARACTERISTIC_UUID}"
// PSDI Service UUID: Fixed value for Developer Trial
#define PSDI_SERVICE_UUID "{PSDI_SERVICE_UUI}"
#define PSDI_CHARACTERISTIC_UUID "{PSDI_CHARACTERISTIC_UUID}"
BLEServer* thingsServer;
BLESecurity *thingsSecurity;
BLEService* userService;
BLEService* psdiService;
BLECharacteristic* psdiCharacteristic;
BLECharacteristic* writeCharacteristic;
BLECharacteristic* notifyCharacteristic;
bool deviceConnected = false;
bool oldDeviceConnected = false;
class serverCallbacks: public BLEServerCallbacks {
void onConnect(BLEServer* pServer) {
deviceConnected = true;
};
void onDisconnect(BLEServer* pServer) {
deviceConnected = false;
}
};
boolean flg = false;
class writeCallback: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *bleWriteCharacteristic) {
std::string value = bleWriteCharacteristic->getValue();
String read_value;
if (value.length() > 0) {
Serial.println("*********");
Serial.print("New value: ");
for (int i = 0; i < value.length(); i++)
read_value = value[i];
if (read_value == "1"){
flg = true;
}
Serial.println();
Serial.println("*********");
}
}
};
void setup() {
M5.begin();
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();
digitalWrite(LED_EXT_PIN,LOW);
if( !M5.M5Ink.isInit()){
Serial.printf("Ink Init faild");
while (1) delay(100);
}
M5.M5Ink.clear();
if( InkPageSprite.creatSprite(0,0,200,200,true) != 0 ){
Serial.printf("Ink Sprite creat faild");
}
Serial.printf("Ink Sprite creat successful");
draw_nehuda();
draw_nedan(false);
InkPageSprite.pushSprite();
}
void loop() {
if (flg) {
InkPageSprite.clear();
draw_nehuda();
draw_nedan(true);
InkPageSprite.pushSprite();
flg = false;
}
if ( M5.BtnUP.wasPressed() ) {
// デバッグ用
String mes = "S"+(String)1;
notifyCharacteristic->setValue(mes.c_str());
notifyCharacteristic->notify();
Serial.println("BtnUP.wasPressed() == TRUE");
}
if ( M5.BtnDOWN.wasPressed() ) {
// 再接続用
Serial.println("BtnDOWN.wasPressed() == TRUE");
oldDeviceConnected = false;
}
// Connection
if (deviceConnected && !oldDeviceConnected) {
oldDeviceConnected = deviceConnected;
delay(3000);
String mes = "S"+(String)2;
notifyCharacteristic->setValue(mes.c_str());
notifyCharacteristic->notify();
Serial.println("connected");
delay(3000);
uint16_t connId = thingsServer->getConnId();
thingsServer->disconnect(connId);
thingsServer->getAdvertising()->stop();
}
M5.update();
}
void setupServices(void) {
// Create BLE Server
thingsServer = BLEDevice::createServer();
thingsServer->setCallbacks(new serverCallbacks());
// Setup User Service
userService = thingsServer->createService(USER_SERVICE_UUID);
// Create Characteristics for User Service
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());
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();
}
6. 2で作成したサーバーに、サーバー側からM5CoreInkにwriteする処理を追加
M5CoreInkに接続・writeする処理
import asyncio
import time
from bleak import BleakClient, BleakScanner
WRITE_CHARACTERISTIC_UUID = "WRITE_CHARACTERISTIC_UUID"
mac_addr = "M5 CoreInkのMACアドレス"
async def run(address, loop):
async with BleakClient(address, loop=loop) as client:
x = await client.is_connected()
print("Connected: {0}".format(x))
write_value = bytearray(b"1")
await client.write_gatt_char(WRITE_CHARACTERISTIC_UUID, write_value)
print("write")
# 5秒後に終了
await asyncio.sleep(5.0, loop=loop)
if __name__=="__main__":
loop = asyncio.get_event_loop()
loop.run_until_complete(run(mac_addr, loop))
アンケートに回答したことを受信した際、上記処理を呼び出す
from write import run, mac_addr, WRITE_CHARACTERISTIC_UUID
#--- 略 ---
###
# メッセージ受信時
###
@handler.add(MessageEvent, message=TextMessage)
def handle_message(event):
if event.message.text == "送信しました":
loop = asyncio.get_event_loop()
loop.run_until_complete(run(mac_addr, loop))
line_bot_api.reply_message(
event.reply_token,
TextSendMessage(text="割引されるのでお待ちを")
)
7. アンケートフォームを作成する
アンケートフォームを作成します。
ソースコードはこちら