From a3ae78e5e8302ca99a732b999e9412cb910d1689 Mon Sep 17 00:00:00 2001
From: ypc <15051963820@163.com>
Date: Wed, 10 Jun 2026 13:33:53 +0800
Subject: [PATCH] =?UTF-8?q?<=E4=BF=AE=E6=94=B9>=201=E3=80=81=E8=B0=83?=
=?UTF-8?q?=E6=95=B4webserver=E6=A8=A1=E5=9D=97=EF=BC=8C=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?=E5=A4=9A=E8=BF=9E=E6=8E=A5=EF=BC=8C=E5=90=84=E8=BF=9E=E6=8E=A5?=
=?UTF-8?q?=E5=8D=95=E8=B5=84=E6=BA=90=EF=BC=8C=E8=BF=9E=E6=8E=A5=E6=96=AD?=
=?UTF-8?q?=E5=BC=80=E9=87=8A=E6=94=BE=E8=B5=84=E6=BA=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
claude/工程/libiec61850s模块分析.md | 273 ++++++++
claude/工程/libmms_s模块分析.md | 379 +++++++++++
claude/工程/libweb_server模块分析.md | 217 +++++++
claude/工程/websocket-server.md | 598 ++++++++++++++++++
claude/问题处理文档.md | 34 +
.../{libmoongoose => libmongoose}/makefile | 2 +-
release/src/protocol/makefile | 2 +-
release/src/system/RTU/makefile | 9 +-
.../inc/.gitkeep | 0
.../src/mongoose.c | 0
src/system/libweb_server/inc/ws_method.h | 10 +-
src/system/libweb_server/src/web_server.cpp | 119 +++-
src/system/libweb_server/src/ws_method.cpp | 357 +++++++----
13 files changed, 1832 insertions(+), 168 deletions(-)
create mode 100644 claude/工程/libiec61850s模块分析.md
create mode 100644 claude/工程/libmms_s模块分析.md
create mode 100644 claude/工程/libweb_server模块分析.md
create mode 100644 claude/工程/websocket-server.md
rename release/src/protocol/{libmoongoose => libmongoose}/makefile (95%)
rename src/protocol/{libmoongoose => libmongoose}/inc/.gitkeep (100%)
rename src/protocol/{libmoongoose => libmongoose}/src/mongoose.c (100%)
diff --git a/claude/工程/libiec61850s模块分析.md b/claude/工程/libiec61850s模块分析.md
new file mode 100644
index 0000000..04d61cf
--- /dev/null
+++ b/claude/工程/libiec61850s模块分析.md
@@ -0,0 +1,273 @@
+# libiec61850s 模块分析
+
+**日期**:2026-06-10
+
+---
+
+## 1. 模块概览
+
+`libiec61850s` 是 IEC 61850 MMS 服务端的应用线程模块(`app_iec61850s`),属于系统层的第 9 号线程。它作为桥接层,连接三个子系统:
+
+```
+┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
+│ DataCenter │ ←→ │ libiec61850s │ ←→ │ libmms_s │
+│ (信号数据中心) │ │ (应用线程/桥接层) │ │ (MMS 服务端封装) │
+└─────────────────┘ └──────────────────┘ └─────────────────┘
+ ↕
+ ┌──────────────────┐
+ │ mms_s.xml 配置 │
+ └──────────────────┘
+```
+
+### 目录结构
+
+```
+src/system/libiec61850s/
+├── inc/
+│ ├── iec61850s.h # 头文件(包含 mySystem/myMms_s 等)
+│ └── parse_xml.h # XML 配置解析类型 + 接口
+└── src/
+ ├── iec61850s.cpp # 应用线程主逻辑(~487 行)
+ └── parse_xml.cpp # mms_s.xml 解析(~129 行)
+```
+
+---
+
+## 2. 应用线程模型
+
+与所有 app 线程一致,libiec61850s 遵循 `init1 → init2 → fun_cb` 三段式:
+
+| 阶段 | 函数 | 主要工作 |
+|------|------|---------|
+| init1 | `app_iec61850s_init1()` | 解析配置、注册回调 |
+| init2 | `app_iec61850s_init2()` | 初始化信号、启动 MMS 服务器 |
+| fun_cb | `app_iec61850s()` | 主循环(3 定时器,当前空闲) |
+
+### 2.1 init1 流程([iec61850s.cpp:392](src/system/libiec61850s/src/iec61850s.cpp#L392))
+
+```
+1. get_base_path() → 获取进程目录
+2. parse_mms_xml("config/MMS/mms_s.xml") → 解析 XML 配置
+3. mms_s_dbg_switch(false) → 关闭调试
+4. mms_s_file_path_set(base_path) → 设置文件根目录
+5. mms_s_value_update_register(&cb) → 注册值更新回调
+```
+
+### 2.2 init2 流程([iec61850s.cpp:421](src/system/libiec61850s/src/iec61850s.cpp#L421))
+
+```
+1. iec61850s_signals_init() → 初始化五类信号
+2. mms_s_init("config/MMS/PCS.icd", 102) → 启动 MMS 服务器
+```
+
+### 2.3 主循环([iec61850s.cpp:446](src/system/libiec61850s/src/iec61850s.cpp#L446))
+
+三个定时器事件(`EV_TIMER1/2/3`)当前均为空闲,仅 `EV_TIMER3` 做 `run_cnt++`。信号驱动的工作全部通过 libmms_s 的内部线程和 DataCenter 回调完成——本线程主要作为容器,维护 MMS 服务器的生命周期。
+
+---
+
+## 3. 配置解析子系统([parse_xml.cpp](src/system/libiec61850s/src/parse_xml.cpp))
+
+### 3.1 XML 结构(mms_s.xml)
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### 3.2 数据结构
+
+```cpp
+typedef struct {
+ std::vector vec_st; // 遥信
+ std::vector vec_mx; // 遥测
+ std::vector vec_co; // 遥控
+ std::vector vec_ao; // 定值
+ std::vector vec_param; // 参数
+} stru_mms_cfg;
+```
+
+每个 Signal 只需要 `link`(sAddr)属性,`type` 和 `ctrl_model` 在后续从 DataCenter 获取。
+
+---
+
+## 4. 五类信号初始化
+
+`iec61850s_signals_init()` 按顺序初始化五类信号([iec61850s.cpp:347](src/system/libiec61850s/src/iec61850s.cpp#L347)):
+
+### 4.1 遥信(ST)初始化
+
+```
+for each st signal:
+ dc_signal_out_link_with_callback(saddr, &p_data, iec61850s_st_mx_change_callback)
+```
+
+将 DataCenter 的 `out` 信号与本地指针绑定,注册变化回调 `iec61850s_st_mx_change_callback`。
+
+### 4.2 遥测(MX)初始化
+
+逻辑与 ST 完全一致,共用同一个回调函数。
+
+### 4.3 ST/MX 变化回调
+
+```cpp
+iec61850s_st_mx_change_callback(saddr, type, p_data, p_last_data)
+ ↓
+dc_get_signal_val(p_data, type) → 获取当前值字符串
+ ↓
+g_mms_s_value_update_cb(saddr, val) → 更新 MMS 模型中的 DA 值
+```
+
+数据流向:**DataCenter 信号变化 → 回调 → libmms_s::mms_s_value_update() → IedServer 模型更新**
+
+### 4.4 遥控(CO)初始化
+
+```
+for each co signal:
+ dc_get_yk_signal_info(saddr, desc, type, ctrl_model, &p_data)
+ ↓
+mms_s_control_register(&g_vec_control, iec61850s_control_callback)
+```
+
+控制执行时,`mms_s_control.cpp` 的 `control_handler()` 会触发 `iec61850s_control_callback()`,根据 `ctrl_model` 调用 DataCenter 的 `dc_signal_yk_set_status()`。
+
+**ctrl_model 映射**:
+| MMS Control Model | DataCenter 动作 |
+|-------------------|----------------|
+| DIRECT_NORMAL / DIRECT_ENHANCED | `SIGNAL_CTRL_TYPE::DIRECT_NORMAL` → `dc_signal_yk_set_status(DIRECT)` |
+| SBO_NORMAL / SBO_ENHANCED | `SIGNAL_CTRL_TYPE::SBO_NORMAL` → select + direct 两步 |
+| STATUS_ONLY | 仅日志,不操作 |
+
+### 4.5 定值(AO)初始化
+
+```
+for each ao signal:
+ dc_get_ao_signal_info(saddr, desc, type, null, ctrl_model, &p_data, null)
+ ↓
+mms_s_setting_register(&g_vec_setting, iec61850s_setting_callback)
+```
+
+客户端写定值时,`mms_s_setting.cpp` 的 `writeAccessHandler()` 校验通过后触发 `iec61850s_setting_callback()`,调用 `dc_signal_ao_set_val()`。
+
+### 4.6 参数(Param)初始化
+
+```
+for each param signal:
+ dc_get_param_signal_info(saddr, desc, type, null, ctrl_model, &p_data_vec, null)
+ ↓
+mms_s_param_register(&g_vec_param, iec61850s_param_callback)
+```
+
+参数支持多定值区(`p_data[MMS_S_PARAM_MAX]`,最大 16 组)。客户端确认编辑后,`edit_sg_confirmation_handler()` 触发 `iec61850s_param_callback()`,调用 `dc_signal_param_set_val()`,传入 `setting_zone` 索引。
+
+---
+
+## 5. 完整数据流向
+
+### 5.1 上行数据(RTU → 客户端)
+
+```
+DataCenter 信号变化
+ → iec61850s_st_mx_change_callback()
+ → g_mms_s_value_update_cb(saddr, val)
+ → mms_s_value_update() [mms_s_value.cpp]
+ → IedServer_update*AttributeValue()
+ → libiec61850 内部触发报告(根据 TrgOps 配置)
+ → MMS 报告上送到客户端
+```
+
+### 5.2 下行数据(客户端 → RTU)
+
+**控制**:
+```
+客户端 Select/Operate
+ → libiec61850 → control_handler() [mms_s_control.cpp]
+ → iec61850s_control_callback()
+ → dc_signal_yk_set_status()
+```
+
+**定值**:
+```
+客户端 Write SP
+ → libiec61850 → writeAccessHandler() [mms_s_setting.cpp]
+ → 值校验(范围/步长)
+ → iec61850s_setting_callback()
+ → dc_signal_ao_set_val()
+```
+
+**参数(定值组)**:
+```
+客户端切换定值组
+ → libiec61850 → param_active_sg_changed_handler() [mms_s_param.cpp]
+ → param_load_active_sg_values() → 加载 SG DA
+客户端编辑确认
+ → edit_sg_confirmation_handler() [mms_s_param.cpp]
+ → iec61850s_param_callback()
+ → dc_signal_param_set_val(setting_zone)
+```
+
+---
+
+## 6. 数据结构
+
+### 6.1 本地信号存储
+
+| 类型 | 容器 | 数据结构 |
+|------|------|---------|
+| 遥信 | `g_vec_st` | `vector` — {base, p_data} |
+| 遥测 | `g_vec_mx` | `vector` — {base, p_data} |
+| 遥控 | `g_vec_control` | `vector` — {base, p_data} |
+| 定值 | `g_vec_setting` | `vector` — {base, p_data} |
+| 参数 | `g_vec_param` | `vector` — {base, param_num, p_data[]} |
+
+### 6.2 公共信号基类([myMms_s.h](release/inc/myMms_s.h))
+
+```cpp
+typedef struct {
+ char saddr[MMS_S_STR_LEN]; // 短地址(如 "st.0", "mx.5")
+ char desc[MMS_S_STR_LEN]; // 描述
+ uint8_t type; // 数据类型(DATA_TYPE_*)
+ uint8_t ctrl_model; // 控制模型
+} stru_mms_s_signal_base;
+```
+
+---
+
+## 7. 与 libmms_m / libiec61850m 的对比
+
+| 维度 | MMS 客户端 (m) | MMS 服务端 (s) |
+|------|---------------|---------------|
+| **角色** | 主动连接 IED,订阅报告 | 被动等待客户端连接 |
+| **模型来源** | 从 IED 发现 | ICD 文件预定义 |
+| **数据方向** | 先订阅 RCB,再接收报告 | 收到控制/写请求后更新 DataCenter |
+| **线程模型** | libmms_m 启动 pthread,libiec61850m 做桥接 | libmms_s 启动 pthread,libiec61850s 做桥接 |
+| **桥接层复杂度** | 复杂:RCB 发现/匹配/订阅/GI 触发/重连 | 相对简单:注册信号+回调,libmms_s 处理细节 |
+| **配置方式** | mms_m.xml(IED 连接参数) | mms_s.xml(信号映射)+ PCS.icd(模型定义) |
+
+---
+
+## 8. 对外接口
+
+| 函数 | 说明 |
+|------|------|
+| `app_iec61850s_init1(void *arg)` | 第一段初始化:解析 XML、注册值更新回调 |
+| `app_iec61850s_init2(void *arg)` | 第二段初始化:初始化信号、启动 MMS 服务器 |
+| `app_iec61850s(void *arg)` | 主线程循环 |
+| `parse_mms_xml(path)` | 解析 mms_s.xml 配置 |
+| `mms_cfg_ptr_get()` | 获取配置数据指针 |
+| `show_mms_xml(cfg)` | 打印配置内容(调试) |
diff --git a/claude/工程/libmms_s模块分析.md b/claude/工程/libmms_s模块分析.md
new file mode 100644
index 0000000..9571c7d
--- /dev/null
+++ b/claude/工程/libmms_s模块分析.md
@@ -0,0 +1,379 @@
+# libmms_s 模块分析
+
+**日期**:2026-06-10
+
+---
+
+## 1. 模块概览
+
+`libmms_s` 是 IEC 61850 MMS 服务端的封装库,基于 `libiec61850` v1.5.3 的服务端 API,提供 ICD 文件解析、动态数据模型创建、IedServer 生命周期管理,以及控制/定值/参数/文件等子系统的初始化。
+
+### 目录结构
+
+```
+src/protocol/libmms_s/
+├── inc/
+│ ├── mms_s.h # 核心头文件:日志宏、全局访问接口、类型转换
+│ ├── mms_s_icd.h # ICD 解析数据结构(完整 SCL 类型体系)
+│ ├── mms_s_model.h # 动态模型创建接口
+│ ├── mms_s_value.h # DA 初始值设定
+│ ├── mms_s_control.h # 控制功能接口
+│ ├── mms_s_param.h # 定值组参数接口
+│ ├── mms_s_setting.h # 定值(SP)接口
+│ └── mms_s_file.h # 文件传输服务接口
+└── src/
+ ├── mms_s.cpp # 核心:init/run/task/类型转换
+ ├── mms_s_icd.cpp # ICD XML 解析器(tinyxml2)
+ ├── mms_s_model.cpp # 动态 IedModel 创建
+ ├── mms_s_control.cpp # 控制(SBO/直控)处理
+ ├── mms_s_param.cpp # 定值组(SG/SE)管理
+ ├── mms_s_setting.cpp # 定值(SP)管理
+ ├── mms_s_value.cpp # 数据属性初始值设定
+ └── mms_s_file.cpp # MMS 文件传输服务
+```
+
+---
+
+## 2. 核心架构
+
+### 2.1 单例全局状态
+
+```cpp
+LOCAL IedServer gp_iedServer = NULL; // 全局 IED 服务器(单例)
+LOCAL stru_icd *gp_icd = NULL; // 全局 ICD 数据(单例)
+LOCAL int g_running = 0; // 线程运行标志
+LOCAL bool g_dbg_switch = false; // 调试输出开关
+```
+
+整个库采用单例模式,全局只有一个 IedServer 和一个 ICD 数据结构。
+
+### 2.2 模块分层
+
+```
+┌──────────────────────────────────────────┐
+│ myMms_s.h │ ← 公共 API + 类型定义
+├──────────────────────────────────────────┤
+│ mms_s.cpp (init / run / 类型转换) │ ← 库入口
+├──────────────────────────────────────────┤
+│ mms_s_icd.cpp │ mms_s_model.cpp │ ← ICD 解析 → 模型构建
+├──────────────────────────────────────────┤
+│ mms_s_value.cpp (DA 初始值) │ ← 模型回调
+├──────────────────────────────────────────┤
+│ mms_s_control.cpp │ mms_s_param.cpp │ ← 功能模块
+│ mms_s_setting.cpp │ mms_s_file.cpp │
+├──────────────────────────────────────────┤
+│ libiec61850 (libiec61850.a) │ ← 底层协议栈
+└──────────────────────────────────────────┘
+```
+
+---
+
+## 3. 完整初始化流程
+
+`mms_s_init()` 的执行顺序([mms_s.cpp:261](src/protocol/libmms_s/src/mms_s.cpp#L261)):
+
+```
+1. icd_parse(icd_path) → 解析 ICD 文件 → stru_icd
+2. model_init(*gp_icd) → 创建动态数据模型 → IedModel
+3. IedServerConfig_create() → 创建服务器配置
+4. IedServer_createWithConfig() → 创建 IedServer 实例
+5. control_init() → 安装控制回调
+6. param_init() → 安装定值组回调
+7. file_init() → 安装文件服务
+8. IedServer_setRCBEventHandler() → 注册 RCB 事件监听
+9. IedServer_start(port) → 启动服务器(默认端口 102)
+10. setting_init() → 初始化定值
+11. IedServer_isRunning() → 检查运行状态
+12. Thread_create(mms_s_run_task) → 启动后台线程
+```
+
+---
+
+## 4. ICD 解析子系统
+
+### 4.1 数据结构体系([mms_s_icd.h](src/protocol/libmms_s/inc/mms_s_icd.h))
+
+对应 IEC 61850-6 SCL 标准的完整类型体系:
+
+**数据类型模板层(DataTypeTemplates)**:
+| 结构 | 说明 | 关键字段 |
+|------|------|---------|
+| `stru_LNodeType` | 逻辑节点类型 | lnClass, vec_do, map_do(key:name) |
+| `stru_DO` | 数据对象定义 | desc, type(→ DOType id) |
+| `stru_DOType` | 数据对象类型 | cdc, vec_da, map_da, vec_sdo, map_sdo |
+| `stru_DA` | 数据属性定义 | bType, type, dchg, qchg, fc, text |
+| `stru_DAType` | 数据属性类型 | vec_bda, map_bda(key:name) |
+| `stru_BDA` | 基本数据属性 | bType, type, fc, dchg, qchg |
+| `stru_EnumType` | 枚举类型 | vec_ord, map_enumVal(key:ord) |
+
+**实例化模型层(IED/Server)**:
+| 结构 | 说明 |
+|------|------|
+| `stru_ied` | IED 顶层模型(含 model, name, 各逻辑设备) |
+| `stru_LDevice` | 逻辑设备(含 Ldevice 指针, sgcb, ln0, vec_ln) |
+| `stru_LN0` | LLN0(含 数据集, 报告控制, 定值控制) |
+| `stru_LN` | 普通逻辑节点 |
+| `stru_DOI` | 数据对象实例(含 SDI, DAI) |
+| `stru_SDI` | 子数据对象实例(可递归嵌套) |
+| `stru_DAI` | 数据属性实例(含 sAddr, val) |
+| `stru_DataSet` | 数据集(含 FCDA 列表) |
+| `stru_FCDA` | 功能约束数据属性(ldInst/lnClass/doName/daName/fc) |
+| `stru_ReportControl` | 报告控制块(含 TrgOps, OptFields, RptEnabled_max) |
+| `stru_SettingControl` | 定值控制块(actSG, numOfSGs) |
+
+**运行时辅助结构**:
+| 结构 | 说明 |
+|------|------|
+| `stru_all_DO` | DO 运行时树(含 libiec61850 DataObject 指针, map_all_sdo, map_all_da) |
+| `stru_all_DA` | DA 运行时树(含 libiec61850 DataAttribute 指针, map_all_da 子节点) |
+| `stru_contorl_DO` | 控制 DO 定位信息(ldevice_inst/ln_class/ln_inst/do_name/p_all_do) |
+| `stru_saddr_point` | sAddr 到模型节点的映射(node, da_t, da_q) |
+| `stru_setting_info` | 定值信息(DA 指针, 类型, 值) |
+
+### 4.2 解析流程([mms_s_icd.cpp](src/protocol/libmms_s/src/mms_s_icd.cpp))
+
+```
+icd_parse(icd_file)
+├── XMLDocument.LoadFile()
+├── parse_ied() → 解析 IED 实例
+│ ├── parse_LDevice() → 每个逻辑设备
+│ │ ├── parse_LN0() → LLN0(数据集/报告控制/定值控制/DOI)
+│ │ │ ├── parse_DataSet()
+│ │ │ ├── parse_ReportControl()
+│ │ │ │ ├── parse_ReportControl_TrgOps()
+│ │ │ │ ├── parse_ReportControl_OptFields()
+│ │ │ │ └── parse_ReportControl_RptEnable() → RptEnabled_max
+│ │ │ ├── parse_DOI() → parse_SDI() / parse_DAI()
+│ │ │ └── parse_SettingControl()
+│ │ └── parse_LN() → 普通 LN + DOI
+│ └── (沿 SCL > IED > AccessPoint > Server > LDevice 路径)
+├── parse_dataTypeTemplates() → 解析模板
+│ ├── parse_LNodeType()
+│ ├── parse_DOType()
+│ ├── parse_DAType()
+│ ├── parse_EnumType()
+│ └── parse_BDA_fc() ×2 → 传播 FC/dchg/qchg 到嵌套 BDA
+└── check_ied_dataTypeTemplates() → 一致性校验
+ └── 递归验证每个实例节点都能在模板中找到定义
+```
+
+### 4.3 BDA 属性传播机制
+
+关键设计:当 DA 的 type 指向 DAType(Struct 类型)时,`parse_BDA_fc()` 将父级 DA 的 fc/dchg/qchg 属性向下传播到 DAType 中所有 BDA,确保嵌套 Struct 的底层 BDA 也能继承正确的 FC 约束。
+
+---
+
+## 5. 动态模型创建子系统
+
+### 5.1 model_init() 主流程([mms_s_model.cpp:1319](src/protocol/libmms_s/src/mms_s_model.cpp#L1319))
+
+```
+1. IedModel_create(name)
+2. model_ldevice_init()
+ ├── LogicalDevice_create()
+ ├── model_ln0_init()
+ │ ├── LogicalNode_create("LLN0")
+ │ ├── model_dataobject_init() → 创建所有 DO/SDO/DA
+ │ ├── model_DataSet_init() → 创建数据集+条目
+ │ ├── model_ReportControlBlock_init() → 创建 RCB
+ │ └── SettingGroupControlBlock_create()
+ └── model_LNodes_init() → 创建普通 LN
+3. model_sync_ied_default_values() → 同步 ICD DAI 中的 sAddr/val
+4. model_search_control_DataObjects() → 搜索 ctlModel→vec_control_do
+5. icd.ied.model->initializer = mms_s_values_init → 注册回调
+```
+
+### 5.2 类型映射表
+
+**bType → DataAttributeType**([mms_s_model.cpp:43](src/protocol/libmms_s/src/mms_s_model.cpp#L43)):
+`BOOLEAN → IEC61850_BOOLEAN`, `INT32 → IEC61850_INT32`, `FLOAT32 → IEC61850_FLOAT32`, `Struct → IEC61850_CONSTRUCTED`, `Quality → IEC61850_QUALITY`, `Timestamp → IEC61850_TIMESTAMP`, `Dbpos → IEC61850_GENERIC_BITSTRING` 等,共约 25 种映射。
+
+**FC → FunctionalConstraint**([mms_s_model.cpp:86](src/protocol/libmms_s/src/mms_s_model.cpp#L86)):
+`ST/MX/SP/SV/CF/DC/SG/SE/SR/OR/BL/EX/CO/US/MS/RP/BR/LG/GO` → 对应 IEC61850 枚举。
+
+### 5.3 SG/SE 双数据属性设计
+
+当 DA 的 FC=SG 时,除了创建 FC=SG 的 DA 外,还会额外创建一个 FC=SE 的同名 DA(key 加 `_SE` 后缀)。反之 FC=SE 同理。sAddr 映射时也会对应添加 `_SG` / `_SE` 后缀区分。
+
+这是一个独创设计——在同一个 DO 下同时暴露 SG(当前激活值)和 SE(编辑缓冲区值),使得外部系统可以同时访问定值的"当前生效值"和"正在编辑中的值"。
+
+### 5.4 sAddr 映射机制
+
+ICD 文件中 DAI 元素的 `sAddr` 属性定义了该数据点与外部数据源的绑定关系。`model_sync_ied_default_values()` 将 sAddr 写入模型节点的 `sAddr` 字段,同时建立:
+- `icd.ied.vec_saddr` — sAddr 列表
+- `icd.ied.map_saddr_point` — sAddr → ModelNode 映射
+
+对于 FC=SG 的点,sAddr 后缀 `_SG`;FC=SE 的点后缀 `_SE`。
+
+---
+
+## 6. 控制子系统([mms_s_control.cpp](src/protocol/libmms_s/src/mms_s_control.cpp))
+
+### 6.1 双回调模式
+
+| 回调 | 阶段 | 职能 |
+|------|------|------|
+| `check_handler()` | Select / Interlock | 权限校验,匹配合法 DO 则返回 CONTROL_ACCEPTED |
+| `control_handler()` | Operate | 执行控制命令,更新 t(时间戳)+ stVal,触发外部回调 |
+
+### 6.2 控制执行流程
+
+```
+客户端 → Select Request → check_handler() → CONTROL_ACCEPTED
+客户端 → Operate Request → check_handler() (interlock check) → control_handler()
+ ├── IedServer_updateUTCTimeAttributeValue(t)
+ ├── 匹配 sAddr → cb_control() → 通知应用层
+ └── 根据 stVal 类型:
+ ├── BOOLEAN → IedServer_updateAttributeValue()
+ └── Dbpos → IedServer_updateDbposValue()
+```
+
+### 6.3 控制 DO 发现
+
+`model_search_control_DataObjects()` 遍历模型树,找到所有包含 `ctlModel` DA 的 DO,存入 `icd.ied.vec_control_do`。`control_init()` 遍历该列表,为每个安装 `control_handler` 和 `check_handler`。
+
+---
+
+## 7. 定值组子系统([mms_s_param.cpp](src/protocol/libmms_s/src/mms_s_param.cpp))
+
+### 7.1 SG vs SE
+
+| FC | 含义 | mmsValue 加载时机 |
+|----|------|------------------|
+| SG | Setting Group — 当前激活定值区的实际值 | 激活定值组切换时加载 |
+| SE | Setting Editable — 编辑缓冲区值 | 编辑定值组切换时加载 |
+
+### 7.2 回调链
+
+```
+激活定值组切换 → param_active_sg_changed_handler() → param_load_active_sg_values()
+编辑定值组切换 → param_edit_sg_changed_handler() → param_load_edit_sg_values()
+编辑确认 → edit_sg_confirmation_handler() → 回读 SE 值 → cb_param()
+```
+
+### 7.3 值的加载与校验
+
+- 加载:根据 sAddr+"_SG"/"_SE" 在 `map_saddr_point` 中查找节点,按 MMS 类型分发更新函数
+- 回读:`edit_sg_confirmation_handler()` 读取 SE DA 当前值,做 min/max/step 校验,通过后触发外部回调 `g_param_cb`
+- 类型分发表:`g_param_update_funcs[]`(写)和 `g_param_get_funcs[]`(读),覆盖 BOOLEAN/INT/UINT/FLOAT/STRING
+
+---
+
+## 8. 定值子系统([mms_s_setting.cpp](src/protocol/libmms_s/src/mms_s_setting.cpp))
+
+### 8.1 与参数的差异
+
+定值(FC=SP)不参与定值组切换,是单一设置值。
+
+### 8.2 写保护机制
+
+1. 全局访问策略:`IedServer_setWriteAccessPolicy(IEC61850_FC_SP, ACCESS_POLICY_DENY)`
+2. 精确放行:通过 `IedServer_handleWriteAccess()` 为已注册 sAddr 安装 `writeAccessHandler`
+3. `writeAccessHandler` 做值校验(范围+步长),通过后触发外部回调 `cb_setting`
+
+### 8.3 校验流程
+
+```
+客户端写 SP 值 → writeAccessHandler()
+├── 匹配 sAddr
+├── 类型分发 (setting_get_BOOLEAN/INT/UINT/FLOAT/STRING)
+│ └── setting_min_max_step_get() → 读取同级 minVal/maxVal/stepSize
+│ └── 范围校验 + 步长校验
+├── cb_setting() → 通知应用层
+└── return DATA_ACCESS_ERROR_SUCCESS → libiec61850 更新 mmsValue
+```
+
+---
+
+## 9. 数据属性值初始化([mms_s_value.cpp](src/protocol/libmms_s/src/mms_s_value.cpp))
+
+`mms_s_values_init()` 作为 `IedModel.initializer` 回调,在 IedServer 创建过程中由 libiec61850 自动调用。
+
+**初始化类型覆盖**:BOOLEAN, INT8U/32, FLOAT32, VisString*, Unicode*, Timestamp, Dbpos, Enum(特殊:按文本值或序号查找枚举值)。
+
+**Dbpos 特殊处理**:`mms_s_da_value_init_Dbpos()` 按 val 值映射到 DBPOS_INTERMEDIATE_STATE(0)/OFF(1)/ON(2)/BAD_STATE(>=3)。
+
+### 9.1 值更新路径
+
+`mms_s_value_update()` 是预留的值更新函数,通过 `mms_s_value_update_register()` 注册给外部。当数据中心信号变化时,上层调用此函数更新模型中的 DA 值,同时自动更新父 DO 的 `t`(时间戳)和 `q`(品质)。
+
+---
+
+## 10. 文件传输服务([mms_s_file.cpp](src/protocol/libmms_s/src/mms_s_file.cpp))
+
+提供 MMS 文件传输基础能力:
+- 设置文件存储根目录 `IedServer_setFilestoreBasepath()`
+- 访问控制:禁止重命名、禁止删除 `IEDSERVER.BIN`
+- 连接事件日志
+
+---
+
+## 11. 后台运行线程
+
+```cpp
+LOCAL void *mms_s_run_task(void *parameter)
+{
+ while(g_running) {
+ IedServer_lockDataModel(gp_iedServer);
+ IedServer_unlockDataModel(gp_iedServer);
+ Thread_sleep(100); // 100ms 周期
+ }
+ IedServer_stop(gp_iedServer);
+ IedServer_destroy(gp_iedServer);
+ IedModel_destroy(iedModel);
+}
+```
+
+线程负责保护 IedServer 生命周期,通过 lock/unlock 持有模型锁保持服务活跃。收到 SIGINT 后 `g_running=0`,线程退出并销毁资源。
+
+---
+
+## 12. RCB 事件监听
+
+`rcbEventHandler()` 监听客户端的报告控制块操作([mms_s.cpp:48](src/protocol/libmms_s/src/mms_s.cpp#L48)):
+
+| 事件 | 含义 |
+|------|------|
+| `RCB_EVENT_ENABLE` | 客户端使能报告 |
+| `RCB_EVENT_DISABLE` | 客户端关闭报告 |
+| `RCB_EVENT_RESERVED` | 客户端预订 RCB |
+| `RCB_EVENT_UNRESERVED` | 客户端释放预订 |
+| `RCB_EVENT_GI` | 总召触发 |
+| `RCB_EVENT_SET_PARAMETER` | 客户端设置 RCB 参数(如 TrgOps 等) |
+| `RCB_EVENT_GET_PARAMETER` | 客户端获取 RCB 参数 |
+
+**特殊处理**:当客户端设置 `TrgOps` 参数时,服务器强制追加 `dchg`(`rcb->trgOps |= 0x01`),确保数据变化一定能触发报告上送。
+
+---
+
+## 13. 对外 API 汇总
+
+| API | 文件 | 说明 |
+|-----|------|------|
+| `mms_s_init(icd_path, port)` | mms_s.cpp | 完整初始化 MMS 服务器 |
+| `mms_s_dbg_switch(on)` | mms_s.cpp | 调试开关 |
+| `mms_s_get_icd_ptr()` | mms_s.cpp | 获取 ICD 数据 |
+| `mms_s_get_ied_server_ptr()` | mms_s.cpp | 获取 IedServer |
+| `mms_s_control_register(...)` | mms_s_control.cpp | 注册控制点 |
+| `mms_s_setting_register(...)` | mms_s_setting.cpp | 注册定值 |
+| `mms_s_param_register(...)` | mms_s_param.cpp | 注册参数(定值组) |
+| `mms_s_file_path_set(path)` | mms_s_file.cpp | 设置文件根路径 |
+| `mms_s_value_update_register(cb)` | mms_s_value.cpp | 注册值更新回调 |
+| `mms_s_get_string_by_mms_type(...)` | mms_s.cpp | MMS 类型→字符串 |
+| `mms_s_get_string_by_type(...)` | mms_s.cpp | 自定义类型→字符串 |
+
+---
+
+## 14. 已知问题
+
+### 14.1 check_handler 未调用 cb_interlock
+
+`mms_s_control.h` 中声明了 `mms_s_interlock_cb` 和 `mms_s_set_interlock_cb()`,但 `check_handler()` 中仅检查 ICD 中是否存在对应 DO,`cb_interlock` 未被实际调用。外部注册的联锁检查回调不会生效。
+
+### 14.2 param_min_max_step_get 实现不一致
+
+`mms_s_param.cpp` 的 `param_min_max_step_get()` 通过遍历 ModelNode 父子关系查找 minVal/maxVal/stepSize(向上找到 DataObject 再遍历 firstChild),而 `mms_s_setting.cpp` 的 `setting_min_max_step_get()` 是通过 parent->firstChild 直接遍历。两者逻辑不同,前者更复杂(向上一级再到 DO),可能是修正后的版本,但后者仍保留了旧逻辑。
+
+### 14.3 mms_s_value_update 未使用 g_iec61850s_value_init_cb_map
+
+`mms_s_value_update()` 使用 `g_iec61850s_value_update_cb_map`(按 DataAttributeType 索引),但表中大部分类型回调为 NULL,仅 BOOLEAN/INT32/INT32U/FLOAT32/Enum/VisString32/Unicode255/Timestamp 有实际实现。其他类型(如 INT8/INT16/INT64/FLOAT64 等)更新时会被跳过。
diff --git a/claude/工程/libweb_server模块分析.md b/claude/工程/libweb_server模块分析.md
new file mode 100644
index 0000000..2b019eb
--- /dev/null
+++ b/claude/工程/libweb_server模块分析.md
@@ -0,0 +1,217 @@
+# libweb_server 模块分析
+
+**日期**:2026-06-10
+
+---
+
+## 1. 模块概览
+
+`libweb_server` 是 RTU 的嵌入式 Web 服务器模块(`app_web_server` 线程),属于系统层的第 7 号线程。基于 Mongoose v7.21,提供 HTTP 静态文件服务 + WebSocket 实时数据通道。
+
+### 目录结构
+
+```
+src/system/libweb_server/
+├── inc/
+│ ├── web_server.h # 对外接口(app_web_server_init1/2, app_web_server)
+│ └── ws_method.h # WebSocket 消息处理方法声明
+└── src/
+ ├── web_server.cpp # HTTP/WS 服务器 + mongoose 事件循环
+ └── ws_method.cpp # WebSocket 命令解析 + JSON 数据推送
+```
+
+---
+
+## 2. 架构
+
+### 2.1 线程模型
+
+```
+┌─────────────────────────────────────────────────┐
+│ app_web_server 线程 (RTU 9号线程) │
+│ │
+│ init1: web_server_init() │
+│ → mg_mgr_init + mg_http_listen(8000) │
+│ → pthread_create → web_server_run (独立线程) │
+│ │
+│ init2: (空) │
+│ │
+│ fun_cb: 事件循环 (3个定时器) │
+│ EV_TIMER3 → ws_task() (每秒推送数据) │
+└─────────────────────────────────────────────────┘
+ │
+ │ g_ws_conns (连接列表) + g_ws_sessions (会话资源)
+ │
+┌─────────────────────────────────────────────────┐
+│ web_server_run 线程 (mongoose 事件线程) │
+│ │
+│ while(1) mg_mgr_poll(500ms) │
+│ → web_server_task() 回调 │
+│ MG_EV_HTTP_MSG → WS升级 / 静态文件 │
+│ MG_EV_WS_MSG → 接收命令 │
+│ MG_EV_CLOSE → 断连清理 │
+└─────────────────────────────────────────────────┘
+```
+
+### 2.2 会话隔离模型
+
+每个 WebSocket 连接拥有独立的 `stru_ws_session`,包含自己的五类信号集。连接建立时开辟、断开时释放。
+
+```
+客户端A (WebSocket) ─→ session_A: {out_signals_A, in_signals_A, yk_signals_A, ...}
+客户端B (WebSocket) ─→ session_B: {out_signals_B, in_signals_B, yk_signals_B, ...}
+```
+
+`ws_task()` 遍历所有 session,为每个 session 构建独立的 JSON 并发送到对应连接。
+
+### 2.3 数据流
+
+```
+浏览器 ←── HTTP ──→ mongoose ←── 静态文件 (web_root/)
+浏览器 ←── WS ────→ mongoose ←── JSON 命令/数据 ←── ws_method.cpp
+ ↕
+ DataCenter
+```
+
+---
+
+## 3. web_server.cpp 核心逻辑
+
+### 3.1 全局状态
+
+```cpp
+g_web_root → 静态文件根目录(进程目录 + "web_root")
+mgr → mongoose 事件管理器
+g_ws_conns → vector 活跃 WebSocket 连接列表
+g_ws_conns_mutex → pthread 互斥锁,保护 g_ws_conns
+g_ws_sessions → map 每连接独立信号资源(ws_method.cpp)
+g_ws_session_mutex → pthread 互斥锁,保护 g_ws_sessions
+```
+
+### 3.2 事件处理 (web_server_task)
+
+| 事件 | 处理 |
+|------|------|
+| `MG_EV_HTTP_MSG` | URI=`/ws` → `mg_ws_upgrade()` 升级为 WebSocket;其他 → `mg_http_serve_dir()` 静态文件 |
+| `MG_EV_WS_MSG` | 将连接加入 `g_ws_conns`,消息经 `std::string` 安全复制后传给 `ws_recv()` |
+| `MG_EV_WS_CTL` | CLOSE 帧 → 调用 `ws_session_destroy()` + 从 `g_ws_conns` 移除 |
+| `MG_EV_CLOSE` | 调用 `ws_session_destroy()` + 从 `g_ws_conns` 移除 |
+
+### 3.3 ws_send_all / ws_send_one
+
+- `ws_send_all()`:遍历 `g_ws_conns` 广播,跳过死连接
+- `ws_send_one(conn_id, ...)`:按 `c->id` 精确定位单连接发送
+
+均加 `g_ws_conns_mutex` 锁保护。
+
+---
+
+## 4. ws_method.cpp 命令处理
+
+### 4.1 会话结构
+
+```cpp
+struct stru_ws_session {
+ vector out_signals; // 本连接订阅的遥信
+ vector in_signals; // 本连接订阅的遥测
+ vector yk_signals; // 本连接订阅的遥控
+ vector ao_signals; // 本连接订阅的定值
+ vector param_signals; // 本连接订阅的参数
+};
+```
+
+全局 `g_ws_sessions: map` 管理所有连接的独立资源。`g_ws_session_mutex` 保护。
+
+### 4.2 信号模型
+
+每个连接 `add` 信号时仅影响自己的 session,不同客户端之间完全隔离。
+
+| 类型 | session 字段 | 支持操作 |
+|------|-------------|---------|
+| out (遥信) | `session.out_signals` | add / del / set |
+| in (遥测输入) | `session.in_signals` | add / del |
+| yk (遥控) | `session.yk_signals` | add / del / set (含 SBO) |
+| ao (定值) | `session.ao_signals` | add / del / set (含 SBO) |
+| param (参数) | `session.param_signals` | add / del / set (含 SBO, 多定值区) |
+
+### 4.2 WebSocket 上行命令格式 (JSON)
+
+```json
+{
+ "curd": "add|del|set",
+ "signal_type": "out|in|yk|ao|param",
+ "saddr": "st.0",
+ "signal_data": "1.5",
+ "setting_zone": "0"
+}
+```
+
+### 4.3 下行数据格式 (JSON)
+
+每秒 (EV_TIMER3) 推送完整快照,五类信号分数组。修复后仅在值变化时推送。
+
+### 4.4 SBO 控制流程
+
+遥控/定值/参数支持 `DIRECT_NORMAL`(直接执行)和 `SBO_NORMAL`(选择-执行两步),通过 DataCenter 的 `dc_signal_yk_set_status` / `dc_signal_ao_set_val` / `dc_signal_param_set_val` 执行。
+
+---
+
+## 5. 缺陷与修复
+
+### 5.1 缺陷清单(修复前)
+
+| # | 严重度 | 位置 | 问题 |
+|---|--------|------|------|
+| 1 | 严重 | web_server.cpp L14 | `p_conn` 单指针,仅支持一个 WS 客户端 |
+| 2 | 严重 | web_server.cpp | 无 `MG_EV_CLOSE` 处理,断连后悬空指针 |
+| 3 | 严重 | web_server.cpp | `p_conn` 无锁保护,多线程竞态 |
+| 4 | 严重 | web_server.cpp L66, ws_method.cpp L519 | `mg_str` 非 null-terminated 传给 `printf`/`cJSON_Parse`(UB) |
+| 5 | 高危 | web_server.cpp L66 | 调试 `printf` 遗留 |
+| 6 | 高危 | ws_method.cpp L386 | SBO `task_sleep_ms(1000)` 阻塞事件循环 |
+| 7 | 中危 | — | 无心跳保活机制 |
+| 8 | 中危 | ws_method.cpp | 每秒全量推送,数据不变也发送 |
+| 9 | 中危 | ws_method.cpp L400,L502 | `LOG_E("%d")` 缺少对应参数 |
+| 10 | 中危 | web_server.cpp L22 | `ws_send` 未校验 `is_websocket`/`is_draining` |
+| 11 | 严重 | ws_method.cpp L21-25 | 多客户端共享同一套全局信号资源,客户端之间信号干扰 |
+
+### 5.2 修复措施
+
+**第一轮(2026-06-10)**:
+
+| # | 修复 |
+|---|------|
+| 1 | `p_conn` → `g_ws_conns: vector`,遍历广播 |
+| 2 | 新增 `MG_EV_CLOSE` + `MG_EV_WS_CTL(CLOSE)` 从列表移除 |
+| 3 | 新增 `pthread_mutex_t g_ws_mutex`,列表读写前加锁 |
+| 4 | `std::string(wm->data.buf, wm->data.len)` 安全复制后再用 |
+| 5 | `printf` → `LOG_I` |
+| 6 | 删除 `task_sleep_ms(1000)` |
+| 7 | 当前 500ms poll 周期可替代心跳,mg_timer 需配合 wakeup_init 略复杂暂不引入 |
+| 8 | `stru_ws_signal` 新增 `last_val`,`ws_task()` 仅在值变化时发送 |
+| 9 | 补全 `p_signal->ctrl_type` 参数 |
+| 10 | `ws_send()` 循环中增加 `c->is_websocket && !c->is_draining` 检查 |
+
+**第二轮(2026-06-10):会话隔离重构**:
+
+| # | 修复 |
+|---|------|
+| 11 | 引入 `stru_ws_session` 每连接独立信号集,全局 `g_ws_sessions: map` 管理 |
+| — | `ws_recv(c, ...)` 增加连接参数,操作仅影响对应 session |
+| — | `ws_task()` 遍历 sessions,为每个连接构建独立 JSON → `ws_send_one(conn_id, ...)` |
+| — | `ws_session_destroy(c)` 在 `MG_EV_CLOSE`/`WS_CTL(CLOSE)` 时释放该连接所有信号资源 |
+| — | 所有 `add/del/set/make` 函数改为接受 `stru_ws_session&` 参数,无状态纯函数 |
+
+---
+
+## 6. 对外接口
+
+| 函数 | 文件 | 说明 |
+|------|------|------|
+| `app_web_server_init1(arg)` | web_server.cpp | 第一段初始化,启动 HTTP/WS 服务器 |
+| `app_web_server_init2(arg)` | web_server.cpp | 第二段初始化(空) |
+| `app_web_server(arg)` | web_server.cpp | 主线程循环,定时器驱动 `ws_task()` |
+| `ws_send_all(p_tx, tx_len)` | web_server.cpp | 向所有 WS 客户端广播相同数据 |
+| `ws_send_one(conn_id, p_tx, tx_len)` | web_server.cpp | 向单个 WS 连接发送数据 |
+| `ws_recv(c, p_rx, rx_len)` | ws_method.cpp | 解析 WS 命令 JSON,操作仅在 c 对应 session 内生效 |
+| `ws_task()` | ws_method.cpp | 遍历所有 session,各自构建增量 JSON 并推送 |
+| `ws_session_destroy(c)` | ws_method.cpp | 释放连接对应的所有信号资源 |
diff --git a/claude/工程/websocket-server.md b/claude/工程/websocket-server.md
new file mode 100644
index 0000000..815bd11
--- /dev/null
+++ b/claude/工程/websocket-server.md
@@ -0,0 +1,598 @@
+# WebSocket 服务端深度分析
+
+## 一、概述
+
+Mongoose 的 WebSocket 实现位于 `src/ws.c`(~302 行),基于 RFC 6455 规范,同时支持服务端和客户端。本文档聚焦**服务端**的使用和内部实现。
+
+**核心流程**:
+
+```
+HTTP 请求到达 (Upgrade: websocket)
+ → 用户处理器检测到 WebSocket 升级请求
+ → 调用 mg_ws_upgrade() 完成握手
+ → pfn 切换为 mg_ws_cb(WebSocket 协议处理器)
+ → 后续消息通过 MG_EV_WS_MSG 事件传递
+ → mg_ws_send() 发送帧
+```
+
+## 二、数据结构
+
+### 帧操作码(Opcode)
+
+```c
+// 定义在 ws.h
+#define WEBSOCKET_OP_CONTINUE 0 // 分片帧的后续帧
+#define WEBSOCKET_OP_TEXT 1 // 文本帧(UTF-8)
+#define WEBSOCKET_OP_BINARY 2 // 二进制帧
+#define WEBSOCKET_OP_CLOSE 8 // 关闭连接
+#define WEBSOCKET_OP_PING 9 // 心跳请求
+#define WEBSOCKET_OP_PONG 10 // 心跳响应
+```
+
+### WebSocket 消息结构
+
+```c
+// 定义在 ws.h 第 12-15 行
+struct mg_ws_message {
+ struct mg_str data; // 消息数据(引用 c->recv 缓冲区,零拷贝)
+ uint8_t flags; // 帧标志字节(FIN + Opcode)
+};
+```
+
+`flags` 字节的高位是 FIN 标志(bit 7),低 4 位是操作码:
+
+```
+flags = 0b1xxx_xxxx → FIN=1(最后一帧)
+flags = 0b0xxx_xxxx → FIN=0(还有后续帧)
+flags & 15 → 操作码(0-15)
+```
+
+### 内部帧解析结构(ws.c 第 12-16 行)
+
+```c
+struct ws_msg {
+ uint8_t flags; // 帧标志
+ size_t header_len; // 帧头长度(含掩码 key)
+ size_t data_len; // 数据长度
+};
+```
+
+## 三、WebSocket 服务端完整使用流程
+
+### 3.1 最小示例
+
+```c
+#include "mongoose.h"
+
+// 统一的 HTTP + WebSocket 事件处理函数
+static void fn(struct mg_connection *c, int ev, void *ev_data) {
+ if (ev == MG_EV_HTTP_MSG) {
+ struct mg_http_message *hm = (struct mg_http_message *) ev_data;
+ // 判断是否是 WebSocket 升级请求
+ if (mg_http_get_header(hm, "Sec-WebSocket-Key")) {
+ mg_ws_upgrade(c, hm, NULL); // 执行握手,切换到 WS 模式
+ } else {
+ // 普通 HTTP 请求处理
+ mg_http_reply(c, 200, "", "hello\n");
+ }
+ } else if (ev == MG_EV_WS_MSG) {
+ // WebSocket 消息到达(握手完成后)
+ struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
+ // 判断消息类型
+ if (wm->flags & WEBSOCKET_OP_TEXT) {
+ // 文本消息:回显
+ mg_ws_send(c, wm->data.buf, wm->data.len, WEBSOCKET_OP_TEXT);
+ } else if (wm->flags & WEBSOCKET_OP_BINARY) {
+ // 二进制消息处理
+ }
+ } else if (ev == MG_EV_CLOSE) {
+ // 连接关闭(包括 WebSocket 关闭)
+ }
+}
+
+int main() {
+ struct mg_mgr mgr;
+ mg_mgr_init(&mgr);
+ mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL);
+ for (;;) mg_mgr_poll(&mgr, 1000);
+}
+```
+
+### 3.2 完整事件流
+
+```
+连接建立:
+ MG_EV_OPEN → 连接创建
+
+HTTP 阶段:
+ MG_EV_ACCEPT → 连接被接受(可在此时初始化 TLS)
+ MG_EV_READ → 数据到达
+ MG_EV_HTTP_MSG → HTTP 请求完整接收
+ → 用户检测 Sec-WebSocket-Key 头部
+ → 调用 mg_ws_upgrade(c, hm, NULL)
+
+WebSocket 握手:
+ mg_ws_upgrade() 内部:
+ → 计算 Sec-WebSocket-Accept
+ → 发送 HTTP 101 响应
+ → c->pfn = mg_ws_cb
+ → c->is_websocket = 1
+ → 触发 MG_EV_WS_OPEN
+
+WebSocket 通信阶段:
+ MG_EV_WS_MSG → 文本/二进制消息到达
+ MG_EV_WS_CTL → 控制帧到达(Ping/Pong/Close)
+ MG_EV_READ → 原始数据到达(ws_cb 内部处理)
+ MG_EV_WRITE → 数据写入完成
+
+连接关闭:
+ MG_EV_CLOSE → 连接关闭
+```
+
+## 四、握手过程详解 (`mg_ws_upgrade`)
+
+### 4.1 函数签名(ws.c 第 269 行)
+
+```c
+void mg_ws_upgrade(struct mg_connection *c, struct mg_http_message *hm,
+ const char *fmt, ...);
+```
+
+### 4.2 内部实现
+
+```
+mg_ws_upgrade():
+ 1. 从 HTTP 头部提取 "Sec-WebSocket-Key"
+ → 如果不存在,返回 426 Upgrade Required 错误
+ 2. 可选:提取 "Sec-WebSocket-Protocol"(子协议协商)
+ 3. 调用 ws_handshake() 生成握手响应
+ 4. 设置 c->pfn = mg_ws_cb(切换协议处理器)
+ 5. 设置 c->is_websocket = 1
+ 6. 设置 c->is_resp = 0(标记响应完成)
+ 7. 触发 MG_EV_WS_OPEN 事件
+```
+
+### 4.3 握手计算 (`ws_handshake`, 第 35 行)
+
+```c
+static void ws_handshake(struct mg_connection *c, const struct mg_str *wskey,
+ const struct mg_str *wsproto, const char *fmt,
+ va_list *ap) {
+ const char *magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // RFC 6455 魔数
+ unsigned char sha[20], b64_sha[30];
+
+ // 1. SHA1(client_key + magic)
+ mg_sha1_ctx sha_ctx;
+ mg_sha1_init(&sha_ctx);
+ mg_sha1_update(&sha_ctx, (unsigned char *) wskey->buf, wskey->len);
+ mg_sha1_update(&sha_ctx, (unsigned char *) magic, 36);
+ mg_sha1_final(sha, &sha_ctx);
+
+ // 2. Base64 编码 SHA1 结果
+ mg_base64_encode(sha, sizeof(sha), (char *) b64_sha, sizeof(b64_sha));
+
+ // 3. 构建 HTTP 101 响应
+ mg_xprintf(mg_pfn_iobuf, &c->send,
+ "HTTP/1.1 101 Switching Protocols\r\n"
+ "Upgrade: websocket\r\n"
+ "Connection: Upgrade\r\n"
+ "Sec-WebSocket-Accept: %s\r\n",
+ b64_sha);
+
+ // 4. 添加用户自定义响应头(通过 fmt 参数)
+ if (fmt != NULL) mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, &ap);
+
+ // 5. 可选的子协议响应头
+ if (wsproto != NULL) {
+ mg_printf(c, "Sec-WebSocket-Protocol: %.*s\r\n", ...);
+ }
+
+ // 6. 结束响应头
+ mg_send(c, "\r\n", 2);
+}
+```
+
+**关键常量**:魔数 `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` 是 RFC 6455 第 4.2.2 节规定的固定值,用于防止跨协议攻击。
+
+### 4.4 握手时的进阶用法
+
+**添加自定义响应头**(如 Cookie、Token 等):
+
+```c
+mg_ws_upgrade(c, hm, "Set-Cookie: token=%s\r\nX-User: %s\r\n", token, username);
+```
+
+**子协议协商**:
+
+```c
+struct mg_str *proto = mg_http_get_header(hm, "Sec-WebSocket-Protocol");
+// Mongoose 会自动回显匹配的协议,也可手动处理
+mg_ws_upgrade(c, hm, NULL);
+```
+
+## 五、帧格式详解
+
+### 5.1 RFC 6455 帧结构
+
+```
+ 0 1 2 3
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
++-+-+-+-+-------+-+-------------+-------------------------------+
+|F|R|R|R| opcode|M| Payload len | Extended payload length |
+|I|S|S|S| (4) |A| (7) | (16/64) |
+|N|V|V|V| |S| | (if payload len==126/127) |
+| |1|2|3| |K| | |
++-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+| Extended payload length continued, if payload len == 127 |
++ - - - - - - - - - - - - - - - +-------------------------------+
+| |Masking-key, if MASK set to 1 |
++-------------------------------+-------------------------------+
+| Masking-key (continued) | Payload Data |
++-------------------------------- - - - - - - - - - - - - - - - +
+: Payload Data continued ... :
++ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+| Payload Data continued ... |
++---------------------------------------------------------------+
+```
+
+### 5.2 Mongoose 的帧头构建 (`mkhdr`, 第 96 行)
+
+```c
+static size_t mkhdr(size_t len, int op, bool is_client, uint8_t *buf) {
+ size_t n = 0;
+ buf[0] = (uint8_t) (op | 128); // FIN=1, Opcode=op
+ if (len < 126) { // 7位长度足够
+ buf[1] = (unsigned char) len;
+ n = 2;
+ } else if (len < 65536) { // 16位扩展长度
+ uint16_t tmp = mg_htons((uint16_t) len);
+ buf[1] = 126;
+ memcpy(&buf[2], &tmp, sizeof(tmp));
+ n = 4;
+ } else { // 64位扩展长度
+ buf[1] = 127;
+ // 先写高32位,再写低32位
+ tmp = mg_htonl((uint32_t) (((uint64_t) len) >> 32));
+ memcpy(&buf[2], &tmp, sizeof(tmp));
+ tmp = mg_htonl((uint32_t) (len & 0xffffffffU));
+ memcpy(&buf[6], &tmp, sizeof(tmp));
+ n = 10;
+ }
+ // 客户端帧需要掩码
+ if (is_client) {
+ buf[1] |= 1 << 7; // 设置 MASK 位
+ mg_random(&buf[n], 4); // 生成随机掩码 key
+ n += 4;
+ }
+ return n;
+}
+```
+
+**注意**:**服务端发送的帧不需要掩码**(RFC 6455 第 5.1 节)。只有客户端发送到服务端的帧才需要掩码。Mongoose 通过 `is_client` 标志来控制。
+
+### 5.3 帧解析 (`ws_process`, 第 66 行)
+
+```c
+static size_t ws_process(uint8_t *buf, size_t len, struct ws_msg *msg) {
+ memset(msg, 0, sizeof(*msg));
+ if (len >= 2) {
+ n = buf[1] & 0x7f; // 7位载荷长度
+ mask_len = buf[1] & 128 ? 4 : 0; // MASK 位 → 掩码 key 长度
+ msg->flags = buf[0];
+
+ if (n < 126 && len >= mask_len) {
+ msg->data_len = n;
+ msg->header_len = 2 + mask_len;
+ } else if (n == 126 && len >= 4 + mask_len) {
+ msg->header_len = 4 + mask_len;
+ msg->data_len = (((size_t) buf[2]) << 8) | buf[3]; // 16位长度
+ } else if (len >= 10 + mask_len) {
+ msg->header_len = 10 + mask_len;
+ msg->data_len = be32(buf+2)<<32 | be32(buf+6); // 64位长度
+ }
+ }
+ // 安全检查:数据长度不能超过 1GB
+ if (msg->data_len > 1024 * 1024 * 1024) return 0;
+ if (msg->header_len + msg->data_len > len) return 0; // 数据不完整
+
+ // 如果有掩码,解码
+ if (mask_len > 0) {
+ uint8_t *p = buf + msg->header_len, *m = p - mask_len;
+ for (i = 0; i < msg->data_len; i++) p[i] ^= m[i & 3]; // XOR 解码
+ }
+ return msg->header_len + msg->data_len;
+}
+```
+
+### 5.4 掩码处理
+
+**为什么需要掩码**:RFC 6455 要求客户端发往服务端的所有帧必须掩码。这是为了防止"缓存投毒攻击"——恶意脚本通过浏览器发送精心构造的 WebSocket 帧,可能被中间代理缓存误解为 HTTP 请求。
+
+**解码算法**(异或):
+
+```
+for (i = 0; i < data_len; i++)
+ payload[i] = payload[i] ^ masking_key[i % 4]
+```
+
+**发送时的掩码**(`mg_ws_mask`, ws.c 第 124 行):
+
+```c
+static void mg_ws_mask(struct mg_connection *c, size_t len) {
+ if (c->is_client && c->send.buf != NULL) {
+ uint8_t *p = c->send.buf + c->send.len - len, *mask = p - 4;
+ for (i = 0; i < len; i++) p[i] ^= mask[i & 3];
+ }
+}
+```
+
+只在客户端模式(`c->is_client == true`)时对数据执行掩码。服务端不需要。
+
+## 六、协议处理器 `mg_ws_cb` 详解(ws.c 第 166 行)
+
+这是 WebSocket 连接的核心处理器,注册为 `c->pfn`,处理所有接收到的帧:
+
+```
+mg_ws_cb 处理流程(MG_EV_READ 事件时):
+
+ 1. 客户端模式:检查握手是否完成
+ → 未完成:调用 mg_ws_client_handshake() 验证 HTTP 101 响应
+ → 完成:继续解析帧
+
+ 2. 循环解析帧:
+ while (ws_process() 成功解析一帧) {
+
+ 3. 根据操作码分类处理:
+
+ ┌─ WEBSOCKET_OP_CONTINUE (0):
+ │ 分片帧 → 触发 MG_EV_WS_CTL
+ │
+ ├─ WEBSOCKET_OP_TEXT (1) / WEBSOCKET_OP_BINARY (2):
+ │ 如果 FIN=1 (完整帧):
+ │ → 触发 MG_EV_WS_MSG(用户在此接收消息)
+ │ 如果 FIN=0 (分片开始):
+ │ → 不触发事件(等待后续帧组装)
+ │
+ ├─ WEBSOCKET_OP_CLOSE (8):
+ │ → 触发 MG_EV_WS_CTL
+ │ → 回显 CLOSE 帧给对端
+ │ → 设置 c->is_draining = 1(优雅关闭)
+ │
+ ├─ WEBSOCKET_OP_PING (9):
+ │ → 自动回复 PONG
+ │ → 触发 MG_EV_WS_CTL(通知用户)
+ │
+ ├─ WEBSOCKET_OP_PONG (10):
+ │ → 触发 MG_EV_WS_CTL(用户可检测心跳响应)
+ │
+ └─ 未知操作码:
+ → mg_error() 关闭连接
+
+ 4. 分片帧处理(ws.c 第 215-233 行):
+ 如果 FIN=0 或 op=CONTINUE:
+ → 第一条分片帧:保留 1 字节 op 标记
+ → 后续帧:剥离帧头,保留数据
+ → 所有分片数据累积在 c->recv 中
+ 如果 FIN=1 且 op=CONTINUE:
+ → 分片结束,触发 MG_EV_WS_MSG
+ → 从 c->recv 中删除完整消息
+ }
+```
+
+### 分片帧的处理细节
+
+Mongoose 支持分片帧的自动组装:
+
+```
+客户端发送三条分片帧:
+ Frame 1: FIN=0, op=TEXT, data="Hello "
+ Frame 2: FIN=0, op=CONTINUE, data="World"
+ Frame 3: FIN=1, op=CONTINUE, data="!"
+
+Mongoose 内部处理:
+ 1. 收到 Frame 1:
+ → 保留 flags 在 c->recv 中 (buf[0]=0x01 TEXT)
+ → 剥离帧头,数据变为 "\x01Hello "
+ → ofs 追踪到数据末尾
+ 2. 收到 Frame 2:
+ → 剥离帧头,数据追加 → "\x01Hello World"
+ → ofs 更新
+ 3. 收到 Frame 3:
+ → 剥离帧头,数据追加 → "\x01Hello World!"
+ → FIN=1, op=CONTINUE: 触发 MG_EV_WS_MSG
+ → m.flags = c->recv.buf[0] (TEXT)
+ → m.data = "Hello World!" (跳过第1字节的标记)
+ → 删除已处理数据
+```
+
+## 七、发送函数详解
+
+### 7.1 `mg_ws_send()` — 发送一条完整消息(第 132 行)
+
+```c
+size_t mg_ws_send(struct mg_connection *c, const void *buf, size_t len, int op);
+```
+
+流程:
+1. 调用 `mkhdr()` 构建帧头
+2. 发送帧头(通过 `mg_send` 写入 `c->send` 缓冲区)
+3. 发送数据
+4. 如果是客户端模式 → 对数据执行掩码
+
+```c
+// 使用示例
+mg_ws_send(c, "hello", 5, WEBSOCKET_OP_TEXT); // 发送文本
+mg_ws_send(c, data, len, WEBSOCKET_OP_BINARY); // 发送二进制
+mg_ws_send(c, NULL, 0, WEBSOCKET_OP_PING); // 发送 Ping
+```
+
+### 7.2 `mg_ws_printf()` — 格式化发送(第 26 行)
+
+```c
+size_t mg_ws_printf(struct mg_connection *c, int op, const char *fmt, ...);
+
+// 使用示例
+mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{\"temp\":%.2f,\"unit\":\"%s\"}", 23.5, "C");
+```
+
+内部调用 `mg_vxprintf()` 格式化到 `c->send`,然后调用 `mg_ws_wrap()` 添加帧头。
+
+### 7.3 `mg_ws_wrap()` — 为已有数据添加帧头(第 289 行)
+
+```c
+size_t mg_ws_wrap(struct mg_connection *c, size_t len, int op);
+```
+
+这个是内部函数,用于为已经在 `c->send` 中的数据添加 WebSocket 帧头。适用场景:先写入 JSON 数据到 send 缓冲区,再包装成 WS 帧。
+
+```c
+// mg_ws_printf 的内部流程:
+c->send.len → mg_vxprintf 写入 JSON 数据
+ → mg_ws_wrap 在前面插入帧头
+ → mg_ws_mask 如果是客户端则掩码
+```
+
+## 八、服务端完整事件处理最佳实践
+
+### 8.1 标准事件处理模板
+
+```c
+static void fn(struct mg_connection *c, int ev, void *ev_data) {
+ // 1. TLS 初始化(如果需要 WSS)
+ if (ev == MG_EV_ACCEPT) {
+ struct mg_tls_opts opts = {
+ .cert = mg_str(s_cert_pem),
+ .key = mg_str(s_key_pem),
+ };
+ mg_tls_init(c, &opts);
+ }
+
+ // 2. WebSocket 握手
+ if (ev == MG_EV_HTTP_MSG) {
+ struct mg_http_message *hm = (struct mg_http_message *) ev_data;
+ struct mg_str *ws_key = mg_http_get_header(hm, "Sec-WebSocket-Key");
+ if (ws_key != NULL) {
+ // 可以在此做认证
+ // struct mg_str *token = mg_http_get_header(hm, "Authorization");
+ mg_ws_upgrade(c, hm, NULL); // 升级到 WebSocket
+ } else {
+ mg_http_reply(c, 200, "", "Use WebSocket\n");
+ }
+ }
+
+ // 3. WebSocket 连接建立
+ if (ev == MG_EV_WS_OPEN) {
+ struct mg_http_message *hm = (struct mg_http_message *) ev_data;
+ // hm 是发起升级的原始 HTTP 请求,可以获取 URI、Cookie 等
+ MG_INFO(("WebSocket connected, URI: %.*s", hm->uri.len, hm->uri.buf));
+ }
+
+ // 4. 接收 WebSocket 消息
+ if (ev == MG_EV_WS_MSG) {
+ struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
+ uint8_t op = wm->flags & 15;
+ if (op == WEBSOCKET_OP_TEXT) {
+ // 文本消息
+ MG_INFO(("TEXT: %.*s", wm->data.len, wm->data.buf));
+ // 回显
+ mg_ws_send(c, wm->data.buf, wm->data.len, WEBSOCKET_OP_TEXT);
+ } else if (op == WEBSOCKET_OP_BINARY) {
+ // 二进制消息
+ MG_INFO(("BINARY: %zu bytes", wm->data.len));
+ // 处理二进制数据
+ }
+ }
+
+ // 5. 控制帧通知
+ if (ev == MG_EV_WS_CTL) {
+ struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
+ uint8_t op = wm->flags & 15;
+ if (op == WEBSOCKET_OP_PING) {
+ MG_DEBUG(("Ping received"));
+ // Ping 已被 Mongoose 自动回复 Pong
+ } else if (op == WEBSOCKET_OP_PONG) {
+ MG_DEBUG(("Pong received"));
+ // 可用于检测客户端存活
+ } else if (op == WEBSOCKET_OP_CLOSE) {
+ MG_INFO(("Client sent close"));
+ }
+ }
+
+ // 6. 连接关闭
+ if (ev == MG_EV_CLOSE) {
+ MG_INFO(("WebSocket disconnected"));
+ }
+}
+```
+
+### 8.2 广播消息给多个客户端
+
+```c
+static void broadcast(struct mg_mgr *mgr, const char *msg, size_t len) {
+ struct mg_connection *c;
+ for (c = mgr->conns; c != NULL; c = c->next) {
+ if (c->is_websocket && !c->is_listening) { // 只发给 WS 客户端
+ mg_ws_send(c, msg, len, WEBSOCKET_OP_TEXT);
+ }
+ }
+}
+```
+
+### 8.3 心跳检测
+
+```c
+// 定时发送 Ping 帧检测客户端是否存活
+static void heartbeat_timer(void *arg) {
+ struct mg_mgr *mgr = (struct mg_mgr *) arg;
+ struct mg_connection *c;
+ for (c = mgr->conns; c != NULL; c = c->next) {
+ if (c->is_websocket && !c->is_listening) {
+ mg_ws_send(c, NULL, 0, WEBSOCKET_OP_PING);
+ }
+ }
+}
+
+// 在 main 中添加定时器
+mg_timer_add(&mgr, 30000, MG_TIMER_REPEAT, heartbeat_timer, &mgr);
+
+// 在事件处理器中检测 Pong
+if (ev == MG_EV_WS_CTL) {
+ struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
+ if ((wm->flags & 15) == WEBSOCKET_OP_PONG) {
+ // 客户端存活确认
+ }
+}
+```
+
+## 九、服务端 vs 客户端差异总结
+
+| 特性 | 服务端 | 客户端 |
+|------|--------|--------|
+| 帧掩码 | **不掩码** | **必须掩码**(4 字节随机 key + XOR) |
+| 握手方式 | `mg_ws_upgrade()` | `mg_ws_connect()` |
+| 握手角色 | 接收 `Sec-WebSocket-Key`,计算 `Accept` | 生成随机 `Key`,验证 `Accept` |
+| Ping/Pong | 可主动发送 | 可主动发送 |
+| 关闭帧 | 收到后回显 | 收到后回显 |
+| `is_client` | `false` | `true`(由 `mg_connect` 设置) |
+
+## 十、调试与诊断
+
+### 启用 hex dump
+
+```c
+// 在 MG_EV_ACCEPT 或 MG_EV_WS_OPEN 中设置
+c->is_hexdumping = 1;
+```
+
+这会将 WebSocket 帧的原始字节打印到日志中(通过 `mg_hexdump()`)。
+
+### 常见问题排查
+
+1. **客户端收到 "not http" 错误**:握手 URL 没有使用 `http://` 或 `https://` 前缀
+2. **握手被拒绝**:检查 `Sec-WebSocket-Key` 是否存在;URL 路径是否正确
+3. **消息收到乱码**:检查客户端是否正确掩码;操作码是否正确(TEXT=1, BINARY=2)
+4. **内存泄漏**:确保 `mg_mgr_poll()` 被循环调用,否则连接不会释放
+
+## 十一、MQTT over WebSocket
+
+Mongoose 支持 MQTT over WebSocket。当 MQTT 连接 URL 使用 `mqtt://` 方案且底层升级为 WebSocket 时,MQTT 模块会自动使用 WebSocket 帧封装 MQTT 包。这是服务端常用的场景——通过 WebSocket 传输 MQTT 协议。
diff --git a/claude/问题处理文档.md b/claude/问题处理文档.md
index 6b2cd25..c42da85 100644
--- a/claude/问题处理文档.md
+++ b/claude/问题处理文档.md
@@ -20,3 +20,37 @@
**涉及文件**:`myMms_m.h`, `mms_m.h`, `mms_m.cpp`, `iec61850m.cpp`
---
+
+### #2 libweb_server 模块缺陷修复
+
+**问题**:WebSocket 服务端模块存在 10 个缺陷,含严重级别(单客户端、多线程竞态、悬空指针、non-null-terminated UB)和高危/中等级别(调试 printf、SBO 阻塞、LOG 格式化缺失等)。
+
+**需求**:
+1. 兼容多个 WebSocket 客户端同时连接
+2. 完善断连处理
+3. 多线程安全保护
+4. 修复 UB 和格式化问题
+5. 增量推送优化(仅变化时发送)
+
+**处理计划**:[libweb_server模块分析](../工程/libweb_server模块分析.md)
+
+**状态**:✅ 已完成
+**涉及文件**:`web_server.cpp`, `ws_method.cpp`
+
+---
+
+### #3 libweb_server 多连接资源共享冲突
+
+**问题**:支持多客户端后,所有连接共享同一套全局信号资源(`g_ws_out_signals` 等),一个客户端的 add/del 操作会影响其他客户端的数据推送。
+
+**需求**:
+1. 每个连接独立的信号资源(per-connection session)
+2. 连接建立时自动开辟资源,断开时自动释放
+3. `ws_task()` 按 session 独立构建 JSON 推送到对应连接
+
+**处理计划**:[libweb_server模块分析](../工程/libweb_server模块分析.md)
+
+**状态**:✅ 已完成
+**涉及文件**:`ws_method.h`, `ws_method.cpp`, `web_server.cpp`
+
+---
diff --git a/release/src/protocol/libmoongoose/makefile b/release/src/protocol/libmongoose/makefile
similarity index 95%
rename from release/src/protocol/libmoongoose/makefile
rename to release/src/protocol/libmongoose/makefile
index da2ff81..cde171b 100644
--- a/release/src/protocol/libmoongoose/makefile
+++ b/release/src/protocol/libmongoose/makefile
@@ -1,7 +1,7 @@
# 包含外部Makefile
include ./../../../linux.mk
-ProjectName := libmoongoose
+ProjectName := libmongoose
DIR := $(realpath $(CURDIR)/..)
diff --git a/release/src/protocol/makefile b/release/src/protocol/makefile
index aa27fe1..5ad1bf0 100644
--- a/release/src/protocol/makefile
+++ b/release/src/protocol/makefile
@@ -6,7 +6,7 @@ SUBDIRS += ./libicp67
SUBDIRS += ./libmms_m
SUBDIRS += ./libmms_s
-SUBDIRS += ./libmoongoose
+SUBDIRS += ./libmongoose
diff --git a/release/src/system/RTU/makefile b/release/src/system/RTU/makefile
index 51f20d5..2978f36 100644
--- a/release/src/system/RTU/makefile
+++ b/release/src/system/RTU/makefile
@@ -16,7 +16,7 @@ MakeDirCommand := mkdir -p
# Libs := -lcom_channel -lcom_scan -liec -lself_ptl -lfunc -ltask -lcomm -lshell -lpthread
Libs := -lcom_channel -lcom_scan -liec -liec61850m -liec61850s -lself_ptl -lweb_server -ldatacenter
-Libs += -l60870 -licp67 -lmms_m -lmms_s -lmoongoose
+Libs += -l60870 -licp67 -lmms_m -lmms_s -lmongoose
Libs += -liec61850
Libs += -lmy_xxhash -lcmd -lmd5 -lxml -lcJSON -lcomm -ltask -lfunc
Libs += -lpthread
@@ -64,12 +64,15 @@ $(OutputFile): $(ALL_OBJECTS)
$(CPP) -o $@ $^ $(LDFLAGS)
@if [ -f "$@" ]; then \
echo "Build successful: $@"; \
- # 显示文件信息 \
file $@; \
- # 显示链接的库 \
echo "Linked libraries:"; \
ldd $@ 2>/dev/null || echo "(static binary)"; \
fi
+ @if [ "$(ARCH)" = "x86" ]; then \
+ $(MakeDirCommand) $(TEST_DIR); \
+ cp $(OutputFile) $(TEST_DIR)/$(ProjectName); \
+ echo "Copied to $(TEST_DIR)/$(ProjectName)"; \
+ fi
# 重新构建
rebuild: veryclean all
diff --git a/src/protocol/libmoongoose/inc/.gitkeep b/src/protocol/libmongoose/inc/.gitkeep
similarity index 100%
rename from src/protocol/libmoongoose/inc/.gitkeep
rename to src/protocol/libmongoose/inc/.gitkeep
diff --git a/src/protocol/libmoongoose/src/mongoose.c b/src/protocol/libmongoose/src/mongoose.c
similarity index 100%
rename from src/protocol/libmoongoose/src/mongoose.c
rename to src/protocol/libmongoose/src/mongoose.c
diff --git a/src/system/libweb_server/inc/ws_method.h b/src/system/libweb_server/inc/ws_method.h
index 906da00..641aff4 100644
--- a/src/system/libweb_server/inc/ws_method.h
+++ b/src/system/libweb_server/inc/ws_method.h
@@ -2,6 +2,10 @@
#include "myBase.h"
-void ws_recv(const char *p_rx, uint16_t rx_len);
-void ws_send(const char *p_tx, uint16_t tx_len);
-void ws_task(void);
\ No newline at end of file
+struct mg_connection;
+
+void ws_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len);
+void ws_send_all(const char *p_tx, uint16_t tx_len);
+void ws_send_one(unsigned long conn_id, const char *p_tx, uint16_t tx_len);
+void ws_session_destroy(struct mg_connection *c);
+void ws_task(void);
diff --git a/src/system/libweb_server/src/web_server.cpp b/src/system/libweb_server/src/web_server.cpp
index ed58c15..7220bf3 100644
--- a/src/system/libweb_server/src/web_server.cpp
+++ b/src/system/libweb_server/src/web_server.cpp
@@ -11,25 +11,55 @@ static const char *s_web_root = "web_root";
std::string g_web_root = "";
static struct mg_mgr mgr;
-static struct mg_connection *p_conn = NULL;
+LOCAL std::vector g_ws_conns;
+LOCAL pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_t thread_id;
-
-
-void ws_send(const char *p_tx, uint16_t tx_len)
+void ws_send_all(const char *p_tx, uint16_t tx_len)
{
- if(NULL == p_conn || NULL == p_tx || 0 == tx_len)
+ if(NULL == p_tx || 0 == tx_len)
{
- LOG_E("ws_send arg not valid, p_conn:%p, p_tx:%p, tx_len:%d", p_conn, p_tx, tx_len);
+ LOG_E("ws_send_all arg not valid, p_tx:%p, tx_len:%d", p_tx, tx_len);
return;
}
- mg_ws_send(p_conn, p_tx, tx_len, WEBSOCKET_OP_TEXT);
- // LOG_I("ws_send %d bytes: %s", tx_len, p_tx);
+ pthread_mutex_lock(&g_ws_conns_mutex);
+ for(uint32_t i = 0; i < g_ws_conns.size(); i++)
+ {
+ struct mg_connection *c = g_ws_conns[i];
+ if(c->is_websocket && !c->is_draining)
+ {
+ mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT);
+ }
+ }
+ pthread_mutex_unlock(&g_ws_conns_mutex);
}
-LOCAL void web_server_recv(const char *p_rx, uint16_t rx_len)
+
+void ws_send_one(unsigned long conn_id, const char *p_tx, uint16_t tx_len)
+{
+ if(NULL == p_tx || 0 == tx_len)
+ {
+ LOG_E("ws_send_one arg not valid, p_tx:%p, tx_len:%d", p_tx, tx_len);
+ return;
+ }
+
+ pthread_mutex_lock(&g_ws_conns_mutex);
+ for(uint32_t i = 0; i < g_ws_conns.size(); i++)
+ {
+ struct mg_connection *c = g_ws_conns[i];
+ if(c->id == conn_id && c->is_websocket && !c->is_draining)
+ {
+ mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT);
+ break;
+ }
+ }
+ pthread_mutex_unlock(&g_ws_conns_mutex);
+}
+
+
+LOCAL void web_server_recv(struct mg_connection *c, const char *p_rx, uint16_t rx_len)
{
if(NULL == p_rx || 0 == rx_len)
{
@@ -37,9 +67,7 @@ LOCAL void web_server_recv(const char *p_rx, uint16_t rx_len)
return;
}
- // LOG_I("web_server_recv %d bytes: %s", rx_len, p_rx);
-
- ws_recv(p_rx, rx_len);
+ ws_recv(c, p_rx, rx_len);
}
LOCAL void web_server_task(struct mg_connection *c, int ev, void *p_data)
@@ -60,12 +88,63 @@ LOCAL void web_server_task(struct mg_connection *c, int ev, void *p_data)
}
else if(ev == MG_EV_WS_MSG)
{
- p_conn = c;
struct mg_ws_message *wm = (struct mg_ws_message *) p_data;
- printf("Got %d bytes: %s\n", (int) wm->data.len, wm->data.buf);
-
- web_server_recv(wm->data.buf, (int)wm->data.len);
+ bool found = false;
+ pthread_mutex_lock(&g_ws_conns_mutex);
+ for(uint32_t i = 0; i < g_ws_conns.size(); i++)
+ {
+ if(g_ws_conns[i] == c)
+ {
+ found = true;
+ break;
+ }
+ }
+ if(!found)
+ {
+ g_ws_conns.push_back(c);
+ }
+ pthread_mutex_unlock(&g_ws_conns_mutex);
+
+ std::string msg(wm->data.buf, wm->data.len);
+ LOG_I("WS recv %zu bytes: %s", msg.size(), msg.c_str());
+
+ web_server_recv(c, msg.c_str(), (uint16_t)msg.size());
+ }
+ else if(ev == MG_EV_WS_CTL)
+ {
+ struct mg_ws_message *wm = (struct mg_ws_message *) p_data;
+ uint8_t op = wm->flags & 15;
+ if(op == WEBSOCKET_OP_CLOSE)
+ {
+ ws_session_destroy(c);
+
+ pthread_mutex_lock(&g_ws_conns_mutex);
+ for(uint32_t i = 0; i < g_ws_conns.size(); i++)
+ {
+ if(g_ws_conns[i] == c)
+ {
+ g_ws_conns.erase(g_ws_conns.begin() + i);
+ break;
+ }
+ }
+ pthread_mutex_unlock(&g_ws_conns_mutex);
+ }
+ }
+ else if(ev == MG_EV_CLOSE)
+ {
+ ws_session_destroy(c);
+
+ pthread_mutex_lock(&g_ws_conns_mutex);
+ for(uint32_t i = 0; i < g_ws_conns.size(); i++)
+ {
+ if(g_ws_conns[i] == c)
+ {
+ g_ws_conns.erase(g_ws_conns.begin() + i);
+ break;
+ }
+ }
+ pthread_mutex_unlock(&g_ws_conns_mutex);
}
}
@@ -137,8 +216,8 @@ void *app_web_server(void *arg)
while (1)
{
- task_event_recv(p_app->p_event,
- EV_TIMER1 | EV_TIMER2 | EV_TIMER3,
+ 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);
@@ -160,7 +239,7 @@ void *app_web_server(void *arg)
ws_task();
}
}
-
-}
\ No newline at end of file
+
+}
diff --git a/src/system/libweb_server/src/ws_method.cpp b/src/system/libweb_server/src/ws_method.cpp
index 598394c..3237d50 100644
--- a/src/system/libweb_server/src/ws_method.cpp
+++ b/src/system/libweb_server/src/ws_method.cpp
@@ -3,6 +3,7 @@
#include "cJSON.h"
#include "mySystem.h"
#include "myDatacenter.h"
+#include "mongoose.h"
typedef struct
{
@@ -10,56 +11,62 @@ typedef struct
std::string desc;
uint8_t data_type;
uint8_t ctrl_type;
- // void *p_data;
std::vector vec_p_data;
std::vector vec_p_default_data;
stru_signal_ctrl *p_ctrl;
stru_signal_param *p_param;
+ std::string last_val;
}stru_ws_signal;
-LOCAL std::vector g_ws_out_signals;
-LOCAL std::vector g_ws_in_signals;
-LOCAL std::vector g_ws_yk_signals;
-LOCAL std::vector g_ws_ao_signals;
-LOCAL std::vector g_ws_param_signals;
+typedef struct
+{
+ std::vector out_signals;
+ std::vector in_signals;
+ std::vector yk_signals;
+ std::vector ao_signals;
+ std::vector param_signals;
+}stru_ws_session;
+
+LOCAL std::map g_ws_sessions;
+LOCAL pthread_mutex_t g_ws_session_mutex = PTHREAD_MUTEX_INITIALIZER;
-LOCAL void add_out_signal(const std::string& saddr);
-LOCAL void add_in_signal(const std::string& saddr);
-LOCAL void add_yk_signal(const std::string& saddr);
-LOCAL void add_ao_signal(const std::string& saddr);
-LOCAL void add_param_signal(const std::string& saddr);
+LOCAL void add_out_signal(stru_ws_session &s, const std::string& saddr);
+LOCAL void add_in_signal(stru_ws_session &s, const std::string& saddr);
+LOCAL void add_yk_signal(stru_ws_session &s, const std::string& saddr);
+LOCAL void add_ao_signal(stru_ws_session &s, const std::string& saddr);
+LOCAL void add_param_signal(stru_ws_session &s, const std::string& saddr);
-LOCAL void del_out_signal(const std::string& saddr);
-LOCAL void del_in_signal(const std::string& saddr);
-LOCAL void del_yk_signal(const std::string& saddr);
-LOCAL void del_ao_signal(const std::string& saddr);
-LOCAL void del_param_signal(const std::string& saddr);
+LOCAL void del_out_signal(stru_ws_session &s, const std::string& saddr);
+LOCAL void del_in_signal(stru_ws_session &s, const std::string& saddr);
+LOCAL void del_yk_signal(stru_ws_session &s, const std::string& saddr);
+LOCAL void del_ao_signal(stru_ws_session &s, const std::string& saddr);
+LOCAL void del_param_signal(stru_ws_session &s, const std::string& saddr);
-LOCAL void set_out_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
-LOCAL void set_in_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
-LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
-LOCAL void set_ao_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
-LOCAL void set_param_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
+LOCAL void set_out_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
+LOCAL void set_in_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
+LOCAL void set_yk_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
+LOCAL void set_ao_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
+LOCAL void set_param_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
-LOCAL int make_out_signal_json(cJSON *root);
-LOCAL int make_in_signal_json(cJSON *root);
-LOCAL int make_yk_signal_json(cJSON *root);
-LOCAL int make_ao_signal_json(cJSON *root);
-LOCAL int make_param_signal_json(cJSON *root);
+LOCAL int make_out_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
+LOCAL int make_in_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
+LOCAL int make_yk_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
+LOCAL int make_ao_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
+LOCAL int make_param_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
typedef struct
{
- void (*add_signal)(const std::string& saddr);
- void (*del_signal)(const std::string& saddr);
- void (*set_signal_data)(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
- int (*make_signal_json)(cJSON *root);
+ void (*add_signal)(stru_ws_session &s, const std::string& saddr);
+ void (*del_signal)(stru_ws_session &s, const std::string& saddr);
+ void (*set_signal_data)(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val);
+ int (*make_signal_json)(stru_ws_session &s, cJSON *root, bool &has_change);
}stru_ws_signal_method;
-LOCAL std::map g_ws_signal_methods_map =
+LOCAL std::map g_ws_signal_methods_map =
{
{"out", {add_out_signal, del_out_signal, set_out_signal_data, make_out_signal_json}},
{"in", {add_in_signal, del_in_signal, set_in_signal_data, make_in_signal_json}},
@@ -83,22 +90,21 @@ LOCAL int signal_is_exist(const std::string& saddr, const std::vector *p_signals = &g_ws_out_signals;
- stru_ws_signal *p = nullptr;
+ std::vector *p_signals = &s.out_signals;
+ stru_ws_signal *p = nullptr;
- if(0 == signal_is_exist(saddr, g_ws_out_signals))
+ if(0 == signal_is_exist(saddr, s.out_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
}
- p_signals->reserve(p_signals->size() + 1);
p_signals->push_back({saddr, "", 0, 0});
p = &p_signals->back();
void *p_data = nullptr;
-
+
if(0 != dc_get_out_signal_info(saddr, p->desc, p->data_type, &p_data))
{
LOG_E("add_signals: dc_get_out_signal_info failed, saddr = %s", saddr.c_str());
@@ -111,12 +117,12 @@ LOCAL void add_out_signal(const std::string& saddr)
p->vec_p_data.push_back(p_data);
}
-LOCAL void add_in_signal(const std::string& saddr)
+LOCAL void add_in_signal(stru_ws_session &s, const std::string& saddr)
{
- std::vector *p_signals = &g_ws_in_signals;
+ std::vector *p_signals = &s.in_signals;
stru_ws_signal *p = nullptr;
- if(0 == signal_is_exist(saddr, g_ws_in_signals))
+ if(0 == signal_is_exist(saddr, s.in_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
@@ -137,16 +143,16 @@ LOCAL void add_in_signal(const std::string& saddr)
p->vec_p_data.push_back(p_data);
}
-LOCAL void add_yk_signal(const std::string& saddr)
+LOCAL void add_yk_signal(stru_ws_session &s, const std::string& saddr)
{
- std::vector *p_signals = &g_ws_yk_signals;
+ std::vector *p_signals = &s.yk_signals;
stru_ws_signal *p = nullptr;
- if(0 == signal_is_exist(saddr, g_ws_yk_signals))
+ if(0 == signal_is_exist(saddr, s.yk_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
- }
+ }
p_signals->push_back({saddr, "", 0, 0});
p_signals->back().p_ctrl = new stru_signal_ctrl;
@@ -177,15 +183,15 @@ LOCAL void add_yk_signal(const std::string& saddr)
p->vec_p_default_data.clear();
p_signals->pop_back();
return;
- }
+ }
}
-LOCAL void add_ao_signal(const std::string& saddr)
+LOCAL void add_ao_signal(stru_ws_session &s, const std::string& saddr)
{
- std::vector *p_signals = &g_ws_ao_signals;
+ std::vector *p_signals = &s.ao_signals;
stru_ws_signal *p = nullptr;
- if(0 == signal_is_exist(saddr, g_ws_ao_signals))
+ if(0 == signal_is_exist(saddr, s.ao_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
@@ -212,7 +218,7 @@ LOCAL void add_ao_signal(const std::string& saddr)
}
p->vec_p_data.push_back(p_data);
p->vec_p_default_data.push_back(p_default_data);
-
+
p->p_ctrl->type = p->ctrl_type;
p->p_ctrl->step = SIGNAL_CTRL_STEP::READY;
p->p_ctrl->data_type = p->data_type;
@@ -227,15 +233,15 @@ LOCAL void add_ao_signal(const std::string& saddr)
p->vec_p_default_data.clear();
p_signals->pop_back();
return;
- }
+ }
}
-LOCAL void add_param_signal(const std::string& saddr)
+LOCAL void add_param_signal(stru_ws_session &s, const std::string& saddr)
{
- std::vector *p_signals = &g_ws_param_signals;
- stru_ws_signal *p = nullptr;
+ std::vector *p_signals = &s.param_signals;
+ stru_ws_signal *p = nullptr;
- if(0 == signal_is_exist(saddr, g_ws_param_signals))
+ if(0 == signal_is_exist(saddr, s.param_signals))
{
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
return;
@@ -244,7 +250,7 @@ LOCAL void add_param_signal(const std::string& saddr)
p_signals->back().p_ctrl = new stru_signal_ctrl;
p_signals->back().p_param = new stru_signal_param;
p = &p_signals->back();
-
+
if(0 != dc_get_param_signal_info(saddr, p->desc, p->data_type, p->p_param, p->ctrl_type, &p->vec_p_data, &p->vec_p_default_data))
{
LOG_E("add_signals: dc_get_param_signal_info failed, saddr = %s", saddr.c_str());
@@ -293,48 +299,48 @@ LOCAL void del_signal(std::vector &signals, const std::string& s
p_signal->vec_p_data.clear();
p_signal->vec_p_default_data.clear();
-
+
signals.erase(signals.begin() + i);
return;
}
- }
+ }
- LOG_E("del_signal: not found saddr = %s", saddr.c_str());
+ LOG_E("del_signal: not found saddr = %s", saddr.c_str());
}
-LOCAL void del_out_signal(const std::string& saddr)
+LOCAL void del_out_signal(stru_ws_session &s, const std::string& saddr)
{
- del_signal(g_ws_out_signals, saddr);
+ del_signal(s.out_signals, saddr);
}
-LOCAL void del_in_signal(const std::string& saddr)
+LOCAL void del_in_signal(stru_ws_session &s, const std::string& saddr)
{
- del_signal(g_ws_in_signals, saddr);
+ del_signal(s.in_signals, saddr);
}
-LOCAL void del_yk_signal(const std::string& saddr)
+LOCAL void del_yk_signal(stru_ws_session &s, const std::string& saddr)
{
- del_signal(g_ws_yk_signals, saddr);
+ del_signal(s.yk_signals, saddr);
}
-LOCAL void del_ao_signal(const std::string& saddr)
+LOCAL void del_ao_signal(stru_ws_session &s, const std::string& saddr)
{
- del_signal(g_ws_ao_signals, saddr);
+ del_signal(s.ao_signals, saddr);
}
-LOCAL void del_param_signal(const std::string& saddr)
+LOCAL void del_param_signal(stru_ws_session &s, const std::string& saddr)
{
- del_signal(g_ws_param_signals, saddr);
+ del_signal(s.param_signals, saddr);
}
-LOCAL void set_out_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
+LOCAL void set_out_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
uint8_t data[128] = {0};
- for(uint32_t i = 0; i < g_ws_out_signals.size(); i++)
+ for(uint32_t i = 0; i < s.out_signals.size(); i++)
{
- stru_ws_signal *p_signal = &g_ws_out_signals.at(i);
+ stru_ws_signal *p_signal = &s.out_signals.at(i);
if(p_signal->saddr == saddr)
{
dc_set_signal_val_from_str(data, p_signal->data_type, val);
@@ -348,18 +354,18 @@ LOCAL void set_out_signal_data(const std::string& saddr, const uint8_t setting_z
}
}
-LOCAL void set_in_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
+LOCAL void set_in_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
LOG_E("signal_type is in, not support set_signal_data, saddr = %s", saddr.c_str());
}
-LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
+LOCAL void set_yk_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
uint8_t data[128] = {0};
-
- for(uint32_t i = 0; i < g_ws_yk_signals.size(); i++)
+
+ for(uint32_t i = 0; i < s.yk_signals.size(); i++)
{
- stru_ws_signal *p_signal = &g_ws_yk_signals.at(i);
+ stru_ws_signal *p_signal = &s.yk_signals.at(i);
if(p_signal->saddr == saddr)
{
dc_set_signal_val_from_str(data, p_signal->data_type, val);
@@ -383,8 +389,7 @@ LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zo
return;
}
- task_sleep_ms(1000);
- MY_LOG_I("saddr %s, select value %s success !!!", saddr.c_str(), val.c_str());
+ LOG_I("saddr %s, select value %s success !!!", saddr.c_str(), val.c_str());
if(0 != dc_signal_yk_set_status(saddr, SIGNAL_CTRL_STEP::DIRECT, *p_signal->p_ctrl, (void *)data))
{
@@ -392,12 +397,12 @@ LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zo
return;
}
- MY_LOG_I("saddr %s, direct value %s success !!!", saddr.c_str(), val.c_str());
+ LOG_I("saddr %s, direct value %s success !!!", saddr.c_str(), val.c_str());
return;
}
else
{
- LOG_E("p_signal->ctrl_type is invalid, p_signal->ctrl_type = %d");
+ LOG_E("p_signal->ctrl_type is invalid, p_signal->ctrl_type = %d", p_signal->ctrl_type);
return;
}
}
@@ -405,13 +410,13 @@ LOCAL void set_yk_signal_data(const std::string& saddr, const uint8_t setting_zo
}
-LOCAL void set_ao_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
+LOCAL void set_ao_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
uint8_t data[128] = {0};
-
- for(uint32_t i = 0; i < g_ws_ao_signals.size(); i++)
+
+ for(uint32_t i = 0; i < s.ao_signals.size(); i++)
{
- stru_ws_signal *p_signal = &g_ws_ao_signals.at(i);
+ stru_ws_signal *p_signal = &s.ao_signals.at(i);
if(p_signal->saddr == saddr)
{
dc_set_signal_val_from_str(data, p_signal->data_type, val);
@@ -437,7 +442,7 @@ LOCAL void set_ao_signal_data(const std::string& saddr, const uint8_t setting_zo
}
LOG_I("saddr %s, select, value %s success !!!", saddr.c_str(), val.c_str());
-
+
if(0 != dc_signal_ao_set_val(saddr, SIGNAL_CTRL_STEP::DIRECT, *p_signal->p_ctrl, (void *)data))
{
LOG_E("set_signal_data: dc_signal_ao_set_val direct failed, saddr = %s, val = %s", saddr.c_str(), val.c_str());
@@ -456,17 +461,17 @@ LOCAL void set_ao_signal_data(const std::string& saddr, const uint8_t setting_zo
}
}
-LOCAL void set_param_signal_data(const std::string& saddr, const uint8_t setting_zone, const std::string& val)
+LOCAL void set_param_signal_data(stru_ws_session &s, const std::string& saddr, const uint8_t setting_zone, const std::string& val)
{
uint8_t data[128] = {0};
- for(uint32_t i = 0; i < g_ws_param_signals.size(); i++)
+ for(uint32_t i = 0; i < s.param_signals.size(); i++)
{
- stru_ws_signal *p_signal = &g_ws_param_signals.at(i);
+ stru_ws_signal *p_signal = &s.param_signals.at(i);
if(p_signal->saddr == saddr)
{
dc_set_signal_val_from_str(data, p_signal->data_type, val);
-
+
if(p_signal->ctrl_type == SIGNAL_CTRL_TYPE::DIRECT_NORMAL)
{
if(0 != dc_signal_param_set_val(saddr, SIGNAL_CTRL_STEP::DIRECT, *p_signal->p_ctrl, setting_zone, (void *)data))
@@ -499,7 +504,7 @@ LOCAL void set_param_signal_data(const std::string& saddr, const uint8_t setting
}
else
{
- LOG_E("p_signal->ctrl_type is invalid, p_signal->ctrl_type = %d");
+ LOG_E("p_signal->ctrl_type is invalid, p_signal->ctrl_type = %d", p_signal->ctrl_type);
return;
}
}
@@ -508,7 +513,7 @@ LOCAL void set_param_signal_data(const std::string& saddr, const uint8_t setting
-void ws_recv(const char* p_rx, uint16_t rx_len)
+void ws_recv(struct mg_connection *c, const char* p_rx, uint16_t rx_len)
{
if(nullptr == p_rx || 0 == rx_len)
{
@@ -522,14 +527,14 @@ void ws_recv(const char* p_rx, uint16_t rx_len)
LOG_E("ws_recv: cJSON_Parse failed, p_rx = %s", p_rx);
return;
}
-
+
if(!cJSON_IsObject(root))
{
LOG_E("ws_recv: root is not an object, p_rx = %s", p_rx);
cJSON_Delete(root);
return;
}
-
+
cJSON *saddr = cJSON_GetObjectItem(root, "saddr");
cJSON *signal_type = cJSON_GetObjectItem(root, "signal_type");
cJSON *signal_data = cJSON_GetObjectItem(root, "signal_data");
@@ -570,7 +575,7 @@ void ws_recv(const char* p_rx, uint16_t rx_len)
LOG_E("ws_recv: saddr is empty, p_rx = %s", p_rx);
return;
}
-
+
if(signal_type_str.empty())
{
LOG_E("ws_recv: signal_type is empty, p_rx = %s", p_rx);
@@ -589,32 +594,37 @@ void ws_recv(const char* p_rx, uint16_t rx_len)
return;
}
+ pthread_mutex_lock(&g_ws_session_mutex);
+
+ stru_ws_session &s = g_ws_sessions[c->id];
+
stru_ws_signal_method method = g_ws_signal_methods_map[signal_type_str];
if(0 == curd_str.compare("add"))
{
- method.add_signal(saddr_str);
+ method.add_signal(s, saddr_str);
}
else if(0 == curd_str.compare("del"))
{
- method.del_signal(saddr_str);
+ method.del_signal(s, saddr_str);
}
else if(0 == curd_str.compare("set"))
{
- method.set_signal_data(saddr_str, atoi(setting_zone_str.c_str()), signal_data_str);
+ method.set_signal_data(s, saddr_str, atoi(setting_zone_str.c_str()), signal_data_str);
}
-
+
+ pthread_mutex_unlock(&g_ws_session_mutex);
}
-LOCAL int make_out_signal_json(cJSON *root)
+LOCAL int make_out_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *out_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "out", out_arr);
- for(uint32_t i = 0; i < g_ws_out_signals.size(); i++)
+ for(uint32_t i = 0; i < s.out_signals.size(); i++)
{
- stru_ws_signal *p_signal = &g_ws_out_signals.at(i);
+ stru_ws_signal *p_signal = &s.out_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@@ -629,19 +639,25 @@ LOCAL int make_out_signal_json(cJSON *root)
std::string val = dc_get_signal_val(p_signal->vec_p_data[0], p_signal->data_type);
cJSON_AddItemToObject(item, "val", cJSON_CreateString(val.c_str()));
cJSON_AddItemToArray(out_arr, item);
+
+ if(val != p_signal->last_val)
+ {
+ p_signal->last_val = val;
+ has_change = true;
+ }
}
return 0;
}
-LOCAL int make_in_signal_json(cJSON *root)
+LOCAL int make_in_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *in_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "in", in_arr);
- for(uint32_t i = 0; i < g_ws_in_signals.size(); i++)
+ for(uint32_t i = 0; i < s.in_signals.size(); i++)
{
- stru_ws_signal *p_signal = &g_ws_in_signals.at(i);
+ stru_ws_signal *p_signal = &s.in_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@@ -655,18 +671,24 @@ LOCAL int make_in_signal_json(cJSON *root)
std::string val = dc_get_signal_val(p_signal->vec_p_data[0], p_signal->data_type);
cJSON_AddItemToObject(item, "val", cJSON_CreateString(val.c_str()));
cJSON_AddItemToArray(in_arr, item);
+
+ if(val != p_signal->last_val)
+ {
+ p_signal->last_val = val;
+ has_change = true;
+ }
}
return 0;
}
-LOCAL int make_yk_signal_json(cJSON *root)
+LOCAL int make_yk_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *yk_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "yk", yk_arr);
- for(uint32_t i = 0; i < g_ws_yk_signals.size(); i++)
+ for(uint32_t i = 0; i < s.yk_signals.size(); i++)
{
- stru_ws_signal *p_signal = &g_ws_yk_signals.at(i);
+ stru_ws_signal *p_signal = &s.yk_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@@ -681,19 +703,25 @@ LOCAL int make_yk_signal_json(cJSON *root)
cJSON_AddItemToObject(item, "val", cJSON_CreateString(val.c_str()));
cJSON_AddItemToObject(item, "ctrl_type", cJSON_CreateNumber(p_signal->ctrl_type));
cJSON_AddItemToArray(yk_arr, item);
+
+ if(val != p_signal->last_val)
+ {
+ p_signal->last_val = val;
+ has_change = true;
+ }
}
return 0;
}
-LOCAL int make_ao_signal_json(cJSON *root)
+LOCAL int make_ao_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *ao_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "ao", ao_arr);
-
- for(uint32_t i = 0; i < g_ws_ao_signals.size(); i++)
+
+ for(uint32_t i = 0; i < s.ao_signals.size(); i++)
{
- stru_ws_signal *p_signal = &g_ws_ao_signals.at(i);
+ stru_ws_signal *p_signal = &s.ao_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@@ -719,19 +747,25 @@ LOCAL int make_ao_signal_json(cJSON *root)
cJSON_AddItemToObject(item, "default_val", cJSON_CreateString(default_val.c_str()));
cJSON_AddItemToArray(ao_arr, item);
+
+ if(val != p_signal->last_val)
+ {
+ p_signal->last_val = val;
+ has_change = true;
+ }
}
return 0;
}
-LOCAL int make_param_signal_json(cJSON *root)
+LOCAL int make_param_signal_json(stru_ws_session &s, cJSON *root, bool &has_change)
{
cJSON *param_arr = cJSON_CreateArray();
cJSON_AddItemToObject(root, "param", param_arr);
- for(uint32_t i = 0; i < g_ws_param_signals.size(); i++)
+ for(uint32_t i = 0; i < s.param_signals.size(); i++)
{
- stru_ws_signal *p_signal = &g_ws_param_signals.at(i);
+ stru_ws_signal *p_signal = &s.param_signals.at(i);
cJSON *item = cJSON_CreateObject();
if(nullptr == item)
{
@@ -782,44 +816,87 @@ LOCAL int make_param_signal_json(cJSON *root)
void ws_task()
{
- if(g_ws_out_signals.empty() && g_ws_in_signals.empty() && g_ws_yk_signals.empty() && g_ws_ao_signals.empty() && g_ws_param_signals.empty())
- {
- return;
- }
-
std::vector signal_type_list = {"out", "in", "yk", "ao", "param"};
- cJSON *root = cJSON_CreateObject();
- if(nullptr == root)
- {
- LOG_E("ws_task: cJSON_CreateObject failed");
- return;
- }
+ pthread_mutex_lock(&g_ws_session_mutex);
- for(uint32_t i = 0; i < signal_type_list.size(); i++)
+ for(auto &pair : g_ws_sessions)
{
- if(g_ws_signal_methods_map.find(signal_type_list[i]) != g_ws_signal_methods_map.end())
+ unsigned long conn_id = pair.first;
+ stru_ws_session &s = pair.second;
+
+ if(s.out_signals.empty() && s.in_signals.empty() && s.yk_signals.empty() && s.ao_signals.empty() && s.param_signals.empty())
{
- if(0 != g_ws_signal_methods_map[signal_type_list[i]].make_signal_json(root))
- {
- cJSON_Delete(root);
+ continue;
+ }
- LOG_E("ws_task: make_signal_json %s failed", signal_type_list[i].c_str());
- return;
+ cJSON *root = cJSON_CreateObject();
+ if(nullptr == root)
+ {
+ LOG_E("ws_task: cJSON_CreateObject failed");
+ continue;
+ }
+
+ bool has_change = false;
+
+ for(uint32_t i = 0; i < signal_type_list.size(); i++)
+ {
+ if(g_ws_signal_methods_map.find(signal_type_list[i]) != g_ws_signal_methods_map.end())
+ {
+ if(0 != g_ws_signal_methods_map[signal_type_list[i]].make_signal_json(s, root, has_change))
+ {
+ cJSON_Delete(root);
+
+ LOG_E("ws_task: make_signal_json %s failed", signal_type_list[i].c_str());
+ root = nullptr;
+ break;
+ }
}
}
+
+ if(nullptr == root)
+ {
+ continue;
+ }
+
+ if(!has_change)
+ {
+ cJSON_Delete(root);
+ continue;
+ }
+
+ char *p_tx = cJSON_Print(root);
+ if(nullptr == p_tx)
+ {
+ LOG_E("ws_task: cJSON_Print failed");
+ cJSON_Delete(root);
+ continue;
+ }
+
+ ws_send_one(conn_id, p_tx, strlen(p_tx));
+
+ cJSON_Delete(root);
+ free(p_tx);
}
- char *p_tx = cJSON_Print(root);
- if(nullptr == p_tx)
+ pthread_mutex_unlock(&g_ws_session_mutex);
+}
+
+
+void ws_session_destroy(struct mg_connection *c)
+{
+ if(nullptr == c)
{
- LOG_E("ws_task: cJSON_Print failed");
- cJSON_Delete(root);
return;
}
- ws_send(p_tx, strlen(p_tx));
+ pthread_mutex_lock(&g_ws_session_mutex);
- cJSON_Delete(root);
- free(p_tx);
-}
\ No newline at end of file
+ auto it = g_ws_sessions.find(c->id);
+ if(it != g_ws_sessions.end())
+ {
+ g_ws_sessions.erase(it);
+ }
+
+ pthread_mutex_unlock(&g_ws_session_mutex);
+}