#include <Arduino.h>
#include <WiFi.h>
#include <time.h>
#include <LiquidCrystal_I2C.h>
#include <DHT.h>
DHT dht(15, DHT22);
LiquidCrystal_I2C lcd(0x27, 20, 4);
bool setClockStatus = false;
bool setDhtStatus = false;
int prevYear = -1, prevMonth = -1, prevMday = -1, prevWday = -1, prevHour = -1, prevMinute = -1, prevSecond = -1; // 기존 값 비교를 위해 시간 데이터 범위 밖의 값으로 초기화
float prevTemp = -41.0, prevHumi = -1.0; // 기존값 비교를 위해 센서 감지 범위 밖의 값으로 초기화
unsigned long ntpMillis = 0;
const char* ssid = "Wokwi-GUEST"; // WiFi 이름 (2.4GHz 전용)
const char* password = ""; // WiFi 비밀번호
/*
NTP 서버 리스트
pool.ntp.org - NTP 서버 자동 지정
time.google.com - 구글 제공 NTP 서버
time.windows.com - 마이크로소프트 제공 NTP 서버(Windows 운영체제 기본값)
time.apple.com - 애플 제공 NTP 서버(Apple 기기 기본값)
ntp.ubuntu.com - 우분투(리눅스) 제공 NTP 서버
ntp[1~3].kr - 한국 공인 NTP 서버
time.kma.go.kr - 한국 기상청 제공 NTP 서버, KST
ntp[1~3].kornet.net - 코넷 재공 NTP 서버, KST
ntp.kisa.or.kr - 한국인터넷진흥원 제공 NTP 서버, KST
*/
const char* ntpServer1 = "time.windows.com"; // 메인 NTP 서버 지정
const char* ntpServer2 = "pool.ntp.org"; // 보조 NTP 서버 지정
const long gmtValue = +9; // 시간대 설정
const long gmtOffset_sec = 3600 * (gmtValue); //
const int daylightOffset_sec = 0; // 서머타임
byte degreeSymbol[8] = { // 섭씨 기호 사용자 지정
0b00011,
0b00011,
0b00000,
0b00000,
0b00000,
0b00000,
0b00000,
0b00000
};
void setup() {
WiFi.disconnect(true); // 기존 연결 상태 초기화
lcd.init();
lcd.backlight();
dht.begin();
lcd.createChar(0, degreeSymbol); // 사용자 지정 기호 생성
// 시작 인트로 화면
lcd.clear();
lcd.setCursor(0, 1);
lcd.print("ESP32 CLOCK & DHT22");
lcd.setCursor(3, 2);
lcd.print("Initializing..");
delay(2000);
// WiFi 연결 화면
lcd.clear();
lcd.setCursor(0, 1);
lcd.print("Wi-Fi Connecting to");
lcd.setCursor(0, 2);
lcd.print(ssid);
lcd.print("...");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
// 연결 완료 여부 확인 및 재연결 시도 함수
tryReconnectWiFi();
// 연결 완료 여부 확인 후 코드 진행 및 재부팅
if (WiFi.status() == WL_CONNECTED) {
lcd.clear();
lcd.setCursor(5, 1);
lcd.print("Connected!");
delay(500);
} else {
lcd.clear();
lcd.setCursor(1, 1);
lcd.print("Failed to connect.");
lcd.setCursor(0, 2);
lcd.print("Rebooting in 5 sec..");
delay(5000);
ESP.restart();
}
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2); // NTP 서버 설정
ntpMillis = millis();
}
void loop() {
unsigned long nowMillis = millis();
printLocalTime(); // 시계 출력 함수
printDHT(); // DHT22 온습도센서 출력 함수
if (nowMillis - ntpMillis >= 3600000) { // 1시간 마다 NTP <-> ESP32 시간 보정
ntpMillis = nowMillis;
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2);
}
}
void printLocalTime() {
struct tm timeinfo; // 시간 구조체 생성
if (!getLocalTime(&timeinfo)) { // NTP 서버 시간 불러오기 실패시
lcd.setCursor(0, 0);
lcd.print("Failed to retrieve ");
lcd.setCursor(0, 1);
lcd.print("time from NTP server");
setClockStatus = false;
return;
}
if (!setClockStatus) { // 날짜와 시간 사이의 점과 콜론 초기 생성
setClock();
setClockStatus = true;
}
// 현재 값과 이전 값을 비교하여 변할 경우만 출력
// lcd.clear() 함수 사용 안함으로서 LCD 전체 깜빡임 현상 해결
if (prevSecond != timeinfo.tm_sec) { // 초
prevSecond = timeinfo.tm_sec;
lcd.setCursor(9, 1);
lcd.print(&timeinfo, "%S");
}
if (prevMinute != timeinfo.tm_min) { // 분
prevMinute = timeinfo.tm_min;
lcd.setCursor(6, 1);
lcd.print(&timeinfo, "%M");
}
if (prevHour != timeinfo.tm_hour) { // 시간
prevHour = timeinfo.tm_hour;
lcd.setCursor(0, 1);
lcd.print(&timeinfo, "%p %I"); // 24시간 형식 : %H <== 사용시 setClock() 함수 수정 필요!
}
if (prevWday != timeinfo.tm_wday) { // 요일
prevWday = timeinfo.tm_wday;
switch (timeinfo.tm_wday) { // 3글자 축약 형식 : %a
// 글자 수 6개
case 0: // Sunday
case 1: // Monday
case 5: // Friday
lcd.setCursor(11, 0);
lcd.print(&timeinfo, "%A ");
break;
// 글자 수 7개
case 2: // Tuesday
lcd.setCursor(11, 0);
lcd.print(&timeinfo, "%A ");
break;
// 글자 수 8개
case 4: // Thursday
case 6: // Saturday
lcd.setCursor(11, 0);
lcd.print(&timeinfo, "%A ");
break;
// 글자 수 9개
case 3: // Wednesday
lcd.setCursor(11, 0);
lcd.print(&timeinfo, "%A");
break;
}
}
if (prevMday != timeinfo.tm_mday) { // 일
prevMday = timeinfo.tm_mday;
lcd.setCursor(8, 0);
lcd.print(&timeinfo, "%d");
}
if (prevMonth != timeinfo.tm_mon) { // 월
prevMonth = timeinfo.tm_mon;
lcd.setCursor(5, 0);
lcd.print(&timeinfo, "%m");
}
if (prevYear != timeinfo.tm_year) { // 년
prevYear = timeinfo.tm_year;
lcd.setCursor(0, 0);
lcd.print(&timeinfo, "%Y");
}
}
void printDHT() {
// 현재 센서값과 이전 센서값을 비교하여 다를 경우에만 출력, LCD 깜빡임 해결
float temp = dht.readTemperature();
float humi = dht.readHumidity();
// 센서 값을 불러올 수 없는 경우
if (isnan(temp) || isnan(humi)) {
lcd.setCursor(0, 2);
lcd.print("Failed to retrieve ");
lcd.setCursor(0, 3);
lcd.print("Temp and Humi values");
setDhtStatus = false;
prevTemp = -41.0;
prevHumi = -1.0;
return;
}
// 기본 템플릿으로 초기화 되지 않은 경우
if (!setDhtStatus) {
setDHT();
setDhtStatus = true;
}
// 기존 온도와 현재 온도가 다를 경우
if (setDhtStatus && prevTemp != temp) {
prevTemp = temp;
lcd.setCursor(13, 2);
lcd.print(" ");
if (temp < -10.0) { // 온도, 습도 값 출력 시 16, N 칸에 소수점을 맞춤.
lcd.setCursor(13, 2);
lcd.print(temp, 1);
} else if (temp >= 0.0 && temp < 10.0) {
lcd.setCursor(15, 2);
lcd.print(temp, 1);
} else {
lcd.setCursor(14, 2);
lcd.print(temp, 1);
}
}
// 기존 습도와 현재 습도가 다를 경우
if (setDhtStatus && prevHumi != humi) {
prevHumi = humi;
lcd.setCursor(13, 3);
lcd.print(" ");
if (humi < 10.0) { // 온도, 습도 값 출력 시 16, N 칸에 소수점을 맞춤.
lcd.setCursor(15, 3);
lcd.print(humi, 1);
} else if (humi == 100.0) {
lcd.setCursor(13, 3);
lcd.print(humi, 1);
} else {
lcd.setCursor(14, 3);
lcd.print(humi, 1);
}
}
}
void tryReconnectWiFi() {
unsigned long startAttemptTime = millis();
const unsigned long timeout = 10000;
const int maxRetries = 5;
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < maxRetries) {
if (millis() - startAttemptTime >= timeout) {
lcd.clear();
lcd.setCursor(1, 1);
lcd.print("Failed to connect.");
lcd.setCursor(3, 2);
lcd.print("Reconnecting..");
WiFi.disconnect(true);
WiFi.begin(ssid, password);
startAttemptTime = millis();
attempts++;
}
delay(500);
}
}
void setClock() {
lcd.setCursor(0, 0);
lcd.print(" . . ");
lcd.setCursor(0, 1);
//lcd.print(" : : "); // 24시간제
lcd.print(" : : "); // 12시간제
}
void setDHT() {
lcd.setCursor(0, 2);
lcd.print("Temperature : ");
lcd.setCursor(18, 2);
lcd.write(byte(0));
lcd.print("C");
lcd.setCursor(0, 3);
lcd.print("Humidity : %");
}