育苗箱ベンチレータ/DeepSleep復帰時間2

育苗ケース温度調整システムのシリーズ6回目でDeepSleepからの復帰時間についてスケッチを作成しました。

内容は、夜間60分間隔でDeepSleepを入れている状態から、日中の7秒間隔の稼働時間帯への移行する時に、最大で1時間DeepSleepが稼働時間帯へオーバーラップしてしまう為、毎回稼働時間内か時間外かの判定を入れる、というものでした。

具体的には、60分間隔の時間帯の稼働時間帯に差し掛かる最後の起動が、稼働開始設定時間の50分前だった場合、現在時刻の60分後の分、秒と稼働開始設定時間の分、秒の差分を最後のDeepSleep時間に設定する、というものです。

これを実行すると、夜間60分間隔で起動するはずが、20秒程度づつ間隔が長くなっていく現象が発生しました。原因として考えられるのは、起動時のWiFi接続時間と何か所かのDelay時間です。そこで、DeepSleep設定時間の計算に、はっきり決めてあるdelay時間だけ考慮するようにしました。

void loop() {
  //略(時間判定、設定時間内処理、設定時間外処理)
 
    if (hourStart == sTime.hour){
      unsigned long SLEEP1 = ((minuteStart*60 + secondStart) - ((sTime.minute*60 + sTime.second) + 2 + 10)) *1000*1000;

      delay(10000);
      esp_deep_sleep(SLEEP1);
 
    } else if (hourStart - sTime.hour == 1 && minuteStart <= sTime.minute){
        unsigned long SLEEP2 = (interval*60 - ((sTime.minute*60+sTime.second)-(minuteStart*60+secondStart)) + 2 + 10) *1000*1000;
 
        delay(10000);
        esp_deep_sleep(SLEEP2);
 
      } else {
        unsigned long SLEEP3 = (interval*60 - (2 + 10)) *1000*1000;
 
        delay(10000);
        esp_deep_sleep(SLEEP3);
      }
}  

上のSLEEP1,2,3の計算式の”2 + 10″がdelay時間の補正部分です。これで夜間のズレは10秒程度に短縮されましたが、解消はしませんでした。

後で触れますが、そもそもこのDeepSleep設定時間計算式にも間違いがあります。それはそれとして、ズレを解消しようとして、補正時間部分を計算するように改造しました。

Offset = setup_time + loop_time

もともと20秒程度発生していたズレの内訳は、一つはWiFi接続時間、もう一つはloop内に置いているDelayです。

・Setup時間

WiFi接続はSetup内で行うので、Setup時間を計測します。Setup関数の一番最初をSetup_startとしてmillis()で時間を記録します。Setupの最後に、millis()で記録した現在時間とSetup_start時間の差分を取ります。

void setup() {
  start_time = millis(); //←Setupの開始時間
  
 (略) 

  WiFi.begin(SSID, PASSWORD);
  while (connecting){
    Serial.print("WiFi connecting to the AP: ");
    Serial.println(SSID);
    unsigned long connect_start = millis();
    while (WiFi.status() != WL_CONNECTED && connect_start + 10000 > millis()){
      Serial.print (".");
      delay(500);
    }
    if (WiFi.status() == WL_CONNECTED){
      connecting = false;
      delay(500);
    } else {
      Serial.println("retry");
      delay(500);
      esp_deep_sleep(500);
    }
  }

 (略)

  setup_time = start_time - millis(); //setup開始時間と終了時間の差

}

スケッチはこんな感じ。setup_timeはmillis()で計測しているので、単位はミリ秒(ms)です。

・loop_time

loop関数内でのDelay時間もSetup時間と同じやり方ですね。loop関数の最初にloopStartとしてmillis()で時間を記録し、それぞれのDeepSleep直前までのmillis()の差分を取ります。

