PR

ESP32-WROOM-32Eで履歴データをダウンロードする

組み込みエンジニア
本記事はプロモーションが含まれています。

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

Webサーバーを実装することでクライアントからに対してダウンロード要求のレスポンスを返すことができます。SDカードに保管した温湿度センサーの履歴をブラウザー上に表示したダウンロードリンクを使って履歴データを取得する方法をまとめています。

下記記事で動作確認した温湿度センサーのグラフ表示にダウンロードリンクを追加して動作確認を行います。

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

本記事ではダウンロードした履歴とSDカードの履歴が一致するかを確認します。

ESP32-WROOM-32E開発ボード(秋月電子)を使用しArduino IDEで開発を行います。また温湿度センサー:AE-SHT35(秋月電子)、SDカードモジュール:AE-microSD-LLCNV(秋月電子)を使用しています。

ESP32-WROOM-32Eを使って動作確認したことをまとめています。

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

ダウンロードリンクを実装する

Webサーバーを使ってクライアントからの接続要求を受け付けます。接続要求で表示するHTMLのページにダウンロードリンクを実装しSDカード内に保存した履歴をダウンロードできるようにします。Webサーバーを実装する方法を下記記事にまとめています。

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

ダウンロードリンクを実装する方法はポイントは以下の通りです。

  1. WebServerでSDカードのデータを参照する
  2. HTMLデータ内の<a>タグにdownload属性を追加する

PR:技術系の通信教育講座ならJTEX

ESP32-WROOM-32Eと各種モジュールの構成

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

ESP32-WROOM-32Eと各種モジュールの構成を示しています。回路図の番号はESP32 -WROOM-32Eの左上を1ピンとした時反時計回りにピンを数えた場合の番号としています。ピン番号横の()内の番号はシルク印刷されているピンの名称です。

SHT35モジュールはWire(I2C)通信を使用します。Wireを使用する場合はプルアップ抵抗が必要ですがSHT35モジュールに実装されているため必要ありません。

SDカードモジュールの電源は5VなのでESP32-WROOM-32Eの5Vを供給しています。SDカードの操作はSPI通信を使用します。ESP32-WROOM-32EはVSPIとHSPIがありますが、特に指定しない場合VSPIピンが有効になります。HSPIを使用する場合は下記記事に使用例をまとめていますので参考にしてください。

ESP32-WROOM-32EのSPIでBME280の情報を取得

SDカードにSHT35から取得した温湿度データの履歴を保存します。ブラウザー上で温湿度データをグラフで表示しますが温湿度データの履歴をダウンロードできるようにリンクを設置します。

WebServerでSDカードのデータを参照する

#include <WebServer.h>
#include <SD.h>

String filepath = "/sht35.txt"; //SDカードのファイル
WebServer Wserver(80);
//SDカードを参照する場合
Wserver.serveStatic(filepath.c_str(),SD, filepath.c_str());
//フラッシュを参照する場合
Wserver.serveStatic(filepath.c_str(),SPIFFS, filepath.c_str());

WebServerクラスのserverStatic()関数でSDカードに保管したデータを参照できるようにします。

serverStatic()関数の第1引数はファイルを表示するURI(URL)を指定します。第2引数はFSクラス(ファイルの操作を定義したもの)で宣言されたファイルの種別を指定します。今回はSDカードを参照するのでSDを指定します。第3引数はファイルのパスを指定します。

第2引数にSPIFFSを指定するとフラッシュを参照することができるのでフラッシュないのデータをダウンロードさせることもできます。

フラッシュは書き込み回数に制限があるため頻繁な書き込みには向かないため更新頻度の高い履歴をダウンロードさせる場合の用途には向きません。

スポンサーリンク

HTMLデータの生成

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

  str += "<body>";
  str += "<a href='sht35.txt' download='sht35.txt'>履歴をダウンロード</a>";
  str += "</body>";
}

HTMLデータを作成してブラウザに表示します。ダウンロードリンクは<a>タグで作成します。<a>タグでは属性を指定しない場合はhrefで指定したページに遷移します。<a>タグにdownload属性を追加することで指定したURLにリンクされたファイルがダウンロードできるようになります。

