2026年8月15日土曜日

2ch温度ロガー(デジタル温度センサ版)の回路図・スケッチ

夏休み、少し帰省していました。帰省してもやることは庭木の剪定、伐採、草むしり、除草剤撒きなど自宅と同じです。あとお墓参り。往路では大渋滞にはまり普段の2倍の時間がかかり、帰路では高速で台風一過の土砂降りに見舞われトホホです。

天気鍋は結構利用していますが、雲が多くて最高温度記録は更新できていません。最高温度に達する前にお昼のそうめん冷や麦に使ってしまうのも一因ですが。今日は冷やし中華に利用。午前中、雲が多くて温度は60℃弱でした。

天気鍋の温度推移の記録に使っている2ch温度ロガーは昨年9月に作り始め、今年の2月には、回路図・スケッチをアップしていました(アップしたことを忘れていた)。ところが、温度センサの防水が破れて異常値を記録 するようになってしまったため、急遽 防水対応のデジタル温度プローブへの交換を行いました。最初からこれを使えばよかった。

以下、これまでの経緯です。

主な機能は以下の通り。
  • 測定の正確な日時は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秒毎の割り込みで目覚めて正時になったか確認(再びスリープ)
デジタル温度センサ(プローブ)へ交換したあとの回路図とスケッチです。無保証です。ハード、ソフトの修正をできるだけ少なく済ませたので無駄な処理などもあります。今ならAIがもっとスマートなコードを書いてくれそう。


//---------------------------------------------------------------------------------------
//                                                                         '26.07.29 naka
//                                                                         '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秒毎の割り込みで目覚めて正時になったか確認(再びスリープ)
//
//  改版履歴
//  '26.07.29 センサをディジタルセンサDS18B20に変更
//---------------------------------------------------------------------------------------
#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; // 温度センサの出力ピンを接続
const int SENSOR_PIN2 = 33; // 温度センサの出力ピンを接続

esp_adc_cal_characteristics_t adc_chars;

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

// デジタルセンサ
// https://github.com/PaulStoffregen/OneWire
#include <OneWire.h>
// https://github.com/milesburton/Arduino-Temperature-Control-Library
#include <DallasTemperature.h>
#define TEMPERATURE_PRECISION 12 // sensor resolution: 9 - 12bit

OneWire oneWireCh1(SENSOR_PIN1);
OneWire oneWireCh2(SENSOR_PIN2);
DallasTemperature sensorsCh1(&oneWireCh1);
DallasTemperature sensorsCh2(&oneWireCh2);
DeviceAddress tempDeviceAddressCh1;
DeviceAddress tempDeviceAddressCh2;

// 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);

  // モニタ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();

  // デジタルセンサ
  sensorsCh1.getAddress(tempDeviceAddressCh1, 0); // get device0 address
  sensorsCh1.setResolution(tempDeviceAddressCh1, TEMPERATURE_PRECISION); // set resolution
  sensorsCh2.getAddress(tempDeviceAddressCh2, 0); // get device0 address
  sensorsCh2.setResolution(tempDeviceAddressCh2, TEMPERATURE_PRECISION); // set resolution

  sensorsCh1.begin();
  sensorsCh2.begin();

  // ダミーで1回空読み(1発目に-127℃が返ってくることがあったので)
  sensorsCh1.requestTemperatures(); // request temp.
  float temp = sensorsCh1.getTempCByIndex(0); 
  sensorsCh2.requestTemperatures(); // request temp.
  temp = sensorsCh2.getTempCByIndex(0); 

  // ログファイル名を決めるためにタイムスタンプ取得
  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 = sens_temp_digitalCh1();
    float temp2 = sens_temp_digitalCh2();
    char buff1[8],buff2[8],buff[24];
    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 sens_temp_digitalCh1() {
  float temp;
  sensorsCh1.requestTemperatures();       // request temp.
  temp = sensorsCh1.getTempCByIndex(0);   // get temp.
  return temp;
}

float sens_temp_digitalCh2() {
  float temp;
  sensorsCh2.requestTemperatures();       // request temp.
  temp = sensorsCh2.getTempCByIndex(0);   // get temp.
  return temp;
}

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 件のコメント:

コメントを投稿