void loop() {
  loopStart = millis(); //←loop関数開始時間
  
 (略)

  } else {
    nowTime();
    delay(2000);
  
    float HUM = dht.readHumidity();
    float AmbTemp = dht.readTemperature();

  (略)

        } else {
            Serial.println("SLEEP4");
            delay(1000);
            
            loop_time = millis() - loopStart; //←loopStartとの差分
            offset = (setup_time + loop_time)/1000; //s
            
            unsigned long SLEEP4 = (interval*60 - offset) *1000*1000;

            int hour,min,sec;
            hour = (SLEEP4/1000000)/3600;
            min = ((SLEEP4/1000000)%3600)/60;
            sec = (SLEEP4/1000000)%60;

            Serial.print("sleep time ");
            Serial.printf("%d:%d:%d\n", hour,min,sec);

            delay(100);

            esp_deep_sleep(SLEEP4);
          }
  }
}  

スケッチはこんな感じ。こちらも時間はmillis()で計測しているので、単位はミリ秒(ms)です。

Sleep時間を確認する為、秒単位に変換したSleep時間を時、分、秒の形でSerial表示しています。変換方法はcook1293さんのプログラミング交差点に記載の記事そっくりそのまま使わせてもらいました。

時間・分・秒への変換
プログラミング交差点

全部秒単位で表示するより、h:m:sで表示した方が人間には分かりやすいですね。

テスト結果

早速スケッチをコンパイルし、XIAO ESP32C3にインストールして実行してみると。。。

なんと、設定時間直前にDeepSleepから復帰して再びDeepSleepに入り、その後1時間くらい経ってから復帰、設定時間内モードで稼働するようになりました。

実際に苗を入れた育苗箱でテストしたもんで、5月初旬でも朝日がばっちり当たると箱内温度は40℃超まで行きます。枯れやしないかヒヤヒヤしました。あまりの衝撃に画像を保存するのを忘れていましたが、検証の為に復帰時間だけは一部メモしていました。設定時間、時間判定とスリープモードをまとめると下の表の通りです。

開始設定時刻時間判定
1, 2, 3
sleep mode
04 : 24 : 1107 : 10 : 00false, true, true4
05 : 24 : 0107 : 10 : 00false, true, true4
06 : 23 : 5407 : 10 : 00false, true, true2
07 : 09 : 4307 : 10 : 00true, false, true1
08 : 21 : 1907 : 10 : 00true, true, true
08 : 21 : 2707 : 10 : 00true, true, true

開始設定時刻は7:10なので、各復帰時の時間判定基準は全て7:10です。時間判定1は復帰時刻の時間(hour)が設定開始時刻の時間(hour)以上、設定終了時刻の時間(hour)以下ならtrue、それ以外はfalse、時間判定2は復帰時刻が設定開始時刻の時間(hour)が同じで、復帰時間の分(minute)が設定時刻の分(minute)以上でtrue、未満でfalse、時間判定3は、復帰時刻と設定終了時刻の時間(hour)が同じで、復帰時刻の分(minute)が設定終了時刻の分(minute)以下でtrue、超過でfalseです。ループごとに時間判定1,2,3は全てtrueで初期化、毎回時間判定を行って、falseがあれば時間外、全てがtrueで設定時間内判定となります。

sleep modeは、復帰時刻が設定時間外の場合に、設定時刻の時間(hour)と復帰時刻の時間(hour)が同じ場合にsleep1、設定開始時刻の時間(hour)と復帰時刻の時間(hour)の差が1、かつ復帰時刻の分(minute)が設定時刻の分(minute)以上の場合sleep2、それ以外がsleep4です。

復帰時刻06:23:54では、時間判定1はfalse、時間判定2と3は範囲外なのでtrueで、設定時間外判定です。その時のsleep modeは、設定開始時間7:10と復帰時間6:23の時間(hour)の差が1、かつ復帰時間の分(minute)≧設定時刻の分(minute)なので、sleep2です。

この時のsleep2のdeepsleep時間計算は下の通り。

