856 lines
22 KiB
C++
856 lines
22 KiB
C++
#include <Arduino.h>
|
|
#include <WiFi.h>
|
|
// 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 <Firebase_ESP_Client.h>
|
|
#include <WiFiClientSecure.h>
|
|
#include "DHT.h"
|
|
#include <Wire.h>
|
|
#include "RTClib.h"
|
|
#include "time.h"
|
|
#include <esp_heap_caps.h>
|
|
|
|
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);
|
|
} |