この記事はenebular Advent Calendar 2019の4日目です。
#はじめに
某社内エンジニアコミュニティ 野良HACK のむらまさと申します。
先日9/22に開催された技術書典7にて光りモノをハックするという技術本を頒布しました。
この本では光りモノをテーマにした様々な工作とその製作過程をまとめた本で、その中でB1144と呼ばれるLEDネームバッジの解析を行っています。
このバッジ、BLE機能を搭載していて、スマホアプリから自由に表示するテキストが設定できるスグレモノなのですが、残念なことに技適を取得していません。
なので技適取得済みのBLEモジュールを使用して同じことができるデバイスを作りました。
我々はこれを野良バッジと呼んでます。(バッジと呼べるサイズではないですが…)
今回はプロトタイプ、小サイズで格安のLEDマトリクスを入手できれば徐々に小型化していく予定です。
アプリからB1144にテキストを送る仕組みを簡単に書くとこんな感じ。
我々が作った野良バッジはアプリとB1144で行われているBLEの中身を解析して、ほぼほぼ同じ動きができるものですが、問題が一つ。
B1144は縦11ドット、野良バッジは縦8ドット、アプリから送ったテキストは欠けて表示されてしまいます、こんな感じに。
それを解決するためのツールをメンバーが作ったのですが当時は定型文を送れるものだけだったので、どうせならとインターネット経由の遠隔テキスト送信ツールを作ることにしました。
BLEモジュールや上記ツールについては現在BOOTHにて発売中の光りモノをハックするをご参照ください。
※書籍版は技術書典当日は大量に在庫を抱えましたが、秋葉原ラジオデパートのShigezoneさんにて委託販売していただいた結果ほぼほぼ完売、手売り分が数部残っている状態です。
##参考にしたページ
色々なキーワードでググった結果、下記のサイトに行き着きました。
M5StackとFirebaseを使ってenebularからトイレを流してみる
M5StackとFirebaseを使ってenebularとつないでみよう
どちらも某コミュニティでよく見かける方達でした(笑)
#変えたところ
参照先ではM5Stackを使用していますが、テキストを表示する野良バッジに向けてテキストを送るツールにLCDがついてるのもアレなので、ESP32を使用することにしました。
Wi-FiとBLEを同時に動かすと何故かESP32がリブートする謎の事象にハマってしまい、その回避策として大幅に改修することにしました。
初回起動時にはWi-Fiで接続、Firebaseで更新があればソフトウェアリセットしてBLEモードで起動し、文字情報をマトリクス化したデータを送信する仕組みです。
##ESP32のソースコード
#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);
}
ライブラリを含むフルバージョンはこちら
##enebularのフロー
**危ない!**enebularのAdvent Calendarだったこれ。
ここからが本番です。フローは以下の2つで構成されています。
テキスト入力/送信用のページを表示するフローとFirebaseにテキスト情報を送信するフローです。
Web Browser -> enebular
<!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>
こんな感じの送信フォームが表示されます。
このフォームから野良バッジが表示するテキスト、表示スピード、表示方向を設定・入力します。
enebular -> Firebase
上記の送信フォームで入力した情報がこちらに送信されますので、よしなに編集してやります。
編集したPayloadを指定したパスにFirebaseに送信します。
#一連の動作確認を動画で
enebularからESP32を操作して野良バッジにテキストを送る。 pic.twitter.com/ZJdaJo6pJW
— むらまさ@野良HACK (@muramasa2764) December 3, 2019
以上です。
野良バッジが気になった方は光りモノをハックするを読んでみてください。