NTP時刻・温度・湿度・気圧データのSDカードロガー制作。日本語フォントもSDカードから読み込む ( ESP-WROOM-02 使用 )

ESP8266 ( ESP-WROOM-02 )

8.スケッチを入力

では、IDEに以下のスケッチを入力してみてください。
【ソースコード】 (※無保証 ※PCの場合、ダブルクリックすればコード全体を選択できます)

#include <SD_UTF8toSJIS.h>
#include <SD_ShinonomeFONTread.h>
#include <OLED_SSD1351.h>
#include <BME280_SPI.h>
#include <SD.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>
#include <TimeLib.h> //timeライブラリver1.4の場合

const uint8_t sclk = 14; //OLED & SDcard & BME280 SCLK
const uint8_t mosi =13; //Master Output Slave Input ESP8266=Master, BME280 & OLED & SD = slave 
const uint8_t miso =12; //Master Input Slave Output
const uint8_t cs_bme = 2; //BME280 CS(ChipSelect)
const uint8_t cs_OLED = 16; //OLED CS(ChipSelect)
const uint8_t cs_SD = 15; //SDcard CS(ChipSelect)
const uint8_t DCpin =  5; //OLED D/C (Data/Command)
const uint8_t RSTpin =  4; //OLED Reset

SD_UTF8toSJIS SDu8ts; //ライブラリのクラス名定義
SD_ShinonomeFONTread SDsfr; //ライブラリのクラス名定義
OLED_SSD1351 ssd1351; //ライブラリのクラス名定義
BME280_SPI bme280spi(sclk, mosi, miso, cs_bme); //BME280ライブラリクラス定義。ここでSPI通信のピン設定をする

const char* UTF8SJIS_file = "font/Utf8Sjis.tbl"; //UTF8 Shift_JIS 変換テーブルファイル名を記載しておく
const char* ZenkakuFontFile = "font/shnmk16.bdf"; //全角フォントファイル名を宣言
const char* HalfFontFile = "font/shnm8x16.bdf"; //半角フォントファイル名を宣言
const char* DataFile = "DATALOG.TXT"; //気温・湿度・気圧データログファイル名を宣言

uint8_t get_sjis[3][16];
uint16_t sj_length[8]; //Shift_JISコードの長さ
uint8_t font_buf[16][16];
uint8_t SnnmDotOut[16][16];

uint16_t ppp[128], ttt[128], hhh[128]; //グラフプロットの値

uint32_t M_time1, M_time2;

uint32_t Graph_plot_interval = 143000; //143秒毎にグラフプロット及びdatalogファイル書き込み。Log全体は5時間になる。
boolean FirstPlot = false; //初回にファイルに書き込むためのフラグ

File dataFile; //ログデータFileクラス宣言
File UtoS; //UTF8 to Shift_JIS変換Fileクラス宣言
File SinoZ; //全角東雲フォントFileクラス宣言
File SinoH; //半角東雲フォントFileクラス宣言

//-------NTPサーバー定義----------------
IPAddress timeServer(52, 168, 138, 145); //time.windows.com
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
const int timeZone = 9;     // Tokyo
WiFiUDP Udp;
unsigned int localPort = 8888;  // local port to listen for UDP packets
uint32_t NTPlastTime = 0;

//-------時刻表示定義-------------------
String time_str, ymd_str;
const char* week_jp[7] = {"sun","mon","tue","wed","thu","fri","sat"};
uint8_t MinuteTime;

