2026年2月3日火曜日

2ch温度ロガーの製作(12;完成)

サクッと作ろうと思っていた温度ロガーですが、昨年の9月から始めてようやく完成。製作記事がふっとんで気力回復がイマイチなのでここまで。

特徴は次のような感じ。

  • 2chの温度が測定できる
  • WiFiに繋ぐことでntpサーバから日時を取得して記録
  • ログはmicroSDにcsvファイル形式で記録
  • 測定時間間隔を設定できる
  • リチウム電池で2~3日は動作すると思う(実測はしてない)
  • 充電はUSB-TypeC


csvファイル形式でログが記録できるので、Excelで簡単にグラフ化できる。下記のグラフは温度の校正がイマイチでちょっと高めにでている。



内部の様子。左端ケース外はリチウム電池。ケース内側左端はmicroSDのスロット(秋月製)、中央はESP32、右端は充放電モジュール(USB-TypeC)。刺さっているのはプログラム書き込み用のアダプタ


リチウム電池を入れた様子。


自分でも使い方を忘れそうなので側面にラベルを貼った。


 WiFiのアクセスポイントなどの情報は、microSD内のsetup.txtファイルに記載する。
# 温度ロガー設定ファイル
# 例)
# SSID mySSID
# PW   myPassWord
# Interval 0  (0:1秒, 1:10秒, 2:30秒, 3:1分, 4:5分, 5:10分)
#
SSID mySSID
PW myPassWord
Interval 0

側面のmicroSDスロット。


反対側側面からUSB-TypeCで充電中。小さな穴から充放電モジュールの赤色LEDが点灯しているのが分かる。


充電が完了すると青色に光る。




以下、測定までのシーケンス。

電源を入れるとWiFiに接続する。黄色いタクトスイッチを押しながら電源を入れるとWiFiには接続しない。その際のタイムスタンプは2026.1.1 00:00:00になる。WiFi接続に失敗しても同様。


接続完了。


測定時間間隔の設定。1秒、10秒、30秒、1分、5分、10分で指定でき、黄色いタクトスイッチを押すと切り替わる。デフォルトはmicroSD内のsetup.txtに記載。無操作で5秒経過すると、測定開始。


測定開始。測定は指定した時刻が時間間隔で正時になったとき。例えば10秒毎なら0秒、10秒、20秒…のときに測定し、1分毎なら毎分0秒のときに測定する。


これまでの経緯。温度センサの値を読み取るAD変換の補正法などChatGPTに聞いて試行錯誤したので最初の頃と最終版は異なっている。



需要があるか分かりませんが、回路図(クリックで拡大)とスケッチ(キレイではない)です。いずれも無保証です。


処理は1秒毎のタイマ割り込みでスリープから目覚めて、そのときの時刻が測定時間間隔の正時と一致したら測定してmicroSDに書き出して、またスリープする。一致しないとすぐにスリープ。

//---------------------------------------------------------------------------------------
//                                                                         '26.01.31 naka
//                                                                   ver.0 '25.09.14 naka
//  ESP32 2ch温度ロガー(小型液晶に時刻も表示)
//
//  ・測定の正確な日時はWiFi経由のntpサーバから取得
//  ・microSDのsetup.txtファイルに平文でWiFiのSSID,Password 及び測定時間間隔の
//    デフォルトの番号を記載(0:1秒, 1:10秒, 2:30秒, 3:1分, 4:5分, 5:10分)
//    例)SSID mySSID
//        PW   myPassWord
//        Interval 0
//  ・時間間隔は電源投入後にタクトスイッチでも変更可能(5秒経過すると測定を開始)
//  ・WiFiがない環境で使う際は、タクトスイッチを押しながら電源オン
//  ・WiWi設定があっても繋がらない場合には、複数回トライして諦める
//  ・WiWiがない場合、或いは何度トライして繋がらない場合には日時を26年1月1日00:00:00とする
//  ・ログはmicroSD内にタイムスタンプ付のcsvファイルとして格納する
//  ・ファイル名が重複する場合にはファイル名に追番が付く
//  ・WiFiに繋がない、繋がらない、ntpサーバに繋がらない際の日時は"2026-01-01 00:00:00"となる
//  ・測定は指定した時刻が時間間隔で正時になったとき
//    例)10秒毎なら0秒、10秒、20秒…のとき、1分毎なら毎分0秒のとき
//  ・スリープから1秒毎の割り込みで目覚めて正時になったか確認(再びスリープ)
//---------------------------------------------------------------------------------------
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ESP32Time.h>
#include <SD.h>
#include "FS.h"
#include "SPIFFS.h"
#include "esp_adc_cal.h"
ESP32Time rtc;

