気の向くままに辿るIT/ICT/IoT
IoT・電子工作

ESP32液晶時計データをTCP通信でブラウザ・Processingにも表示

ホーム前へ次へ
ESP8266って?

ESP32液晶時計データをTCP通信でブラウザ・Processingにも表示

ESP32液晶時計データをTCP通信でブラウザ・Processingにも表示

2018/09/01

 先日作ったESP32とTFT 1.8インチ液晶、DHT11モジュールを使った温度計・湿度計付きデジタル時計のデータをTCPを介してブラウザとProcessingのウィンドウにもリアルタイム表示させてみるページ。

 と言っても、やっつけ仕事(遊び)なので、あしからず。

概要

 ESP32をサーバ、ProcessingをクライアントとしたTCP通信によるデータ送受信であり、シリアル通信ではないのでWiFi環境さえあれば、ESP32とパソコン間は、無線化できます。

 可変のIP指定は非現実的であり、Processingは、Avahi(Bonjour)をインストール可能なパソコン上にあるのでmDNSを使ってホスト名.local(今回は、esp32clock.local)で常にサーバにアクセスできるようにしました。

 ちなみにESP32とProcessing何れをクラサバにしてもTCP通信は可だし、パソコン側をサーバとした場合でもOS上のホスト名.localで同様にアクセスできます。

 今回は、パケット到達が保証されるTCP通信ですが、パケットの到達には無関心な一方、高速でリアルタイム向きなUDP通信でもできるでしょう。

前提

 ここでは、Arduino IDEを使うのでArduino IDEの[ツール] => [ボード]から[espressif/arduino-esp32]を選択、ESPにスケッチをアップロードできる状態であること。

 Arduino IDE 1,8,6で追加された[ツール] => [ライブラリを管理...]メニューか、従来の[スケッチ] => [ライブラリをインクルード...] => [ライブラリを管理...]メニューを辿ってライブラリ管理画面を開き、TFT 1.8液晶用に[Adafruit GFX Library]、[Adafruit ST7735 and ST7789 Library]、DHT11/DHT22センサーモジュール用に[DHT sensor library for ESPx]を検索、インストールしておくこと。

 尚、ESP32でmDNSを使う場合、ESPmDNSヘッダファイルをincludeする(ESP-01やESP-12などでは、ESP8266mDNS)。

 Processingをインストール、使える状態にあること。

 相応のブラウザが使える状態にあること。

 参考までに自身の使用しているOSは、Debian(Linux)、ブラウザは、Mozilla Firefox、Arduino IDEのバージョンは、1.8.6(1.8.6を素直に起動できない場合、起動方法参照)。

必要なモノ

 今回は、WiFiモジュールの載ったESP32の開発ボードを使いましたが、技適はさておき、GPIOピンの数は足りるはずなのでESP12あたりでもできるでしょう。

必要な電子部品類

 ここでは、ESP32開発ボードを使いましたが、ESP-WROOM-32モジュール単体の場合には、ピンホールが半円だし、ブレッドボードとピッチも合わないため、別途、市販のブレイクアウトボードを買うか、自作でごにょごにょする必要があるでしょう。

 尚、ESP32は、入力電圧定格3.3V、今回使ったTFT1.8インチもDHT11センサーも3.3Vでいけました。

ESP32開発ボードへのスケッチのアップロード

 ESP32開発ボードの内、DOIT製DevKit V1及び互換ボードをArduino IDEで使う場合、スケッチのアップロード時、書き込む際にボード上のRESET/RST押したまま、BOOTボタンを押し、RSTを放す、もしくは、BOOTボタンのみを押して放す必要がありましたが、Arduino IDE 1.8.6では、その必要がなくなったようです。

 少なくとも後者の操作が必要だった自身のボードにおいては、この必要がなくなったことを確認済み。

 よってArduino IDE 1.8.6を使っている場合、特記すべきことはありません。

回路

