From b00b998ac17b4f780cf01426b88d37674dcbb8c4 Mon Sep 17 00:00:00 2001
From: ypc <15051963820@163.com>
Date: Fri, 10 Jul 2026 15:42:18 +0800
Subject: [PATCH] =?UTF-8?q?feat(libplc):=20PLC=20=E9=80=BB=E8=BE=91?=
=?UTF-8?q?=E5=BC=95=E6=93=8E=E6=A8=A1=E5=9D=97=20+=20=E6=96=87=E6=A1=A3?=
=?UTF-8?q?=E8=A1=A5=E5=85=A8=20+=20=E8=AE=A1=E5=88=92=E5=90=8C=E6=AD=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## libplc 模块 (新)
- src/system/libplc/inc/plc.h / src/plc.cpp: 1252 行, 原工程完整复刻
- 10 种逻辑门 (AND/OR/NOT/62P/62D/RISING/FALLING/SR/RS)
- Kahn 拓扑排序 + 非阻塞定时器 (CLOCK_MONOTONIC)
- 嵌套 Comb XML 递归解析 + R/S 引脚属性
- self_ptl 遥信联动 + datacenter out 信号输出
- dc_get_signal_info_by_id 封装为 plc_set/get_out_value_by_id
- printf → PLC_DBG 可控调试宏 + 'plc dbg on|off' 命令开关
- 代码规范: Tab 缩进/条件分支分行/大括号独占行/函数注释
- 接口适配: func_proc_self_dir/dc_signal_out module_id 对齐
- release/src/system/libplc/makefile + release 头文件
## 文档补全 (之前开发未写文档)
- docs/system/libiec/API-libiec.md (26 个 CS10x 回调, 架构, MQ 流)
- docs/system/libcom_router/API-libcom_router.md
- docs/system/libself_ptl/API-libself_ptl.md
- docs/protocol/libicp67/API-libicp67.md
## 计划同步
- PLAN.md: 阶段 0~4 全部回写状态 ⬜→✅
- COMPLETION.md: 清理陈旧 libiec ⬜ 条目
- docs/README.md: 新增 libiec/libcom_router/libself_ptl/libicp67 索引
- PLAN.md 3.4: 新增 if/大括号/多语句格式规范
- 工作流: 新增 '回写 PLAN.md' 步骤
## X-Macro 注册
- app_modules.h: APP_MODULE(PLC, ...)
- release/src/system/makefile: SUBDIRS 新增 ./libplc
---
COMPLETION.md | 1 -
PLAN.md | 80 +-
docs/README.md | 4 +
docs/protocol/libicp67/API-libicp67.md | 271 +++
.../system/libcom_router/API-libcom_router.md | 177 ++
docs/system/libiec/API-libiec.md | 257 +++
docs/system/libself_ptl/API-libself_ptl.md | 209 ++
release/src/system/libplc/makefile | 39 +
release/src/system/makefile | 2 +-
src/system/inc/app_modules.h | 1 +
src/system/libplc/inc/plc.h | 38 +
src/system/libplc/src/plc.cpp | 1917 +++++++++++++++++
12 files changed, 2964 insertions(+), 32 deletions(-)
create mode 100644 docs/protocol/libicp67/API-libicp67.md
create mode 100644 docs/system/libcom_router/API-libcom_router.md
create mode 100644 docs/system/libiec/API-libiec.md
create mode 100644 docs/system/libself_ptl/API-libself_ptl.md
create mode 100644 release/src/system/libplc/makefile
create mode 100644 src/system/libplc/inc/plc.h
create mode 100644 src/system/libplc/src/plc.cpp
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);