ESP32-WROOM-32Eで温湿度データをグラフで表示する

組み込みエンジニア

こんにちは、ENGかぴです。

Webサーバーを実装しクライアントからのレスポンスデータにchart.jsライブラリを組み込むことでデータをグラフ表示することができます。温湿度センサであるSHT35-DISを使って取得したデータをグラフ表示する方法をまとめています。

ESP32-WROOM-32Eの開発環境はArduino IDEを使用しています。またESP32-WROOM-32E開発ボード(秋月電子)及びAE-SHT35(秋月電子)を使用しています。

温湿度データをグラフで表示する

温湿度データの取得方法及びWebサーバーを実装する方法を下記記事にまとめています。

ESP32-WROOM-32EでWebServerを実装する

Chart.jsを使ってブラウザー上にグラフ表示する方法を下記記事まとめています。

Chart.jsを使ってデータをブラウザー上でグラフ表示する

ChartライブラリをEPS32-WROOM-32EのFlashに書き込む

EPS32-WROOM-32Eでグラフ表示するためにはHTMLデータを生成する箇所でChart.jsのライブラリを読み込む必要があります。

Chart.jsのライブラリの参照URLを外部のリンクにする方法はESP-WROOM-32Eとリンクが取れないためESP-WROOM-32E内にライブラリを実装する必要があります。

ESP32-WROOM-32EではSPIFFSライブラリを使用することでFlash領域をデータ領域として使用することができます。SPIFFSはFlashの一部をデータ領域として使用できるようにしてSDカードのようにアクセスできるように構成されています。

ESP32 Sketch Data Uploadを追加する手順
  • 手順1
    SPIFFSのプラグインをダウンロード
  • 手順2
    Arudinoスケッチ保存場所にtoolsフォルダを作成
  • 手順3
    toolsフォルダにダウンロードしたファイルを解凍して移動(コピー)

SPIFFSにファイルを書き込む

SPIFFSプラグインをダウンロードしてArduino IDEのツールに「ESP32 Sketch Data Upload」を追加します。

GitHub-arduino-esp32fs-プラグインのダウンロード

ダウンロードしたファイルをArduinoフォルダに追加します。ダウンロードしたファイルを解凍すると「ESP32FS」フォルダが生成されます。Arduinoのスケッチブックの保存場所に指定しているフォルダ内で「tools」フォルダを作成してtoolsファルダー内にESP32FSをコピー(移動)します。

Arduinoフォルダを保存場所
Arduinoフォルダを保存場所

環境設定でスケッチブックの保存場所を確認することができます。初期のままであればドキュメント内のArduinoフォルダが指定されています。例ではC:\WorkSpace\Arduinoを指定しています。

解凍したファイルを作成したtoolsフォルダに移動
解凍したファイルを作成したtoolsフォルダに移動

toolsフォルダにESP32FSを追加した後でArduino IDEを起動するとツール欄にESP32 Sketch Data Upload」が追加されます。

ESP32 Sketch Data Upload追加後
ESP32 Sketch Data Upload追加後

ESP32 Sketch Data UploadをクリックするとFlashにデータを書き込むことができますがFlashにデータを書き込む前に書き込むファイルを準備する必要があります。Arduinoファイルが保管されているフォルダに「data」フォルダを作成します。

Flashに書き込むファイル
Flashに書き込むファイル

例ではArduinoファイルが「wifi-sht35-webs-chat」フォルダにあります。このフォルダ内に「data」フォルダを作成しChatr.min.jsとファビコンのアイコン(アイコン指定しない場合は不要)を追加しています。ファビコンはページのアイコンですが指定しない場合はデフォルトのアイコンとなります。

「data」フォルダにファイルを追加しツール内のESP32 Sketch Data Uploadをクリックすると書き込みが開始します。

SPIFFSからデータを取得する

#include <SPIFFS.h>
WebServer Wserver(80);

SPIFFS.begin();
Wserver.serveStatic("/myfavicon.ico",SPIFFS,"/myfavicon.ico");
Wserver.serveStatic("/chart.min.js",SPIFFS, "/chart.min.js");

SPIFFSライブラリを使用するためにSPIFFS.hをインクルードします。SPIFFS.begin()でSPIFFSに関する初期化を行います。

