본문 바로가기
아두이노/자동 화분 급수기

#5 제작 - 스케쥴링, unix time(epoch, POSIX) 변환

by songbum 2024. 1. 29.

정해진 시각에 급수를 하기 위한 스케쥴링 규칙은 다음과 같다.

  • 스케쥴링에 의해 급수된 마지막 시각을 변수에 저장
  • 매일 아침 정해진 시각에 위 변수 값을 체크해서, 정해진 날짜 이상 지났으면 급수
    • 12월 ~ 2월 : AM9 에 체크해서 3일 전에 마지막으로 급수했으면 급수
    • 3월 ~ 5월 : AM8 에 체크해서 2일 전에 마지막으로 급수했으면 급수
    • 6월 ~ 9월 : AM7 에 체크해서 1일 전에 마지막으로 급수했으면 급수
    • 10월 ~ 11월 :  AM8 에 체크해서 2일 전에 마지막으로 급수했으면 급수
  • 몇 초 동안 밸브를 열어 급수를 할지는, 실제 사용해보면서 공급되는 물의 양을 체크해서 조정

 

시간 계산이 필요하므로 날짜 데이터를 unix time 포맷으로 저장하는 게 좋을 거 같은데, 사용중인 virtuabotixRTC 라이브러리에서는 unix time 포맷은 지원하지 않는다.  그래서 년/월/일/시/분/초 포맷을 unix time 포맷으로 쉽게 변환할 수 있는 방법을 찾아야했다.

 

인터넷을 검색해서 Time 라이브러리를 찾았다.  아두이노 IDE 에서 바로 사용할 수는 없고 수동으로 라이브러리를 추가해줘야해서 아래 gitHub 에서 zip 파일을 다운로드 받았다.

https://github.com/PaulStoffregen/Time

 

GitHub - PaulStoffregen/Time: Time library for Arduino

Time library for Arduino. Contribute to PaulStoffregen/Time development by creating an account on GitHub.

github.com

 

아두이노 IDE 에서 Sketch -> Include Library > Add .Zip Library 로 들어간 후, 다운로드 받은 zip 파일을 선택해줬다. 

 

그리고 아두이노 IDE 에서 다음과 같이 코딩한 후 실행시켜 봤다.

#include <TimeLib.h>

void setup() {
  Serial.begin(9600);

  time_t nowTime = convert_unixtime(2024, 2, 6, 9, 0, 0);
  Serial.println(nowTime);  
}

void loop() {
}

time_t convert_unixtime(int YYYY, byte MM, byte DD, byte hh, byte mm, byte ss) {
  tmElements_t tmEle;
  tmEle.Year = YYYY - 1970;
  tmEle.Month = MM;
  tmEle.Day = DD;
  tmEle.Hour = hh;
  tmEle.Minute = mm;
  tmEle.Second = ss;
  return makeTime(tmEle);
}

 

