#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <DHT.h>
#include <RTClib.h>
// Konfigurasi OLED
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Konfigurasi RTC DS1307
RTC_DS1307 rtc;
// Konfigurasi Sensor & DHT
#define DHTPIN 4
#define DHTTYPE DHT22
DHT dht(DHTPIN, DHTTYPE);
// Konfigurasi Relay (6 Relay)
const int RELAY_K1_SPD1 = 25;
const int RELAY_K1_SPD2 = 26;
const int RELAY_K1_SPD3 = 27;
const int RELAY_K2_SPD1 = 14;
const int RELAY_K2_SPD2 = 12;
const int RELAY_K2_SPD3 = 13;
// Konfigurasi Rotary Encoder
#define ENCODER_CLK 18
#define ENCODER_DT 19
#define ENCODER_SW 5
// Variabel Global Suhu
float currentTemp = 0;
float tempUpper = 30.0;
float tempLower = 27.0;
unsigned long lastMeasureTime = 0;
int last_clk_state;
bool last_sw_state = HIGH;
unsigned long sw_press_time = 0;
// Batasan Suhu (Limits)
const float MAX_TEMP_UPPER = 33.0;
const float MIN_TEMP_LOWER = 20.0;
// Variabel Waktu Operasional
int startHour = 7;
int startMin = 0;
int endHourNormal = 15;
int endMinNormal = 0;
int endHourFriday = 14;
int endMinFriday = 0;
bool workDaysOnly = true;
bool isSystemActiveTime = true;
// State Navigasi Menu
enum MenuState {
MAIN_SCREEN, MENU_LIST, MENU_TEMP_SUB, MENU_TIME_SUB,
SET_UPPER, SET_LOWER, SET_K1, SET_K2, SET_MODE,
SET_START_TIME, SET_END_NORM, SET_END_FRI, SET_WORKDAYS
};
MenuState currentState = MAIN_SCREEN;
// Konfigurasi Mode Kipas
bool isGroupMode = true;
// Menu Utama
int menuIndex = 0;
const char* menuLabelsGroup[] = {"Pengaturan Suhu", "Pengaturan Waktu", "Mode: GRUP", "Kontrol Kipas", "Kembali"};
const char* menuLabelsSingle[] = {"Pengaturan Suhu", "Pengaturan Waktu", "Mode: MANDIRI", "Kontrol Kipas 1", "Kontrol Kipas 2", "Kembali"};
// Sub-Menu Suhu
int tempSubIndex = 0;
const int totalTempSubItems = 3;
const char* tempSubLabels[] = {"Suhu Atas (ON)", "Suhu Bawah (OFF)", "Kembali"};
// Sub-Menu Waktu
int timeSubIndex = 0;
const int totalTimeSubItems = 5;
const char* timeSubLabels[] = {"Jam Mulai", "Jam Selesai (S-K)", "Jam Selesai (Jmt)", "Hari Aktif", "Kembali"};
// Variabel Kontrol Internal Halaman
// subMenuSelection: 0 = Edit Jam, 1 = Edit Menit, 2 = Tombol Simpan Fokus
int subMenuSelection = 0;
// Mode Kipas (0:OFF, 1:SPD1, 2:SPD2, 3:SPD3, 4:AUTO)
int fan1Mode = 4;
int fan2Mode = 4;
void setup() {
Serial.begin(115200);
int relayPins[] = {RELAY_K1_SPD1, RELAY_K1_SPD2, RELAY_K1_SPD3, RELAY_K2_SPD1, RELAY_K2_SPD2, RELAY_K2_SPD3};
for(int pin : relayPins) {
pinMode(pin, OUTPUT);
digitalWrite(pin, LOW);
}
pinMode(ENCODER_CLK, INPUT);
pinMode(ENCODER_DT, INPUT);
pinMode(ENCODER_SW, INPUT_PULLUP);
dht.begin();
if (!rtc.begin()) {
Serial.println("RTC tidak ditemukan");
}
if (!rtc.isrunning()) {
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
for(;;);
}
display.clearDisplay();
last_clk_state = digitalRead(ENCODER_CLK);
last_sw_state = digitalRead(ENCODER_SW);
}
void stopFan1() {
digitalWrite(RELAY_K1_SPD1, LOW); digitalWrite(RELAY_K1_SPD2, LOW); digitalWrite(RELAY_K1_SPD3, LOW);
}
void stopFan2() {
digitalWrite(RELAY_K2_SPD1, LOW); digitalWrite(RELAY_K2_SPD2, LOW); digitalWrite(RELAY_K2_SPD3, LOW);
}
void setFan1Speed(int speed) {
stopFan1();
if (speed == 1) digitalWrite(RELAY_K1_SPD1, HIGH);
else if (speed == 2) digitalWrite(RELAY_K1_SPD2, HIGH);
else if (speed == 3) digitalWrite(RELAY_K1_SPD3, HIGH);
}
void setFan2Speed(int speed) {
stopFan2();
if (speed == 1) digitalWrite(RELAY_K2_SPD1, HIGH);
else if (speed == 2) digitalWrite(RELAY_K2_SPD2, HIGH);
else if (speed == 3) digitalWrite(RELAY_K2_SPD3, HIGH);
}
void checkOperatingTime() {
DateTime now = rtc.now();
int currentHour = now.hour();
int currentMin = now.minute();
int currentDay = now.dayOfTheWeek();
bool isWorkDay = (currentDay >= 1 && currentDay <= 5);
int activeEndHour = (currentDay == 5) ? endHourFriday : endHourNormal;
int activeEndMin = (currentDay == 5) ? endMinFriday : endMinNormal;
// Hitung total menit dari tengah malam untuk perbandingan yang mudah
int nowMins = currentHour * 60 + currentMin;
int startMins = startHour * 60 + startMin;
int endMins = activeEndHour * 60 + activeEndMin;
bool isInHourRange = (nowMins >= startMins && nowMins < endMins);
if (workDaysOnly) {
isSystemActiveTime = isWorkDay && isInHourRange;
} else {
isSystemActiveTime = isInHourRange;
}
}
void loop() {
checkOperatingTime();
// 1. Logika Sensor & Kipas
if (millis() - lastMeasureTime > 2000) {
float t = dht.readTemperature();
if (!isnan(t)) currentTemp = t;
lastMeasureTime = millis();
auto processFan = [&](int &mode, void (*setSpd)(int), void (*stopF)()) {
if (!isSystemActiveTime) {
stopF();
return;
}
if (mode == 4) { // Mode AUTO
if (currentTemp >= tempUpper) {
if (currentTemp >= tempUpper + 2.0) setSpd(3);
else if (currentTemp >= tempUpper + 1.0) setSpd(2);
else setSpd(1);
} else if (currentTemp <= tempLower) stopF();
} else { // Mode Manual
if (mode == 0) stopF();
else setSpd(mode);
}
};
if (isGroupMode) {
processFan(fan1Mode, setFan1Speed, stopFan1);
processFan(fan1Mode, setFan2Speed, stopFan2);
} else {
processFan(fan1Mode, setFan1Speed, stopFan1);
processFan(fan2Mode, setFan2Speed, stopFan2);
}
}
// 2. Input Rotary Encoder
int current_clk_state = digitalRead(ENCODER_CLK);
if (current_clk_state != last_clk_state) {
if (current_clk_state == LOW) {
bool clkUp = (digitalRead(ENCODER_DT) != current_clk_state);
int totalItems = isGroupMode ? 5 : 6;
switch(currentState) {
case MENU_LIST:
if (clkUp) menuIndex = (menuIndex + 1) % totalItems;
else menuIndex = (menuIndex - 1 + totalItems) % totalItems;
break;
case MENU_TEMP_SUB:
if (clkUp) tempSubIndex = (tempSubIndex + 1) % totalTempSubItems;
else tempSubIndex = (tempSubIndex - 1 + totalTempSubItems) % totalTempSubItems;
break;
case MENU_TIME_SUB:
if (clkUp) timeSubIndex = (timeSubIndex + 1) % totalTimeSubItems;
else timeSubIndex = (timeSubIndex - 1 + totalTimeSubItems) % totalTimeSubItems;
break;
case SET_UPPER:
case SET_LOWER:
case SET_K1:
case SET_K2:
case SET_MODE:
case SET_START_TIME:
case SET_END_NORM:
case SET_END_FRI:
case SET_WORKDAYS:
if (subMenuSelection == 0) { // Mode Edit Jam (atau Parameter Tunggal)
if (currentState == SET_UPPER) {
if (clkUp) tempUpper += 0.5; else tempUpper -= 0.5;
if (tempUpper > MAX_TEMP_UPPER) tempUpper = MAX_TEMP_UPPER;
if (tempUpper <= tempLower) tempUpper = tempLower + 0.5;
} else if (currentState == SET_LOWER) {
if (clkUp) tempLower += 0.5; else tempLower -= 0.5;
if (tempLower < MIN_TEMP_LOWER) tempLower = MIN_TEMP_LOWER;
if (tempLower >= tempUpper) tempLower = tempUpper - 0.5;
} else if (currentState == SET_K1 || currentState == SET_K2) {
int &mode = (currentState == SET_K1) ? fan1Mode : fan2Mode;
if (clkUp) mode = (mode + 1) % 5; else mode = (mode - 1 + 5) % 5;
} else if (currentState == SET_MODE) {
isGroupMode = !isGroupMode;
} else if (currentState == SET_START_TIME) {
if (clkUp) startHour = (startHour + 1) % 24; else startHour = (startHour - 1 + 24) % 24;
} else if (currentState == SET_END_NORM) {
if (clkUp) endHourNormal = (endHourNormal + 1) % 24; else endHourNormal = (endHourNormal - 1 + 24) % 24;
} else if (currentState == SET_END_FRI) {
if (clkUp) endHourFriday = (endHourFriday + 1) % 24; else endHourFriday = (endHourFriday - 1 + 24) % 24;
} else if (currentState == SET_WORKDAYS) {
workDaysOnly = !workDaysOnly;
}
}
else if (subMenuSelection == 1) { // Mode Edit Menit (Kelipatan 5)
auto adjustMin = [&](int &m) {
if (clkUp) m = (m + 5) % 60;
else m = (m - 5 + 60) % 60;
};
if (currentState == SET_START_TIME) adjustMin(startMin);
else if (currentState == SET_END_NORM) adjustMin(endMinNormal);
else if (currentState == SET_END_FRI) adjustMin(endMinFriday);
}
break;
default: break;
}
}
last_clk_state = current_clk_state;
}
// 3. Input Button (Short Press Only)
bool current_sw_state = digitalRead(ENCODER_SW);
if (current_sw_state == LOW && last_sw_state == HIGH) {
delay(50); // Debounce sederhana
}
if (current_sw_state == HIGH && last_sw_state == LOW) {
switch(currentState) {
case MAIN_SCREEN:
currentState = MENU_LIST;
break;
case MENU_LIST:
if (menuIndex == 0) currentState = MENU_TEMP_SUB;
else if (menuIndex == 1) currentState = MENU_TIME_SUB;
else if (menuIndex == 2) { currentState = SET_MODE; subMenuSelection = 0; }
else if (isGroupMode) {
if (menuIndex == 3) { currentState = SET_K1; subMenuSelection = 0; }
else currentState = MAIN_SCREEN;
} else {
if (menuIndex == 3) { currentState = SET_K1; subMenuSelection = 0; }
else if (menuIndex == 4) { currentState = SET_K2; subMenuSelection = 0; }
else currentState = MAIN_SCREEN;
}
break;
case MENU_TEMP_SUB:
if (tempSubIndex == 0) { currentState = SET_UPPER; subMenuSelection = 0; }
else if (tempSubIndex == 1) { currentState = SET_LOWER; subMenuSelection = 0; }
else currentState = MENU_LIST;
break;
case MENU_TIME_SUB:
if (timeSubIndex == 0) { currentState = SET_START_TIME; subMenuSelection = 0; }
else if (timeSubIndex == 1) { currentState = SET_END_NORM; subMenuSelection = 0; }
else if (timeSubIndex == 2) { currentState = SET_END_FRI; subMenuSelection = 0; }
else if (timeSubIndex == 3) { currentState = SET_WORKDAYS; subMenuSelection = 0; }
else currentState = MENU_LIST;
break;
case SET_UPPER:
case SET_LOWER:
case SET_MODE:
case SET_WORKDAYS:
case SET_K1:
case SET_K2:
if (subMenuSelection == 0) subMenuSelection = 2;
else {
if (currentState == SET_UPPER || currentState == SET_LOWER) currentState = MENU_TEMP_SUB;
else currentState = MENU_LIST;
subMenuSelection = 0;
}
break;
case SET_START_TIME:
case SET_END_NORM:
case SET_END_FRI:
if (subMenuSelection == 0) subMenuSelection = 1; // Pindah ke Menit
else if (subMenuSelection == 1) subMenuSelection = 2; // Pindah ke Simpan
else {
currentState = MENU_TIME_SUB;
subMenuSelection = 0;
}
break;
}
}
last_sw_state = current_sw_state;
drawUI();
}
void drawUI() {
display.clearDisplay();
display.setTextColor(SSD1306_WHITE);
if (currentState == MAIN_SCREEN) {
DateTime now = rtc.now();
display.setTextSize(1);
char timeStr[20];
sprintf(timeStr, "%02d:%02d:%02d", now.hour(), now.minute(), now.second());
display.setCursor(0, 0); display.print(timeStr);
display.setCursor(95, 0); display.print(isSystemActiveTime ? "ACT" : "SLP");
display.drawLine(0, 9, 128, 9, SSD1306_WHITE);
display.setCursor(10, 18); display.print("Suhu:");
display.setTextSize(3);
display.setCursor(45, 18); display.print(currentTemp, 1);
display.setTextSize(1);
int activeEndH = (now.dayOfTheWeek() == 5) ? endHourFriday : endHourNormal;
int activeEndM = (now.dayOfTheWeek() == 5) ? endMinFriday : endMinNormal;
display.setCursor(0, 45); display.print("Aktif: ");
char schedStr[15];
sprintf(schedStr, "%02d:%02d-%02d:%02d", startHour, startMin, activeEndH, activeEndM);
display.print(schedStr);
display.setCursor(0, 56); display.print(workDaysOnly ? "Senin-Jumat" : "Setiap Hari");
display.setCursor(85, 56);
if (isGroupMode) {
display.print("G:"); display.print(fan1Mode == 4 ? "A" : (fan1Mode == 0 ? "O" : String(fan1Mode)));
} else {
display.print("1:"); display.print(fan1Mode == 4 ? "A" : String(fan1Mode));
display.print(" 2:"); display.print(fan2Mode == 4 ? "A" : String(fan2Mode));
}
}
else if (currentState == MENU_LIST || currentState == MENU_TEMP_SUB || currentState == MENU_TIME_SUB) {
display.setTextSize(1);
display.setCursor(0,0);
if(currentState == MENU_LIST) display.print("MENU UTAMA");
else if(currentState == MENU_TEMP_SUB) display.print("SETTING SUHU");
else display.print("SETTING WAKTU");
display.drawLine(0, 9, 128, 9, SSD1306_WHITE);
int count, idx;
const char** labels;
if(currentState == MENU_LIST) {
count = isGroupMode ? 5 : 6; idx = menuIndex;
labels = isGroupMode ? menuLabelsGroup : menuLabelsSingle;
} else if(currentState == MENU_TEMP_SUB) {
count = totalTempSubItems; idx = tempSubIndex;
labels = tempSubLabels;
} else {
count = totalTimeSubItems; idx = timeSubIndex;
labels = timeSubLabels;
}
for(int i=0; i<count; i++) {
int y = 14 + (i * 9);
if(i == idx) {
display.fillRect(0, y-1, 128, 9, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(4, y);
display.println(labels[i]);
}
}
else {
display.setCursor(0,0);
if(currentState == SET_UPPER) display.print("SUHU ATAS (ON)");
else if(currentState == SET_LOWER) display.print("SUHU BAWAH (OFF)");
else if(currentState == SET_MODE) display.print("MODE KERJA");
else if(currentState == SET_START_TIME) display.print("JAM MULAI");
else if(currentState == SET_END_NORM) display.print("JAM SELESAI (S-K)");
else if(currentState == SET_END_FRI) display.print("JAM SELESAI (JMT)");
else if(currentState == SET_WORKDAYS) display.print("HARI KERJA");
else display.print(currentState == SET_K1 ? "KIPAS 1" : "KIPAS 2");
display.drawLine(0, 9, 128, 9, SSD1306_WHITE);
if (subMenuSelection == 0) display.drawRect(23, 18, 30, 24, SSD1306_WHITE);
else if (subMenuSelection == 1) display.drawRect(73, 18, 30, 24, SSD1306_WHITE);
display.setTextSize(2);
display.setCursor(25, 22);
if (currentState == SET_UPPER) display.print(tempUpper, 1);
else if (currentState == SET_LOWER) display.print(tempLower, 1);
else if (currentState == SET_MODE) display.print(isGroupMode ? "GRUP" : "MANDIRI");
else if (currentState == SET_WORKDAYS) display.print(workDaysOnly ? "S-J" : "TIAP HR");
else if (currentState == SET_START_TIME || currentState == SET_END_NORM || currentState == SET_END_FRI) {
int h, m;
if(currentState == SET_START_TIME) { h = startHour; m = startMin; }
else if(currentState == SET_END_NORM) { h = endHourNormal; m = endMinNormal; }
else { h = endHourFriday; m = endMinFriday; }
char buf[10];
sprintf(buf, "%02d:%02d", h, m);
display.print(buf);
}
else {
int m = (currentState == SET_K1) ? fan1Mode : fan2Mode;
display.print(m == 4 ? "AUTO" : (m == 0 ? "OFF" : "SPD " + String(m)));
}
display.setTextSize(1);
if (subMenuSelection == 2) {
display.fillRect(30, 50, 68, 12, SSD1306_WHITE);
display.setTextColor(SSD1306_BLACK);
} else {
display.drawRect(30, 50, 68, 12, SSD1306_WHITE);
display.setTextColor(SSD1306_WHITE);
}
display.setCursor(45, 52); display.print("SIMPAN");
}
display.display();
}