WebサーバーライブラリにおいてserveStatic()でFlashに保管したライブラリやファビコンをSPIFFSを使って参照できるようにします。

グラフ表示する用のデータを準備する

float tempdat[CHART_SZ];
float humiddat[CHART_SZ];

void charDataSet(void){

  memmove( &tempdat[1], &tempdat[0],(CHART_SZ-1)*sizeof(float));
  memmove( &humiddat[1], &humiddat[0],(CHART_SZ-1)*sizeof(float));
  tempdat[0] = temp;
  humiddat[0] = humi;
}

Chart.jsで表示するための配列を生成します。SHT35-DISから取得したデータを配列の先頭に新しいデータが更新されるように格納します。

memmove()で配列の先頭から配列の最後尾よりも-1した範囲を配列の1つ分だけずらすことで配列の先頭を空けてから最新のデータを配列の先頭に格納します。

この処理をSHT35-DISから取得したタイミングで繰り返すことで配列の先頭から降順にデータが古くなり配列の先頭には常に最新のデータが格納されます。

HTMLデータの生成

void HtmlSet(void){
  String str = "";

  str += "<head>";
  str += "<link rel='shortcut icon' href='/myfavicon.ico' />"; //ファビコンを指定
  str += "<script src = '/chart.min.js'></script>"; //chart.min.jsを読み込み 
  str += "</head>";

HTMLデータを作成してブラウザに表示します。HTMLデータの<head>部分にファビコンを使用する場合は使用するファイルを指定します。<script>でchart.min.jsを読み込む指定を行います。

  str += "       datasets: [{";
  str += "         label: '温度',";
  str += "         fill: false,";
  str += "         borderColor: 'red',";
  str += "         borderWidth: 1,";
  str += "         pointRadius: 0,";
  str += "         pointHoverBorderWidth: 10,";
  str += "         data: [";
                     for(uint16_t i = 0; i< CHART_SZ; i++ ){
                       str += tempdat[i]; 
                       if(i != CHART_SZ-1 )str += ",";
                     }
  str += "         ]";
  str += "       }, {";

Chart.jsでX軸やY軸のデータを配列名で指定することでグラフの表示ができますが、HTML上の<script>と温湿度情報を格納している変数の関連付けができないため温湿度情報の配列名をそのまま指定することができません。

配列に値を入れて文字列を生成する方法でグラフデータをセットする必要があります。配列のサイズ分for分で値をdata[]に追加してHTMLデータを生成しています。

例)文字列追加の様子
配列の一番目を追加:data[25.03,
配列の二番目を追加:data[25.03,25.05,
配列のN番目を追加:data[25.03,25.05,・・・,25.07,
配列の追加が完了:data[25.03,25.05,・・・,25.07,・・・,25.04]

一文字でも間違うと表示できなくなるためテキストファイルなどでHTML形式で文章を生成しGoogle Chromeで開いて表示できることを確認しておくとよいでしょう。

ESP32のString型は65535バイト分(2バイト)までしか対応していないため文字の増やし過ぎには注意が必要です。

動作確認

動作確認用の回路図
動作確認用の回路図

ESP32-WROOM-32EとSHT35-DISの配線例を示しています。回路図の番号はESP32 -WROOM-32Eの左上を1ピンとした時反時計回りにピンを数えた場合の番号としています。ピン番号横の()内の番号はシルク印刷されているピンの名称です。

温湿度情報のグラフ表示の結果
温湿度情報のグラフ表示の結果

スマホのGoogle ChromeでIPアドレス「192.168.11.2」を入力しリクエストを送るとESP32-WROOM-32Eからスマホに返信し温湿度データと時間経過による温湿度情報がグラフ表示されました。

IPアドレス「192.168.11.2/ti」と存在しないURLを指定するとFile Not Foundが表示されることも確認しています。

スマホで温湿度情報をグラフで表示することは他のセンサーの動作情報を視覚的に確認することができるため応用範囲が広がりそうです。

ソースコード全体

以下のソースコードはコンパイルして動作確認をしております。チェックはしておりますがコメントなど細かな部分で間違っている可能性があります。参考としてお使いいただければと思います。

#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>

#define SLAVE_ADRS 0x45
#define POLYNOMIAL 0x31
#define PIN_DI  22
#define CHART_SZ 600
#define TIME_OFF -1 //タイマーを使用しない場合
#define TIME_UP 0 //タイムアップ
#define BASE_CNT 10 //ベースタイマカウント値
#define TIME_SHT35_MAX 100 //SHT35の計測タイマ  
#define TIME_OUT_MAX 20  //SHT35の通信タイムアウト

typedef enum{
  SHT35_MEASURE = 0,
  SHT35_WAIT,
  SHT35_READ,       
  SHT35_MAX  
}SHT35_MODE;

const char *ssid = "EngKapi1"; //SSID
const char *pass = "22223333"; //password
const IPAddress ip(192,168,11,2); //IPアドレス
const IPAddress subnet(255,255,255,0); //サブネットマスク
  
/* 変数宣言 */
WebServer Wserver(80);
SHT35_MODE mode = SHT35_MEASURE;
uint32_t beforetimCnt = millis();
float temp;
float humi;
float tempdat[CHART_SZ];
float humiddat[CHART_SZ];
int16_t timSht35start;
int16_t  timSht35Out = TIME_OFF;
uint8_t singleshot[2] = { 0x2C, 0x06};
uint8_t chksum[2];
uint8_t rxdata[6];

/* プロトタイプ宣言 */
void mainApp(void);
void Sht35Measure(uint8_t dev_id,uint8_t* cmd, uint16_t len);
bool Sht35GetData(uint8_t dev_id,uint8_t* reg_data, uint16_t len);
uint8_t Crc8Calc(uint8_t *data, uint8_t sz );
void HtmlSet(void);
void handleNotFound(void);
void charDataSet(void);

void setup() {

  Serial.begin(115200);
  Wire.begin();

  WiFi.softAP(ssid, pass); //WiFiのアクセスポイントの設定
  WiFi.softAPConfig(ip, ip, subnet); //アクセスポイントのIP及びサブネットマスク
  SPIFFS.begin();
  Wserver.serveStatic("/myfavicon.ico",SPIFFS,"/myfavicon.ico");
  Wserver.serveStatic("/chart.min.js",SPIFFS, "/chart.min.js");
  
  Wserver.on("/", HTTP_GET, HtmlSet); //URLを指定して処理する関数を指定
  Wserver.onNotFound(handleNotFound); //URLが存在しない場合の処理する関数を指定
  Wserver.begin(); //Webサーバーの開始
}

void loop() {

  mainTimer();
  mainApp();
  Wserver.handleClient();
}
/* タイマ管理 */
void mainTimer(void){

  if ( millis() - beforetimCnt > BASE_CNT ){
    beforetimCnt = millis();

    if( timSht35start > TIME_UP ){
      --timSht35start;
    }
    if( timSht35Out > TIME_UP ){
      --timSht35Out;
    }
  }
}
/* メイン処理関数 */
void mainApp(void){
    
  switch(mode){
    case SHT35_MEASURE:
      if( timSht35start == TIME_UP ){
        timSht35start = TIME_OFF;
        timSht35Out = TIME_OUT_MAX;
        Sht35Measure(SLAVE_ADRS, &singleshot[0], sizeof(singleshot));
        mode = SHT35_WAIT;
      }
      break;
    case SHT35_WAIT:
      if( digitalRead(PIN_DI) == 1){
        mode = SHT35_READ;
      }
      break;
    case SHT35_READ:   
      if( Sht35GetData(SLAVE_ADRS, &rxdata[0], sizeof(rxdata))){
        uint16_t tempHex;
        uint16_t humiHex;

        chksum[0] = Crc8Calc(&rxdata[0],2); //tempのCRCチェック
        chksum[1] = Crc8Calc(&rxdata[3],2); //humiのCRCチェック
            
        if( chksum[0] == rxdata[2] && chksum[1] == rxdata[5]){
          tempHex = ((uint16_t)rxdata[0] << 8) | rxdata[1];
          humiHex =((uint16_t)rxdata[3] << 8) | rxdata[4];
          temp = (tempHex / 65535.00) * 175 - 45;
          humi = (humiHex / 65535.0) * 100.0;

          Serial.print("temp: ");
          Serial.print(temp);
          Serial.print("℃  ");
          Serial.print("humi :");
          Serial.print(humi);
          Serial.print("%  ");
          Serial.println();
          charDataSet();
        }
            
        timSht35start = TIME_SHT35_MAX;
        timSht35Out = TIME_OFF;
        mode = SHT35_MEASURE;
       }
      break;
    }
    
    if( timSht35Out == TIME_UP ){
      timSht35Out = TIME_OFF;
      mode = SHT35_MEASURE;
      timSht35start = TIME_SHT35_MAX;
    }
}
/* SHT35へコマンド送出 */
void Sht35Measure(uint8_t dev_id,uint8_t* cmd, uint16_t len){

  Wire.beginTransmission(dev_id); //スレーブが存在するか確認
  byte error = Wire.endTransmission();
  if( error == 0){ //スレーブが存在する場合下の処理
    Wire.beginTransmission(dev_id);
    for( uint16_t i=0; i < len; i++ ){
      Wire.write(*cmd); //lenサイズ分だけデータを書き込む
      ++cmd;
    }
    Wire.endTransmission(); //ストップ・コンディションの発行
  }
}
/* SHT35からデータを取得 */
bool Sht35GetData(uint8_t dev_id,uint8_t* reg_data, uint16_t len){
  bool  ret = false;

  Wire.beginTransmission(dev_id); //スタート・コンディションの発行
  byte error = Wire.endTransmission();
  if( error == 0){ //スレーブが存在する場合下の処理
    if( Wire.requestFrom(dev_id, len) == len ){
      for( uint16_t i=0; i < len; i++ ){
        *reg_data = Wire.read(); //len分だけデータをリードする
        ++reg_data;
      }
      ret = true;
    }
  }
  return ret;
}
/* CRC8計算関数 */
uint8_t Crc8Calc(uint8_t *data, uint8_t sz ){
  uint8_t crc = 0xFF;
  uint8_t i,j;
    
  for( i = 0; i < sz; i++){
    crc ^= *data;
        
    for( j = 0; j < 8; j++ ){
      if( crc & 0x80 ){
        crc = ( crc << 1 ) ^ POLYNOMIAL;
      }
      else{
        crc = crc << 1;
      }
    }
    ++data;
  }
  return crc;
}
/* 温湿度データをセット*/
void charDataSet(void){

  memmove( &tempdat[1], &tempdat[0],(CHART_SZ-1)*sizeof(float));
  memmove( &humiddat[1], &humiddat[0],(CHART_SZ-1)*sizeof(float));
  tempdat[0] = temp;
  humiddat[0] = humi;
}

/* クライアントに返信するhtmlデータを生成 */
void HtmlSet(void){
  String str = "";

  str += "<html lang=\"ja\">";
  str += "<head>";
  //str += "<meta http-equiv=\"refresh\" content=\"5\">";
  str += "<meta charset=\"UTF-8\">";
  str += "<title>Sensor SHT35</title>";
  str += "<link rel='shortcut icon' href='/myfavicon.ico' />";
  str += "<script src = '/chart.min.js'></script>"; 
  str += "</head>";
  str += "<body>";
  str += "<h1>ESP32-SHT35-DIS温湿度センサ</h1>";
  str += "<h2>WebServerライブラリを使用</h2>";
  str += "<h2>温度: ";
  str += temp;
  str += "℃  ";
  str += "湿度: ";
  str += humi;
  str += "%RH";
  str += "</h2>";
  str += "<div style='height: 600px; width: 800px; margin: auto;'>";
  str += "<canvas id='ChartID'></canvas>";
  str += "</div>";
  str += "<script>";
  str += "var ctx = document.getElementById('ChartID').getContext('2d');";
  str += "var myChart = new Chart(ctx, {";
  str += "   type: 'line',";
  str += "   data: {";
  str += "     labels: [ ";
                 for(uint16_t i = 0; i< CHART_SZ; i++ ){
                   str += i; 
                   if(i != CHART_SZ-1 )str += ",";
                 }   
  str += "     ],";
  str += "       datasets: [{";
  str += "         label: '温度',";
  str += "         fill: false,";
  str += "         borderColor: 'red',";
  str += "         borderWidth: 1,";
  str += "         pointRadius: 0,";
  str += "         pointHoverBorderWidth: 10,";
  str += "         data: [";
                     for(uint16_t i = 0; i< CHART_SZ; i++ ){
                       str += tempdat[i]; 
                       if(i != CHART_SZ-1 )str += ",";
                     }
  str += "         ]";
  str += "       }, {";
  str += "         label: '湿度',";
  str += "         fill: false,";
  str += "         borderColor: 'blue',";
  str += "         borderWidth: 1,";
  str += "         pointRadius: 0,";
  str += "         pointHoverBorderWidth: 10,";
  str += "         pointStyle: 'triangle',";
  str += "         yAxisID: 'y2',";
  str += "         data: [";
                     for(uint16_t i = 0; i< CHART_SZ; i++ ){
                       str += humiddat[i]; 
                       if(i != CHART_SZ-1 )str += ",";
                     }
  str += "         ]";
  str += "       }]";
  str += "     },";
  str += "     options: {";
  str += "       responsive: true,";
  str += "       plugins: {";
  str += "         title: {";
  str += "           display: true,";
  str += "           text: 'SHT35-DISから取得した温度・湿度',";
  str += "           font: {";
  str += "             size: 18,";
  str += "           },";
  str += "         },";
  str += "       },";
  str += "       scales: {";
  str += "         x: {";
  str += "           display: true,";
  str += "           stacked: false,";
  str += "           title: {";
  str += "             display: true,";
  str += "             text: 'サンプル数',";
  str += "             font: {";
  str += "               size: 16,";
  str += "             },";      
  str += "           },";
  str += "         },";
  str += "         y: {";
  str += "           min: -20,";
  str += "           max: 80,";
  str += "           title: {";
  str += "             display: true,";
  str += "             text: '温度[℃]',";
  str += "             color: 'red',";
  str += "             font: {";
  str += "               size: 16,";
  str += "             },";
  str += "           },";
  str += "           ticks:{";
  str += "             display: true,";          
  str += "             color: 'red',";
  str += "           },";
  str += "         },";
  str += "         y2: {";
  str += "           min: 0,";
  str += "           max: 100,";
  str += "           title: {";
  str += "             display: true,";
  str += "             text: '湿度[%]',";
  str += "             color: 'blue',";
  str += "             font: {";
  str += "               size: 16,";
  str += "             },";
  str += "           },";
  str += "           position: 'right',";
  str += "           ticks:{";
  str += "             display: true,";
  str += "             color: 'blue',";
  str += "           },";
  str += "         },";
  str += "       },";
  str += "     },";
  str += "})";
  str += "</script>";
  str += "</body>";
  str += "</html>";

  Wserver.send(200,"text/html", str); 
  //HTTPレスポンス200でhtmlデータとして送信
}
/* URLが存在しない場合の処理 */
void handleNotFound(void) {

  String message = "File Not Found\n\n";
  message += "URI: ";
  message += Wserver.uri();
  message += "\nMethod: ";
  message += (Wserver.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += Wserver.args();
  message += "\n";

  for (uint8_t i = 0; i < Wserver.args(); i++) {
    message += " " + Wserver.argName(i) + ": " + Wserver.arg(i) + "\n";
  }
  Wserver.send(404, "text/plain", message); //テキストファイルであることを示している。
}

本ソースコードでグラフ表示するためにChart.jsライブラリをESP32 Sketch Data Uploadを使ってフラッシュ領域に書き込んでおく必要があります。

関連リンク

Arduinoのライブラリを使って動作確認を行ったことを下記リンクにまとめています。

Arduinoで学べるマイコンのソフト開発と標準ライブラリの使い方

Seeeduino XIAOで学べるソフト開発と標準ライブラリの使い方

ESP32-WROOM-32Eで学べるソフト開発と標準ライブラリの使い方

GEEKJOB-未経験からITエンジニアに【オンライン無料体験】

最後まで、読んでいただきありがとうございました。

タイトルとURLをコピーしました