정해진 시각에 급수를 하기 위한 스케쥴링 규칙은 다음과 같다.
- 스케쥴링에 의해 급수된 마지막 시각을 변수에 저장
- 매일 아침 정해진 시각에 위 변수 값을 체크해서, 정해진 날짜 이상 지났으면 급수
- 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 = 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;
}
}
'아두이노 > 자동 화분 급수기' 카테고리의 다른 글
#4 제작 - 릴레이 멈춤 현상, RC스너버 (0) | 2024.01.03 |
---|---|
#3 기획 및 검토 - 솔레노이드 밸브, 릴레이 (0) | 2023.08.23 |
#2 기획 및 검토 - SSD1306 OLED 모듈(I2C) (0) | 2023.08.13 |
#1 기획 및 검토 - DS1302 RTC 모듈, LCD 모듈(I2C) (0) | 2023.06.23 |