#define SD_CS 16   // SD card chip select
#define TACT_SW 25 // タクトスイッチ(WiFi未使用指示、測定時間間隔セット用)
const int SENSOR_PIN1 = 32; // 温度センサMCP9700Aの出力ピンを接続したADCピン
const int SENSOR_PIN2 = 33; // 温度センサMCP9700Aの出力ピンを接続したADCピン

esp_adc_cal_characteristics_t adc_chars;

#define SETUP_FILE "/setup.txt"
#include <FaBoLCDmini_AQM0802A.h>
FaBoLCDmini_AQM0802A lcd;

// microSDのsetupファイルに記載されているものが設定される
char ssid[24];
char password[24];

int interval;
int interval_i = 0;
const int interval_tbl[] = {1,10,30,60,300,600}; // 測定間隔(秒)
const int interval_tbl_size = 6;
char LogFile[64]; // 記録用のcsvファイル名(タイムスタンプから自動作成)

void setup()
{
  char msg[17];
  struct tm timeinfo;
  char logfilename[64];

  pinMode(TACT_SW, INPUT);
  pinMode(SENSOR_PIN1, ANALOG);
  pinMode(SENSOR_PIN2, ANALOG);

  analogReadResolution(12); // 分解能12bit
  analogSetAttenuation(ADC_11db);

  // ADCキャリブレーション
  esp_adc_cal_characterize(
    ADC_UNIT_1,           // GPIO32?39
    ADC_ATTEN_DB_11,      // 0?3.3V
    ADC_WIDTH_BIT_12,
    1100,                 // デフォルトVref(mV)
    &adc_chars
  );

  // モニタLCD設定
  SetupLCD();
  delay(1000);

  // setupファイルを読み、WiFiのssid,password、測定間隔のデフォルトを取得
  if (!SD.begin(SD_CS, SPI, 24000000)) {
    dispLCD_msg("microSD failed  ","                ");
    while(true); 
  }
  read_setup_file(SETUP_FILE);

  bool wifi_flag = false;
  if (digitalRead(TACT_SW)==LOW) {
    dispLCD_msg("WiFi dosen't use","                ");
    while(digitalRead(TACT_SW)==LOW);
  }
  else {
    dispLCD_msg("Connecting WiFi ","                ");
    if (wifi_connect()) {
      wifi_flag = true;
    }
    else {
      dispLCD_msg("Connect failed  ","                ");
      delay(1000);
    }
  }
  delay(1000);

  if (wifi_flag) {
      // WiFi接続に成功した場合、NTPサーバに接続し、時刻設定
      configTime(9 * 3600L, 0, "ntp.nict.jp", "time.google.com", "ntp.jst.mfeed.ad.jp"); 
      int loopcnt = 0;
      while (!getLocalTime(&timeinfo)) {
        dispLCD_msg("Failed get time ","Retrying ...    ");
        if (++loopcnt>5) { // 念のため5回繰り返してダメなら抜ける
          wifi_flag = false;
          break; 
        }
        delay(500);
      }
    dispLCD_msg("                ","                ");
    wifi_disconnect(); // 以降はWiFi未接続にする
  }

  // 起動時にSWが押されていたか、WiFi接続に失敗か、ntpサーバからの時刻取得に失敗した場合は下記の日付と時刻設定
  if (!wifi_flag) {
    rtc.setTime(1767225600); // 2026-01-01 00:00:00		
  }

  // 測定時間間隔の設定(5秒無操作で戻ってくる)
  set_interval();

  // ログファイル名を決めるためにタイムスタンプ取得
  getLocalTime(&timeinfo);
  sprintf(logfilename, "%02d%02d%02d_%02d%02d%02d.csv",(timeinfo.tm_year + 1900)%100, timeinfo.tm_mon + 1, timeinfo.tm_mday,timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);

  // WiFiに繋がない場合、時刻情報を取得できないためタイムスタンプのファイル名が同じになることを防ぐ。
  get_unique_filename("",logfilename,LogFile,sizeof(LogFile));

  // 測定開始メッセージ
  dispLCD_msg("Start           ","     measurement");

  // 1,000,000us = 1sのタイマー設定
  esp_sleep_enable_timer_wakeup(1000000);

}