ESP32TFT1.8DHT11
19MISO-
5CS-
18SCL-
23SDA-
17A0-
(16)(RESET)-
4-DATA
3.3VVCC
LED+-
GNDGND
LED--

 配線は、目的は違えど流れ着いたMhageGH/esp32_ST7735_Movieを参考にさせて頂きましたが、スケッチと回路を一致させれば、特殊なピンを避ければ、なお面倒がありませんが、どのピンでも問題ないでしょう。

 今回、ESP32開発ボードには、GPIOが38ピンあるEspressif DevKitC V4と同じっぽい一方、4隅の穴からDOIT DevKit風にも見えるものを使いましたが、30ピンのDOIT DevKit V1や互換機には、GPIO17はないようなので他の空いたピンを使い、スケッチにも反映させればよいでしょう。

 注意すべきは、このTFT 1.8(KMR-1.8 SPI)は、なぜか、SPIというかI2Cっぽく、ケーブル2本は、スケッチとは別の配線をする必要がある点。(ちなみにArduinoでは同様に4本別の配線を要した)

 具体的には、スケッチ上では、KMR-1.8液晶のSCLK/MOSIに接続することになっているESP32のピン(今回は18/23)を回路上では、SCL/SDAに配線する必要があります。

 そうしないと液晶に何も表示されず戸惑うことになります。(バックライトは点くし、もにょもにょ何やら映そうとはしますが、結果何も表示されない。)

 ちなみに、このTFT液晶、購入当時、そうとは知らず、返送不要で返金してもらったものの、後で使えることがわかり、再支払いした経緯があります。

ESP32側スケッチ

// Server ESP32/ESP-WROOM-32
#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiClient.h>
#include <time.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7735.h>
#include <Adafruit_ST7789.h>
#include <SPI.h>
#include <DHTesp.h>
 
DHTesp DHT;
 
#define DHT11_PIN 4
 
// You can use any (4 or) 5 pins
#define sclk 18  // SCL
#define mosi 23  // SDA
#define cs 5   // CS
#define dc 17   // A0
#define rst 16 // you can also connect this to the Arduino reset
 
Adafruit_ST7735 tft = Adafruit_ST7735(cs, dc, rst); 
 
// WiFi Setting
#define WIFI_SSID  "SSID"
#define WIFI_PASSWORD  "PASSWORD"
#define JST   3600*9
 
// TCP server at port 80 will respond to HTTP requests
WiFiServer server(80);
 
void setup(void) {
 Serial.begin(115200);
 
 // Use this initializer if you're using a 1.8" TFT
 tft.initR(INITR_BLACKTAB);  // initialize a ST7735S chip, black tab
 
 Serial.println("init");
 
 uint16_t time = millis();
 tft.fillScreen(ST77XX_BLACK);
 time = millis() - time;
 
 Serial.println(time, DEC);
 delay(500);
 
 Serial.println("done");
 delay(1000);
 
 // WiFi starting
 Serial.println("WiFi connecting...");
 WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
 while(WiFi.status() != WL_CONNECTED) {
  Serial.print('.');
  delay(500);
 }
 Serial.println();
 Serial.printf("Connected, IP address: ");
 Serial.println(WiFi.localIP());
 Serial.println("WiFi connected!");
 
 // print the received signal strength:
 long rssi = WiFi.RSSI();
 Serial.print("signal strength (RSSI):");
 Serial.print(rssi);
 Serial.println(" dBm");
 
 // Set up mDNS responder:
 // - first argument is the domain name, in this example
 //  the fully-qualified domain name is "esp8266.local"
 // - second argument is the IP address to advertise
 //  we send our IP address on the WiFi network
 if (!MDNS.begin("esp32clock")) {
  Serial.println("Error setting up MDNS responder!");
  while(1) {
    delay(1000);
  }
 }
 Serial.println("mDNS responder started");
 
 // Start TCP (HTTP) server
 server.begin();
 Serial.println("TCP server started");
 
 // Add service to MDNS-SD
 MDNS.addService("http", "tcp", 80);
 
 // NTP start
// configTime( JST, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
 configTime( 9 * 3600L, 0, "ntp.nict.jp", "ntp.jst.mfeed.ad.jp");
 DHT.setup(DHT11_PIN, DHTesp::DHT11);
 delay(1000);
}
 
