ESP32で作るBeebotteダッシュボード

Beebotteは、IoT向けのクラウドサービスで、温度や湿度などをMQTTやRestAPIでプッシュすると、タンキングしてグラフ化してくれたりします。
それに加えて、Pushされるとそれを契機にMQTTで更新があったことを通知してくれます。

Beebotte
　https://beebotte.com/

これをうまく使って、LCD付きのESP32を立ち上げておいて、通知があったら更新値を更新して表示してくれるようなダッシュボードを作ろうと思います。以下が完成イメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/4c9775d1-17d9-9f97-633f-1928ac2f9409.png)

いつもの通り、GitHubに上げておきました。

poruruba/BeebotteDashboard
　https://github.com/poruruba/BeebotteDashboard

必要なものは、LCD付きのESP32と、ダッシュボード上の文字列の表示位置を定義するJsonファイルや背景画像を置くためのWebサーバです。

(2021/2/11 更新)
・画面レイアウトのエディタも作ってみました。ソースは同じGitHubに置いておきました。

#Beebotteアカウント作成

Beebotteのページに行って、右上のSign upからアカウントを作成します。

Beebotte
　https://beebotte.com/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a373e99e-8439-bbad-6a7b-7723a94c1440.png)

次に、適当なチャネルを作成し、次のようにChannelのPropertiesを編集します。とりあえずチャネル名はtestにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/68dad14f-3a83-6574-02c2-d40799717d02.png)

今回は例えば、以下のようなResourceを作ります。

|name|description|type|SoS|
|---|---|---|---|
|temperature|Room temprature|temperature|On|
|humidity|Room humidity|humidity|On|
|items|Qiita items|number|On|
|likes|Qiita likes|number|On|
|followers|Qiita followers|number|On|

temperatureとhumidityという型を使っていますが、浮動小数(number)です。
SoSをOnにしていますが、これは、BeebotteのMQTTに接続したタイミングで、最新値を通知してもらうためのものです。

次に、左側のナビゲーションから「Account Settings」を選択し、タブ「Account Manageming」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/484f01cb-569b-616d-4340-f96f568bcc38.png)

Create new Tokenボタンを押下し、適当なLabel名を入力し、「data:read」のチェックボックスをOnにして、最後にCreate Tokenボタンを押下します。
そうすると、「iamtkn_XXXXXXXXXXXXX」といった感じのIAMトークンが払い出されるので、これを覚えておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a2e808c2-454c-b6c0-b839-48aefc419f87.png)

#Arduinoで使用するライブラリ

以下のライブラリを使います。

lovyan03/LovyanGFX
　https://github.com/lovyan03/LovyanGFX

LCDに背景画像を表示したり、文字列を表示したりします。

knolleary/pubsubclient
　https://github.com/knolleary/pubsubclient

Beebotteからの通知をMQTTで受信します。

ArduinoJson
　https://arduinojson.org/

Beebotteから受信したJsonファイルを解析します。

LovyanGFXはいろいろなLCDに対応しています。ご自身がお持ちのチップに合わせてください。
私は以下を使いました。

http://www.lilygo.cn/prod_view.aspx?TypeId=50032&Id=1157&FId=t3:50032:3
チップはST7789で、解像度は240x320です。

#Arduino環境依存部

以下の部分は、環境に合わせて変更してください。

```c:src/main.cpp
const char* url_background = "【背景画像ファイルのURL】/background.png"; // PNG
const char* url_layout = "【レイアウトファイルのURL】/layout.json";  // Json

const char* wifi_ssid = "【WiFiアクセスポイントのSSID】";
const char* wifi_password = "【WiFiアクセスポイントのパスワード】";

#define MQTT_CLIENT_NAME  "TMusic" // MQTTサーバ接続時のクライアント名
#define MQTT_USER "【IAMトークン】"
```

また、上記のURLに背景画像ファイルとレイアウトファイルを置いてください。
MQTT_CLIENT_NAMEは適当な名前で良いです。
MQTT_USERは、Beebotteで作成したIAMトークンです。

ヘッダファイル"LGFX_Config_TTGO_TMusic.hpp"をインクルードしていますが、利用するLCDのチップに合わせてください。

#Arduino：setup()