convert_unixtime() 함수는 년,월,일,시,분,초 를 파라미터로 받아 unix time 포맷으로 변환해 리턴해준다.  위와 같이 2024년 2월 6일 9시 0분 0초를 unix time 으로 변환하면 1707210000 이 나온다.  온라인 변환기(https://www.epochconverter.com/)에서 이 값을 다시 변환해 보니 정확하게 복원 되는 것을 확인할 수 있었다.

 

이 함수를 이용하면 아두이노 프로그램 코드가 좀 더 직관적이 돼서 작업을 쉽게 할 수 있을 거다.

 

 

스케쥴링 규칙 부분은 복잡하기도 하고 자주 바뀔 수 있는 부분이라 별도의 함수로 분리해 작성했다.

chk_schedule() 함수는 현재 시각을 파라미터로 받아 분석해서, 급수를 해야 시간대이면 true 를 리턴하고 아니면 false 를 리턴하도록 했다.

bool chk_schedule(int year, byte month, byte day, byte hour, byte minutes, byte seconds) {
  byte selectedHour;
  long termDay;
  byte feedSeconds = 5;

  switch (month) {
    case 12:
    case 1:
    case 2:
      selectedHour = 9;
      termDay = 3 * 86400;
      break;
    case 3:
    case 4:
    case 5:
      selectedHour = 8;
      termDay = 2 * 86400;
      feedSeconds += 1;
      break;
    case 6:
    case 7:
    case 8:
      selectedHour = 7;
      termDay = 1 * 86400;
      feedSeconds += 2;
      break;
    case 9:
    case 10:
    case 11:
      selectedHour = 8;
      termDay = 2 * 86400;
      feedSeconds += 1;
      break;
  }
  
  // 급수 여부를 체크해야 하는 시간대임
  if ( (hour == selectedHour) && (minutes == 0) ) {
    time_t cTime_unixtime = convert_unixtime(year, month, day, hour, minutes, seconds);
    // 급수를 시작하지 않은 상태임
    if (feedingStart <= feedingEnd) {
      // 마지막으로 급수한 이후, 시간이 많이 안 지났으므로 급수하지 않음
      if ( cTime_unixtime < (feedingStart + termDay) ) {
        return false;
      }
      // 마지막으로 급수한 이후, 정해놓은 시간이 지났으므로 다시 급수해야 함
      else {      
        return true;
      }
    }
    // 급수를 시작한 상태임
    else {
      // 정해진 시간(초) 동안은 계속 급수
      if ( cTime_unixtime <= (feedingStart + feedSeconds) ) {
        return true;
      }
      // 정해진 시간이 지났으니 급수를 종료
      else {
        return false;
      }
    }
  }
  // 급수 여부를 체크해야 하는 시간대가 아님
  else {
    return false;
  }
}

 

selectedHour 변수는 급수를 시작하는 시(hour) 값으로, 계절에 따라 값이 달라져야 하므로 switch 문으로 현재 월(month) 에 따라 적절한 값을 저장하도록 했다. 

termDay 변수는 마지막으로 급수를 한 이후 며칠이 지난 후에야 다시 급수를 할지를 지정하는 값으로, 마찬가지로 switch 문에서 값을 저장하도록 했다.  초(seconds) 단위로 값을 저장하며, 하루가 86400초(60초 X 60분 X 24시간) 이므로 

termDay = 3 * 86400 은 3일후에 다시 급수를 하라는 의미가 된다.

feedSeconds 변수는 급수를 몇 초 동안 할 지를 저장하는 값으로, 기본적으로 5초 동안 하되 switch 문을 이용해 계절에 따라 조금 더 하거나 덜 할 수 있도록 했다.  현재 구현된 로직으로는 최대 60초를 넘을 수 없다는 제약이 생기지만, 실 사용에서는 문제되지 않을 거 같다.

 

feedingStart 변수는 함수 외부에서 정의된 값으로, 급수가 시작된 시각을 저장해 놓게 된다.  위 chk_schedule() 함수를 호출해서 true 가 리턴되면 급수를 시작하면서 이 변수에 현재 시각을 저장해 놓게 된다.

feedingEnd 변수는 함수 외부에서 정의된 값으로, 급수가 종료된 시각을 저장해 놓게 된다.   chk_schedule() 함수를 호출해서 false 가 리턴되면 급수를 종료하면서 이 변수에 현재 시각을 저장해 놓게 된다.

 

 

이제 아두이노 IDE 에서 아래와 같이 프로그램을 완성한 후 업로드해준다.

#include <virtuabotixRTC.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <TimeLib.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C  // 0x3D for 128x64, 0x3C for 128x32

virtuabotixRTC myRTC(2,3,4);

String nowTime;

long feedingStart;
long feedingEnd;

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

int valvePin = 5;
int switch1Pin = 6;
int switch2Pin = 7;
int switch1Value;
int switch2Value;
bool isDelay;

void setup() {
  Serial.begin(9600);
  //myRTC.setDS1302Time(0, 28, 23, 1, 21, 1, 2024);
  while(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS) && millis()<5000) {    
  }
  display.display();

  digitalWrite(valvePin, HIGH);
  pinMode(valvePin, OUTPUT);

  pinMode(switch1Pin, INPUT_PULLUP);
  pinMode(switch2Pin, INPUT_PULLUP);

  // 변수 초기화
  feedingStart = convert_unixtime(2024, 1, 1, 0, 0, 0);
  feedingEnd = feedingStart;
}