void loop()
{ 
  // 温度を測定してログファイルに書き出す
  measurement(interval);

  // ライトスリープ(タイマー割り込みで1秒毎に目覚める)
  esp_light_sleep_start();
}

void set_interval() {
  disp_interval(interval_i); // 現在の設定時間を表示

  int prevmsec = millis();
  while ((millis() - prevmsec)<5000) {  // 5秒反応が無ければ時間設定を抜ける
    if (digitalRead(TACT_SW)==LOW) {
      interval_i++;
      prevmsec = millis();
      if (interval_i>=interval_tbl_size) interval_i = 0;

      disp_interval(interval_i); // 現在の設定時間を表示
      while (digitalRead(TACT_SW)==LOW) {}  // SWが離されるまで待つ
      delay(20); //チャタリング対策
      prevmsec = millis();
    }
  }

  interval = interval_tbl[interval_i];
  return;
}

void disp_interval(int i) {
  int time = interval_tbl[i];
  char msg[20];

  if (time<60)
    sprintf(msg,"%11d(sec)",time);
  else
    sprintf(msg,"%11d(min)",time/60);
  dispLCD_msg("set interval    ",msg);
}

void measurement(int period) {
  char yymmddhhmmss[17],hhmmss[10],timestamp[17];
  struct tm timeinfo;
  getLocalTime(&timeinfo);

  // 測定する時間なのか確認(割り込みは1秒毎なので)
  bool flag = false;
  if (period==1) flag = true;
  else if (period==10 && timeinfo.tm_sec%10==0) flag = true; 
  else if (period==30 && timeinfo.tm_sec%30==0) flag = true; 
  else if (period==60 && timeinfo.tm_sec==0)    flag = true; 
  else if (period==120 && timeinfo.tm_sec==0 && timeinfo.tm_min%2==0) flag = true; 

  if (flag) {
    sprintf(timestamp,"%02d:%02d:%02d",timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec);
    sprintf(yymmddhhmmss, "%02d%02d%02d %s",(timeinfo.tm_year + 1900)%100, timeinfo.tm_mon + 1, timeinfo.tm_mday, timestamp);

    float temp1 = temp_sensing(SENSOR_PIN1);
    float temp2 = temp_sensing(SENSOR_PIN2);
    char buff1[6],buff2[6],buff[20];
    dtostrf(temp1,4,1,buff1);
    dtostrf(temp2,4,1,buff2);
    sprintf(buff, "%4s%cC , %4s%cC",buff1,0xDF,buff2,0xDF);
    dispLCD_msg(yymmddhhmmss,buff);

    write_logfile(timestamp,buff1,buff2);  // microSDに書き込み
  }

}

float temp_sensing(const int sensor) {
  float offset;

  uint32_t voltage_mV = 0;
  const int N = 100;
  for ( int i = 0 ; i < N ; i++ ) {
    int adcRaw = analogRead(sensor);
    // ADC値 → 電圧(mV)(補正済み)
    voltage_mV += esp_adc_cal_raw_to_voltage(adcRaw, &adc_chars);
  }

  // 電圧 → 温度(℃)
  float temperatureC = ((float)voltage_mV/N - 500.0) / 10.0;

  // 温度センサ毎の校正
  if (sensor==SENSOR_PIN1) offset = -0.9;
  else offset = -1.2;

  return temperatureC + offset;
}

void dispLCD_msg(char msg1[],char msg2[]) {
  lcd.clear();
  lcd.setCursor(0, 0); // Col,Raw
  lcd.print(msg1);
  lcd.setCursor(0, 1); // Col,Raw
  lcd.print(msg2);
}

void write_logfile(char* timestamp, char* buff1,char* buff2){
  char buff[32];
  File outputFile = SD.open(LogFile,FILE_APPEND);
  
  sprintf(buff,"%s,%s,%s",timestamp,buff1,buff2);
  outputFile.println(buff);
  outputFile.close();
}