void loop() {
 time_t t;
 struct tm *tm;
 static const char *wd[7] = {"Sun","Mon","Tue","Wed","Thr","Fri","Sat"};
 char rdate[30], rday[30], rtime[30], rsec[30];
 
 t = time(NULL);
 tm = localtime(&t);
 
 Serial.printf(" %04d/%02d/%02d(%s) %02d:%02d:%02d\n",
    tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday,
    wd[tm->tm_wday],
    tm->tm_hour, tm->tm_min, tm->tm_sec);
 sprintf(rdate, "%04d/%02d/%02d",
    tm->tm_year+1900, tm->tm_mon+1, tm->tm_mday);
 sprintf(rday, " (%s)",
    wd[tm->tm_wday]);
 sprintf(rtime, " %02d:%02d",
    tm->tm_hour, tm->tm_min);
 sprintf(rsec, ":%02d",
    tm->tm_sec);
 delay(1000 - millis()%1000);
 
 float humidity = DHT.getHumidity();
 float temperature = DHT.getTemperature();
 
 Serial.print(DHT.getStatusString());
 Serial.print("\t");
 Serial.print(humidity, 1);
 Serial.print("\t\t");
 Serial.print(temperature, 1);
 Serial.print("\t\t");
 Serial.print(DHT.toFahrenheit(temperature), 1);
 Serial.print("\t\t");
 Serial.print(DHT.computeHeatIndex(temperature, humidity, false), 1);
 Serial.print("\t\t");
 Serial.println(DHT.computeHeatIndex(DHT.toFahrenheit(temperature), humidity, true), 1);
 
 Serial.print("Date : ");
 Serial.print(rdate);
 Serial.println(rday);
 Serial.print("Time : ");
 Serial.print(rtime);
 Serial.println(rsec);
 
 tft.setCursor(0, 20);
 tft.fillScreen(ST77XX_BLACK);
 tft.setTextColor(ST77XX_WHITE);
 
 tft.setTextSize(2);
 tft.println(rdate);
 tft.setCursor(0, 40);
 tft.println(rday);
 tft.setCursor(0, 70);
 
 tft.setTextSize(3);
 tft.print(rtime);
 tft.setTextSize(1);
 tft.println(rsec);
 tft.setCursor(0, 110);
 
 tft.setTextSize(2);
 tft.print(" ");
 tft.print(temperature, 1);
 tft.setTextSize(1);
 tft.println(" C");
 
 tft.setCursor(0, 130);
 tft.setTextSize(2);
 tft.print(" ");
 tft.print(humidity, 1);
 tft.setTextSize(1);
 tft.println(" %");
 
 // listen for incoming clients
 WiFiClient client = server.available();
 if (client) {
  Serial.println("new client");
  // an http request ends with a blank line
  boolean currentLineIsBlank = true;
  while (client.connected()) {
   if (client.available()) {
    char c = client.read();
    Serial.write(c);
    // if you've gotten to the end of the line (received a newline
    // character) and the line is blank, the http request has ended,
    // so you can send a reply
    if (c == '\n' && currentLineIsBlank) {
     // send a standard http response header
     client.println("HTTP/1.1 200 OK");
     client.println("Content-Type: text/html");
     client.println("Connection: close"); // the connection will be closed after completion of the response
     client.println("Refresh: 5"); // refresh the page automatically every 5 sec
     client.println();
     client.println("<!DOCTYPE HTML>");
     client.println("<html>");
 
     client.print("TARGET : ");
     client.print(rdate);
     client.print(rday);
     client.print(" ");
     client.print(rtime);
     client.print(rsec);
     client.print(" ");
     client.print(temperature, 1);
     client.print(" C");
     client.print(" ");
     client.print(humidity, 1);
     client.println(" %");
     client.println("<br />");
 
     client.println("</html>");
     break;
    }
    if (c == '\n') {
     // you're starting a new line
     currentLineIsBlank = true;
    } else if (c != '\r') {
     // you've gotten a character on the current line
     currentLineIsBlank = false;
    }
   }
  }
  // give the web browser time to receive the data
  delay(1);
 
  // close the connection:
  client.stop();
  Serial.println("client disonnected");
 }
}

 今回、サーバとしたESP32のスケッチは、これ。

 そのまま使えますが、少なくとも自身で利用可能なWiFiルーター、アクセスポイント用にSSID/PASSWORDは書き換える必要があります(WiFi接続できないと時計も表示されない)。

[2020/04/27]

 あれ、WiFi接続とNTPクライアントの実験によるとNTPのconfigTime()の第一引数が違ったらしい...エラーも出ずに使えてるのに...他で試そうとしたら、コンパイルエラーに。

 正しくは、[9 * 3600L](時差9h x 3600sec)のようで間違えた第一引数JSTは、configTzTime()のものらしい...しかもクォートもいるっぽい。

Processing側スケッチ

// Client Processing
import processing.net.*;
 
String serverAddress = "esp32clock.local";
int port = 80;
 
Client c;
 
String[] arraySTR;
String s;
 
void setup()
{
 size(500, 200);
 textFont(createFont("SanSerif", 16));
}
 
void draw()
{
 c = new Client(this, serverAddress, port);
 c.write("GET / HTTP/1.1\n\n"); // Use the HTTP "GET" command to ask for a webpage
}
 