download属性を追加しない場合は普通のリンクとなりファイルの内容を表示するページに遷移してしまいます。

SDライブラリの準備と初期化

#include <SPI.h>
#include <SD.h>

File myfile; //SDカードの状態を格納

void setup() {

  if(!SD.begin()){
    //初期化失敗の処理
  }
}

SDカードの操作はSPI通信を使用するためSD.hとSPI.hの2つのライブラリをインクルードする必要があります。SPIのスレーブ選択のDOを指定する必要がありますがデフォルトで5ピンが指定されています。

SDカードのファイルに関する情報等を管理するにFile型のクラス変数を変数を宣言します。例ではmyfileの宣言しています。

SDライブラリのbegin()関数を使用してSDカードに関する情報を初期化します。引数にはSDカードを選択するためのスレーブセレクト(SS)のピン番号を指定します。指定しない場合はデフォルトの5ピンで初期化が行われます。

SDカードが挿入されていない場合など失敗した場合は戻り値がfalseになるので必要があれば失敗したときの処理を入れます。

広告

SDに履歴を追加する

myfile = SD.open(filepath,FILE_APPEND);//追加で書き込む"a"を指定

if( myfile ){ //ファイルが開けたら書き込む
  myfile.print(no);
  myfile.print(",");
  myfile.print(temp);
  myfile.print(",");
  myfile.println(humid);
  myfile.close(); //ファイルを閉じる
  ++no;
}
else{
  Serial.println("NG");
}

ESP32シリーズではSDカードの書き込みのパターンを使い分ける必要があります。ファイルを開いてデータを更新する場合はFILE_WRITE(“w”)を指定し、ファイルを開いてデータを追加する場合はFILE_APPEND(“a”)を指定します。

ESP32シリーズ以外のArduino環境のSDカードライブラリではWRITEとAPPENDの区別はなくWRITEを指定するとデータの追加になります。一部仕様が異なるため注意が必要です。

例ではFILE_APPENDでファイルを開いて履歴の番号、温度、湿度の情報をSDカードに書き込んでいます。それぞれのデータの間に”,”を入れているのはcsvファイルで展開するときにセルの区切りにするためです。

FILE_WRITEでファイルを開いた場合はファイルの内容が更新されるため1行分のデータしか確認することができません。

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

グラフデータの作成

ブラウザー上でグラフとして表示するデータの配列を作成します。SHT35-DISから取得した温湿度データを配列の先頭に最古のデータが配置されるようにデータを格納することで時系列でデータの変化の履歴データを作成します。

getdata.temp[]の配列はリングポインタの構成としwpを更新しながら取得した温湿度データを格納していきます。wpはgetdata[]において次にデータを格納する場所を示すポインタなのでwp+1のデータが時系列でみた時getdata[]内の最古のデータになります。

最古のデータの位置をrpの初期値としてrpを更新しながらgetdata.temp[]配列のサイズ分wavetemp[]の配列にデータを移し替えることで最古のデータを先頭にした時系列のデータになります。

広告

動作確認

ブラウザーから接続要求を行い温湿度データのグラフとダウンロードリンクが実装されていることを確認します。ダウンロードリンクから履歴データをダウンロードしてSDカードに保存している履歴データと比較します。

温湿度情報のグラフ表示とダウンロードリンクの表示
温湿度情報のグラフ表示とダウンロードリンクの表示

スマホでWiFiのアクセスポイントに接続し「192.168.11.2」を入力しリクエストを送るとESP32-WROOM-32Eからスマホに返信し温湿度データと時間経過による温湿度情報がグラフ表示されました。また「履歴をダウンロード」のリンクが追加されていることが確認できました。

「履歴をダウンロード」をクリックすると履歴をダウンロードするか確認のダイアログが表示されるのでダウンロードを許可すると指定したフォルダ内に「sht32.txt」がダウンロードされます。

