diff --git a/COMPLETION.md b/COMPLETION.md index dbec397..c36bccf 100644 --- a/COMPLETION.md +++ b/COMPLETION.md @@ -37,7 +37,6 @@ | 通讯抽象层测试 | `test_comm.cpp`(17项全通过) | ✅ | | Modbus 协议栈 | `libmodbus`(开源库 v3.1.12,导入+日志适配) | ✅ | | IEC 60870-5-104 | `lib60870`(原工程自研协议栈,导入) | ✅ | -| IEC 点表解析 | `libiec` | ⬜ | | Modbus 主站管理 | `libmodbus_m`(原工程自研模块,导入+适配) | ✅ | | 通讯通道解码 | `libcom_decode` | ⬜ | | Web 管理服务 | `libweb_server`(3 文件拆分,单线程,EV_STOP 支持) | ✅ | diff --git a/PLAN.md b/PLAN.md index e7bdbed..905a750 100644 --- a/PLAN.md +++ b/PLAN.md @@ -198,6 +198,26 @@ int my_module_do_something(uint32_t id, const char *data); - 函数行数控制在 80 行以内 - 复杂逻辑拆分为独立静态函数 - 错误路径必须打 LOG_E +- **if/for/while 等关键字后必须有空格**,如 `if (x)` 而非 `if(x)` +- **条件判断与分支必须分行**,禁止同行写法: + - ❌ `if (x) do_thing();` + - ❌ `if (x) { do_thing(); }` + - ❌ `if (x) {`(大括号不许同行) + - ✅ 统一为: + ```c + if (x) + { + do_thing(); + } + ``` + - 对 `for` / `while` / `else` / `else if` 同样适用,函数定义也须换行: + ```c + static void foo(void) + { + ... + } + ``` +- **多语句禁止同列**,如 `memset(&l, 0, sizeof(l)); l.dst_pin = -1;` 应各占一行 --- @@ -218,64 +238,64 @@ int my_module_do_something(uint32_t id, const char *data); | 序号 | 任务 | 模块 | 产出 | 状态 | |------|------|------|------|------| -| 0.1 | 搭建构建系统 | — | `release/linux.mk`, `release/makefile`, `release/build.sh` | ⬜ | -| 0.2 | 创建 `myBase.h` 基础定义 | — | `release/inc/myBase.h`(宏、类型、大小端转换) | ⬜ | -| 0.3 | 实现侵入式链表 | `liblist` | `src/public/liblist/inc/list.h` | ⬜ | -| 0.4 | 实现日志系统 | `liblog` | `src/public/liblog/inc/myLog.h`, `src/myLog.c` | ⬜ | -| 0.5 | 实现基础工具函数 | `libfunc` | `src/public/libfunc/inc/myFunc.h`, `src/myFunc.c` | ⬜ | -| 0.6 | 创建全部预留模块的目录+stub | 所有 P3 | 7 个预留模块的目录骨架 + 返回 0 的 stub | ⬜ | -| 0.7 | 链表 + 日志单元测试 | — | `src/test/test_list.c`, `test_log.c` | ⬜ | -| 0.8 | **编译验证 + 代码审查** | — | 全量编译通过、测试通过 | ⬜ | +| 0.1 | 搭建构建系统 | — | `release/linux.mk`, `release/makefile`, `release/build.sh` | ✅ | +| 0.2 | 创建 `myBase.h` 基础定义 | — | `release/inc/myBase.h`(宏、类型、大小端转换) | ✅ | +| 0.3 | 实现侵入式链表 | `liblist` | `src/public/liblist/inc/list.h` | ✅ | +| 0.4 | 实现日志系统 | `liblog` | `src/public/liblog/inc/myLog.h`, `src/myLog.c` | ✅ | +| 0.5 | 实现基础工具函数 | `libfunc` | `src/public/libfunc/inc/myFunc.h`, `src/myFunc.c` | ✅ | +| 0.6 | 创建全部预留模块的目录+stub | 所有 P3 | 7 个预留模块的目录骨架 + 返回 0 的 stub | ✅ | +| 0.7 | 链表 + 日志单元测试 | — | `src/test/test_list.c`, `test_log.c` | ✅ | +| 0.8 | **编译验证 + 代码审查** | — | 全量编译通过、测试通过 | ✅ | ### 阶段 1:数据基础设施 | 序号 | 任务 | 模块 | 产出 | 状态 | |------|------|------|------|------| -| 1.1 | 实现 MD5 摘要算法 | `libmd5` | `src/public/libmd5/` | ⬜ | -| 1.2 | 实现 xxHash 快速哈希 | `libmy_xxhash` | `src/public/libmy_xxhash/` | ⬜ | -| 1.3 | 实现 cJSON 解析 | `libcJSON` | `src/public/libcJSON/`(从 RTU 裁剪) | ⬜ | -| 1.4 | 设计信号数据模型 | — | `dc_signal_t` 等结构体设计文档 | ⬜ | -| 1.5 | 实现数据中心 | `libdatacenter` | C 接口 CRUD,C++ 内部用 std::map/vector | ⬜ | -| 1.6 | 实现任务调度 | `libtask` | C 接口,C++ 内部 std::thread + std::chrono | ⬜ | -| 1.7 | 编写单元测试 | — | `test_cjson.c`, `test_datacenter.cpp` | ⬜ | -| 1.8 | **编译验证 + 代码审查** | — | 全量编译通过、测试通过 | ⬜ | +| 1.1 | 实现 MD5 摘要算法 | `libmd5` | `src/public/libmd5/` | ✅ | +| 1.2 | 实现 xxHash 快速哈希 | `libmy_xxhash` | `src/public/libmy_xxhash/` | ✅ | +| 1.3 | 实现 cJSON 解析 | `libcJSON` | `src/public/libcJSON/`(从 RTU 裁剪) | ✅ | +| 1.4 | 设计信号数据模型 | — | `dc_signal_t` 等结构体设计文档 | ✅ | +| 1.5 | 实现数据中心 | `libdatacenter` | C 接口 CRUD,C++ 内部用 std::map/vector | ✅ | +| 1.6 | 实现任务调度 | `libtask` | C 接口,C++ 内部 std::thread + std::chrono | ✅ | +| 1.7 | 编写单元测试 | — | `test_cjson.c`, `test_datacenter.cpp` | ✅ | +| 1.8 | **编译验证 + 代码审查** | — | 全量编译通过、测试通过 | ✅ | ### 阶段 2:通讯与协议 | 序号 | 任务 | 模块 | 产出 | 状态 | |------|------|------|------|------| -| 2.1 | 实现通讯抽象层 | `libcomm` | 串口/TCP 统一 C 接口,内部 C++ 封装 | ⬜ | -| 2.2 | 实现命令处理框架 | `libcmd` | C 接口,内部 std::unordered_map + std::queue | ⬜ | -| 2.3 | 实现 XML 解析 | `libxml` | C 接口(myXml.h),内部 tinyxml2 C++ 实现 | ⬜ | -| 2.4 | 实现 Modbus 协议栈 | `libmodbus` | RTU/TCP 从站,纯C | ⬜ | -| 2.5 | 实现 IEC 60870-5-104 | `lib60870` | 总召、遥测、遥信、遥控,纯C | ⬜ | -| 2.6 | 实现 IEC 点表解析 | `libiec` | `src/system/libiec/` | ⬜ | -| 2.7 | 实现 Modbus 主站管理 | `libmodbus_m` | 轮询队列、采集调度 | ⬜ | +| 2.1 | 实现通讯抽象层 | `libcomm` | 串口/TCP 统一 C 接口,内部 C++ 封装 | ✅ | +| 2.2 | 实现命令处理框架 | `libcmd` | C 接口,内部 std::unordered_map + std::queue | ✅ | +| 2.3 | 实现 XML 解析 | `libxml` | C 接口(myXml.h),内部 tinyxml2 C++ 实现 | ✅ | +| 2.4 | 实现 Modbus 协议栈 | `libmodbus` | RTU/TCP 从站,纯C | ✅ | +| 2.5 | 实现 IEC 60870-5-104 | `lib60870` | 总召、遥测、遥信、遥控,纯C | ✅ | +| 2.6 | 实现 IEC 点表解析 | `libiec` | `src/system/libiec/` | ✅ | +| 2.7 | 实现 Modbus 主站管理 | `libmodbus_m` | 轮询队列、采集调度 | ✅ | | 2.8 | 编写单元测试 | — | `test_modbus.c`, `test_60870.c` | ⬜ | -| 2.9 | **编译验证 + 代码审查** | — | 全量编译通过、测试通过 | ⬜ | +| 2.9 | **编译验证 + 代码审查** | — | 全量编译通过、测试通过 | ✅ | ### 阶段 3:系统集成 | 序号 | 任务 | 模块 | 产出 | 状态 | |------|------|------|------|------| | 3.1 | 实现通讯通道解码 | `libcom_decode` | 多通道帧解析、协议识别分发 | ⬜ | -| 3.2 | 实现 Web 管理服务 | `libweb_server` | REST API + WebSocket | ⬜ | -| 3.3 | 实现主程序入口 | `RTU` | 模块初始化、主循环 | ⬜ | +| 3.2 | 实现 Web 管理服务 | `libweb_server` | REST API + WebSocket | ✅ | +| 3.3 | 实现主程序入口 | `RTU` | 模块初始化、主循环 | ✅ | | 3.4 | 端到端集成测试 | — | 多模块联调 | ⬜ | -| 3.5 | **编译验证 + 代码审查** | — | x86 + ARM 交叉编译全通过 | ⬜ | +| 3.5 | **编译验证 + 代码审查** | — | x86 + ARM 交叉编译全通过 | ✅ | | 3.6 | 编写 README 文档 | — | 编译指南、架构图、使用说明 | ⬜ | ### 阶段 4(后续扩展):预留模块实现 | 序号 | 任务 | 模块 | 状态 | |------|------|------|------| -| 4.1 | ICP67 专有协议 | `libicp67` | ⬜ | +| 4.1 | ICP67 专有协议 | `libicp67` | ✅ | | 4.2 | IEC 61850 MMS 主站 | `libmms_m` + `libiec61850m` | ⬜ | | 4.3 | IEC 61850 MMS 从站 | `libmms_s` + `libiec61850s` | ⬜ | -| 4.4 | 嵌入式 HTTP 服务 | `libmongoose` | ⬜ | +| 4.4 | 嵌入式 HTTP 服务 | `libmongoose` | ✅ | | 4.5 | MQTT 客户端 | `libmy_mosquitto` | ⬜ | | 4.6 | PLC 逻辑引擎 | `libplc` | ⬜ | -| 4.7 | 内部自定义协议 | `libself_ptl` | ⬜ | +| 4.7 | 内部自定义协议 | `libself_ptl` | ✅ | --- diff --git a/docs/README.md b/docs/README.md index a51117c..bf06cb7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -30,6 +30,7 @@ |------|------|------|------| | libmodbus | [API-libmodbus.md](protocol/libmodbus/API-libmodbus.md) | C (开源库) | Modbus 协议栈 v3.1.12 | | lib60870 | [API-lib60870.md](protocol/lib60870/API-lib60870.md) | C (自研) | IEC 60870-5-101/103/104 协议栈 | +| libicp67 | [API-libicp67.md](protocol/libicp67/API-libicp67.md) | 纯 C | ICP67 自定义协议(帧解析+编解码+核心逻辑) | | libmongoose | [API-libmongoose.md](protocol/libmongoose/API-libmongoose.md) | C (MIT 开源) | HTTP/WebSocket 嵌入式网络库 v7.22 | ### 系统模块 (system/) @@ -37,7 +38,10 @@ | 模块 | 文档 | 语言 | 说明 | |------|------|------|------| | RTU | [API-RTU.md](system/RTU/API-RTU.md) | C + C++ | 主程序入口、X-Macro 框架、模块生命周期 | +| libiec | [API-libiec.md](system/libiec/API-libiec.md) | C 接口+C++ 内部 | IEC-104 服务端(26回调+环形缓冲+数据中心桥接) | +| libcom_router | [API-libcom_router.md](system/libcom_router/API-libcom_router.md) | C++ | 通信路由中枢(转发规则+协议绑定 dispatch) | | libmodbus_m | [API-libmodbus_m.md](system/libmodbus_m/API-libmodbus_m.md) | C++ | Modbus 主站管理(通道/点表/轮询/写入) | +| libself_ptl | [API-libself_ptl.md](system/libself_ptl/API-libself_ptl.md) | C++ | 内部自定义协议应用层(多FTU帧组装+文件操作) | | libweb_server | [API-libweb_server.md](system/libweb_server/API-libweb_server.md) | C++ | Web 管理服务(HTTP + WebSocket 实时推送) | ``` diff --git a/docs/protocol/libicp67/API-libicp67.md b/docs/protocol/libicp67/API-libicp67.md new file mode 100644 index 0000000..80906bf --- /dev/null +++ b/docs/protocol/libicp67/API-libicp67.md @@ -0,0 +1,271 @@ +# libicp67 — ICP67 自定义协议库 API 参考 + +## 模块概述 + +**位置**: `src/protocol/libicp67/` + +**定位**: ICP67 二进制协议引擎 — 负责帧定界、编解码、重传管理。纯 C 实现,extern "C" 接口。 + +**依赖**: `liblog` (日志), POSIX `sem_t` (信号量) + +**被依赖**: `libself_ptl` (自协议应用层) + +## 协议帧格式 + +``` +| 0x67 | len(2B) | 0x67 | src | dst | dir | ti | cot | info_addr(2B) | dev_addr(4B) | data... | CRC(1B) | 0x16 | +| 帧头1 | 长度 | 帧头2 | 源 | 目的 | 方向 |类型| 原因 | 信息体地址 | 设备地址 | 数据体 | 校验和 | 帧尾 | +``` + +- **帧头**: 0x67 0x67 (双字节定界) +- **校验**: 8-bit 累加和 (CRC8) +- **帧尾**: 0x16 +- **设备地址**: src/dst 取值 0=维护软件, 1=主板, 2=主板, 3=采样板, 4=LCD + +## 架构 + +``` +libself_ptl (应用层) + │ + ├─ icp67_init() 初始化 + ├─ icp67_set_*_cb() 注册 17 个回调 + │ + ▼ +libicp67 (协议引擎) + ├─ icp67_search_frame() 帧定界 + ├─ icp67_decode() 解码分发 + ├─ icp67_timer_handler() 重传管理 + ├─ Sender 函数 (13个) 组帧发送 + └─ 16 种 TI 解码函数 +``` + +## 文件清单 + +| 文件 | 行数 | 说明 | +|---|---|---| +| `release/inc/myIcp67.h` | 331 | 公共契约: 帧结构、回调类型、`stru_icp67`、常量宏 | +| `src/protocol/libicp67/inc/icp67.h` | ~500 | 内部头: TI/COT枚举、ASDU结构体、内部API声明 | +| `src/protocol/libicp67/src/icp67_frame.c` | 126 | 帧定界、CRC校验、帧构造、调试打印 | +| `src/protocol/libicp67/src/icp67_decode.c` | 1024 | 16种TI解码+文件/ZIP子命令分发 | +| `src/protocol/libicp67/src/icp67_core.c` | 645 | 初始化、回调注册、重传管理、Sender函数、定时器 | + +## 对外 API + +### 初始化 + +```c +void icp67_init(stru_icp67 *p_icp67, icp67_send_cb send_cb, void *arg); +``` + +初始化协议实例,绑定发送回调和用户参数。内部自动绑定: +- `search_frame_cb` → `icp67_search_frame()` +- `decode_cb` → `icp67_decode()` +- `timer_handler_cb` → `icp67_timer_handler()` +- 创建重传信号量 `rtx_sem` +- 初始化 13 个 Sender 回调为内部函数 +- 设置超时参数 `tm_out = 1000ms / 10ms = 100 tick` + +```c +int icp67_set_ao_cfg_md5(stru_icp67 *p_icp67, uint8_t *p_md5); +``` + +设置 AO 参数配置的 MD5 校验值 (16字节),用于后续 TI_1/TI_2 帧的 MD5 验证。 + +### 回调注册 (17 个 setter) + +**pop_out 类 (3 个)** — 解码后回调注册: +| 函数 | 注册回调 | 触发场景 | +|---|---|---| +| `icp67_set_ao_pop_out_cb` | `ao_pop_out_cb` | TI_1 参数读取响应 | +| `icp67_set_iec_point_tbl_pop_out_cb` | `iec_point_tbl_pop_out_cb` | TI_3 点表读取响应 | +| `icp67_set_self_check_pop_out_cb` | `self_check_pop_out_cb` | TI_6 自检响应 | + +**文件操作类 (6 个):** +| 函数 | 注册回调 | 触发场景 | +|---|---|---| +| `icp67_set_dir_pop_out_cb` | `dir_pop_out_cb` | TI_11_2 目录响应 | +| `icp67_set_file_read_act_confirm_cb` | `file_read_act_confirm_cb` | TI_11_4 读文件激活响应 | +| `icp67_set_file_read_cb` | `file_read_cb` | TI_11_5 读文件内容 | +| `icp67_set_file_write_act_confirm_cb` | `file_write_act_confirm_cb` | TI_11_8 写文件激活响应 | +| `icp67_set_file_write_confirm_cb` | `file_write_confirm_cb` | TI_11_10 写文件确认 | +| `icp67_set_file_write_end_cb` | `file_write_end_cb` | TI_11_10 写文件结束 | + +**ZIP 类 (2 个):** +| 函数 | 注册回调 | 触发场景 | +|---|---|---| +| `icp67_set_zip_dir_pop_out_cb` | `zip_dir_pop_out_cb` | TI_30_1 录波目录 | +| `icp67_set_zip_file_pop_out_cb` | `zip_file_pop_out_cb` | TI_30_3 录波文件内容 | + +**数据传输类 (6 个):** +| 函数 | 注册回调 | 触发场景 | +|---|---|---| +| `icp67_set_mx_trans_cb` | `mx_trans_cb` | TI_200 遥测传输 | +| `icp67_set_st_trans_cb` | `st_trans_cb` | TI_201 遥信传输 | +| `icp67_set_dd_trans_cb` | `dd_trans_cb` | TI_202 电度传输 | +| `icp67_set_soe_trans_cb` | `soe_trans_cb` | TI_204 SOE 事件 | +| `icp67_set_fault_trans_cb` | `fault_trans_cb` | TI_205 故障事件 | +| `icp67_set_mx_change_cb` | `mx_change_cb` | TI_207 遥测扰动 | + +### 帧操作 (内部 API) + +```c +int icp67_search_frame(uint8_t *p_data, uint16_t len, uint16_t *p_pos, uint16_t *p_valid_len); +int icp67_decode(stru_icp67 *p_icp67, uint8_t *p_data, uint16_t len); +void icp67_timer_handler(stru_icp67 *p_icp67); +void icp67_make_head(uint8_t dst, uint8_t ti, uint8_t cot, uint16_t inf, uint8_t *p_tx, uint16_t *tx_len); +void icp67_make_tail(uint8_t *p_tx, uint16_t *tx_len); +void icp67_show_frame(uint8_t *p_data, uint16_t len); +``` + +### 重传管理 (线程安全) + +```c +void icp67_rtx_flag_set(stru_icp67 *p_icp67, uint8_t flag, uint8_t resend_cnt, uint32_t tm_cnt); +void icp67_rtx_flag_get(stru_icp67 *p_icp67, uint8_t *flag, uint8_t *resend_cnt, uint32_t *tm_cnt); +void icp67_rtx_resend_inc(stru_icp67 *p_icp67); +void icp67_rtx_tm_inc(stru_icp67 *p_icp67); +``` + +所有读写操作通过 `rtx_sem` 信号量保护。 + +## 核心数据结构 + +### stru_icp67 — 协议实例 + +```c +typedef struct _icp67 { + void *arg; // 用户数据 (interface ID) + icp67_send_cb send_cb; // 发送回调 (→物理层) + icp67_search_frame_cb search_frame_cb; // 帧搜索 + icp67_decode_cb decode_cb; // 解码 + icp67_timer_handler_cb timer_handler_cb; // 定时器 + stru_genneral_method method; // 回调表 (实例化) + uint8_t md5[16]; // AO参数MD5 + uint8_t tx[2048]; // 发送缓冲区 + uint8_t resend_tx[2048]; // 重发备份 + uint8_t rtx_flag; // 重传标志 + sem_t rtx_sem; // 重传信号量 + uint8_t resend_cnt; // 重发计数 + uint32_t tm_out; // 超时阈值 + uint32_t tm_cnt; // 当前tick +} icp67; +``` + +### stru_genneral_method — 回调表 (29 个函数指针) + +- **Sender 组** (13个): 方向 self_ptl → icp67 → 物理层 + - ao_get, ao_set, iec_point_tbl_get, time_set, self_check_get + - upgrade_start, dir_read, file_read_act, file_write_act + - file_write, file_read_confirm, file_write_end_confirm + +- **Receiver 组** (16个): 方向 物理层 → icp67 → self_ptl + - ao_pop_out, iec_point_tbl_pop_out, self_check_pop_out + - dir_pop_out, file_read_act_confirm, file_read + - file_write_act_confirm, file_write_confirm, file_write_end + - zip_dir_pop_out, zip_file_pop_out + - mx_trans, st_trans, dd_trans, soe_trans, fault_trans, mx_change + +## TI 消息类型清单 + +| TI | 名称 | COT | 方向 | 说明 | +|:--:|---|---|---|---| +| 1 | 读独立编码参数 | 5→7 | 请求→响应 | 带 MD5 校验的参数读取 | +| 2 | 写独立编码参数 | 6→7 | 激活→确认 | 参数写入,含 MD5 | +| 3 | 读组合编码参数 | 5→7 | 请求→响应 | 点表/校准数据,支持分帧 | +| 4 | 写组合编码参数 | - | - | 空桩 (未使用) | +| 5 | 对时 | 5→7 | 请求→响应 | 终端时间同步 | +| 6 | 自检读取 | 5→7 | 请求→响应 | 终端状态/版本号 | +| 9 | 升级 | 5→7 | 请求→响应 | 固件升级 | +| 11 | 文件操作 | 5→7 | 请求→响应 | 目录/文件读写, 9 种子命令 | +| 30 | 录波文件 | 5 | 上行 | ZIP 波形目录/文件传输 | +| 200 | 遥测传输 | 6 | 上行 | 采样板→主板 MX | +| 201 | 遥信传输 | 6 | 上行 | 采样板→主板 ST | +| 202 | 计量传输 | 6 | 上行 | 采样板→主板 DD | +| 203 | 遥控交互 | - | - | 空桩 (未使用) | +| 204 | SOE 传输 | 3 | 上行 | 事件顺序记录 | +| 205 | 故障传输 | 3 | 上行 | 故障事件 (含 ST+MX) | +| 207 | 扰动传输 | 3 | 上行 | 遥测扰动数据 | + +## 与原工程的差异 + +| 维度 | 原 RTU (icp67.cpp) | 重构后 (纯C 4文件) | +|---|---|---| +| 回调表 | 全局 `g_genneral_method` | 实例化 `p_icp67->method` | +| TI 映射 | C++ `std::map` | C 数组 + 线性查表 | +| 故障解码 | `std::vector` | 固定数组 `[MAX_FAULT_*_INFO]` | +| 文件格式 | `icp67.cpp` (1429行) + `general_method.cpp` (88行) | 4 文件: frame(126) + decode(1024) + core(645) + header(~830) | +| 语言 | C++ | 纯 C (extern "C" 接口) | +| 常量 | 魔数 | 命名 `#define` (ICP67_TX_BUF_SIZE 等) | +| 空桩 | TI_4/TI_203 空 | LOG_E + RX_FLAG 设置 | + +## 完成状态 + +### ✅ 已完成 + +- [x] 帧定界/校验 (search_frame, CRC) +- [x] 16 种 TI 解码 (含文件/ZIP 子命令) +- [x] 13 个 Sender 函数 (组帧发送) +- [x] 17 个回调 setter (实例化注册) +- [x] 重传管理 (4 函数, sem 线程安全) +- [x] 定时器处理 (10ms tick, 5 次重试) +- [x] TI_4/TI_203 空桩填充 +- [x] 多实例隔离 (回调表实例化) +- [x] 魔数 → 命名常量 +- [x] C 数组查表替代 std::map +- [x] 单元测试 (21 项全通过) + +### ⬜ 未完成 + +- [ ] TI_203 遥控交互实现 (FTU 固件不支持, 暂缓) + +> 以下两项因 FTU 固件不可改动, 已删除: +> ~~CRC 升级 (需 FTU 同步改帧格式)~~ +> ~~帧序号/去重 (需 FTU 同步改帧格式)~~ + +## 新增功能 (2026-07-09) + +### 可配置超时/重试 + +```c +// 旧 +void icp67_init(stru_icp67 *p, icp67_send_cb cb, void *arg); + +// 新: 传 0 使用默认值 (1000ms, 5次) +void icp67_init(stru_icp67 *p, icp67_send_cb cb, void *arg, + uint32_t tm_out_ms, uint8_t resend_max); + +// 示例 +icp67_init(&icp, send_cb, &iface, 2000, 3); // 串口慢速: 2s超时, 重试3次 +icp67_init(&icp, send_cb, &iface, 0, 0); // 使用默认值 +``` + +`stru_icp67` 新增字段: `resend_max`, `tm_out_ms`。 + +### 零拷贝环形缓冲区搜索 + +```c +int icp67_search_frame_in_ring(const uint8_t *buf, uint16_t size, + uint16_t rptr, uint16_t cnt, + uint16_t *p_pos, uint16_t *p_valid_len); +``` + +直接从环形缓冲区读指针扫描帧, 省去 `get_rx_data()` 的 2048 字节 `memcpy`。 +`self_ptl` 已接入, 仅 IEC-104 检测保留一次全量拷贝。 + +## 测试覆盖 + +| 测试项 | 覆盖内容 | 状态 | +|---|---|---| +| T1 帧构造 | make_head + make_tail | ✅ | +| T2 帧搜索 | 正常帧、无效帧头、NULL参数 | ✅ | +| T3 CRC | 基本累加、溢出回绕 | ✅ | +| T4 初始化 | 绑定回调、信号量、超时参数 | ✅ | +| T5 重传管理 | set/get、resend_inc | ✅ | +| T6 回调注册 | 17 个 setter 全路径 | ✅ | +| T7 解码无效 | NULL参数 | ✅ | +| T8 定时器 | NULL、空闲态 | ✅ | +| T9 MD5 | 正常设置、NULL参数 | ✅ | +| T10 未知 TI | 0xFF 帧 | ✅ | +| T11 Sender | self_check_get、iec_point_tbl_get、NULL安全 | ✅ | +| T12 解码 TI | TI_1 MD5 匹配、TI_4 空桩 | ✅ | +| T13 多实例 | 实例间回调隔离 | ✅ | diff --git a/docs/system/libcom_router/API-libcom_router.md b/docs/system/libcom_router/API-libcom_router.md new file mode 100644 index 0000000..8eb0169 --- /dev/null +++ b/docs/system/libcom_router/API-libcom_router.md @@ -0,0 +1,177 @@ +# libcom_router — 通信路由中枢 API 参考 + +## 模块概述 + +**位置**: `src/system/libcom_router/` + +**定位**: RTU 通信路由中枢。负责: +- 转发通道管理 (TCP/串口 → FTU 串口透传) +- FTU 串口接收分路 (按帧内容区分维护PC/self_ptl) +- 跨模块回调注册 (与 self_ptl 解耦) + +**依赖**: `libcomm`, `libicp67`(帧头检测), `liblog`, `libdatacenter` + +**不依赖**: self_ptl, libiec (通过 `app_wiring()` 运行时解耦) + +## 架构 + +``` + libcom_router + ┌──────────┼──────────┐ + │ │ + 转发通道(TCP) FTU 串口 + │ │ + Step1: 直接透传 ────→ ┌── Step2: 分路 ──┐ + │ │ + ICP67 dev=0 IEC 0x68 ICP67 dev=1 + │ │ │ + 回传转发源 回传转发源 self_ptl + (维护软件) (维护软件) (RTU自身) +``` + +**注意**: RTU 自身的 IEC-104 不走 com_router,由 libiec 直管 libcomm。 + +## 文件清单 + +| 文件 | 行数 | 说明 | +|---|---|---| +| `inc/com_router.h` | 98 | 通道配置、转发规则、回调注册 API | +| `src/com_router.cpp` | 385 | 通道管理、配置加载、状态回调、主循环 | +| `src/com_router_dispatch.cpp` | 191 | 转发透传 + FTU 串口分路 | + +## 对外 API + +### 配置加载 + +```c +int com_router_config_load(const char *json_path); +``` + +从 JSON 加载通道配置和转发规则。未调用时使用硬编码默认配置。 + +### 回调注册 (模块解耦) + +```c +typedef void (*com_router_dispatch_cb)(uint32_t interface, const unsigned char *data, int len); + +int com_router_register_dispatch(const char *bind_app, com_router_dispatch_cb cb); +``` + +协议模块通过此 API 接收 FTU 串口的 ICP67 dev=1 帧。 + +**调用者**: `app_sys.cpp` → `app_wiring()` + +**示例**: +```c +com_router_register_dispatch("self_ptl", self_ptl_router_wrapper); +``` + +### 查询 API + +```c +uint32_t com_router_interface_get(const char *key); // key → interface +int com_router_fd_get(const char *key); // key → socket_fd +int com_router_id_get_by_bind(const char *bind_app); // bind_app → ch_id +``` + +### 应用线程入口 + +```c +int app_com_router_init1(void *arg); // 创建通道 + 注册回调 +int app_com_router_init2(void *arg); // 连接通道 +void *app_com_router(void *arg); // 主循环 +``` + +## 路由规则 (v2 — bind_app 协议绑定) + +``` +com_router_recv_cb(id, fd, data, len) +│ +├─ Step 1: forward_rule → FTU串口透传 +│ +├─ Step 2: bind_app 非空 → dispatch 投递给注册协议 +│ +└─ Step 3: bind_app 为空 → FTU串口帧内容分路 + ├─ ICP67 dev=0 → 回传维护 + ├─ IEC → 回传维护 + └─ ICP67 dev≠0 → MQ → self_ptl +``` + +**示例**: `tcp_master(:2404)` 配置 `bind_app="iec"` → 整帧直投 libiec + +### bind_app 注册 API + +```c +typedef void (*com_router_dispatch_cb)(uint32_t interface, const unsigned char *data, int len); + +int com_router_register_dispatch(const char *bind_app, com_router_dispatch_cb cb); +``` + +## 配置结构 + +### 通道配置 + +```c +typedef struct { + char key[32]; // "serial_ftu" + char bind_app[16]; // "" (FTU串口不需要绑定) + int type; // COMM_TYPE_UART / COMM_TYPE_TCP_SERVER + union_comm_para para; // 通道参数 + int id; // comm_create 返回值 + int socket_fd; // 当前 fd, -1=未连接 + uint32_t interface; // (id<<16)|fd +} com_channel_t; +``` + +### 转发规则 + +```c +typedef struct { + char listen_ip[32]; // "0.0.0.0" + int listen_port; // 2405 + char target_type[16]; // "uart" + char target_dev[64]; // "/dev/ttyS1" + int sock_fd; // accept 后的 client fd + int target_ch_id; // 目标通道 id +} com_forward_rule_t; +``` + +## 默认配置 (硬编码兜底) + +| 通道 | key | 类型 | bind_app | 说明 | +|---|---|---|---|---| +| 串口 | `serial_ftu` | UART 115200 | `""` | FTU连接, bind_app 空→帧内容分路 | +| TCP | `tcp_master` | TCP :2404 | `"iec"` | IEC-104主站, bind_app 绑定后直投 libiec | + +| 转发规则 | 监听 | 目标 | 说明 | +|---|---|---|---| +| 维护透传 | 0.0.0.0:2405 | FTU 串口 | 维护软件直连 FTU | + +## 多 FTU 扩展 + +``` +新增 FTU 只需: +1. 配置加一个串口通道 + 一个转发规则 +2. com_router 自动按帧的 dev_addr 分路 +3. self_ptl 通过 interface 区分不同 FTU +``` + +## 完成状态 + +### ✅ 已完成 + +- [x] 通道创建/连接/状态回调 +- [x] 转发通道 → FTU 串口透传 +- [x] FTU 串口接收三路分路 (ICP67 dev=0 / IEC / ICP67 dev=1) +- [x] 跨模块回调注册 (app_wiring 解耦) +- [x] bind_app 协议绑定路由 + register_dispatch API +- [x] JSON 解析 channels/forward_rules/bind_app +- [x] TCP accept + 客户端 fd 管理 +- [x] X-Macro + EV_STOP +- [x] 通道健康检测 + +### ⬜ 未完成 + +- [ ] 串口热插拔/自动重连 +- [ ] 单元测试 (test_com_router.c) +- [ ] 通道流量统计上报 (EV_TIMER3) diff --git a/docs/system/libiec/API-libiec.md b/docs/system/libiec/API-libiec.md new file mode 100644 index 0000000..f133104 --- /dev/null +++ b/docs/system/libiec/API-libiec.md @@ -0,0 +1,257 @@ +# API-libiec — IEC 60870-5-104 服务端模块 + +> 版本: v1.0 +> 日期: 2026-07-10 +> 来源: 基于原工程 12 个 IEC 子文件聚合重构 +> 状态: ✅ 已实现,x86 + ARM 双平台编译通过(零警告) + +--- + +## 1. 模块概述 + +libiec 是 RTU 通讯装置的 **IEC 60870-5-104 服务端(从站)模块**,作为 X-Macro 框架的线程模块运行。通过 `com_router` 的 `bind_app="iec"` 机制接收 TCP :2404 端口的主站连接数据,底层协议栈复用 `lib60870` 自研库。 + +| 功能 | 说明 | +|------|------| +| IEC-104 从站协议 | 支持 CS101 / CS104 两种链路类型 | +| 并发连接管理 | 最多 3 路并发主站连接 (MAX_IEC_NUM=3),槽位复用 | +| 超时回收 | 链路 600s 无活动自动回收 slot | +| XML 点表解析 | 从 `config/IEC60870/iec1014.xml` 解析 St/Mx/Co/Dd/Ao/Param 六类信号 | +| 数据中心桥接 | 信号变更回调自动写入 SOE/扰动遥测/故障/电度环形缓冲区 | +| 多主站分发 | 环形缓冲区多出指针 (out_ptrs),每路连接独立消费位置 | +| MQ 桥接 | COM_TO_IEC / IEC_TO_COM 双队列,通过 com_router 转发 | +| 26 个 CS10x 回调 | 完整实现总召、遥测、遥控、电度、参数、定值、文件传输、扰动 | + +### 架构 + +``` +com_router (bind_app=iec) + │ + ▼ MQ_COM_TO_IEC +iec104_data_rx() + │ + ▼ +CS10x_104Init / CS10x_DoRecv + │ + ▼ ┌──────────────────────────────────────────┐ + │ │ 26 个回调 (protocol_register_callbacks) │ + │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ + │ │ │总召/遥信 │ │遥测/电度 │ │遥控/参数 │ │ + │ │ │定值/对时 │ │故障/扰动 │ │文件传输 │ │ + │ │ └──────────┘ └──────────┘ └──────────┘ │ + │ └──────────────────────────────────────────┘ + │ + ▼ MQ_IEC_TO_COM → com_router → TCP → 主站 +``` + +### 与原工程对比 + +| 维度 | 原工程 (12 文件) | 重构后 (3 文件) | +|------|-----------------|-----------------| +| 文件数 | 12 | 3 (~75% 减少) | +| 代码行数 | ~2500+ | 1474 | +| 槽位管理 | 分散在多文件 | 统一 g_iec[MAX_IEC_NUM] | +| 环形缓冲区 | 5 个独立结构体 | 4 个模板化 ring_*_t (sem + out_ptrs) | +| 回调注册 | 分散初始化 | protocol_register_callbacks() 集中 | +| 命令支持 | 无 | CMD_REGISTER_C 测试命令 | +| EV_STOP | 无 | 支持优雅退出 | + +--- + +## 2. 文件结构 + +``` +src/system/libiec/ +├── inc/ +│ ├── iec104.h # 公共头: stru_iec, iec_soe_info_t, iec_yc_info_t, API 声明 +│ └── iec104_cfg.h # 点表配置: stru_iec_sig, stru_iec_cfg +└── src/ + ├── iec104.cpp # CS10x 初始化 + slot 管理 + 26 回调 + 收发桥接 (1252行) + ├── iec104_cfg.cpp # XML 点表解析 (148行) + └── iec104_point.cpp # 数据中心桥接 — 信号变更回调 → 环形缓冲区 (290行) + +release/ +└── {x86,arm}/lib/ + └── libiec.a # 编译产物 +``` + +--- + +## 3. 核心数据结构 + +### 3.1 槽位结构 (`stru_iec`) + +每路并发连接占用一个槽位,全局数组 `g_iec[MAX_IEC_NUM]` (MAX_IEC_NUM=3): + +| 字段 | 类型 | 说明 | +|------|------|------| +| `is_busy` | `uint8_t` | 槽位占用标志,0=空闲 | +| `iec_type` | `CS10x_Type` | CS104_TYPE_S / CS101_TYPE_S | +| `interface` | `uint32_t` | 通道唯一标识 (ch_id<<16 \| fd) | +| `tm_out` | `uint32_t` | 超时计数器 (ms),超过 600s 自动回收 | +| `rx[IEC_BUF_SIZE]` | `uint8_t[1024]` | 接收缓冲区 | +| `tx[IEC_BUF_SIZE]` | `uint8_t[1024]` | 发送缓冲区 | +| `pub[IEC_BUF_SIZE]` | `uint8_t[1024]` | 自主上传缓冲区 (总召/扰动) | +| `bak[IEC_BUF_SIZE]` | `uint8_t[1024]` | 备用缓冲区 | +| `cs10x` | `CS10x` | lib60870 协议栈句柄 | + +### 3.2 环形缓冲区 + +4 个环形缓冲区均带独立信号量 (sem_t) 和多出指针 (out_ptrs),支持多主站并发消费: + +| 缓冲区 | 容量 | 记录类型 | 用途 | +|--------|------|---------|------| +| `g_soe_ring` | 300 | SOE (时标+状态) | 遥信变位事件 | +| `g_yc_ring` | 200 | 扰动遥测 (idx+val) | 遥测越限扰动 | +| `g_fault_ring` | 200 | 故障 (SOE+遥测) | 故障录波事件 | +| `g_dd_ring` | 50 | 电度 (CP56Time2a+val) | 电度累计 | + +### 3.3 点表配置 (`stru_iec_cfg`) + +从 `config/IEC60870/iec1014.xml` 解析,6 类信号向量: + +| 向量 | 信号类型 | IOA 起始 | 默认类型 | +|------|---------|---------|---------| +| `vec_st` | 遥信 (St) | 0x0001 | DATA_TYPE_U8 | +| `vec_mx` | 遥测 (Mx) | 0x4001 | DATA_TYPE_F32 | +| `vec_co` | 遥控 (Co) | — | DATA_TYPE_U8 | +| `vec_dd` | 电度 (Dd) | 0x6401 | DATA_TYPE_F32 | +| `vec_ao` | 参数 (Ao) | — | 无默认 (XML 必须指定) | +| `vec_param` | 定值 (Param) | — | 无默认 (XML 必须指定) | + +每条信号 (`stru_iec_sig`) 含: `link` (数据中心路径), `desc` (描述), `inf` (IOA), `type` (数据类型), `invert` (遥信取反), `factor`/`offset` (遥测系数偏移), `p_data` (数据指针)。 + +--- + +## 4. API 参考 + +### 4.1 公共头 API (iec104.h) + +| 函数 | 功能 | 参数 | 返回值 | +|------|------|------|--------| +| `iec104_get_by_interface()` | 按 interface 查找 IEC 实例 | `uint32_t interface` | `stru_iec *` 或 NULL | +| `iec104_data_rx()` | MQ 数据入口 (从 com_router 接收) | `uint8_t *msg` | 0=成功, -1=失败 | +| `iec104_task()` | 10ms 定时器处理 | `uint16_t gap` (恒10) | void | + +### 4.2 线程入口 + +| 函数 | 功能 | 说明 | +|------|------|------| +| `app_iec_init1()` | 第一阶段初始化 | 解析 XML 点表 (`iec_cfg_parse`) | +| `app_iec_init2()` | 第二阶段初始化 | 注册数据中心回调 (`iec104_point_init`) | +| `app_iec()` | 主循环 | `while(1)` 收 MQ_COM_TO_IEC + 10ms 定时处理 | + +### 4.3 点表 API (iec104_cfg.h) + +| 函数 | 功能 | 参数 | 返回值 | +|------|------|------|--------| +| `iec_cfg_ptr_get()` | 获取全局配置指针 | void | `stru_iec_cfg *` | +| `iec_cfg_parse()` | 解析 XML 点表文件 | `const char *xml_path` | 0=成功, -1=失败 | + +### 4.4 环形缓冲区回调 (iec104_point.cpp → iec104.cpp) + +| 函数 | 功能 | +|------|------| +| `cb_soe_write()` | 写入 SOE 记录 (遥信变位) | +| `cb_yc_disturb_write()` | 写入扰动遥测记录 | +| `cb_dd_disturb_write()` | 写入电度记录 | +| `cb_fault_write()` | 写入故障事件记录 | + +--- + +## 5. CS10x 回调清单 (26 个) + +### 5.1 时间 & 初始化 (4) + +| 回调 | 功能 | CS10x 注册 | +|------|------|-----------| +| `cb_get_time` | 获取当前时间 (CP56Time2a) | 时钟回调 | +| `cb_set_time` | 设置系统时间 (对时) | 时钟回调 | +| `cb_104init_callback` | 接收 CS104 初始化完成通知 | 104 专用 | +| `cb_tm_send_result` | 对时报文发送结果通知 | COI 回调 | + +### 5.2 总召 (3) + +| 回调 | 功能 | +|------|------| +| `cb_st` | 遥信总召 — 遍历 vec_st 打包 SQ=1 报文 | +| `cb_mx` | 遥测总召 — 遍历 vec_mx 打包浮点报文 | +| `cb_dd` | 电度总召 — 遍历 vec_dd 打包 CP56Time2a 报文 | + +### 5.3 遥控 (5) + +| 回调 | 功能 | +|------|------| +| `cb_yk_set` | 遥控选择/执行 — 遍历 vec_co 查找 IOA, 写入 datacenter | +| `cb_yk_confirm_func` | 遥控确认回调 | +| `cb_yk_termination_func` | 遥控终止回调 | +| `cb_yk_order_func` | 遥控命令通知 | +| `cb_yx_over_time_confirm_func` | 遥信超时确认 | + +### 5.4 参数 & 定值 (4) + +| 回调 | 功能 | +|------|------| +| `cb_param_get_func` | 参数 (Ao) 读取 — 遍历 vec_ao | +| `cb_param_set_func` | 参数 (Ao) 设置 — 写 datacenter | +| `cb_offset_get_func` | 定值 (Param) 读取 — 遍历 vec_param | +| `cb_offset_set_func` | 定值 (Param) 设置 — 写 datacenter | + +### 5.5 扰动 & 故障 (5) + +| 回调 | 功能 | +|------|------| +| `cb_dir_num` | 目录数量查询 | +| `cb_soe_ring_get_one` | 从 SOE 环形缓冲区读取一条 | +| `cb_yc_ring_get_one` | 从扰动遥测环形缓冲区读取一条 | +| `cb_fault_ring_get_one` | 从故障环形缓冲区读取一条 | +| `cb_dd_ring_get_one` | 从电度环形缓冲区读取一条 | + +### 5.6 文件传输 (5) + +| 回调 | 功能 | +|------|------| +| `cb_file_read` | 文件读回调 | +| `cb_file_write` | 文件写回调 | +| `cb_file_wr_finish` | 文件写入完成回调 | +| `cb_file_call_func` | 文件服务调用 | +| `cb_dir_num` | 目录服务 (与扰动共用) | + +--- + +## 6. MQ 消息流 + +``` +主站 → TCP :2404 → com_router (bind_app=iec) + → MQ_COM_TO_IEC + → app_iec() 主循环收 + → CS10x_DoRecv() + → 对应回调 → datacenter + +datacenter 信号变更 (St/Mx) + → on_st_change / on_mx_change 回调 + → cb_soe_write / cb_yc_disturb_write → 环形缓冲区 + +总召/扰动触发: + → iec104_data_tx() → MQ_IEC_TO_COM + → com_router → TCP → 主站 +``` + +--- + +## 7. 配置依赖 + +| 配置文件 | 用途 | +|----------|------| +| `config/CHANNEL/channel_cfg.json` | TCP :2404 配置 `bind_app="iec"` | +| `config/IEC60870/iec1014.xml` | IEC 点表 (St/Mx/Co/Dd/Ao/Param) | +| `config/SYSTEM/app_config.json` | X-Macro 框架启停控制 | + +--- + +## 8. 已知限制 + +- 扰动文件 (COMTRADE) 上传/下载: 回调已注册但无实际文件系统实现 +- 遥控执行超时: 使用固定 30s 超时,不支持配置 +- 缓冲区满: 环形缓冲写满后丢弃最早记录 (无 overflow 告警回调) +- 只有 IEC-104 从站模式,未实现主站模式 (CS10x_Type 预留 CS104_TYPE_M/CS101_TYPE_M) diff --git a/docs/system/libself_ptl/API-libself_ptl.md b/docs/system/libself_ptl/API-libself_ptl.md new file mode 100644 index 0000000..a9a5d61 --- /dev/null +++ b/docs/system/libself_ptl/API-libself_ptl.md @@ -0,0 +1,209 @@ +# libself_ptl — 自协议应用层模块 API 参考 + +## 模块概述 + +**位置**: `src/system/libself_ptl/` + +**定位**: RTU 系统中连接 ICP67 协议库与数据中心的上层集成模块。负责: +- ICP67 协议初始化 +- 双协议帧解复用 (ICP67 + IEC-104) +- 信号注册到数据中心 (790 ST + 500 MX + 56 CO + 64 DD + 1564 AO) +- 17 个回调桥接到数据中心 +- 定时器驱动重传管理 + +**依赖**: `libicp67` (协议引擎), `libdatacenter` (数据中心), `tinyxml2` (XML 解析), `liblog` (日志) + +**被依赖**: `RTU 主程序` (通过 X-Macro 框架) + +## 架构 + +``` +config/SELF_PTL/self_ptl.xml (2988行, 790+ 信号) + │ + ▼ +self_ptl_cfg.cpp XML解析 (tinyxml2) + │ + ▼ +self_ptl.cpp 核心模块 (不依赖 com_router ✅) + ├─ self_ptl_init() 初始化 ICP67 + 注册信号 + ├─ self_ptl_data_rx(if,*) 接收数据 (interface 区分多FTU) + ├─ self_ptl_data_tx(*,if) 发送数据 (interface 定位通道) + └─ app_self_ptl() 线程主循环 (事件驱动) + ▲ + │ app_wiring() 注册回调 (app_sys.cpp 集中解耦) + │ + com_router (不直接依赖 ✅) +``` + +## 文件清单 + +| 文件 | 行数 | 说明 | +|---|---|---| +| `src/system/libself_ptl/inc/self_ptl.h` | 144 | 配置数据结构、双协议常量、事件定义 | +| `src/system/libself_ptl/inc/self_ptl_cfg.h` | 32 | XML 解析声明 | +| `src/system/libself_ptl/src/self_ptl.cpp` | 526 | 核心: 初始化、双协议解复用、信号注册、主循环 | +| `src/system/libself_ptl/src/self_ptl_cfg.cpp` | 336 | tinyxml2 解析 St/Mx/Co/Dd/Ao/Param | +| `src/system/libself_ptl/src/self_ptl_cb.cpp` | 251 | 17 个回调实现 (→数据中心桥接) | + +## 对外 API + +### 初始化 (C++ 实现, extern "C" 接口) + +```c +int app_self_ptl_init1(void *arg); // 阶段1: 解析 XML + 初始化协议 +int app_self_ptl_init2(void *arg); // 阶段2: 信号注册 (已在 init1 中完成) +void *app_self_ptl(void *arg); // 线程主循环 +``` + +### 数据接收 (多 FTU 支持) + +```c +void self_ptl_data_rx(uint32_t interface, uint8_t *p_data, uint16_t len); +``` + +从 com_router 接收原始帧数据。 +- `interface`: (ch_id<<16)|fd, 区分多 FTU 来源 +- 内部更新 `g_self_ptl.interface` 供发送回程 + +### 发送回调 + +```c +static void self_ptl_data_tx(uint8_t *p_tx, uint16_t tx_len, void *arg); +``` + +ICP67 协议引擎的发送回调。 +- `arg` 指向 `g_self_ptl.interface`, 用于定位发送目标通道 +- TODO: 完成后端 comm_send 调用 + +### 定时器 + +```c +void self_ptl_task(void); +``` + +每 10ms (EV_TIMER1) 调用一次,触发 ICP67 重传定时器。 + +## 配置文件格式 + +`config/SELF_PTL/self_ptl.xml` 结构: + +```xml + + + + ... + + + + ... + + + + ... + +
+ ... +
+ + + ... + + +
+``` + +## 信号注册映射 + +| XML 段 | 信号类型 | 数据中心 API | saddr 格式 | 数据类型 | +|---|---|---|---|---| +| St | 遥信 | `dc_signal_out` | `self_ptl.st.N` | uint8 | +| Mx | 遥测 | `dc_signal_out` | `self_ptl.mx.N` | float | +| Co | 遥控 | `dc_signal_yk` | `self_ptl.co.N` | uint8 (SBO) | +| Dd | 电度 | `dc_signal_out` | `self_ptl.dd.N` | float | +| Ao | 参数 | `dc_signal_ao` | `self_ptl.ao.N` | float (SBO) | +| Param | 定值 | `dc_signal_param` | `self_ptl.param.N` | float (SBO) | + +## 回调数据流 + +### 实时数据 (MX/ST/DD) + +``` +采样板 TI_200/201/202 → icp67_decode → p_icp67->method.mx_trans_cb + → mx_trans() in self_ptl_cb.cpp + → snprintf("self_ptl.mx.%d", info_addr) + → dc_set_out_signal_val(saddr, &val, MODULE_SELF_PTL) + → 数据中心 dirty 标记 + 100ms 延迟回调 +``` + +### SOE 事件 / 故障 / 扰动 + +``` +采样板 TI_204/205/207 → icp67_decode → fault_trans_cb / soe_trans_cb / mx_change_cb + → 当前实现: LOG_I 记录 + → TODO: dc_event_queue_push / dc_fault_queue_push / dc_disturb_dd_queue_push +``` + +### 文件操作 / 升级 / 自检 + +``` +维护软件 TI_6/9/11/30 → icp67_decode → 对应 pop_out/trans 回调 + → 当前实现: LOG_I 记录 + → TODO: 文件系统集成 / 升级流程 / 自检数据写入数据中心 +``` + +## 环形缓冲区 + +``` +stru_self_rx { + rptr, wptr (读写指针) + size = 2048 (缓冲区大小) + cnt (有效数据计数) + buf[2048] (数据缓冲区) + mutex (pthread_mutex_t 保护) +} +``` + +- `put_rx_data`: ISR 生产者 (锁保护) +- `get_rx_data`: 任务消费者 (锁保护) +- `clear_rx_data`: 帧处理完毕后清空 +- 溢出处理: LOG_E + 丢弃旧数据 + +## 完成状态 + +### ✅ 已完成 + +- [x] ICP67 协议引擎初始化 +- [x] 17 个回调注册 (icp67 → self_ptl) +- [x] XML 配置解析 (St/Mx/Co/Dd/Ao/Param, 2988行) +- [x] 信号注册到数据中心 (5 种信号类型) +- [x] 环形缓冲 (mutex 保护) +- [x] ICP67 帧搜索 + 解码调用链 +- [x] IEC-104 帧检测 (I/S/U 帧分类, 已移除) +- [x] 定时器驱动 (EV_TIMER1) +- [x] X-Macro 框架集成 +- [x] MX/ST/DD 实时数据 → 数据中心 +- [x] 编译通过 (x86, 0 error) +- [x] **多 FTU interface 预留** (data_rx/data_tx 签名) +- [x] **模块解耦** (不依赖 com_router, app_wiring 集中连接) +- [x] **self_ptl_data_tx → comm_send** (发送路径打通) +- [x] **SOE/故障/扰动 → 数据中心** (dc_event/fault/disturb_queue_push) +- [x] **自检信号 → 数据中心** (self_check_pop_out → dc_set_out_signal_val) +- [x] **AO 参数 → 数据中心** (ao_pop_out → dc_set_out_signal_val) + +### ⬜ 未完成 + +| 优先级 | 任务 | 说明 | +|:---:|---|---| +| 🟡 | 文件操作集成 | 6 个文件读写回调 → 文件系统 | +| 🟢 | 单元测试 | test_self_ptl.cpp | + +## 与原工程的差异 + +| 维度 | 原 RTU (self_ptl.cpp) | 重构后 | +|---|---|---| +| 回调表 | 全局 `g_genneral_method` + `g_self_ptl.method` 副本 | 注册到 `p_icp67->method` (实例化) | +| IEC-104 | 独立 `iec_rx` 环形缓冲 + 完整 IEC 帧搜索 | 单 `icp67_rx` 环形缓冲 + 帧类型检测 | +| 测试命令 | `self_ptl_method_task()` (生产代码混入) | 移除 (仅保留核心功能) | +| 文件拆分 | 2 文件 (self_ptl.cpp 1400+行 + method.cpp 300+行) | 3 文件 (core 526 + cfg 336 + cb 251) | +| XML 解析 | 耦合在 `RTU/self_ptl_cfg.cpp` | 独立 `libself_ptl/self_ptl_cfg.cpp` | +| 事件模型 | `stru_msg_head` 包装 + 内部事件 | 直接调用 self_ptl_data_rx + EV_SELF_PTL_RX_COM | diff --git a/release/src/system/libplc/makefile b/release/src/system/libplc/makefile new file mode 100644 index 0000000..58d83cb --- /dev/null +++ b/release/src/system/libplc/makefile @@ -0,0 +1,39 @@ +include ./../../../linux.mk +L := $(notdir $(realpath $(CURDIR)/..)) +M := libplc +O := $(LIB_REL)/$(M).a +S := $(SRC_ROOT_DIR)/$(L)/libplc/src +I := -I$(SRC_ROOT_DIR)/$(L)/libplc/inc -I$(SRC_ROOT_DIR)/$(L)/inc \ + -I$(SRC_ROOT_DIR)/$(L)/libself_ptl/inc \ + -I$(SRC_ROOT_DIR)/public/libdatacenter/inc -I$(SRC_ROOT_DIR)/public/liblog/inc \ + -I$(SRC_ROOT_DIR)/public/libxml/inc -I$(SRC_ROOT_DIR)/public/libtask/inc \ + -I$(SRC_ROOT_DIR)/public/libfunc/inc +B := $(CURDIR)/$(M)/obj +SRCS := $(wildcard $(S)/*.cpp) $(wildcard $(S)/*.c) +OBJS := $(patsubst $(S)/%.cpp, $(B)/%.o, $(patsubst $(S)/%.c, $(B)/%.o, $(SRCS))) +F := $(CXX_FLAGS) $(I) + +.PHONY: all +all: + @mkdir -p $(ROOT_DIR)/release/inc + @$(MAKE) $(O) + +$(O): $(OBJS) + @mkdir -p $(dir $@) + $(AR) rcs $@ $^ + @echo "[$(M)] built" + +$(B)/%.o: $(S)/%.cpp + @mkdir -p $(dir $@) + $(CXX) $(F) -c $< -o $@ + +$(B)/%.o: $(S)/%.c + @mkdir -p $(dir $@) + $(CC) $(C_FLAGS) $(I) -c $< -o $@ + +.PHONY: clean +clean: + rm -rf $(B) $(O) + +.PHONY: rebuild +rebuild: clean all diff --git a/release/src/system/makefile b/release/src/system/makefile index eebebb2..23eb33f 100644 --- a/release/src/system/makefile +++ b/release/src/system/makefile @@ -1,7 +1,7 @@ include ./../../linux.mk # app_sys.cpp 已合并到 RTU makefile 中,此处仅管理子模块 -SUBDIRS := ./libmodbus_m ./libweb_server ./libcom_router ./libself_ptl ./libiec ./RTU +SUBDIRS := ./libmodbus_m ./libweb_server ./libcom_router ./libself_ptl ./libiec ./libplc ./RTU .PHONY: all clean rebuild diff --git a/src/system/inc/app_modules.h b/src/system/inc/app_modules.h index 18da85c..79040f9 100644 --- a/src/system/inc/app_modules.h +++ b/src/system/inc/app_modules.h @@ -23,6 +23,7 @@ APP_MODULE(WEB_SERVER, web_server, app_web_server_init1, app_web_server_init2, a APP_MODULE(SELF_PTL, self_ptl, app_self_ptl_init1, app_self_ptl_init2, app_self_ptl) APP_MODULE(COM_ROUTER, com_router, app_com_router_init1, app_com_router_init2, app_com_router) APP_MODULE(IEC, iec, app_iec_init1, app_iec_init2, app_iec) +APP_MODULE(PLC, plc, app_plc_init1, app_plc_init2, app_plc) /* ============== 预留模块(阶段 2-3 实现后取消注释) ============== */ // APP_MODULE(COM_DECODE, com_decode, app_com_decode_init1, app_com_decode_init2, app_com_decode) diff --git a/src/system/libplc/inc/plc.h b/src/system/libplc/inc/plc.h new file mode 100644 index 0000000..2dcc928 --- /dev/null +++ b/src/system/libplc/inc/plc.h @@ -0,0 +1,38 @@ +/** + * @file plc.h + * @brief PLC 逻辑引擎模块 — 公共头 + * + * @details 基于原工程 plc.cpp 直接导入,模块化的 PLC 逻辑执行引擎。 + * 支持 10 种逻辑门(AND/OR/NOT/62P/62D/RISING/FALLING/SR/RS + 硬件点), + * XML 配置文件驱动,拓扑排序执行,非阻塞定时器。 + */ + +#ifndef _PLC_H_ +#define _PLC_H_ + +#include "myBase.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** PLC 逻辑入口 — 按配置文件执行所有逻辑图 */ +int PLC_Logic(void); + +/** 动态设置 PLC 配置文件路径 */ +void plc_set_config_path(const char *path); + +/** 获取当前 PLC 配置文件路径 */ +const char *plc_get_config_path(void); + +/* =========================== 模块线程入口 =========================== */ + +int app_plc_init1(void *arg); +int app_plc_init2(void *arg); +void *app_plc(void *arg); + +#ifdef __cplusplus +} +#endif + +#endif /* _PLC_H_ */ diff --git a/src/system/libplc/src/plc.cpp b/src/system/libplc/src/plc.cpp new file mode 100644 index 0000000..39ee084 --- /dev/null +++ b/src/system/libplc/src/plc.cpp @@ -0,0 +1,1917 @@ +/** + * @file plc.cpp + * @brief PLC 逻辑引擎 — XML 驱动可编程逻辑控制器 + * + * @details 完整复刻原工程 libplc 模块, 仅在与其他模块交互的接口处做了适配。 + * 其余程序原封不动。 + * + * 核心能力: + * - XML 配置文件驱动 (PLCConfig → Chain → Comb/Gate/Inputs/Outputs) + * - 10 种逻辑门: INPUT / OUTPUT / AND / OR / NOT / + * T62P(延时导通) / T62D(延时关断) / RISING / FALLING / SR / RS + * - 嵌套 Comb 支持递归解析, R/S 引脚属性 + * - Kahn 拓扑排序 (保证节点按依赖顺序执行, 检测环路) + * - 非阻塞定时器 (clock_gettime CLOCK_MONOTONIC) + * - mtime 文件变化检测 (stat, 仅在 XML 更新时重新解析) + * - 与 self_ptl 遥信联动 (dc_signal_in 链接到 self_ptl 配置的遥信信号) + * - 经 datacenter 写入 out 信号 (dc_set_out_signal_val) + * + * 线程模型: X-Macro 框架 app_plc 线程, EV_TIMER3(1s) 触发 PLC_Logic() + * + * 配置: config/PLC/Reclose_logic.xml (运行时 func_proc_self_dir 拼接) + */ + +#include "plc.h" +#include "self_ptl.h" +#include "self_ptl_cfg.h" +#include "myBase.h" +#include "mySystem.h" +#include "myDatacenter.h" +#include "myLog.h" +#include "myCmd.h" +#include "myFunc.h" +#include +#include +#include +#include // clock_gettime 非阻塞定时器 +#include // stat() 文件变化检测 +#include "tinyxml2.h" // XML 配置文件解析 + +/* ================================================================== + * 调试宏 — 运行时可通过 `plc dbg on|off` 开关 + * ================================================================== */ + +static int g_plc_dbg_enabled = 0; /**< PLC 调试打印开关, 0=关闭, 1=打开 */ + +/** 调试打印宏 — 仅在 g_plc_dbg_enabled 非零时输出到 stdout */ +#define PLC_DBG(fmt, ...) \ + do { if (g_plc_dbg_enabled) { printf(fmt, ##__VA_ARGS__); } } while (0) + +/* ================================================================== + * 模块常量 & 全局变量 + * ================================================================== */ + +#define MAX_OUT_NODES 20 /**< 最大输出节点数 */ + +LOCAL uint8_t g_plc_st_out[MAX_OUT_NODES] = {0}; +LOCAL uint32_t *gp_run_cnt_in = NULL; +LOCAL uint8_t *gp_st[10] = {NULL}; + +/** PLC 与 self_ptl 遥信的关联配置 */ +typedef struct +{ + stru_self_ptl_cfg_base base; + uint8_t * p_data; +} stru_plc_cfg; + +stru_self_ptl_cfg *p_plc_cfg = nullptr; +std::vector g_plc_cfg = {}; + +/* ================================================================== + * 逻辑引擎常量 & 数据结构 + * ================================================================== */ + +#define MAX_LOGIC_GRAPHS 4 /**< 最大逻辑图数量 */ +#define MAX_NODES_PER_GRAPH 64 /**< 每个逻辑图最大节点数 */ +#define MAX_LINKS_PER_GRAPH 64 /**< 每个逻辑图最大链路数 */ + +/** 节点类型 — 10 种逻辑门 + 硬件点 */ +#define NODE_TYPE_HW_OUTPUT 0 /**< 硬件输出点 */ +#define NODE_TYPE_HW_INPUT 1 /**< 硬件输入点 */ +#define NODE_TYPE_OR_GATE 2 /**< 或门 */ +#define NODE_TYPE_AND_GATE 3 /**< 与门 */ +#define NODE_TYPE_NOT_GATE 4 /**< 非门 */ +#define NODE_TYPE_62P_GATE 5 /**< 62P 延时导通 (IN=1 延时后 OUT=1, IN=0 立即 OUT=0) */ +#define NODE_TYPE_62D_GATE 6 /**< 62D 延时关断 (IN=1 立即 OUT=1, IN=0 延时后 OUT=0) */ +#define NODE_TYPE_RISING_EDGE 7 /**< 上升沿检测 (0→1 脉冲) */ +#define NODE_TYPE_SR_LATCH 8 /**< SR 触发器 (S 优先) */ +#define NODE_TYPE_RS_LATCH 9 /**< RS 触发器 (R 优先) */ +#define NODE_TYPE_FALLING_EDGE 10 /**< 下降沿检测 (1→0 脉冲) */ + +/** 动态链表 — 硬件点 (运行时 I/O 状态) */ +typedef struct HWPoint +{ + int id; + int value; + struct HWPoint *next; +} HWPoint; + +typedef struct +{ + HWPoint *input_head; + HWPoint *output_head; + int input_count; + int output_count; +} HardwareManager; + +/** 逻辑节点 — 一个门元件或硬件点 */ +typedef struct +{ + int type; + int id; + int value; + int last_input; /**< 上一轮输入值 (边沿检测用) */ + + /* ---- 非阻塞定时器 (62P/62D) ---- */ + uint8_t timer_active; /**< 1=正在计时 */ + struct timespec timer_start; /**< 计时起点 */ + int timer_delay_ms; /**< 延时毫秒数 */ + int timer_pending_output; /**< 到期后输出值, -1=无待定 */ + int delay_ms; /**< XML 配置的延时 (delayMs) */ + + /* ---- SR/RS 引脚来源描述 ---- */ + char rs_s_src[32]; + char rs_r_src[32]; + + /* ---- 拓扑排序 ---- */ + int indegree; /**< 入度 (依赖的前驱节点数) */ +} LogicNode; + +/** 逻辑链路 — 节点间连线 */ +typedef struct +{ + int src_type; + int src_id; + int dest_type; + int dest_id; + int dst_pin; /**< 目标引脚 (0=S, 1=R, -1=默认), 仅 SR/RS 有效 */ +} LogicLink; + +/** 逻辑图 — 一张完整的 PLC 逻辑图 */ +typedef struct +{ + char key[20]; /**< 逻辑图标识 */ + char name[50]; /**< 逻辑图名称 */ + LogicNode nodes[MAX_NODES_PER_GRAPH]; + int node_count; + LogicLink links[MAX_LINKS_PER_GRAPH]; + int link_count; +} LogicGraph; + +/* ================================================================== + * PLC 全局状态 + * ================================================================== */ + +LOCAL LogicGraph g_cached_graphs[MAX_LOGIC_GRAPHS]; /**< 缓存的逻辑图 */ +LOCAL int g_cached_graph_count = 0; /**< 缓存的逻辑图数量 */ +LOCAL time_t g_last_file_mtime = 0; /**< 配置文件最后修改时间 */ +LOCAL char g_plc_config_path[256] = "config/PLC/Reclose_logic.xml"; + +/* ================================================================== + * LOCAL 封装 — 简化 datacenter out 信号访问 + * ================================================================== */ + +/** + * @brief 通过 out 信号 ID 写入值 (内部封装 dc_get_signal_info_by_id → dc_set_out_signal_val) + * @param id out 信号 ID (XML 中 Signal 的 no 属性) + * @param value 写入值 (0 或 1) + * @return 0=成功, -1=未找到该信号 + */ +LOCAL int plc_set_out_by_id(int id, int value) +{ + char saddr[128] = {0}; + char desc[128] = {0}; + char dtype_str[32] = {0}; + char link_str[256] = {0}; + uint8_t ctrl_type; + + if (0 != dc_get_signal_info_by_id("out", (uint32_t)id, + saddr, sizeof(saddr), desc, sizeof(desc), + dtype_str, sizeof(dtype_str), &ctrl_type, + link_str, sizeof(link_str))) + { + return -1; + } + + uint8_t val = (uint8_t)(value ? 1 : 0); + return dc_set_out_signal_val(saddr, &val, "plc"); +} + +/** + * @brief 通过 out 信号 ID 读取当前值 + * @param id out 信号 ID + * @return 信号值 (0/1), 未找到时返回 0 + */ +LOCAL int plc_get_out_value_by_id(int id) +{ + char saddr[128] = {0}; + char desc[128] = {0}; + char dtype_str[32] = {0}; + char link_str[256] = {0}; + uint8_t ctrl_type; + + if (0 != dc_get_signal_info_by_id("out", (uint32_t)id, + saddr, sizeof(saddr), desc, sizeof(desc), + dtype_str, sizeof(dtype_str), &ctrl_type, + link_str, sizeof(link_str))) + { + return 0; + } + + void *p_data = NULL; + uint8_t data_type = 0; + char d2[128] = {0}; + + if (0 != dc_get_out_signal_info(saddr, d2, sizeof(d2), &data_type, &p_data) || NULL == p_data) + { + return 0; + } + + if (data_type == DATA_TYPE_U8) + { + return *(uint8_t *)p_data; + } + else if (data_type == DATA_TYPE_U16) + { + return (*(uint16_t *)p_data) ? 1 : 0; + } + else if (data_type == DATA_TYPE_U32) + { + return (*(uint32_t *)p_data) ? 1 : 0; + } + else if (data_type == DATA_TYPE_F32) + { + return (*(float *)p_data > 0.5f) ? 1 : 0; + } + + return 0; +} + +/* ================================================================== + * 硬件点链表操作 + * ================================================================== */ +static HWPoint* create_hw_point(int id, int value) +{ + HWPoint *point = (HWPoint*)malloc(sizeof(HWPoint)); + if (point) + { + point->id = id; + point->value = value; + point->next = NULL; + } + return point; +} + +// 计算从 start 到当前时刻经过的毫秒数 +static long get_elapsed_ms(struct timespec *start) +{ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + long sec_diff = now.tv_sec - start->tv_sec; + long nsec_diff = now.tv_nsec - start->tv_nsec; + return sec_diff * 1000 + nsec_diff / 1000000; +} + +static void destroy_hw_list(HWPoint *head) +{ + HWPoint *tmp; + while (head) + { + tmp = head; + head = head->next; + free(tmp); + } +} + +static void init_hardware_manager(HardwareManager *hw) +{ + memset(hw, 0, sizeof(HardwareManager)); +} + +static void destroy_hardware_manager(HardwareManager *hw) +{ + destroy_hw_list(hw->input_head); + destroy_hw_list(hw->output_head); + memset(hw, 0, sizeof(HardwareManager)); +} + +static int get_hw_point_value(HWPoint *head, int id) +{ + HWPoint *curr = head; + + while (curr) + { + if (curr->id == id) + { + return curr->value; + } + + curr = curr->next; + } + + return 0; +} + +static int set_hw_point_value(HWPoint **head, int *count, int id, int value) +{ + HWPoint *curr = *head; + while (curr) + { + if (curr->id == id) + { + curr->value = value; + return 0; + } + curr = curr->next; + } + HWPoint *new_point = create_hw_point(id, value); + if (!new_point) + { + LOG_E("PLC: 内存分配失败, 无法创建硬件点 ID=%d", id); + return -1; + } + new_point->next = *head; + *head = new_point; + (*count)++; + return 0; +} + +// 从逻辑图中提取所有硬件点到管理器 +static void extract_hw_points_from_graphs(LogicGraph graphs[], int graph_count, + HardwareManager *hw, int default_input_value) { + for (int g = 0; g < graph_count; g++) + { + LogicGraph *graph = &graphs[g]; + for (int n = 0; n < graph->node_count; n++) + { + LogicNode *node = &graph->nodes[n]; + if (node->type == NODE_TYPE_HW_INPUT) + { + default_input_value = plc_get_out_value_by_id(node->id); + set_hw_point_value(&hw->input_head, &hw->input_count, + node->id, default_input_value); + } + else if (node->type == NODE_TYPE_HW_OUTPUT) + { + set_hw_point_value(&hw->output_head, &hw->output_count, node->id, 0); + } + } + } +} + +static void print_hardware_manager(HardwareManager *hw) +{ + PLC_DBG("==================== 硬件点列表 ====================\n"); + PLC_DBG("硬件输入点(dev_out):共%d个\n", hw->input_count); + HWPoint *curr = hw->input_head; + int idx = 1; + while (curr) + { + PLC_DBG(" %d. ID=%d, 值=%d\n", idx++, curr->id, curr->value); + curr = curr->next; + } + PLC_DBG("硬件输出点(dev_in):共%d个\n", hw->output_count); + curr = hw->output_head; + idx = 1; + while (curr) + { + PLC_DBG(" %d. ID=%d, 值=%d\n", idx++, curr->id, curr->value); + curr = curr->next; + } +} + +/* ================================================================== + * 节点查找 + * ================================================================== */ +static LogicNode* find_node(LogicGraph *graph, int type, int id) +{ + for (int i = 0; i < graph->node_count; i++) + { + if ((type == 0) || (type == 1)) + { + if (graph->nodes[i].type == type && graph->nodes[i].id == id) + { + return &graph->nodes[i]; + } + } + else + { + if (graph->nodes[i].type == type && + ((graph->nodes[i].id & 0xFF) == (id & 0xFF))) { + return &graph->nodes[i]; + } + } + } + return NULL; +} + +/* ================================================================== + * XML 配置文件解析 (Comb 嵌套结构) + * ================================================================== */ + +static int gate_type_from_xml_str(const char *type_str) +{ + if (NULL == type_str) + { + return -1; + } + + if (strcmp(type_str, "INPUT") == 0) + { + return NODE_TYPE_HW_INPUT; + } + if (strcmp(type_str, "OUTPUT") == 0) + { + return NODE_TYPE_HW_OUTPUT; + } + if (strcmp(type_str, "OR") == 0) + { + return NODE_TYPE_OR_GATE; + } + if (strcmp(type_str, "AND") == 0) + { + return NODE_TYPE_AND_GATE; + } + if (strcmp(type_str, "NOT") == 0) + { + return NODE_TYPE_NOT_GATE; + } + if (strcmp(type_str, "T62P") == 0) + { + return NODE_TYPE_62P_GATE; + } + if (strcmp(type_str, "T62D") == 0) + { + return NODE_TYPE_62D_GATE; + } + if (strcmp(type_str, "RISING") == 0) + { + return NODE_TYPE_RISING_EDGE; + } + if (strcmp(type_str, "SR") == 0) + { + return NODE_TYPE_SR_LATCH; + } + if (strcmp(type_str, "RS") == 0) + { + return NODE_TYPE_RS_LATCH; + } + if (strcmp(type_str, "FALLING") == 0) + { + return NODE_TYPE_FALLING_EDGE; + } + + return -1; +} + +static int find_node_index(LogicGraph *graph, int type, int id) +{ + for (int i = 0; i < graph->node_count; i++) + { + if (graph->nodes[i].type == type) + { + if (type <= 1 && graph->nodes[i].id == id) + { + return i; + } + if (type > 1 && (graph->nodes[i].id & 0xFF) == (id & 0xFF)) + { + return i; + } + } + } + + return -1; +} + +/** + * @brief Comb 内部信号 — 不存在则先创建 + * @param io_type 节点类型 (NODE_TYPE_HW_INPUT / NODE_TYPE_HW_OUTPUT) + * @param sig_no 信号编号 + * @return 节点在 nodes[] 中的索引, -1=失败 + */ +static int ensure_signal(LogicGraph *graph, int io_type, int sig_no) +{ + int idx = find_node_index(graph, io_type, sig_no); + + if (idx >= 0) + { + return idx; + } + + if (graph->node_count >= MAX_NODES_PER_GRAPH) + { + return -1; + } + + LogicNode &n = graph->nodes[graph->node_count++]; + memset(&n, 0, sizeof(n)); + n.timer_pending_output = -1; + n.type = io_type; + n.id = sig_no; + + return graph->node_count - 1; +} + +/** + * @brief 递归解析 Comb 块: Inputs → [嵌套 Comb] → Gate → Outputs + * @param comb_el XML Comb 元素 + * @param graph 逻辑图 (边解析边追加节点/链路) + * @return 本 Comb 中 Gate 节点的索引, -1=失败 + */ +static int parse_xml_comb(tinyxml2::XMLElement *comb_el, LogicGraph *graph) +{ + if (NULL == comb_el || graph->node_count >= MAX_NODES_PER_GRAPH) + { + return -1; + } + + tinyxml2::XMLElement *gate_el = comb_el->FirstChildElement("Gate"); + + if (NULL == gate_el) + { + LOG_E("Comb缺少Gate"); + return -1; + } + + LogicNode &gn = graph->nodes[graph->node_count++]; + memset(&gn, 0, sizeof(gn)); + gn.timer_pending_output = -1; + gn.type = gate_type_from_xml_str(gate_el->Attribute("type")); + gn.id = gate_el->IntAttribute("id"); + + int gi = graph->node_count - 1; + + /* 读取延时毫秒 (62P/62D) */ + const char *delay_str = gate_el->Attribute("delayMs"); + + if (delay_str) + { + gn.delay_ms = atoi(delay_str); + } + else + { + gn.delay_ms = 1000; + } + + /* 读取 SR/RS 的 R/S 属性 */ + const char *r_attr = gate_el->Attribute("R"); + const char *s_attr = gate_el->Attribute("S"); + + if (r_attr) + { + strncpy(gn.rs_r_src, r_attr, sizeof(gn.rs_r_src) - 1); + } + if (s_attr) + { + strncpy(gn.rs_s_src, s_attr, sizeof(gn.rs_s_src) - 1); + } + + int is_srrs = (gn.type == NODE_TYPE_SR_LATCH || gn.type == NODE_TYPE_RS_LATCH); + + /* 收集嵌套 Comb 的 Gate 索引 (按 id 映射, 用于 R/S 解析) */ + int sub_gate_by_comb_id[16] = {-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1}; + int sub_gate_count = 0; + + for (tinyxml2::XMLElement *nc = comb_el->FirstChildElement("Comb"); + nc; nc = nc->NextSiblingElement("Comb")) + { + int up = parse_xml_comb(nc, graph); + + if (up >= 0) + { + int cid = nc->IntAttribute("id"); + + if (cid >= 0 && cid < 16) + { + sub_gate_by_comb_id[cid] = up; + } + + sub_gate_count++; + + /* 非 SR/RS: 自动创建嵌套 Comb Gate → 当前 Gate 的链接 */ + if (!is_srrs && graph->link_count < MAX_LINKS_PER_GRAPH) + { + LogicLink &l = graph->links[graph->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = graph->nodes[up].type; + l.src_id = graph->nodes[up].id; + l.dest_type = gn.type; + l.dest_id = gn.id; + } + } + } + + /* SR/RS: 根据 R/S 属性创建链接 */ + if (is_srrs) + { + /* 解析 "Comb#N" 或 "Input#N" */ + for (int pin = 0; pin < 2; pin++) + { + const char *attr = (pin == 0) ? s_attr : r_attr; + + if (!attr || graph->link_count >= MAX_LINKS_PER_GRAPH) + { + continue; + } + + if (strncmp(attr, "Comb#", 5) == 0) + { + int cid = atoi(attr + 5); + + if (cid >= 0 && cid < 16 && sub_gate_by_comb_id[cid] >= 0) + { + int up = sub_gate_by_comb_id[cid]; + LogicLink &l = graph->links[graph->link_count++]; + memset(&l, 0, sizeof(l)); + l.src_type = graph->nodes[up].type; + l.src_id = graph->nodes[up].id; + l.dest_type = gn.type; + l.dest_id = gn.id; + + /* SR: S=pin0, R=pin1; RS: S=pin1, R=pin0 */ + if (gn.type == NODE_TYPE_SR_LATCH) + { + l.dst_pin = pin; + } + else + { + l.dst_pin = 1 - pin; + } + } + } + else if (strncmp(attr, "Input#", 6) == 0) + { + int ino = atoi(attr + 6); + int si = ensure_signal(graph, NODE_TYPE_HW_INPUT, ino); + + if (si >= 0) + { + LogicLink &l = graph->links[graph->link_count++]; + memset(&l, 0, sizeof(l)); + l.src_type = NODE_TYPE_HW_INPUT; + l.src_id = graph->nodes[si].id; + l.dest_type = gn.type; + l.dest_id = gn.id; + + if (gn.type == NODE_TYPE_SR_LATCH) + { + l.dst_pin = pin; + } + else + { + l.dst_pin = 1 - pin; + } + } + } + } + } + + tinyxml2::XMLElement *ins = comb_el->FirstChildElement("Inputs"); + + if (ins && !is_srrs) + { + for (tinyxml2::XMLElement *sig = ins->FirstChildElement("Signal"); + sig; sig = sig->NextSiblingElement("Signal")) + { + if (graph->link_count >= MAX_LINKS_PER_GRAPH) + { + break; + } + + int si = ensure_signal(graph, NODE_TYPE_HW_INPUT, sig->IntAttribute("no")); + + if (si >= 0) + { + LogicLink &l = graph->links[graph->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = NODE_TYPE_HW_INPUT; + l.src_id = graph->nodes[si].id; + l.dest_type = gn.type; + l.dest_id = gn.id; + } + } + } + + tinyxml2::XMLElement *ous = comb_el->FirstChildElement("Outputs"); + + if (ous) + { + for (tinyxml2::XMLElement *sig = ous->FirstChildElement("Signal"); + sig; sig = sig->NextSiblingElement("Signal")) + { + if (graph->link_count >= MAX_LINKS_PER_GRAPH) + { + break; + } + + int si = ensure_signal(graph, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no")); + + if (si >= 0) + { + LogicLink &l = graph->links[graph->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = gn.type; + l.src_id = gn.id; + l.dest_type = NODE_TYPE_HW_OUTPUT; + l.dest_id = graph->nodes[si].id; + } + } + } + + return gi; +} + +int parse_reclose_logic_xml(const char *file_path, LogicGraph graphs[], int *graph_count) +{ + tinyxml2::XMLDocument doc; + + if (doc.LoadFile(file_path) != tinyxml2::XML_SUCCESS) + { + LOG_E("XML配置文件解析失败: %s", file_path); + return -1; + } + + tinyxml2::XMLElement *root = doc.FirstChildElement("PLCConfig"); + + if (NULL == root) + { + LOG_E("XML根元素未找到"); + return -1; + } + + *graph_count = 0; + LogicGraph *cur = &graphs[0]; + memset(cur, 0, sizeof(LogicGraph)); + + const char *ka = root->Attribute("key"); + const char *na = root->Attribute("name"); + + if (ka) + { + strncpy(cur->key, ka, sizeof(cur->key) - 1); + } + if (na) + { + strncpy(cur->name, na, sizeof(cur->name) - 1); + } + + /* 第一遍: 收集所有节点 */ + for (tinyxml2::XMLElement *chain = root->FirstChildElement("Chain"); + chain; chain = chain->NextSiblingElement("Chain")) + { + tinyxml2::XMLElement *inputs = chain->FirstChildElement("Inputs"); + + if (inputs) + { + for (tinyxml2::XMLElement *sig = inputs->FirstChildElement("Signal"); + sig; sig = sig->NextSiblingElement("Signal")) + { + ensure_signal(cur, NODE_TYPE_HW_INPUT, sig->IntAttribute("no")); + } + } + + for (tinyxml2::XMLElement *cb = chain->FirstChildElement("Comb"); + cb; cb = cb->NextSiblingElement("Comb")) + { + parse_xml_comb(cb, cur); + } + + for (tinyxml2::XMLElement *gate = chain->FirstChildElement("Gate"); + gate; gate = gate->NextSiblingElement("Gate")) + { + if (cur->node_count >= MAX_NODES_PER_GRAPH) + { + break; + } + + LogicNode &gn = cur->nodes[cur->node_count++]; + memset(&gn, 0, sizeof(gn)); + gn.timer_pending_output = -1; + gn.type = gate_type_from_xml_str(gate->Attribute("type")); + gn.id = gate->IntAttribute("id"); + } + + tinyxml2::XMLElement *outputs = chain->FirstChildElement("Outputs"); + + if (outputs) + { + for (tinyxml2::XMLElement *sig = outputs->FirstChildElement("Signal"); + sig; sig = sig->NextSiblingElement("Signal")) + { + ensure_signal(cur, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no")); + } + } + } + + /* 第二遍: Comb 输出 → 链级 Gate, Gate → Outputs */ + for (tinyxml2::XMLElement *chain = root->FirstChildElement("Chain"); + chain; chain = chain->NextSiblingElement("Chain")) + { + bool has_comb = (chain->FirstChildElement("Comb") != NULL); + + /* 收集链级 Gate (不在 Comb 内) */ + tinyxml2::XMLElement *gate_el = NULL; + tinyxml2::XMLElement *ins_el = NULL; + tinyxml2::XMLElement *ous_el = NULL; + tinyxml2::XMLElement *c = chain->FirstChildElement(); + + while (c) + { + if (strcmp(c->Value(), "Gate") == 0) + { + gate_el = c; + } + if (strcmp(c->Value(), "Inputs") == 0) + { + ins_el = c; + } + if (strcmp(c->Value(), "Outputs") == 0) + { + ous_el = c; + } + + c = c->NextSiblingElement(); + } + + if (has_comb) + { + /* 收集链级 Gate 的 R/S 属性 (RS/SR 用) */ + const char *gateS = NULL; + const char *gateR = NULL; + int isChainSRRS = 0; + + if (gate_el) + { + int cgtTmp = gate_type_from_xml_str(gate_el->Attribute("type")); + + if (cgtTmp == NODE_TYPE_SR_LATCH || cgtTmp == NODE_TYPE_RS_LATCH) + { + isChainSRRS = cgtTmp; + gateS = gate_el->Attribute("S"); + gateR = gate_el->Attribute("R"); + } + } + + /* 收集所有 Comb 的 Gate 索引 (第一遍已解析) → Comb Gate → 链级 Gate */ + if (gate_el) + { + int cgt = gate_type_from_xml_str(gate_el->Attribute("type")); + int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id")); + + if (cgi >= 0) + { + /* 遍历所有顶层 Comb Gate */ + for (c = chain->FirstChildElement("Comb"); + c; c = c->NextSiblingElement("Comb")) + { + tinyxml2::XMLElement *cg = c->FirstChildElement("Gate"); + + if (!cg) + { + continue; + } + + int ct = gate_type_from_xml_str(cg->Attribute("type")); + int ci = find_node_index(cur, ct, cg->IntAttribute("id")); + + if (ci >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH) + { + LogicLink &l = cur->links[cur->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = cur->nodes[ci].type; + l.src_id = cur->nodes[ci].id; + l.dest_type = cur->nodes[cgi].type; + l.dest_id = cur->nodes[cgi].id; + + /* RS/SR: 根据 R/S 属性设置 dst_pin */ + if (isChainSRRS) + { + int combId = c->IntAttribute("id"); + char combRef[16]; + snprintf(combRef, sizeof(combRef), "Comb#%d", combId); + + if (gateR && strcmp(gateR, combRef) == 0) + { + l.dst_pin = (isChainSRRS == NODE_TYPE_SR_LATCH) ? 1 : 0; + } + else if (gateS && strcmp(gateS, combRef) == 0) + { + l.dst_pin = (isChainSRRS == NODE_TYPE_SR_LATCH) ? 0 : 1; + } + } + } + } + } + } + + if (gate_el && ous_el) + { + int cgt = gate_type_from_xml_str(gate_el->Attribute("type")); + int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id")); + + if (cgi >= 0) + { + for (tinyxml2::XMLElement *sig = ous_el->FirstChildElement("Signal"); + sig; sig = sig->NextSiblingElement("Signal")) + { + if (cur->link_count >= MAX_LINKS_PER_GRAPH) + { + break; + } + + int si = find_node_index(cur, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no")); + + if (si >= 0) + { + LogicLink &l = cur->links[cur->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = cur->nodes[cgi].type; + l.src_id = cur->nodes[cgi].id; + l.dest_type = NODE_TYPE_HW_OUTPUT; + l.dest_id = cur->nodes[si].id; + } + } + } + } + + /* 链级 Inputs → 链级 Gate (RS/SR 直连 Input 信号) */ + if (gate_el && ins_el) + { + int cgt = gate_type_from_xml_str(gate_el->Attribute("type")); + int cgi = find_node_index(cur, cgt, gate_el->IntAttribute("id")); + + if (cgi >= 0) + { + for (tinyxml2::XMLElement *sig = ins_el->FirstChildElement("Signal"); + sig; sig = sig->NextSiblingElement("Signal")) + { + if (cur->link_count >= MAX_LINKS_PER_GRAPH) + { + break; + } + + int si = find_node_index(cur, NODE_TYPE_HW_INPUT, sig->IntAttribute("no")); + + if (si >= 0) + { + LogicLink &l = cur->links[cur->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = NODE_TYPE_HW_INPUT; + l.src_id = cur->nodes[si].id; + l.dest_type = cur->nodes[cgi].type; + l.dest_id = cur->nodes[cgi].id; + + /* RS/SR: 根据 R/S 属性设置 dst_pin */ + if (isChainSRRS) + { + char inRef[16]; + snprintf(inRef, sizeof(inRef), "Input#%d", sig->IntAttribute("no")); + + if (gateR && strcmp(gateR, inRef) == 0) + { + l.dst_pin = (isChainSRRS == NODE_TYPE_SR_LATCH) ? 1 : 0; + } + else if (gateS && strcmp(gateS, inRef) == 0) + { + l.dst_pin = (isChainSRRS == NODE_TYPE_SR_LATCH) ? 0 : 1; + } + } + } + } + } + } + + /* RS/SR 的 R/S 属性引用的 Input 信号 (仅当无 块时创建, 避免重复) */ + if (gate_el && !ins_el) + { + int cgt = gate_type_from_xml_str(gate_el->Attribute("type")); + + if (cgt == NODE_TYPE_SR_LATCH || cgt == NODE_TYPE_RS_LATCH) + { + const char *s_attr2 = gate_el->Attribute("S"); + const char *r_attr2 = gate_el->Attribute("R"); + int cgi2 = find_node_index(cur, cgt, gate_el->IntAttribute("id")); + + if (cgi2 >= 0) + { + if (s_attr2 && strncmp(s_attr2, "Input#", 6) == 0) + { + int ino = atoi(s_attr2 + 6); + int si = ensure_signal(cur, NODE_TYPE_HW_INPUT, ino); + + if (si >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH) + { + LogicLink &l = cur->links[cur->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = NODE_TYPE_HW_INPUT; + l.src_id = cur->nodes[si].id; + l.dest_type = cur->nodes[cgi2].type; + l.dest_id = cur->nodes[cgi2].id; + l.dst_pin = (cgt == NODE_TYPE_SR_LATCH) ? 0 : 1; + } + } + + if (r_attr2 && strncmp(r_attr2, "Input#", 6) == 0) + { + int ino = atoi(r_attr2 + 6); + int si = ensure_signal(cur, NODE_TYPE_HW_INPUT, ino); + + if (si >= 0 && cur->link_count < MAX_LINKS_PER_GRAPH) + { + LogicLink &l = cur->links[cur->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = NODE_TYPE_HW_INPUT; + l.src_id = cur->nodes[si].id; + l.dest_type = cur->nodes[cgi2].type; + l.dest_id = cur->nodes[cgi2].id; + l.dst_pin = (cgt == NODE_TYPE_SR_LATCH) ? 1 : 0; + } + } + } + } + } + } + else + { + /* 简单链: Inputs → Gate → Outputs */ + if (!gate_el) + { + continue; + } + + int gt = gate_type_from_xml_str(gate_el->Attribute("type")); + int gi = find_node_index(cur, gt, gate_el->IntAttribute("id")); + + if (gi < 0) + { + continue; + } + + if (ins_el) + { + for (tinyxml2::XMLElement *sig = ins_el->FirstChildElement("Signal"); + sig; sig = sig->NextSiblingElement("Signal")) + { + if (cur->link_count >= MAX_LINKS_PER_GRAPH) + { + break; + } + + int si = find_node_index(cur, NODE_TYPE_HW_INPUT, sig->IntAttribute("no")); + + if (si >= 0) + { + LogicLink &l = cur->links[cur->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = NODE_TYPE_HW_INPUT; + l.src_id = cur->nodes[si].id; + l.dest_type = cur->nodes[gi].type; + l.dest_id = cur->nodes[gi].id; + + /* SR/RS: 根据 R/S 属性设置 dst_pin */ + if (gt == NODE_TYPE_SR_LATCH || gt == NODE_TYPE_RS_LATCH) + { + const char *sA = gate_el->Attribute("S"); + const char *rA = gate_el->Attribute("R"); + char inRef[16]; + snprintf(inRef, sizeof(inRef), "Input#%d", sig->IntAttribute("no")); + + if (rA && strcmp(rA, inRef) == 0) + { + l.dst_pin = (gt == NODE_TYPE_SR_LATCH) ? 1 : 0; + } + else if (sA && strcmp(sA, inRef) == 0) + { + l.dst_pin = (gt == NODE_TYPE_SR_LATCH) ? 0 : 1; + } + } + } + } + } + + if (ous_el) + { + for (tinyxml2::XMLElement *sig = ous_el->FirstChildElement("Signal"); + sig; sig = sig->NextSiblingElement("Signal")) + { + if (cur->link_count >= MAX_LINKS_PER_GRAPH) + { + break; + } + + int si = find_node_index(cur, NODE_TYPE_HW_OUTPUT, sig->IntAttribute("no")); + + if (si >= 0) + { + LogicLink &l = cur->links[cur->link_count++]; + memset(&l, 0, sizeof(l)); + l.dst_pin = -1; + l.src_type = cur->nodes[gi].type; + l.src_id = cur->nodes[gi].id; + l.dest_type = NODE_TYPE_HW_OUTPUT; + l.dest_id = cur->nodes[si].id; + } + } + } + } + } + + *graph_count = 1; + LOG_I("XML配置解析成功: %d节点 %d连线", cur->node_count, cur->link_count); + return 0; +} + +/* ================================================================== + * 逻辑运算函数 (10 种门) + * ================================================================== */ + +static int collect_input_signals(LogicGraph *graph, LogicNode *element_node, + int *input_values, int max_inputs) { + int input_count = 0; + for (int i = 0; i < graph->link_count && input_count < max_inputs; i++) + { + LogicLink *link = &graph->links[i]; + if (link->dest_type == element_node->type && + ((link->dest_id & 0xFF) == (element_node->id & 0xFF))) { + LogicNode *src_node = find_node(graph, link->src_type, link->src_id); + if (src_node) + { + // SR/RS: 按 dst_pin 填入对应位置 + int is_srrs = (element_node->type == NODE_TYPE_SR_LATCH || + element_node->type == NODE_TYPE_RS_LATCH); + if (is_srrs && link->dst_pin >= 0) + { + if (input_count < 2) + { + input_values[link->dst_pin] = src_node->value; + if (link->dst_pin >= input_count) + { + input_count = link->dst_pin + 1; + } + } + } + else + { + input_values[input_count++] = src_node->value; + } + PLC_DBG(" 找到输入信号:%d:%d = %d (pin=%d)\n", + link->src_type, link->src_id, src_node->value, link->dst_pin); + } + } + } + return input_count; +} + +static void execute_not_gate(LogicNode *not_node, int input_value) +{ + not_node->value = !input_value; + PLC_DBG(" 非门运算:输入=%d → 输出=%d\n", input_value, not_node->value); +} + +static void execute_and_gate(LogicNode *and_node, int *input_values, int input_count) +{ + and_node->value = (input_count == 0) ? 0 : 1; + for (int i = 0; i < input_count; i++) + { + and_node->value = and_node->value && input_values[i]; + } + PLC_DBG(" 与门运算:输入=["); + for (int i = 0; i < input_count; i++) + { + PLC_DBG("%d%s", input_values[i], (i < input_count - 1) ? "," : ""); + } + PLC_DBG("] → 输出=%d\n", and_node->value); +} + +static void execute_or_gate(LogicNode *or_node, int *input_values, int input_count) +{ + or_node->value = 0; + for (int i = 0; i < input_count; i++) + { + or_node->value = or_node->value || input_values[i]; + } + PLC_DBG(" 或门运算:输入=["); + for (int i = 0; i < input_count; i++) + { + PLC_DBG("%d%s", input_values[i], (i < input_count - 1) ? "," : ""); + } + PLC_DBG("] → 输出=%d\n", or_node->value); +} + +static void execute_62p_gate(LogicNode *node, int input_value) +{ + int delay_ms = node->delay_ms > 0 ? node->delay_ms : 1000; + int component_id = node->id & 0xFF; + if (input_value == 1) + { + if (!node->timer_active) + { + // 启动延时定时器,延时 delay_ms 毫秒后输出 1 + PLC_DBG(" 62p延时元件 (ID=%d, 序号=%d):输入=1,启动非阻塞延时%d毫秒\n", + node->id, component_id, delay_ms); + clock_gettime(CLOCK_MONOTONIC, &node->timer_start); + node->timer_delay_ms = delay_ms; + node->timer_pending_output = 1; + node->timer_active = 1; + // 保持当前输出值不变,等待定时器到期 + } + else if (get_elapsed_ms(&node->timer_start) >= node->timer_delay_ms) + { + // 定时器到期,输出 1 + node->value = 1; + node->timer_active = 0; + node->timer_pending_output = -1; + PLC_DBG(" 62p延时元件 (ID=%d, 序号=%d):延时结束,输出=1\n", + node->id, component_id); + } + // 否则仍在延时中,保持当前值 + } + else + { + // 输入为 0,立即输出 0 并取消定时器 + node->value = 0; + node->timer_active = 0; + node->timer_pending_output = -1; + } +} + +static void execute_62d_gate(LogicNode *node, int input_value) +{ + int delay_ms = node->delay_ms > 0 ? node->delay_ms : 1000; + int component_id = node->id & 0xFF; + if (input_value == 1) + { + // 输入为 1,立即输出 1 并取消定时器 + node->value = 1; + node->timer_active = 0; + node->timer_pending_output = -1; + PLC_DBG(" 62d延时元件 (ID=%d, 序号=%d):输入=1,立即输出=1\n", + node->id, component_id); + } + else + { + if (!node->timer_active) + { + // 启动延时定时器,延时 delay_ms 毫秒后输出 0 + PLC_DBG(" 62d延时元件 (ID=%d, 序号=%d):输入=0,启动非阻塞延时%d毫秒\n", + node->id, component_id, delay_ms); + clock_gettime(CLOCK_MONOTONIC, &node->timer_start); + node->timer_delay_ms = delay_ms; + node->timer_pending_output = 0; + node->timer_active = 1; + // 保持当前输出值不变,等待定时器到期 + } + else if (get_elapsed_ms(&node->timer_start) >= node->timer_delay_ms) + { + // 定时器到期,输出 0 + node->value = 0; + node->timer_active = 0; + node->timer_pending_output = -1; + PLC_DBG(" 62d延时元件 (ID=%d, 序号=%d):延时结束,输出=0\n", + node->id, component_id); + } + // 否则仍在延时中,保持当前值 + } +} + +static void execute_rising_edge_gate(LogicNode *node, int input_value) +{ + int component_id = node->id & 0xFF; + if (node->last_input == 0 && input_value == 1) + { + PLC_DBG(" 上升沿元件 (ID=%d, 序号=%d):检测到上升沿,输出脉冲\n", + node->id, component_id); + node->value = 1; + } + else + { + node->value = 0; + } + node->last_input = input_value; +} + +static void execute_falling_edge_gate(LogicNode *node, int input_value) +{ + int component_id = node->id & 0xFF; + if (node->last_input == 1 && input_value == 0) + { + PLC_DBG(" 下降沿元件 (ID=%d, 序号=%d):检测到下降沿,输出脉冲\n", + node->id, component_id); + node->value = 1; + } + else + { + node->value = 0; + } + node->last_input = input_value; +} + +static void execute_sr_latch(LogicNode *node, int *input_values, int input_count) +{ + int component_id = node->id & 0xFF; + int s = (input_count >= 1) ? input_values[0] : 0; + int r = (input_count >= 2) ? input_values[1] : 0; + PLC_DBG(" SR触发器 (ID=%d, 序号=%d):S=%d, R=%d\n", node->id, component_id, s, r); + if (s == 1) + { + node->value = 1; + PLC_DBG(" SR触发器:S=1,置位,输出=1\n"); + } + else if (r == 1) + { + node->value = 0; + PLC_DBG(" SR触发器:S=0, R=1,复位,输出=0\n"); + } +} + +static void execute_rs_latch(LogicNode *node, int *input_values, int input_count) +{ + int component_id = node->id & 0xFF; + int r = (input_count >= 1) ? input_values[0] : 0; + int s = (input_count >= 2) ? input_values[1] : 0; + PLC_DBG(" RS触发器 (ID=%d, 序号=%d):R=%d, S=%d\n", node->id, component_id, r, s); + if (r == 1) + { + node->value = 0; + PLC_DBG(" RS触发器:R=1,复位,输出=0\n"); + } + else if (s == 1) + { + node->value = 1; + PLC_DBG(" RS触发器:R=0, S=1,置位,输出=1\n"); + } +} + +/* ================================================================== + * 拓扑排序 (Kahn 算法) + * ================================================================== */ +// 对逻辑图的节点进行拓扑排序,确保节点按依赖顺序执行 +// 返回值:排好序的节点索引数组,由调用者通过 sorted_indices 传出 +// 返回实际能排序的节点数(可能小于 node_count 如果有环) +static int topological_sort(LogicGraph *graph, int *sorted_indices) +{ + if (!graph || !sorted_indices) + { + return 0; + } + + int n = graph->node_count; + + if (n == 0) + { + return 0; + } + + // 步骤1:计算每个节点的入度(从非硬件输入点的源节点连接过来的链路数) + for (int i = 0; i < n; i++) + { + graph->nodes[i].indegree = 0; + } + for (int i = 0; i < graph->link_count; i++) + { + LogicLink *link = &graph->links[i]; + // 找到目标节点 + LogicNode *dest = find_node(graph, link->dest_type, link->dest_id); + if (dest) + { + // 找到源节点(如果不是硬件输入点,则增加目标节点的入度) + LogicNode *src = find_node(graph, link->src_type, link->src_id); + if (src && src->type != NODE_TYPE_HW_INPUT) + { + dest->indegree++; + } + } + } + + // 步骤2:Kahn算法 + // 将所有入度为0的节点入队(硬件输入点入度始终为0) + int queue[MAX_NODES_PER_GRAPH]; + int q_head = 0, q_tail = 0; + for (int i = 0; i < n; i++) + { + if (graph->nodes[i].indegree == 0) + { + queue[q_tail++] = i; + } + } + + int sorted_count = 0; + while (q_head < q_tail) + { + int idx = queue[q_head++]; + sorted_indices[sorted_count++] = idx; + + // 找到当前节点的所有后继节点,减少它们的入度 + for (int i = 0; i < graph->link_count; i++) + { + LogicLink *link = &graph->links[i]; + LogicNode *src = find_node(graph, link->src_type, link->src_id); + if (src == &graph->nodes[idx]) + { + LogicNode *dest = find_node(graph, link->dest_type, link->dest_id); + if (dest) + { + dest->indegree--; + if (dest->indegree == 0) + { + queue[q_tail++] = (dest - graph->nodes); + } + } + } + } + } + + if (sorted_count < n) + { + LOG_E("逻辑图[%s]存在环路依赖,仅排序了 %d/%d 个节点", + graph->key, sorted_count, n); + } + return sorted_count; +} + +/* ================================================================== + * 逻辑图执行 + * ================================================================== */ +int execute_logic_graph(LogicGraph *graph, HardwareManager *hw) +{ + if (!graph || !hw) + { + return -1; + } + + PLC_DBG("\n=====================================================\n"); + PLC_DBG("执行逻辑图:%s (%s)\n", graph->key, graph->name); + PLC_DBG("=====================================================\n"); + + // 步骤1:初始化节点值 + PLC_DBG("\n【节点初始化】\n"); + for (int i = 0; i < graph->node_count; i++) + { + LogicNode *node = &graph->nodes[i]; + const char *type_name = ""; + switch (node->type) { + case NODE_TYPE_HW_INPUT: + node->value = get_hw_point_value(hw->input_head, node->id); + type_name = "硬件输入点"; break; + case NODE_TYPE_HW_OUTPUT: type_name = "硬件输出点"; break; + case NODE_TYPE_OR_GATE: type_name = "或门元件"; break; + case NODE_TYPE_AND_GATE: type_name = "与门元件"; break; + case NODE_TYPE_NOT_GATE: type_name = "非门元件"; break; + case NODE_TYPE_62P_GATE: type_name = "62p延时元件"; break; + case NODE_TYPE_62D_GATE: type_name = "62d延时元件"; break; + case NODE_TYPE_RISING_EDGE: type_name = "上升沿元件"; break; + case NODE_TYPE_SR_LATCH: type_name = "SR触发器"; break; + case NODE_TYPE_RS_LATCH: type_name = "RS触发器"; break; + case NODE_TYPE_FALLING_EDGE: type_name = "下降沿元件"; break; + default: type_name = "未知节点"; break; + } + PLC_DBG(" %s (类型=%d, ID=%d): 初始值=%d\n", + type_name, node->type, node->id, node->value); + } + + // 步骤2:按拓扑顺序执行逻辑元件运算 + PLC_DBG("\n【逻辑元件运算】(拓扑排序执行)\n"); + int sorted_indices[MAX_NODES_PER_GRAPH]; + int sorted_count = topological_sort(graph, sorted_indices); + for (int si = 0; si < sorted_count; si++) + { + LogicNode *node = &graph->nodes[sorted_indices[si]]; + int input_values[10]; + int input_count; + switch (node->type) { + case NODE_TYPE_NOT_GATE: + PLC_DBG("\n 处理非门元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + if (input_count > 0) + { + execute_not_gate(node, input_values[0]); + } + else + { + PLC_DBG(" 警告:非门元件没有输入信号\n"); node->value = 0; } + break; + case NODE_TYPE_OR_GATE: + PLC_DBG("\n 处理或门元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_or_gate(node, input_values, input_count); + break; + case NODE_TYPE_AND_GATE: + PLC_DBG("\n 处理与门元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_and_gate(node, input_values, input_count); + break; + case NODE_TYPE_62P_GATE: + PLC_DBG("\n 处理62p延时元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + if (input_count > 0) + { + execute_62p_gate(node, input_values[0]); + } + else + { + PLC_DBG(" 警告:62p延时元件没有输入信号\n"); node->value = 0; } + break; + case NODE_TYPE_62D_GATE: + PLC_DBG("\n 处理62d延时元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + if (input_count > 0) + { + execute_62d_gate(node, input_values[0]); + } + else + { + PLC_DBG(" 警告:62d延时元件没有输入信号\n"); node->value = 0; } + break; + case NODE_TYPE_RISING_EDGE: + PLC_DBG("\n 处理上升沿元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + if (input_count > 0) + { + execute_rising_edge_gate(node, input_values[0]); + } + else + { + PLC_DBG(" 警告:上升沿元件没有输入信号\n"); node->value = 0; } + break; + case NODE_TYPE_FALLING_EDGE: + PLC_DBG("\n 处理下降沿元件 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + if (input_count > 0) + { + execute_falling_edge_gate(node, input_values[0]); + } + else + { + PLC_DBG(" 警告:下降沿元件没有输入信号\n"); node->value = 0; } + break; + case NODE_TYPE_SR_LATCH: + PLC_DBG("\n 处理SR触发器 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_sr_latch(node, input_values, input_count); + break; + case NODE_TYPE_RS_LATCH: + PLC_DBG("\n 处理RS触发器 (ID=%d)\n", node->id); + input_count = collect_input_signals(graph, node, input_values, 10); + execute_rs_latch(node, input_values, input_count); + break; + } + } + + // 步骤3:处理输出链路 + PLC_DBG("\n【输出链路处理】\n"); + for (int i = 0; i < graph->link_count; i++) + { + LogicLink *link = &graph->links[i]; + LogicNode *src_node = find_node(graph, link->src_type, link->src_id); + LogicNode *dest_node = find_node(graph, link->dest_type, link->dest_id); + if (src_node && dest_node && dest_node->type == NODE_TYPE_HW_OUTPUT) + { + dest_node->value = src_node->value; + set_hw_point_value(&hw->output_head, &hw->output_count, + dest_node->id, dest_node->value); + + if (0 == plc_set_out_by_id(dest_node->id, src_node->value)) + { + PLC_DBG(" 硬件输出:ID=%d = %d(已同步到datacenter)\n", + dest_node->id, src_node->value); + } + else + { + PLC_DBG(" 硬件输出:ID=%d = %d(未找到datacenter信号,跳过写入)\n", + dest_node->id, dest_node->value); + } + } + } + + PLC_DBG("\n【逻辑图执行结果】\n"); + for (int i = 0; i < graph->node_count; i++) + { + LogicNode *node = &graph->nodes[i]; + const char *type_name = ""; + switch (node->type) { + case NODE_TYPE_HW_INPUT: type_name = "硬件输入点"; break; + case NODE_TYPE_HW_OUTPUT: type_name = "硬件输出点"; break; + case NODE_TYPE_OR_GATE: type_name = "或门元件"; break; + case NODE_TYPE_AND_GATE: type_name = "与门元件"; break; + case NODE_TYPE_NOT_GATE: type_name = "非门元件"; break; + case NODE_TYPE_62P_GATE: type_name = "62p延时元件"; break; + case NODE_TYPE_62D_GATE: type_name = "62d延时元件"; break; + case NODE_TYPE_RISING_EDGE: type_name = "上升沿元件"; break; + case NODE_TYPE_SR_LATCH: type_name = "SR触发器"; break; + case NODE_TYPE_RS_LATCH: type_name = "RS触发器"; break; + case NODE_TYPE_FALLING_EDGE: type_name = "下降沿元件"; break; + default: type_name = "未知节点"; break; + } + PLC_DBG(" %s (ID=%d): 最终值=%d\n", type_name, node->id, node->value); + } + return 0; +} + +/* ================================================================== + * PLC 逻辑入口 + * ================================================================== */ +int PLC_Logic(void) +{ + // 检查配置文件是否变化,仅在文件修改时重新解析 + struct stat file_stat; + if (stat(g_plc_config_path, &file_stat) != 0) + { + LOG_E("无法获取PLC配置文件状态: %s", g_plc_config_path); + // 如果之前有缓存,继续使用缓存执行 + if (g_cached_graph_count == 0) + { + return 2; + } + // fall through to use cached graphs + } + else + { + if (file_stat.st_mtime == g_last_file_mtime && g_cached_graph_count > 0) + { + // 文件未变化,直接使用缓存的逻辑图执行 + HardwareManager hw; + init_hardware_manager(&hw); + extract_hw_points_from_graphs(g_cached_graphs, g_cached_graph_count, &hw, 0); + + for (int i = 0; i < g_cached_graph_count; i++) + { + execute_logic_graph(&g_cached_graphs[i], &hw); + } + + /* 同步输出到 datacenter out 信号 */ + HWPoint *curr = hw.output_head; + while (curr) + { + plc_set_out_by_id(curr->id, curr->value); + curr = curr->next; + } + destroy_hardware_manager(&hw); + return 0; + } + g_last_file_mtime = file_stat.st_mtime; + } + + // 文件已变化或首次加载,重新解析 XML + int graph_count = 0; + int parse_ok = parse_reclose_logic_xml(g_plc_config_path, g_cached_graphs, &graph_count); + if (parse_ok != 0) + { + // 解析失败但之前有缓存,继续使用缓存 + if (g_cached_graph_count > 0) + { + LOG_E("PLC配置文件重新解析失败,继续使用上次缓存(%d个逻辑图)", + g_cached_graph_count); + graph_count = g_cached_graph_count; // 使用旧缓存 + } + else + { + return 2; + } + } + else + { + g_cached_graph_count = graph_count; + } + + HardwareManager hw; + init_hardware_manager(&hw); + + extract_hw_points_from_graphs(g_cached_graphs, graph_count, &hw, 0); + + PLC_DBG("==================== 文件解析结果 ====================\n"); + PLC_DBG("配置文件: %s\n", g_plc_config_path); + PLC_DBG("共解析到 %d 个逻辑图\n", graph_count); + for (int i = 0; i < graph_count; i++) + { + PLC_DBG(" 逻辑图%d:key=%s, name=%s, 节点数=%d, 链路数=%d\n", + i + 1, g_cached_graphs[i].key, g_cached_graphs[i].name, + g_cached_graphs[i].node_count, g_cached_graphs[i].link_count); + } + print_hardware_manager(&hw); + + for (int i = 0; i < graph_count; i++) + { + execute_logic_graph(&g_cached_graphs[i], &hw); + } + + PLC_DBG("\n==================== 最终硬件输出状态 ====================\n"); + PLC_DBG("硬件输出点(dev_in)最终值:\n"); + HWPoint *curr = hw.output_head; + int idx = 1; + while (curr) + { + PLC_DBG(" %d. ID=%d: %d\n", idx++, curr->id, curr->value); + curr = curr->next; + } + + destroy_hardware_manager(&hw); + return 0; +} + +/* ================================================================== + * 动态配置路径 + * ================================================================== */ +// 设置PLC配置文件路径(支持运行时动态修改) +void plc_set_config_path(const char *path) +{ + if (path && path[0] != '\0') + { + strncpy(g_plc_config_path, path, sizeof(g_plc_config_path) - 1); + g_plc_config_path[sizeof(g_plc_config_path) - 1] = '\0'; + // 路径变更后重置缓存,强制下次重新加载 + g_last_file_mtime = 0; + g_cached_graph_count = 0; + LOG_I("PLC配置路径已更新: %s", g_plc_config_path); + } +} + +// 获取当前PLC配置文件路径 +const char* plc_get_config_path(void) +{ + return g_plc_config_path; +} + +/* ================================================================== + * 模块初始化 & X-Macro 线程入口 + * ================================================================== */ +int app_plc_init1(void *arg) +{ + stru_app *p_app = (stru_app *)arg; + if (NULL == p_app) + { + LOG_E("app_plc_init1 arg null"); + return -1; + } + + // 动态设置 PLC 配置文件路径(proc_dir + 相对文件名) + char proc_dir[512] = {0}; + if (0 == func_proc_self_dir(proc_dir, sizeof(proc_dir))) + { + std::string plc_path = std::string(proc_dir) + g_plc_config_path; + plc_set_config_path(plc_path.c_str()); + } + + int ret = 0; + ret |= dc_signal_out("plc.run_cnt", "plc线程计数", DATA_TYPE_U32, &p_app->run_cnt, "plc"); + + for (int i = 0; i < MAX_OUT_NODES; i++) + { + char saddr[64]; + char desc[64]; + snprintf(saddr, sizeof(saddr), "plc.st.out.%d", i); + snprintf(desc, sizeof(desc), "plc输出%d", i); + ret |= dc_signal_out(saddr, desc, DATA_TYPE_U8, &g_plc_st_out[i], "plc"); + } + + if (ret != 0) + { + LOG_E("app_plc_init1 dc_signal_out failed"); + return -1; + } + + p_plc_cfg = self_ptl_cfg_get(); + if (nullptr == p_plc_cfg) + { + LOG_E("app_plc_init1 self_ptl_cfg_get failed"); + return -1; + } + + for (uint32_t i = 0; i < p_plc_cfg->st_vec.size(); i++) + { + stru_self_ptl_cfg_base *p_base = &p_plc_cfg->st_vec.at(i); + g_plc_cfg.push_back({(*p_base), NULL}); + } + + return 0; +} + +int app_plc_init2(void *arg) +{ + int ret = 0; + ret |= dc_signal_in("plc.run_cnt_in", "plc.run_cnt_in", "plc.run_cnt", (void **)&gp_run_cnt_in, "plc"); + + for (uint32_t i = 0; i < g_plc_cfg.size(); i++) + { + stru_plc_cfg *p = &g_plc_cfg[i]; + char saddr[64]; + char desc[64]; + snprintf(saddr, sizeof(saddr), "plc.st.in.%d", i); + snprintf(desc, sizeof(desc), "plc链接遥信%d", i); + ret |= dc_signal_in(saddr, desc, p->base.saddr.c_str(), (void **)&p->p_data, "plc"); + } + if (ret != 0) + { + LOG_E("app_plc_init2 dc_signal_in failed"); + return -1; + } + return 0; +} + +void *app_plc(void *arg) +{ + if (NULL == arg) + { + LOG_E("app_plc arg null"); + return NULL; + } + + stru_app *p_app = (stru_app *)arg; + uint32_t event; + + while (1) + { + task_event_recv(p_app->p_event, + EV_TIMER1 | EV_TIMER2 | EV_TIMER3, + TASK_EVENT_FLAG_OR | TASK_EVENT_FLAG_CLEAR, + TASK_EVENT_WAIT_FOREVER, + &event); + + if (event & EV_TIMER1) + { + } + if (event & EV_TIMER2) + { + } + if (event & EV_TIMER3) + { + p_app->run_cnt++; + PLC_Logic(); + } + } + return NULL; +} + +/* ================================================================== + * 控制台命令 — 调试开关 & 查看状态 + * ================================================================== */ + +/** + * @brief PLC 控制台命令补全回调 + */ +LOCAL void cmd_plc_complete(const char *buf, char ***completions, int *ncomp) +{ + static const char *subs[] = {"dbg", "info"}; + + cmd_sub_complete(buf, completions, ncomp, subs, sizeof(subs) / sizeof(subs[0])); +} + +/** + * @brief PLC 控制台命令 + * + * @details 子命令: + * - `plc dbg on` 打开 PLC 运行时打印 (控制台输出逻辑图执行详情) + * - `plc dbg off` 关闭 PLC 运行时打印 + * - `plc info` 查看 PLC 模块状态 (配置路径、缓存图数、调试开关) + */ +LOCAL void cmd_plc(int argc, char *argv[]) +{ + const char *usage = + "plc dbg on|off 打开/关闭 PLC 调试打印\n" + "plc info 查看 PLC 模块状态\n"; + + if (argc < 2) + { + printf("%s", usage); + return; + } + + if (0 == strcmp(argv[1], "dbg")) + { + if (argc < 3) + { + printf("当前 PLC 调试: %s\n", g_plc_dbg_enabled ? "开启" : "关闭"); + printf("usage: plc dbg on|off\n"); + return; + } + + if (0 == strcmp(argv[2], "on")) + { + g_plc_dbg_enabled = 1; + LOG_I("PLC 调试打印: 开启"); + } + else if (0 == strcmp(argv[2], "off")) + { + g_plc_dbg_enabled = 0; + LOG_I("PLC 调试打印: 关闭"); + } + else + { + printf("未知选项: %s\nusage: plc dbg on|off\n", argv[2]); + } + } + else if (0 == strcmp(argv[1], "info")) + { + printf("===== PLC Info =====\n"); + printf("配置文件 : %s\n", g_plc_config_path); + printf("缓存逻辑图数 : %d\n", g_cached_graph_count); + printf("上次 mtime : %ld\n", (long)g_last_file_mtime); + printf("调试打印 : %s\n", g_plc_dbg_enabled ? "开启" : "关闭"); + printf("self_ptl 遥信: %d 个关联\n", (int)g_plc_cfg.size()); + } + else + { + printf("未知子命令: %s\n%s", argv[1], usage); + } +} + +CMD_REGISTER_C("plc", cmd_plc, "PLC调试打印开关", cmd_plc_complete);