void clientEvent(Client c) {
 s = c.readString();
 if (s != null) {
  arraySTR = s.split("\n");
  for(int i = 0;i < arraySTR.length; i++){
   if (arraySTR[i].contains("TARGET")) {
    println(arraySTR[i]);
    background(0);
    text(arraySTR[i], 15, 40);
   }
  }
 }
 else { println("That is " + s); }
}
 

 今回、クライアントとしたProcessingのスケッチは、これ。

 ただ、トライ&エラーの結果であって機能はするも書き方が、これで正しいのか、自信はありません。

実行

ESP32温湿度計付き液晶時計データをブラウザ・Processingで表示
  1. 社内、宅内無線LANルーターまたは、アクセスポイントにアクセスできるよう準備・確認
  2. ESP32とProcessingそれぞれにスケッチをアップロード
  3. サーバとしたESP32をシリアルモニタ、もしくはTFT液晶表示などで動作を確認
  4. Processingのスケッチを実行
  5. ブラウザのURL入力欄にスケッチそのままならesp32clock.localと入れてEnter

のようにすれば、TFT液晶、Processingのウィンドウ、ブラウザ上に日付、時刻、温度、湿度が表示されるはず。

 リセット・再起動なく進めるためには、ルーター、サーバ(今回はESP32)、クライアント(今回はProcessingやブラウザ)の順番は重要。

 もし、うまく表示されない場合は、ESP32ボード上のRST/RESETボタンを押下、それでも表示されない場合は、配線を見直し、Processingについては、再起動してみるなりします。

 ただし、ブラウザ表示は、無理矢理、5秒毎にリフレッシュしている為、短時間で通信が切断されることがままある、同時にProcessingウィンドウを開いていた場合、これもハングアップします。

 ただ、Processingについては、Processing 3.4にしてから、別件でもフリーズしたり、保存するか否かで挙動が異なったり、スケッチだけでなく、Processingごと再起動する必要があったりするため、Processingのバージョンによる可能性もあります。

 Arduino IDEのシリアルモニタ、Processingのポップアップウィンドウとコンソール、そしてブラウザ、それぞれの表示上の1秒程度の微妙なズレは気にしない。

$ cat auto_processing_sketch.sh
# Processingをインストール済みの場合
processing-java --sketch=/path/to/Sketch_Directory --run
# Processingを展開のみでインストールしていない場合
#/path/to/processing-java --sketch=/path/to/Sketch_Directory --run
$ sh auto_processing_sketch.sh

 スクリプトからProcessingのスケッチを自動実行させることもできます。

 端末から実行させるとProcessingのポップアップウィンドウだけでなく、端末上にもテキストベースの通信結果が表示されます。(ちなみに、この時、もちろん、Processing IDEは起動しないというか表示されない。)

 OSごとに相応の設定をすれば、ラズパイなどのマイコンやパソコン起動時にProcessingのスケッチを自動実行させることもできるでしょう。

ESP32で温度計・湿度計付き時計

 TFT液晶表示した写真(は、時計を作った時の画像を流用している為、当然、日付時刻は異なる)。

 今回は、このようにTFT液晶に表示をしつつ、このデータを元にブラウザ上にも、Processingのウィンドウ上にも日付時刻、温度、湿度を表示してみた、また、無線LAN経由なのでWiFi機能搭載のESP32時計は、電波が届く範囲なら、どこにあってもよいという話。

 とは言え、冒頭書いたように、やっつけなのでProcessingやブラウザへの表示については、実用には程遠いですが...。

