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>
163 lines
5.0 KiB
C++
163 lines
5.0 KiB
C++
#include <unity.h>
|
|
#include <ArduinoJson.h>
|
|
|
|
struct TestConfig {
|
|
char wifi_ssid[33];
|
|
char wifi_password[65];
|
|
char mqtt_broker[65];
|
|
uint16_t mqtt_port;
|
|
uint8_t modbus_address;
|
|
uint32_t modbus_baud;
|
|
uint16_t poll_interval_sec;
|
|
uint8_t transport_priority;
|
|
};
|
|
|
|
static bool validateConfig(TestConfig& cfg) {
|
|
if (strlen(cfg.wifi_ssid) == 0) return false;
|
|
if (cfg.mqtt_port == 0 || cfg.mqtt_port > 65535) return false;
|
|
if (cfg.modbus_address == 0 || cfg.modbus_address > 247) return false;
|
|
if (cfg.modbus_baud != 9600 && cfg.modbus_baud != 19200 &&
|
|
cfg.modbus_baud != 38400 && cfg.modbus_baud != 57600 &&
|
|
cfg.modbus_baud != 115200) return false;
|
|
if (cfg.poll_interval_sec < 1 || cfg.poll_interval_sec > 3600) return false;
|
|
if (cfg.transport_priority > 3) return false;
|
|
return true;
|
|
}
|
|
|
|
void test_valid_config() {
|
|
TestConfig cfg;
|
|
strncpy(cfg.wifi_ssid, "TestNetwork", sizeof(cfg.wifi_ssid));
|
|
strncpy(cfg.wifi_password, "password123", sizeof(cfg.wifi_password));
|
|
strncpy(cfg.mqtt_broker, "broker.example.com", sizeof(cfg.mqtt_broker));
|
|
cfg.mqtt_port = 1883;
|
|
cfg.modbus_address = 1;
|
|
cfg.modbus_baud = 9600;
|
|
cfg.poll_interval_sec = 5;
|
|
cfg.transport_priority = 0;
|
|
|
|
TEST_ASSERT_TRUE(validateConfig(cfg));
|
|
}
|
|
|
|
void test_empty_ssid_invalid() {
|
|
TestConfig cfg = {};
|
|
cfg.mqtt_port = 1883;
|
|
cfg.modbus_address = 1;
|
|
cfg.modbus_baud = 9600;
|
|
cfg.poll_interval_sec = 5;
|
|
|
|
TEST_ASSERT_FALSE(validateConfig(cfg));
|
|
}
|
|
|
|
void test_invalid_modbus_address() {
|
|
TestConfig cfg;
|
|
strncpy(cfg.wifi_ssid, "TestNetwork", sizeof(cfg.wifi_ssid));
|
|
cfg.mqtt_port = 1883;
|
|
cfg.modbus_address = 0;
|
|
cfg.modbus_baud = 9600;
|
|
cfg.poll_interval_sec = 5;
|
|
cfg.transport_priority = 0;
|
|
|
|
TEST_ASSERT_FALSE(validateConfig(cfg));
|
|
|
|
cfg.modbus_address = 248;
|
|
TEST_ASSERT_FALSE(validateConfig(cfg));
|
|
}
|
|
|
|
void test_invalid_baud_rate() {
|
|
TestConfig cfg;
|
|
strncpy(cfg.wifi_ssid, "TestNetwork", sizeof(cfg.wifi_ssid));
|
|
cfg.mqtt_port = 1883;
|
|
cfg.modbus_address = 1;
|
|
cfg.modbus_baud = 12345;
|
|
cfg.poll_interval_sec = 5;
|
|
cfg.transport_priority = 0;
|
|
|
|
TEST_ASSERT_FALSE(validateConfig(cfg));
|
|
}
|
|
|
|
void test_poll_interval_bounds() {
|
|
TestConfig cfg;
|
|
strncpy(cfg.wifi_ssid, "TestNetwork", sizeof(cfg.wifi_ssid));
|
|
cfg.mqtt_port = 1883;
|
|
cfg.modbus_address = 1;
|
|
cfg.modbus_baud = 9600;
|
|
cfg.transport_priority = 0;
|
|
|
|
cfg.poll_interval_sec = 0;
|
|
TEST_ASSERT_FALSE(validateConfig(cfg));
|
|
|
|
cfg.poll_interval_sec = 3601;
|
|
TEST_ASSERT_FALSE(validateConfig(cfg));
|
|
|
|
cfg.poll_interval_sec = 1;
|
|
TEST_ASSERT_TRUE(validateConfig(cfg));
|
|
|
|
cfg.poll_interval_sec = 3600;
|
|
TEST_ASSERT_TRUE(validateConfig(cfg));
|
|
}
|
|
|
|
void test_config_from_json() {
|
|
const char* json = R"({"wifi":{"ssid":"TestNet","password":"pass123"},"mqtt":{"broker":"mqtt.test","port":8883},"modbus":{"address":5,"baud":19200,"poll_interval":10}})";
|
|
|
|
JsonDocument doc;
|
|
DeserializationError err = deserializeJson(doc, json);
|
|
TEST_ASSERT_EQUAL(DeserializationError::Ok, err);
|
|
|
|
TestConfig cfg = {};
|
|
strncpy(cfg.wifi_ssid, doc["wifi"]["ssid"] | "", sizeof(cfg.wifi_ssid));
|
|
strncpy(cfg.wifi_password, doc["wifi"]["password"] | "", sizeof(cfg.wifi_password));
|
|
strncpy(cfg.mqtt_broker, doc["mqtt"]["broker"] | "", sizeof(cfg.mqtt_broker));
|
|
cfg.mqtt_port = doc["mqtt"]["port"] | 1883;
|
|
cfg.modbus_address = doc["modbus"]["address"] | 1;
|
|
cfg.modbus_baud = doc["modbus"]["baud"] | 9600;
|
|
cfg.poll_interval_sec = doc["modbus"]["poll_interval"] | 5;
|
|
cfg.transport_priority = 0;
|
|
|
|
TEST_ASSERT_EQUAL_STRING("TestNet", cfg.wifi_ssid);
|
|
TEST_ASSERT_EQUAL(8883, cfg.mqtt_port);
|
|
TEST_ASSERT_EQUAL(5, cfg.modbus_address);
|
|
TEST_ASSERT_EQUAL(19200, cfg.modbus_baud);
|
|
TEST_ASSERT_EQUAL(10, cfg.poll_interval_sec);
|
|
TEST_ASSERT_TRUE(validateConfig(cfg));
|
|
}
|
|
|
|
void test_config_to_json() {
|
|
TestConfig cfg;
|
|
strncpy(cfg.wifi_ssid, "MyNetwork", sizeof(cfg.wifi_ssid));
|
|
strncpy(cfg.mqtt_broker, "broker.local", sizeof(cfg.mqtt_broker));
|
|
cfg.mqtt_port = 1883;
|
|
cfg.modbus_address = 1;
|
|
cfg.modbus_baud = 9600;
|
|
cfg.poll_interval_sec = 5;
|
|
|
|
JsonDocument doc;
|
|
JsonObject root = doc.to<JsonObject>();
|
|
root["wifi"]["ssid"] = cfg.wifi_ssid;
|
|
root["mqtt"]["broker"] = cfg.mqtt_broker;
|
|
root["mqtt"]["port"] = cfg.mqtt_port;
|
|
root["modbus"]["address"] = cfg.modbus_address;
|
|
root["modbus"]["baud"] = cfg.modbus_baud;
|
|
root["modbus"]["poll_interval"] = cfg.poll_interval_sec;
|
|
|
|
String output;
|
|
serializeJson(doc, output);
|
|
|
|
TEST_ASSERT_TRUE(output.indexOf("MyNetwork") >= 0);
|
|
TEST_ASSERT_TRUE(output.indexOf("1883") >= 0);
|
|
}
|
|
|
|
void setup() {
|
|
delay(2000);
|
|
UNITY_BEGIN();
|
|
RUN_TEST(test_valid_config);
|
|
RUN_TEST(test_empty_ssid_invalid);
|
|
RUN_TEST(test_invalid_modbus_address);
|
|
RUN_TEST(test_invalid_baud_rate);
|
|
RUN_TEST(test_poll_interval_bounds);
|
|
RUN_TEST(test_config_from_json);
|
|
RUN_TEST(test_config_to_json);
|
|
UNITY_END();
|
|
}
|
|
|
|
void loop() {}
|