<修改> 1、调整webserver模块,增加多连接,各连接单资源,连接断开释放资源
This commit is contained in:
parent
e18f239d77
commit
a3ae78e5e8
|
|
@ -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
|
||||||
|
<Config>
|
||||||
|
<St> <!-- 遥信信号 -->
|
||||||
|
<Signal link="st.0" />
|
||||||
|
</St>
|
||||||
|
<Mx> <!-- 遥测信号 -->
|
||||||
|
<Signal link="mx.0" />
|
||||||
|
</Mx>
|
||||||
|
<Co> <!-- 遥控信号 -->
|
||||||
|
<Signal link="co.0" />
|
||||||
|
</Co>
|
||||||
|
<Ao> <!-- 定值信号 -->
|
||||||
|
<Signal link="ao.0" />
|
||||||
|
</Ao>
|
||||||
|
<Param> <!-- 参数信号 -->
|
||||||
|
<Signal link="param.0" />
|
||||||
|
</Param>
|
||||||
|
</Config>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 数据结构
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef struct {
|
||||||
|
std::vector<stru_mms_s_signal_base> vec_st; // 遥信
|
||||||
|
std::vector<stru_mms_s_signal_base> vec_mx; // 遥测
|
||||||
|
std::vector<stru_mms_s_signal_base> vec_co; // 遥控
|
||||||
|
std::vector<stru_mms_s_signal_base> vec_ao; // 定值
|
||||||
|
std::vector<stru_mms_s_signal_base> 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<stru_local_st_mx>` — {base, p_data} |
|
||||||
|
| 遥测 | `g_vec_mx` | `vector<stru_local_st_mx>` — {base, p_data} |
|
||||||
|
| 遥控 | `g_vec_control` | `vector<stru_mms_s_control>` — {base, p_data} |
|
||||||
|
| 定值 | `g_vec_setting` | `vector<stru_mms_s_setting>` — {base, p_data} |
|
||||||
|
| 参数 | `g_vec_param` | `vector<stru_mms_s_param>` — {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)` | 打印配置内容(调试) |
|
||||||
|
|
@ -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 等)更新时会被跳过。
|
||||||
|
|
@ -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<mg_connection*> 活跃 WebSocket 连接列表
|
||||||
|
g_ws_conns_mutex → pthread 互斥锁,保护 g_ws_conns
|
||||||
|
g_ws_sessions → map<conn_id, stru_ws_session> 每连接独立信号资源(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<stru_ws_signal> out_signals; // 本连接订阅的遥信
|
||||||
|
vector<stru_ws_signal> in_signals; // 本连接订阅的遥测
|
||||||
|
vector<stru_ws_signal> yk_signals; // 本连接订阅的遥控
|
||||||
|
vector<stru_ws_signal> ao_signals; // 本连接订阅的定值
|
||||||
|
vector<stru_ws_signal> param_signals; // 本连接订阅的参数
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
全局 `g_ws_sessions: map<conn_id, stru_ws_session>` 管理所有连接的独立资源。`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<mg_connection*>`,遍历广播 |
|
||||||
|
| 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<conn_id, session>` 管理 |
|
||||||
|
| — | `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 | 释放连接对应的所有信号资源 |
|
||||||
|
|
@ -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 协议。
|
||||||
|
|
@ -20,3 +20,37 @@
|
||||||
**涉及文件**:`myMms_m.h`, `mms_m.h`, `mms_m.cpp`, `iec61850m.cpp`
|
**涉及文件**:`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`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# 包含外部Makefile
|
# 包含外部Makefile
|
||||||
include ./../../../linux.mk
|
include ./../../../linux.mk
|
||||||
|
|
||||||
ProjectName := libmoongoose
|
ProjectName := libmongoose
|
||||||
|
|
||||||
|
|
||||||
DIR := $(realpath $(CURDIR)/..)
|
DIR := $(realpath $(CURDIR)/..)
|
||||||
|
|
@ -6,7 +6,7 @@ SUBDIRS += ./libicp67
|
||||||
|
|
||||||
SUBDIRS += ./libmms_m
|
SUBDIRS += ./libmms_m
|
||||||
SUBDIRS += ./libmms_s
|
SUBDIRS += ./libmms_s
|
||||||
SUBDIRS += ./libmoongoose
|
SUBDIRS += ./libmongoose
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 -lself_ptl -lfunc -ltask -lcomm -lshell -lpthread
|
||||||
|
|
||||||
Libs := -lcom_channel -lcom_scan -liec -liec61850m -liec61850s -lself_ptl -lweb_server -ldatacenter
|
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 += -liec61850
|
||||||
Libs += -lmy_xxhash -lcmd -lmd5 -lxml -lcJSON -lcomm -ltask -lfunc
|
Libs += -lmy_xxhash -lcmd -lmd5 -lxml -lcJSON -lcomm -ltask -lfunc
|
||||||
Libs += -lpthread
|
Libs += -lpthread
|
||||||
|
|
@ -64,12 +64,15 @@ $(OutputFile): $(ALL_OBJECTS)
|
||||||
$(CPP) -o $@ $^ $(LDFLAGS)
|
$(CPP) -o $@ $^ $(LDFLAGS)
|
||||||
@if [ -f "$@" ]; then \
|
@if [ -f "$@" ]; then \
|
||||||
echo "Build successful: $@"; \
|
echo "Build successful: $@"; \
|
||||||
# 显示文件信息 \
|
|
||||||
file $@; \
|
file $@; \
|
||||||
# 显示链接的库 \
|
|
||||||
echo "Linked libraries:"; \
|
echo "Linked libraries:"; \
|
||||||
ldd $@ 2>/dev/null || echo "(static binary)"; \
|
ldd $@ 2>/dev/null || echo "(static binary)"; \
|
||||||
fi
|
fi
|
||||||
|
@if [ "$(ARCH)" = "x86" ]; then \
|
||||||
|
$(MakeDirCommand) $(TEST_DIR); \
|
||||||
|
cp $(OutputFile) $(TEST_DIR)/$(ProjectName); \
|
||||||
|
echo "Copied to $(TEST_DIR)/$(ProjectName)"; \
|
||||||
|
fi
|
||||||
|
|
||||||
# 重新构建
|
# 重新构建
|
||||||
rebuild: veryclean all
|
rebuild: veryclean all
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
#include "myBase.h"
|
#include "myBase.h"
|
||||||
|
|
||||||
void ws_recv(const char *p_rx, uint16_t rx_len);
|
struct mg_connection;
|
||||||
void ws_send(const char *p_tx, uint16_t tx_len);
|
|
||||||
|
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);
|
void ws_task(void);
|
||||||
|
|
@ -11,25 +11,55 @@ static const char *s_web_root = "web_root";
|
||||||
std::string g_web_root = "";
|
std::string g_web_root = "";
|
||||||
static struct mg_mgr mgr;
|
static struct mg_mgr mgr;
|
||||||
|
|
||||||
static struct mg_connection *p_conn = NULL;
|
LOCAL std::vector<struct mg_connection *> g_ws_conns;
|
||||||
|
LOCAL pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
static pthread_t thread_id;
|
static pthread_t thread_id;
|
||||||
|
|
||||||
|
|
||||||
|
void ws_send_all(const char *p_tx, uint16_t tx_len)
|
||||||
|
|
||||||
void ws_send(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;
|
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)
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// LOG_I("web_server_recv %d bytes: %s", rx_len, p_rx);
|
ws_recv(c, p_rx, rx_len);
|
||||||
|
|
||||||
ws_recv(p_rx, rx_len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOCAL void web_server_task(struct mg_connection *c, int ev, void *p_data)
|
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)
|
else if(ev == MG_EV_WS_MSG)
|
||||||
{
|
{
|
||||||
p_conn = c;
|
|
||||||
struct mg_ws_message *wm = (struct mg_ws_message *) p_data;
|
struct mg_ws_message *wm = (struct mg_ws_message *) p_data;
|
||||||
|
|
||||||
printf("Got %d bytes: %s\n", (int) wm->data.len, wm->data.buf);
|
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);
|
||||||
|
|
||||||
web_server_recv(wm->data.buf, (int)wm->data.len);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "mySystem.h"
|
#include "mySystem.h"
|
||||||
#include "myDatacenter.h"
|
#include "myDatacenter.h"
|
||||||
|
#include "mongoose.h"
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
|
|
@ -10,53 +11,59 @@ typedef struct
|
||||||
std::string desc;
|
std::string desc;
|
||||||
uint8_t data_type;
|
uint8_t data_type;
|
||||||
uint8_t ctrl_type;
|
uint8_t ctrl_type;
|
||||||
// void *p_data;
|
|
||||||
std::vector<void *> vec_p_data;
|
std::vector<void *> vec_p_data;
|
||||||
std::vector<void *> vec_p_default_data;
|
std::vector<void *> vec_p_default_data;
|
||||||
stru_signal_ctrl *p_ctrl;
|
stru_signal_ctrl *p_ctrl;
|
||||||
stru_signal_param *p_param;
|
stru_signal_param *p_param;
|
||||||
|
std::string last_val;
|
||||||
}stru_ws_signal;
|
}stru_ws_signal;
|
||||||
|
|
||||||
LOCAL std::vector<stru_ws_signal> g_ws_out_signals;
|
typedef struct
|
||||||
LOCAL std::vector<stru_ws_signal> g_ws_in_signals;
|
{
|
||||||
LOCAL std::vector<stru_ws_signal> g_ws_yk_signals;
|
std::vector<stru_ws_signal> out_signals;
|
||||||
LOCAL std::vector<stru_ws_signal> g_ws_ao_signals;
|
std::vector<stru_ws_signal> in_signals;
|
||||||
LOCAL std::vector<stru_ws_signal> g_ws_param_signals;
|
std::vector<stru_ws_signal> yk_signals;
|
||||||
|
std::vector<stru_ws_signal> ao_signals;
|
||||||
|
std::vector<stru_ws_signal> param_signals;
|
||||||
|
}stru_ws_session;
|
||||||
|
|
||||||
|
LOCAL std::map<unsigned long, stru_ws_session> 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_out_signal(stru_ws_session &s, const std::string& saddr);
|
||||||
LOCAL void add_in_signal(const std::string& saddr);
|
LOCAL void add_in_signal(stru_ws_session &s, const std::string& saddr);
|
||||||
LOCAL void add_yk_signal(const std::string& saddr);
|
LOCAL void add_yk_signal(stru_ws_session &s, const std::string& saddr);
|
||||||
LOCAL void add_ao_signal(const std::string& saddr);
|
LOCAL void add_ao_signal(stru_ws_session &s, const std::string& saddr);
|
||||||
LOCAL void add_param_signal(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_out_signal(stru_ws_session &s, const std::string& saddr);
|
||||||
LOCAL void del_in_signal(const std::string& saddr);
|
LOCAL void del_in_signal(stru_ws_session &s, const std::string& saddr);
|
||||||
LOCAL void del_yk_signal(const std::string& saddr);
|
LOCAL void del_yk_signal(stru_ws_session &s, const std::string& saddr);
|
||||||
LOCAL void del_ao_signal(const std::string& saddr);
|
LOCAL void del_ao_signal(stru_ws_session &s, const std::string& saddr);
|
||||||
LOCAL void del_param_signal(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_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(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(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(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(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_out_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
|
||||||
LOCAL int make_in_signal_json(cJSON *root);
|
LOCAL int make_in_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
|
||||||
LOCAL int make_yk_signal_json(cJSON *root);
|
LOCAL int make_yk_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
|
||||||
LOCAL int make_ao_signal_json(cJSON *root);
|
LOCAL int make_ao_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
|
||||||
LOCAL int make_param_signal_json(cJSON *root);
|
LOCAL int make_param_signal_json(stru_ws_session &s, cJSON *root, bool &has_change);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
void (*add_signal)(const std::string& saddr);
|
void (*add_signal)(stru_ws_session &s, const std::string& saddr);
|
||||||
void (*del_signal)(const std::string& saddr);
|
void (*del_signal)(stru_ws_session &s, const std::string& saddr);
|
||||||
void (*set_signal_data)(const std::string& saddr, const uint8_t setting_zone, const std::string& val);
|
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)(cJSON *root);
|
int (*make_signal_json)(stru_ws_session &s, cJSON *root, bool &has_change);
|
||||||
}stru_ws_signal_method;
|
}stru_ws_signal_method;
|
||||||
|
|
||||||
LOCAL std::map<std::string, stru_ws_signal_method> g_ws_signal_methods_map =
|
LOCAL std::map<std::string, stru_ws_signal_method> g_ws_signal_methods_map =
|
||||||
|
|
@ -83,18 +90,17 @@ LOCAL int signal_is_exist(const std::string& saddr, const std::vector<stru_ws_si
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOCAL void add_out_signal(const std::string& saddr)
|
LOCAL void add_out_signal(stru_ws_session &s, const std::string& saddr)
|
||||||
{
|
{
|
||||||
std::vector<stru_ws_signal> *p_signals = &g_ws_out_signals;
|
std::vector<stru_ws_signal> *p_signals = &s.out_signals;
|
||||||
stru_ws_signal *p = nullptr;
|
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());
|
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
p_signals->reserve(p_signals->size() + 1);
|
|
||||||
p_signals->push_back({saddr, "", 0, 0});
|
p_signals->push_back({saddr, "", 0, 0});
|
||||||
p = &p_signals->back();
|
p = &p_signals->back();
|
||||||
void *p_data = nullptr;
|
void *p_data = nullptr;
|
||||||
|
|
@ -111,12 +117,12 @@ LOCAL void add_out_signal(const std::string& saddr)
|
||||||
p->vec_p_data.push_back(p_data);
|
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<stru_ws_signal> *p_signals = &g_ws_in_signals;
|
std::vector<stru_ws_signal> *p_signals = &s.in_signals;
|
||||||
stru_ws_signal *p = nullptr;
|
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());
|
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
|
||||||
return;
|
return;
|
||||||
|
|
@ -137,12 +143,12 @@ LOCAL void add_in_signal(const std::string& saddr)
|
||||||
p->vec_p_data.push_back(p_data);
|
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<stru_ws_signal> *p_signals = &g_ws_yk_signals;
|
std::vector<stru_ws_signal> *p_signals = &s.yk_signals;
|
||||||
stru_ws_signal *p = nullptr;
|
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());
|
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
|
||||||
return;
|
return;
|
||||||
|
|
@ -180,12 +186,12 @@ LOCAL void add_yk_signal(const std::string& saddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOCAL void add_ao_signal(const std::string& saddr)
|
LOCAL void add_ao_signal(stru_ws_session &s, const std::string& saddr)
|
||||||
{
|
{
|
||||||
std::vector<stru_ws_signal> *p_signals = &g_ws_ao_signals;
|
std::vector<stru_ws_signal> *p_signals = &s.ao_signals;
|
||||||
stru_ws_signal *p = nullptr;
|
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());
|
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
|
||||||
return;
|
return;
|
||||||
|
|
@ -230,12 +236,12 @@ LOCAL void add_ao_signal(const std::string& saddr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOCAL void add_param_signal(const std::string& saddr)
|
LOCAL void add_param_signal(stru_ws_session &s, const std::string& saddr)
|
||||||
{
|
{
|
||||||
std::vector<stru_ws_signal> *p_signals = &g_ws_param_signals;
|
std::vector<stru_ws_signal> *p_signals = &s.param_signals;
|
||||||
stru_ws_signal *p = nullptr;
|
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());
|
LOG_E("add_signals: signal is already exist, saddr = %s", saddr.c_str());
|
||||||
return;
|
return;
|
||||||
|
|
@ -302,39 +308,39 @@ LOCAL void del_signal(std::vector<stru_ws_signal> &signals, const std::string& s
|
||||||
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};
|
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)
|
if(p_signal->saddr == saddr)
|
||||||
{
|
{
|
||||||
dc_set_signal_val_from_str(data, p_signal->data_type, val);
|
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());
|
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};
|
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)
|
if(p_signal->saddr == saddr)
|
||||||
{
|
{
|
||||||
dc_set_signal_val_from_str(data, p_signal->data_type, val);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
task_sleep_ms(1000);
|
LOG_I("saddr %s, select value %s success !!!", saddr.c_str(), val.c_str());
|
||||||
MY_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))
|
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;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
else
|
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;
|
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};
|
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)
|
if(p_signal->saddr == saddr)
|
||||||
{
|
{
|
||||||
dc_set_signal_val_from_str(data, p_signal->data_type, val);
|
dc_set_signal_val_from_str(data, p_signal->data_type, val);
|
||||||
|
|
@ -456,13 +461,13 @@ 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};
|
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)
|
if(p_signal->saddr == saddr)
|
||||||
{
|
{
|
||||||
dc_set_signal_val_from_str(data, p_signal->data_type, val);
|
dc_set_signal_val_from_str(data, p_signal->data_type, val);
|
||||||
|
|
@ -499,7 +504,7 @@ LOCAL void set_param_signal_data(const std::string& saddr, const uint8_t setting
|
||||||
}
|
}
|
||||||
else
|
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;
|
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)
|
if(nullptr == p_rx || 0 == rx_len)
|
||||||
{
|
{
|
||||||
|
|
@ -589,32 +594,37 @@ void ws_recv(const char* p_rx, uint16_t rx_len)
|
||||||
return;
|
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];
|
stru_ws_signal_method method = g_ws_signal_methods_map[signal_type_str];
|
||||||
|
|
||||||
if(0 == curd_str.compare("add"))
|
if(0 == curd_str.compare("add"))
|
||||||
{
|
{
|
||||||
method.add_signal(saddr_str);
|
method.add_signal(s, saddr_str);
|
||||||
}
|
}
|
||||||
else if(0 == curd_str.compare("del"))
|
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"))
|
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 *out_arr = cJSON_CreateArray();
|
||||||
cJSON_AddItemToObject(root, "out", out_arr);
|
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();
|
cJSON *item = cJSON_CreateObject();
|
||||||
if(nullptr == item)
|
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);
|
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_AddItemToObject(item, "val", cJSON_CreateString(val.c_str()));
|
||||||
cJSON_AddItemToArray(out_arr, item);
|
cJSON_AddItemToArray(out_arr, item);
|
||||||
|
|
||||||
|
if(val != p_signal->last_val)
|
||||||
|
{
|
||||||
|
p_signal->last_val = val;
|
||||||
|
has_change = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
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 *in_arr = cJSON_CreateArray();
|
||||||
cJSON_AddItemToObject(root, "in", in_arr);
|
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();
|
cJSON *item = cJSON_CreateObject();
|
||||||
if(nullptr == item)
|
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);
|
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_AddItemToObject(item, "val", cJSON_CreateString(val.c_str()));
|
||||||
cJSON_AddItemToArray(in_arr, item);
|
cJSON_AddItemToArray(in_arr, item);
|
||||||
|
|
||||||
|
if(val != p_signal->last_val)
|
||||||
|
{
|
||||||
|
p_signal->last_val = val;
|
||||||
|
has_change = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
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 *yk_arr = cJSON_CreateArray();
|
||||||
cJSON_AddItemToObject(root, "yk", yk_arr);
|
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();
|
cJSON *item = cJSON_CreateObject();
|
||||||
if(nullptr == item)
|
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, "val", cJSON_CreateString(val.c_str()));
|
||||||
cJSON_AddItemToObject(item, "ctrl_type", cJSON_CreateNumber(p_signal->ctrl_type));
|
cJSON_AddItemToObject(item, "ctrl_type", cJSON_CreateNumber(p_signal->ctrl_type));
|
||||||
cJSON_AddItemToArray(yk_arr, item);
|
cJSON_AddItemToArray(yk_arr, item);
|
||||||
|
|
||||||
|
if(val != p_signal->last_val)
|
||||||
|
{
|
||||||
|
p_signal->last_val = val;
|
||||||
|
has_change = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
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 *ao_arr = cJSON_CreateArray();
|
||||||
cJSON_AddItemToObject(root, "ao", ao_arr);
|
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();
|
cJSON *item = cJSON_CreateObject();
|
||||||
if(nullptr == item)
|
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_AddItemToObject(item, "default_val", cJSON_CreateString(default_val.c_str()));
|
||||||
|
|
||||||
cJSON_AddItemToArray(ao_arr, item);
|
cJSON_AddItemToArray(ao_arr, item);
|
||||||
|
|
||||||
|
if(val != p_signal->last_val)
|
||||||
|
{
|
||||||
|
p_signal->last_val = val;
|
||||||
|
has_change = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0;
|
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 *param_arr = cJSON_CreateArray();
|
||||||
cJSON_AddItemToObject(root, "param", param_arr);
|
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();
|
cJSON *item = cJSON_CreateObject();
|
||||||
if(nullptr == item)
|
if(nullptr == item)
|
||||||
{
|
{
|
||||||
|
|
@ -782,44 +816,87 @@ LOCAL int make_param_signal_json(cJSON *root)
|
||||||
|
|
||||||
void ws_task()
|
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<std::string> signal_type_list = {"out", "in", "yk", "ao", "param"};
|
std::vector<std::string> signal_type_list = {"out", "in", "yk", "ao", "param"};
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_ws_session_mutex);
|
||||||
|
|
||||||
|
for(auto &pair : g_ws_sessions)
|
||||||
|
{
|
||||||
|
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())
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
cJSON *root = cJSON_CreateObject();
|
cJSON *root = cJSON_CreateObject();
|
||||||
if(nullptr == root)
|
if(nullptr == root)
|
||||||
{
|
{
|
||||||
LOG_E("ws_task: cJSON_CreateObject failed");
|
LOG_E("ws_task: cJSON_CreateObject failed");
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool has_change = false;
|
||||||
|
|
||||||
for(uint32_t i = 0; i < signal_type_list.size(); i++)
|
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(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(root))
|
if(0 != g_ws_signal_methods_map[signal_type_list[i]].make_signal_json(s, root, has_change))
|
||||||
{
|
{
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
|
|
||||||
LOG_E("ws_task: make_signal_json %s failed", signal_type_list[i].c_str());
|
LOG_E("ws_task: make_signal_json %s failed", signal_type_list[i].c_str());
|
||||||
return;
|
root = nullptr;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(nullptr == root)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!has_change)
|
||||||
|
{
|
||||||
|
cJSON_Delete(root);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
char *p_tx = cJSON_Print(root);
|
char *p_tx = cJSON_Print(root);
|
||||||
if(nullptr == p_tx)
|
if(nullptr == p_tx)
|
||||||
{
|
{
|
||||||
LOG_E("ws_task: cJSON_Print failed");
|
LOG_E("ws_task: cJSON_Print failed");
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
ws_send(p_tx, strlen(p_tx));
|
ws_send_one(conn_id, p_tx, strlen(p_tx));
|
||||||
|
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
free(p_tx);
|
free(p_tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_ws_session_mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void ws_session_destroy(struct mg_connection *c)
|
||||||
|
{
|
||||||
|
if(nullptr == c)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_lock(&g_ws_session_mutex);
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue