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>
153 lines
4.4 KiB
C++
153 lines
4.4 KiB
C++
#include <unity.h>
|
|
#include <ArduinoJson.h>
|
|
|
|
void test_telemetry_serialization() {
|
|
JsonDocument doc;
|
|
JsonObject root = doc.to<JsonObject>();
|
|
root["ts"] = 1716000000;
|
|
root["dev"] = "ACV-TEST01";
|
|
|
|
JsonObject v = root["v"].to<JsonObject>();
|
|
v["a"] = 230.1;
|
|
v["b"] = 231.4;
|
|
v["c"] = 229.8;
|
|
|
|
JsonObject i = root["i"].to<JsonObject>();
|
|
i["a"] = 15.2;
|
|
i["b"] = 14.8;
|
|
i["c"] = 15.5;
|
|
|
|
JsonObject p = root["p"].to<JsonObject>();
|
|
p["act"] = 10.5;
|
|
p["react"] = 2.1;
|
|
p["app"] = 10.7;
|
|
|
|
root["f"] = 50.01;
|
|
|
|
String output;
|
|
serializeJson(doc, output);
|
|
|
|
TEST_ASSERT_TRUE(output.length() > 0);
|
|
TEST_ASSERT_TRUE(output.indexOf("ACV-TEST01") >= 0);
|
|
TEST_ASSERT_TRUE(output.indexOf("230.1") >= 0);
|
|
}
|
|
|
|
void test_telemetry_deserialization() {
|
|
const char* json = R"({"ts":1716000000,"dev":"ACV-TEST01","v":{"a":230.1,"b":231.4,"c":229.8},"f":50.01})";
|
|
|
|
JsonDocument doc;
|
|
DeserializationError err = deserializeJson(doc, json);
|
|
|
|
TEST_ASSERT_EQUAL(DeserializationError::Ok, err);
|
|
TEST_ASSERT_EQUAL(1716000000, doc["ts"].as<long>());
|
|
TEST_ASSERT_EQUAL_STRING("ACV-TEST01", doc["dev"].as<const char*>());
|
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 230.1, doc["v"]["a"].as<float>());
|
|
TEST_ASSERT_FLOAT_WITHIN(0.01, 50.01, doc["f"].as<float>());
|
|
}
|
|
|
|
void test_command_parsing() {
|
|
const char* json = R"({"cmd":"wifi_set","request_id":"cmd-abc123","params":{"ssid":"TestNet","password":"secret"}})";
|
|
|
|
JsonDocument doc;
|
|
DeserializationError err = deserializeJson(doc, json);
|
|
|
|
TEST_ASSERT_EQUAL(DeserializationError::Ok, err);
|
|
TEST_ASSERT_EQUAL_STRING("wifi_set", doc["cmd"].as<const char*>());
|
|
TEST_ASSERT_EQUAL_STRING("cmd-abc123", doc["request_id"].as<const char*>());
|
|
TEST_ASSERT_EQUAL_STRING("TestNet", doc["params"]["ssid"].as<const char*>());
|
|
}
|
|
|
|
void test_heartbeat_serialization() {
|
|
JsonDocument doc;
|
|
JsonObject root = doc.to<JsonObject>();
|
|
root["ts"] = 1716000000;
|
|
root["dev"] = "ACV-TEST01";
|
|
root["fw"] = "1.0.0";
|
|
root["up"] = 86400;
|
|
root["boot"] = 5;
|
|
root["hb"] = 100;
|
|
|
|
JsonObject health = root["health"].to<JsonObject>();
|
|
health["heap_free"] = 120000;
|
|
health["heap_min"] = 80000;
|
|
|
|
JsonObject modbus = root["modbus"].to<JsonObject>();
|
|
modbus["connected"] = true;
|
|
modbus["success"] = 1000;
|
|
modbus["errors"] = 2;
|
|
|
|
String output;
|
|
serializeJson(doc, output);
|
|
|
|
TEST_ASSERT_TRUE(output.indexOf("\"boot\":5") >= 0);
|
|
TEST_ASSERT_TRUE(output.indexOf("\"heap_free\":120000") >= 0);
|
|
}
|
|
|
|
void test_alert_serialization() {
|
|
JsonDocument doc;
|
|
JsonObject root = doc.to<JsonObject>();
|
|
root["alert"] = "overvoltage";
|
|
root["severity"] = "warning";
|
|
root["message"] = "Phase A voltage 265.3V exceeds threshold 260.0V";
|
|
root["phase"] = "a";
|
|
root["value"] = 265.3;
|
|
root["threshold"] = 260.0;
|
|
|
|
String output;
|
|
serializeJson(doc, output);
|
|
|
|
TEST_ASSERT_TRUE(output.indexOf("overvoltage") >= 0);
|
|
TEST_ASSERT_TRUE(output.indexOf("265.3") >= 0);
|
|
}
|
|
|
|
void test_registration_payload() {
|
|
JsonDocument doc;
|
|
JsonObject root = doc.to<JsonObject>();
|
|
root["device_id"] = "ACV-AABBCCDDEEFF";
|
|
root["mac"] = "AA:BB:CC:DD:EE:FF";
|
|
root["firmware"] = "1.0.0";
|
|
root["hardware"] = "TTGO T-Call v1.4";
|
|
|
|
JsonArray caps = root["capabilities"].to<JsonArray>();
|
|
caps.add("modbus");
|
|
caps.add("wifi");
|
|
caps.add("gsm");
|
|
caps.add("sd");
|
|
caps.add("ota");
|
|
|
|
String output;
|
|
serializeJson(doc, output);
|
|
|
|
TEST_ASSERT_TRUE(output.indexOf("ACV-AABBCCDDEEFF") >= 0);
|
|
TEST_ASSERT_TRUE(output.indexOf("\"modbus\"") >= 0);
|
|
}
|
|
|
|
void test_empty_json_handling() {
|
|
JsonDocument doc;
|
|
DeserializationError err = deserializeJson(doc, "{}");
|
|
TEST_ASSERT_EQUAL(DeserializationError::Ok, err);
|
|
TEST_ASSERT_TRUE(doc["missing"].isNull());
|
|
}
|
|
|
|
void test_invalid_json_handling() {
|
|
JsonDocument doc;
|
|
DeserializationError err = deserializeJson(doc, "not json");
|
|
TEST_ASSERT_NOT_EQUAL(DeserializationError::Ok, err);
|
|
}
|
|
|
|
void setup() {
|
|
delay(2000);
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_telemetry_serialization);
|
|
RUN_TEST(test_telemetry_deserialization);
|
|
RUN_TEST(test_command_parsing);
|
|
RUN_TEST(test_heartbeat_serialization);
|
|
RUN_TEST(test_alert_serialization);
|
|
RUN_TEST(test_registration_payload);
|
|
RUN_TEST(test_empty_json_handling);
|
|
RUN_TEST(test_invalid_json_handling);
|
|
UNITY_END();
|
|
}
|
|
|
|
void loop() {}
|