#include #include #include #include #include #include #include #include "pin_config.h" #include "config_manager.h" #include "wifi_manager.h" #include "gsm_manager.h" #include "transport_manager.h" #include "mqtt_client.h" #include "acuvim_reader.h" #include "sd_manager.h" #include "web_server.h" #include "ota_manager.h" #include "heartbeat_manager.h" #include "health_monitor.h" #include "command_handler.h" ConfigManager configMgr; WifiManager wifiMgr; HardwareSerial modemSerial(1); GsmManager gsmMgr(modemSerial); TransportManager transportMgr; MqttClient mqttClient; AcuvimReader acuvim; SdManager sdMgr; OtaManager otaMgr; HeartbeatManager heartbeatMgr; HealthMonitor healthMon; CommandHandler cmdHandler; WebPortal webPortal; WiFiClient wifiNetClient; unsigned long lastPollMs = 0; bool sleepScheduled = false; void checkCrashLoop() { esp_reset_reason_t reason = esp_reset_reason(); bool wasCrash = (reason == ESP_RST_PANIC || reason == ESP_RST_INT_WDT || reason == ESP_RST_TASK_WDT || reason == ESP_RST_WDT); Preferences prefs; prefs.begin("acuvim_sys", false); if (wasCrash) { uint8_t crashes = prefs.getUChar("crash_count", 0) + 1; prefs.putUChar("crash_count", crashes); Serial.printf("[Main] Crash detected (count: %d)\n", crashes); if (crashes >= 5) { Serial.println("[Main] Crash loop detected, performing factory reset"); prefs.putUChar("crash_count", 0); prefs.end(); configMgr.reset(); configMgr.save(); ESP.restart(); } } else { prefs.putUChar("crash_count", 0); } prefs.end(); } void printAcuvimData(const AcuvimData& d) { Serial.println("========================================"); Serial.println(" Acuvim II Readings"); Serial.println("========================================"); Serial.printf(" Va: %7.1f V Vb: %7.1f V Vc: %7.1f V\n", d.voltage_a, d.voltage_b, d.voltage_c); Serial.printf(" Ia: %7.2f A Ib: %7.2f A Ic: %7.2f A\n", d.current_a, d.current_b, d.current_c); Serial.printf(" P: %7.3f kW Q: %7.3f kVAR S: %7.3f kVA\n", d.active_power, d.reactive_power, d.apparent_power); Serial.printf(" PF: %7.3f F: %7.2f Hz\n", d.power_factor, d.frequency); Serial.printf(" Import: %.1f kWh Export: %.1f kWh\n", d.import_active_energy, d.export_active_energy); Serial.printf(" Read: %lu ms Success: %lu Errors: %lu Heap: %lu\n", acuvim.getLastReadDurationMs(), acuvim.getSuccessCount(), acuvim.getErrorCount(), (unsigned long)ESP.getFreeHeap()); Serial.println("========================================\n"); } void setup() { Serial.begin(115200); while (!Serial && millis() < 3000) { ; } Serial.println(); Serial.println("========================================"); Serial.println(" Tau Acuvim II Monitor"); Serial.printf(" Firmware: %s\n", FW_VERSION); Serial.printf(" Build: %s %s\n", __DATE__, __TIME__); Serial.println("========================================"); Serial.println(); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); // --- OTA self-test validation --- { const esp_partition_t* running = esp_ota_get_running_partition(); esp_ota_img_states_t ota_state; if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) { if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) { Serial.println("[OTA] New firmware pending validation - marking valid after successful boot"); esp_ota_mark_app_valid_cancel_rollback(); } } } // --- Crash loop detection --- checkCrashLoop(); // --- Watchdog (30s timeout, panic on expiry) --- esp_task_wdt_init(30, true); esp_task_wdt_add(NULL); // --- Config --- configMgr.begin(); DeviceConfig& cfg = configMgr.get(); // --- Modbus --- acuvim.begin(cfg.modbus_slave_addr, cfg.modbus_baud_rate); // --- SD Card --- sdMgr.begin(cfg.device_id); sdMgr.setRetentionDays(cfg.sd_retention_days); // --- GSM --- gsmMgr.begin(); // --- Transport --- transportMgr.begin(wifiMgr, gsmMgr, configMgr, &wifiNetClient); // --- Health Monitor --- healthMon.begin(configMgr, mqttClient, acuvim); // --- Command Handler --- cmdHandler.begin(configMgr, wifiMgr, gsmMgr, transportMgr, mqttClient, acuvim, otaMgr, sdMgr, healthMon); // --- MQTT --- mqttClient.begin(transportMgr.getClient()); mqttClient.setConfig(cfg); mqttClient.onCommand([](const String& topic, const String& payload) { cmdHandler.handleCommand(topic, payload); }); // --- Transport change callback --- transportMgr.onTransportChange([](TransportType type) { if (type == TransportType::NONE) { Serial.println("[Main] No transport available"); mqttClient.disconnect(); return; } Serial.printf("[Main] Transport changed to %s, reconnecting MQTT...\n", TransportManager::transportTypeName(type)); mqttClient.disconnect(); mqttClient.begin(transportMgr.getClient()); mqttClient.setConfig(configMgr.get()); mqttClient.connect(); heartbeatMgr.sendNow(); }); // --- WiFi: start AP first (always available for configuration) --- wifiMgr.startAP(cfg.device_id); // --- WiFi: STA connection --- if (cfg.wifi_enabled && cfg.wifi_ssid[0] != '\0') { wifiMgr.onConnect([]() { Serial.println("[Main] WiFi connected"); transportMgr.forceEvaluate(); }); wifiMgr.onDisconnect([]() { Serial.println("[Main] WiFi lost"); transportMgr.forceEvaluate(); }); wifiMgr.begin(cfg.wifi_ssid, cfg.wifi_password); } else { Serial.println("[Main] WiFi STA disabled or not configured"); } // --- GSM: connect if enabled and modem available --- if (gsmMgr.isModemAvailable() && cfg.gsm_enabled && cfg.gsm_apn[0] != '\0') { gsmMgr.connect(cfg.gsm_apn, cfg.gsm_user, cfg.gsm_pass); transportMgr.forceEvaluate(); } // --- OTA Manager --- otaMgr.begin(configMgr, transportMgr, gsmMgr); // --- Heartbeat Manager --- heartbeatMgr.begin(configMgr, transportMgr, mqttClient, acuvim, wifiMgr, gsmMgr, sdMgr, otaMgr); // --- Web Portal --- webPortal.begin(configMgr, wifiMgr, mqttClient, acuvim, gsmMgr, transportMgr, sdMgr, otaMgr); Serial.printf("[Main] Poll interval: %d s (GSM: %d s)\n", cfg.poll_interval_sec, cfg.gsm_poll_interval_sec); Serial.println("[Main] Setup complete\n"); } void loop() { esp_task_wdt_reset(); wifiMgr.loop(); transportMgr.loop(); mqttClient.loop(); sdMgr.loop(); otaMgr.loop(); heartbeatMgr.loop(); webPortal.loop(); // Device registration on first MQTT connect if (mqttClient.isConnected() && !cmdHandler.isRegistered()) { cmdHandler.sendRegistration(); } if (mqttClient.isConnected() && sdMgr.hasQueuedData()) { sdMgr.drainBatch(mqttClient, 5); } DeviceConfig& cfg = configMgr.get(); unsigned long intervalMs; if (transportMgr.getActiveTransport() == TransportType::GSM) { intervalMs = (unsigned long)cfg.gsm_poll_interval_sec * 1000; } else { intervalMs = (unsigned long)cfg.poll_interval_sec * 1000; } unsigned long now = millis(); if (now - lastPollMs < intervalMs) { return; } lastPollMs = now; digitalWrite(LED_PIN, HIGH); AcuvimData data; if (acuvim.readAll(data)) { printAcuvimData(data); webPortal.setLatestData(data); cmdHandler.setLatestData(data); healthMon.evaluate(data); if (mqttClient.isConnected()) { const char* connType = TransportManager::transportTypeName( transportMgr.getActiveTransport()); int signal = transportMgr.getSignalStrength(); const char* opName = nullptr; String opStr; if (transportMgr.getActiveTransport() == TransportType::GSM) { opStr = gsmMgr.getOperator(); opName = opStr.c_str(); } mqttClient.publishTelemetry(data, connType, signal, opName); } else if (sdMgr.isAvailable()) { sdMgr.bufferTelemetry(data); } } else { Serial.printf("[Main] Read failed - error: 0x%02X, consecutive: %lu, total: %lu\n", acuvim.getLastError(), acuvim.getConsecutiveErrors(), acuvim.getErrorCount()); healthMon.evaluate(data); } digitalWrite(LED_PIN, LOW); // Deep sleep support if (cfg.sleep_duration_min > 0 && !sleepScheduled) { unsigned long wakeMs = (unsigned long)cfg.wake_duration_sec * 1000; if (millis() > wakeMs) { sleepScheduled = true; Serial.printf("[Main] Entering deep sleep for %d minutes\n", cfg.sleep_duration_min); heartbeatMgr.sendNow(); delay(500); mqttClient.disconnect(); wifiMgr.disconnect(); gsmMgr.powerOff(); esp_task_wdt_delete(NULL); uint64_t sleepMicros = (uint64_t)cfg.sleep_duration_min * 60 * 1000000; esp_sleep_enable_timer_wakeup(sleepMicros); esp_deep_sleep_start(); } } }