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); +}