LINEThingsとM5CoreInkを使ってO2Oマーケティングを体験してみる

## 概要
LINEThingsを使ってLINEとデバイスを連携させ、O2Oマーケティングの一例を再現してみる。今回は、M5Stack CoreInkという通信機能を持った電子ペーパーを値札として用い、LINEでアンケートに答えるとその値札の表示を変更し、購買を促す(本当かな？)といったものを作成した。

※注: 筆者がガス欠で説明不十分な点があるかと思います:pray:(2020/12/11)
## やったこと
- LINEThingsを用いて、LINEとM5Stack CoreInkを連携
- LIFF上でアンケートに答えてもらったら、値札の表示を変更する

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">M5 CoreInkを電子値札のようにしてみた。 <a href="https://t.co/xr9Qbo7Fq8">pic.twitter.com/xr9Qbo7Fq8</a></p>&mdash; ぱぱでぃーの (@TransFat06g) <a href="https://twitter.com/TransFat06g/status/1337187447080271880?ref_src=twsrc%5Etfw">December 11, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 動作環境

- 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とは

![getting-started-00.804e6cba.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234523/fd7f8fe7-e4ec-4b96-e031-5409fb3fbf8e.jpeg)

> LINE Thingsは、LINEを介して、チャネルとBluetooth® Low Energy対応デバイスを連携し、操作を可能にするIoTプラットフォームです。
https://developers.line.biz/ja/docs/line-things/about-line-things/

## M5Stack CoreInkとは

ざっと説明すると、WifiやBluetooth通信機能を備えた電子ペーパーデバイスです。

<img width="480" alt="PXL_20201201_104424090.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234523/325aa7b2-c7e0-54ac-1dac-12cff160e668.jpeg">
下記のページに詳細な説明があります。よければ、参考にしてください。
- [スイッチサイエンス販売ページ](https://www.switch-science.com/catalog/6735/)

## この記事でいうO2Oマーケティングとは

**オンラインでの施策を、オフラインに反映させ、顧客の購買行動を促進させるマーケティンング手法。**とします。


本当は、参考ページのようなオフライン、オンライン両方で相互誘導できるようなマーケティング手法のことを言うのだと思います。。。（ざっくり認識です。）

**今回の記事で具体例を挙げてみると、服屋さんに来店した顧客に、LINE上で回答できるアンケートを配信する。そのアンケートに回答すると、店舗にある値札が値引きされたり、おすすめのコーディネートなどが表示されたりする。それによって、顧客の購買意欲促進を狙う。（て感じなのかな...）**

参考ページ:
- [O2Oの上位概念？「OMO」をLINEで実践！ LINEプラットフォームを利用するメリットを徹底考察](https://webtan.impress.co.jp/e/2020/04/08/35440)

## フロー図

大体のフローはこんな感じです。
①今回作成するLINEBOTを友達登録している人のLINEと、値札(M5Stack CoreInk)がbluetooth接続される
②アンケートを送信する
③アンケートに回答する
④値札の表示が変更される

![スクリーンショット 2020-12-11 10.14.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234523/cf5a3ffa-6557-e7c4-1993-75c63935fa82.png)

## 手順

### 1. LINEBOTを作成する

適宜、LINEBOTを作成してください。
参考ページ:
- https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-console

### 2. Webhookされるサーバーを作成する

今回はLINEThingsを用いて、デバイスから送信されたメッセージに合わせて、利用者に送信するメッセージを変更する必要があります。ですので、メッセージを受け取り、処理をする部分を担うサーバーを作成します。

**公式SDK**
Messaging API対応のLINE公式SDKはこちらです。下記の中でお好きな言語を選択してください。

- Java
- PHP
- Go
- Perl
- Ruby
- Python
- Node.js

私は、Pythonを用い、サーバー側の処理を作成しました。
[ソースコードはこちら](https://github.com/shaw-papadino/line_things_shop)

今回特筆すべき部分としては、以下の処理で、M5 CoreInkからメッセージを受信した際に実行されるメソッドです。

```main.py
#---略---

###
# 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で作成したサーバーを接続する

ここでは、
参考ページ:
- https://developers.line.biz/ja/docs/messaging-api/building-bot/#setting-webhook-url

### 4. LINEBotでLINEThingsを使えるようにする

下記記事がめちゃくちゃ詳細に記載してくれているので、おすすめです。

参考資料:
- https://qiita.com/hktechno/items/12781e38b09e10c20da2#line-things-%E8%87%AA%E5%8B%95%E9%80%9A%E4%BF%A1%E6%A9%9F%E8%83%BD%E3%82%92%E4%BD%93%E9%A8%93%E3%81%97%E3%82%88%E3%81%86


### 5. M5Stack CoreInkがLINEやサーバーと接続するための処理を追加する

ソースコードが割と長いため、折りたたんでいます。
<details><summary>LINEやサーバーとの接続時に使用するソースコード</summary><div>


```ble.ino
#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();
}
```
</div></details>

### 6. 2で作成したサーバーに、サーバー側からM5CoreInkにwriteする処理を追加

#### M5CoreInkに接続・writeする処理
```write.py
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))

```

#### アンケートに回答したことを受信した際、上記処理を呼び出す
```main.py
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. アンケートフォームを作成する

アンケートフォームを作成します。
[ソースコードはこちら](https://github.com/shaw-papadino/line_things_shop)
<img width="480" alt="スクリーンショット 2020-12-11 9.21.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/234523/9af088b4-33e0-8f66-905e-4b82ca3ced97.png">