void SetupLCD() {
  delay(150);
  lcd.begin();
  lcd.command(0x38);
  delay(1);
  lcd.command(0x39);
  delay(1);
  lcd.command(0x14);
  delay(1);
  lcd.command(0x71);
  delay(1);
//  lcd.command(0x51);  // 5V
  lcd.command(0x56);  // 3.3V
  delay(2);
  lcd.command(0x6c);
  delay(300);
  lcd.command(0x38);
  delay(1);
  lcd.command(0x01);
  delay(2);
  lcd.command(0x0c);
  delay(2);
}

// Wifi接続
bool wifi_connect() {
  char msg[128];
  bool connected = false;

  for (int i=0;i<3;i++) { // WiFi接続、3回繰り返す
    WiFi.begin(ssid, password);    
    long int StartTime=millis();
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        if ((StartTime+5000) < millis()) break; // 5秒待って繋がらないときは諦める
    } 
  
    if (WiFi.status() == WL_CONNECTED) {
      IPAddress ip = WiFi.localIP();
      sprintf(msg,"%d.%d.%d.%d",ip[0], ip[1], ip[2], ip[3]);
      dispLCD_msg("Connected IP    ",msg);
      connected = true;
      break;
    }
    delay(100);
  } 

  if (connected)
    return true;
  else {
    wifi_disconnect();
    delay(100);
    return false;
  }

}

void wifi_disconnect() {
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
}

int read_setup_file(char* fileName) {
  char  buff[128];
  char  key[128],val[128];

  File setupFile = SD.open(fileName,FILE_READ);
  if (setupFile) {
    while (readLine(setupFile,buff)>=0) {
      if (buff[0]!='#') {
        sscanf(buff,"%s %s",&key,&val);
        if (strcmp(key,"SSID")==0) {
          strcpy(ssid,val);
        }
        else if (strcmp(key,"PW")==0) {
          strcpy(password,val);
        }
        else if (strcmp(key,"Interval")==0) {
          int ival;
          sscanf(val,"%d",&ival);
            if (ival>=0 && ival<interval_tbl_size) { // 異常値チェック
              interval_i = ival;
            }
          }
      }
    }
    setupFile.close();
  }

  return 0;
}

// microSDから1行読み取る
int readLine(File fp, char *buff){
  int i = 0;
  if(!fp.available()) {
    return -1;
  }
  while (fp.available()) {
    char c = fp.read();
    if (c=='\r');
    else if (c=='\n') break;
    else buff[i++] = c;
  }
  buff[i] = 0;
  
  return i;
}

/**
 * 重複しないファイル名を生成する関数 (ChatGPTに書いてもらった)
 * @param fs_prefix マウントポイント (例: "/spiffs")
 * @param base_name 元のファイル名 (例: "data.txt")
 * @param out_path  結果を格納するバッファ
 * @param max_len   バッファの最大サイズ
 */
void get_unique_filename(const char* fs_prefix, const char* base_name, char* out_path, size_t max_len) {
    char temp_path[128];
    char name_part[64];
    char ext_part[16];
    
    // 拡張子を探す
    const char *dot = strrchr(base_name, '.');
    if (dot) {
        size_t name_len = dot - base_name;
        strncpy(name_part, base_name, name_len);
        name_part[name_len] = '\0';
        strcpy(ext_part, dot);
    } else {
        strcpy(name_part, base_name);
        ext_part[0] = '\0';
    }

    // 最初は元の名前でチェック
    snprintf(out_path, max_len, "%s/%s", fs_prefix, base_name);
    struct stat st;
    int counter = 1;

    // ファイルが存在し続ける間、ループ
    while (SD.exists(out_path)) {
        // "パス/ベース名 + 数字 + 拡張子" を組み立て
        snprintf(out_path, max_len, "%s/%s_%d%s", fs_prefix, name_part, counter, ext_part);
        counter++;
        
        // 無限ループ防止(必要に応じて)
        if (counter > 999) break; 
    }
}

//---------------------------------------------------------------------------------------
// EOF
//---------------------------------------------------------------------------------------

0 件のコメント:

コメントを投稿