#include "heartbeat_manager.h" #include #include #include #include static String getResetReasonString() { esp_reset_reason_t reason = esp_reset_reason(); switch (reason) { case ESP_RST_POWERON: return "POWER_ON"; case ESP_RST_EXT: return "EXTERNAL"; case ESP_RST_SW: return "SW_RESET"; case ESP_RST_PANIC: return "PANIC"; case ESP_RST_INT_WDT: return "INT_WDT"; case ESP_RST_TASK_WDT: return "TASK_WDT"; case ESP_RST_WDT: return "WDT"; case ESP_RST_DEEPSLEEP: return "DEEP_SLEEP"; case ESP_RST_BROWNOUT: return "BROWNOUT"; default: return "UNKNOWN"; } } static uint32_t getEpochTime() { time_t now; time(&now); return (now < 1700000000) ? millis() / 1000 : (uint32_t)now; } void HeartbeatManager::begin(ConfigManager& config, TransportManager& transport, MqttClient& mqtt, AcuvimReader& acuvim, WifiManager& wifi, GsmManager& gsm, SdManager& sd, OtaManager& ota) { _config = &config; _transport = &transport; _mqtt = &mqtt; _acuvim = &acuvim; _wifi = &wifi; _gsm = &gsm; _sd = &sd; _ota = &ota; _lastHeartbeatMs = 0; _heartbeatCount = 0; _sentInitial = false; loadBootCount(); Serial.printf("[HB] Boot count: %lu, last reset: %s\n", _bootCount, getResetReasonString().c_str()); } void HeartbeatManager::loadBootCount() { Preferences prefs; prefs.begin("acuvim_sys", false); _bootCount = prefs.getUInt("boot_count", 0) + 1; prefs.putUInt("boot_count", _bootCount); prefs.end(); } void HeartbeatManager::loop() { if (!_mqtt->isConnected()) return; if (!_sentInitial) { _sentInitial = true; sendNow(); return; } DeviceConfig& cfg = _config->get(); unsigned long intervalMs = (unsigned long)cfg.heartbeat_interval_sec * 1000; if (millis() - _lastHeartbeatMs >= intervalMs) { sendNow(); } } void HeartbeatManager::sendNow() { if (!_mqtt->isConnected()) return; String payload = buildPayload(); DeviceConfig& cfg = _config->get(); char topic[128]; snprintf(topic, sizeof(topic), "%s/%s/heartbeat", cfg.mqtt_topic_prefix, cfg.device_id); if (_mqtt->publish(topic, payload.c_str())) { _heartbeatCount++; _lastHeartbeatMs = millis(); } else { Serial.println("[HB] Publish failed"); } } String HeartbeatManager::buildPayload() { JsonDocument doc; DeviceConfig& cfg = _config->get(); doc["ts"] = getEpochTime(); doc["dev"] = cfg.device_id; doc["fw"] = FW_VERSION; doc["up"] = millis() / 1000; doc["boot"] = _bootCount; doc["hb"] = _heartbeatCount + 1; // Connection info JsonObject conn = doc["conn"].to(); const char* transportName = TransportManager::transportTypeName(_transport->getActiveTransport()); conn["type"] = transportName; JsonObject wifiObj = conn["wifi"].to(); wifiObj["ssid"] = _wifi->getSSID(); wifiObj["rssi"] = _wifi->getRSSI(); wifiObj["ip"] = _wifi->getIPAddress(); JsonObject gsmObj = conn["gsm"].to(); gsmObj["enabled"] = cfg.gsm_enabled; gsmObj["connected"] = _gsm->isConnected(); gsmObj["signal"] = _gsm->getSignalPercent(); gsmObj["operator"] = _gsm->getOperator(); conn["mqtt"] = _mqtt->isConnected(); // Health JsonObject health = doc["health"].to(); health["heap_free"] = ESP.getFreeHeap(); health["heap_min"] = ESP.getMinFreeHeap(); health["psram_free"] = ESP.getFreePsram(); health["reset_reason"] = getResetReasonString(); health["uptime_sec"] = millis() / 1000; if (ESP.getMinFreeHeap() < 30000) { Serial.printf("[HB] WARNING: Low min free heap: %lu bytes\n", (unsigned long)ESP.getMinFreeHeap()); } // Modbus JsonObject modbus = doc["modbus"].to(); uint32_t total = _acuvim->getSuccessCount() + _acuvim->getErrorCount(); modbus["connected"] = _acuvim->getConsecutiveErrors() == 0 && _acuvim->getSuccessCount() > 0; modbus["success"] = _acuvim->getSuccessCount(); modbus["errors"] = _acuvim->getErrorCount(); modbus["error_rate"] = (total > 0) ? serialized(String((float)_acuvim->getErrorCount() * 100.0f / total, 1)) : serialized("0.0"); modbus["last_error"] = _acuvim->getLastError(); modbus["last_read_ms"] = _acuvim->getLastReadDurationMs(); // SD JsonObject sd = doc["sd"].to(); sd["available"] = _sd->isAvailable(); sd["queued"] = _sd->getQueuedCount(); if (_sd->isAvailable()) { sd["free_mb"] = (uint32_t)(_sd->getFreeSpace() / 1048576); } // OTA JsonObject ota = doc["ota"].to(); ota["version"] = FW_VERSION; ota["partition"] = _ota->getRunningPartition(); ota["update_available"] = _ota->isUpdateAvailable(); String output; serializeJson(doc, output); return output; }