技術書典7向けに作成した野良バッジをenebularで遠隔操作。

この記事は[enebular Advent Calendar 2019](https://qiita.com/advent-calendar/2019/enebular)の4日目です。

#はじめに
某社内エンジニアコミュニティ [野良HACK](https://qiita.com/yskmjp/items/cedbfd3f3980c42a8771) のむらまさと申します。
先日9/22に開催された[技術書典7](https://techbookfest.org/event/tbf07)にて[光りモノをハックする](https://norahack.booth.pm/items/1565529)という技術本を頒布しました。
![hikarimono_hack_hyoushi.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/e00dc8b2-e472-c008-b2ab-df755bd651d3.png)
この本では光りモノをテーマにした様々な工作とその製作過程をまとめた本で、その中でB1144と呼ばれるLEDネームバッジの解析を行っています。
![B1144_shot.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/f1018cfd-be0f-e271-02f5-37145d212c16.png)

このバッジ、BLE機能を搭載していて、スマホアプリから自由に表示するテキストが設定できるスグレモノなのですが、残念なことに**技適を取得していません。**

なので技適取得済みのBLEモジュールを使用して同じことができるデバイスを作りました。
我々はこれを**野良バッジ**と呼んでます。(バッジと呼べるサイズではないですが…)
今回はプロトタイプ、小サイズで格安のLEDマトリクスを入手できれば徐々に小型化していく予定です。

アプリからB1144にテキストを送る仕組みを簡単に書くとこんな感じ。
![B1144文字表示まで.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/9fab8ea6-005d-423d-7e29-19ab91fc7a48.png)
我々が作った野良バッジはアプリとB1144で行われているBLEの中身を解析して、ほぼほぼ同じ動きができるものですが、問題が一つ。
![問題.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/6f2944ec-ab29-8bd7-55fe-eb000589f0cd.png)
B1144は縦11ドット、野良バッジは縦8ドット、アプリから送ったテキストは欠けて表示されてしまいます、こんな感じに。
![8-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/80e62aa8-4ae3-56f9-76c9-db0b6cc1ba0f.png)

それを解決するためのツールをメンバーが作ったのですが当時は定型文を送れるものだけだったので、どうせならとインターネット経由の遠隔テキスト送信ツールを作ることにしました。
BLEモジュールや上記ツールについては現在BOOTHにて発売中の[光りモノをハックする](https://norahack.booth.pm/items/1565529)をご参照ください。
※書籍版は技術書典当日は大量に在庫を抱えましたが、秋葉原ラジオデパートの[Shigezone](https://shigezone.com/)さんにて委託販売していただいた結果ほぼほぼ完売、手売り分が数部残っている状態です。

##参考にしたページ
色々なキーワードでググった結果、下記のサイトに行き着きました。
[M5StackとFirebaseを使ってenebularからトイレを流してみる](https://qiita.com/yamacho1111/items/b2ef97c02546f7684fec)
[M5StackとFirebaseを使ってenebularとつないでみよう](https://blog.enebular.com/firebase/m5stack-firebase-enebular-collaboration/)
どちらも某コミュニティでよく見かける方達でした(笑)

#変えたところ
参照先ではM5Stackを使用していますが、テキストを表示する野良バッジに向けてテキストを送るツールにLCDがついてるのもアレなので、ESP32を使用することにしました。
Wi-FiとBLEを同時に動かすと何故かESP32がリブートする謎の事象にハマってしまい、その回避策として大幅に改修することにしました。
初回起動時にはWi-Fiで接続、Firebaseで更新があればソフトウェアリセットしてBLEモードで起動し、文字情報をマトリクス化したデータを送信する仕組みです。

##ESP32のソースコード
```c:nora_enefire_esp.ino
#include "BLEDevice.h"
#include <IOXhop_FirebaseESP32.h>
#include <WiFi.h>
#include <string.h>
#include <EEPROM.h>
#include "NoraBLE.h"
#include <rom/rtc.h>
#include <ArduinoJson.h>

// LED表示情報を含むデータを確保数るEEPROMのサイズ
#define EEPROM_SIZE 512

// Wi-Fi,Firebaseの接続情報
// 各々の環境を設定してください。
#define WIFI_SSID             "SSID"
#define WIFI_PASSWORD         "PASSWORD"
#define FIREBASE_DATABASE_URL "FIREBASE-DATABASE-URL"
#define FIREBASE_STREAM       "FIREBASE-STREAM"

// LED表示情報
// EEPROMに保存するLED表示に関する情報
struct NORA_FB_INFO
{
  int mode;           // 再起動前のモード
  int display_speed;  // 表示スピード
  int display_mode;   // 表示モード
  int strlen;         // payload 文字数
  int bytelen;        // payload バイト数
  char payload[256];  // payload ドットマトリクスデータ
};

// FireBaseからの読み込み情報カウンタ
// FireBase接続時に読み込むデータの1回目は更新前のもののため、無視するために使用する。
// 今となってはboolにしておけばよかった。
int FireBaseWriteCount = 0;

// 野良BLEコントラスタ NoraBLE.h 参照
CNoraBLE noraBLE;
// BLE Pelipheralの起動判定フラグ
static bool peripheral_start = false;


// BLEスキャン成功時のコールバック
// ライブラリ参照
class MyAdvertisedDeviceCallbacks: public BLEAdvertisedDeviceCallbacks{
  void onResult(BLEAdvertisedDevice advertisedDevice){
    int num = 0;  
      
    // 野良バッジを検出したらLEDデバイスとして1件クラスに追加する
    if(strcmp(advertisedDevice.getName().c_str(), BLE_DEVICE_NAME) == 0){
      num = noraBLE.AddDevice(advertisedDevice);
      if(num >= DEVICENUM_MAX){
        advertisedDevice.getScan()->stop();
      }
    }
  }
};

// 初期設定
void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("[start setup()]");
  
  // FireBase読み込みカウンタ初期化
  FireBaseWriteCount = 0;

  // EEPROMを準備
  if (!EEPROM.begin(EEPROM_SIZE))
  {
    Serial.println("failed to initialise EEPROM");
    delay(1000);
  }  
  
  // reset理由をチェック
  RESET_REASON r = rtc_get_reset_reason(0);
  Serial.print("CPU0 reset reason = ");
  Serial.println(r);
  delay(100);
  
  // software reset 以外の場合
  if (r != 12)
  {
    Serial.print("Hard Reset");

    // BLE Pelipheralの起動判定フラグはOFF
    peripheral_start = false;
    // Wi-FiでFirabaseに接続
    nora_firebaseConnector();
    
  }else{
    // software reset の場合
    NORA_FB_INFO readEEPdata;
    // EEPROM読み込み
    EEPROM.get<NORA_FB_INFO>(0, readEEPdata);
    //前モードが Wi-Fiの場合
    Serial.print("起動前モード：");
    Serial.println(readEEPdata.mode);
    if(readEEPdata.mode == 0xF0){
      // BLE Pelipheralの起動判定フラグをON
      peripheral_start = true;

      // BLEScan開始
      nora_BLE_Scanner();
            
    // 前モードがBluetoothの場合
    }else{
      peripheral_start = false;
      //Wi-FiでFirabaseに接続
      nora_firebaseConnector();
    }
  }
}

void loop() {
  // EEPROM 読み込み/書き込み 用データ格納領域確保
  NORA_FB_INFO readFireBaseData;
  NORA_FB_INFO writeFireBaseData;

  // BLE Pelipheralの起動判定フラグがONの場合
  if(peripheral_start == true){
    Serial.println("LED Scan End!!\n");
    Serial.println("EEPROM読み込み開始");
    // LED表示情報の読み込み開始
    EEPROM.get<NORA_FB_INFO>(0, readFireBaseData);
    int dspspeed = readFireBaseData.display_speed;
    int dspmode  = readFireBaseData.display_mode;
    int splen    = readFireBaseData.strlen;
    int bplen    = readFireBaseData.bytelen;
    char blesenddata[bplen];
    
    Serial.print("EEPROM:文字列：");

    // Payloadの格納
    for(int loop = 0; loop < bplen; loop++){
      blesenddata[loop] = readFireBaseData.payload[loop];
      Serial.print(blesenddata[loop]);
    }
    Serial.println();
    Serial.print("EEPROM:文字列長：");
    Serial.println(splen);
    Serial.print("EEPROM:文字列バイト数：");
    Serial.println(bplen);
    Serial.print("EEPROM:表示スピード：");
    Serial.println(dspspeed);
    Serial.print("EEPROM:表示モード：");
    Serial.println(dspmode);

    // BLE送信開始
    nora_ble_send(blesenddata, splen, dspspeed, dspmode);

    // EEPROMにモード書き込み(Wi-FI[0xF0] → BLE[0xF1])
    writeFireBaseData.mode = 0xF1;
    EEPROM.put<NORA_FB_INFO>(0, writeFireBaseData);
    EEPROM.commit();

    // 1分待機
    delay(60000);
    
    Serial.println("1分後に再起動");

    // ソフトリセット開始
    softReset();
  }
}

// software reset
void softReset()
{
  Serial.println("Going to software reset now...");
  delay(100);
  ESP.restart();
}

// nora badge sender
void nora_ble_send(char *norastr, int noralen, int noraspeed, int noramode){
  noraBLE.SetSpeed(noraspeed);
  noraBLE.SetMode(noramode);
  noraBLE.SetString(norastr, noralen);
  noraBLE.WriteBLE(0);
}


// firebase connector
void nora_firebaseConnector(){
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  Serial.print("connecting");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println();
        
  // WiFi Connected
  Serial.println("\nWiFi Connected.");
  Serial.println(WiFi.localIP());

  //Firebase接続開始
  Firebase.begin(FIREBASE_DATABASE_URL);
  Firebase.stream( FIREBASE_STREAM, [](FirebaseStream stream) {
    String eventType = stream.getEvent();
    eventType.toLowerCase();
    Serial.print("event: ");
    Serial.println(eventType);
    if (eventType == "put") {
      String path = stream.getPath();
      String data = stream.getDataString();
      Serial.print("data: ");
      Serial.println(stream.getDataString());
      if(FireBaseWriteCount == 0){
        //最初の読み込みは無視する。
        FireBaseWriteCount++;
      }else{
        FireBaseWriteCount = 0;
        //文字列抽出
        StaticJsonBuffer<512> fbgetJsonData;
        JsonObject& reciveJson = fbgetJsonData.parseObject(data);
        int data_len           = reciveJson["len"];
        String payloadStrings  = reciveJson["value"];
        int payloadSpeed       = reciveJson["speed"];
        int payloadMode        = reciveJson["mode"];
        int dataByeLength      = payloadStrings.length() + 1; 
        char sendStringData[dataByeLength];
        payloadStrings.toCharArray(sendStringData, dataByeLength);
        Serial.print("抽出文字列：");
        Serial.println(payloadStrings);
        Serial.print("文字サイズ：");
        Serial.println(data_len);
        Serial.print("文字バイトサイズ：");
        Serial.println(dataByeLength);
        Serial.print("表示スピード：");
        Serial.println(payloadSpeed);
        Serial.print("表示モード：");
        Serial.println(payloadMode);
            
        NORA_FB_INFO writeData;
        writeData.mode          = 0xF0;
        writeData.display_speed = payloadSpeed;
        writeData.display_mode  = payloadMode;
        writeData.strlen        = data_len;
        writeData.bytelen       = dataByeLength;
        for(int wloop = 0; wloop < dataByeLength; wloop++){
          writeData.payload[wloop] = sendStringData[wloop];
        }
        EEPROM.put<NORA_FB_INFO>(0, writeData);
        EEPROM.commit();
        Serial.println("ESP32 SoftReset");
        delay(500);
        softReset();
       }
    }
  });
}

//BLE Periperal scanner
void nora_BLE_Scanner(){
  Serial.println("Start Application...\n Please Wait 5sec\n");
  // BLE init
  BLEDevice::init("");
  // 20秒間Scan
  BLEScan *pBLEScan = BLEDevice::getScan();
  pBLEScan->setAdvertisedDeviceCallbacks(new MyAdvertisedDeviceCallbacks());
  pBLEScan->setActiveScan(true);
  pBLEScan->start(20);  
}
```
ライブラリを含むフルバージョンは[こちら](https://github.com/muramasa2764/ESP32Firebase-BLE)

##enebularのフロー
**危ない！**enebularのAdvent Calendarだったこれ。
ここからが本番です。フローは以下の２つで構成されています。
テキスト入力/送信用のページを表示するフローとFirebaseにテキスト情報を送信するフローです。
![nora-flow01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/936dd265-0d33-e332-3530-20edd185476c.png)
### Web Browser -> enebular
![ene_html.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/adc58087-2b10-90d9-23ab-5a3a41a7978b.png)
テキスト送信フォームはこんな感じ。

```html:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>野良LED String Sender</title>
    <script language="javascript" type="text/javascript">
    function FormSubmit() {
      var target = document.getElementById("form01");
      target.method = "post";
      target.submit();
    }
    </script>
</head>
<body>
    <B>野良HACK LEDバッジにテキスト送れちゃうヤーツ</B> Powerd by enebular<br/>
    <BR/>
    適当なテキストをぶっこんでください。<BR/>    
  <form id="form01" method="post" action="自分のenebular　URL/nora-out">
    <div>表示テキスト<input name="ledvalue" type="text" /></div>
    <span>表示スピード</span>
    <select name="ledspeed" onchange="calc(this)">
        <option value="7">8
        <option value="6" selected>7
        <option value="5">6
        <option value="4">5
        <option value="3">4
        <option value="2">3
        <option value="1">2
        <option value="0">1
    </select>
</br>
    <span>表示方向</span>
    <select name="ledmode" onchange="calc(this)">
        <option value="0" selected>左
        <option value="1">右
    </select>
    <br/>
    <input type="button" value="送信" onclick="FormSubmit()">
  </form>
</body>
</html>
```

こんな感じの送信フォームが表示されます。
このフォームから野良バッジが表示するテキスト、表示スピード、表示方向を設定・入力します。
![nora-site.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/2b1f2862-c961-9add-547e-e9421c6693ed.png)


### enebular -> Firebase
上記の送信フォームで入力した情報がこちらに送信されますので、よしなに編集してやります。
![ene_function.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/113d7d37-3c01-1ba5-af73-fb22f67800db.png)

編集したPayloadを指定したパスにFirebaseに送信します。

![ene_firebase.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115904/6adc301a-2510-d3cb-358b-534f2e9640c3.png)

#一連の動作確認を動画で
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">enebularからESP32を操作して野良バッジにテキストを送る。 <a href="https://t.co/ZJdaJo6pJW">pic.twitter.com/ZJdaJo6pJW</a></p>&mdash; むらまさ@野良HACK (@muramasa2764) <a href="https://twitter.com/muramasa2764/status/1201898094138253312?ref_src=twsrc%5Etfw">December 3, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

以上です。
野良バッジが気になった方は[光りモノをハックする](https://norahack.booth.pm/items/1565529)を読んでみてください。