void setup(){
unsigned long setup_start = millis()
(略)
unsigned long setup_time = millis() - setup_start;
}

void loop(){
unsigned long loopStart = millis();
(略)
unsigned long loop_time = millis() - loopStart;

unsigned long offset = setup_time + loop_time;

if (hourStart - sTime.hour == 1 && minuteStart <= sTime.minute){
        unsigned long SLEEP2 = (interval*60 - ((sTime.minute*60+sTime.second)-(minuteStart*60+secondStart)) + offset) *1000*1000;
 
        delay(10000);
        esp_deep_sleep(SLEEP2);
  }
}

sleep2のdeepsleep時間を手計算してみます。offsetを15秒とし、マイクロ秒に換算する為の*1000*1000を無視して、

$$ \begin{eqnarray}
sleep2 \ &=& \ (60 \times 60) \ -\ \{(23 \times 60 + 54) \ -\ (10 \times 60 + 0) \ – \ 15\} \\
&=& \ 2751
\end{eqnarray}$$

2751秒となります。分秒に換算すると、45分51秒ですね。
復帰時間が06:23:54でしたので、45分51秒後は、07:09:45ですね。実績では次の復帰時刻は07:09:43でしたので、大体一致します。sleep2は問題ないですね。

・unsigned longに負の値を入れる

問題はsleep1です。
設定開始時間が07:10:00なので、復帰時刻07:09:43の後のdeepsleep時間は数秒で7:10付近で復帰となるはずですが、実績では07:09:43の次の復帰時刻は08:21:19となっています。

sleep1の計算式は次の通りです。