まずLCDの準備

```c:src/main.cpp
  lcd.init();
  lcd.setRotation(1);
  lcd.setBrightness(128);
  lcd.setColorDepth(24);
```

日本語フォントを使いたかったので、24ドットのゴシック書体を使いました。

```c:src/main.cpp
#define FONT_NAME   lgfxJapanGothicP_24
#define FONT_SIZE   24.0f

  lcd.setFont(&FONT_NAME);
```

WiFiに接続後、Ntp時刻同期しています。

```c:src/main.cpp
  wifi_connect(wifi_ssid, wifi_password);
  configTzTime("JST-9", "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
```

次に、背景画像ファイルと文字列のレイアウトファイルをHTTP Getでダウンロードしています。

```c:src/main.cpp
const char* url_background = "【背景画像ファイルのURL】/background.png"; // PNG
const char* url_layout = "【レイアウトファイルのURL】/layout.json";  // Json

  background_buffer_length = sizeof(background_buffer);
  ret = doHttpGet(url_background, background_buffer, &background_buffer_length);
  if( ret != 0 )
    Serial.println("doHttpGet Error");

  layout_buffer_length = sizeof(layout_buffer);
  ret = doHttpGet(url_layout, (uint8_t*)layout_buffer, &layout_buffer_length);
  if( ret != 0 )
    Serial.println("doHttpGet Error");
```

背景画像ファイルは、LovyanGFXがPNGまたはJPEGをサポートしていますのでどちらでもよいですが、今回はPNGにしています。

レイアウトファイルは、以下のフォーマットのJsonファイルです。

```
[
　文字列のオブジェクトの配列
]
```

文字列のオブジェクトは以下のフォーマットです。

```
{
  “type”: “date|time|string|number|float|bbt_string|bbt_number|bbt_float”,
  “x”: X座標,
  “y”: Y座標,
  “size”: フォントサイズ,
  “align”: “left|center|rignt”,
  “color”: RGB値,
  “value”: 値,
  “digit”: 浮動小数での小数点以下の桁数,
  “topic”: Beebotteのトピック名,
}
```

typeは表示する値の型です。
　date: 現在の日付を表示します。
　time: 現在の時間を表示します。
　string: valueに示した文字列を表示します。
　number: valueに示した数値を表示します。
　float: valueに示した浮動小数を表示します。
　bbt_string: Beebotteから文字列の通知を受けて表示します。topicの指定が必要です。
　bbt_number: Beebotteから数値の通知を受けて表示します。topicの指定が必要です。
　bbt_float: Beebotteから浮動小数の通知を受けて表示します。topicの指定が必要です。

x, yは、ディスプレイ上のどの位置に表示するかを指定します。
sizeは、フォントサイズです。今回は24ドットのゴシックフォントをベースとしており、それより大きな値を指定した場合には拡大、小さい値を指定した場合は縮小表示されます。
alignは、指定されたx座標に対して、文字列を中央に配置するか、右寄せまたは左寄せで配置するかを指定します。
colorは、表示する文字列のRGB値です。赤の場合は16711680(0xFF0000)、青の場合は255(0x0000FF)です。
valueは、表示する対象の値です。ただし、bbt_*** 場合にはBeebotteから取得するため参照しません。
digitは、浮動小数の小数点以下の桁数です。type=floatまたはbbt_floatのときのみ参照されます。
topicは、type=bbt_*** で参照され、Beebotteから取得する対象のリソースをMQTTのtopic名で指定します。topic名は「チャネル名/リソース名」です。

まずは以下の部分で上記のレイアウトファイルのうち、valueの部分を取得します。

```c:src/main.cpp
  JsonArray array = json_layout.as<JsonArray>();
  for( int i = 0 ; i < array.size() ; i++ ){
    const char* type = array[i]["type"];

    if( strcmp(type, "string") == 0 ){
      const char* value = array[i]["value"];
      strcpy(lines[i], value);
    }else
    if( strcmp(type, "number") == 0 ){
      long value = array[i]["value"];
      sprintf(lines[i], "%ld", value);
    }else
    if( strcmp(type, "float") == 0 ){
      double value = array[i]["value"];
      char format[9] = "%lf";
      if( array[i]["digit"] ){
        int digit = array[i]["digit"];
        sprintf(format, "%%.%dlf", digit);
      }
      sprintf(lines[i], format, value);
    }
  }
```