void loop() {
  myRTC.updateTime();

  nowTime = String(myRTC.year) + "/" + String(myRTC.month) + "/" + String(myRTC.dayofmonth) + "\n";
  nowTime += String(myRTC.hours) + ":" + String(myRTC.minutes) + ":" + String(myRTC.seconds);
  //Serial.println(nowTime);

  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(SSD1306_WHITE);
  display.setCursor(0, 0);
  display.println(nowTime);
  display.display();

  isDelay = false;
  switch1Value = digitalRead(switch1Pin);
  switch2Value = digitalRead(switch2Pin);
  if ( (switch1Value == 1) && (switch2Value == 1) ) {
    if (digitalRead(valvePin) == LOW) {
      Serial.println("Manual Stop");
      digitalWrite(valvePin, HIGH);
      delay(200);
      isDelay = true;
    }
  }
  else if ( (switch1Value == 1) && (switch2Value == 0) ) {
    if (digitalRead(valvePin) == HIGH) {
      Serial.println("Manual Start");
      digitalWrite(valvePin, LOW);
      isDelay = true;
      delay(200);
    }
  }
  else if ( (switch1Value == 0) && (switch2Value == 1) ) {
    //Serial.println("Schedule");     

    // 급수해야 하는 시간대임
    if (chk_schedule(myRTC.year, myRTC.month, myRTC.dayofmonth, myRTC.hours, myRTC.minutes, myRTC.seconds) == true) {
      // 현재 급수 중이 아닐 때는 급수를 시작
      if (digitalRead(valvePin) == HIGH) {
        Serial.println("Start");
        digitalWrite(valvePin, LOW);
        isDelay = true;
        feedingStart = convert_unixtime(myRTC.year, myRTC.month, myRTC.dayofmonth, myRTC.hours, myRTC.minutes, myRTC.seconds);
        Serial.println(feedingStart);
        delay(200);
      }      
    }
    // 급수하면 안 되는 시간대임
    else {
      // 이미 급수 중일 때는 급수를 중지
      if (digitalRead(valvePin) == LOW) {
        Serial.println("Stop");
        digitalWrite(valvePin, HIGH);
        isDelay = true;
        feedingEnd = convert_unixtime(myRTC.year, myRTC.month, myRTC.dayofmonth, myRTC.hours, myRTC.minutes, myRTC.seconds);
        Serial.println(feedingEnd);
        delay(200);
      }
    }
  }

  if (isDelay == true) {
    delay(800);
  }
  else {
    delay(1000);
  }
}

time_t convert_unixtime(int year, byte month, byte day, byte hour, byte minutes, byte seconds) {
  tmElements_t tmEle;
  tmEle.Year = year - 1970;
  tmEle.Month = month;
  tmEle.Day = day;
  tmEle.Hour = hour;
  tmEle.Minute = minutes;
  tmEle.Second = seconds;
  return makeTime(tmEle);
}

bool chk_schedule(int year, byte month, byte day, byte hour, byte minutes, byte seconds) {
  byte selectedHour;
  int termDay;
  byte feedSeconds = 5;

  switch (month) {
    case 12:
    case 1:
    case 2:
      selectedHour = 9;
      termDay = 3 * 86400;
      break;
    case 3:
    case 4:
    case 5:
      selectedHour = 8;
      termDay = 2 * 86400;
      feedSeconds += 1;
      break;
    case 6:
    case 7:
    case 8:
      selectedHour = 7;
      termDay = 1 * 86400;
      feedSeconds += 2;
      break;
    case 9:
    case 10:
    case 11:
      selectedHour = 8;
      termDay = 2 * 86400;
      feedSeconds += 1;
      break;
  }

  // 급수 여부를 체크해야 하는 시간대임
  if ( (hour == selectedHour) && (minutes == 0) ) {
    time_t cTime_unixtime = convert_unixtime(year, month, day, hour, minutes, seconds);
    // 급수를 시작하지 않은 상태임
    if (feedingStart <= feedingEnd) {
      // 마지막으로 급수한 이후, 시간이 많이 안 지났으므로 급수하지 않음
      if ( cTime_unixtime < (feedingStart + termDay) ) {
        return false;
      }
      // 마지막으로 급수한 이후, 정해놓은 시간이 지났으므로 다시 급수해야 함
      else {      
        return true;
      }
    }
    // 급수를 시작한 상태임
    else {
      // 정해진 시간(초) 동안은 계속 급수
      if ( cTime_unixtime <= (feedingStart + feedSeconds) ) {
        return true;
      }
      // 정해진 시간이 지났으니 급수를 종료
      else {
        return false;
      }
    }
  }
  // 급수 여부를 체크해야 하는 시간대임
  else {
    return false;
  }
}

 

실제 수도꼭지에 연결하고 테스트를 해 봤는데, 현재가 3월이므로 chk_schedule() 함수에서 정의한 대로 2일 간격으로 아침 8시에 6초간 물이 나와야 한다.  급수된 물이 모이도록 아래에 세숫대야를 놓은 후 며칠 동안 수시로 체크해 봤는데, 스케쥴대로 정상작동하고 있음을 확인할 수 있었다.