//*********************セットアップ********************************
void setup() {
  uint8_t t_sb = 5; //stanby 1000ms
  uint8_t filter = 0; //filter O = off
  uint8_t osrs_t = 4; //OverSampling Temperature x4
  uint8_t osrs_p = 4; //OverSampling Pressure x4
  uint8_t osrs_h = 4; //OverSampling Humidity x4
  uint8_t Mode = 3; //Normal mode
 
  Udp.begin(localPort);
  Serial.begin(115200);
   
  pinMode(sclk, OUTPUT);
  pinMode(mosi, OUTPUT);
  pinMode(miso, INPUT); //ここはINPUTなので注意
  pinMode(cs_OLED, OUTPUT);
  pinMode(cs_SD, OUTPUT);
  pinMode(cs_bme, OUTPUT);
  pinMode(DCpin, OUTPUT);
  pinMode(RSTpin, OUTPUT);
  
  bme280spi.BME280_SPI_Init(t_sb, filter, osrs_t, osrs_p, osrs_h, Mode);

  ssd1351.SSD1351_Init(sclk, mosi, cs_OLED, DCpin, RSTpin);
  delay(300);
  ssd1351.SSD1351_BlackOut(); //黒画面出力

  for(int i=0; i<16; i++) { //初期化しておく
    for(int j=0; j<16; j++){
      font_buf[i][j] = 0;
      SnnmDotOut[i][j] = 0;
    }
  }

  uint8_t Red, Green, Blue; //Red(Max=31),Green(Max=63),Blue(Max=31)

  Red = 3; Green = 8; Blue = 4;
  ssd1351.SSD1351_RectFill(0, 48, 127, 111, Red, Green, Blue);
  Red = 31; Green = 63; Blue = 31;
  ssd1351.SSD1351_RectLine(0, 48, 127, 111, Red, Green, Blue);

  Serial.println(); Serial.print("Initializing SD card...");
  if (!SD.begin(cs_SD,SPI_FULL_SPEED)) {
    Serial.println("Card failed, or not present");
    return;
  }
  Serial.println("card initialized.");
/*
  if(SD.remove("DATALOG.TXT")){ //removeクラスは変数のconst char* DataFileを使うとエラーになるので注意
    Serial.println("Removed datalog.txt");
  }else{
    Serial.println("Not removed datalog.txt");
  }
*/  
  UtoS = SD.open(UTF8SJIS_file, FILE_READ);
  if (UtoS == NULL) {
    Serial.print("Utf8Sjis.tbl File not found");
    return;
  }else{
    Serial.println("Utf8Sjis.tbl File OK!");
  }
  SinoH = SD.open(HalfFontFile, FILE_READ);
  if (SinoH == NULL) {
    Serial.print("shnm8x16.bdf File not found");
    return;
  }else{
    Serial.println("shnm8x16.bdf File OK!");
  }
  SinoZ = SD.open(ZenkakuFontFile, FILE_READ);
  if (SinoZ == NULL) {
    Serial.print("shnmk16.bdf File not found");
    return;
  }else{
    Serial.println("shnmk16.bdf File OK!");
  }
  
  //NTPサーバーから時刻を取得
  delay(2000); //ここの秒数が少ないとESP8266のWi-Fiが起動していないので十分余裕を持つこと
  setSyncProvider(getNtpTime);
  delay(1000);

  NTPlastTime = millis();
  MinuteTime = now();

  for(int i=0; i<128; i++){
    ppp[i] = 127; ttt[i] = 127; hhh[i] = 127;
  }

}
//******************メインループ************************************
void loop() {
  double temperature = 0.0, pressure = 0.0, humidity = 0.0;
  uint8_t T_Red = 0, T_Green = 63, T_Blue = 0; //気温表示色。Red(Max=31),Green(Max=63),Blue(Max=31)
  uint8_t H_Red = 0, H_Green = 0, H_Blue = 31; //湿度表示色。Red(Max=31),Green(Max=63),Blue(Max=31)
  uint8_t P_Red = 31, P_Green = 10, P_Blue = 10; //気圧表示色。Red(Max=31),Green(Max=63),Blue(Max=31)
  String oled_str;
  uint16_t Len; //Shift_JISコードのバイトの長さ
  char month_chr[3], day_chr[3], h_chr[3], m_chr[3], s_chr[3];
  char index_chr[3];

  if(millis() - NTPlastTime > 600000){ //10分毎にNTP時刻補正
    setSyncProvider(getNtpTime);
    Serial.print("Get NTP Time ----------------");
    Serial.println(String(year()) + "/" + String(month()) + "/" + String(day()) + "/" + String(hour()) + ":" + String(minute()) + ":" + String(second()));
    NTPlastTime = millis();
  }

  if(minute() != MinuteTime){
    sprintf(h_chr, "%2d", hour());//ゼロを空白で埋める場合は%2dとすれば良い
    sprintf(m_chr, "%02d", minute());
    sprintf(index_chr,"%03d",round((Graph_plot_interval*126)/60000)); //グラフプロット間隔からログ全体時間の計算
    
    SD.begin(cs_SD,SPI_FULL_SPEED); //フォントファイルを呼び込む前に必ずSD.beginクラスが必要
    SDu8ts.UTF8_to_SJIS_str_cnv(UtoS, "5時間前    " + String(h_chr) + ":" + String(m_chr), get_sjis[0], &Len);
    SDsfr.SjisToShinonome16FontRead_ALL(SinoZ, SinoH, 0, 0, get_sjis[0], Len, font_buf);
  
    pinMode(sclk, OUTPUT);
    pinMode(mosi, OUTPUT);
    ssd1351.SSD1351_8x16_DisplayOut_1col_LtoR(0, 112, 31, 63, 0, Len, font_buf);
    MinuteTime = minute();
  }
  
  if(millis()-M_time1 > 2000){ //2秒毎にディスプレイにデータ表示
    pinMode(miso, INPUT); //bme280を複数デバイスのうちで使う場合はこれがないと計測できない
    temperature = bme280spi.Read_Temperature();
    pressure = bme280spi.Read_Pressure();
    humidity = bme280spi.Read_Humidity();

    sprintf(h_chr, "%02d", hour());//ゼロを空白で埋める場合は%2dとすれば良い
    sprintf(m_chr, "%02d", minute());
    sprintf(s_chr, "%02d", second());
    sprintf(month_chr, "%02d", month());
    sprintf(day_chr, "%02d", day());
    time_str = String(h_chr) + ':' + String(m_chr) + ':' + String(s_chr);
    ymd_str = String(year()) + '/' + String(month_chr) + '/' + String(day_chr) + "(" + String(week_jp[weekday()-1]) + ")";
    
    Serial.println("-----------------------");
    Serial.print("Temperature = "); Serial.print(temperature); Serial.println(" *C");
    Serial.print("Humidity = "); Serial.print(humidity); Serial.println(" %");
    Serial.print("Pressure = "); Serial.print(pressure); Serial.println(" hPa");
  
    oled_str = "気温= "+String(temperature)+" ℃ ";
    SD.begin(cs_SD,SPI_FULL_SPEED); //フォントファイルを呼び込む前に必ずSD.beginクラスが必要
    SDu8ts.UTF8_to_SJIS_str_cnv(UtoS, oled_str, get_sjis[0], &Len);
    SDsfr.SjisToShinonome16FontRead_ALL(SinoZ, SinoH, 0, 0, get_sjis[0], Len, font_buf);
    pinMode(sclk, OUTPUT);
    pinMode(mosi, OUTPUT);
    ssd1351.SSD1351_8x16_DisplayOut_1col_LtoR(0, 0, T_Red, T_Green, T_Blue, Len, font_buf);
  
    oled_str = "湿度= "+String(humidity)+" %";
    SD.begin(cs_SD,SPI_FULL_SPEED); //フォントファイルを呼び込む前に必ずSD.beginクラスが必要
    SDu8ts.UTF8_to_SJIS_str_cnv(UtoS, oled_str, get_sjis[1], &Len);
    SDsfr.SjisToShinonome16FontRead_ALL(SinoZ, SinoH, 0, 0, get_sjis[1], Len, font_buf);
    pinMode(sclk, OUTPUT);
    pinMode(mosi, OUTPUT);
    ssd1351.SSD1351_8x16_DisplayOut_1col_LtoR(0, 16, H_Red, H_Green, H_Blue, Len, font_buf);
  
    oled_str = "気圧= "+String(pressure)+"hPa";
    SD.begin(cs_SD,SPI_FULL_SPEED); //フォントファイルを呼び込む前に必ずSD.beginクラスが必要
    SDu8ts.UTF8_to_SJIS_str_cnv(UtoS, oled_str, get_sjis[2], &Len);
    SDsfr.SjisToShinonome16FontRead_ALL(SinoZ, SinoH, 0, 0, get_sjis[2], Len, font_buf);
    pinMode(sclk, OUTPUT);
    pinMode(mosi, OUTPUT);
    ssd1351.SSD1351_8x16_DisplayOut_1col_LtoR(0, 32, P_Red, P_Green, P_Blue, Len, font_buf);

    if(millis()-M_time2 > Graph_plot_interval || FirstPlot == false){
      for(int i=126; i>0; i--){
        ttt[i] = ttt[i-1];
        hhh[i] = hhh[i-1];
        ppp[i] = ppp[i-1];
      }
      ttt[0] = round(temperature);
      hhh[0] = round(humidity*0.64);
      ppp[0] = round(pressure)-956;
      
      SD.begin(cs_SD,SPI_FULL_SPEED); //ファイルを呼び込む前に必ずSD.beginクラスが必要
      dataFile = SD.open(DataFile, FILE_WRITE);
      if (dataFile == NULL) {
        Serial.print("datalog.txt File not found");
        return;
      }else{
        Serial.println("datalog.txt File OK!");
      }
      if (dataFile) {
        dataFile.println(ymd_str + "," + time_str + "," +  String(temperature) + "," + String(humidity) + "," + String(pressure));
        dataFile.close();
        Serial.println("---------------datalog.txt writed");
      }
      // if the file isn't open, pop up an error:
      else {
        Serial.println("error opening datalog.txt");
      }
    
      pinMode(sclk, OUTPUT);
      pinMode(mosi, OUTPUT);
      for(int i=0; i<126; i++){
        for(int j=0; j<62; j++){
          if(ttt[i] == j){
            ssd1351.SSD1351_1pixel_DisplayOut((127-1)-i, (111-1)-ttt[i], T_Red, T_Green, T_Blue);
          }else if(hhh[i] == j){
            ssd1351.SSD1351_1pixel_DisplayOut((127-1)-i, (111-1)-hhh[i], H_Red, H_Green, H_Blue);
          }else if(ppp[i] == j){
            ssd1351.SSD1351_1pixel_DisplayOut((127-1)-i, (111-1)-ppp[i], P_Red, P_Green, P_Blue);
          }else{
            ssd1351.SSD1351_1pixel_DisplayOut((127-1)-i, (111-1)-j, 3, 8, 4);
          }
        }
      }
      M_time2 = millis();
      FirstPlot = true;
    }
    M_time1 = millis();
  }
}
//*************************NTP Time 関数**************************************
time_t getNtpTime()
{
  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  sendNTPpacket(timeServer);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}