ダウンロードしたファイル
ダウンロードしたファイル
ファイルの内容
ファイルの内容

ダウンロードするとフォルダ内にsht35.txtがファイル一覧に表示されます。sht35(12).txtのように()がついているのは同じファイル名で複数回ダウンロードを行った場合に表示されます。ファイルの内容を確認すると履歴の番号、温度データ、湿度データが保存されているのが分かります。次にSDカードのファイルを確認します。

SDカードのファイル
SDカードのファイル

SDカードに保存している履歴ファイルの「sht35.txt」を確認するとダウンロードしたデータと一致していることが確認できました。ダウンロードした「sht35.txt」の拡張子をcsvに変更してEXCELデータに変換後グラフ表示しました。

履歴データをエクセルでグラフ化した結果
履歴データをエクセルでグラフ化した結果

履歴のデータは60秒に1回分のデータを保存しているためブラウザー上で表示しているグラフ(1秒毎にデータを更新)よりも広範囲のデータの傾向が確認できます。用途に応じて履歴保存の頻度を調整することで履歴データが貯まり過ぎるのを防ぐことができます。

ダウンロードしたデータはSDカードのファイルを参照しているので当然の結果ですがSDカードを取り出さずに履歴データを確認する方法としての一例になると思います。

スポンサーリンク

ソースコード全体

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

#include <Wire.h>
#include <WiFi.h>
#include <WebServer.h>
#include <SPIFFS.h>
#include <SPI.h>
#include <SD.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の通信タイムアウト
#define SD_CS 5

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

typedef struct{
  uint16_t wp;
  uint16_t rp;
  float temp[CHART_SZ];
  float humid[CHART_SZ];
}DATA_RING;

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 humid;
float wavetemp[CHART_SZ];
float wavehumid[CHART_SZ];
DATA_RING getdata;
int16_t timSht35start;
int16_t  timSht35Out = TIME_OFF;
uint8_t singleshot[2] = { 0x2C, 0x06};
uint8_t chksum[2];
uint8_t rxdata[6];
uint8_t sdset;
uint32_t no;

File myfile;String filepath = "/sht35.txt";

/* プロトタイプ宣言 */
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();

  if(!SD.begin(SD_CS)){
    Serial.println("initialization failed!");
    while (1);
  }

  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.serveStatic(filepath.c_str(),SD, filepath.c_str());
  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;
          humid = (humiHex / 65535.0) * 100.0;

          getdata.temp[getdata.wp] = temp;
          getdata.humid[getdata.wp] = humid;

          if(++getdata.wp >= CHART_SZ ){
            getdata.wp = 0;
          }

          Serial.print("temp: ");
          Serial.print(temp);
          Serial.print("℃  ");
          Serial.print("humi :");
          Serial.print(humid);
          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){
  int16_t rp;
  uint16_t i;
      
  rp = getdata.wp; //wp更新後なので最古のデータとなる
  //表示用の波形データを生成
  for( i = 0; i < CHART_SZ; i++){
    wavetemp[i] = getdata.temp[rp];
    wavehumid[i] = getdata.humid[rp];
 
    if( ++rp >= CHART_SZ ){
      rp = 0;
    }
  }

  if((++sdset % 60) == 0){
    
    sdset = 0;
    //SDカードに保存
    myfile = SD.open(filepath,FILE_APPEND);

    if( myfile ){ //ファイルが開けたら書き込む
      myfile.print(no);
      myfile.print(",");
      myfile.print(temp);
      myfile.print(",");
      myfile.println(humid);
      myfile.close(); //ファイルを閉じる
      ++no;
    }
    else{
      Serial.println("NG");
    }
  }
}

/* クライアントに返信する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 += humid;
  str += "%RH";
  str += "</h2>";
  str += "<a href='sht35.txt' download='sht35.txt'>履歴をダウンロード</a>";
  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 += wavetemp[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 += wavehumid[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を使ってフラッシュ領域に書き込んでおく必要があります。下記記事にまとめていますので参考にしてください。

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

関連リンク

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

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

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

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

広告

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

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