#include #include // Enable verbose debug from Firebase_ESP_Client (helps reveal which host/step fails) #define FIREBASE_ESP_CLIENT_DEBUG_PORT Serial #define FIREBASE_ESP_CLIENT_DEBUG_LEVEL 2 #include #include #include "DHT.h" #include #include "RTClib.h" #include "time.h" #include static const char* WIFI_SSID = "oven"; static const char* WIFI_PASSWORD = "1sampai8"; static const char* FIREBASE_API_KEY = "AIzaSyC-Wdu6vNC0dRyF33S3_C2wpVt9I6wnp0w"; static const char* FIREBASE_DATABASE_URL = "https://smartoven-f9fdd-default-rtdb.firebaseio.com/"; char apiKey[100] = ""; char databaseUrl[200] = ""; FirebaseData fbdo; FirebaseAuth auth; FirebaseConfig config; static bool signupOK = false; static bool signUpAnonymousWithRetry(uint32_t retryDelayMs = 2000) { // Anonymous sign-in: email/password kosong for (;;) { signupOK = Firebase.signUp(&config, &auth, "", ""); if (signupOK) { Serial.println("[FB] signUp(anonymous): OK"); return true; } Serial.print("[FB] signUp(anonymous): FAIL "); Serial.println(config.signer.signupError.message.c_str()); delay(retryDelayMs); } } static bool waitFirebaseReady(uint32_t timeoutMs = 15000) { Serial.print("[FB] Waiting ready"); uint32_t start = millis(); while (!Firebase.ready() && (millis() - start) < timeoutMs) { delay(250); Serial.print('.'); } Serial.println(); Serial.println(Firebase.ready() ? "[FB] Ready" : "[FB] Not ready (timeout)"); return Firebase.ready(); } static bool fbIsPathNotExist() { // Firebase_ESP_Client uses negative codes for internal errors. // -103 is commonly used for "path not exist" in RTDB get. if (fbdo.httpCode() == -103) return true; String r = fbdo.errorReason(); r.toLowerCase(); return r.indexOf("path not exist") >= 0; } // ===== HISTORY (RTDB) ===== static const bool ENABLE_HISTORY = true; static const uint32_t HISTORY_INTERVAL_MS = 1800000; // 30 menit static uint32_t lastHistoryPushMs = 0; static bool fbPushHistory(float temp, float hum, int heaterDb, int fanDb, int mode, bool active) { if (WiFi.status() != WL_CONNECTED) return false; if (!Firebase.ready()) return false; time_t epoch = time(nullptr); if (epoch <= 0) { Serial.println("[FB] WARN pushHistory skip (time not set)"); return false; } FirebaseJson json; json.set("ts", (int)epoch); json.set("temp", temp); json.set("hum", hum); json.set("heater", heaterDb); json.set("fan", fanDb); json.set("mode", mode); json.set("active", active ? 1 : 0); bool ok = Firebase.RTDB.pushJSON(&fbdo, "/history", &json); if (ok) { Serial.print("[FB] OK pushHistory key="); Serial.println(fbdo.pushName()); } else { Serial.print("[FB] FAIL pushHistory code="); Serial.print(fbdo.httpCode()); Serial.print(" reason="); Serial.println(fbdo.errorReason()); } return ok; } // ===== FIREBASE HELPERS (STATUS LOG) ===== static bool fbSetFloat(const char* path, float value) { if (WiFi.status() != WL_CONNECTED) { Serial.print("[FB] FAIL setFloat "); Serial.print(path); Serial.println(" (WiFi not connected)"); return false; } if (!Firebase.ready()) { Serial.print("[FB] FAIL setFloat "); Serial.print(path); Serial.println(" (Firebase not ready)"); return false; } bool ok = Firebase.RTDB.setFloat(&fbdo, path, value); if (ok) { Serial.print("[FB] OK setFloat "); Serial.print(path); Serial.print(" = "); Serial.println(value); } else { Serial.print("[FB] FAIL setFloat "); Serial.print(path); Serial.print(" code="); Serial.print(fbdo.httpCode()); Serial.print(" reason="); Serial.println(fbdo.errorReason()); } return ok; } static bool fbSetInt(const char* path, int value) { if (WiFi.status() != WL_CONNECTED) { Serial.print("[FB] FAIL setInt "); Serial.print(path); Serial.println(" (WiFi not connected)"); return false; } if (!Firebase.ready()) { Serial.print("[FB] FAIL setInt "); Serial.print(path); Serial.println(" (Firebase not ready)"); return false; } bool ok = Firebase.RTDB.setInt(&fbdo, path, value); if (ok) { Serial.print("[FB] OK setInt "); Serial.print(path); Serial.print(" = "); Serial.println(value); } else { Serial.print("[FB] FAIL setInt "); Serial.print(path); Serial.print(" code="); Serial.print(fbdo.httpCode()); Serial.print(" reason="); Serial.println(fbdo.errorReason()); } return ok; } static bool fbGetInt(const char* path, int& outValue) { if (WiFi.status() != WL_CONNECTED) { Serial.print("[FB] FAIL getInt "); Serial.print(path); Serial.println(" (WiFi not connected)"); return false; } if (!Firebase.ready()) { Serial.print("[FB] FAIL getInt "); Serial.print(path); Serial.println(" (Firebase not ready)"); return false; } bool ok = Firebase.RTDB.getInt(&fbdo, path); if (ok) { outValue = fbdo.intData(); Serial.print("[FB] OK getInt "); Serial.print(path); Serial.print(" = "); Serial.println(outValue); } else { if (fbIsPathNotExist()) { outValue = 0; Serial.print("[FB] WARN getInt "); Serial.print(path); Serial.println(" (path not exist -> default 0)"); return true; } Serial.print("[FB] FAIL getInt "); Serial.print(path); Serial.print(" code="); Serial.print(fbdo.httpCode()); Serial.print(" reason="); Serial.println(fbdo.errorReason()); } return ok; } static bool fbGetFloat(const char* path, float& outValue) { if (WiFi.status() != WL_CONNECTED) { Serial.print("[FB] FAIL getFloat "); Serial.print(path); Serial.println(" (WiFi not connected)"); return false; } if (!Firebase.ready()) { Serial.print("[FB] FAIL getFloat "); Serial.print(path); Serial.println(" (Firebase not ready)"); return false; } bool ok = Firebase.RTDB.getFloat(&fbdo, path); if (ok) { outValue = fbdo.floatData(); Serial.print("[FB] OK getFloat "); Serial.print(path); Serial.print(" = "); Serial.println(outValue); } else { if (fbIsPathNotExist()) { outValue = 0.0f; Serial.print("[FB] WARN getFloat "); Serial.print(path); Serial.println(" (path not exist -> default 0)"); return true; } Serial.print("[FB] FAIL getFloat "); Serial.print(path); Serial.print(" code="); Serial.print(fbdo.httpCode()); Serial.print(" reason="); Serial.println(fbdo.errorReason()); } return ok; } // ===== RTC ===== RTC_DS3231 rtc; static bool rtcAvailable = false; static void scanI2C() { Serial.println("I2C scan..."); uint8_t found = 0; for (uint8_t addr = 1; addr < 127; addr++) { Wire.beginTransmission(addr); uint8_t err = Wire.endTransmission(); if (err == 0) { Serial.print(" Found device at 0x"); if (addr < 16) Serial.print('0'); Serial.println(addr, HEX); found++; } } if (found == 0) { Serial.println(" No I2C devices found (cek wiring SDA/SCL + pullup + pin Wire.begin)"); } } static void printHeap(const char* tag) { Serial.print("[HEAP] "); Serial.print(tag); Serial.print(" free="); Serial.print(ESP.getFreeHeap()); Serial.print(" min="); Serial.print(ESP.getMinFreeHeap()); Serial.print(" 8bit="); Serial.println((uint32_t)heap_caps_get_free_size(MALLOC_CAP_8BIT)); } static bool extractHostFromUrl(const char* url, char* outHost, size_t outSize) { if (!url || !outHost || outSize < 4) return false; outHost[0] = '\0'; const char* p = url; if (strncmp(p, "https://", 8) == 0) p += 8; else if (strncmp(p, "http://", 7) == 0) p += 7; const char* end = strchr(p, '/'); size_t len = end ? (size_t)(end - p) : strlen(p); if (len == 0 || len >= outSize) return false; memcpy(outHost, p, len); outHost[len] = '\0'; return true; } static bool testTcpConnect(const char* host, uint16_t port) { WiFiClient client; client.setTimeout(5000); bool ok = client.connect(host, port); client.stop(); Serial.print("[NET] TCP "); Serial.print(host); Serial.print(":"); Serial.print(port); Serial.print(" => "); Serial.println(ok ? "OK" : "FAIL"); return ok; } static bool testTlsConnectInsecure(const char* host, uint16_t port) { WiFiClientSecure client; client.setInsecure(); client.setTimeout(7000); printHeap("before TLS"); bool ok = client.connect(host, port); Serial.print("[NET] TLS(insecure) "); Serial.print(host); Serial.print(":"); Serial.print(port); Serial.print(" => "); Serial.println(ok ? "OK" : "FAIL"); client.stop(); printHeap("after TLS"); return ok; } static void netDiagnostics() { Serial.println("[NET] Diagnostics..."); printHeap("start"); // DNS test IPAddress ip; bool dnsOk = WiFi.hostByName("www.google.com", ip); Serial.print("[NET] DNS www.google.com => "); Serial.println(dnsOk ? ip.toString() : String("FAIL")); // TCP/TLS to Google testTcpConnect("www.google.com", 443); testTlsConnectInsecure("www.google.com", 443); // Firebase host test char fbHost[128]; if (extractHostFromUrl(FIREBASE_DATABASE_URL, fbHost, sizeof(fbHost))) { bool fbDnsOk = WiFi.hostByName(fbHost, ip); Serial.print("[NET] DNS "); Serial.print(fbHost); Serial.print(" => "); Serial.println(fbDnsOk ? ip.toString() : String("FAIL")); testTcpConnect(fbHost, 443); testTlsConnectInsecure(fbHost, 443); } else { Serial.println("[NET] Could not parse Firebase host from URL"); } } // ===== NTP ===== const char* ntpServer1 = "pool.ntp.org"; const char* ntpServer2 = "time.google.com"; const char* ntpServer3 = "time.nist.gov"; const long gmtOffset_sec = 7 * 3600; const int daylightOffset_sec = 0; unsigned long lastSync = 0; const unsigned long syncInterval = 3600000; // 1 jam // ===== DHT ===== #define DHTPIN 23 #define DHTTYPE DHT22 DHT dht(DHTPIN, DHTTYPE); // ===== RELAY ===== const int relayHeater = 12; const int relayFan = 13; // ===== LED ===== const int ledHeater = 12; // ===== RELAY LOGIC ===== #define RELAY_ON LOW #define RELAY_OFF HIGH // ===== VARIABLE ===== int startHour, startMinute; int endHour, endMinute; float targetTemp; float targetTempFan; // ===== KALIBRASI ===== float tempOffset = 0; float humOffset = 0; int mode = 0; int manualHeater = 0; int manualFan = 0; // ===== WIFI CONNECT ===== static bool connectWiFi(uint32_t timeoutMs = 20000) { WiFi.mode(WIFI_STA); WiFi.setSleep(false); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); Serial.print("WiFi connecting to: "); Serial.println(WIFI_SSID); uint32_t start = millis(); while (WiFi.status() != WL_CONNECTED && (millis() - start) < timeoutMs) { delay(300); Serial.print('.'); } Serial.println(); if (WiFi.status() == WL_CONNECTED) { Serial.print("WiFi connected, IP: "); Serial.println(WiFi.localIP()); return true; } Serial.println("WiFi gagal connect"); return false; } static int monthStrToIndex(const char* mon) { // __DATE__ format: "Mmm dd yyyy" static const char* months[] = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"}; for (int i = 0; i < 12; i++) { if (strncmp(mon, months[i], 3) == 0) return i; } return 0; } static bool setTimeFromCompileUtc() { // fallback kalau NTP diblokir: set waktu dari waktu compile sketch // Ini cukup buat TLS (sertifikat) asal firmware baru di-upload (waktunya mendekati sekarang). const char* dateStr = __DATE__; // "May 20 2026" const char* timeStr = __TIME__; // "HH:MM:SS" char mon[4] = {0}; int day = 1; int year = 2020; int hour = 0; int minute = 0; int second = 0; memcpy(mon, dateStr, 3); mon[3] = '\0'; day = atoi(dateStr + 4); year = atoi(dateStr + 7); hour = atoi(timeStr); minute = atoi(timeStr + 3); second = atoi(timeStr + 6); struct tm t; memset(&t, 0, sizeof(t)); t.tm_year = year - 1900; t.tm_mon = monthStrToIndex(mon); t.tm_mday = day; t.tm_hour = hour; t.tm_min = minute; t.tm_sec = second; t.tm_isdst = 0; setenv("TZ", "UTC0", 1); tzset(); time_t epoch = mktime(&t); if (epoch <= 0) return false; struct timeval tv; tv.tv_sec = epoch; tv.tv_usec = 0; settimeofday(&tv, nullptr); Serial.print("[TIME] Fallback set from compile UTC: "); Serial.println(ctime(&epoch)); return true; } // ===== SYNC RTC ===== static bool syncNtpTime(uint32_t timeoutMs = 20000) { // set timezone for localtime configTime(gmtOffset_sec, daylightOffset_sec, ntpServer1, ntpServer2, ntpServer3); struct tm timeinfo; uint32_t start = millis(); while (!getLocalTime(&timeinfo) && (millis() - start) < timeoutMs) { delay(500); Serial.print('#'); } Serial.println(); if (!getLocalTime(&timeinfo)) { Serial.println("NTP gagal"); return false; } // validasi kasar: tahun harus sudah masuk akal if (timeinfo.tm_year + 1900 < 2020) { Serial.println("NTP dapat tapi waktu belum valid"); return false; } Serial.print("NTP OK: "); Serial.print(timeinfo.tm_year + 1900); Serial.print('-'); Serial.print(timeinfo.tm_mon + 1); Serial.print('-'); Serial.print(timeinfo.tm_mday); Serial.print(' '); Serial.print(timeinfo.tm_hour); Serial.print(':'); Serial.print(timeinfo.tm_min); Serial.print(':'); Serial.println(timeinfo.tm_sec); if (rtcAvailable) { rtc.adjust(DateTime( timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec )); Serial.println("RTC Sync NTP"); } return true; } static void syncRTC() { if (syncNtpTime()) { Serial.println(rtcAvailable ? "RTC/NTP sync OK" : "NTP sync OK (RTC tidak ada)"); } } // ===== SETUP ===== void setup() { Serial.begin(115200); delay(2000); Serial.println("BOOT..."); // ===== PIN MODE ===== pinMode(relayHeater, OUTPUT); pinMode(relayFan, OUTPUT); pinMode(ledHeater, OUTPUT); digitalWrite(relayHeater, RELAY_OFF); digitalWrite(relayFan, RELAY_OFF); digitalWrite(ledHeater, LOW); // ===== DHT ===== dht.begin(); // ===== I2C ===== Wire.begin(13, 14); scanI2C(); // ===== RTC ===== rtcAvailable = rtc.begin(); if (!rtcAvailable) { Serial.println("RTC error (DS3231 tidak terdeteksi). Lanjut tanpa RTC (pakai NTP). "); } // ===== FIREBASE CONFIG (HARDCODE) ===== strncpy(apiKey, FIREBASE_API_KEY, sizeof(apiKey) - 1); apiKey[sizeof(apiKey) - 1] = '\0'; strncpy(databaseUrl, FIREBASE_DATABASE_URL, sizeof(databaseUrl) - 1); databaseUrl[sizeof(databaseUrl) - 1] = '\0'; if (strlen(apiKey) == 0 || strlen(databaseUrl) == 0) { Serial.println("[WARN] FIREBASE_API_KEY / FIREBASE_DATABASE_URL masih kosong. Isi dulu di bagian atas sketch."); } // ===== WIFI ===== if (!connectWiFi()) { // retry terus sampai konek while (!connectWiFi()) { delay(2000); } } // ===== NTP TIME (PENTING UNTUK SSL/TLS) ===== // Banyak error SSL Firebase terjadi kalau waktu belum terset saat TLS handshake. Serial.println("Sync NTP..."); bool ntpOk = syncNtpTime(60000); if (!ntpOk) { Serial.println("[TIME] NTP gagal. Coba fallback waktu compile..."); setTimeFromCompileUtc(); } time_t nowEpoch = time(nullptr); Serial.print("[TIME] Now epoch: "); Serial.println((long)nowEpoch); if (nowEpoch > 0) { Serial.print("[TIME] Now: "); Serial.println(ctime(&nowEpoch)); } Serial.print("Heap before Firebase: "); Serial.println(ESP.getFreeHeap()); // ===== NETWORK DIAGNOSTICS ===== // Ini bantu bedain: DNS/port443 block vs TLS init/heap issue. netDiagnostics(); // ===== FIREBASE ===== config.api_key = apiKey; config.database_url = databaseUrl; // Anonymous auth ONLY (tanpa test_mode). // Pastikan di Firebase Console: Authentication -> Sign-in method -> Anonymous di-enable. signUpAnonymousWithRetry(); // buffer & timeout tuning (membantu masalah SSL init / write error) // coba lebih kecil dulu biar nggak kehabisan heap fbdo.setBSSLBufferSize(2048, 512); fbdo.setResponseSize(2048); config.timeout.serverResponse = 10 * 1000; Firebase.begin(&config, &auth); Firebase.reconnectWiFi(true); waitFirebaseReady(); // ===== RTC SYNC ===== if (rtcAvailable) { // kalau RTC ada, pastikan sudah disesuaikan syncRTC(); } lastSync = millis(); Serial.println("🔥 SYSTEM READY"); } // ===== LOOP ===== void loop() { // ===== WIFI CHECK ===== if (WiFi.status() != WL_CONNECTED) { WiFi.reconnect(); } // ===== RTC PERIODIC SYNC ===== if (WiFi.status() == WL_CONNECTED && millis() - lastSync > syncInterval) { syncRTC(); lastSync = millis(); } // ===== FIREBASE READY CHECK ===== if (WiFi.status() != WL_CONNECTED) { Serial.println("[FB] Skip: WiFi not connected"); delay(1000); return; } if (!Firebase.ready()) { static uint32_t lastNotReadyLog = 0; if (millis() - lastNotReadyLog > 5000) { Serial.println("[FB] Skip: Firebase not ready"); lastNotReadyLog = millis(); } delay(200); // tetap lanjut loop (biar logic lokal jalan), tapi operasi Firebase akan FAIL di wrapper } if (!signupOK) { Serial.println("[FB] Skip: signUp not OK"); delay(2000); return; } // ===== GET FIREBASE ===== fbGetInt("/control/start_hour", startHour); fbGetInt("/control/start_minute", startMinute); fbGetInt("/control/end_hour", endHour); fbGetInt("/control/end_minute", endMinute); fbGetFloat("/control/target_temp", targetTemp); fbGetFloat("/control/target_temp_fan", targetTempFan); fbGetInt("/control/mode", mode); fbGetInt("/manual/heater", manualHeater); fbGetInt("/manual/fan", manualFan); // fallback kalau target fan belum diset if (targetTempFan <= 0) targetTempFan = targetTemp; // ===== GET KALIBRASI ===== fbGetFloat("/calibration/temp_offset", tempOffset); fbGetFloat("/calibration/hum_offset", humOffset); // ===== CURRENT TIME ===== int hourNow = 0; int minuteNow = 0; bool timeOk = false; if (rtcAvailable) { DateTime now = rtc.now(); hourNow = now.hour(); minuteNow = now.minute(); timeOk = true; } else { struct tm timeinfo; if (getLocalTime(&timeinfo, 1000)) { hourNow = timeinfo.tm_hour; minuteNow = timeinfo.tm_min; timeOk = true; } } int currentTime = hourNow * 60 + minuteNow; int startTime = startHour * 60 + startMinute; int endTime = endHour * 60 + endMinute; bool active = false; if (timeOk) { active = (startTime <= endTime) ? (currentTime >= startTime && currentTime <= endTime) : (currentTime >= startTime || currentTime <= endTime); } else { // kalau waktu tidak valid, fail-safe: nonaktif Serial.println("[TIME] Tidak dapat ambil waktu (RTC/NTP). Mode AUTO akan nonaktif."); active = false; } // ===== SENSOR ===== float temp = dht.readTemperature(); float hum = dht.readHumidity(); // DHT read kadang gagal (NaN) karena timing/interrupt/WiFi atau wiring. // Coba retry sekali agar lebih stabil. if (isnan(temp) || isnan(hum)) { Serial.println("[SENSOR] DHT read failed, retry..."); delay(250); temp = dht.readTemperature(); hum = dht.readHumidity(); } // ===== VALIDASI SENSOR ===== if (isnan(temp) || isnan(hum)) { Serial.println("[SENSOR] DHT error (NaN). Cek wiring VCC/GND/DATA, pullup 4.7k-10k ke VCC, kabel jangan panjang, pastikan sensor DHT22 & pin GPIO benar."); delay(2000); return; } // ===== APPLY KALIBRASI ===== temp += tempOffset; hum += humOffset; // ===== CONTROL ===== bool heaterState = false; bool fanState = false; if (mode == 1) { // ===== MANUAL MODE ===== heaterState = (manualHeater != 0); fanState = (manualFan != 0); digitalWrite( relayFan, fanState ? RELAY_ON : RELAY_OFF ); } else { // ===== AUTO MODE ===== if (active) { heaterState = (temp < targetTemp); fanState = (temp > targetTempFan); digitalWrite( relayFan, fanState ? RELAY_ON : RELAY_OFF ); } else { heaterState = false; fanState = false; digitalWrite(relayFan, RELAY_OFF); } } // ===== APPLY HEATER ===== digitalWrite( relayHeater, heaterState ? RELAY_ON : RELAY_OFF ); digitalWrite( ledHeater, heaterState ? HIGH : LOW ); // ===== SEND SENSOR ===== bool okTemp = fbSetFloat("/sensor/temperature", temp); bool okHum = fbSetFloat("/sensor/humidity", hum); // ===== SEND RELAY ===== int heaterDb = heaterState ? 1 : 0; int fanDb = fanState ? 1 : 0; bool okHeater = fbSetInt("/relay/heater", heaterDb); bool okFan = fbSetInt("/relay/fan", fanDb); // Mirror manual state to match relay state (biar UI manual & relay selalu sama) bool okManualHeater = fbSetInt("/manual/heater", heaterDb); bool okManualFan = fbSetInt("/manual/fan", fanDb); Serial.print("[FB] Upload summary: "); Serial.println((okTemp && okHum && okHeater && okFan && okManualHeater && okManualFan) ? "OK" : "FAIL"); // ===== DEBUG ===== Serial.print("Time: "); Serial.print(hourNow); Serial.print(":"); Serial.print(minuteNow); Serial.print(" ("); Serial.print(rtcAvailable ? "RTC" : "NTP"); Serial.println(")"); Serial.print("Temp: "); Serial.println(temp); Serial.print("Humidity: "); Serial.println(hum); Serial.print("Temp Offset: "); Serial.println(tempOffset); Serial.print("Hum Offset: "); Serial.println(humOffset); Serial.print("Mode: "); Serial.println(mode == 1 ? "MANUAL" : "AUTO"); Serial.print("Active: "); Serial.println(active); // ===== PUSH HISTORY ===== if (ENABLE_HISTORY) { uint32_t nowMs = millis(); if (lastHistoryPushMs == 0 || (nowMs - lastHistoryPushMs) >= HISTORY_INTERVAL_MS) { fbPushHistory(temp, hum, heaterDb, fanDb, mode, active); lastHistoryPushMs = nowMs; } } delay(3000); }