1182 lines
34 KiB
C++
1182 lines
34 KiB
C++
#include <WiFi.h>
|
|
#include <time.h>
|
|
#include <DHT.h>
|
|
#include <Firebase_ESP_Client.h>
|
|
#include "addons/TokenHelper.h"
|
|
#include "addons/RTDBHelper.h"
|
|
#include <Preferences.h>
|
|
#include <IRrecv.h>
|
|
#include <IRsend.h>
|
|
#include <IRac.h>
|
|
#include <IRutils.h>
|
|
#include <IRtext.h>
|
|
#include <EEPROM.h>
|
|
|
|
#define WIFI_SSID "Test"
|
|
#define WIFI_PASSWORD "12345678"
|
|
#define API_KEY "AIzaSyANA0Sj8y2FQr9XXFAgprbqGNT0TIXxRfY"
|
|
#define DATABASE_URL "https://sensor-iot-45bb6-default-rtdb.firebaseio.com/"
|
|
#define USER_EMAIL "elonaviola992@gmail.com"
|
|
#define USER_PASSWORD "12345678"
|
|
|
|
#define DHTPIN 4
|
|
#define DHTTYPE DHT22
|
|
#define MQ135_PIN 34
|
|
#define GP2Y_LEDPIN 5
|
|
#define GP2Y_VOPIN 33
|
|
#define STATUS_LED_PIN 2
|
|
|
|
const uint16_t kRecvPin = 14;
|
|
const uint16_t kSendPin = 27;
|
|
const uint16_t kCaptureBufferSize = 2048;
|
|
const uint8_t kTimeout = 80;
|
|
const uint16_t kFrequency = 38;
|
|
|
|
#define MQ135_CAL_ADC 960
|
|
#define MQ135_CAL_PPM 400.0
|
|
#define MQ135_PARA 116.6020682
|
|
#define MQ135_PARB 2.769034857
|
|
#define MQ135_RL 22.0
|
|
#define MQ135_VCC 5.0
|
|
float MQ135_R0 = 76.63;
|
|
|
|
#define MQ135_ADC_BERSIH 1250
|
|
#define MQ135_PPM_BERSIH 400.0
|
|
const float MQ135_SLOPE_PPM_PER_ADC = MQ135_PPM_BERSIH / (float)MQ135_ADC_BERSIH;
|
|
|
|
#define PM25_ADC_ZERO 1450.0
|
|
#define PM25_SLOPE 0.137
|
|
#define PM25_OFFSET 0.0
|
|
|
|
const float DHT_SUHU_OFFSET = -1.7;
|
|
const float DHT_KELEMBAPAN_OFFSET = -2.0;
|
|
|
|
const unsigned long READ_INTERVAL_MS = 5000UL;
|
|
const unsigned long WIFI_RETRY_MS = 10000UL;
|
|
const unsigned long FIREBASE_RETRY_MS = 15000UL;
|
|
const unsigned long AC_AUTO_INTERVAL_MS = 300000UL;
|
|
const unsigned long FIREBASE_POLL_MS = 2000UL;
|
|
|
|
unsigned long lastReadMillis = 0;
|
|
int lastHistoryMinuteSaved = -1;
|
|
unsigned long lastWifiTryMillis = 0;
|
|
unsigned long lastFirebaseTry = 0;
|
|
unsigned long lastAutoACMillis = 0;
|
|
unsigned long lastFirebasePoll = 0;
|
|
|
|
bool wifiConnected = false;
|
|
bool firebaseReady = false;
|
|
bool timeSynced = false;
|
|
bool resetReasonSudahDikirim = false;
|
|
bool autoACEnabled = true;
|
|
|
|
enum ModeKontrol {
|
|
KONTROL_AUTO,
|
|
KONTROL_MANUAL
|
|
};
|
|
ModeKontrol modeKontrol = KONTROL_AUTO;
|
|
unsigned long lastManualCommandTime = 0;
|
|
const unsigned long AUTO_RESUME_MS = 300000UL;
|
|
|
|
const char* ntpServer = "pool.ntp.org";
|
|
const long gmtOffsetSec = 7 * 3600;
|
|
const int daylightOffsetSec = 0;
|
|
|
|
DHT dht(DHTPIN, DHTTYPE);
|
|
float suhu = 25.0, kelembapan = 50.0, co2ppm = 400.0, pm25 = 0.0;
|
|
float mq135AdcRaw = 0, gp2yAdcRaw = 0;
|
|
float mq135Voltage = 0, gp2yVoltage = 0;
|
|
|
|
float amankanNilai(float v, float def) {
|
|
if (isnan(v) || isinf(v)) return def;
|
|
return v;
|
|
}
|
|
|
|
FirebaseData fbdo;
|
|
FirebaseAuth auth;
|
|
FirebaseConfig config;
|
|
Preferences prefs;
|
|
|
|
IRrecv irrecv(kRecvPin, kCaptureBufferSize, kTimeout, true);
|
|
IRsend irsend(kSendPin);
|
|
IRac ac(kSendPin);
|
|
decode_results results;
|
|
|
|
#define EEPROM_SIZE 64
|
|
#define ADDR_MAGIC 0
|
|
#define ADDR_PROTOCOL 4
|
|
#define ADDR_MODEL 6
|
|
#define ADDR_VALID 8
|
|
|
|
enum WorkMode {
|
|
MODE_IDLE,
|
|
MODE_CONFIG,
|
|
MODE_SWEEP,
|
|
MODE_LEARN
|
|
};
|
|
WorkMode currentMode = MODE_IDLE;
|
|
|
|
struct ACProtocolDef {
|
|
decode_type_t protocol;
|
|
const char* name;
|
|
const char* description;
|
|
bool hasModel;
|
|
int minModel;
|
|
int maxModel;
|
|
const char* modelNames[8];
|
|
};
|
|
|
|
ACProtocolDef acProtocols[] = {
|
|
{ PANASONIC_AC, "PANASONIC_AC", "Panasonic AC", true, 1, 6,
|
|
{"", "LKE", "NKE", "DKE", "JKE", "CKP", "RKR"} },
|
|
{ DAIKIN, "DAIKIN", "Daikin", false, -1, -1, {} },
|
|
{ LG, "LG", "LG AC", true, 1, 4,
|
|
{"", "GE6711AR2853M", "LG6711A20083V", "AKB74955603", "AKB73757604"} },
|
|
{ SHARP_AC, "SHARP_AC", "Sharp AC", true, 1, 3,
|
|
{"", "A705", "A903", "A907"} },
|
|
{ GREE, "GREE", "Gree AC", true, 1, 3,
|
|
{"", "YAW1F", "YBOFB", "YX1FSF"} },
|
|
};
|
|
|
|
const int TOTAL_PROTOCOLS = sizeof(acProtocols) / sizeof(acProtocols[0]);
|
|
|
|
struct ACState {
|
|
decode_type_t protocol;
|
|
int model;
|
|
bool power;
|
|
stdAc::opmode_t mode;
|
|
float degrees;
|
|
stdAc::fanspeed_t fanspeed;
|
|
stdAc::swingv_t swingv;
|
|
stdAc::swingh_t swingh;
|
|
bool light;
|
|
bool beep;
|
|
bool econo;
|
|
bool filter;
|
|
bool turbo;
|
|
bool quiet;
|
|
bool sleep;
|
|
bool autoMode;
|
|
};
|
|
|
|
ACState currentState;
|
|
|
|
int sweepIndex = 0;
|
|
int sweepModel = 0;
|
|
bool hasSavedConfig = false;
|
|
uint16_t* lastRawArray = nullptr;
|
|
uint16_t lastRawLen = 0;
|
|
|
|
String lastAutoAction = "none";
|
|
unsigned long lastAutoTriggerTime = 0;
|
|
|
|
void setup() {
|
|
Serial.begin(115200);
|
|
delay(500);
|
|
|
|
pinMode(STATUS_LED_PIN, OUTPUT);
|
|
digitalWrite(STATUS_LED_PIN, LOW);
|
|
pinMode(GP2Y_LEDPIN, OUTPUT);
|
|
digitalWrite(GP2Y_LEDPIN, HIGH);
|
|
analogReadResolution(12);
|
|
|
|
EEPROM.begin(EEPROM_SIZE);
|
|
|
|
dht.begin();
|
|
hitungR0MQ135();
|
|
|
|
irsend.begin();
|
|
irrecv.setUnknownThreshold(12);
|
|
irrecv.enableIRIn();
|
|
|
|
WiFi.setSleep(false);
|
|
connectWiFi();
|
|
|
|
if (wifiConnected) {
|
|
lastFirebaseTry = 0;
|
|
ensureFirebaseReady(millis());
|
|
}
|
|
|
|
initACState();
|
|
catatBootKeFlash();
|
|
|
|
if (loadConfigFromEEPROM()) {
|
|
printCurrentConfig();
|
|
}
|
|
}
|
|
|
|
void initACState() {
|
|
currentState.protocol = UNKNOWN;
|
|
currentState.model = -1;
|
|
currentState.power = false;
|
|
currentState.mode = stdAc::opmode_t::kCool;
|
|
currentState.degrees = 25;
|
|
currentState.fanspeed = stdAc::fanspeed_t::kAuto;
|
|
currentState.swingv = stdAc::swingv_t::kOff;
|
|
currentState.swingh = stdAc::swingh_t::kOff;
|
|
currentState.light = true;
|
|
currentState.beep = false;
|
|
currentState.econo = false;
|
|
currentState.filter = false;
|
|
currentState.turbo = false;
|
|
currentState.quiet = false;
|
|
currentState.sleep = false;
|
|
currentState.autoMode = true;
|
|
}
|
|
|
|
bool isDalamKonfigurasi() {
|
|
return (currentMode == MODE_CONFIG || currentMode == MODE_SWEEP);
|
|
}
|
|
|
|
const char* modeKontrolToString() {
|
|
return (modeKontrol == KONTROL_AUTO) ? "auto" : "manual";
|
|
}
|
|
|
|
void loop() {
|
|
unsigned long now = millis();
|
|
|
|
ensureWifiConnected(now);
|
|
ensureFirebaseReady(now);
|
|
updateStatusLed();
|
|
|
|
if (firebaseReady && !resetReasonSudahDikirim) {
|
|
kirimDiagnostikBoot();
|
|
resetReasonSudahDikirim = true;
|
|
}
|
|
|
|
if (modeKontrol == KONTROL_MANUAL && lastManualCommandTime > 0) {
|
|
if (now - lastManualCommandTime >= AUTO_RESUME_MS) {
|
|
modeKontrol = KONTROL_AUTO;
|
|
lastManualCommandTime = 0;
|
|
if (firebaseReady) kirimACStateToFirebase();
|
|
}
|
|
}
|
|
|
|
if (!isDalamKonfigurasi()) {
|
|
if (now - lastReadMillis >= READ_INTERVAL_MS) {
|
|
lastReadMillis = now;
|
|
bacaSemuaSensor();
|
|
tampilkanSerial();
|
|
if (firebaseReady) {
|
|
kirimDataTerkini();
|
|
kirimACStateToFirebase();
|
|
}
|
|
}
|
|
|
|
cekDanSimpanHistoryPerMenit();
|
|
|
|
if (modeKontrol == KONTROL_AUTO && autoACEnabled && hasSavedConfig && (now - lastAutoACMillis >= AC_AUTO_INTERVAL_MS)) {
|
|
lastAutoACMillis = now;
|
|
processAutoTrigger();
|
|
}
|
|
}
|
|
|
|
if (firebaseReady && (now - lastFirebasePoll >= FIREBASE_POLL_MS)) {
|
|
lastFirebasePoll = now;
|
|
pollFirebaseCommand();
|
|
}
|
|
|
|
if (currentMode == MODE_LEARN) {
|
|
if (irrecv.decode(&results)) {
|
|
processLearnCapture();
|
|
irrecv.resume();
|
|
}
|
|
}
|
|
|
|
if (currentMode == MODE_SWEEP) {
|
|
if (irrecv.decode(&results)) {
|
|
irrecv.resume();
|
|
}
|
|
}
|
|
|
|
delay(10);
|
|
}
|
|
|
|
void processAutoTrigger() {
|
|
if (currentState.protocol == UNKNOWN) return;
|
|
|
|
bool suhuBuruk = (suhu < 18.0) || (suhu > 30.0);
|
|
bool kelembapanRendah = (kelembapan < 30.0);
|
|
bool kelembapanTinggi = (kelembapan > 70.0);
|
|
bool co2Buruk = (kategoriCO2(co2ppm) == "Buruk");
|
|
bool pm25Buruk = (kategoriPM25(pm25) == "Buruk");
|
|
|
|
bool adaKondisiBuruk = suhuBuruk || kelembapanRendah || kelembapanTinggi ||
|
|
co2Buruk || pm25Buruk;
|
|
|
|
if (!adaKondisiBuruk) {
|
|
lastAutoAction = "none";
|
|
return;
|
|
}
|
|
|
|
bool needDry = kelembapanTinggi;
|
|
bool needCool = suhuBuruk || kelembapanRendah;
|
|
bool needFan = co2Buruk || pm25Buruk;
|
|
|
|
String action = "none";
|
|
|
|
if (needDry) {
|
|
action = "auto_dry";
|
|
currentState.power = true;
|
|
currentState.mode = stdAc::opmode_t::kDry;
|
|
currentState.fanspeed = stdAc::fanspeed_t::kAuto;
|
|
currentState.degrees = 25;
|
|
} else if (needCool) {
|
|
action = "auto_cool";
|
|
currentState.power = true;
|
|
currentState.mode = stdAc::opmode_t::kCool;
|
|
currentState.degrees = 24;
|
|
currentState.fanspeed = stdAc::fanspeed_t::kAuto;
|
|
} else if (needFan) {
|
|
action = "auto_fan";
|
|
currentState.power = true;
|
|
currentState.mode = stdAc::opmode_t::kFan;
|
|
currentState.fanspeed = stdAc::fanspeed_t::kHigh;
|
|
}
|
|
|
|
if (action != "none" && action != lastAutoAction) {
|
|
lastAutoAction = action;
|
|
lastAutoTriggerTime = millis();
|
|
sendCurrentState();
|
|
if (firebaseReady) {
|
|
kirimACStateToFirebase();
|
|
catatAutoTriggerHistory(action);
|
|
}
|
|
}
|
|
}
|
|
|
|
void catatAutoTriggerHistory(const String &action) {
|
|
if (!firebaseReady || !Firebase.ready()) return;
|
|
|
|
unsigned long epoch = (unsigned long)time(nullptr);
|
|
String path = "/ac/auto_trigger_history/" + String(epoch);
|
|
|
|
FirebaseJson json;
|
|
json.set("action", action);
|
|
json.set("suhu", amankanNilai(suhu, 25.0));
|
|
json.set("kelembapan", amankanNilai(kelembapan, 50.0));
|
|
json.set("co2_ppm", amankanNilai(co2ppm, 400.0));
|
|
json.set("pm25_ugm3", amankanNilai(pm25, 0.0));
|
|
json.set("timestamp", epoch);
|
|
|
|
Firebase.RTDB.setJSON(&fbdo, path.c_str(), &json);
|
|
}
|
|
|
|
void pollFirebaseCommand() {
|
|
if (!firebaseReady || !Firebase.ready()) return;
|
|
|
|
if (Firebase.RTDB.getJSON(&fbdo, "/ac/control")) {
|
|
FirebaseJson *json = fbdo.to<FirebaseJson*>();
|
|
if (json != nullptr) {
|
|
FirebaseJsonData jsonDataCmd;
|
|
FirebaseJsonData jsonDataVal;
|
|
|
|
json->get(jsonDataCmd, "command");
|
|
json->get(jsonDataVal, "value");
|
|
|
|
String cmdStr = jsonDataCmd.stringValue;
|
|
String valStr = jsonDataVal.stringValue;
|
|
|
|
if (cmdStr.length() > 0) {
|
|
executeFlutterCommand(cmdStr, valStr);
|
|
|
|
FirebaseJson clearJson;
|
|
clearJson.set("command", "");
|
|
clearJson.set("value", "");
|
|
clearJson.set("executed", true);
|
|
clearJson.set("executed_at", (unsigned long)time(nullptr));
|
|
Firebase.RTDB.setJSON(&fbdo, "/ac/control", &clearJson);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void executeFlutterCommand(String &cmd, String &val) {
|
|
cmd.toLowerCase();
|
|
val.toLowerCase();
|
|
|
|
if (cmd == "control_mode") {
|
|
if (val == "auto") {
|
|
modeKontrol = KONTROL_AUTO;
|
|
lastManualCommandTime = 0;
|
|
} else if (val == "manual") {
|
|
modeKontrol = KONTROL_MANUAL;
|
|
lastManualCommandTime = millis();
|
|
}
|
|
if (firebaseReady) kirimACStateToFirebase();
|
|
return;
|
|
}
|
|
|
|
if (cmd == "power") {
|
|
currentState.power = (val == "on" || val == "true" || val == "1");
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "mode") {
|
|
if (val == "cool") currentState.mode = stdAc::opmode_t::kCool;
|
|
else if (val == "heat") currentState.mode = stdAc::opmode_t::kHeat;
|
|
else if (val == "dry") currentState.mode = stdAc::opmode_t::kDry;
|
|
else if (val == "fan") currentState.mode = stdAc::opmode_t::kFan;
|
|
else if (val == "auto") currentState.mode = stdAc::opmode_t::kAuto;
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "temp") {
|
|
int t = val.toInt();
|
|
if (t >= 16 && t <= 32) {
|
|
currentState.degrees = t;
|
|
sendCurrentState();
|
|
}
|
|
}
|
|
else if (cmd == "fan") {
|
|
if (val == "auto") currentState.fanspeed = stdAc::fanspeed_t::kAuto;
|
|
else if (val == "low") currentState.fanspeed = stdAc::fanspeed_t::kLow;
|
|
else if (val == "med" || val == "medium") currentState.fanspeed = stdAc::fanspeed_t::kMedium;
|
|
else if (val == "high") currentState.fanspeed = stdAc::fanspeed_t::kHigh;
|
|
else if (val == "max") currentState.fanspeed = stdAc::fanspeed_t::kMax;
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "swing") {
|
|
currentState.swingv = (val == "on" || val == "true") ? stdAc::swingv_t::kAuto : stdAc::swingv_t::kOff;
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "turbo") {
|
|
currentState.turbo = (val == "on" || val == "true");
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "quiet") {
|
|
currentState.quiet = (val == "on" || val == "true");
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "econo") {
|
|
currentState.econo = (val == "on" || val == "true");
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "light") {
|
|
currentState.light = (val == "on" || val == "true");
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "filter") {
|
|
currentState.filter = (val == "on" || val == "true");
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "sleep") {
|
|
currentState.sleep = (val == "on" || val == "true");
|
|
sendCurrentState();
|
|
}
|
|
else if (cmd == "auto_mode") {
|
|
autoACEnabled = (val == "on" || val == "true" || val == "1");
|
|
}
|
|
else if (cmd == "config") {
|
|
startConfigMode();
|
|
}
|
|
else if (cmd == "select_brand") {
|
|
if (currentMode == MODE_CONFIG) {
|
|
int idx = val.toInt();
|
|
if (idx > 0 && idx <= TOTAL_PROTOCOLS) {
|
|
startSweepForBrand(idx - 1);
|
|
}
|
|
}
|
|
}
|
|
else if (cmd == "confirm") {
|
|
if (currentMode == MODE_SWEEP) {
|
|
if (val == "y") sweepConfirm(true);
|
|
else if (val == "n") sweepConfirm(false);
|
|
else if (val == "skip") skipBrand();
|
|
}
|
|
}
|
|
else if (cmd == "cancel_config") {
|
|
if (currentMode == MODE_CONFIG || currentMode == MODE_SWEEP) {
|
|
batalkanKonfigurasi();
|
|
}
|
|
}
|
|
else if (cmd == "save_config") {
|
|
saveConfigToEEPROM();
|
|
kirimStatusKonfigurasiKeFirebase();
|
|
}
|
|
else if (cmd == "load_config") {
|
|
if (loadConfigFromEEPROM()) {
|
|
printCurrentConfig();
|
|
}
|
|
kirimStatusKonfigurasiKeFirebase();
|
|
}
|
|
else if (cmd == "clear_config") {
|
|
clearEEPROM();
|
|
kirimStatusKonfigurasiKeFirebase();
|
|
}
|
|
else if (cmd == "learn") {
|
|
startLearnMode();
|
|
}
|
|
else if (cmd == "sendraw") {
|
|
sendLastRawData();
|
|
}
|
|
}
|
|
|
|
void kirimStatusKonfigurasiKeFirebase() {
|
|
if (!firebaseReady || !Firebase.ready()) return;
|
|
|
|
FirebaseJson json;
|
|
String modeStr;
|
|
switch (currentMode) {
|
|
case MODE_CONFIG: modeStr = "config"; break;
|
|
case MODE_SWEEP: modeStr = "sweep"; break;
|
|
case MODE_LEARN: modeStr = "learn"; break;
|
|
default: modeStr = "idle"; break;
|
|
}
|
|
json.set("mode", modeStr);
|
|
json.set("has_config", hasSavedConfig);
|
|
|
|
if (currentMode == MODE_SWEEP && sweepIndex < TOTAL_PROTOCOLS) {
|
|
ACProtocolDef &p = acProtocols[sweepIndex];
|
|
String modelName = "(default)";
|
|
if (p.hasModel && sweepModel > 0) modelName = String(p.modelNames[sweepModel]);
|
|
json.set("step", "confirm");
|
|
json.set("brand_index", sweepIndex + 1);
|
|
json.set("brand_name", p.name);
|
|
json.set("brand_desc", p.description);
|
|
json.set("model_name", modelName);
|
|
json.set("message", "AC merespon sinyal ini? kirim command 'confirm' dengan value y/n/skip");
|
|
} else if (currentMode == MODE_CONFIG) {
|
|
json.set("step", "select_brand");
|
|
json.set("message", "Pilih merek AC: kirim command 'select_brand' dengan value nomor 1-" + String(TOTAL_PROTOCOLS));
|
|
} else if (currentMode == MODE_LEARN) {
|
|
json.set("step", "learning");
|
|
json.set("message", "Arahkan remote asli ke IR receiver lalu tekan tombol");
|
|
} else {
|
|
json.set("step", hasSavedConfig ? "ready" : "not_configured");
|
|
json.set("message", hasSavedConfig ? "Konfigurasi AC siap digunakan" : "Belum ada konfigurasi. Kirim command 'config' untuk mulai setup");
|
|
}
|
|
json.set("updated_at", (unsigned long)time(nullptr));
|
|
|
|
Firebase.RTDB.setJSON(&fbdo, "/ac/config_status", &json);
|
|
}
|
|
|
|
void kirimDaftarMerekKeFirebase() {
|
|
if (!firebaseReady || !Firebase.ready()) return;
|
|
|
|
FirebaseJsonArray arr;
|
|
for (int i = 0; i < TOTAL_PROTOCOLS; i++) {
|
|
FirebaseJson item;
|
|
item.set("index", i + 1);
|
|
item.set("name", acProtocols[i].name);
|
|
item.set("description", acProtocols[i].description);
|
|
arr.add(item);
|
|
}
|
|
Firebase.RTDB.setArray(&fbdo, "/ac/config_brands", &arr);
|
|
}
|
|
|
|
void kirimACStateToFirebase() {
|
|
if (!firebaseReady || !Firebase.ready()) return;
|
|
|
|
FirebaseJson json;
|
|
json.set("power", currentState.power);
|
|
json.set("mode", modeToString(currentState.mode));
|
|
json.set("temp", currentState.degrees);
|
|
json.set("fan", fanToString(currentState.fanspeed));
|
|
json.set("swing", currentState.swingv != stdAc::swingv_t::kOff);
|
|
json.set("turbo", currentState.turbo);
|
|
json.set("quiet", currentState.quiet);
|
|
json.set("econo", currentState.econo);
|
|
json.set("light", currentState.light);
|
|
json.set("filter", currentState.filter);
|
|
json.set("sleep", currentState.sleep);
|
|
json.set("auto_mode", autoACEnabled);
|
|
json.set("control_mode", modeKontrolToString());
|
|
json.set("last_auto_action", lastAutoAction);
|
|
json.set("protocol", typeToString(currentState.protocol));
|
|
json.set("model", currentState.model);
|
|
json.set("has_config", hasSavedConfig);
|
|
json.set("last_update", (unsigned long)time(nullptr));
|
|
|
|
Firebase.RTDB.setJSON(&fbdo, "/ac/state", &json);
|
|
}
|
|
|
|
bool sendCurrentState() {
|
|
if (currentState.protocol == UNKNOWN) {
|
|
return false;
|
|
}
|
|
|
|
ac.next.protocol = currentState.protocol;
|
|
ac.next.model = currentState.model;
|
|
ac.next.power = currentState.power;
|
|
ac.next.mode = currentState.mode;
|
|
ac.next.celsius = true;
|
|
ac.next.degrees = currentState.degrees;
|
|
ac.next.fanspeed = currentState.fanspeed;
|
|
ac.next.swingv = currentState.swingv;
|
|
ac.next.swingh = currentState.swingh;
|
|
ac.next.light = currentState.light;
|
|
ac.next.beep = currentState.beep;
|
|
ac.next.econo = currentState.econo;
|
|
ac.next.filter = currentState.filter;
|
|
ac.next.turbo = currentState.turbo;
|
|
ac.next.quiet = currentState.quiet;
|
|
ac.next.sleep = currentState.sleep ? 0 : -1;
|
|
ac.next.clock = -1;
|
|
|
|
bool ok = ac.sendAc();
|
|
|
|
if (ok) {
|
|
printStateJSON();
|
|
if (firebaseReady) kirimACStateToFirebase();
|
|
}
|
|
return ok;
|
|
}
|
|
|
|
void startConfigMode() {
|
|
clearEEPROM();
|
|
sweepIndex = 0;
|
|
sweepModel = 0;
|
|
|
|
currentMode = MODE_CONFIG;
|
|
kirimDaftarMerekKeFirebase();
|
|
kirimStatusKonfigurasiKeFirebase();
|
|
}
|
|
|
|
void batalkanKonfigurasi() {
|
|
currentMode = MODE_IDLE;
|
|
lastReadMillis = millis();
|
|
kirimStatusKonfigurasiKeFirebase();
|
|
}
|
|
|
|
void listBrands() {
|
|
for (int i = 0; i < TOTAL_PROTOCOLS; i++) {
|
|
Serial.print(i + 1);
|
|
Serial.print(F(". "));
|
|
Serial.print(acProtocols[i].name);
|
|
Serial.print(F(" - "));
|
|
Serial.println(acProtocols[i].description);
|
|
if ((i + 1) % 10 == 0) delay(5);
|
|
}
|
|
}
|
|
|
|
void startSweepForBrand(int index) {
|
|
if (index < 0 || index >= TOTAL_PROTOCOLS) {
|
|
return;
|
|
}
|
|
sweepIndex = index;
|
|
sweepModel = (acProtocols[index].hasModel) ? 0 : -1;
|
|
currentMode = MODE_SWEEP;
|
|
sweepSendCurrent();
|
|
}
|
|
|
|
void sweepSendCurrent() {
|
|
if (sweepIndex >= TOTAL_PROTOCOLS) {
|
|
currentMode = MODE_IDLE;
|
|
lastReadMillis = millis();
|
|
kirimStatusKonfigurasiKeFirebase();
|
|
return;
|
|
}
|
|
ACProtocolDef &p = acProtocols[sweepIndex];
|
|
int modelToUse = -1;
|
|
if (p.hasModel) {
|
|
if (sweepModel == 0) { modelToUse = -1; }
|
|
else { modelToUse = sweepModel; }
|
|
}
|
|
currentState.protocol = p.protocol;
|
|
currentState.model = modelToUse;
|
|
currentState.power = true;
|
|
currentState.mode = stdAc::opmode_t::kCool;
|
|
currentState.degrees = 25;
|
|
currentState.fanspeed = stdAc::fanspeed_t::kAuto;
|
|
irrecv.resume();
|
|
bool ok = sendCurrentState();
|
|
if (ok) {
|
|
kirimStatusKonfigurasiKeFirebase();
|
|
}
|
|
else { delay(500); sweepNext(); }
|
|
}
|
|
|
|
void sweepConfirm(bool responded) {
|
|
if (responded) {
|
|
saveConfigToEEPROM();
|
|
currentMode = MODE_IDLE;
|
|
lastReadMillis = millis();
|
|
kirimStatusKonfigurasiKeFirebase();
|
|
kirimACStateToFirebase();
|
|
} else {
|
|
sweepNext();
|
|
}
|
|
}
|
|
|
|
void sweepNext() {
|
|
ACProtocolDef &p = acProtocols[sweepIndex];
|
|
if (p.hasModel && sweepModel < p.maxModel) sweepModel++;
|
|
else { sweepModel = 0; sweepIndex++; }
|
|
sweepSendCurrent();
|
|
}
|
|
|
|
void skipBrand() {
|
|
sweepModel = 0; sweepIndex++;
|
|
sweepSendCurrent();
|
|
}
|
|
|
|
void saveConfigToEEPROM() {
|
|
if (currentState.protocol == UNKNOWN) {
|
|
return;
|
|
}
|
|
EEPROM.writeString(ADDR_MAGIC, "IRAC");
|
|
EEPROM.put(ADDR_PROTOCOL, (int)currentState.protocol);
|
|
EEPROM.put(ADDR_MODEL, currentState.model);
|
|
EEPROM.write(ADDR_VALID, 0x01);
|
|
EEPROM.commit();
|
|
hasSavedConfig = true;
|
|
printCurrentConfig();
|
|
}
|
|
|
|
bool loadConfigFromEEPROM() {
|
|
String magic = EEPROM.readString(ADDR_MAGIC);
|
|
if (magic != "IRAC") return false;
|
|
uint8_t valid = EEPROM.read(ADDR_VALID);
|
|
if (valid != 0x01) return false;
|
|
int protocolVal, modelVal;
|
|
EEPROM.get(ADDR_PROTOCOL, protocolVal);
|
|
EEPROM.get(ADDR_MODEL, modelVal);
|
|
currentState.protocol = (decode_type_t)protocolVal;
|
|
currentState.model = modelVal;
|
|
hasSavedConfig = true;
|
|
return true;
|
|
}
|
|
|
|
void clearEEPROM() {
|
|
for (int i = 0; i < EEPROM_SIZE; i++) EEPROM.write(i, 0);
|
|
EEPROM.commit();
|
|
hasSavedConfig = false;
|
|
initACState();
|
|
}
|
|
|
|
void printCurrentConfig() {
|
|
Serial.println(F("--- KONFIGURASI AC ---"));
|
|
if (currentState.protocol == UNKNOWN) {
|
|
Serial.println(F("Protocol: BELUM DIKONFIGURASI"));
|
|
} else {
|
|
Serial.print(F("Protocol: ")); Serial.println(typeToString(currentState.protocol));
|
|
Serial.print(F("Model : ")); Serial.println(currentState.model);
|
|
}
|
|
Serial.print(F("Auto : ")); Serial.println(autoACEnabled ? F("ON") : F("OFF"));
|
|
Serial.print(F("Control : ")); Serial.println(modeKontrol == KONTROL_AUTO ? F("AUTO") : F("MANUAL"));
|
|
}
|
|
|
|
void printStateJSON() {
|
|
Serial.print(F("{\"ac_state\":{\"power\":"));
|
|
Serial.print(currentState.power ? F("true") : F("false"));
|
|
Serial.print(F(",\"mode\":\""));
|
|
Serial.print(modeToString(currentState.mode));
|
|
Serial.print(F("\",\"temp\":"));
|
|
Serial.print(currentState.degrees);
|
|
Serial.print(F(",\"fan\":\""));
|
|
Serial.print(fanToString(currentState.fanspeed));
|
|
Serial.print(F("\",\"swing\":"));
|
|
Serial.print(currentState.swingv != stdAc::swingv_t::kOff ? F("true") : F("false"));
|
|
Serial.print(F(",\"turbo\":"));
|
|
Serial.print(currentState.turbo ? F("true") : F("false"));
|
|
Serial.print(F(",\"quiet\":"));
|
|
Serial.print(currentState.quiet ? F("true") : F("false"));
|
|
Serial.print(F(",\"econo\":"));
|
|
Serial.print(currentState.econo ? F("true") : F("false"));
|
|
Serial.print(F(",\"light\":"));
|
|
Serial.print(currentState.light ? F("true") : F("false"));
|
|
Serial.print(F(",\"filter\":"));
|
|
Serial.print(currentState.filter ? F("true") : F("false"));
|
|
Serial.print(F(",\"sleep\":"));
|
|
Serial.print(currentState.sleep ? F("true") : F("false"));
|
|
Serial.print(F(",\"auto_mode\":"));
|
|
Serial.print(autoACEnabled ? F("true") : F("false"));
|
|
Serial.print(F(",\"control_mode\":\""));
|
|
Serial.print(modeKontrolToString());
|
|
Serial.print(F("\",\"protocol\":\""));
|
|
Serial.print(typeToString(currentState.protocol));
|
|
Serial.print(F("\",\"model\":"));
|
|
Serial.print(currentState.model);
|
|
Serial.print(F(",\"last_auto\":\""));
|
|
Serial.print(lastAutoAction);
|
|
Serial.println(F("\"}}"));
|
|
}
|
|
|
|
const char* modeToString(stdAc::opmode_t mode) {
|
|
switch (mode) {
|
|
case stdAc::opmode_t::kAuto: return "auto";
|
|
case stdAc::opmode_t::kCool: return "cool";
|
|
case stdAc::opmode_t::kHeat: return "heat";
|
|
case stdAc::opmode_t::kDry: return "dry";
|
|
case stdAc::opmode_t::kFan: return "fan";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
const char* fanToString(stdAc::fanspeed_t fan) {
|
|
switch (fan) {
|
|
case stdAc::fanspeed_t::kAuto: return "auto";
|
|
case stdAc::fanspeed_t::kMin: return "min";
|
|
case stdAc::fanspeed_t::kLow: return "low";
|
|
case stdAc::fanspeed_t::kMedium: return "med";
|
|
case stdAc::fanspeed_t::kHigh: return "high";
|
|
case stdAc::fanspeed_t::kMax: return "max";
|
|
default: return "unknown";
|
|
}
|
|
}
|
|
|
|
void startLearnMode() {
|
|
currentMode = MODE_LEARN;
|
|
}
|
|
|
|
void processLearnCapture() {
|
|
decode_type_t detected = results.decode_type;
|
|
for (int i = 0; i < TOTAL_PROTOCOLS; i++) {
|
|
if (acProtocols[i].protocol == detected) {
|
|
currentState.protocol = detected;
|
|
currentState.model = -1;
|
|
break;
|
|
}
|
|
}
|
|
if (lastRawArray != nullptr) delete[] lastRawArray;
|
|
lastRawLen = getCorrectedRawLength(&results);
|
|
lastRawArray = resultToRawArray(&results);
|
|
}
|
|
|
|
void sendLastRawData() {
|
|
if (lastRawArray == nullptr || lastRawLen == 0) {
|
|
return;
|
|
}
|
|
irsend.sendRaw(lastRawArray, lastRawLen, kFrequency);
|
|
}
|
|
|
|
void hitungR0MQ135() {
|
|
float voutCal = (MQ135_CAL_ADC / 4095.0) * 3.3;
|
|
if (voutCal <= 0.0) voutCal = 0.01;
|
|
float rsCal = ((MQ135_VCC * MQ135_RL) / voutCal) - MQ135_RL;
|
|
float ratioCal = pow(MQ135_PARA / MQ135_CAL_PPM, 1.0 / MQ135_PARB);
|
|
MQ135_R0 = rsCal / ratioCal;
|
|
}
|
|
|
|
float bacaCO2ppm() {
|
|
long total = 0;
|
|
for (int i = 0; i < 10; i++) { total += analogRead(MQ135_PIN); delay(5); }
|
|
float adcAvg = total / 10.0;
|
|
mq135AdcRaw = adcAvg;
|
|
float vout = (adcAvg / 4095.0) * 3.3;
|
|
mq135Voltage = vout;
|
|
|
|
float ppm = adcAvg * MQ135_SLOPE_PPM_PER_ADC;
|
|
if (ppm < 0 || isnan(ppm)) ppm = 0;
|
|
|
|
return ppm;
|
|
}
|
|
|
|
float bacaPM25() {
|
|
long total = 0;
|
|
for (int i = 0; i < 5; i++) {
|
|
digitalWrite(GP2Y_LEDPIN, LOW);
|
|
delayMicroseconds(280);
|
|
delayMicroseconds(40);
|
|
int adc = analogRead(GP2Y_VOPIN);
|
|
digitalWrite(GP2Y_LEDPIN, HIGH);
|
|
delayMicroseconds(9680);
|
|
total += adc;
|
|
}
|
|
float adcAvg = total / 5.0;
|
|
gp2yAdcRaw = adcAvg;
|
|
gp2yVoltage = (adcAvg / 4095.0) * 3.3;
|
|
float ugm3 = PM25_SLOPE * (adcAvg - PM25_ADC_ZERO) + PM25_OFFSET;
|
|
|
|
if (ugm3 < 0 || isnan(ugm3)) ugm3 = 0;
|
|
|
|
return ugm3;
|
|
}
|
|
|
|
void bacaSemuaSensor() {
|
|
float t = dht.readTemperature();
|
|
float h = dht.readHumidity();
|
|
if (!isnan(t)) {
|
|
suhu = t + DHT_SUHU_OFFSET;
|
|
}
|
|
if (!isnan(h)) {
|
|
kelembapan = h + DHT_KELEMBAPAN_OFFSET;
|
|
}
|
|
co2ppm = bacaCO2ppm();
|
|
pm25 = bacaPM25();
|
|
}
|
|
|
|
String kategoriSuhu(float t) {
|
|
if (t >= 24 && t <= 27) return "Sangat Baik";
|
|
else if ((t >= 20 && t < 24) || (t > 27 && t <= 30)) return "Baik";
|
|
else return "Buruk";
|
|
}
|
|
|
|
String kategoriKelembapan(float h) {
|
|
if (h >= 40 && h <= 60) return "Sangat Baik";
|
|
else if ((h >= 30 && h < 40) || (h > 60 && h <= 70)) return "Baik";
|
|
else return "Buruk";
|
|
}
|
|
|
|
String kategoriCO2(float ppm) {
|
|
if (ppm < 400) return "Sangat Baik";
|
|
else if (ppm <= 1000) return "Baik";
|
|
else return "Buruk";
|
|
}
|
|
|
|
String kategoriPM25(float ugm3) {
|
|
if (ugm3 <= 15) return "Sangat Baik";
|
|
else if (ugm3 <= 35) return "Baik";
|
|
else return "Buruk";
|
|
}
|
|
|
|
int rankStatus(const String &s) {
|
|
if (s == "Sangat Baik") return 2;
|
|
if (s == "Baik") return 1;
|
|
return 0;
|
|
}
|
|
|
|
String statusKeseluruhan(const String &s1, const String &s2, const String &s3, const String &s4) {
|
|
int r = min(rankStatus(s1), min(rankStatus(s2), min(rankStatus(s3), rankStatus(s4))));
|
|
if (r >= 2) return "Sangat Baik";
|
|
if (r == 1) return "Baik";
|
|
return "Buruk";
|
|
}
|
|
|
|
void tampilkanSerial() {
|
|
String sSuhu = kategoriSuhu(suhu);
|
|
String sKelembapan = kategoriKelembapan(kelembapan);
|
|
String sCO2 = kategoriCO2(co2ppm);
|
|
String sPM25 = kategoriPM25(pm25);
|
|
String sKeseluruhan = statusKeseluruhan(sSuhu, sKelembapan, sCO2, sPM25);
|
|
|
|
Serial.println(F("---------------------------------------"));
|
|
Serial.printf("Suhu : %.1f C [%s]\n", suhu, sSuhu.c_str());
|
|
Serial.printf("Kelembapan : %.1f %% [%s]\n", kelembapan, sKelembapan.c_str());
|
|
Serial.printf("CO2 : %.1f ppm [%s]\n", co2ppm, sCO2.c_str());
|
|
Serial.printf("PM2.5 : %.1f ug/m3 [%s]\n", pm25, sPM25.c_str());
|
|
Serial.printf("STATUS : %s\n", sKeseluruhan.c_str());
|
|
Serial.printf("AC Auto : %s | Control: %s | Last: %s\n",
|
|
autoACEnabled ? "ON" : "OFF",
|
|
modeKontrol == KONTROL_AUTO ? "AUTO" : "MANUAL",
|
|
lastAutoAction.c_str());
|
|
Serial.printf("WiFi : %s | Firebase: %s\n", wifiConnected ? "OK" : "NO", firebaseReady ? "OK" : "NO");
|
|
}
|
|
|
|
void kirimDataTerkini() {
|
|
if (!firebaseReady || WiFi.status() != WL_CONNECTED) return;
|
|
if (!Firebase.ready()) { firebaseReady = false; return; }
|
|
|
|
String sSuhu = kategoriSuhu(suhu);
|
|
String sKelembapan = kategoriKelembapan(kelembapan);
|
|
String sCO2 = kategoriCO2(co2ppm);
|
|
String sPM25 = kategoriPM25(pm25);
|
|
String sKeseluruhan = statusKeseluruhan(sSuhu, sKelembapan, sCO2, sPM25);
|
|
|
|
FirebaseJson json;
|
|
json.set("suhu", amankanNilai(suhu, 25.0), 2);
|
|
json.set("suhu_status", sSuhu);
|
|
json.set("kelembapan", amankanNilai(kelembapan, 50.0));
|
|
json.set("kelembapan_status", sKelembapan);
|
|
json.set("co2_ppm", amankanNilai(co2ppm, 400.0));
|
|
json.set("co2_status", sCO2);
|
|
json.set("co2_adc_raw", amankanNilai(mq135AdcRaw, 0.0));
|
|
json.set("pm25_ugm3", amankanNilai(pm25, 0.0));
|
|
json.set("pm25_status", sPM25);
|
|
json.set("pm25_adc_raw", amankanNilai(gp2yAdcRaw, 0.0));
|
|
json.set("status_keseluruhan", sKeseluruhan);
|
|
json.set("wifi_status", wifiConnected ? "connected" : "disconnected");
|
|
json.set("ac_auto_enabled", autoACEnabled);
|
|
json.set("ac_control_mode", modeKontrolToString());
|
|
json.set("ac_last_auto_action", lastAutoAction);
|
|
json.set("last_update", (unsigned long)time(nullptr));
|
|
|
|
if (!Firebase.RTDB.setJSON(&fbdo, "/monitoring/current", &json)) {
|
|
if (fbdo.errorReason().indexOf("auth") >= 0 || fbdo.errorReason().indexOf("token") >= 0) {
|
|
firebaseReady = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void cekDanSimpanHistoryPerMenit() {
|
|
if (!timeSynced) return;
|
|
|
|
time_t epoch = time(nullptr);
|
|
struct tm timeinfo;
|
|
localtime_r(&epoch, &timeinfo);
|
|
|
|
bool menitKelipatan5 = (timeinfo.tm_min % 5 == 0);
|
|
|
|
if (menitKelipatan5 && timeinfo.tm_min != lastHistoryMinuteSaved) {
|
|
lastHistoryMinuteSaved = timeinfo.tm_min;
|
|
if (firebaseReady) simpanHistory();
|
|
}
|
|
}
|
|
|
|
void simpanHistory() {
|
|
if (!firebaseReady || !timeSynced) return;
|
|
if (!Firebase.ready()) { firebaseReady = false; return; }
|
|
|
|
unsigned long epoch = (unsigned long)time(nullptr);
|
|
String path = "/monitoring/history/" + String(epoch);
|
|
|
|
String sSuhu = kategoriSuhu(suhu);
|
|
String sKelembapan = kategoriKelembapan(kelembapan);
|
|
String sCO2 = kategoriCO2(co2ppm);
|
|
String sPM25 = kategoriPM25(pm25);
|
|
String sKeseluruhan = statusKeseluruhan(sSuhu, sKelembapan, sCO2, sPM25);
|
|
|
|
FirebaseJson json;
|
|
json.set("suhu", amankanNilai(suhu, 25.0), 2);
|
|
json.set("suhu_status", sSuhu);
|
|
json.set("kelembapan", amankanNilai(kelembapan, 50.0));
|
|
json.set("kelembapan_status", sKelembapan);
|
|
json.set("co2_ppm", amankanNilai(co2ppm, 400.0));
|
|
json.set("co2_status", sCO2);
|
|
json.set("pm25_ugm3", amankanNilai(pm25, 0.0));
|
|
json.set("pm25_status", sPM25);
|
|
json.set("status_keseluruhan", sKeseluruhan);
|
|
json.set("timestamp", epoch);
|
|
|
|
Firebase.RTDB.setJSON(&fbdo, path.c_str(), &json);
|
|
}
|
|
|
|
void connectWiFi() {
|
|
WiFi.mode(WIFI_STA);
|
|
WiFi.setAutoReconnect(true);
|
|
WiFi.persistent(true);
|
|
|
|
const int MAX_PERCOBAAN_AWAL = 3;
|
|
int percobaan = 0;
|
|
|
|
while (WiFi.status() != WL_CONNECTED && percobaan < MAX_PERCOBAAN_AWAL) {
|
|
percobaan++;
|
|
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
|
unsigned long start = millis();
|
|
while (WiFi.status() != WL_CONNECTED && millis() - start < 20000) {
|
|
delay(300);
|
|
digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
|
|
}
|
|
|
|
if (WiFi.status() != WL_CONNECTED) {
|
|
delay(1500);
|
|
}
|
|
}
|
|
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
wifiConnected = true;
|
|
syncTime();
|
|
}
|
|
}
|
|
|
|
void ensureWifiConnected(unsigned long now) {
|
|
if (WiFi.status() == WL_CONNECTED) {
|
|
if (!wifiConnected) {
|
|
wifiConnected = true;
|
|
syncTime();
|
|
}
|
|
return;
|
|
}
|
|
if (wifiConnected) {
|
|
wifiConnected = false;
|
|
firebaseReady = false;
|
|
}
|
|
if (now - lastWifiTryMillis >= WIFI_RETRY_MS) {
|
|
lastWifiTryMillis = now;
|
|
WiFi.disconnect();
|
|
delay(100);
|
|
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
|
|
}
|
|
}
|
|
|
|
void syncTime() {
|
|
if (WiFi.status() != WL_CONNECTED) return;
|
|
configTime(gmtOffsetSec, daylightOffsetSec, ntpServer);
|
|
struct tm timeinfo;
|
|
if (getLocalTime(&timeinfo, 5000)) {
|
|
timeSynced = true;
|
|
}
|
|
}
|
|
|
|
void ensureFirebaseReady(unsigned long now) {
|
|
if (firebaseReady) {
|
|
if (Firebase.ready()) return;
|
|
firebaseReady = false;
|
|
}
|
|
if (WiFi.status() != WL_CONNECTED) return;
|
|
if (!timeSynced) return;
|
|
if (now - lastFirebaseTry < FIREBASE_RETRY_MS) return;
|
|
lastFirebaseTry = now;
|
|
|
|
if (ESP.getFreeHeap() < 20000) {
|
|
return;
|
|
}
|
|
|
|
fbdo.clear();
|
|
delay(100);
|
|
config.api_key = API_KEY;
|
|
config.database_url = DATABASE_URL;
|
|
auth.user.email = USER_EMAIL;
|
|
auth.user.password = USER_PASSWORD;
|
|
config.token_status_callback = tokenStatusCallback;
|
|
config.timeout.socketConnection = 5000;
|
|
config.timeout.serverResponse = 5000;
|
|
|
|
Firebase.begin(&config, &auth);
|
|
Firebase.reconnectWiFi(true);
|
|
|
|
unsigned long start = millis();
|
|
while (!Firebase.ready() && millis() - start < 8000) {
|
|
delay(100);
|
|
yield();
|
|
}
|
|
|
|
if (Firebase.ready()) {
|
|
firebaseReady = true;
|
|
kirimStatusKonfigurasiKeFirebase();
|
|
}
|
|
}
|
|
|
|
void updateStatusLed() {
|
|
static unsigned long lastToggle = 0;
|
|
static bool ledState = false;
|
|
unsigned long now = millis();
|
|
if (firebaseReady) {
|
|
digitalWrite(STATUS_LED_PIN, HIGH);
|
|
return;
|
|
}
|
|
unsigned long interval = wifiConnected ? 200 : 600;
|
|
if (now - lastToggle >= interval) {
|
|
lastToggle = now;
|
|
ledState = !ledState;
|
|
digitalWrite(STATUS_LED_PIN, ledState ? HIGH : LOW);
|
|
}
|
|
}
|
|
|
|
void kirimDiagnostikBoot() {
|
|
esp_reset_reason_t alasan = esp_reset_reason();
|
|
String teksAlasan = alasanResetKeString(alasan);
|
|
FirebaseJson json;
|
|
json.set("reset_reason", teksAlasan);
|
|
json.set("boot_time", (unsigned long)time(nullptr));
|
|
json.set("free_heap_saat_boot", (int)ESP.getFreeHeap());
|
|
Firebase.RTDB.setJSON(&fbdo, "/monitoring/diagnostik_boot_terakhir", &json);
|
|
}
|
|
|
|
void catatBootKeFlash() {
|
|
prefs.begin("diag", false);
|
|
int jumlahBoot = prefs.getInt("boot_count", 0) + 1;
|
|
prefs.putInt("boot_count", jumlahBoot);
|
|
esp_reset_reason_t alasan = esp_reset_reason();
|
|
String teksAlasan = alasanResetKeString(alasan);
|
|
prefs.putString("last_reason", teksAlasan);
|
|
if (alasan != ESP_RST_POWERON) {
|
|
int jumlahAneh = prefs.getInt("abnormal_count", 0) + 1;
|
|
prefs.putInt("abnormal_count", jumlahAneh);
|
|
}
|
|
prefs.end();
|
|
}
|
|
|
|
String alasanResetKeString(esp_reset_reason_t alasan) {
|
|
switch (alasan) {
|
|
case ESP_RST_POWERON: return "Power-on (normal)";
|
|
case ESP_RST_BROWNOUT: return "BROWNOUT - suplai daya kurang!";
|
|
case ESP_RST_TASK_WDT: return "TASK WATCHDOG - blocking terlalu lama";
|
|
case ESP_RST_INT_WDT: return "INTERRUPT WATCHDOG";
|
|
case ESP_RST_PANIC: return "PANIC/CRASH firmware";
|
|
case ESP_RST_SW: return "Software reset (ESP.restart())";
|
|
case ESP_RST_DEEPSLEEP: return "Bangun dari deep sleep";
|
|
case ESP_RST_EXT: return "External reset (tombol RESET)";
|
|
default: return "Lainnya (kode: " + String((int)alasan) + ")";
|
|
}
|
|
}
|