Tau.Acuvim/firmware/src/main.cpp
Renier Forster 84a0668c54 Initial commit: Tau Acuvim IoT monitoring system
Complete IoT monitoring platform for Acuvim II power meters via ESP32.

Firmware (Phases 1-7):
- ESP32-WROVER-B (TTGO T-Call v1.4) with RS485 Modbus RTU
- WiFi STA+AP concurrent mode with GSM/GPRS failover
- Transport abstraction layer with 4 priority modes
- MQTT protocol with 20 commands, LWT, QoS, exponential backoff
- SD card offline buffering with JSONL rotation and non-blocking drain
- OTA firmware updates with dual partition rollback protection
- Watchdog timer, crash loop detection, Acuvim health monitoring
- Captive portal provisioning with AP mode

Console backend (Phase 8):
- .NET 10 minimal API with PostgreSQL + EF Core
- JWT authentication, SignalR real-time updates
- MQTTnet 5.x bridge service with health monitoring
- Device, telemetry, firmware, alert, group management
- Rate limiting, security headers, Swagger/OpenAPI

Frontend (Phase 9):
- React 18 + TypeScript + Vite with Ant Design 5
- ECharts telemetry visualization, TanStack Query
- SignalR live updates, device management UI
- Dashboard, fleet management, firmware deployment

Testing & Production (Phase 10):
- 28 firmware unit tests (Modbus, JSON, config, version)
- 23 xUnit backend tests (device, telemetry, command, alert)
- Docker Compose with nginx, TLS MQTT, PostgreSQL
- Production deployment, commissioning, and troubleshooting docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-16 19:05:32 +02:00

288 lines
9.5 KiB
C++

#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <esp_system.h>
#include <Preferences.h>
#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();
}
}
}