そして、type=dateまたはtimeのために現在日時を文字列に変換したのち、画像ファイルの表示とレイアウトファイルに指定されたすべての文字列をLCDに表示します。

```c:src/main.cpp
  updateTime();
  draw_lines();
```

以下の部分で、MQTTでBeebotteに接続する準備をします。

```c:src/main.cpp
  // バッファサイズの変更
  client.setBufferSize(MQTT_BUFFER_SIZE);
  // MQTTコールバック関数の設定
  client.setCallback(mqtt_callback);
  // MQTTブローカに接続
  client.setServer(mqtt_server, mqtt_port);
```

#Arduino：loop()

実際のMQTTへの接続処理はloop()の以下の部分です。
Beebotteへの接続完了後、レイアウトファイルに示されているtopicのすべてに対して、Subscribeしています。

```c:src/main.cpp
  client.loop();
  // MQTT未接続の場合、再接続
  while(!client.connected() ){
    Serial.println("Mqtt Reconnecting");
    if( client.connect(MQTT_CLIENT_NAME, MQTT_USER, MQTT_PASSWORD) ){
      // MQTT Subscribe
      JsonArray array = json_layout.as<JsonArray>();
      for( int i = 0 ; i < array.size() ; i++ ){
        if( array[i]["topic"] ){
          const char* topic = array[i]["topic"];
          client.subscribe(topic);
        }
      }
      Serial.println("Mqtt Connected and Subscribing");
      break;
    }
    delay(1000);
  }
```

MQTTでSubscribeによるメッセージが受信されたときには以下が呼び出されるようにしています。

```c:src/main.cpp
void mqtt_callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Mqtt Received: ");
  Serial.println(topic);
  DeserializationError err = deserializeJson(json_subscribe, payload, length);
  if( err ){
    Serial.println("Deserialize error");
    Serial.println(err.c_str());
    return;
  }

  JsonArray array = json_layout.as<JsonArray>();
  for( int i = 0 ; i < array.size() ; i++ ){
    if( !array[i]["topic"])
      continue;

    const char* _topic = array[i]["topic"];
    if( strcmp( _topic, topic) != 0 )
      continue;

    const char* type = array[i]["type"];
    if( strcmp(type, "bbt_string") == 0 ){
      const char* value = json_subscribe["data"];
      strcpy(lines[i], value);
    }else
    if( strcmp(type, "bbt_number") == 0 ){
      long value = json_subscribe["data"];
      sprintf(lines[i], "%ld", value);
    }else
    if( strcmp(type, "bbt_float") == 0 ){
      double value = json_subscribe["data"];
      char format[9] = "%lf";
      if( array[i]["digit"] ){
        int digit = array[i]["digit"];
        sprintf(format, "%%.%dlf", digit);
      }
      sprintf(lines[i], format, value);
    }
  }

  draw_lines();
}
```

レイアウトファイルに記載のトピック名と受信元のトピック名が合致しているものを探し、そこで指定されたtypeに従って文字列にして変数linesに格納しています。

そして、最後に、draw_lines()を呼び出すことで、背景画像をLCDに表示した上に、linesで示された文字列を表示しています。

draw_lines()は以下の感じです。レイアウトファイルの定義に従ってLCDに表示処理しています。

```c:src/main.cpp
void draw_lines(void){
  lcd.drawPng(background_buffer, background_buffer_length, 0, 0);

  JsonArray array = json_layout.as<JsonArray>();
  for( int i = 0 ; i < array.size() ; i++ ){
    const char* type = array[i]["type"];
    int x = array[i]["x"] | 0;
    int y = array[i]["y"] | 0;
    int size = array[i]["size"] | 8;
    long color = array[i]["color"] | 0xffffff;
    const char* align = array[i]["align"] | "left";

    char* str;
    if( strcmp(type, "date") == 0 ){
      str = str_date;
    }else
    if( strcmp(type, "time") == 0 ){
      str = str_time;
    }else{
      str = lines[i];
    }

    lcd.setTextColor(lcd.color565((color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff));
    lcd.setTextSize(size / FONT_SIZE);
    int tsize = lcd.textWidth(str);
    if( strcmp(align, "right") == 0 ){
      lcd.setCursor(x - tsize, y);
    }else
    if( strcmp(align, "center") == 0 ){
      lcd.setCursor(x - tsize / 2, y);
    }else{
      lcd.setCursor(x, y);
    }
    lcd.print(str);
  }
}
```