if (hourStart == sTime.hour){
   unsigned long SLEEP1 = ((minuteStart*60 + secondStart) - ((sTime.minute*60 + sTime.second) + offset)) *1000*1000;

   delay(10000);
   esp_deep_sleep(SLEEP1);

offsetの計算部分は省略です。これを手計算してみると、復帰時間07:09:43、設定開始時間07:10:00、offsetは15秒、マイクロ秒換算の*1000*1000をそのまま計算するとして、

$$ \begin{eqnarray}
sleep1 \ &=& \ ((10 \ \times \ 60 \ + \ 0) \ – \ ((9 \ \times \ 60 \ + \ 43) \ + \ 15)) \ \times 10^{6} \\
&=& \ 2 \times 10^{6}
\end{eqnarray} $$

2 x 106マイクロ秒なので、2秒ですね。WiFi接続時間と途中のDelayを考えるとこんなもんでしょう。ただ、実績では、07:09:43の次の復帰時刻は08:21:19です。DeepSleep時間はマイクロ秒換算で4296 x 106マイクロ秒になります。

最初全く意味が分かりませんでした。ただ、実績のDeepSleep時間は32bitのunsigned longの最大値4,294,967,295に非常に近いです。

少し調べると、unsigned long型の変数に負の値を入れると最大値に近い数字を返す、ということが起こる事が分かりました。Geminiに聞くと、ラップアラウンドという現象が起こって、その範囲の上限に近い数字を返す、とのこと。

unsigned long型を宣言しているSleep1の値が負になったとしたら、DeepSleep時間がunsigned longの上限付近の値となったとしても不思議ではありません。そう思って07:09:43からのDeepSleep時間の計算を見ると、1回WiFi接続に失敗、再接続してsetup時間が数秒伸びるだけで、Sleep1は負となります。

全く同じ状況は再現しづらいので、sleep1が負となるように、offsetの内訳の内、loop_timeの値を調整してみました。現在時刻は13:12:47、設定開始時刻は13:15:00です。結果は下の通り。

ちょっと小さいですが、シリアルモニタの一番下にSleep1として返ってきた時間が表示されています。時間は3831秒。換算すると1時間3分51秒です。本当は2分13秒となるはずのSleep1が1時間超になりました。試運転時に発生した、設定開始時間直前の復帰時刻の次の復帰時刻が1時間以上後、という現象が再現できたことから、起動時間が少し長くてunsigned long型としていたSleep1に負の値が入り、結果としてunsigned longの最大値付近の数字をSleep1として返したことが原因と推測しました。

・そもそもSleep1、Sleep2にはoffsetは不要なのでは

ここまでやっておいてなんですが、気づいてしまいました。現在時刻と設定開始時刻の差を取っているSleep1とSleep2には、setup時間やloop時間を考慮する必要が無いということに。。。

Sleep1、Sleep2の時間を計算する直前に現在時刻を取得するので、起動してからの時間は関係なかったですね。あー、なんでこれ入れてたんだろう。。。

折角unsigned longへの負の値が入って最大値を返される、ということに気づきましたが、不要な議論でした。まあ整数型について再確認できたので良しとしようかな。

・スケッチ

Sleep1、Sleep2の計算式を修正したスケッチを貼っておきます。設定終了時間後の設定となるSleep3、Sleep4はoffset(=setup_time + loop_time)が残っていますが、さすがにインターバルの60minを超えることは無いと思うので、そのままです。本当はマイナス判定とか入れようかと考えてはいました。とりあえずこれでDeepSleep復帰時間は一旦完成です。

#include <DFRobot_SD3031.h>
#include <Wire.h>

#include <OneWire.h>  // For Water Temp.
#include <DallasTemperature.h>
#define ONE_WIRE_BUS D3
#define SENSOR_BIT 9  // resolution of temp meter

OneWire oneWire(ONE_WIRE_BUS);
DallasTemperature sensors(&oneWire);

float pot_Temp;  //pot soil Temp

DFRobot_SD3031 rtc;  

#include <WiFi.h>  //wifi setting
const char SSID[] = "*****";
const char PASSWORD[] = "*******";
WiFiClient client;

#include <time.h>

#include <DHT.h>  //DHT22, ambient temp, humidity
#include <DHT_U.h>
#include <Adafruit_Sensor.h>
#define DHT_pin D0
#define DHTTYPE DHT22
DHT dht(DHT_pin, DHTTYPE);

#include <Ambient.h>  //Ambient setting
unsigned int channelId = *****; //Channel name "Seedling Box"
const char* writeKey = "****************";
Ambient ambient;

const byte hourStart = 7; //Set start time in hour
const byte minuteStart = 30; //Set start time in minute
const byte hourEnd = 16; //Set End time in hour
const byte minuteEnd = 30; //Set End time in minute
const byte secondStart = 0; // fixed 0
const byte secondEnd = 0; // fixed 0

const byte interval = 60; //in minutes

long start_time;
long setup_time;
long loopStart;
long loop_time;
long offset;

#define B1 D1

void setup() {
  start_time = millis();
  Serial.begin(115200);
  struct tm timeInfo;
  rtc.begin();
  dht.begin();

  pinMode(DHT_pin, INPUT);       //D0 --DHT sensor, ambient temp/humidity
  pinMode(B1, OUTPUT);           //D1 --cooling fan(intake, outtake)
                                 //D2 --vacant
  pinMode(ONE_WIRE_BUS, INPUT);  //D3 --ONE WIRE BUS, DS18B20
                                 //D4 --vacant
                                 //D5 --vacant
                                 //D6 --vacant
                                 //D7 --vacant
  pinMode(D8, OUTPUT);           //D8 --Signal, out of setting timezone
                                 //D9 --vacant/caution when you use as LED pin
  pinMode(D10, OUTPUT);          //D10 --Signal, inside setting timezone
  
  bool connecting = true;  //WiFi connection, true = connection in progress, false = connected
  WiFi.begin(SSID, PASSWORD);
  while (connecting){
    Serial.print("WiFi connecting to the AP: ");
    Serial.println(SSID);
    unsigned long connect_start = millis();
    while (WiFi.status() != WL_CONNECTED && connect_start + 10000 > millis()){
      Serial.print (".");
      delay(500);
    }
    if (WiFi.status() == WL_CONNECTED){
      connecting = false;
      delay(500);
    } else {
      Serial.println("retry");
      delay(500);
      esp_deep_sleep(500);
    }
  }

  Serial.println("connected.");
  Serial.print("IP adress:");
  Serial.println(WiFi.localIP());

  ambient.begin(channelId, writeKey, &client);

  configTzTime("JST-9", "ntp.nict.jp","ntp.jst.mfeed.ad.jp");
  getLocalTime(&timeInfo);

  rtc.setTime(timeInfo.tm_year +1900, timeInfo.tm_mon +1, timeInfo.tm_mday, timeInfo.tm_hour, timeInfo.tm_min, timeInfo.tm_sec);

  setup_time = start_time - millis();

}


void loop() {
  loopStart = millis();
  sTimeData_t sTime;  //now time
  sTime = rtc.getRTCTime();

  bool timeJudge1 = true;
  bool timeJudge2 = true;
  bool timeJudge3 = true;

  if (sTime.hour >= hourStart && sTime.hour <= hourEnd){
    timeJudge1 = true;
  } else {
    timeJudge1 = false;
  }

  if (sTime.hour == hourStart && sTime.minute < minuteStart){
    timeJudge2 = false;
  } else if (sTime.hour == hourStart && sTime.minute >= minuteStart){
    timeJudge2 = true;
  }

  if (sTime.hour == hourEnd && sTime.minute > minuteEnd){
    timeJudge3 = false;
  } else if (sTime.hour == hourEnd && sTime.minute <= minuteEnd){
    timeJudge3 = true;
  }

  if (timeJudge1 == true && timeJudge2 == true && timeJudge3 == true){
    nowTime();

    delay(2000);

    float HUM = dht.readHumidity();
    float AmbTemp = dht.readTemperature();

    Serial.print("humidity = ");
    Serial.print(HUM);
    Serial.print(" %");
    Serial.print(", ");
    Serial.print("AmbTemp = ");
    Serial.print(AmbTemp);
    Serial.println(" C");

    ambient.set(1,AmbTemp);
    ambient.set(2,HUM);

    if (AmbTemp >= 32){
      digitalWrite(B1, HIGH);
    } else if (AmbTemp < 29){
      digitalWrite(B1, LOW);
    }

    sensors.requestTemperatures();
    pot_Temp = sensors.getTempCByIndex(0);
    Serial.print("Pot Temp = ");
    Serial.print(pot_Temp);
    Serial.println(" C");

    ambient.set(3, pot_Temp);

    float RTC_TEMP;
    RTC_TEMP = rtc.getTemperatureC();
    Serial.print("RTC Temp = ");
    Serial.print(RTC_TEMP);
    Serial.println(" C");

    ambient.set(4, RTC_TEMP);

    ambient.send();

    delay(5000);

  } else {
    nowTime();

    delay(2000);
  
    float HUM = dht.readHumidity();
    float AmbTemp = dht.readTemperature();

    Serial.print("humidity = ");
    Serial.print(HUM);
    Serial.print(" %");
    Serial.print(", ");
    Serial.print("AmbTemp = ");
    Serial.print(AmbTemp);
    Serial.println(" C");


    ambient.set(1,AmbTemp);
    ambient.set(2,HUM);

    sensors.requestTemperatures();
    pot_Temp = sensors.getTempCByIndex(0);
    Serial.print("Pot Temp = ");
    Serial.print(pot_Temp);
    Serial.println(" C");

    ambient.set(3, pot_Temp);

    float RTC_TEMP;
    RTC_TEMP = rtc.getTemperatureC();
    Serial.print("RTC Temp = ");
    Serial.print(RTC_TEMP);
    Serial.println(" C");

    ambient.set(4, RTC_TEMP);

    ambient.send();

    delay(10000);

    if (hourStart == sTime.hour){
      Serial.println("SLEEP1");
      delay(1000);

      unsigned long SLEEP1 = ((minuteStart*60 + secondStart) - (sTime.minute*60 + sTime.second)) *1000*1000;

      int hour,min,sec;
      hour = (SLEEP1/1000000)/3600;
      min = ((SLEEP1/1000000)%3600)/60;
      sec = (SLEEP1/1000000)%60;

      Serial.print("sleep time ");
      Serial.printf("%d:%d:%d\n", hour,min,sec);

      delay(100);

      esp_deep_sleep(SLEEP1);

    } else if (hourStart - sTime.hour == 1 && minuteStart <= sTime.minute){
        Serial.println("SLEEP2");
        delay(1000);

        unsigned long SLEEP2 = (interval*60 - ((sTime.minute*60+sTime.second) - (minuteStart*60+secondStart))) *1000*1000;

        int hour,min,sec;
        hour = (SLEEP2/1000000)/3600;
        min = ((SLEEP2/1000000)%3600)/60;
        sec = (SLEEP2/1000000)%60;

        Serial.print("sleep time ");
        Serial.printf("%d:%d:%d\n", hour,min,sec);

        delay(100);

        esp_deep_sleep(SLEEP2);

      } else if (sTime.hour == hourEnd || sTime.hour - hourEnd == 1){
          Serial.println("SLEEP3");
          delay(1000);

          loop_time = millis() - loopStart; //ms
          offset = (setup_time + loop_time)/1000; //s
          unsigned long SLEEP3 = (interval*60 - offset) *1000*1000;

          int hour,min,sec;
          hour = (SLEEP3/1000000)/3600;
          min = ((SLEEP3/1000000)%3600)/60;
          sec = (SLEEP3/1000000)%60;

          Serial.print("sleep time ");
          Serial.printf("%d:%d:%d\n", hour,min,sec);

          delay(100);

          esp_deep_sleep(SLEEP3);

        } else {
            Serial.println("SLEEP4");
            delay(1000);
            
            loop_time = millis() - loopStart;
            offset = (setup_time + loop_time)/1000; //s
            
            unsigned long SLEEP4 = (interval*60 - offset) *1000*1000;

            int hour,min,sec;
            hour = (SLEEP4/1000000)/3600;
            min = ((SLEEP4/1000000)%3600)/60;
            sec = (SLEEP4/1000000)%60;

            Serial.print("sleep time ");
            Serial.printf("%d:%d:%d\n", hour,min,sec);

            delay(100);

            esp_deep_sleep(SLEEP4);
        
          }
  }
}  

void nowTime(){
    sTimeData_t sTime;  //now time
    sTime = rtc.getRTCTime();

    Serial.print(sTime.year, DEC);  //year
    Serial.print('/');
    Serial.print(sTime.month, DEC);  //month
    Serial.print('/');
    Serial.print(sTime.day, DEC);  //day
    Serial.print(" (");
    Serial.print(sTime.week);  //week
    Serial.print(") ");
    Serial.print(sTime.hour, DEC);  //hour
    Serial.print(':');
    Serial.print(sTime.minute, DEC);  //minute
    Serial.print(':');
    Serial.print(sTime.second, DEC);  //second
    Serial.println(' ');
}

無駄に遠回りした気もしますが、長くなったので今回はここまで。次は水やり機能追加かな。

コメントを残す

しろすけ技術研究所
主任研究員

こんにちは。市民農園で野菜作りをしているしろすけです。極小規模の栽培管理システム開発をいつかやってみたいと思っていました。2024年から育苗ケース給排気温調や、水耕栽培管理システムの試作を電子工作で始めました。想像以上に検討することがあったので、記録を残すことにします。バイク(SR400)と耕運機(FV200)の整備記録もついでに記録に残しておきます。


しろすけ技術研究所をもっと見る

今すぐ購読し、続きを読んで、すべてのアーカイブにアクセスしましょう。

続きを読む