diff --git a/COMPLETION.md b/COMPLETION.md new file mode 100644 index 0000000..94162d0 --- /dev/null +++ b/COMPLETION.md @@ -0,0 +1,54 @@ +# RTU_ALL_AI 开发完成事项清单 + +> 日期: 2026-07-07 + +--- + +## 阶段 0:基础设施 + +| 任务 | 模块 | 状态 | +|------|------|:----:| +| 搭建构建系统 | `release/linux.mk`, `makefile`, `build.sh`, `copy_headers.sh` | ✅ | +| 创建 myBase.h 基础定义 | `release/inc/myBase.h` | ✅ | +| 实现侵入式链表 | `liblist` | ✅ | +| 实现日志系统 | `liblog` | ✅ | +| 实现基础工具函数(65个) | `libfunc` | ✅ | + +## 阶段 1:数据基础设施 + +| 任务 | 模块 | 状态 | +|------|------|:----:| +| 实现 MD5 摘要算法 | `libmd5` | ✅ | +| 实现 xxHash 快速哈希 | `libmy_xxhash` | ✅ | +| 实现 cJSON 解析 | `libcJSON` | ✅ | +| 实现命令处理框架 | `libcmd` | ✅ | +| 实现 XML 解析 | `libxml` | ✅ | +| 实现数据中心(15文件拆分) | `libdatacenter` | ✅ | +| 实现任务调度 | `libtask` | ✅ | +| 单元测试 | `test_datacenter.cpp`(34项全通过) | ✅ | +| x86 编译验证 | 10/10 模块全通过 | ✅ | +| ARM 交叉编译验证 | 10/10 模块全通过 | ✅ | + +## 阶段 2:通讯与协议 + +| 任务 | 模块 | 状态 | +|------|------|:----:| +| 通讯抽象层 | `libcomm` | ⬜ | +| Modbus 协议栈 | `libmodbus` | ⬜ | +| IEC 60870-5-104 | `lib60870` | ⬜ | +| IEC 点表解析 | `libiec` | ⬜ | +| Modbus 主站管理 | `libmodbus_m` | ⬜ | +| 通讯通道解码 | `libcom_decode` | ⬜ | +| Web 管理服务 | `libweb_server` | ⬜ | +| 主程序入口 | `RTU` | ⬜ | + +## 阶段 3:系统集成 + +| 任务 | 模块 | 状态 | +|------|------|:----:| +| 端到端集成测试 | — | ⬜ | +| README 文档 | — | ⬜ | + +## 阶段 4(预留) + +ICP67 / IEC61850 MMS / MQTT / 嵌入式HTTP / PLC / 内部协议 —— 全部 ⬜ diff --git a/src/test/test_datacenter.cpp b/src/test/test_datacenter.cpp new file mode 100644 index 0000000..e4fdba5 --- /dev/null +++ b/src/test/test_datacenter.cpp @@ -0,0 +1,894 @@ +/** + * @file test_datacenter.cpp + * @brief 数据中心模块单元测试 + * @details 覆盖:生命周期、5 种信号注册/链接、out 延迟回调、yk 同步控制、 + * ao SBO 流程、param 多区定值、信号查询、类型工具、事件队列 + */ +#include "myDatacenter.h" +#include "myBase.h" +#include +#include +#include + +/* ===== 测试辅助 ===== */ + +static int g_pass = 0; +static int g_fail = 0; + +#define TEST(name) \ + do \ + { \ + printf(" TEST: %s ... ", name); \ + } while (0) + +#define OK() \ + do \ + { \ + printf("OK\n"); \ + g_pass++; \ + } while (0) + +#define FAIL(msg) \ + do \ + { \ + printf("FAIL: %s\n", msg); \ + g_fail++; \ + } while (0) + +/* ===== 回调测试变量 ===== */ + +static int g_out_changed = 0; +static int g_yk_changed = 0; +static int g_ao_changed = 0; +static int g_param_changed = 0; +static int g_soe_received = 0; + +static void on_out_changed(const char *saddr, uint8_t data_type, + const void *p_new, const void *p_old) +{ + (void)saddr; + (void)data_type; + (void)p_new; + (void)p_old; + + g_out_changed++; +} + +static void on_yk_changed(const char *saddr, dc_ctrl_step_t step, + uint8_t data_type, uint8_t zone, + const void *p_data) +{ + (void)saddr; + (void)step; + (void)data_type; + (void)zone; + (void)p_data; + + g_yk_changed++; +} + +static void on_ao_changed(const char *saddr, dc_ctrl_step_t step, + uint8_t data_type, uint8_t zone, + const void *p_data) +{ + (void)saddr; + (void)step; + (void)data_type; + (void)zone; + (void)p_data; + + g_ao_changed++; +} + +static void on_param_changed(const char *saddr, dc_ctrl_step_t step, + uint8_t data_type, uint8_t zone, + const void *p_data) +{ + (void)saddr; + (void)step; + (void)data_type; + (void)zone; + (void)p_data; + + g_param_changed++; +} + +static void on_soe_pop(void *p_data) +{ + (void)p_data; + + g_soe_received++; +} + +/* ===== 测试用例 ===== */ + +/** + * @brief 测试 1:生命周期 + */ +static void test_lifecycle(void) +{ + TEST("dc_init()"); + { + int r = dc_init(NULL); + + if (DC_OK != r) + { + FAIL("init failed"); + + return; + } + + OK(); + } + + TEST("dc_init() double"); + { + int r = dc_init(NULL); + + if (DC_OK != r) + { + FAIL("double init should return OK"); + + return; + } + + OK(); + } + + TEST("dc_cleanup()"); + { + dc_cleanup(); + + /* re-init for following tests */ + dc_init(NULL); + OK(); + } +} + +/** + * @brief 测试 2:out 信号注册 + 设值 + 延迟回调 + */ +static void test_out_signal(void) +{ + int yc_val = 100; + + TEST("dc_signal_out()"); + { + int r = dc_signal_out("RPC1:YC:1", "test yc", DATA_TYPE_S32, + &yc_val, "protocol"); + + if (DC_OK != r) + { + FAIL("register failed"); + + return; + } + + OK(); + } + + TEST("dc_signal_out() duplicate"); + { + int r = dc_signal_out("RPC1:YC:1", "dup", DATA_TYPE_S32, + &yc_val, "protocol"); + + if (DC_ERR_EXISTS != r) + { + FAIL("duplicate should return EXISTS"); + + return; + } + + OK(); + } + + TEST("dc_signal_out_link_with_callback()"); + { + void *p = NULL; + + g_out_changed = 0; + int r = dc_signal_out_link_with_callback("RPC1:YC:1", &p, + on_out_changed, "web"); + + if (DC_OK != r || p != &yc_val) + { + FAIL("link failed"); + + return; + } + + OK(); + } + + TEST("dc_set_out_signal_val() + dc_run_100ms()"); + { + g_out_changed = 0; + yc_val = 200; + int r = dc_set_out_signal_val("RPC1:YC:1", &yc_val, "protocol"); + + if (DC_OK != r) + { + FAIL("set val failed"); + + return; + } + + /* 回路阻断:protocol 自身不会被回调 */ + dc_run_100ms(); + + if (g_out_changed != 1) + { + char msg[64]; + + snprintf(msg, sizeof(msg), "cb count=%d, expected 1", g_out_changed); + FAIL(msg); + + return; + } + + OK(); + } +} + +/** + * @brief 测试 3:in 信号注册 + */ +static void test_in_signal(void) +{ + TEST("dc_signal_in()"); + { + void *p = NULL; + int r = dc_signal_in("RPC1:YC:1:in", "test in", "RPC1:YC:1", + &p, "protocol"); + + if (DC_OK != r || !p) + { + FAIL("in register failed"); + + return; + } + + OK(); + } + + TEST("dc_signal_in() bad link"); + { + void *p = NULL; + int r = dc_signal_in("RPC1:YC:99:in", "bad", "RPC1:YC:99", + &p, "protocol"); + + if (DC_ERR_NOTFOUND != r) + { + FAIL("bad link should return NOTFOUND"); + + return; + } + + OK(); + } +} + +/** + * @brief 测试 4:yk 信号 + 同步控制 + */ +static void test_yk_signal(void) +{ + uint8_t yk_val = 0; + + TEST("dc_signal_yk()"); + { + int r = dc_signal_yk("RPC1:YK:1", "test yk", DATA_TYPE_U8, + DC_CTRL_DIRECT, &yk_val, on_yk_changed, "web"); + + if (DC_OK != r) + { + FAIL("yk register failed"); + + return; + } + + OK(); + } + + TEST("dc_signal_yk_set_status() DIRECT"); + { + g_yk_changed = 0; + dc_ctrl_t ctrl = {}; + + ctrl.type = DC_CTRL_DIRECT; + ctrl.data_type = DATA_TYPE_U8; + + uint8_t v = 1; + int r = dc_signal_yk_set_status("RPC1:YK:1", DC_STEP_DIRECT, + &ctrl, &v, "protocol"); + + if (DC_OK != r || yk_val != 1) + { + FAIL("yk set failed"); + + return; + } + + if (g_yk_changed != 1) + { + FAIL("yk callback not triggered"); + + return; + } + + /* 回路阻断:web 自身 */ OK(); + } + + TEST("dc_signal_yk_set_status() SBO flow"); + { + g_yk_changed = 0; + dc_ctrl_t ctrl = {}; + + ctrl.type = DC_CTRL_SBO; + ctrl.data_type = DATA_TYPE_U8; + ctrl.p_data = malloc(1); + *(uint8_t *)ctrl.p_data = 2; + + uint8_t sel_val = 2; + int r = dc_signal_yk_set_status("RPC1:YK:1", DC_STEP_SELECT, + &ctrl, &sel_val, "web"); + + if (DC_OK != r) + { + FAIL("SBO SELECT failed"); + + free(ctrl.p_data); + + return; + } + + /* DIRECT with matching value */ + r = dc_signal_yk_set_status("RPC1:YK:1", DC_STEP_DIRECT, + &ctrl, &sel_val, "web"); + + if (DC_OK != r) + { + FAIL("SBO DIRECT failed"); + + free(ctrl.p_data); + + return; + } + + free(ctrl.p_data); + OK(); + } +} + +/** + * @brief 测试 5:ao 信号 + SBO 控制 + */ +static void test_ao_signal(void) +{ + float ao_val = 0.0f; + + TEST("dc_signal_ao()"); + { + int r = dc_signal_ao("RPC1:AO:1", "test ao", DATA_TYPE_F32, + DC_CTRL_SBO, &ao_val, on_ao_changed, "protocol"); + + if (DC_OK != r) + { + FAIL("ao register failed"); + + return; + } + + OK(); + } + + TEST("dc_signal_ao_set_val() SBO"); + { + g_ao_changed = 0; + dc_ctrl_t ctrl = {}; + + ctrl.type = DC_CTRL_SBO; + ctrl.data_type = DATA_TYPE_F32; + ctrl.p_data = malloc(4); + *(float *)ctrl.p_data = 3.14f; + + float v = 3.14f; + int r = dc_signal_ao_set_val("RPC1:AO:1", DC_STEP_SELECT, + &ctrl, &v, "web"); + + if (DC_OK != r) + { + FAIL("ao SELECT failed"); + + free(ctrl.p_data); + + return; + } + + r = dc_signal_ao_set_val("RPC1:AO:1", DC_STEP_DIRECT, + &ctrl, &v, "web"); + + if (DC_OK != r) + { + FAIL("ao DIRECT failed"); + + free(ctrl.p_data); + + return; + } + + free(ctrl.p_data); + + if (g_ao_changed < 2) + { + FAIL("ao callback not triggered"); + + return; + } + + /* ctrl->step should be reset to READY */ + if (ctrl.step != DC_STEP_READY) + { + FAIL("ctrl step not reset to READY"); + + return; + } + + OK(); + } + + TEST("dc_signal_ao_set_val_without_check()"); + { + g_ao_changed = 0; + float v = 2.71f; + int r = dc_signal_ao_set_val_without_check("RPC1:AO:1", DATA_TYPE_F32, + &v, "protocol"); + + if (DC_OK != r) + { + FAIL("ao set without check failed"); + + return; + } + + OK(); + } +} + +/** + * @brief 测试 6:param 多区定值 + */ +static void test_param_signal(void) +{ + uint32_t p1 = 10; + uint32_t p2 = 20; + void *parr[2] = {&p1, &p2}; + + TEST("dc_signal_param()"); + { + int r = dc_signal_param("RPC1:PARAM:1", "test param", DATA_TYPE_U32, + DC_CTRL_SBO, parr, 2, on_param_changed, "web"); + + if (DC_OK != r) + { + FAIL("param register failed"); + + return; + } + + OK(); + } + + TEST("dc_signal_param_set_val() zone 0"); + { + g_param_changed = 0; + dc_ctrl_t ctrl = {}; + + ctrl.type = DC_CTRL_SBO; + ctrl.data_type = DATA_TYPE_U32; + ctrl.p_data = malloc(4); + *(uint32_t *)ctrl.p_data = 55; + + uint32_t v = 55; + int r = dc_signal_param_set_val("RPC1:PARAM:1", DC_STEP_SELECT, + &ctrl, 0, &v, "web"); + + if (DC_OK != r) + { + FAIL("param SELECT failed"); + + free(ctrl.p_data); + + return; + } + + r = dc_signal_param_set_val("RPC1:PARAM:1", DC_STEP_DIRECT, + &ctrl, 0, &v, "web"); + + if (DC_OK != r) + { + FAIL("param DIRECT failed"); + + free(ctrl.p_data); + + return; + } + + free(ctrl.p_data); + + if (p1 != 55) + { + FAIL("zone0 not updated"); + + return; + } + + OK(); + } +} + +/** + * @brief 测试 7:信号查询 + */ +static void test_query(void) +{ + TEST("dc_get_out_signal_info()"); + { + char desc[64] = {0}; + uint8_t dt = 0; + void *p = NULL; + int r = dc_get_out_signal_info("RPC1:YC:1", desc, 64, &dt, &p); + + if (DC_OK != r || dt != DATA_TYPE_S32) + { + FAIL("query out failed"); + + return; + } + + OK(); + } + + TEST("dc_get_signal_count()"); + { + int n = dc_get_signal_count("out"); + + if (n < 1) + { + FAIL("out count should be >= 1"); + + return; + } + + n = dc_get_signal_count("param"); + + if (n < 1) + { + FAIL("param count should be >= 1"); + + return; + } + + OK(); + } + + TEST("dc_get_signal_info_by_id()"); + { + char saddr[64] = {0}; + char desc[64] = {0}; + char dtstr[32] = {0}; + uint8_t ct = 0; + int r = dc_get_signal_info_by_id("out", 0, saddr, 64, desc, 64, + dtstr, 32, &ct, NULL, 0); + + if (DC_OK != r) + { + FAIL("get by id failed"); + + return; + } + + OK(); + } + + TEST("dc_get_signal_val()"); + { + uint32_t v = 42; + char buf[32] = {0}; + int r = dc_get_signal_val(&v, DATA_TYPE_U32, buf, 32); + + if (DC_OK != r || 0 != strcmp(buf, "42")) + { + FAIL("val to str failed"); + + return; + } + + OK(); + } +} + +/** + * @brief 测试 8:类型工具 + */ +static void test_type_util(void) +{ + TEST("dc_get_type_name()"); + { + const char *n = dc_get_type_name(DATA_TYPE_F32); + + if (0 != strcmp(n, "float")) + { + FAIL("type name mismatch"); + + return; + } + + OK(); + } + + TEST("dc_get_type_id_by_name()"); + { + uint8_t id = dc_get_type_id_by_name("uint32_t"); + + if (DATA_TYPE_U32 != id) + { + FAIL("type id mismatch"); + + return; + } + + OK(); + } + + TEST("dc_get_type_len()"); + { + if (4 != dc_get_type_len(DATA_TYPE_F32)) + { + FAIL("float len should be 4"); + + return; + } + + if (8 != dc_get_type_len(DATA_TYPE_D64)) + { + FAIL("double len should be 8"); + + return; + } + + OK(); + } + + TEST("dc_create_data + dc_destroy_data"); + { + void *p = dc_create_data(DATA_TYPE_S32); + + if (!p) + { + FAIL("create data failed"); + + return; + } + + dc_destroy_data(p, DATA_TYPE_S32); + OK(); + } + + TEST("dc_copy_val + dc_compare_val"); + { + int a = 10; + int b = 0; + + dc_copy_val(&b, &a, DATA_TYPE_S32); + + if (b != 10) + { + FAIL("copy val failed"); + + return; + } + + if (0 != dc_compare_val(&a, &b, DATA_TYPE_S32)) + { + FAIL("compare equal failed"); + + return; + } + + b = 20; + + if (-1 != dc_compare_val(&a, &b, DATA_TYPE_S32)) + { + FAIL("compare unequal failed"); + + return; + } + + OK(); + } + + TEST("dc_set_val_from_str()"); + { + float f = 0.0f; + int r = dc_set_val_from_str(&f, DATA_TYPE_F32, "3.14"); + + if (DC_OK != r || f < 3.13f || f > 3.15f) + { + FAIL("set from str failed"); + + return; + } + + OK(); + } + + TEST("dc_set_val_from_str() IP"); + { + uint8_t ip[4] = {0}; + int r = dc_set_val_from_str(ip, DATA_TYPE_IP, "192.168.1.1"); + + if (DC_OK != r || ip[0] != 192 || ip[1] != 168 || ip[2] != 1 || ip[3] != 1) + { + FAIL("IP parse failed"); + + return; + } + + OK(); + } +} + +/** + * @brief 测试 9:事件队列 + */ +static void test_event_queue(void) +{ + TEST("dc_event_queue_push + register"); + { + int r = dc_event_register_queue_pop(on_soe_pop); + + if (DC_OK != r) + { + FAIL("register pop failed"); + + return; + } + + g_soe_received = 0; + + int dummy = 99; + r = dc_event_queue_push(&dummy); + + if (DC_OK != r) + { + FAIL("event push failed"); + + return; + } + + /* 消耗队列 */ + dc_run_100ms(); + + if (g_soe_received < 1) + { + FAIL("event not consumed"); + + return; + } + + OK(); + } + + TEST("dc_disturb_dd_queue_push"); + { + int dummy = 1; + int r = dc_disturb_dd_queue_push(&dummy); + + if (DC_OK != r) + { + FAIL("disturb push failed"); + + return; + } + + OK(); + } + + TEST("dc_fault_queue_push"); + { + int dummy = 1; + int r = dc_fault_queue_push(&dummy); + + if (DC_OK != r) + { + FAIL("fault push failed"); + + return; + } + + OK(); + } +} + +/** + * @brief 测试 10:异步管理 + */ +static void test_async_mgmt(void) +{ + TEST("dc_async_pending_count()"); + { + int n = dc_async_pending_count(); + + if (n < 0) + { + FAIL("pending count failed"); + + return; + } + + OK(); + } + + TEST("dc_async_set_default_timeout()"); + { + dc_async_set_default_timeout(5000); + OK(); + } + + TEST("dc_param_cfg_changed()"); + { + int r = dc_param_cfg_changed(); + + if (r < 0) + { + FAIL("cfg changed failed"); + + return; + } + + OK(); + } +} + +/* ===== 入口 ===== */ + +int main(void) +{ + printf("=== test_datacenter ===\n\n"); + + /* 初始化 */ + dc_init(NULL); + + /* 信号注册 */ + test_out_signal(); + test_in_signal(); + test_yk_signal(); + test_ao_signal(); + test_param_signal(); + + /* 查询 */ + test_query(); + + /* 工具 */ + test_type_util(); + + /* 事件队列 */ + test_event_queue(); + + /* 异步管理 */ + test_async_mgmt(); + + /* 清理 */ + dc_cleanup(); + + /* 生命周期 */ + dc_init(NULL); + test_lifecycle(); + dc_cleanup(); + + /* 结果 */ + printf("\n=== result: %d pass, %d fail ===\n", g_pass, g_fail); + + return (g_fail > 0) ? 1 : 0; +}