#Beebotteのデータの更新

Beebotteに格納するデータを更新します。
MQTTやRestAPIのほかに、各言語ごとにライブラリが用意されていますのでそれを使うと楽です。

　https://beebotte.com/docs/mqtt
　https://beebotte.com/docs/restapi
　https://beebotte.com/libs

Tutorialは一読するのが良いです。
　https://beebotte.com/tutorials

上記を使ってBeebotte上のリソースを更新すると、MQTTのSubscribeに更新値が飛んでくるので、それをESP32が受け取って、LCD画面を更新します。
これで、ESP32のBeebotteダッシュボードが完成です。

#レイアウトファイルの例

例を示しておきます。

```json:layout.json
[
	{
		"type": "date",
		"x": 0,
		"y": 0,
		"size": 32,
		"color": 0
	},
	{
		"type": "time",
		"x": 250,
		"y": 0,
		"size": 32,
		"align": "center",
		"color": 0
	},
	{
		"type": "string",
		"x": 0,
		"y": 30,
		"size": 24,
		"value": "温度",
		"color": 255
	},
	{
		"type": "bbt_float",
		"x": 110,
		"y": 30,
		"size": 24,
		"color": 255,
		"align": "right",
		"digit": 1,
		"topic": "test/temperature"
	},
	{
		"type": "string",
		"x": 120,
		"y": 30,
		"size": 24,
		"align": "left",
		"value": "℃",
		"color": 255
	},

	{
		"type": "string",
		"x": 0,
		"y": 60,
		"size": 24,
		"value": "湿度",
		"color": 255
	},
	{
		"type": "bbt_float",
		"x": 110,
		"y": 60,
		"size": 24,
		"color": 255,
		"digit": 1,
		"align": "right",
		"topic": "test/humidity"
	},
	{
		"type": "string",
		"x": 120,
		"y": 60,
		"size": 24,
		"value": "%",
		"align": "left",
		"color": 255
	},

	{
		"type": "string",
		"x": 50,
		"y": 90,
		"size": 16,
		"align": "right",
		"value": "items",
		"color": 16711680
	},
	{
		"type": "bbt_number",
		"x": 55,
		"y": 90,
		"size": 20,
		"align": "left",
		"color": 16711680,
		"topic": "test/items"
	},

	{
		"type": "string",
		"x": 140,
		"y": 90,
		"size": 16,
		"value": "LGTM",
		"align": "right",
		"color": 16711680
	},
	{
		"type": "bbt_number",
		"x": 145,
		"y": 90,
		"size": 20,
		"color": 16711680,
		"align": "left",
		"topic": "test/likes"
	},

	{
		"type": "string",
		"x": 270,
		"y": 90,
		"size": 16,
		"value": "followers",
		"align": "right",
		"color": 16711680
	},
	{
		"type": "bbt_number",
		"x": 275,
		"y": 90,
		"size": 20,
		"color": 16711680,
		"align": "left",
		"topic": "test/followers"
	}
]
```

これを320×240のLCDに表示させるとこんな感じになります。(背景画像は適当なものをお借りしました)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/4c9775d1-17d9-9f97-633f-1928ac2f9409.png)


#終わりに

以下の投稿を参考にしています。

　[ESP32でバイナリファイルのダウンロード・アップロード](https://qiita.com/poruruba/items/82a683866aef872665a4)
　[M5Core2のLCDにWebページのスクリーンショットを表示する](https://qiita.com/poruruba/items/c2f1f5437aa1fe11513c)
　[ATOM Matrixでボタン押下時にMQTT Publish、ただそれだけ](https://qiita.com/poruruba/items/5d9434aec4895f8a6f7a)

なんか楽しくなってきたので、エディタも作りました。
こんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/c3c53b14-4970-d947-7398-776224cdc94e.png)

ソースは、同じGitHubに置いておきました。

以上