//*************************NTP Time 関数**************************************
void sendNTPpacket(IPAddress &address)
{// send an NTP request to the time server at the given address
  // set all bytes in the buffer to 0
  memset(packetBuffer, 0, NTP_PACKET_SIZE);
  // Initialize values needed to form NTP request
  // (see URL above for details on the packets)
  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  // 8 bytes of zero for Root Delay & Root Dispersion
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
  // all NTP fields have been given values, now
  // you can send a packet requesting a timestamp:                 
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

●1-4行:
自作ライブラリのインクルードです。

●5行目:
Arduino IDE の標準のSDカードライブラリのインクルードです。
ESP8266 ボードにも同じライブラリがありますが、自動でESP8266用のSD.hに切り替わるようです。

●6-7行:
Arduino core for ESP8266 Wi-Fi chip にある標準ライブラリです。

●8行目:
Arduino 標準のTimeライブラリのインクルードです。

●10-17行:
ESP-WROOM-02 ( ESP8266 ) のGPIOのSPI通信用ピン設定です。

●19-22行:
自作ライブラリのクラス名宣言です。好きな名前に設定できます。

●24-27行:
micro SD カード内にあるフォントファイルやログデータファイルを宣言します。
注意しなければならないのは、拡張子を除くファイル名は8文字以下です。SDカード内のファイル名と合わせて下さい。
フォントファイルはファイル名の前に先に説明したフォルダ名を入れてください。
ログデータファイルはルート最上位としてます。

●38行:
グラフをプロットする間隔、および、SDカードに記録する間隔を定義してます。
グラフは枠線を除くと126ドットなので、143秒ごとにプロットするとグラフ全体が5時間丁度となります。

●41-44行:
ファイルクラス名を宣言してます。グローバル変数領域で宣言することが大事です。

●46-53行:
NTPサーバーに関する定義ですが、これはArduino IDEサンプルスケッチの流用です。

●62-67行目:
これは前回の記事前々回の記事を参照してください。

●72-79行目:
ESP-WROOM-02 ( ESP8266 ) のGPIOピンのモードを設定します。特に74行目だけがINPUTとなるので注意してください。

●81-99行目:
これは前回の記事を参照してください。

●102行目:
SD.hライブラリではCS ( Chip Select )ピンだけ指定します。ということは、ESP8266を自動判断して、SCLK や MOSIピンを設定してしまうということです。ですから、GPIO を正しく設定していないと、SDカードと他のSPIデバイスと連結する場合は正しく動作しませんので要注意です。
SPI_FULL_SPEED という変数はArduino15フォルダにある、ESP8266のSDライブラリで宣言されている変数で、値は 8000000 です。これが最大値のようです。

●107-113行:
ここはコメントアウトしていますが、既にあるDATALOGファイルを消去して、新たに作り直す場合はコメントを消してください。
ここで注意してほしいのは、ファイル名をDataFileとしてはいけません。これをやるとエラーになります
なぜかは分かりませんが、”DATALOG.TXT” としないと動作しません。
また、removeしない場合は、DATALOGファイルにデータを追記します。

●114-134行:
フォントファイルや文字コード変換テーブルファイルを読み取りモードで開きます。

●137-139行:
ここで、NTPサーバーにアクセスして、時刻を取得して、ESP-WROOM-02 ( ESP8266 )の内部時刻を補正します。
起動して直ぐだと、ESP-WROOM-02 のWi-Fiが起動していない場合があるので、2秒待ちます。

●160-165行:
10分毎にNTPサーバーから時刻を取得して、補正します。

●167-180行:
時刻の分数が変化したら、OLEDディスプレイの時刻を更新します。
ここでとても重要なのが、172行目のSD.begin です。
SDカードからフォントを読み込む場合に、この関数を直前に呼び込んでおかないとうまく動作しません。
恐らく、その前にOLED やBME280などのデバイスでGPIOピンを設定したために、SDカードのGPIOピン設定を初期化しないと動作しないようです。SDカードも自由にピン設定できればいいのですが・・・。
そして、もう一つ重要なのが、176行と177行です。
先にSD.beginでGPIOピンの初期化が行われたために、ここでSCLKとMOSIピンのモードを設定しなおさないとディスプレイに上手く表示されません。SD.hライブラリはかなり厄介で、いろいろと実験した結果、こうなりました。
以降、フォントファイルを読み込む時と、ディスプレイに表示させる時はこんな指定が繰り返されます。

●183-186行:
2秒毎にBME280の温度・湿度・気圧データを読み込みます。
183行目が重要で、先に述べたように、SD.beginでピン設定が変えられた場合はこれが無いとBME280は正しく動作しません。

●188-192行:
SDカードにログを記録する場合、読み出すことを考えてsprintf関数を使って時刻の桁数を揃えます。

●201-223行:
OLEDディスプレイに温度・湿度・気圧データを表示させてます。
SDカードからフォントを読み込む直前にSD.beginを呼び出し、OLEDディスプレイに表示させる直前にpinMode設定をしているのがポイントです。

●225-270行:
これは、前回の記事とほぼ同じですが、235-251行でSDカードにデータを書き込んでいるところが違います。
データはカンマで区切ってます。
書き込み終わったらかならずファイルをclose()します。

●274-319行:
これはArduino IDE のNTPのサンプルスケッチそのままです。

9.コンパイル、実行する

では、ご自分のWi-Fiルーター(アクセスポイント)を立ち上げて、しばらく待ってから、スケッチをコンパイルして実行させてみましょう。
NTPサーバーアクセスはUDPなので、SSIDやパスワード不要でスルーしてインターネットにアクセスします。
シリアルモニターではこう表示されればOKです。

最初の文字化けは例のように無視してOKです。
datalog.txt writed
と表示されたときにSDカードにデータを書き込んでいます。

しばらく動作させた後、SDカードに記録されたデータをパソコンのメモ帳などのテキストエディタでDATALOG.TXTファイルを見てみましょう。
下図の様になっていればOKです。

どうでしょうか?
結構イイ感じでデータロガーを手作りすることができました。
台風が来る時には気圧データの変化が良く分かっておもしろいです。
これをうまく生かせば、気圧の変化で関節痛が起こる人達に役立つ何かが出来そうな気がしますね。

今回は以上です。
次回は、これをスマホに送信して表示させることに挑戦してみようと思います。

ではまた・・・。

Amazon.co.jp 当ブログのおすすめ

スイッチサイエンス ESPr Developer 32 Type-C SSCI-063647
スイッチサイエンス
¥2,420(2024/04/26 03:52時点)
ZEROPLUS ロジックアナライザ LAP-C(16032)
ZEROPLUS
¥22,504(2024/04/25 14:16時点)
Excelでわかるディープラーニング超入門
技術評論社
¥2,068(2024/04/25 21:41時点)

コメント

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