既知の不具合・現象

 本来、ブラウザでリアルタイム表示させる場合、AjaxやWebSocket、WebSocket他の手法を内包したnode.jsベースのSocket.ioなどを使うのが正攻法なのでしょうが、覚える気力はなく、横着してHTTPプロトコルのrefreshを使うに留めたため、前述の通り、重すぎてブラウザ上は途中、通信が遮断されます。

 この時、Processingも巻き添えになるように見受けられますが、液晶+Processingのみなら落ちることはなさ気。

 リアルタイムと言いつつ、ブラウザ上では5秒ごとにリフレッシュしています(リフレッシュで毎秒は厳しすぎます)が、実際のところ、再表示まで約11秒かかっています。

 また、リアルタイムと言っても先のスクリーンショットのようにESP32、ブラウザ、Processing間で若干タイムラグがあり、表示が不一致となることがままあります。(Processingのコンソール上はそんなことないようですが、ポップアップウィンドウ上は表示が間に合わないかのように秒飛びすることがあります。)

 Processingのウィンドウ上において、なぜか、行末方向の情報が任意の文字数欠落することがありますが、ウィンドウだけでなく、コンソールもなので受信及び抽出・表示が1秒では間に合っていない可能性も。(Processingとブラウザ上の表示データは、ESP32(サーバ)側で成形したものをそのまま表示しているだけ。)

 これも前述の通り、Processingのバージョンに依存するのか、正しい挙動を得るために実行を止め、スケッチを保存し、再実行、時には、スケッチだけでなく、Processingごと再起動を要することもあります。(再起動しないと[最近開いたファイル]メニューが消えていることもあります。)

 TFT液晶表示だけの時はなかったような気もしますが、そもそも当然、あり得ることなのか、入力電圧が不足することがあるのか、配線の接触によるのか、たまに温度、湿度の値が、[nan]になることがあります。

 時にブラウザは、ESP32をリセットしないとアクセスできないことがあります。

 Processingに至っては、リセットしてもUnknownHostExceptionやNullPointerExceptionなどとなって機能しなくなることがあり、原因は、ホスト名.localにアクセスできないこと、pingしても「名前またはサービスが不明」となるし、avahiデーモンを再起動してもダメ...ただ、ESP32側の電源を入れ直すと回復する...これは、avahiの入ったパソコンの電源をOFFにし、その後、ONにしたからかもと思って再起動してみましたが、ちゃんと機能する...OFFからONまでが短時間と長時間で違うのか...も?

実用可能性

 数日経って、できた時計を眺めながら、ふと、できるよね?という思いがよぎり、確認するだけしてみるか...程度の動機だったこともあり、一応できるにはできたという時点で...というか、いまのところ、これ以上、手を入れる気もなく、中途半端な状態で公開するに至りましたが、ちゃんと実装すれば、意外と使えるかも。

 今回の場合なら、日付、時刻、温度、湿度を、TFT液晶の前にいなくても無線LAN+スマホ、タブレット、パソコンのブラウザ経由で確認できるわけですが、日付時刻はともかく、どこか特定の場所の温湿度を確認したい場合には、温度・湿度が無線で遠隔でわかるというのはよいかも。

 例えば、天気予報APIからの情報とか、GPS位置情報とか、他のテキストデータでもいいわけだし、ESP32側にカメラでもあれば、ブラウザ上に画像や映像表示、ドアフォンや見守り・監視カメラも無線で遠隔で確認できたり...既にそんな商品もあるけど、この仕組みでできそうな気も。

省電力化・スリープモード

 https://www.espressif.com/sites/default/files/documentation/9b-esp8266-low_power_solutions_en.pdf => ESP8266 Low-Power Solutions(...パスは同じ、ファイル名末尾のsolutionsとen.pdfの間のアンスコが1つ増えただけ...)によれば、ESP8266/ESP-WROOM-02/ESP-WROOM-32には、3種類のスリープモードがありますが、電池駆動を考えている為、最も省電力な数μAのDeep Sleepモードを使いたいところですが、未実装。

 ただ...、ACアダプタ付きUSB充電器やパソコンにUSB接続した場合には、機能するのですが、電池駆動の場合、プラス/マイナスをESP32開発ボードの5V/GNDピンに接続すればよいのですが、単3電池x2(eneloop)昇圧+USBモバイルバッテリでも、単3電池x4(eneloop)+電池ボックスでもWiFi用には、電流が不足するようで無負荷で入力電圧5V以上はありますが、接続すると2.6V台あたりまで電圧降下、液晶上にも表示しようという努力は認められますが、表示しきれない...。

 スリープ以前に電池駆動にできるのだろうか...?できなければ、NTPなしでRTCモジュール併用?それか、コンセント接続前提?

 ESP8266の動作が不安定なときの対策案は、電池駆動においても解決策になり得るか?

 いや、それはそれとして乾電池で連続340日間動作するESP8266搭載ワイヤレスセンサESP32 WiFi 温湿度計の長期間動作が、電池駆動を含めた解決策なのか?

 ただ、データロガーではなく、モニタ付き温湿度計付き時計なのでどこまで取り入れられるかは未知数ですが、中でもULP、これを使った場合、時計も秒を出力せず、温湿度も1分ごとに計測、Deep Sleepから復帰、表示、Deep Sleep...なんてこともできたりするのだとすれば、1筋の光明が...。

ホーム前へ次へ