<修改> 1、创建mimo分支,后续此分支使用小米mimo code开发
This commit is contained in:
parent
3bf70c5b6d
commit
f4b84506a6
|
|
@ -1,3 +0,0 @@
|
||||||
- [user-language](user_language.md) — 用户要求所有对话和思考过程使用中文显示
|
|
||||||
- [project-rules](project-rules.md) — 项目开发规范:编译/运行路径、排查流程、文档管理、plan归档到mid目录
|
|
||||||
- [code-style](code-style.md) — C/C++代码格式规范:Tab缩进、Allman括号、Yoda条件、空行规则
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
---
|
|
||||||
name: code-style
|
|
||||||
description: 项目 C/C++ 代码格式规范 — 缩进、括号、空格、空行、条件写法
|
|
||||||
metadata:
|
|
||||||
type: feedback
|
|
||||||
---
|
|
||||||
|
|
||||||
## 缩进
|
|
||||||
|
|
||||||
使用 **Tab 字符**缩进(显示宽度 4),不使用空格缩进。
|
|
||||||
|
|
||||||
## 括号风格(Allman 风格)
|
|
||||||
|
|
||||||
所有大括号 **独占一行**,包括函数、if、for、while、struct:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
LOCAL void check_sg_zone_change()
|
|
||||||
{
|
|
||||||
if(g_sg_zone_saddr.empty())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for(auto &setting : g_vec_setting)
|
|
||||||
{
|
|
||||||
if(strcmp(setting.base.saddr, zone_saddr) == 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
单条语句也保留大括号,不省略。
|
|
||||||
|
|
||||||
## 空格规则
|
|
||||||
|
|
||||||
- 关键字与括号之间 **不加空格**:`if(`, `for(`, `while(`, `switch(`
|
|
||||||
- 函数名与括号之间 **不加空格**:`func(args)`
|
|
||||||
- 指针声明:`type *name`(`*` 前有空格,后无空格)
|
|
||||||
- 引用声明:`type &name`
|
|
||||||
|
|
||||||
## Yoda 条件
|
|
||||||
|
|
||||||
常量写在比较运算符左侧:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
if(NULL == ptr) // ✓ 正确
|
|
||||||
if(0 != ret) // ✓ 正确
|
|
||||||
if(ptr == NULL) // ✗ 不符合项目风格
|
|
||||||
```
|
|
||||||
|
|
||||||
## 空行
|
|
||||||
|
|
||||||
- 函数之间:两行空行
|
|
||||||
- 逻辑块之间:一行空行
|
|
||||||
- `#include` 区块末尾:一行空行
|
|
||||||
|
|
||||||
## typedef struct
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
typedef struct
|
|
||||||
{
|
|
||||||
uint32_t field1;
|
|
||||||
uint8_t field2;
|
|
||||||
}stru_type_name;
|
|
||||||
```
|
|
||||||
|
|
||||||
结构体成员无缩进额外层级(与 `{` 对齐)。
|
|
||||||
|
|
||||||
## 变量声明
|
|
||||||
|
|
||||||
- 局部静态变量用 `LOCAL` 宏(= `static`)
|
|
||||||
- 全局变量用 `g_` 前缀
|
|
||||||
- 结构体用 `stru_` 前缀
|
|
||||||
- 枚举用 `enum_` 前缀,枚举值用 `ENUM_` 前缀
|
|
||||||
|
|
||||||
**Why:** 从项目中多个核心源文件(iec61850s.cpp、self_ptl.cpp、ws_method.cpp、dc_signal.cpp)提取的共识格式规范
|
|
||||||
**How to apply:** 新增代码和修改现有代码时必须遵守此格式。本次仅统一缩进/括号/空格/空行,命名问题暂不处理。
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
---
|
|
||||||
name: project-rules
|
|
||||||
description: 项目开发规范 — 编译/运行路径、排查流程、文档管理、plan归档要求
|
|
||||||
metadata:
|
|
||||||
node_type: memory
|
|
||||||
type: feedback
|
|
||||||
originSessionId: 5e65af11-b197-479e-a82f-c2e5b475a8c3
|
|
||||||
---
|
|
||||||
|
|
||||||
## 核心规则
|
|
||||||
|
|
||||||
1. **对话语言**:全程使用中文显示
|
|
||||||
2. **编译路径**:`./release/build.sh`(x86)或 `./release/build.sh arm`(ARM交叉编译)
|
|
||||||
3. **执行路径**:`./test/RTU`
|
|
||||||
4. **排查问题流程**:先重新读取对应位置的源码,再对照问题或打印信息排查,不要依赖记忆中的旧代码
|
|
||||||
5. **问题记录**:每次解决的问题都追加到 `claude/问题处理文档.md`
|
|
||||||
6. **文档目录**:
|
|
||||||
- `claude/mid/` — 存放中间文档、plan 文档
|
|
||||||
- `claude/工程/` — 存放按模块记录的项目工程文档
|
|
||||||
7. **每次读取项目工程时**,把读到的东西按模块生成文档记录到 `claude/工程/` 文件夹
|
|
||||||
8. **解决问题的 plan**:每次制定并执行 plan 解决问题后,将 plan 内容整理为文档存入 `claude/mid/` 目录
|
|
||||||
|
|
||||||
**Why:** 用户明确要求的项目开发规范和工作流程
|
|
||||||
**How to apply:** 每次操作前检查这些规则,特别是编译/运行路径和问题排查流程。问题解决后必须追加到问题处理文档,同时将 plan 归档到 mid 目录。
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,354 +0,0 @@
|
||||||
# libiec61850m 模块工程文档
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
libiec61850m 是 RTU 项目中 IEC 61850 MMS **客户端应用线程**,位于 `src/system/libiec61850m/`。它作为应用层桥接器,将底层的 `libmms_m` 封装库与 RTU 的**数据中心(Datacenter)**连接起来,实现完整的 IEC 61850 客户端功能。
|
|
||||||
|
|
||||||
### 在系统架构中的位置
|
|
||||||
|
|
||||||
```
|
|
||||||
app_iec61850m (应用线程)
|
|
||||||
↓ 调用 API
|
|
||||||
libiec61850m (本模块) ──── 信号注册/回调
|
|
||||||
↓ 调用 libmms_m API ↓ dc_signal_*
|
|
||||||
libmms_m (协议封装层) Datacenter (数据中心)
|
|
||||||
↓ IEC 61850 Client API
|
|
||||||
libiec61850 (第三方库)
|
|
||||||
↓ MMS/TCP
|
|
||||||
远端 IED 设备
|
|
||||||
```
|
|
||||||
|
|
||||||
### 核心职责
|
|
||||||
|
|
||||||
1. **配置解析**:将 XML 配置文件解析为 `stru_cfg` 结构
|
|
||||||
2. **信号注册**:将信号点注册到数据中心(out/yk/ao/param)
|
|
||||||
3. **数据桥接**:将 libmms_m 回调的数据转发到数据中心
|
|
||||||
4. **遥控/设值转换**:将数据中心的变化回调转为 libmms_m 事件
|
|
||||||
|
|
||||||
### 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
src/system/libiec61850m/
|
|
||||||
├── inc/
|
|
||||||
│ ├── iec61850m.h ← 模块主头文件(app 入口声明)
|
|
||||||
│ └── parse_xml.h ← XML 配置解析声明
|
|
||||||
└── src/
|
|
||||||
├── iec61850m.cpp ← 核心实现(~830 行)
|
|
||||||
└── parse_xml.cpp ← XML 解析实现(~320 行)
|
|
||||||
```
|
|
||||||
|
|
||||||
引用头文件 `release/inc/myMms_m.h`(数据结构定义)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. XML 配置文件解析(parse_xml.cpp)
|
|
||||||
|
|
||||||
### 1.1 XML 结构
|
|
||||||
|
|
||||||
```xml
|
|
||||||
<Root>
|
|
||||||
<Para desc="描述" host_ip="192.168.1.100" host_port="102" ied="IEDNAME"/>
|
|
||||||
<Point desc="描述">
|
|
||||||
<St> <!-- 遥信 -->
|
|
||||||
<Item no="1" saddr="..." desc="..." type="2" fc="0" LDev="TEMPLATE" LNode="GGIO1" DoName="Ind1"/>
|
|
||||||
</St>
|
|
||||||
<Mx> <!-- 遥测 -->
|
|
||||||
<Item no="1" saddr="..." desc="..." type="6" fc="1" LDev="TEMPLATE" LNode="MMXU1" DoName="TotW"/>
|
|
||||||
</Mx>
|
|
||||||
<Co> <!-- 遥控 -->
|
|
||||||
<Item no="1" saddr="..." desc="..." type="2" fc="12" LDev="TEMPLATE" LNode="GGIO1" DoName="SPCSO1" ctlModel="2"/>
|
|
||||||
</Co>
|
|
||||||
<Ao> <!-- 模拟输出/设值 -->
|
|
||||||
<Item no="1" saddr="..." desc="..." type="6" fc="12" LDev="TEMPLATE" LNode="GGIO1" DoName="AnOut1"/>
|
|
||||||
</Ao>
|
|
||||||
<Param> <!-- 参数/定值 -->
|
|
||||||
<Item no="1" saddr="..." desc="..." type="6" fc="6" LDev="PROT" LNode="PDIS1" DoName="PhStr"/>
|
|
||||||
</Param>
|
|
||||||
</Point>
|
|
||||||
</Root>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.2 元素/属性映射
|
|
||||||
|
|
||||||
| XML 元素 | 说明 |
|
|
||||||
|---------|------|
|
|
||||||
| `Root` | 根节点 |
|
|
||||||
| `Para` | 连接参数(IP、端口、IED 名) |
|
|
||||||
| `Point` | 信号点容器 |
|
|
||||||
| `St/Mx/Co/Ao/Param` | 5 种信号类型子节点 |
|
|
||||||
| `Item` | 单个信号点 |
|
|
||||||
|
|
||||||
| Item 属性 | 存储字段 | 说明 |
|
|
||||||
|----------|---------|------|
|
|
||||||
| `saddr` | `item.saddr` | 短地址(数据中心标识) |
|
|
||||||
| `desc` | `item.desc` | 描述 |
|
|
||||||
| `type` | `item.type` | MMS 数据类型(MMS_BOOLEAN=2, FLOAT=6, INTEGER=4 等) |
|
|
||||||
| `fc` | `item.fc` | 功能约束(ST=0, MX=1, CO=12, SG=6 等) |
|
|
||||||
| `LDev` | `item.ldev` | 逻辑设备名 |
|
|
||||||
| `LNode` | `item.lnode` | 逻辑节点名 |
|
|
||||||
| `DoName` | `item.doname` | 数据对象名 |
|
|
||||||
| `ctlModel` | `item.ctrl_model` | 控制模型(仅 Co 类型需要) |
|
|
||||||
|
|
||||||
**引用自动生成规则**:
|
|
||||||
```
|
|
||||||
reference = ied + LDev + "/" + LNode + "." + DoName
|
|
||||||
例如: "IEDNAMETEMPLATE/GGIO1.Ind1"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 公共 API
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// 解析 XML 配置文件,返回 stru_cfg*
|
|
||||||
stru_cfg *parse_cfg(const std::string &cfg_file);
|
|
||||||
|
|
||||||
// 打印配置内容到 stdout
|
|
||||||
void show_cfg(stru_cfg &cfg);
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 核心实现(iec61850m.cpp)
|
|
||||||
|
|
||||||
### 2.1 类型映射
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// MMS 类型 → 本地 DATA_TYPE_* 类型
|
|
||||||
LOCAL std::map<uint8_t, uint8_t> g_mms_m_type_to_local_type = {
|
|
||||||
{MMS_BOOLEAN, DATA_TYPE_U8},
|
|
||||||
{MMS_INTEGER, DATA_TYPE_S32},
|
|
||||||
{MMS_UNSIGNED, DATA_TYPE_U32},
|
|
||||||
{MMS_FLOAT, DATA_TYPE_F32},
|
|
||||||
{MMS_STRING, DATA_TYPE_STR},
|
|
||||||
};
|
|
||||||
|
|
||||||
// IEC 61850 控制模型 → RTU 本地控制类型
|
|
||||||
LOCAL std::map<uint8_t, uint8_t> g_mms_m_ctrl_type_to_local_ctrl_type = {
|
|
||||||
{CONTROL_MODEL_STATUS_ONLY, SIGNAL_CTRL_TYPE::NONE},
|
|
||||||
{CONTROL_MODEL_DIRECT_NORMAL, SIGNAL_CTRL_TYPE::DIRECT_NORMAL},
|
|
||||||
{CONTROL_MODEL_SBO_NORMAL, SIGNAL_CTRL_TYPE::SBO_NORMAL},
|
|
||||||
{CONTROL_MODEL_DIRECT_ENHANCED, SIGNAL_CTRL_TYPE::DIRECT_NORMAL},
|
|
||||||
{CONTROL_MODEL_SBO_ENHANCED, SIGNAL_CTRL_TYPE::SBO_NORMAL},
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 模块实例管理
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
typedef struct {
|
|
||||||
int fd; // mms_m 客户端句柄
|
|
||||||
const char *prj_name; // 项目/配置文件名(如 "mms_m.xml")
|
|
||||||
int debug; // 调试标志(0=关, 1=开)
|
|
||||||
uint32_t connectionTimeout; // 连接超时 [毫秒, 默认10000]
|
|
||||||
stru_mms_m_event mms_event; // 预分配的事件(用于遥控/设值)
|
|
||||||
stru_cfg *p_cfg; // 配置指针
|
|
||||||
} stru_iec61850m_info;
|
|
||||||
|
|
||||||
LOCAL std::vector<stru_iec61850m_info> g_vec_iec61850m_info = {
|
|
||||||
{
|
|
||||||
.fd = -1,
|
|
||||||
.prj_name = "mms_m.xml",
|
|
||||||
.debug = MMS_M_DEBUG_PRINT_OFF,
|
|
||||||
.connectionTimeout = 10000,
|
|
||||||
.mms_event = { .p_func = mms_event_back },
|
|
||||||
.p_cfg = NULL,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
当前只配置了**一个**客户端实例,配置文件路径为 `<进程目录>/config/MMS/mms_m.xml`。
|
|
||||||
|
|
||||||
### 2.3 初始化流程
|
|
||||||
|
|
||||||
```
|
|
||||||
app_iec61850m_init1()
|
|
||||||
├── dc_signal_out("iec61850m.run_cnt", ...) ← 注册运行计数
|
|
||||||
├── get_base_path() → g_61850m_prj_path ← "<进程目录>/config/MMS/"
|
|
||||||
│
|
|
||||||
└── iec61850m_init()
|
|
||||||
├── for each g_vec_iec61850m_info[i]:
|
|
||||||
│ ├── parse_cfg(g_61850m_prj_path + prj_name) ← 解析 XML
|
|
||||||
│ ├── show_cfg() ← 打印配置
|
|
||||||
│ │
|
|
||||||
│ ├── iec61850m_signal_init(*p_cfg) ← 注册信号到数据中心
|
|
||||||
│ │ ├── 为每个信号点分配数据内存 (dc_create_data_ptr_by_type)
|
|
||||||
│ │ ├── dc_signal_out() ← ST/MX 类型
|
|
||||||
│ │ ├── dc_signal_yk() ← CO 类型, 带 iec61850m_signal_co_change_callback
|
|
||||||
│ │ ├── dc_signal_ao() ← AO 类型, 带 iec61850m_signal_ao_change_callback
|
|
||||||
│ │ └── dc_signal_param() ← Param 类型, 带 iec61850m_signal_param_change_callback
|
|
||||||
│ │
|
|
||||||
│ ├── mms_m_out_init(p_cfg, debug, timeout) ← 启动 MMS 客户端
|
|
||||||
│ │
|
|
||||||
│ ├── mms_m_out_bind_param_zone_signal(fd, p_cfg->point.p_ao[0].saddr)
|
|
||||||
│ │ └── 将第一个 AO 信号绑定为定值区指示器
|
|
||||||
│ │
|
|
||||||
│ ├── mms_m_out_get_value(fd, mms_data_back) ← 注册数据回调
|
|
||||||
│ └── mms_m_out_get_connect_status(fd, iec61850m_connect_status) ← 注册状态回调
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.4 数据类型与信号注册详细
|
|
||||||
|
|
||||||
| 信号类型 | 数据中心函数 | 变化回调 | 参数说明 |
|
|
||||||
|---------|-------------|---------|---------|
|
|
||||||
| ST(遥信) | `dc_signal_out` | 无(只读输出) | p_val[0] 为值 |
|
|
||||||
| MX(遥测) | `dc_signal_out` | 无(只读输出) | p_val[0] 为值 |
|
|
||||||
| CO(遥控) | `dc_signal_yk` | `iec61850m_signal_co_change_callback` | ctrl_model 映射为本地类型 |
|
|
||||||
| AO(模拟输出) | `dc_signal_ao` | `iec61850m_signal_ao_change_callback` | 带 p_default[0] 默认值 |
|
|
||||||
| Param(参数) | `dc_signal_param` | `iec61850m_signal_param_change_callback` | 带 p_default[0], p_val[0..N-1] 多区值 |
|
|
||||||
|
|
||||||
### 2.5 数据回传链路(mms_data_back)
|
|
||||||
|
|
||||||
```
|
|
||||||
mms_data_back() ← libmms_m 数据回调入口
|
|
||||||
│
|
|
||||||
├── 按 reason 过滤:
|
|
||||||
│ 忽略: REASON_DATA_CHANGE, REASON_GI,
|
|
||||||
│ REASON_INTEGRITY, REASON_ALL_CALL
|
|
||||||
│ 处理: REASON_READ_AO, REASON_READ_PARAM
|
|
||||||
│
|
|
||||||
├── 打印日志(按 MMS 类型格式化)
|
|
||||||
│
|
|
||||||
└── 按信号类型匹配并写入 Datacenter:
|
|
||||||
├── ST 点匹配: dc_set_out_signal_val(saddr, p_value)
|
|
||||||
├── MX 点匹配: dc_set_out_signal_val(saddr, p_value)
|
|
||||||
├── AO 点匹配: dc_signal_ao_set_val_without_check(saddr, local_type, p_value)
|
|
||||||
└── Param 点匹配:
|
|
||||||
dc_signal_param_set_val_without_check(saddr, local_type, set_zone-1, p_value)
|
|
||||||
```
|
|
||||||
|
|
||||||
**注意**:`mms_data_back` 只处理 AO 和 Param 的**主动读取**结果。数据变化和报告上送的数据通过 libmms_m 的报告回调机制直接输出,但当前代码中报告回调仅打印日志,未对接数据中心。
|
|
||||||
|
|
||||||
### 2.6 遥控/设值下发链路
|
|
||||||
|
|
||||||
```
|
|
||||||
Datacenter 信号变化
|
|
||||||
│
|
|
||||||
├── CO: iec61850m_signal_co_change_callback()
|
|
||||||
│ └── iec61850m_signal_change_decode() ← 解析 step/data_type/value
|
|
||||||
│ └── mms_send_control() ← 构造 mms_event
|
|
||||||
│ └── mms_m_out_do_set_yk() ← 发送到 libmms_m
|
|
||||||
│
|
|
||||||
├── AO: iec61850m_signal_ao_change_callback()
|
|
||||||
│ └── 同上流程 (ctrl_type = _MMS_M_EVENT_AO_WRITE)
|
|
||||||
│
|
|
||||||
└── Param: iec61850m_signal_param_change_callback()
|
|
||||||
└── 同上流程 (ctrl_type = _MMS_M_EVENT_PARAM_WRITE, 传入 set_zone+1)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.7 连接上线后的处理
|
|
||||||
|
|
||||||
```
|
|
||||||
iec61850m_connect_status(fd, ON_LINE)
|
|
||||||
├── iec61850m_ext_demo(fd)
|
|
||||||
│ ├── mms_m_query_server(fd, ...) ← 查询 IED 信息
|
|
||||||
│ ├── mms_m_read_sg_info(fd, "PROT", ...) ← 读取定值组
|
|
||||||
│ └── 其他 ext demo(文件操作已注释)
|
|
||||||
│
|
|
||||||
├── mms_m_out_read_ao_or_params(fd, AO_READ, NULL) ← 读全部 AO
|
|
||||||
└── mms_m_out_read_ao_or_params(fd, PARAM_READ, NULL) ← 读全部参数
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.8 应用线程函数
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
void *app_iec61850m(void *arg)
|
|
||||||
{
|
|
||||||
// 标准 RTU app 线程模板
|
|
||||||
while (1) {
|
|
||||||
task_event_recv(p_event,
|
|
||||||
EV_TIMER1 | EV_TIMER2 | EV_TIMER3,
|
|
||||||
TASK_EVENT_FLAG_OR | TASK_EVENT_FLAG_CLEAR,
|
|
||||||
TASK_EVENT_WAIT_FOREVER,
|
|
||||||
&event);
|
|
||||||
|
|
||||||
if (event & EV_TIMER1) { ; } // 10ms 定时器(预留)
|
|
||||||
if (event & EV_TIMER2) { ; } // 100ms 定时器(预留)
|
|
||||||
if (event & EV_TIMER3) {
|
|
||||||
p_app->run_cnt++; // 1s 定时器:运行计数
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**注意**:app_iec61850m 线程本身不执行任何 MMS 操作。所有 MMS 通信由 libmms_m 内部的独立 `pthread_task` 线程驱动。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. CLI 调试命令
|
|
||||||
|
|
||||||
```bash
|
|
||||||
iec61850m info # 查看所有客户端实例信息
|
|
||||||
iec61850m yk <fd> <saddr> <val> <ctrl_type> # 手动遥控(框架已有,部分代码注释)
|
|
||||||
iec61850m set <fd> <saddr> <val> <set_type> # 手动设值(框架已有)
|
|
||||||
```
|
|
||||||
|
|
||||||
注册方式:`CMD_REGISTER("iec61850m", cmd_iec61850m, "iec61850客户端线程的控制命令")`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 线程模型
|
|
||||||
|
|
||||||
```
|
|
||||||
RTU 主进程
|
|
||||||
├── app_sys 线程 (ap_sys.cpp)
|
|
||||||
├── ...
|
|
||||||
│
|
|
||||||
├── app_iec61850m 线程 (本模块)
|
|
||||||
│ └── 3 个标准 RTU 定时器 (10ms / 100ms / 1000ms)
|
|
||||||
│ └── 仅 1000ms 定时器:p_app->run_cnt++
|
|
||||||
│
|
|
||||||
├── libmms_m 内部线程 (pthread_task) ← 由 mms_m_out_init() 创建
|
|
||||||
│ └── 主循环 300ms 周期
|
|
||||||
│ ├── 连接状态维护
|
|
||||||
│ ├── 事件处理
|
|
||||||
│ └── 4 个业务定时器 (120s/60s/30s/20s)
|
|
||||||
│
|
|
||||||
└── libiec61850 库内部线程 (IedConnection 线程模式)
|
|
||||||
└── MMS 报文收发、报告处理
|
|
||||||
```
|
|
||||||
|
|
||||||
**三层线程协作**:
|
|
||||||
|
|
||||||
| 层级 | 线程 | 职责 |
|
|
||||||
|------|------|------|
|
|
||||||
| RTU 应用层 | `app_iec61850m` | 信号注册、运行计数 |
|
|
||||||
| 协议封装层 | libmms_m `pthread_task` | 连接管理、事件调度、数据路由 |
|
|
||||||
| 第三方库层 | libiec61850 内部 | MMS 协议栈、报告分发 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 数据流向总览
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────────────────────┐
|
|
||||||
│ 远端 IED (服务端) │
|
|
||||||
└──────────┬───────────────────┘
|
|
||||||
│ MMS/TCP
|
|
||||||
┌──────────▼───────────────────┐
|
|
||||||
│ libiec61850 (第三方库) │
|
|
||||||
│ IedConnection + Report │
|
|
||||||
└──────────┬───────────────────┘
|
|
||||||
│
|
|
||||||
┌──────────────────┴──────────────────┐
|
|
||||||
│ libmms_m (封装层) │
|
|
||||||
│ ┌─────────────────────────────┐ │
|
|
||||||
│ │ mms_m_report_callback() │ │ ← 报告数据(当前仅打印)
|
|
||||||
│ │ mms_m_send_call_all() │ │ ← 周期总召
|
|
||||||
│ │ mms_m_send_read_ao/param() │ │ ← 读 AO/参数
|
|
||||||
│ │ mms_m_send_co() │ │ ← 遥控
|
|
||||||
│ │ mms_m_send_param_write() │ │ ← 设值
|
|
||||||
│ └──────────┬──────────────────┘ │
|
|
||||||
└─────────────┼───────────────────────┘
|
|
||||||
│ mms_m_out_value_cb
|
|
||||||
┌─────────────▼───────────────────────┐
|
|
||||||
│ libiec61850m (应用层) │
|
|
||||||
│ ┌──────────────────────────────┐ │
|
|
||||||
│ │ mms_data_back() │ │ ← 数据回调→Datacenter
|
|
||||||
│ │ iec61850m_signal_*_callback() │ │ ← Datacenter→遥控/设值
|
|
||||||
│ └──────────┬───────────────────┘ │
|
|
||||||
└─────────────┼───────────────────────┘
|
|
||||||
│ dc_signal_* / dc_set_*
|
|
||||||
┌─────────────▼───────────────────────┐
|
|
||||||
│ Datacenter (数据中心) │
|
|
||||||
│ out / in / yk / ao / param │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
@ -1,273 +0,0 @@
|
||||||
# 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)` | 打印配置内容(调试) |
|
|
||||||
|
|
@ -1,671 +0,0 @@
|
||||||
# libmms_m 模块工程文档
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
libmms_m 是 RTU 项目中 IEC 61850 MMS 客户端的**封装库**,位于 `src/protocol/libmms_m/`,负责将 libiec61850 的 C 客户端 API 封装成 RTU 内部的事件驱动架构。
|
|
||||||
|
|
||||||
### 核心职责
|
|
||||||
|
|
||||||
1. **连接管理**:通过异步方式与远端 IED 建立/维护 MMS 连接
|
|
||||||
2. **报告接收**:通过 Report 回调接收 IED 主动上送的变化数据
|
|
||||||
3. **数据操作**:总召、总召遥信遥测、AO 读写、参数(定值)读写
|
|
||||||
4. **控制指令**:遥控(CO)的 Select/Operate/Cancel 操作
|
|
||||||
5. **扩展服务**:文件传输、服务器信息查询、定值组读取
|
|
||||||
|
|
||||||
### 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
src/protocol/libmms_m/
|
|
||||||
├── inc/
|
|
||||||
│ ├── mms_m.h ← 核心头文件(日志宏、数据结构、API 声明)
|
|
||||||
│ ├── mms_m_ext.h ← 扩展功能接口声明(文件/服务器/定值组)
|
|
||||||
│ └── mms_m_errstr.h ← 错误码转字符串
|
|
||||||
└── src/
|
|
||||||
├── mms_m.cpp ← 核心实现(连接/报告/总召/遥控/参数)
|
|
||||||
├── mms_m_errstr.cpp ← 错误码/控制模型/AddCause 转字符串
|
|
||||||
├── mms_m_file.cpp ← 文件服务实现
|
|
||||||
├── mms_m_server.cpp ← 服务器身份/状态查询
|
|
||||||
└── mms_m_sg.cpp ← 定值组信息读取
|
|
||||||
```
|
|
||||||
|
|
||||||
**基础类型定义**位于 `release/inc/myMms_m.h`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 核心数据结构
|
|
||||||
|
|
||||||
### 1.1 全局对象管理
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
static std::map<int, stru_mms_m_obj *> g_mms_m_obj_map;
|
|
||||||
```
|
|
||||||
|
|
||||||
所有客户端实例通过全局 map 管理,key 为 `app_fd`(客户端句柄,从 1 开始自增),value 为对象指针。
|
|
||||||
|
|
||||||
### 1.2 主对象结构 `stru_mms_m_obj`
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
typedef struct {
|
|
||||||
int obj_fd; // 客户端句柄(自增)
|
|
||||||
uint32_t connectionTimeout; // 连接超时 [毫秒]
|
|
||||||
std::string cfg_path; // 配置文件路径
|
|
||||||
stru_cfg *p_cfg; // XML 配置解析结果
|
|
||||||
|
|
||||||
int debug_print_flag; // 调试打印开关(0/1)
|
|
||||||
|
|
||||||
std::string ied_name; // IED 名称
|
|
||||||
MmsValue *param_zone; // 定值区选择值(MmsValue, 可复用)
|
|
||||||
MmsValue *set_confirm; // 定值确认值(boolean, 可复用)
|
|
||||||
|
|
||||||
MMS_STR zone_saddr; // 关联定值区的信号 saddr(如 "ao.0")
|
|
||||||
int current_zone; // 当前定值区号
|
|
||||||
|
|
||||||
stru_mms_m_run run; // 运行时数据
|
|
||||||
std::vector<stru_ldev> ldevs; // 逻辑设备树(LD→LN→DO→Point)
|
|
||||||
std::vector<stru_ld_dataset> ld_datasets; // 数据集和报告配置
|
|
||||||
} stru_mms_m_obj;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.3 运行时结构 `stru_mms_m_run`
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
typedef struct {
|
|
||||||
std::string ip; // 服务器 IP
|
|
||||||
int port; // 服务器端口
|
|
||||||
bool running_init; // 是否已完成初始化
|
|
||||||
|
|
||||||
sem_t sem; // 回调同步信号量
|
|
||||||
pthread_t pthread_task; // 工作线程
|
|
||||||
|
|
||||||
IedConnection con; // libiec61850 连接句柄
|
|
||||||
IedConnectionState con_state; // 当前连接状态
|
|
||||||
IedConnectionState old_con_state; // 上一次连接状态
|
|
||||||
|
|
||||||
stru_mms_m_timer timer[_MMS_M_TIMER_END]; // 4 个定时器
|
|
||||||
|
|
||||||
stru_mms_m_event event; // 当前处理的事件
|
|
||||||
stru_event_queue event_queue; // 事件队列(容量 64)
|
|
||||||
// 事件类型(按枚举):
|
|
||||||
// [select] [operate] [cancel] ─→ 遥控
|
|
||||||
// [mms_m_send_co_select|direct|cancel]
|
|
||||||
// 通过 IedConnection_setRCBValues 发送总召
|
|
||||||
mms_m_out_status_cb out_status_cb; // 连接状态回调
|
|
||||||
std::vector<mms_m_out_value_cb> out_cb_lists; // 数据输出回调列表
|
|
||||||
} stru_mms_m_run;
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.4 数据模型层次结构
|
|
||||||
|
|
||||||
```
|
|
||||||
stru_mms_m_obj
|
|
||||||
├── ldevs[] ← 逻辑设备列表
|
|
||||||
│ └── stru_ldev
|
|
||||||
│ ├── ld_name ← LD 名称(如 "TEMPLATE")
|
|
||||||
│ └── lnodes[] ← 逻辑节点列表
|
|
||||||
│ └── stru_lnode
|
|
||||||
│ ├── ln_name ← LN 名称(如 "GGIO1")
|
|
||||||
│ └── dobjs[] ← 数据对象列表
|
|
||||||
│ └── stru_dobj
|
|
||||||
│ ├── do_name ← DO 名称(如 "Ind1")
|
|
||||||
│ └── p_do_vec[] ← 指向 stru_point_item 的指针集合
|
|
||||||
│
|
|
||||||
└── ld_datasets[] ← 数据集/报告配置
|
|
||||||
└── stru_ld_dataset
|
|
||||||
├── ref ← LD/LN 引用
|
|
||||||
├── ld_name ← LD 名称
|
|
||||||
├── ln_name ← LN 名称
|
|
||||||
├── ln_datasets[] ← 数据集列表
|
|
||||||
│ └── stru_ln_dataset
|
|
||||||
│ ├── dataset_name ← 数据集全名
|
|
||||||
│ ├── dataset_ref ← 数据集引用 ($格式)
|
|
||||||
│ └── members[] ← 成员列表
|
|
||||||
│ └── stru_member
|
|
||||||
│ ├── ref ← FCDA 引用
|
|
||||||
│ ├── reason ← 包含原因
|
|
||||||
│ └── p_do_vec ← 关联的点集合
|
|
||||||
└── ln_rpts[] ← 报告列表
|
|
||||||
└── stru_ln_rpt
|
|
||||||
├── rpt_ref ← RCB 引用
|
|
||||||
├── rpt_ref_with_no ← RCB 引用+编号
|
|
||||||
├── ds_ref ← 关联数据集引用
|
|
||||||
├── type ← URCB 或 BRCB
|
|
||||||
├── rcb ← ClientReportControlBlock 句柄
|
|
||||||
├── p_app ← 指向所属 mms_m_obj
|
|
||||||
└── p_dataset ← 指向关联数据集
|
|
||||||
```
|
|
||||||
|
|
||||||
### 1.5 事件与定时器
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// 事件类型
|
|
||||||
enum {
|
|
||||||
_MMS_M_EVENT_ALL_CALL, // 总召遥信遥测
|
|
||||||
_MMS_M_EVENT_GI_CALL, // 总召(GI)
|
|
||||||
_MMS_M_EVENT_CO_SELECT, // 遥控-选择
|
|
||||||
_MMS_M_EVENT_CO_DIRECT, // 遥控-操作
|
|
||||||
_MMS_M_EVENT_CO_CANCEL, // 遥控-取消
|
|
||||||
_MMS_M_EVENT_AO_READ, // 读取 AO
|
|
||||||
_MMS_M_EVENT_AO_WRITE, // 写入 AO
|
|
||||||
_MMS_M_EVENT_PARAM_READ, // 读取参数
|
|
||||||
_MMS_M_EVENT_PARAM_WRITE, // 写入参数
|
|
||||||
_MMS_M_EVENT_END
|
|
||||||
};
|
|
||||||
|
|
||||||
// 事件结构
|
|
||||||
typedef struct {
|
|
||||||
int app_fd; // 客户端句柄
|
|
||||||
MMS_STR ied; // IED 名称
|
|
||||||
MMS_STR saddr; // 短地址
|
|
||||||
uint8_t value_type; // MMS 数据类型
|
|
||||||
char val[MMS_M_DATA_STRING_LEN]; // 值(字符串形式)
|
|
||||||
uint8_t ctrl_type; // 事件类型
|
|
||||||
int set_zone; // 定值区号
|
|
||||||
void (*p_func)(void *arg, int ret); // 完成回调
|
|
||||||
} stru_mms_m_event;
|
|
||||||
|
|
||||||
// 事件队列(环形缓冲区,容量 64)
|
|
||||||
typedef struct {
|
|
||||||
uint8_t w_ptr; // 写指针
|
|
||||||
uint8_t r_ptr; // 读指针
|
|
||||||
uint8_t size; // 容量
|
|
||||||
uint8_t num; // 当前数量
|
|
||||||
stru_mms_m_event event[EVENT_QUEUE_SIZE];
|
|
||||||
} stru_event_queue;
|
|
||||||
|
|
||||||
// 4 个定时器
|
|
||||||
enum { _MMS_M_TIMER_T0, _MMS_M_TIMER_T1, _MMS_M_TIMER_T2, _MMS_M_TIMER_T3, _MMS_M_TIMER_END };
|
|
||||||
|
|
||||||
// 定时器时间定义
|
|
||||||
#define MMS_M_THREAD_RUN_TM (100 * 3) // 线程循环间隔:300ms
|
|
||||||
#define MMS_M_TIMER_T0 (120 * 3) // T0: 120s(总召所有数据)
|
|
||||||
#define MMS_M_TIMER_T1 (60 * 3) // T1: 60s(GI 触发)
|
|
||||||
#define MMS_M_TIMER_T2 (30 * 3) // T2: 30s(AO + 参数读取)
|
|
||||||
#define MMS_M_TIMER_T3 (20 * 3) // T3: 20s(预留,未使用)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 线程模型与主流程
|
|
||||||
|
|
||||||
### 2.1 生命周期
|
|
||||||
|
|
||||||
```
|
|
||||||
mms_m_out_init()
|
|
||||||
├── 分配 stru_mms_m_obj
|
|
||||||
├── mms_m_ied_init() ← 构建 ldevs 树
|
|
||||||
├── sem_init() ← 初始化信号量
|
|
||||||
├── pthread_create(mms_m_run_thread) ← 创建工作线程
|
|
||||||
└── 加入 g_mms_m_obj_map[id]
|
|
||||||
|
|
||||||
mms_m_run_thread() ← 工作线程入口
|
|
||||||
├── mms_m_timer_init() ← 初始化4个定时器
|
|
||||||
├── 初始化事件队列
|
|
||||||
├── IedConnection_create()
|
|
||||||
├── IedConnection_installStateChangedHandler()
|
|
||||||
├── IedConnection_connectAsync()
|
|
||||||
└── 主循环(300ms 周期)
|
|
||||||
├── mms_m_do_comm() ← 连接状态机
|
|
||||||
└── if CONNECTED → mms_m_run()
|
|
||||||
├── mms_m_run_init() ← 首次建连时执行
|
|
||||||
├── mms_m_do_send() ← 处理事件队列
|
|
||||||
└── mms_m_timer_running() ← 检查定时器
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 连接状态机 `mms_m_do_comm()`
|
|
||||||
|
|
||||||
```
|
|
||||||
状态检查: IedConnection_getState()
|
|
||||||
|
|
||||||
CLOSED/CLOSING → connectAsync() ← 自动重连
|
|
||||||
CONNECTING → 等待
|
|
||||||
CONNECTED → [首次] mms_m_control_init() ← 创建 ControlObjectClient
|
|
||||||
[首次] 触发 out_status_cb(ON_LINE)
|
|
||||||
|
|
||||||
断连检测: old=CONNECTED, new!=CONNECTED
|
|
||||||
→ running_init = false ← 标记需要重新初始化
|
|
||||||
→ 触发 out_status_cb(OFF_LINE)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.3 首次连接初始化 `mms_m_run_init()`
|
|
||||||
|
|
||||||
```
|
|
||||||
1. mms_m_icd_init()
|
|
||||||
├── getLogicalDeviceList() ← 获取 LD 列表
|
|
||||||
├── 遍历每对 LD+LN:
|
|
||||||
│ ├── mms_m_icd_dataset_init() ← 读取该 LN 下的所有 DataSet
|
|
||||||
│ │ └── getDataSetDirectory() ← 读数据集成员
|
|
||||||
│ ├── mms_m_icd_report_init(URCB) ← 发现 URCB 报告
|
|
||||||
│ └── mms_m_icd_report_init(BRCB) ← 发现 BRCB 报告
|
|
||||||
└── ld_datasets 构建完成
|
|
||||||
|
|
||||||
2. mms_m_ld_dataset_match_point_init()
|
|
||||||
└── 将数据集成员与 ldevs 中的 point 关联(构建 p_do_vec)
|
|
||||||
|
|
||||||
3. mms_m_rcb_init()
|
|
||||||
├── getRCBValues() ← 获取每个 RCB 当前值
|
|
||||||
├── 匹配数据集引用 → p_dataset
|
|
||||||
├── setResv(true) ← 预留 RCB
|
|
||||||
├── setTrgOps(dchg|qchg|gi)
|
|
||||||
├── setRptEna(true) ← 使能报告
|
|
||||||
├── installReportHandler() ← 安装回调
|
|
||||||
├── setRCBValues() ← 写入配置到服务器
|
|
||||||
└── setGI(true) ← 触发一次总召
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 数据流程
|
|
||||||
|
|
||||||
### 3.1 周期性总召(T0 定时器)
|
|
||||||
|
|
||||||
```
|
|
||||||
mms_m_do_call_all() ← 每 120s 触发
|
|
||||||
└── push _MMS_M_EVENT_ALL_CALL 事件
|
|
||||||
└── mms_m_send_call_all()
|
|
||||||
└── 遍历 ST + MX 数据点
|
|
||||||
├── IedConnection_readObject() ← 逐一读取
|
|
||||||
├── mms_m_get_mmsValue() ← MmsValue→C 类型
|
|
||||||
├── mms_m_put_value() ← 通过回调输出
|
|
||||||
└── MmsValue_delete() ← 清理
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 报告控制块(RCB)订阅全流程
|
|
||||||
|
|
||||||
RCB 订阅是 MMS 客户端最核心的机制,负责接收 IED 主动上送的数据变化。整个流程分为发现→匹配→激活→接收四个阶段。
|
|
||||||
|
|
||||||
#### 3.2.1 阶段一:发现 RCB(mms_m_icd_init → mms_m_icd_report_init)
|
|
||||||
|
|
||||||
连接成功后,遍历所有 LD→LN,对每对调用 `IedConnection_getLogicalNodeDirectory()` 分别查询两类 RCB:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// 查询 URCB (unbuffered,引用名含 "RP")
|
|
||||||
mms_m_icd_report_init(obj, ld_dataset, ACSI_CLASS_URCB);
|
|
||||||
// 查询 BRCB (buffered,引用名含 "BR")
|
|
||||||
mms_m_icd_report_init(obj, ld_dataset, ACSI_CLASS_BRCB);
|
|
||||||
```
|
|
||||||
|
|
||||||
`mms_m_icd_report_init()` 的核心筛选逻辑:
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
std::string rpt_ref_str = (URCB == acsiClass) ? "RP" : "BR";
|
|
||||||
|
|
||||||
LinkedList reports = IedConnection_getLogicalNodeDirectory(
|
|
||||||
p_con, &err, ld_ds.ref.c_str(), acsiClass);
|
|
||||||
LinkedList report = LinkedList_getNext(reports);
|
|
||||||
|
|
||||||
while (report != NULL) {
|
|
||||||
std::string rpt_data = (char*) report->data;
|
|
||||||
|
|
||||||
// 提取名称和编号:最后2位是编号,前面是名称
|
|
||||||
rpt_name = rpt_data.substr(0, rpt_data.length() - 2);
|
|
||||||
rpt_no = rpt_data.substr(rpt_data.length() - 2);
|
|
||||||
|
|
||||||
rpt_ref = ld_ds.ref + "." + rpt_ref_str + ".";
|
|
||||||
|
|
||||||
if (0 == rpt_no.compare("01")) // 只取编号 "01"
|
|
||||||
{
|
|
||||||
ln_rpt.rpt_ref = rpt_ref + rpt_name;
|
|
||||||
// 例: "TEMPLATE/LLN0.RP.EventsRCB"
|
|
||||||
ln_rpt.rpt_ref_with_no = rpt_ref + rpt_name + rpt_no;
|
|
||||||
// 例: "TEMPLATE/LLN0.RP.EventsRCB01"
|
|
||||||
ld_ds.ln_rpts.push_back(ln_rpt);
|
|
||||||
}
|
|
||||||
// else: 其他编号直接丢弃
|
|
||||||
|
|
||||||
report = LinkedList_getNext(report);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**限制**:只订阅编号末尾为 `"01"` 的 RCB 实例。
|
|
||||||
|
|
||||||
#### 3.2.2 阶段二:匹配数据集(mms_m_ld_dataset_match_point_init)
|
|
||||||
|
|
||||||
将 XML 配置加载的 `ldevs` 信号树与从 IED 发现的 `ld_datasets` 数据集树进行关联,使得每个 dataset member 知道对应哪些信号点(saddr)。
|
|
||||||
|
|
||||||
```
|
|
||||||
数据集 member 的 FCDA 引用示例: "IEDNAME+TEMPLATE/GGIO1.ST.Ind1.stVal[ST]"
|
|
||||||
↓ 解析
|
|
||||||
ld = "IEDNAME+TEMPLATE" → 去 IED 前缀 → "TEMPLATE"
|
|
||||||
ln = "GGIO1"
|
|
||||||
d_name = "Ind1"
|
|
||||||
↓ 匹配 ldevs 树
|
|
||||||
ldevs[ld="TEMPLATE"] → lnodes[ln="GGIO1"] → dobjs[d_name="Ind1"]
|
|
||||||
↓
|
|
||||||
member.p_do_vec = &dobjs.p_do_vec // 建立关联
|
|
||||||
|
|
||||||
如果匹配不到(找不到对应的 LD/LN/DO),member.p_do_vec 为 NULL,
|
|
||||||
该 member 的报告数据将无法输出到任何信号点。
|
|
||||||
```
|
|
||||||
|
|
||||||
引用字符串解析规则([mms_m.cpp:1228-1257](src/protocol/libmms_m/src/mms_m.cpp#L1228-L1257)):
|
|
||||||
- 第1段(`/` 前):LD 名称,需去除 IED 名前缀
|
|
||||||
- 第2段(`.` 前):LN 名称
|
|
||||||
- 第3段(`[` 前):DO 名称
|
|
||||||
- 不匹配此格式的 member 直接跳过
|
|
||||||
|
|
||||||
#### 3.2.3 阶段三:配置并激活 RCB(mms_m_rcb_init)
|
|
||||||
|
|
||||||
对每个已发现的 RCB,执行完整的配置和激活流程([mms_m.cpp:1158-1214](src/protocol/libmms_m/src/mms_m.cpp#L1158-L1214)):
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─ 步骤1:getRCBValues(rcb_ref) 读服务端当前 RCB 值
|
|
||||||
│
|
|
||||||
├─ 步骤2:getDataSetReference(rcb) 获取 RCB 当前关联的数据集
|
|
||||||
│ ↓ ds_ref ← "TEMPLATE/LLN0$Events"
|
|
||||||
├─ 步骤3:匹配本地数据集
|
|
||||||
│ 遍历 ln_datasets,通过 dataset_ref 匹配
|
|
||||||
│ ln_rpt.p_dataset = &matched_dataset
|
|
||||||
│
|
|
||||||
├─ 步骤4:本地设置 RCB 参数
|
|
||||||
│ setResv(true) 预留 RCB(URCB)
|
|
||||||
│ setTrgOps(dchg | qchg | gi) 触发条件:数据变化+品质变化+总召
|
|
||||||
│ setDataSetReference(ds_ref) 确认数据集引用
|
|
||||||
│ setRptEna(true) 使能报告
|
|
||||||
│ ⚠ 未调用 setOptFlds() 完全依赖服务端默认值
|
|
||||||
│
|
|
||||||
├─ 步骤5:installReportHandler() 安装报告回调
|
|
||||||
│ rcbReference = rpt_ref 如 "LD/LLN0.RP.EventsRCB"
|
|
||||||
│ rptId = 从 RCB 获取
|
|
||||||
│ handler = mms_m_report_callback
|
|
||||||
│ parameter = &ln_rpt 回传 RCB 上下文
|
|
||||||
│
|
|
||||||
├─ 步骤6:setRCBValues() 写入服务端
|
|
||||||
│ parametersMask = RCB_ELEMENT_RPT_ENA |
|
|
||||||
│ RCB_ELEMENT_TRG_OPS |
|
|
||||||
│ RCB_ELEMENT_INTG_PD |
|
|
||||||
│ RCB_ELEMENT_GI
|
|
||||||
│ singleRequest = true
|
|
||||||
│ ⚠ mask 包含 INTG_PD 但未调用 setIntgPd()
|
|
||||||
│ ⚠ mask 不含 OPT_FLDS,OptFlds 沿用服务端默认
|
|
||||||
│
|
|
||||||
└─ 步骤7:触发总召
|
|
||||||
setGI(true) → setRCBValues(mask=RCB_ELEMENT_GI)
|
|
||||||
让 IED 立即上送一次完整数据
|
|
||||||
```
|
|
||||||
|
|
||||||
**步骤 6 的潜在问题**:
|
|
||||||
- `parametersMask` 中包含 `RCB_ELEMENT_INTG_PD`,但本地并未调用 `setIntgPd()`,写入的 IntgPd 值实际是步骤1从服务端读取的原始值
|
|
||||||
- `RCB_ELEMENT_OPT_FLDS` 未包含在 mask 中,意味着不会设置服务端的 OptFlds,完全依赖服务端默认配置 — 可能导致报告缺少 `DataReference`、`ConfRev`、`BufferOverflow` 等字段
|
|
||||||
- 使用 `singleRequest=true`,一次 MMS 写请求携带多个变量;如果服务端兼容性不好,可能需要改为 `false`
|
|
||||||
|
|
||||||
#### 3.2.4 阶段四:报告接收与分发(mms_m_report_callback)
|
|
||||||
|
|
||||||
当 IED 发送报告时,libiec61850 回调 `mms_m_report_callback()`([mms_m.cpp:1079-1153](src/protocol/libmms_m/src/mms_m.cpp#L1079-L1153)):
|
|
||||||
|
|
||||||
```
|
|
||||||
ClientReport 到达
|
|
||||||
│ parameter = &ln_rpt ← 阶段三步骤5传入的回调参数
|
|
||||||
│
|
|
||||||
├── ClientReport_getDataSetValues(report) → MmsValue* (MMS_ARRAY)
|
|
||||||
│
|
|
||||||
└── 遍历 dataset->members(按数据集顺序,index 从 0 递增)
|
|
||||||
│
|
|
||||||
├── reason = ClientReport_getReasonForInclusion(report, index)
|
|
||||||
│
|
|
||||||
├── if (reason == NOT_INCLUDED) → 跳过该成员
|
|
||||||
│
|
|
||||||
├── mms_value = MmsValue_getElement(dataset_value, index)
|
|
||||||
│ │
|
|
||||||
│ └── mms_m_get_MmsValue(point_value, out_value, mms_value, ...)
|
|
||||||
│ 递归解包 MmsValue → C 类型
|
|
||||||
│ ├── MMS_STRUCTURE/ARRAY → 递归展开子元素
|
|
||||||
│ ├── MMS_BOOLEAN → *(uint8_t*)p_val
|
|
||||||
│ ├── MMS_INTEGER → *(int32_t*)p_val
|
|
||||||
│ ├── MMS_UNSIGNED → *(uint32_t*)p_val
|
|
||||||
│ ├── MMS_FLOAT → *(float*)p_val
|
|
||||||
│ ├── MMS_BIT_STRING → MmsValue_getBitStringAsIntegerBigEndian()
|
|
||||||
│ ├── MMS_UTC_TIME → 解析时间 → point_value.time
|
|
||||||
│ └── MMS_BINARY_TIME → 解析时间 → point_value.time
|
|
||||||
│
|
|
||||||
├── 如果 mms_value 含时间戳 → tm_flag=1 → 使用报告中的时间
|
|
||||||
│ 否则 → tm_flag=0 → mms_m_get_local_time() 使用本地时间
|
|
||||||
│
|
|
||||||
└── 通过 member.p_do_vec 遍历关联的信号点
|
|
||||||
for (each point_item in p_do_vec)
|
|
||||||
out_value.name = point.saddr
|
|
||||||
out_value.desc = point.desc
|
|
||||||
out_value.reference = point.reference
|
|
||||||
out_value.reason = reason
|
|
||||||
out_value.p_value = 解析后的值指针
|
|
||||||
out_value.time = 时间(报告时间或本地时间)
|
|
||||||
out_value.app_fd = obj.obj_fd
|
|
||||||
↓
|
|
||||||
mms_m_put_value(obj, out_value) // 回调所有注册的 out_cb
|
|
||||||
```
|
|
||||||
|
|
||||||
**数据输出链路**:`mms_m_put_value()` → 遍历 `run.out_cb_lists` → 回调上层注册的数据处理函数(如 `libiec61850m` 中的 `mms_data_back`)。
|
|
||||||
|
|
||||||
#### 3.2.5 断连后的重新订阅
|
|
||||||
|
|
||||||
连接状态机在检测到断连时([mms_m.cpp:1579-1588](src/protocol/libmms_m/src/mms_m.cpp#L1579-L1588)):
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
if (run.con_state != CONNECTED && run.old_con_state == CONNECTED) {
|
|
||||||
obj.run.running_init = false; // 清除已初始化标志
|
|
||||||
out_status_cb(obj.obj_fd, OFF_LINE);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
下一次 `mms_m_run()` 检测到 `running_init==false` 且已重连时,自动**重新执行完整的四阶段订阅流程**(阶段一→二→三),确保断连恢复后重新订阅所有 RCB。
|
|
||||||
|
|
||||||
#### 3.2.6 GI 定时触发
|
|
||||||
|
|
||||||
在阶段三完成首次订阅后,定时器 T1(60s)周期性触发 GI 总召([mms_m.cpp:1636-1649](src/protocol/libmms_m/src/mms_m.cpp#L1636-L1649)):
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// 每 60s 一次
|
|
||||||
event.ctrl_type = _MMS_M_EVENT_GI_CALL;
|
|
||||||
mms_m_push_event(obj, event);
|
|
||||||
|
|
||||||
// → mms_m_send_set_gi()
|
|
||||||
// 遍历所有 RCB → setGI(true) → setRCBValues(mask=RCB_ELEMENT_GI)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3.2.7 流程总览图
|
|
||||||
|
|
||||||
```
|
|
||||||
首次连接 / 断连重连
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
阶段一 ✦ 发现 ──────────────────────────────────────
|
|
||||||
mms_m_icd_init() → mms_m_icd_report_init()
|
|
||||||
遍历 LD→LN → getLogicalNodeDirectory(URCB/BRCB)
|
|
||||||
└── 筛选:只取编号末尾为 "01" 的 RCB
|
|
||||||
产出: ld_datasets[].ln_rpts[] (RCB 列表)
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
阶段二 ✦ 匹配 ──────────────────────────────────────
|
|
||||||
mms_m_ld_dataset_match_point_init()
|
|
||||||
解析 dataset member 的 FCDA 引用 (LD/LN/DO)
|
|
||||||
└── 匹配 ldevs 信号树 → member.p_do_vec 关联
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
阶段三 ✦ 激活 ──────────────────────────────────────
|
|
||||||
mms_m_rcb_init()
|
|
||||||
对每个 RCB:
|
|
||||||
├── getRCBValues() 读取服务端 RCB
|
|
||||||
├── setResv/setTrgOps/setRptEna 配置参数
|
|
||||||
├── installReportHandler() 安装回调
|
|
||||||
├── setRCBValues() 写入服务端
|
|
||||||
└── setGI(true) 触发总召
|
|
||||||
│
|
|
||||||
▼ (此后异步回调)
|
|
||||||
阶段四 ✦ 接收 ──────────────────────────────────────
|
|
||||||
mms_m_report_callback()
|
|
||||||
ClientReport 到达 → 遍历 dataset members
|
|
||||||
├── 跳过 NOT_INCLUDED 成员
|
|
||||||
├── 递归解包 MmsValue → C 类型
|
|
||||||
├── 解析时间戳
|
|
||||||
└── p_do_vec → mms_m_put_value() → 上层回调
|
|
||||||
|
|
||||||
周期性触发:
|
|
||||||
T1 (60s) → GI ─────────────────────────────────→ 阶段四再次触发
|
|
||||||
T0 (120s) → AllCall 读取全部 ST+MX 值
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 遥控流程
|
|
||||||
|
|
||||||
```
|
|
||||||
外部调用 mms_m_out_do_set_yk(event)
|
|
||||||
└── push 遥控事件到事件队列
|
|
||||||
└── mms_m_send_co() ← 处理遥控事件
|
|
||||||
├── 按 saddr 匹配 co_point
|
|
||||||
├── 首次: ControlObjectClient_create()
|
|
||||||
│ └── setOrigin(ORCAT_STATION_CONTROL)
|
|
||||||
├── 构造 set_value (MmsValue_newBoolean)
|
|
||||||
└── 按 ctrl_type 分发:
|
|
||||||
├── SELECT → mms_m_send_co_select()
|
|
||||||
│ ├── SBO_NORMAL: select()
|
|
||||||
│ └── SBO_ENHANCED: selectWithValue()
|
|
||||||
│ └── 先 setCommandTerminationHandler()
|
|
||||||
├── DIRECT → mms_m_send_co_direct()
|
|
||||||
│ ├── operate()
|
|
||||||
│ └── 按 ctrl_model 检查结果:
|
|
||||||
│ ├── DIRECT_NORMAL: 立即回读 stVal 验证
|
|
||||||
│ ├── DIRECT_ENHANCED: 等待 1s 后回读验证
|
|
||||||
│ └── SBO_ENHANCED: 等待 1s(不验证)
|
|
||||||
└── CANCEL → mms_m_send_co_cancel()
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.4 参数(定值)写入流程
|
|
||||||
|
|
||||||
```
|
|
||||||
_PARAM_WRITE 事件
|
|
||||||
└── mms_m_send_param_write()
|
|
||||||
├── 匹配参数点
|
|
||||||
├── 构造 EditSG 引用: {LD}/LLN0.SGCB.EditSG
|
|
||||||
├── 构造 CnfEdit 引用: {LD}/LLN0.SGCB.CnfEdit
|
|
||||||
├── 步骤1: writeObject(EditSG, FC=SP) ← 选择编辑定值组
|
|
||||||
├── 步骤2: writeObject(ref, FC=SE) ← 写入新定值
|
|
||||||
└── 步骤3: writeObject(CnfEdit, FC=SP) ← 确认编辑
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.5 定值区自动切换
|
|
||||||
|
|
||||||
```
|
|
||||||
mms_m_send_read_ao() 中检测:
|
|
||||||
如果某个 AO 信号的 saddr == zone_saddr(绑定的定值区信号)
|
|
||||||
→ 读值后比较 current_zone
|
|
||||||
→ 若变化 (new_zone != current_zone):
|
|
||||||
update current_zone
|
|
||||||
push _MMS_M_EVENT_PARAM_READ 事件 ← 自动触发参数重读
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 对外接口
|
|
||||||
|
|
||||||
### 4.1 myMms_m.h 中声明的主要 API
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// === 生命周期 ===
|
|
||||||
int mms_m_out_init(stru_cfg *p_cfg, int debug_print_flag, uint32_t connectionTimeout);
|
|
||||||
// 返回 app_fd (>0 成功,-1 失败)
|
|
||||||
|
|
||||||
// === 状态回调 ===
|
|
||||||
int mms_m_out_get_connect_status(int fd, mms_m_out_status_cb p_func);
|
|
||||||
// 连接/断开时回调: void (int app_fd, int status) // MMS_M_ON_LINE / MMS_M_OFF_LINE
|
|
||||||
|
|
||||||
// === 数据回调 ===
|
|
||||||
int mms_m_out_get_value(int fd, mms_m_out_value_cb p_func);
|
|
||||||
// 数据到达时回调: void (stru_mms_m_out_value *p_value)
|
|
||||||
|
|
||||||
// === 遥控 ===
|
|
||||||
int mms_m_out_do_set_yk(stru_mms_m_event *p_event);
|
|
||||||
// 发送遥控事件(select/direct/cancel),返回 -1 失败
|
|
||||||
|
|
||||||
// === AO/参数操作 ===
|
|
||||||
int mms_m_out_read_ao_or_params(int app_fd, uint8_t type, const char *saddr);
|
|
||||||
// type: _MMS_M_EVENT_AO_READ 或 _MMS_M_EVENT_PARAM_READ
|
|
||||||
// saddr 为 NULL 则读取全部
|
|
||||||
|
|
||||||
// === 定值区绑定 ===
|
|
||||||
int mms_m_out_bind_param_zone_signal(int app_fd, const char *zone_saddr);
|
|
||||||
// 绑定一个 AO 信号作为定值区指示器
|
|
||||||
|
|
||||||
// === 调试 ===
|
|
||||||
int mms_m_out_debug_print_swicth(int id, int debug_print_flag);
|
|
||||||
|
|
||||||
// === 工具函数 ===
|
|
||||||
void *mms_m_create_data_ptr(uint8_t type); // 按 MMS 类型创建数据指针
|
|
||||||
int mms_m_set_data_value(void *srt, void *dst, uint8_t type); // 复制值
|
|
||||||
int mms_m_get_data_value_str(void *data, uint8_t type, char *str); // 转字符串
|
|
||||||
int mms_m_set_data_by_str(void *data, uint8_t type, const char *str); // 从字符串设置
|
|
||||||
char *mms_m_out_reason_str(int reason); // 原因码转字符串
|
|
||||||
void *mms_m_get_obj(int app_fd); // 获取对象(给扩展模块)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 扩展接口(mms_m_ext.h)
|
|
||||||
|
|
||||||
```cpp
|
|
||||||
// 文件服务
|
|
||||||
typedef void (*mms_m_file_read_cb)(int fd, const char *fn, const uint8_t *d, int len, bool mf, int e);
|
|
||||||
typedef void (*mms_m_file_op_cb)(int fd, const char *fn, int e);
|
|
||||||
int mms_m_read_file(int fd, const char *rf, mms_m_file_read_cb cb);
|
|
||||||
int mms_m_delete_file(int fd, const char *rf, mms_m_file_op_cb cb);
|
|
||||||
|
|
||||||
// 服务器信息查询
|
|
||||||
typedef void (*mms_m_server_cb)(int fd, const char *vendor, const char *model, const char *rev, int log_st, int phy_st, int e);
|
|
||||||
int mms_m_query_server(int fd, mms_m_server_cb cb);
|
|
||||||
|
|
||||||
// 定值组信息
|
|
||||||
typedef void (*mms_m_sg_cb)(int fd, const char *ld, int act_sg, int num_sg, int e);
|
|
||||||
int mms_m_read_sg_info(int fd, const char *ld, mms_m_sg_cb cb);
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 扩展回调类型汇总
|
|
||||||
|
|
||||||
| 回调类型 | 签名 | 用途 |
|
|
||||||
|---------|------|------|
|
|
||||||
| `mms_m_out_status_cb` | `void(int fd, int status)` | 连接状态 |
|
|
||||||
| `mms_m_out_value_cb` | `void(stru_mms_m_out_value *p_value)` | 数据到达 |
|
|
||||||
| `mms_m_file_read_cb` | `void(int, const char*, const uint8_t*, int, bool, int)` | 文件分块读取 |
|
|
||||||
| `mms_m_file_op_cb` | `void(int fd, const char* filename, int err)` | 文件删除结果 |
|
|
||||||
| `mms_m_server_cb` | `void(int, const char*, const char*, const char*, int, int, int)` | 服务器信息 |
|
|
||||||
| `mms_m_sg_cb` | `void(int, const char*, int act_sg, int num_sg, int err)` | 定值组信息 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. 错误码(mms_m_errstr.cpp)
|
|
||||||
|
|
||||||
提供四种枚举值到字符串的映射:
|
|
||||||
|
|
||||||
| 函数 | 映射 |
|
|
||||||
|------|------|
|
|
||||||
| `mms_m_err_str(IedClientError)` | IED 客户端错误(30 个映射) |
|
|
||||||
| `mms_m_control_err_str(ControlLastApplError)` | 控制应用错误(4 个映射) |
|
|
||||||
| `mms_m_ctl_add_cause_str(ControlAddCause)` | 控制附加原因(28 个映射) |
|
|
||||||
| `mms_m_ctrl_model_str(ControlModel)` | 控制模型(5 个映射) |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 文件服务(mms_m_file.cpp)
|
|
||||||
|
|
||||||
### 实现状态
|
|
||||||
|
|
||||||
| 功能 | 状态 | 使用的 API |
|
|
||||||
|------|------|-----------|
|
|
||||||
| 文件打开 | 已实现 | `MmsConnection_fileOpen()` |
|
|
||||||
| 分块异步读取 | 未完成(TODO) | 待实现 `MmsConnection_fileReadAsync()` |
|
|
||||||
| 文件删除 | 已实现 | `MmsConnection_fileDelete()` |
|
|
||||||
|
|
||||||
### 调用方式
|
|
||||||
|
|
||||||
所有文件操作通过 `IedConnection_getMmsConnection()` 获取底层 `MmsConnection` 句柄,然后调用 MMS 低层文件 API。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 设计要点
|
|
||||||
|
|
||||||
1. **事件驱动**:所有对外操作通过事件队列异步执行,避免阻塞调用线程
|
|
||||||
2. **自动重连**:连接断开后自动重试连接,连接恢复后重新初始化报告
|
|
||||||
3. **数据结构三棵树**:
|
|
||||||
- **ldevs**:按 LD→LN→DO 组织的信号点树(用于信号匹配输出)
|
|
||||||
- **ld_datasets/ln_rpts**:数据集和 RCB 树(用于报告接收和 GI 触发)
|
|
||||||
- **g_mms_m_obj_map**:多客户端实例管理
|
|
||||||
4. **定值区自动跟踪**:通过绑定 zone_saddr,自动检测定值区变化并重读参数
|
|
||||||
5. **调试标志**:全局 `debug_print_flag` 控制日志输出详细程度
|
|
||||||
6. **RCB 激活配置缺陷**:
|
|
||||||
- `setRCBValues` 的 `parametersMask` 包含 `RCB_ELEMENT_INTG_PD` 但本地未调用 `setIntgPd()`,写入的是从服务端读回的旧值
|
|
||||||
- `RCB_ELEMENT_OPT_FLDS` 未包含在 mask 中,OptFlds 完全依赖服务端默认值,可能导致报告缺少 `DataReference`、`ConfRev`、`BufferOverflow` 等字段
|
|
||||||
- `mms_m_icd_report_init` 只取编号 `"01"` 的 RCB,其他编号直接丢弃
|
|
||||||
|
|
@ -1,379 +0,0 @@
|
||||||
# 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 等)更新时会被跳过。
|
|
||||||
|
|
@ -1,217 +0,0 @@
|
||||||
# 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 | 释放连接对应的所有信号资源 |
|
|
||||||
|
|
@ -1,598 +0,0 @@
|
||||||
# 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 协议。
|
|
||||||
|
|
@ -0,0 +1,118 @@
|
||||||
|
# Project memory
|
||||||
|
_Durable project-level knowledge. Persists across all sessions in this project. Edit only content under italic instructions._
|
||||||
|
|
||||||
|
## Project context
|
||||||
|
_What is this project? What's its goal? High-level identity._
|
||||||
|
|
||||||
|
**RTU (Remote Terminal Unit)** — 基于 IEC 61850 的智能通信网关,用于电力系统自动化。
|
||||||
|
|
||||||
|
- **核心协议**: IEC 61850 MMS(服务端/客户端)、IEC 101/104(通过 ICP67 协议)
|
||||||
|
- **通信方式**: TCP/UART/UDP 多通道、MQTT(mosquitto)、WebSocket
|
||||||
|
- **数据模型**: 信号数据中心(遥测/遥信/遥控/遥调/参数),支持 XXH128 哈希索引
|
||||||
|
- **技术栈**: C/C++(GCC/Linux ARM 交叉编译)、Mongoose 嵌入式 Web 服务器、linenoise CLI
|
||||||
|
- **编译**: `./release/build.sh`(x86)或 `./release/build.sh arm`(ARM 交叉编译)
|
||||||
|
- **运行**: `./test/RTU`
|
||||||
|
|
||||||
|
### 模块架构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── system/RTU/ # 主程序入口、CLI、自点表配置、应用系统调度
|
||||||
|
├── system/libdatacenter/ # 数据中心(信号注册/变更检测/SBO控制/事件队列)
|
||||||
|
├── system/libcom_channel/ # 通信通道管理(TCP服务端/客户端、串口)
|
||||||
|
├── protocol/libmms_s/ # IEC 61850 MMS 服务端(ICD解析、模型创建、控制/定值/文件)
|
||||||
|
├── protocol/libmms_m/ # IEC 61850 MMS 客户端(总召/读写/报告订阅)
|
||||||
|
├── protocol/libicp67/ # ICP67 协议(IEC 101/104 帧编解码)
|
||||||
|
├── protocol/libmongoose/ # Mongoose HTTP/WebSocket 嵌入式服务器
|
||||||
|
├── public/libmy_mosquitto/ # MQTT 客户端库(mosquitto 2.x)
|
||||||
|
├── public/libcJSON/ # JSON 解析
|
||||||
|
└── public/libcomm/ # 统一通信抽象层(TCP/UART/UDP)
|
||||||
|
```
|
||||||
|
|
||||||
|
9 个应用线程: `app_sys`, `app_cmd`, `app_comm_channel`, `app_com_scan`, `app_iec`, `app_self_ptl`, `app_web_server`, `app_iec61850m`, `app_iec61850s`
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
_Hard constraints from user that every session must respect._
|
||||||
|
|
||||||
|
### 工作流程规则
|
||||||
|
1. **对话语言**: 全程使用中文显示
|
||||||
|
2. **编译路径**: `./release/build.sh`(x86)或 `./release/build.sh arm`(ARM交叉编译)
|
||||||
|
3. **执行路径**: `./test/RTU`
|
||||||
|
4. **排查问题流程**: 先重新读取对应位置的源码,再对照问题或打印信息排查,不要依赖记忆中的旧代码
|
||||||
|
5. **问题记录**: 每次解决的问题都追加到 `claude/问题处理文档.md`
|
||||||
|
6. **文档目录**:
|
||||||
|
- `claude/mid/` — 存放中间文档、plan 文档
|
||||||
|
- `claude/工程/` — 存放按模块记录的项目工程文档
|
||||||
|
7. **每次读取项目工程时**: 把读到的东西按模块生成文档记录到 `claude/工程/` 文件夹
|
||||||
|
8. **解决问题的 plan**: 每次制定并执行 plan 解决问题后,将 plan 内容整理为文档存入 `claude/mid/` 目录
|
||||||
|
9. **Claude→Mimo 记忆转换规则** (2026-06-12, plan 1781250239159-brave-harbor):
|
||||||
|
- `.claude/memory/MEMORY.md` → Mimo `memory/projects/global/MEMORY.md`
|
||||||
|
- `.claude/memory/code-style.md` + `project-rules.md` → MEMORY.md §Rules
|
||||||
|
- `.claude/memory/user_language.md` → Mimo `memory/global/MEMORY.md`
|
||||||
|
- `claude/mid/*.md` → 提取核心知识 → MEMORY.md §Discovered durable knowledge
|
||||||
|
- `claude/工程/*.md` → 提取核心知识 → MEMORY.md §Architecture decisions
|
||||||
|
- 原始 claude 文件保留不动,仅新增 Mimo 记忆文件
|
||||||
|
|
||||||
|
### C/C++ 代码格式规范
|
||||||
|
- **缩进**: 使用 **Tab 字符**缩进(显示宽度 4),不使用空格缩进
|
||||||
|
- **括号风格**: Allman 风格 — 所有大括号独占一行(函数、if、for、while、struct),单条语句也保留大括号
|
||||||
|
- **空格**:
|
||||||
|
- 关键字与括号之间不加空格: `if(`, `for(`, `while(`, `switch(`
|
||||||
|
- 函数名与括号之间不加空格: `func(args)`
|
||||||
|
- 指针声明: `type *name`(`*` 前有空格,后无空格)
|
||||||
|
- 引用声明: `type &name`
|
||||||
|
- **Yoda 条件**: 常量写在比较运算符左侧,`if(NULL == ptr)` 而非 `if(ptr == NULL)`
|
||||||
|
- **空行**: 函数之间两行空行,逻辑块之间一行空行,`#include` 区块末尾一行空行
|
||||||
|
- **typedef struct**: 结构体成员无缩进额外层级(与 `{` 对齐)
|
||||||
|
- **命名约定**: 局部静态变量用 `LOCAL` 宏(= `static`),全局变量用 `g_` 前缀,结构体用 `stru_` 前缀,枚举用 `enum_` 前缀/`ENUM_` 值前缀
|
||||||
|
|
||||||
|
## Architecture decisions
|
||||||
|
_Major design choices with rationale. The "why" matters more than the "what" for future sessions._
|
||||||
|
|
||||||
|
### 1. 数据中心信号变更检测
|
||||||
|
`dc_signal.cpp` 使用 XXH128 哈希对信号快速索引(`signal_out`, `signal_in`, `signal_yk`, `signal_ao`, `signal_param` 五张表)。输出信号变更通过脏队列 + 去重 + 回调循环防护实现增量推送(仅变更时发送),`last_caller_module` 防止同模块回调自循环。
|
||||||
|
|
||||||
|
### 2. WebSocket 多连接隔离
|
||||||
|
每个 WebSocket 连接拥有独立的信号资源 session(per-connection),连接建立时开辟、断开时释放。全局共享方案会导致一个客户端的 add/del 操作影响其他客户端。
|
||||||
|
|
||||||
|
### 3. MMS 客户端事件驱动模型
|
||||||
|
`mms_m.cpp` 使用事件队列 + 状态机模式,定时器驱动周期性操作(T0=120s all-call, T1=60s GI, T2=30s CO, T3=20s param)。RCB 订阅支持可配置化编号过滤。
|
||||||
|
|
||||||
|
### 4. IEC 61850 服务器模型
|
||||||
|
`mms_s_icd.cpp` 解析 SCL XML ICD 文件构建完整数据模型。`mms_s_model.cpp` 动态创建 IedModel(LD/LN/DO/SDO/DA 树)。定值组管理通过 SGCB + SG/SE 镜像 DA 实现编辑区/运行区隔离。
|
||||||
|
|
||||||
|
### 5. 通信通道统一抽象
|
||||||
|
`com_channel.cpp` 管理所有通信通道配置。`libcomm` 提供统一连接/发送/接收/断开接口,按类型分发到 TCP/UART/UDP 实现。ICP66 帧转换为 ICP67 格式。
|
||||||
|
|
||||||
|
### 6. MQTT 通信
|
||||||
|
使用自编译 mosquitto 2.x 客户端库(`libmy_mosquitto`),支持 MQTT v5 特性。
|
||||||
|
|
||||||
|
## Discovered durable knowledge
|
||||||
|
_Cross-task facts that survive across sessions. Promoted from session checkpoints' §7 when proven durable._
|
||||||
|
|
||||||
|
### 已修复的关键问题
|
||||||
|
|
||||||
|
#### #1 RCB 订阅编号可配置化(2026-06-10)
|
||||||
|
- **问题**: `mms_m_icd_report_init()` 硬编码只订阅编号 `"01"` 的 RCB
|
||||||
|
- **修复**: 新增 `mms_m_out_set_rcb_numbers()` API,支持逗号分隔的多编号和通配符 `"*"`
|
||||||
|
- **文件**: `myMms_m.h`, `mms_m.h`, `mms_m.cpp`, `iec61850m.cpp`
|
||||||
|
|
||||||
|
#### #2 libweb_server 多连接支持(2026-06-10)
|
||||||
|
- **问题**: 原 WebSocket 服务端仅支持单客户端,存在多线程竞态、悬空指针等 10 个缺陷
|
||||||
|
- **修复**: 改为多连接,每个连接独立信号资源 session,断开自动释放
|
||||||
|
- **文件**: `web_server.cpp`, `ws_method.h`, `ws_method.cpp`
|
||||||
|
|
||||||
|
#### #3 app_cmd CPU 108% 问题(2026-06-12)
|
||||||
|
- **根因**: `linenoiseEdit()` 中 `read()` 仅检查 `-1` 未处理 EOF(返回 0),非终端环境下形成死循环
|
||||||
|
- **修复**: 改为 `<= 0` 判断 + `enableRawMode()` 返回值检查 + `select()` 非阻塞方案
|
||||||
|
- **文件**: `my_cmd.cpp`, `app_cmd.cpp`
|
||||||
|
|
||||||
|
#### #4 Tab 命令补全前缀丢失(2026-06-12)
|
||||||
|
- **问题**: 子命令 Tab 补全时命令名前缀被覆盖(如 `datacenter param` 变 `param`)
|
||||||
|
- **修复**: Tab 键处理改为找到最后一个空格,只替换空格之后的当前词
|
||||||
|
- **文件**: `my_cmd.cpp`
|
||||||
|
|
||||||
|
#### #5 app_cmd 交互卡顿(2026-06-12)
|
||||||
|
- **问题**: select 非阻塞 + 100ms 定时器导致回车后回显有卡顿感
|
||||||
|
- **修复**: 改为简单阻塞循环 `while(1) { cmd_recv(); }`,linenoise 自管理终端模式
|
||||||
|
- **文件**: `app_cmd.cpp`
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
---
|
# Global memory
|
||||||
name: user-language
|
_Cross-project user preferences. Persists across all projects. Edit only content under italic instructions._
|
||||||
description: 用户要求所有对话、思考过程和中间输出均使用中文显示
|
|
||||||
metadata:
|
## User language preference
|
||||||
node_type: memory
|
_What language should the assistant use for all conversations, thinking, and visible outputs?_
|
||||||
type: user
|
|
||||||
originSessionId: 5e65af11-b197-479e-a82f-c2e5b475a8c3
|
|
||||||
---
|
|
||||||
|
|
||||||
用户要求所有对话内容、思考过程(thinking)、以及所有用户能看到的中间过程都使用中文显示。包括但不限于:回复内容、代码注释、任务列表、提交信息等。代码本身(变量名、函数名等)保持英文不变。
|
用户要求所有对话内容、思考过程(thinking)、以及所有用户能看到的中间过程都使用中文显示。包括但不限于:回复内容、代码注释、任务列表、提交信息等。代码本身(变量名、函数名等)保持英文不变。
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
# Plan: RTU 项目 Claude → Mimo 文档迁移
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
前序任务(T1-T6)已完成:仓库克隆、源码读取、Claude 记忆文件已提取到 Mimo MEMORY.md。本次将 claude/ 目录下的文档和配置全部迁移到 mimo/。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 步骤
|
||||||
|
|
||||||
|
### T7: 保存当前 plan 到 mimo/plan/
|
||||||
|
- 创建 `mimo/plan/` 目录
|
||||||
|
- 将本 plan 内容保存为 `mimo/plan/RTU_Claude到Mimo文档迁移.md`
|
||||||
|
- **规则**: 此后每次制定 plan 实施前,都先将 plan 文档存入 `mimo/plan/`
|
||||||
|
|
||||||
|
### T8: 删除 .claude/memory/ 配置
|
||||||
|
- 前提确认: Claude 记忆文件内容已全部记录在 `mimo/MEMORY.md` 和规范路径 MEMORY.md 中
|
||||||
|
- 删除整个 `.claude/memory/` 目录
|
||||||
|
|
||||||
|
### T9: 创建 mimo/ 子目录结构
|
||||||
|
- 创建 `mimo/工程/`
|
||||||
|
- 创建 `mimo/中间文档/`
|
||||||
|
|
||||||
|
### T10: 复制 mid/ → 中间文档/
|
||||||
|
- 复制 `claude/mid/RCB订阅编号可配置化.md` → `mimo/中间文档/RCB订阅编号可配置化.md`
|
||||||
|
- 复制 `claude/mid/Tab命令补全功能.md` → `mimo/中间文档/Tab命令补全功能.md`
|
||||||
|
- 复制 `claude/mid/app_cmd_CPU108问题修复.md` → `mimo/中间文档/app_cmd_CPU108问题修复.md`
|
||||||
|
|
||||||
|
### T11: 复制 问题处理文档
|
||||||
|
- 复制 `claude/问题处理文档.md` → `mimo/问题处理文档.md`
|
||||||
|
|
||||||
|
### T12: 重写 工程/ 文档(8篇,每篇对照源码彻底重写)
|
||||||
|
对 claude/工程/ 下每个文档:
|
||||||
|
1. 读取原 claude 文档
|
||||||
|
2. 读取对应模块的完整源码
|
||||||
|
3. 用我的理解与思路重写,形成新文档放入 `mimo/工程/`
|
||||||
|
|
||||||
|
| 原文件 | 对应源码模块 |
|
||||||
|
|--------|------------|
|
||||||
|
| `libiec61850_MMS客户端API开发手册.md` | `src/protocol/libmms_m/` + `release/inc/myMms_m.h` |
|
||||||
|
| `libiec61850_MMS服务端API开发手册.md` | `src/protocol/libmms_s/` |
|
||||||
|
| `libiec61850m模块分析.md` | `src/system/libiec61850m/` |
|
||||||
|
| `libiec61850s模块分析.md` | `src/system/libiec61850s/` |
|
||||||
|
| `libmms_m模块分析.md` | `src/protocol/libmms_m/` |
|
||||||
|
| `libmms_s模块分析.md` | `src/protocol/libmms_s/` |
|
||||||
|
| `libweb_server模块分析.md` | `src/system/libweb_server/` |
|
||||||
|
| `websocket-server.md` | `src/protocol/libmongoose/src/mongoose.c` + `src/system/libweb_server/` |
|
||||||
|
|
||||||
|
### T13: 删除 claude/ 和 .claude/ 全部内容
|
||||||
|
- 删除 `claude/` 目录及其所有内容
|
||||||
|
- 删除 `.claude/` 目录(已在上一步删除 memory/,确认无残留)
|
||||||
|
|
||||||
|
### T14: 验证
|
||||||
|
- 确认 `mimo/` 目录结构完整
|
||||||
|
- 确认 `claude/` 和 `.claude/` 已删除
|
||||||
|
- 确认 `mimo/MEMORY.md` 中的路径引用更新(如有涉及 claude/ 路径)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关键文件
|
||||||
|
- **源目录**: `/mnt/AI/AI_mimo/claude/`(待删除)
|
||||||
|
- **源目录**: `/mnt/AI/AI_mimo/.claude/`(待删除)
|
||||||
|
- **目标目录**: `/mnt/AI/AI_mimo/mimo/plan/`
|
||||||
|
- **目标目录**: `/mnt/AI/AI_mimo/mimo/工程/`
|
||||||
|
- **目标目录**: `/mnt/AI/AI_mimo/mimo/中间文档/`
|
||||||
|
- **目标文件**: `/mnt/AI/AI_mimo/mimo/问题处理文档.md`
|
||||||
|
|
||||||
|
## 验证方式
|
||||||
|
1. `tree /mnt/AI/AI_mimo/mimo/` 检查目录结构
|
||||||
|
2. 确认 `claude/` 和 `.claude/` 不存在
|
||||||
|
3. `git status` 确认变更范围
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
# libiec61850_MMS客户端API开发手册
|
||||||
|
|
||||||
|
**日期**: 2026-06-12
|
||||||
|
**基于源码**: `release/inc/myMms_m.h`, `src/system/libiec61850m/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 模块层次
|
||||||
|
|
||||||
|
RTU 的 MMS 客户端分为两层:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ iec61850m (系统层) │ ← 本文档描述
|
||||||
|
│ - 配置解析、信号初始化 │
|
||||||
|
│ - 回调转发给 datacenter │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ libmms_m (协议层) │ ← 底层 API,详见《libmms_m模块分析》
|
||||||
|
│ - IED 连接、定时器、事件 │
|
||||||
|
│ - MMS 协议编解码 │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
`iec61850m` 负责解析 XML 配置(`mms_m.xml`),编排信号初始化流程,将 `libmms_m` 的数据通过回调桥接到数据中心(`datacenter`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心 API(`release/inc/myMms_m.h`)
|
||||||
|
|
||||||
|
### 2.1 `mms_m_out_init`
|
||||||
|
|
||||||
|
```c
|
||||||
|
int mms_m_out_init(stru_cfg *p_cfg, int debug_print_flag, uint32_t connectionTimeout);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 创建一个 IED 连接。
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `p_cfg` | 配置文件结构体指针,必须包含 `path`(配置文件路径) |
|
||||||
|
| `debug_print_flag` | 调试打印开关(1=开, 0=关),开启后打印 ICD 发现详情和 MMS 值 |
|
||||||
|
| `connectionTimeout` | 连接超时(毫秒) |
|
||||||
|
|
||||||
|
**返回值**: 成功返回 `obj_fd`(>0),失败返回 `-1`。
|
||||||
|
|
||||||
|
**副作用**: 创建 `stru_mms_m_obj` 实例,启动独立工作线程 `mms_m_run_thread`,开始异步连接远方 IED。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.2 `mms_m_out_get_connect_status`
|
||||||
|
|
||||||
|
```c
|
||||||
|
int mms_m_out_get_connect_status(int app_fd, mms_m_out_status_cb p_func);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 注册连接状态回调。
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `app_fd` | `mms_m_out_init` 返回的句柄 |
|
||||||
|
| `p_func` | 回调函数 `void(int fd, int status)`,status: `MMS_M_ON_LINE` 或 `MMS_M_OFF_LINE` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.3 `mms_m_out_debug_print_swicth`
|
||||||
|
|
||||||
|
```c
|
||||||
|
int mms_m_out_debug_print_swicth(int app_fd, int debug_print_flag);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 运行时开关调试打印。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.4 `mms_m_out_do_set_yk`
|
||||||
|
|
||||||
|
```c
|
||||||
|
int mms_m_out_do_set_yk(stru_mms_m_event *p_event);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 下达遥控命令(Select/Direct/Cancel)。
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int app_fd; // IED 句柄
|
||||||
|
char ied[64]; // IED 名称(app_fd = 0 时按名称匹配)
|
||||||
|
char saddr[256]; // 信号地址
|
||||||
|
uint8_t ctrl_type; // 操作类型
|
||||||
|
char data[128]; // 操作数据
|
||||||
|
int set_fd; // 操作序号
|
||||||
|
}stru_mms_m_event;
|
||||||
|
```
|
||||||
|
|
||||||
|
`ctrl_type` 取值:
|
||||||
|
- `_MMS_M_EVENT_CO_SELECT`: 选择(SBO 模式第一步)
|
||||||
|
- `_MMS_M_EVENT_CO_DIRECT`: 执行(Direct 或 SBO 第二步)
|
||||||
|
- `_MMS_M_EVENT_CO_CANCEL`: 取消
|
||||||
|
|
||||||
|
**注意**: `app_fd > 0` 时按句柄查找;`app_fd = 0` 时按 `ied` 名称查找。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.5 `mms_m_out_read_ao_or_params`
|
||||||
|
|
||||||
|
```c
|
||||||
|
int mms_m_out_read_ao_or_params(int app_fd, uint8_t type, const char *saddr);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 读取 AO 或 Param 值。
|
||||||
|
|
||||||
|
| 参数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `type` | `_MMS_M_EVENT_AO_READ` 或 `_MMS_M_EVENT_PARAM_READ` |
|
||||||
|
| `saddr` | 指定信号地址(`nullptr` 时读取全部) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.6 `mms_m_out_get_value`
|
||||||
|
|
||||||
|
```c
|
||||||
|
int mms_m_out_get_value(int fd, mms_m_out_value_cb p_func);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 注册数据推送回调。所有读取到的 ST/MX/AO/Param 值都通过此回调向外推送。
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef void (*mms_m_out_value_cb)(stru_mms_m_out_value &val);
|
||||||
|
```
|
||||||
|
|
||||||
|
`stru_mms_m_out_value` 字段:
|
||||||
|
```c
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
int app_fd; // IED 句柄
|
||||||
|
char name[256]; // 信号 saddr
|
||||||
|
char desc[256]; // 信号描述
|
||||||
|
char reference[256]; // MMS 对象引用
|
||||||
|
void *p_value; // 数据指针
|
||||||
|
uint8_t type; // MMS 数据类型(BOOLEAN/INTEGER/UNSIGNED/FLOAT/STRING)
|
||||||
|
int quality; // 品质位
|
||||||
|
stru_mms_m_time time; // 时标
|
||||||
|
int reason; // 原因码(ALL_CALL/GI/dchg/qchg 等)
|
||||||
|
}stru_mms_m_out_value;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.7 `mms_m_out_bind_param_zone_signal`
|
||||||
|
|
||||||
|
```c
|
||||||
|
int mms_m_out_bind_param_zone_signal(int app_fd, const char *zone_saddr);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 绑定定值区号信号。定值区号变化时自动触发参数重读。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.8 `mms_m_out_set_rcb_numbers`
|
||||||
|
|
||||||
|
```c
|
||||||
|
int mms_m_out_set_rcb_numbers(int app_fd, const char *rcb_numbers);
|
||||||
|
```
|
||||||
|
|
||||||
|
**功能**: 设置 RCB 订阅编号。
|
||||||
|
|
||||||
|
| 输入 | 行为 |
|
||||||
|
|------|------|
|
||||||
|
| 不调用 | 默认订阅 `"01"` |
|
||||||
|
| `"01"` | 只订阅 01 号 |
|
||||||
|
| `"01,02,03"` | 订阅多个编号 |
|
||||||
|
| `"*"` | 订阅全部 RCB |
|
||||||
|
| `"abc"` / `"1"` / `"012"` | 非法,返回 -1 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2.9 工具函数
|
||||||
|
|
||||||
|
```c
|
||||||
|
void *mms_m_create_data_ptr(uint8_t type); // 创建数据指针(需手动 delete)
|
||||||
|
int mms_m_set_data_value(void *srt, void *dst, uint8_t type); // 类型化赋值
|
||||||
|
int mms_m_get_data_value_str(void *data, uint8_t type, char *s); // 转字符串
|
||||||
|
int mms_m_set_data_by_str(void *data, uint8_t type, const char *s); // 从字符串设置
|
||||||
|
char *mms_m_out_reason_str(int reason); // 原因码→字符串
|
||||||
|
```
|
||||||
|
|
||||||
|
`type` 取值: `MMS_BOOLEAN`, `MMS_INTEGER`, `MMS_UNSIGNED`, `MMS_FLOAT`, `MMS_STRING`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. iec61850m 初始化流程
|
||||||
|
|
||||||
|
### 3.1 两阶段初始化
|
||||||
|
|
||||||
|
```
|
||||||
|
app_iec61850m_init1:
|
||||||
|
1. 获取进程目录
|
||||||
|
2. 解析 config/MMS/mms_m.xml(点表配置)
|
||||||
|
- St/Mx/Co/Ao/Param 各信号点的 saddr/type/ctrl_model/desc
|
||||||
|
3. 为每个 IED 调用 mms_m_out_init → 获取 obj_fd
|
||||||
|
4. 注册数据回调: mms_m_out_get_value → 桥接到 datacenter
|
||||||
|
5. 注册连接状态回调
|
||||||
|
|
||||||
|
app_iec61850m_init2:
|
||||||
|
1. 调用 mms_m_out_debug_print_swicth(传入调试标志)
|
||||||
|
2. 调用 mms_m_out_set_rcb_numbers(传入 RCB 编号配置)
|
||||||
|
3. 调用 mms_m_out_bind_param_zone_signal(绑定定值区信号)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 数据回调桥接
|
||||||
|
|
||||||
|
iec61850m 的数据回调完成 MMS 值 → datacenter 的转换:
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_m_out_get_value_callback(out_val):
|
||||||
|
→ 根据 reason 分类处理:
|
||||||
|
ALL_CALL: dc_set_out_signal_val(name, p_value, type, quality)
|
||||||
|
GI: dc_set_out_signal_val(name, p_value, type, quality)
|
||||||
|
dchg/qchg: // 报告触发,同上
|
||||||
|
→ 对 CO/AO/Param: 转发到对应的 datacenter 写接口
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 配置文件格式(mms_m.xml)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<IED name="PCS9700" ip="192.168.1.100" port="102" rcb_numbers="01,02">
|
||||||
|
<St>
|
||||||
|
<Signal no="1" link="ST_Signal_1" reference="PCS9700CTRL/GGIO1$ST$Ind1$stVal" /> <!-- (1) -->
|
||||||
|
</St>
|
||||||
|
<Mx>
|
||||||
|
<Signal no="1" link="MX_Signal_1" reference="PCS9700MEAS/MMXU1$MX$PhV$phsA$cVal$mag$f" />
|
||||||
|
</Mx>
|
||||||
|
<Co>
|
||||||
|
<Signal no="1" link="YK_Signal_1" reference="PCS9700CTRL/CSWI1$CO$Pos$Oper$ctlVal" />
|
||||||
|
</Co>
|
||||||
|
<Ao>
|
||||||
|
<Signal no="1" link="SG_Signal_1" reference="PCS9700PROT/LLN0$SG$sg1$StrVal" />
|
||||||
|
</Ao>
|
||||||
|
<Param>
|
||||||
|
<Signal no="1" link="Param_Signal_1" reference="PCS9700PROT/PTOC1$SG$sg1$StrVal" />
|
||||||
|
</Param>
|
||||||
|
</IED>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `link`: 信号 saddr,对应 datacenter 中的信号地址
|
||||||
|
- `reference`: IEC 61850 对象引用,用于 `IedConnection_readObject`
|
||||||
|
- `name`: IED 名称,用于连接日志
|
||||||
|
- `rcb_numbers`: RCB 编号过滤(逗号分隔,可选)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 常见使用场景
|
||||||
|
|
||||||
|
### 5.1 读取遥测/遥信
|
||||||
|
|
||||||
|
```
|
||||||
|
// 自动完成 — 定时器 T0 (120s) 触发 All-Call,T1 (60s) 触发 GI
|
||||||
|
// 数据通过 mms_m_out_get_value 注册的回调推送
|
||||||
|
// 用户只需在 iec61850m_init1 中调用 mms_m_out_get_value 注册回调
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 手动读取 AO 值
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 读取指定 AO
|
||||||
|
mms_m_out_read_ao_or_params(fd, _MMS_M_EVENT_AO_READ, "AO_Signal_1");
|
||||||
|
// 读取全部 AO
|
||||||
|
mms_m_out_read_ao_or_params(fd, _MMS_M_EVENT_AO_READ, nullptr);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 下达遥控命令
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// SBO 模式
|
||||||
|
stru_mms_m_event event = {};
|
||||||
|
event.app_fd = fd;
|
||||||
|
event.ctrl_type = _MMS_M_EVENT_CO_SELECT;
|
||||||
|
strncpy(event.saddr, "YK_Signal_1", sizeof(event.saddr));
|
||||||
|
mms_m_out_do_set_yk(&event);
|
||||||
|
|
||||||
|
// 确认后执行
|
||||||
|
event.ctrl_type = _MMS_M_EVENT_CO_DIRECT;
|
||||||
|
mms_m_out_do_set_yk(&event);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 写入定值
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 定值写入通过 mms_m_out_read_ao_or_params 触发 O→Param 流程
|
||||||
|
// libmms_m 内部自动处理 SG/SE 编辑-确认流程
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 RCB 编号配置
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 只订阅 01 号 RCB
|
||||||
|
mms_m_out_set_rcb_numbers(fd, "01");
|
||||||
|
// 订阅 01, 02, 03 号
|
||||||
|
mms_m_out_set_rcb_numbers(fd, "01,02,03");
|
||||||
|
// 订阅全部
|
||||||
|
mms_m_out_set_rcb_numbers(fd, "*");
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
# libiec61850_MMS服务端API开发手册
|
||||||
|
|
||||||
|
**日期**: 2026-06-12
|
||||||
|
**基于源码**: `src/system/libiec61850s/`, `src/protocol/libmms_s/inc/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 模块层次
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ iec61850s (系统层) │ ← 本文档描述
|
||||||
|
│ - XML配置解析、信号注册 │
|
||||||
|
│ - 回调桥接到 datacenter │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ libmms_s (协议层) │ ← 底层 API,详见《libmms_s模块分析》
|
||||||
|
│ - ICD解析、模型创建、IedServer│
|
||||||
|
│ - 控制/定值/文件服务 │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 核心概念
|
||||||
|
|
||||||
|
### 2.1 sAddr(信号地址)
|
||||||
|
|
||||||
|
sAddr 是 IEC 61850 标准中的短地址字段,在 ICD 文件中定义。RTU 用它作为 datacenter 中信号的唯一标识,打通 "MMS 数据属性 ↔ datacenter 信号" 的映射。
|
||||||
|
|
||||||
|
### 2.2 信号类型
|
||||||
|
|
||||||
|
| 分类 | ICD FC | datacenter 表 | 说明 |
|
||||||
|
|------|--------|-------------|------|
|
||||||
|
| ST(状态) | ST | signal_out | 遥信/双点状态 |
|
||||||
|
| MX(测量) | MX | signal_out | 遥测/测量值 |
|
||||||
|
| CO(控制) | CO | signal_yk | 遥控输出 |
|
||||||
|
| AO(模拟输出) | 自定义 | signal_ao | 定值区号/SP定值 |
|
||||||
|
| Param(参数) | SG/SE | signal_param | 定值组参数(多区) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 配置文件格式(mms_s.xml)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mms_s>
|
||||||
|
<St>
|
||||||
|
<Signal no="1" link="ST_Signal_1" />
|
||||||
|
<Signal no="2" link="ST_Signal_2" />
|
||||||
|
</St>
|
||||||
|
<Mx>
|
||||||
|
<Signal no="1" link="MX_Signal_1" />
|
||||||
|
</Mx>
|
||||||
|
<Co>
|
||||||
|
<Signal no="1" link="YK_Signal_1" />
|
||||||
|
</Co>
|
||||||
|
<Ao>
|
||||||
|
<Signal no="1" link="SG_Signal_1" />
|
||||||
|
</Ao>
|
||||||
|
<Param>
|
||||||
|
<Signal no="1" link="Param_Signal_1" />
|
||||||
|
</Param>
|
||||||
|
</mms_s>
|
||||||
|
```
|
||||||
|
|
||||||
|
- `no`: 序号
|
||||||
|
- `link`: sAddr,对应 datacenter 中的信号地址和 ICD 中的 sAddr 字段
|
||||||
|
|
||||||
|
配置解析后存入 `stru_mms_cfg`,包含 `vec_st`, `vec_mx`, `vec_co`, `vec_ao`, `vec_param` 五个向量。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 初始化流程
|
||||||
|
|
||||||
|
### 4.1 两阶段初始化
|
||||||
|
|
||||||
|
```
|
||||||
|
app_iec61850s_init1:
|
||||||
|
1. 获取进程目录(通过 /proc/self/exe)
|
||||||
|
2. 解析 config/MMS/mms_s.xml → stru_mms_cfg(信号点表)
|
||||||
|
3. 设置调试开关: mms_s_dbg_switch(false)
|
||||||
|
4. 设置文件路径: mms_s_file_path_set(base_path)
|
||||||
|
5. 注册值更新回调: mms_s_value_update_register(&cb)
|
||||||
|
|
||||||
|
app_iec61850s_init2:
|
||||||
|
1. iec61850s_signals_init:
|
||||||
|
- iec61850s_st_signals_init:
|
||||||
|
dc_signal_out_link_with_callback → 获取 ST 信号指针并注册变更回调
|
||||||
|
回调: iec61850s_st_mx_change_callback → mms_s_value_update → 更新 MMS 模型值
|
||||||
|
- iec61850s_mx_signals_init: 同上(MX 信号)
|
||||||
|
- iec61850s_control_signals_init:
|
||||||
|
dc_get_yk_signal_info → 获取控制信号信息
|
||||||
|
mms_s_control_register → 向 libmms_s 注册控制回调
|
||||||
|
回调: iec61850s_control_callback → dc_signal_yk_set_status
|
||||||
|
- iec61850s_setting_signals_init:
|
||||||
|
dc_signal_ao_link_with_callback → 绑定 AO 信号
|
||||||
|
mms_s_setting_register → 注册 SP 定值回调
|
||||||
|
回调: iec61850s_setting_callback → dc_signal_ao_set_val
|
||||||
|
- iec61850s_param_signals_init:
|
||||||
|
dc_get_param_signal_info → 获取 Param 信号指针
|
||||||
|
mms_s_param_register → 注册定值组回调
|
||||||
|
回调: iec61850s_param_callback → dc_signal_param_set_val
|
||||||
|
2. 解析 ICD 文件: config/MMS/PCS.icd
|
||||||
|
3. 调用 mms_s_init(icd_path, 102) 启动 MMS 服务器
|
||||||
|
4. 绑定定值区信号: mms_s_bind_param_zone_signal(zone_saddr, iec61850s_sg_change_callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 信号初始化详细流程
|
||||||
|
|
||||||
|
**ST/MX 信号**(单向:datacenter → MMS):
|
||||||
|
```
|
||||||
|
datacenter 信号值变化
|
||||||
|
→ iec61850s_st_mx_change_callback(saddr, type, p_data, p_last_data)
|
||||||
|
→ dc_get_signal_val(p_data, type) → 字符串值
|
||||||
|
→ mms_s_value_update(saddr, val) → 更新 MMS 模型 → 触发 RCB 报告
|
||||||
|
```
|
||||||
|
|
||||||
|
**控制信号**(双向:MMS 客户端 → datacenter):
|
||||||
|
```
|
||||||
|
远方 MMS 客户端发送控制命令
|
||||||
|
→ libmms_s control_handler → iec61850s_control_callback(control, state)
|
||||||
|
→ 判断 ctrl_model:
|
||||||
|
DIRECT: dc_signal_yk_set_status(DIRECT)
|
||||||
|
SBO: dc_signal_yk_set_status(SELECT) + dc_signal_yk_set_status(DIRECT)
|
||||||
|
STATUS_ONLY: 仅记录日志
|
||||||
|
```
|
||||||
|
|
||||||
|
**AO/SP 定值信号**(双向:MMS → datacenter):
|
||||||
|
```
|
||||||
|
远方客户端修改 SP 定值
|
||||||
|
→ libmms_s setting writeAccessHandler → iec61850s_setting_callback(setting, data)
|
||||||
|
→ dc_set_signal_val_from_str → 解析字符串
|
||||||
|
→ dc_signal_ao_set_val(SELECT+DIRECT or DIRECT only)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Param 定值组信号**(双向,支持多区):
|
||||||
|
```
|
||||||
|
远方客户端 ConfirmEditSG
|
||||||
|
→ libmms_s confirmEditSG_callback → iec61850s_param_callback(param, data, zone)
|
||||||
|
→ dc_signal_param_set_val(zone, data)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 核心 API
|
||||||
|
|
||||||
|
### 5.1 信号注册(iec61850s 内部)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int iec61850s_signals_init();
|
||||||
|
```
|
||||||
|
**功能**: 从 XML 配置初始化全部五类信号,注册各类回调到 libmms_s。在 `app_iec61850s_init2` 中调用。
|
||||||
|
|
||||||
|
### 5.2 值更新(libmms_s 提供)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void mms_s_value_update_register(mms_s_value_update_cb *pp_cb);
|
||||||
|
int mms_s_value_update(const char *saddr, const char *value_str);
|
||||||
|
```
|
||||||
|
**功能**: 更新 MMS 模型中指定 sAddr 的 DA 值,自动更新时标和品质。
|
||||||
|
|
||||||
|
### 5.3 控制注册(libmms_s 提供)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int mms_s_control_register(stru_mms_s_control *p_control, int count, mms_s_control_cb p_callback);
|
||||||
|
```
|
||||||
|
**功能**: 注册控制 DO 的回调处理函数。
|
||||||
|
|
||||||
|
`stru_mms_s_control`:
|
||||||
|
```c
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
stru_mms_s_signal_base base; // saddr, desc, type, ctrl_model
|
||||||
|
void *p_data; // 信号数据指针(指向 datacenter)
|
||||||
|
}stru_mms_s_control;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 SP 定值注册(libmms_s 提供)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int mms_s_setting_register(stru_mms_s_setting *p_setting, int count, mms_s_setting_cb p_callback);
|
||||||
|
```
|
||||||
|
**功能**: 注册 SP(fc=SP)定值的写入回调。
|
||||||
|
|
||||||
|
### 5.5 定值组注册(libmms_s 提供)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int mms_s_param_register(stru_mms_s_param *p_param, int count, mms_s_param_cb p_callback);
|
||||||
|
```
|
||||||
|
**功能**: 注册定值组参数(SG/SE)的确认回调。
|
||||||
|
|
||||||
|
`stru_mms_s_param`:
|
||||||
|
```c
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
stru_mms_s_signal_base base; // saddr, desc, type, ctrl_model
|
||||||
|
void *p_data[MAX_ZONE]; // 各定值区的数据指针
|
||||||
|
uint8_t param_num; // 定值区数量
|
||||||
|
}stru_mms_s_param;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 其他 libmms_s API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int mms_s_init(const char *icd_path, int stack_size); // 启动 MMS 服务器
|
||||||
|
void mms_s_dbg_switch(bool on); // 调试开关
|
||||||
|
void mms_s_file_path_set(const char *base_path); // 文件路径
|
||||||
|
int mms_s_bind_param_zone_signal(const char *zone_saddr, mms_s_sg_change_cb cb); // 绑定定值区
|
||||||
|
struct _sIedModel_wrapper *mms_s_get_icd_ptr(); // 获取 IedModel
|
||||||
|
IedServer mms_s_get_ied_server_ptr(); // 获取 IedServer
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 运行时流程
|
||||||
|
|
||||||
|
### 6.1 主线程循环
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void *app_iec61850s(void *arg):
|
||||||
|
while(1):
|
||||||
|
task_event_recv(p_event, EV_TIMER1 | EV_TIMER2 | EV_TIMER3, ...)
|
||||||
|
|
||||||
|
EV_TIMER1 (10ms): 空闲
|
||||||
|
EV_TIMER2 (100ms): 空闲
|
||||||
|
EV_TIMER3 (1000ms):
|
||||||
|
- check_sg_zone_change(): 检测定值区号变化,更新 MMS 模型
|
||||||
|
- p_app->run_cnt++: 运行计数
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 定值区切换检测
|
||||||
|
|
||||||
|
```
|
||||||
|
ie61850s_sg_change_callback(zone_saddr, new_act_sg):
|
||||||
|
→ 遍历 g_vec_setting 找到匹配的 saddr
|
||||||
|
→ dc_signal_ao_set_val(SELECT + DIRECT) 通过 datacenter 更新值
|
||||||
|
→ 设置 g_sg_zone_saddr 和 g_sg_zone_act_sg
|
||||||
|
|
||||||
|
check_sg_zone_change() (EV_TIMER3 中):
|
||||||
|
→ 如果有待更新信号 → mms_s_value_update(saddr, val)
|
||||||
|
→ 更新 MMS 模型,通知所有订阅客户端
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 常见配置场景
|
||||||
|
|
||||||
|
### 7.1 添加新的遥测信号
|
||||||
|
|
||||||
|
1. 在 ICD 中定义 DA 的 sAddr
|
||||||
|
2. 在 `mms_s.xml` 的 `<Mx>` 中添加 `<Signal link="新sAddr" />`
|
||||||
|
3. 在 datacenter 初始化中注册该信号
|
||||||
|
4. 重启 RTU,初始化时自动绑定
|
||||||
|
|
||||||
|
### 7.2 添加新的遥控信号
|
||||||
|
|
||||||
|
1. ICD 中定义含 `ctlModel` 的 CO DO
|
||||||
|
2. `mms_s.xml` 的 `<Co>` 中添加信号
|
||||||
|
3. datacenter 中注册 yk 信号
|
||||||
|
4. `iec61850s_control_signals_init` 自动通过 sAddr 找到对应的 ModelNode 安装控制回调
|
||||||
|
|
||||||
|
### 7.3 配置多定值区参数
|
||||||
|
|
||||||
|
1. ICD 中 SG 定义多组 SE
|
||||||
|
2. `mms_s.xml` 的 `<Param>` 中添加信号
|
||||||
|
3. datacenter 中注册 param 信号(`dc_signal_param_reg` 需指定定值区数)
|
||||||
|
4. `iec61850s_param_signals_init` 自动获取各定值区数据指针
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
# libiec61850m 模块分析
|
||||||
|
|
||||||
|
**日期**: 2026-06-12
|
||||||
|
**基于源码**: `src/system/libiec61850m/` (iec61850m.cpp, parse_xml.cpp, 共4个文件)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 模块定位
|
||||||
|
|
||||||
|
`libiec61850m` 位于系统层,是 `libmms_m` 的上层封装。它负责:
|
||||||
|
- 解析 XML 配置(`mms_m.xml`)
|
||||||
|
- 编排各 IED 的信号初始化流程
|
||||||
|
- 将 MMS 读取的数据通过回调桥接到 `datacenter`
|
||||||
|
- 接收来自其他模块的遥控命令,转发到 `libmms_m`
|
||||||
|
|
||||||
|
**与其他模块的关系**:
|
||||||
|
```
|
||||||
|
app_sys (调度) → app_iec61850m (线程,EV_TIMER 驱动)
|
||||||
|
↓
|
||||||
|
libiec61850m (本模块,系统层封装)
|
||||||
|
↓ 调用 API
|
||||||
|
libmms_m (协议层,每个 IED 一个线程)
|
||||||
|
↓ IedConnection API
|
||||||
|
远方 IED 装置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 配置解析(parse_xml.cpp)
|
||||||
|
|
||||||
|
### 2.1 XML 结构
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mms_m rcb_numbers="01,02">
|
||||||
|
<IED name="PCS9700" ip="192.168.1.100" port="102">
|
||||||
|
<St><Signal no="1" reference="..." /></St>
|
||||||
|
<Mx><Signal no="1" reference="..." /></Mx>
|
||||||
|
<Co><Signal no="1" reference="..." /></Co>
|
||||||
|
<Ao><Signal no="1" reference="..." /></Ao>
|
||||||
|
<Param><Signal no="1" reference="..." /></Param>
|
||||||
|
</IED>
|
||||||
|
</mms_m>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 解析流程
|
||||||
|
|
||||||
|
```
|
||||||
|
parse_mms_xml(path):
|
||||||
|
→ 加载 XML → <mms_m> 根元素
|
||||||
|
→ mms_m_parse_para(root, cfg):
|
||||||
|
提取全局 rcb_numbers 属性
|
||||||
|
→ 遍历每个 <IED> 子元素:
|
||||||
|
mms_m_parse_base(iedEle, cfg):
|
||||||
|
提取 name, ip, port
|
||||||
|
mms_m_parse_signals: 遍历 St/Mx/Co/Ao/Param
|
||||||
|
→ 每个 Signal: 提取 link(saddr), reference, type, ctrl_model
|
||||||
|
→ 存入 stru_mms_cfg 对应的 vector
|
||||||
|
存入 cfg.vec_ieds
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 数据结构
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
std::string path; // XML 文件路径
|
||||||
|
int rcb_numbers_flag; // 是否有全局 rcb_numbers
|
||||||
|
std::string rcb_numbers; // 全局 RCB 编号(如 "01,02")
|
||||||
|
std::vector<stru_mms_ied_cfg> vec_ieds; // 各 IED 的配置
|
||||||
|
}stru_mms_cfg;
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
std::string name; // IED 名称
|
||||||
|
std::string ip; // IP 地址
|
||||||
|
int port; // MMS 端口(通常 102)
|
||||||
|
std::string rcb_numbers; // 此 IED 的 RCB 编号
|
||||||
|
std::vector<stru_mms_signal> vec_st;
|
||||||
|
std::vector<stru_mms_signal> vec_mx;
|
||||||
|
std::vector<stru_mms_signal> vec_co;
|
||||||
|
std::vector<stru_mms_signal> vec_ao;
|
||||||
|
std::vector<stru_mms_signal> vec_param;
|
||||||
|
}stru_mms_ied_cfg;
|
||||||
|
```
|
||||||
|
|
||||||
|
`stru_mms_signal`(与 `myMms_m.h` 中的 `stru_point_item` 对应):
|
||||||
|
```cpp
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char saddr[MMS_M_STR_LEN]; // 信号地址(link 属性)
|
||||||
|
char reference[MMS_M_REF_LEN]; // MMS 对象引用
|
||||||
|
char desc[MMS_M_STR_LEN]; // 描述
|
||||||
|
uint8_t type; // 数据类型
|
||||||
|
uint8_t ctrl_model; // 控制模型
|
||||||
|
}stru_mms_signal;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 两阶段初始化
|
||||||
|
|
||||||
|
### 3.1 第一阶段:`app_iec61850m_init1`
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 获取进程目录(/proc/self/exe → 项目根目录)
|
||||||
|
2. 解析 config/MMS/mms_m.xml → stru_mms_cfg
|
||||||
|
3. 为每个 IED:
|
||||||
|
a. 将信号配置转换为 libmms_m 需要的 stru_cfg 格式
|
||||||
|
b. 调用 mms_m_out_init(p_cfg, debug, timeout) → obj_fd
|
||||||
|
c. 调用 mms_m_out_get_value(obj_fd, iec61850m_value_callback)
|
||||||
|
注册数据回调 → 桥接到 datacenter
|
||||||
|
d. 调用 mms_m_out_get_connect_status(obj_fd, iec61850m_status_callback)
|
||||||
|
注册连接状态回调
|
||||||
|
4. 保存 obj_fd 列表到全局
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 第二阶段:`app_iec61850m_init2`
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 遍历所有 IED 的 obj_fd:
|
||||||
|
a. mms_m_out_debug_print_swicth(fd, debug_flag) // 设置调试
|
||||||
|
b. 如果配置中指定了 rcb_numbers:
|
||||||
|
mms_m_out_set_rcb_numbers(fd, rcb_numbers)
|
||||||
|
c. 如果配置了定值区信号:
|
||||||
|
mms_m_out_bind_param_zone_signal(fd, zone_saddr)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 为什么分两阶段
|
||||||
|
|
||||||
|
- `init1` 执行时 datacenter 尚未完全初始化
|
||||||
|
- `init2` 在 `init1` 之后执行,此时 datacenter 已就绪,可以安全注册信号绑定
|
||||||
|
|
||||||
|
## 4. 数据回调桥接
|
||||||
|
|
||||||
|
`iec61850m_value_callback` 是核心桥接函数,将 libmms_m 推送的 `stru_mms_m_out_value` 转换为 datacenter 操作:
|
||||||
|
|
||||||
|
```
|
||||||
|
iec61850m_value_callback(out_val):
|
||||||
|
→ 根据 out_val.reason 分类:
|
||||||
|
MMS_M_REASON_ALL_CALL: // 定时全数据刷新
|
||||||
|
MMS_M_REASON_READ_AO: // 主动读取 AO
|
||||||
|
MMS_M_REASON_READ_PARAM: // 主动读取 Param
|
||||||
|
IEC61850_REASON_DATA_CHANGE / QUALITY_CHANGE / GI:
|
||||||
|
// 报告触发
|
||||||
|
→ 写入 datacenter:
|
||||||
|
- ST/MX (out): dc_set_out_signal_val(name, p_value, type)
|
||||||
|
- AO: dc_signal_ao_set_val(...)
|
||||||
|
- Param: dc_signal_param_set_val(...)
|
||||||
|
- CO: // CO 由操作完成回调处理,不走数据回调
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 遥控命令转发
|
||||||
|
|
||||||
|
其他模块(如 CLI、self_ptl)通过以下路径下发遥控:
|
||||||
|
|
||||||
|
```
|
||||||
|
外部模块 → iec61850m_do_set_yk(event)
|
||||||
|
→ 填充 stru_mms_m_event:
|
||||||
|
- app_fd: 目标 IED 句柄
|
||||||
|
- ctrl_type: SELECT/DIRECT/CANCEL
|
||||||
|
- saddr: 信号地址
|
||||||
|
- data: 操作数据
|
||||||
|
→ mms_m_out_do_set_yk(&event) → libmms_m 处理
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 线程模型
|
||||||
|
|
||||||
|
```
|
||||||
|
app_iec61850m 线程:
|
||||||
|
while(1):
|
||||||
|
task_event_recv(EV_TIMER1 | EV_TIMER2 | EV_TIMER3)
|
||||||
|
EV_TIMER1 (10ms): 空闲
|
||||||
|
EV_TIMER2 (100ms): 空闲
|
||||||
|
EV_TIMER3 (1000ms): p_app->run_cnt++
|
||||||
|
```
|
||||||
|
|
||||||
|
主工作不在 `app_iec61850m` 线程中,而是在 libmms_m 为每个 IED 创建的独立线程 `mms_m_run_thread` 中。`app_iec61850m` 线程仅用于接收定时器事件和消息队列。
|
||||||
|
|
||||||
|
## 7. 模块间数据流
|
||||||
|
|
||||||
|
```
|
||||||
|
远方 IED 装置
|
||||||
|
↓ MMS 协议
|
||||||
|
libmms_m (mms_m_run_thread)
|
||||||
|
↓ mms_m_out_value_cb
|
||||||
|
iec61850m_value_callback
|
||||||
|
↓ dc_set_out_signal_val / dc_signal_ao_set_val / dc_signal_param_set_val
|
||||||
|
datacenter (signal_out/signal_ao/signal_param 表)
|
||||||
|
↓ 变更通知
|
||||||
|
其他模块 (self_ptl, com_channel, web_server, iec61850s)
|
||||||
|
|
||||||
|
控制方向(反向):
|
||||||
|
CLI / self_ptl
|
||||||
|
↓ dc_signal_yk_set_status / iec61850m_do_set_yk
|
||||||
|
libmms_m (事件队列 → mms_m_send_co)
|
||||||
|
↓ ControlObjectClient_select/operate/cancel
|
||||||
|
远方 IED 装置
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
# libiec61850s 模块分析
|
||||||
|
|
||||||
|
**日期**: 2026-06-12
|
||||||
|
**基于源码**: `src/system/libiec61850s/` (iec61850s.cpp, parse_xml.cpp, 共4个文件)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 模块定位
|
||||||
|
|
||||||
|
`libiec61850s` 位于系统层,是 `libmms_s` 的上层封装。它负责将 datacenter 中的信号注册到 MMS 服务端模型中,使远方客户端能通过 IEC 61850 MMS 协议读取 RTU 数据并下发控制命令。
|
||||||
|
|
||||||
|
**与其他模块的关系**:
|
||||||
|
```
|
||||||
|
远方 MMS 客户端
|
||||||
|
↓ MMS 协议
|
||||||
|
libmms_s (协议层, IedServer)
|
||||||
|
↑↓ 回调
|
||||||
|
libiec61850s (本模块)
|
||||||
|
↑↓ dc_signal_* API
|
||||||
|
datacenter (信号存储)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 配置解析(parse_xml.cpp)
|
||||||
|
|
||||||
|
### 2.1 XML 结构
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<mms_s>
|
||||||
|
<St>
|
||||||
|
<Signal no="1" link="signal_saddr_1" />
|
||||||
|
</St>
|
||||||
|
<Mx>
|
||||||
|
<Signal no="1" link="signal_saddr_2" />
|
||||||
|
</Mx>
|
||||||
|
<Co>
|
||||||
|
<Signal no="1" link="signal_saddr_3" />
|
||||||
|
</Co>
|
||||||
|
<Ao>
|
||||||
|
<Signal no="1" link="signal_saddr_4" />
|
||||||
|
</Ao>
|
||||||
|
<Param>
|
||||||
|
<Signal no="1" link="signal_saddr_5" />
|
||||||
|
</Param>
|
||||||
|
</mms_s>
|
||||||
|
```
|
||||||
|
|
||||||
|
解析函数 `parse_mms_xml(path)` 使用 tinyxml2 遍历五类信号元素,提取 `link` 属性存入 `stru_mms_cfg`。
|
||||||
|
|
||||||
|
### 2.2 数据结构
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
char saddr[MMS_S_STR_LEN];
|
||||||
|
char desc[MMS_S_STR_LEN];
|
||||||
|
uint8_t type;
|
||||||
|
uint8_t ctrl_model;
|
||||||
|
}stru_mms_s_signal_base;
|
||||||
|
|
||||||
|
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;
|
||||||
|
```
|
||||||
|
|
||||||
|
配置保存在全局变量 `g_cfg` 中,通过 `mms_cfg_ptr_get()` 获取。
|
||||||
|
|
||||||
|
## 3. 两阶段初始化
|
||||||
|
|
||||||
|
### 3.1 `app_iec61850s_init1`(第一阶段)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 获取进程目录
|
||||||
|
2. 解析 config/MMS/mms_s.xml → stru_mms_cfg
|
||||||
|
3. mms_s_dbg_switch(false) // 关闭调试
|
||||||
|
4. mms_s_file_path_set(base_path) // 设置文件服务根目录
|
||||||
|
5. mms_s_value_update_register(&g_mms_s_value_update_cb) // 注册值更新回调
|
||||||
|
```
|
||||||
|
|
||||||
|
此时仅完成配置解析和底层回调注册,datacenter 尚未就绪。
|
||||||
|
|
||||||
|
### 3.2 `app_iec61850s_init2`(第二阶段)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. iec61850s_signals_init(): // 核心信号初始化
|
||||||
|
- iec61850s_st_signals_init:
|
||||||
|
对每个 ST 信号调用 dc_signal_out_link_with_callback
|
||||||
|
→ 获取 datacenter 信号指针 + 注册变更回调
|
||||||
|
→ 回调: st_mx → mms_s_value_update (更新 MMS 模型)
|
||||||
|
|
||||||
|
- iec61850s_mx_signals_init:
|
||||||
|
同上(MX 信号)
|
||||||
|
|
||||||
|
- iec61850s_control_signals_init:
|
||||||
|
对每个 CO 信号:
|
||||||
|
dc_get_yk_signal_info → 获取类型和 ctrl_model
|
||||||
|
mms_s_control_register → 向 libmms_s 注册控制回调
|
||||||
|
回调接收远方控制命令 → dc_signal_yk_set_status
|
||||||
|
|
||||||
|
- iec61850s_setting_signals_init:
|
||||||
|
对每个 AO 信号:
|
||||||
|
dc_signal_ao_link_with_callback → 绑定 SP 定值
|
||||||
|
mms_s_setting_register → 注册写入回调
|
||||||
|
回调接收客户端修改 → dc_signal_ao_set_val
|
||||||
|
|
||||||
|
- iec61850s_param_signals_init:
|
||||||
|
对每个 Param 信号:
|
||||||
|
dc_get_param_signal_info → 获取各定值区指针
|
||||||
|
mms_s_param_register → 注册 ConfirmEditSG 回调
|
||||||
|
回调接收定值确认 → dc_signal_param_set_val
|
||||||
|
|
||||||
|
2. mms_s_init(PCS.icd, 102):
|
||||||
|
解析 ICD → 创建模型 → 启动 IedServer(端口 102)
|
||||||
|
|
||||||
|
3. mms_s_bind_param_zone_signal(zone_saddr, iec61850s_sg_change_callback):
|
||||||
|
绑定定值区号 → 定值区切换时自动同步
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 信号方向与回调链
|
||||||
|
|
||||||
|
### 4.1 数据上行(datacenter → MMS 客户端)
|
||||||
|
|
||||||
|
```
|
||||||
|
datacenter 信号变化
|
||||||
|
→ iec61850s_st_mx_change_callback(saddr, type, p_data, p_last_data)
|
||||||
|
→ dc_get_signal_val → 格式化为字符串
|
||||||
|
→ g_mms_s_value_update_cb(saddr, val_str) // 即 mms_s_value_update
|
||||||
|
→ IedServer_updateAttributeValue → 更新模型值 → 触发 RCB 报告 → 通知订阅客户端
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 控制下行(MMS 客户端 → datacenter)
|
||||||
|
|
||||||
|
```
|
||||||
|
远方客户端控制命令
|
||||||
|
→ libmms_s control_handler
|
||||||
|
→ iec61850s_control_callback(p_control, state):
|
||||||
|
state == 1 (命令到达):
|
||||||
|
根据 ctrl_model:
|
||||||
|
DIRECT/SBO: dc_signal_yk_set_status(SELECT + DIRECT)
|
||||||
|
STATUS_ONLY: 日志记录
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 SP 定值修改(MMS 客户端 → datacenter)
|
||||||
|
|
||||||
|
```
|
||||||
|
远方客户端修改 SP 定值
|
||||||
|
→ libmms_s setting writeAccessHandler
|
||||||
|
→ iec61850s_setting_callback(p_setting, p_data):
|
||||||
|
dc_set_signal_val_from_str → 解析字符串值
|
||||||
|
dc_signal_ao_set_val(SELECT + DIRECT or DIRECT only)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 定值组修改(MMS 客户端 → datacenter)
|
||||||
|
|
||||||
|
```
|
||||||
|
远方客户端 ConfirmEditSG
|
||||||
|
→ libmms_s confirmEditSG_callback
|
||||||
|
→ iec61850s_param_callback(p_param, p_data, zone):
|
||||||
|
dc_set_signal_val_from_str → 解析
|
||||||
|
dc_signal_param_set_val(zone, data)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 定值区切换同步
|
||||||
|
|
||||||
|
```
|
||||||
|
datacenter 定值区号变化
|
||||||
|
→ iec61850s_sg_change_callback(zone_saddr, new_act_sg):
|
||||||
|
找到对应的 setting 信号
|
||||||
|
dc_signal_ao_set_val: 同步新值到 AO 信号
|
||||||
|
设置 g_sg_zone_saddr / g_sg_zone_act_sg
|
||||||
|
→ check_sg_zone_change() (EV_TIMER3 中):
|
||||||
|
mms_s_value_update: 更新 MMS 模型中 SGCB.ActSG 值
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 主线程循环
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void *app_iec61850s(void *arg):
|
||||||
|
while(1):
|
||||||
|
task_event_recv(EV_TIMER1 | EV_TIMER2 | EV_TIMER3)
|
||||||
|
|
||||||
|
EV_TIMER1 (10ms): 空闲
|
||||||
|
EV_TIMER2 (100ms): 空闲
|
||||||
|
EV_TIMER3 (1000ms):
|
||||||
|
check_sg_zone_change() // 定值区号变更同步到 MMS 模型
|
||||||
|
p_app->run_cnt++
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 全局变量
|
||||||
|
|
||||||
|
| 变量 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `g_vec_st` | `vector<stru_local_st_mx>` | ST 信号列表(含 datacenter 指针) |
|
||||||
|
| `g_vec_mx` | `vector<stru_local_st_mx>` | MX 信号列表 |
|
||||||
|
| `g_vec_control` | `vector<stru_mms_s_control>` | 控制信号列表 |
|
||||||
|
| `g_vec_setting` | `vector<stru_mms_s_setting>` | SP 定值信号列表 |
|
||||||
|
| `g_vec_param` | `vector<stru_mms_s_param>` | 定值组参数列表 |
|
||||||
|
| `g_mms_s_value_update_cb` | 函数指针 | MMS 值更新回调(= mms_s_value_update) |
|
||||||
|
| `g_sg_zone_saddr` | string | 待同步的定值区信号地址 |
|
||||||
|
| `g_sg_zone_act_sg` | string | 待同步的新定值区号 |
|
||||||
|
|
||||||
|
## 7. 与 libiec61850m 的对比
|
||||||
|
|
||||||
|
| 特性 | libiec61850m (客户端) | libiec61850s (服务端) |
|
||||||
|
|------|----------------------|----------------------|
|
||||||
|
| 角色 | 主动读取/控制远方 IED | 被动响应远方客户端请求 |
|
||||||
|
| 数据方向 | 拉取(All-Call/GI) | 推送(RCB 报告触发) |
|
||||||
|
| 控制 | 下发控制命令 | 接收并执行控制命令 |
|
||||||
|
| 连接 | 每个 IED 一个连接 | 监听端口,接受多客户端 |
|
||||||
|
| 定时器 | T0(120s)/T1(60s)/T2(30s) | EV_TIMER3(1s) 仅定值区同步 |
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
# libmms_m 模块分析
|
||||||
|
|
||||||
|
**日期**: 2026-06-12
|
||||||
|
**基于源码**: `src/protocol/libmms_m/`(8个文件,约3000行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 模块定位
|
||||||
|
|
||||||
|
`libmms_m` 是 RTU 的 IEC 61850 MMS 客户端库,位于协议层。它通过 libiec61850 库连接到远方 IED 装置,以事件驱动架构实现遥测/遥信读取(All-Call、GI)、遥控执行(SBO/Direct)、AO/参数读写、报告订阅等全部 MMS 客户端功能。
|
||||||
|
|
||||||
|
**与上下层关系**:
|
||||||
|
- **下层**: 依赖 libiec61850 v1.5.x 提供的 IedConnection API
|
||||||
|
- **上层**: 被 `iec61850m`(系统层封装)通过对外 API 调用
|
||||||
|
- **并发**: 每个 IED 连接一个独立线程 `mms_m_run_thread`,通过事件队列 + 信号量与其他线程通信
|
||||||
|
|
||||||
|
## 2. 核心数据结构
|
||||||
|
|
||||||
|
### 2.1 主对象 `stru_mms_m_obj`
|
||||||
|
|
||||||
|
每个 IED 连接创建一个 `stru_mms_m_obj` 实例,全局保存在 `g_mms_m_obj_map`(map<int, obj*>)中,以 `obj_fd` 为句柄。
|
||||||
|
|
||||||
|
| 字段 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `cfg_path` | 配置文件路径,用于错误日志和重复检测 |
|
||||||
|
| `ied_name` | IED 名称,从配置中提取 |
|
||||||
|
| `debug_print_flag` | 调试打印开关(`MMS_M_DEBUG_PRINT_ON = 1`) |
|
||||||
|
| `connectionTimeout` | 连接超时(毫秒) |
|
||||||
|
| `p_cfg` | 指向 `stru_cfg` 的点表配置(ST/MX/CO/AO/Param) |
|
||||||
|
| `ldevs` | LDevice 树(LD→LN→DO→point_item 指针),用于 dataset member 匹配 |
|
||||||
|
| `ld_datasets` | 运行时发现的 LD→Dataset→Member→RCB 完整结构 |
|
||||||
|
| `rcb_numbers` | 可配置的 RCB 编号列表(如 `{"01","02"}`),空则默认 `"01"` |
|
||||||
|
| `zone_saddr` | 绑定定值区信号的 saddr,用于检测定值区切换 |
|
||||||
|
| `current_zone` | 当前定值区号 |
|
||||||
|
| `obj_fd` | 对象句柄,对外 API 用此标识 |
|
||||||
|
| `run` | 运行时状态(连接、定时器、事件队列、回调列表) |
|
||||||
|
|
||||||
|
### 2.2 运行时结构 `stru_mms_m_run`
|
||||||
|
|
||||||
|
| 字段 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| `con` | `IedConnection` 句柄 |
|
||||||
|
| `con_state` / `old_con_state` | 连接状态(前后帧对比检测上线/离线) |
|
||||||
|
| `ip` / `port` | 远方 IED 的 IP/端口 |
|
||||||
|
| `running_init` | ICD 初始化是否完成标志 |
|
||||||
|
| `event_queue` | 环形事件队列(容量 `EVENT_QUEUE_SIZE`) |
|
||||||
|
| `sem` | 信号量,保护事件队列的读写 |
|
||||||
|
| `timer[4]` | 四个定时器 T0-T3 |
|
||||||
|
| `pthread_task` | 工作线程句柄 |
|
||||||
|
| `out_cb_lists` | 数据回调函数列表,向上层推送读取到的值 |
|
||||||
|
| `out_status_cb` | 连接状态回调(上线/离线通知) |
|
||||||
|
|
||||||
|
### 2.3 事件类型 `_MMS_M_EVENT`
|
||||||
|
|
||||||
|
```
|
||||||
|
_MMS_M_EVENT_ALL_CALL // 全数据读取(ST+MX)
|
||||||
|
_MMS_M_EVENT_GI_CALL // 总召
|
||||||
|
_MMS_M_EVENT_CO_SELECT // 遥控选择
|
||||||
|
_MMS_M_EVENT_CO_DIRECT // 遥控执行
|
||||||
|
_MMS_M_EVENT_CO_CANCEL // 遥控取消
|
||||||
|
_MMS_M_EVENT_AO_READ // 读取AO值
|
||||||
|
_MMS_M_EVENT_AO_WRITE // 写入AO值
|
||||||
|
_MMS_M_EVENT_PARAM_READ // 读取参数值
|
||||||
|
_MMS_M_EVENT_PARAM_WRITE // 写入参数值
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 定时器配置
|
||||||
|
|
||||||
|
| 定时器 | 间隔 | 触发动作 |
|
||||||
|
|--------|------|---------|
|
||||||
|
| T0 | 120s | 发送 All-Call → 全数据刷新 |
|
||||||
|
| T1 | 60s | 发送 GI → 总召 |
|
||||||
|
| T2 | 30s | 读取所有 AO + Param |
|
||||||
|
| T3 | 20s | 预留(未使用) |
|
||||||
|
|
||||||
|
定时器实现为倒计数(`cnt--`),在每次 `mms_m_run` 中检查。到零时触发回调并重置计数。
|
||||||
|
|
||||||
|
## 3. 核心流程
|
||||||
|
|
||||||
|
### 3.1 初始化流程
|
||||||
|
|
||||||
|
```
|
||||||
|
上层调用 mms_m_out_init(p_cfg, debug_flag, timeout)
|
||||||
|
→ 检查配置文件是否重复
|
||||||
|
→ new stru_mms_m_obj,初始化基础字段
|
||||||
|
→ mms_m_ied_init(obj):
|
||||||
|
- 提取 ied_name, ip, port
|
||||||
|
- 遍历 ST/MX/CO/AO/Param 点表
|
||||||
|
- 调用 mms_m_add_point_to_ldevs 构建 LD→LN→DO→point 树形索引
|
||||||
|
- 用于后续 dataset member 匹配
|
||||||
|
→ sem_init 信号量
|
||||||
|
→ pthread_create → mms_m_run_thread 启动工作线程
|
||||||
|
→ 返回 obj_fd
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 连接管理(`mms_m_do_comm`)
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_m_run_thread 主循环(每 MMS_M_THREAD_RUN_TM 毫秒):
|
||||||
|
→ mms_m_do_comm:
|
||||||
|
比较 con_state 与 old_con_state:
|
||||||
|
- 非连接状态: IedConnection_connectAsync 异步重连
|
||||||
|
- 新上线 (old != CONNECTED, new == CONNECTED):
|
||||||
|
mms_m_control_init() 重建 ControlObjectClient
|
||||||
|
回调 out_status_cb(ON_LINE)
|
||||||
|
- 新离线 (old == CONNECTED, new != CONNECTED):
|
||||||
|
running_init = false // RCB 需重新订阅
|
||||||
|
回调 out_status_cb(OFF_LINE)
|
||||||
|
old_con_state = con_state
|
||||||
|
→ if CONNECTED: mms_m_run(obj)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 运行流程(`mms_m_run`)
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_m_run(obj):
|
||||||
|
→ mms_m_run_init(obj): // 仅首次执行
|
||||||
|
mms_m_icd_init(obj):
|
||||||
|
- 遍历 LD → LN(通过 IedConnection_getLogicalDeviceList/Directory)
|
||||||
|
- 对每个 LN: mms_m_icd_dataset_init(获取所有 DataSet + Members)
|
||||||
|
- 对每个 LN: mms_m_icd_report_init(URCB + BRCB,按 rcb_numbers 过滤)
|
||||||
|
mms_m_ld_dataset_match_point_init(obj):
|
||||||
|
- 遍历所有 dataset member
|
||||||
|
- 解析 ref 得到 LD/LN/DO 名称
|
||||||
|
- 在 ldevs 树中查找匹配的 DO,建立 member.p_do_vec 指针关系
|
||||||
|
- 报告回调时通过此关系快速定位到具体信号点
|
||||||
|
mms_m_rcb_init(obj):
|
||||||
|
- 获取 RCB 值(Resv→TrgOps→RptEna→GI)
|
||||||
|
- 安装 mms_m_report_callback
|
||||||
|
- 设置 TrgOps = dchg | qchg | GI
|
||||||
|
→ mms_m_do_send(obj): 处理事件队列中的操作
|
||||||
|
→ mms_m_timer_running(obj): 检查四个定时器
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 All-Call 流程
|
||||||
|
|
||||||
|
```
|
||||||
|
T0 定时器到期 → mms_m_do_call_all:
|
||||||
|
→ 推送 _MMS_M_EVENT_ALL_CALL 事件
|
||||||
|
→ 重置 T0
|
||||||
|
|
||||||
|
mms_m_do_send → mms_m_send_call_all:
|
||||||
|
遍历 ST 和 MX 点表:
|
||||||
|
IedConnection_readObject(p_con, point.reference, fc)
|
||||||
|
→ mms_m_get_mmsValue: MMS 类型转换 (BOOLEAN→uint8, INTEGER→int32, UNSIGNED→uint32, FLOAT→float, BIT_STRING→quality)
|
||||||
|
→ 构造 out_val { name, desc, reference, time, value, quality, reason }
|
||||||
|
→ mms_m_put_value: 遍历 out_cb_lists 执行每个回调
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 遥控流程(SBO/Direct)
|
||||||
|
|
||||||
|
```
|
||||||
|
上层调用 mms_m_out_do_set_yk(event):
|
||||||
|
→ 按 app_fd 或 ied_name 定位 obj
|
||||||
|
→ mms_m_push_event
|
||||||
|
|
||||||
|
mms_m_do_send → mms_m_send_co:
|
||||||
|
switch ctrl_type:
|
||||||
|
_MMS_M_EVENT_CO_SELECT: ControlObjectClient_select
|
||||||
|
_MMS_M_EVENT_CO_DIRECT: ControlObjectClient_operate
|
||||||
|
_MMS_M_EVENT_CO_CANCEL: ControlObjectClient_cancel
|
||||||
|
→ mms_m_send_set_callback: 设置操作完成回调
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 AO/Param 读写
|
||||||
|
|
||||||
|
```
|
||||||
|
AO 读取:mms_m_send_read_ao
|
||||||
|
→ 按 saddr 或全表读取
|
||||||
|
→ IedConnection_readObject → 类型转换 → 回调推送
|
||||||
|
|
||||||
|
Param 写入:mms_m_send_param_write
|
||||||
|
→ 检测定值区是否切换(对比 current_zone)
|
||||||
|
→ 定值区变化:
|
||||||
|
读取 SG 信息 → select EditSG → 写入 SE 值 → confirm CnfEdit
|
||||||
|
→ 定值区不变:
|
||||||
|
直接写入 SE 值 → confirm
|
||||||
|
→ 操作完成回调
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.7 报告回调(`mms_m_report_callback`)
|
||||||
|
|
||||||
|
```
|
||||||
|
IedConnection 收到 report → mms_m_report_callback:
|
||||||
|
→ 遍历 dataset members
|
||||||
|
→ 检查 ReasonForInclusion(非 NOT_INCLUDED 才处理)
|
||||||
|
→ mms_m_get_MmsValue 解析数据值和时标
|
||||||
|
→ 通过 member.p_do_vec 找到所有绑定的信号点
|
||||||
|
→ 构造 out_val → 遍历 out_cb_lists 推送
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 对外 API
|
||||||
|
|
||||||
|
| API | 说明 |
|
||||||
|
|-----|------|
|
||||||
|
| `mms_m_out_init(p_cfg, dbg, timeout)` | 创建 IED 连接,返回 obj_fd |
|
||||||
|
| `mms_m_out_get_connect_status(fd, cb)` | 注册连接状态回调 |
|
||||||
|
| `mms_m_out_debug_print_swicth(fd, flag)` | 开关调试打印 |
|
||||||
|
| `mms_m_out_do_set_yk(event)` | 下达遥控命令(Select/Direct/Cancel) |
|
||||||
|
| `mms_m_out_read_ao_or_params(fd, type, saddr)` | 读取 AO/Param 值 |
|
||||||
|
| `mms_m_out_get_value(fd, cb)` | 注册数据回调 |
|
||||||
|
| `mms_m_out_bind_param_zone_signal(fd, saddr)` | 绑定定值区信号 |
|
||||||
|
| `mms_m_out_set_rcb_numbers(fd, numbers)` | 设置 RCB 订阅编号 |
|
||||||
|
| `mms_m_create_data_ptr(type)` | 创建类型对应的数据指针 |
|
||||||
|
| `mms_m_set_data_value(src, dst, type)` | 类型化数据赋值 |
|
||||||
|
| `mms_m_get_data_value_str(data, type, str)` | 类型化数据转字符串 |
|
||||||
|
| `mms_m_set_data_by_str(data, type, str)` | 字符串转类型化数据 |
|
||||||
|
| `mms_m_out_reason_str(reason)` | 原因码转字符串 |
|
||||||
|
| `mms_m_dbg_get(obj)` | 获取调试标志 |
|
||||||
|
| `mms_m_get_obj(fd)` | 按句柄获取对象指针 |
|
||||||
|
|
||||||
|
## 5. 线程安全
|
||||||
|
|
||||||
|
- 事件队列 `push/pop` 受 `sem_wait/sem_post` 保护
|
||||||
|
- `out_cb_lists` 在初始化时注册,运行时只读
|
||||||
|
- `con_state/old_con_state` 仅在主线程(`mms_m_run_thread`)修改
|
||||||
|
- libiec61850 的 report callback 在其他线程触发,通过 `mms_m_push_event` 转入主线程处理
|
||||||
|
|
||||||
|
## 6. 已知问题
|
||||||
|
|
||||||
|
1. **定时器倒计数精度**: 定时器基于循环次数×休眠时间,非高精度。取决于 `MMS_M_THREAD_RUN_TM` 大小
|
||||||
|
2. **事件队列溢出**: 容量 `EVENT_QUEUE_SIZE`,环形覆盖不报错
|
||||||
|
3. **RCB 编号过滤在 report_init 中执行**: 一旦初始化完成,变更 rcb_numbers 需重建连接
|
||||||
|
4. **离线时 running_init 重置**: 重连后整个 ICD 发现流程重新执行
|
||||||
|
|
@ -0,0 +1,283 @@
|
||||||
|
# libmms_s 模块分析
|
||||||
|
|
||||||
|
**日期**: 2026-06-12
|
||||||
|
**基于源码**: `src/protocol/libmms_s/`(16个文件,约7500行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 模块定位
|
||||||
|
|
||||||
|
`libmms_s` 是 RTU 的 IEC 61850 MMS 服务端库,位于协议层。它解析 ICD(IED Capability Description)XML 文件构建完整的数据模型,创建 `IedServer` 对外提供 MMS 服务,实现控制执行、定值组管理、文件服务等功能。
|
||||||
|
|
||||||
|
**与上下层关系**:
|
||||||
|
- **下层**: 依赖 libiec61850 v1.5.x 的 IedServer/IedModel API
|
||||||
|
- **上层**: 被 `iec61850s`(系统层封装)调用初始化,通过回调机制将控制/定值变更通知上层
|
||||||
|
- **数据方向**: 接收远方客户端的 MMS 读写请求,通过回调将控制指令传递给 `iec61850s` → `datacenter`
|
||||||
|
|
||||||
|
## 2. ICD 解析流程(`mms_s_icd.cpp`)
|
||||||
|
|
||||||
|
ICD 文件是 SCL(Substation Configuration Language)格式的 XML。解析流程自顶向下:
|
||||||
|
|
||||||
|
```
|
||||||
|
XML 加载 (tinyxml2)
|
||||||
|
→ SCL 根元素
|
||||||
|
→ DataTypeTemplates 区域:
|
||||||
|
- 遍历 LNodeType(逻辑节点类型)
|
||||||
|
- DO → 引用 DOType
|
||||||
|
- 遍历 DOType(数据对象类型)
|
||||||
|
- SDO → 递归引用 DOType
|
||||||
|
- DA → 属性定义(bType, fc, dchg, qchg)
|
||||||
|
- fc/dchg/qchg 从父级 BDA 向下传播
|
||||||
|
- 遍历 DAType(数据属性类型)
|
||||||
|
- BDA → fc/dchg/qchg 从 BDA 向下传播到子 BDA
|
||||||
|
- 遍历 EnumType(枚举类型)
|
||||||
|
- EnumVal → 枚举值映射
|
||||||
|
→ IED 区域:
|
||||||
|
- AccessPoint → Server
|
||||||
|
- Server → LDevice
|
||||||
|
- LN0 (LLN0):
|
||||||
|
- DOI/SDI/DAI → 运行时值覆盖
|
||||||
|
- DataSet → FCDA 成员
|
||||||
|
- ReportControl → TrgOps, OptFields
|
||||||
|
- SettingControl → numOfSGs, actSG
|
||||||
|
- LN(逻辑节点)→ DOI/SDI/DAI
|
||||||
|
→ 校验:
|
||||||
|
- LN/LN0 的 lnType 在 DataTypeTemplates 中存在
|
||||||
|
- DataSet 的 FCDA 引用的 DO/SDO/DA 在模型中存在
|
||||||
|
```
|
||||||
|
|
||||||
|
核心数据结构:
|
||||||
|
- `stru_all_DO`: 包含 `map_all_sdo`(子 SDO)和 `map_all_da`(子 DA),每个 DA 记录 `sAddr`, `bType`, `fc`
|
||||||
|
- `stru_icd`: 顶层结构,包含 `ied` 对象和 `map_lnode_type`, `map_do_type`, `map_da_type`, `map_enum_type`
|
||||||
|
- `stru_icd::ied`: 包含 `map_ldevice`(逻辑设备),`vec_all_DO`(所有 DO),`vec_control_do`(控制 DO),`vec_saddr`(信号地址列表),`map_saddr_point`(sAddr→模型节点映射)
|
||||||
|
|
||||||
|
## 3. 动态模型创建(`mms_s_model.cpp`)
|
||||||
|
|
||||||
|
将 ICD 解析的 SCL 结构映射为 libiec61850 的 `IedModel` 对象树。
|
||||||
|
|
||||||
|
### 3.1 类型映射
|
||||||
|
|
||||||
|
| ICD bType 字符串 | libiec61850 DataAttributeType |
|
||||||
|
|-----------------|------------------------------|
|
||||||
|
| `BOOLEAN` | IEC61850_BOOLEAN |
|
||||||
|
| `INT8` / `Struct` | IEC61850_INT8 |
|
||||||
|
| `INT16` | IEC61850_INT16 |
|
||||||
|
| `INT32` | IEC61850_INT32 |
|
||||||
|
| `INT64` | IEC61850_INT64 |
|
||||||
|
| `INT128` | IEC61850_INT128 |
|
||||||
|
| `INT8U` | IEC61850_INT8U |
|
||||||
|
| `INT16U` | IEC61850_INT16U |
|
||||||
|
| `INT24U` | IEC61850_INT24U |
|
||||||
|
| `INT32U` | IEC61850_INT32U |
|
||||||
|
| `FLOAT32` | IEC61850_FLOAT32 |
|
||||||
|
| `FLOAT64` | IEC61850_FLOAT64 |
|
||||||
|
| `VisString64` 等 | IEC61850_VISIBLE_STRING64 |
|
||||||
|
| `Unicode255` 等 | IEC61850_UNICODE_STRING255 |
|
||||||
|
| `Timestamp` | IEC61850_TIMESTAMP |
|
||||||
|
| `Dbpos` | IEC61850_DBPOS |
|
||||||
|
| `Quality` | IEC61850_QUALITY |
|
||||||
|
| `Check` | IEC61850_CHECK |
|
||||||
|
| `Tcmd` | IEC61850_TCMD |
|
||||||
|
| `OptFlds` | IEC61850_OPTFLDS |
|
||||||
|
| `TrgOps` | IEC61850_TRGOPS |
|
||||||
|
| `EntryID` | IEC61850_ENTRYID |
|
||||||
|
| `EntryTime` | IEC61850_ENTRYTIME |
|
||||||
|
| `ObjRef` | IEC61850_OBJREF |
|
||||||
|
| `PhyComAddr` | IEC61850_PHYCOMADDR |
|
||||||
|
| `Octet64` | IEC61850_OCTET_STRING_64 |
|
||||||
|
| `Currency` | IEC61850_CURRENCY |
|
||||||
|
| `AnalogueValue` | IEC61850_ANALOGUE_VALUE |
|
||||||
|
| `Unit` | IEC61850_UNIT |
|
||||||
|
| `Vector` | IEC61850_VECTOR |
|
||||||
|
| `ValWithTrans` | IEC61850_VAL_WITH_TRANS |
|
||||||
|
| `CUG` | IEC61850_CUG |
|
||||||
|
|
||||||
|
### 3.2 创建流程
|
||||||
|
|
||||||
|
```
|
||||||
|
model_init(icd):
|
||||||
|
IedModel_create(ied_name) // 创建根模型
|
||||||
|
→ model_ldevice_init(model, icd):
|
||||||
|
遍历每个 LDevice:
|
||||||
|
LogicalDevice_create(name, model)
|
||||||
|
→ model_init_lnodes(ld, icd):
|
||||||
|
遍历每个 LN/LN0:
|
||||||
|
LogicalNode_create(name, ld)
|
||||||
|
→ model_init_do(ln, icd):
|
||||||
|
遍历 DO/SDO/DA 递归创建 DataObject 和 DataAttribute
|
||||||
|
记录 sAddr → ModelNode 映射
|
||||||
|
→ 对 LN0:
|
||||||
|
model_init_datasets → 创建 DataSet + FCDA
|
||||||
|
model_init_rcb → 创建 URCB/BRCB + TrgOps/OptFields
|
||||||
|
model_init_sgcb → 创建 SGCB + SG/SE 镜像
|
||||||
|
→ model_sync_ied_default_values: ICD 默认值同步到模型节点
|
||||||
|
→ model_search_control_DataObjects: 搜索所有含 ctlModel 的 DO
|
||||||
|
→ 设置 model.initializer = mms_s_values_init
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 SG/SE 定值组镜像
|
||||||
|
|
||||||
|
每个 LN0 的 SGCB 下创建 SG(Setting Group)和 SE(Setting Group Edit),SE 是 SG 的镜像:
|
||||||
|
- SG 的 sAddr = 原始 sAddr(如 `PROT/LLN0$SG$sg1$StrVal`)
|
||||||
|
- SE 的 sAddr = `SE_` 前缀版本
|
||||||
|
- `mms_s_param.cpp` 只操作 SE,协议栈负责 SG↔SE 同步
|
||||||
|
|
||||||
|
### 3.4 sAddr 映射
|
||||||
|
|
||||||
|
`sAddr` 是信号地址,IEC 61850 标准字段。解析时保存 `sAddr → (fc, node)` 映射到 `map_saddr_point`,供上层 `iec61850s` 按 sAddr 注册信号回调。
|
||||||
|
|
||||||
|
## 4. MMS 服务器启动(`mms_s.cpp`)
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_s_init(icd_path, stack_size):
|
||||||
|
→ 解析 ICD → 创建 IedModel → model_init
|
||||||
|
→ IedServerConfig 配置:
|
||||||
|
- 设置 selectLlN0ForUnmatchedCommands = true
|
||||||
|
- 设置 maxMmsConnections = default
|
||||||
|
- 设置 reportBufferSize = 8
|
||||||
|
→ IedServer_create(&model) // 创建服务器
|
||||||
|
→ mms_s_control_init(server) // 安装控制回调
|
||||||
|
→ mms_s_param_init(server) // 安装定值组回调
|
||||||
|
→ mms_s_file_init(server) // 安装文件服务
|
||||||
|
→ mms_s_setting_init(server) // 安装 SP 定值回调
|
||||||
|
→ IedServer_start(server, port)
|
||||||
|
→ pthread_create → server_run_thread:
|
||||||
|
while(1) IedServer_processEvent(server, timeout)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 控制服务(`mms_s_control.cpp`)
|
||||||
|
|
||||||
|
### 5.1 控制模型
|
||||||
|
|
||||||
|
支持三种控制模型(由 ICD 中 DO 的 `ctlModel` 决定):
|
||||||
|
- `status-only` (0): 仅状态,不参与控制
|
||||||
|
- `direct-with-normal-security` (1): 直接执行
|
||||||
|
- `sbo-with-normal-security` (2): 选择-执行(Select Before Operate)
|
||||||
|
|
||||||
|
### 5.2 回调链
|
||||||
|
|
||||||
|
```
|
||||||
|
远方客户端发送控制命令
|
||||||
|
→ IedServer 触发 control_handler(control, ctlVal, oper, interlockCheck):
|
||||||
|
- oper == OPERATE: 执行操作
|
||||||
|
stVal 从 ctlVal 中提取
|
||||||
|
若是 SBO: control.oper() → 更新 t, origin, ctlNum
|
||||||
|
若是 Direct: 直接更新 stVal + t
|
||||||
|
调用 p_control_callback(state) → 通知 iec61850s
|
||||||
|
- oper == CANCEL: 取消操作
|
||||||
|
→ 同时触发 check_handler(control, ctlVal, oper, interlockCheck):
|
||||||
|
- interlockCheck == true: 检查连锁条件
|
||||||
|
- 返回 false 阻止操作执行
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 控制注册
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_s_control_register(controls, count, callback):
|
||||||
|
遍历所有控制 DO(由 model_search_control_DataObjects 在 ICD 解析阶段收集)
|
||||||
|
→ 通过 sAddr 在 map_saddr_point 中查找对应的 ModelNode
|
||||||
|
→ IedServer_setControlHandlerEx → 安装 control_handler + check_handler
|
||||||
|
→ 保存 callback 到全局列表
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 定值组管理(`mms_s_param.cpp`)
|
||||||
|
|
||||||
|
### 6.1 SG/SE 编辑流程
|
||||||
|
|
||||||
|
```
|
||||||
|
远方客户端编辑定值:
|
||||||
|
→ SGCB.SelectEditSG(zone): 切换到编辑区
|
||||||
|
→ 加载当前激活 SG 的值到 SE
|
||||||
|
→ 设置 selectEditSG_callback → 通知上层定值区切换
|
||||||
|
→ SE 各 DA 写入新值:
|
||||||
|
→ writeAccessHandler: 校验 min/max/step
|
||||||
|
→ 通过才允许写入
|
||||||
|
→ SGCB.ConfirmEditSG(zone):
|
||||||
|
→ confirmEditSG_callback:
|
||||||
|
读取 SE 所有值 → 校验范围/步长
|
||||||
|
→ 调用 p_param_callback(zone, data) → 通知 iec61850s
|
||||||
|
返回 true 确认 / false 拒绝
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 值校验
|
||||||
|
|
||||||
|
每种数据类型对应独立的校验函数:
|
||||||
|
- BOOLEAN: 直接通过
|
||||||
|
- INT8/16/32: `minVal/maxVal/stepSize` 从 min/max/stepSize DA 读取
|
||||||
|
- INT8U/16U/32U: 同上(无符号)
|
||||||
|
- FLOAT32/64: 校验 + epsilon 容差(`fabs(val - min) > 0.000001`)
|
||||||
|
- VisString/UnicodeString: 长度校验
|
||||||
|
- Enum: 枚举值范围校验
|
||||||
|
|
||||||
|
### 6.3 定值区信号绑定
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_s_bind_param_zone_signal(saddr, callback):
|
||||||
|
→ 在 LN0 的 SGCB 中查找 ActSG DA
|
||||||
|
→ IedServer_handleWriteAccess: 安装 writeAccessHandler
|
||||||
|
→ 当 ActSG 被写入时:
|
||||||
|
读取新 actSG 值
|
||||||
|
调用 callback(saddr, new_act_sg)
|
||||||
|
→ iec61850s 通过 datacenter 更新相关信号
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 固定定值管理(`mms_s_setting.cpp`)
|
||||||
|
|
||||||
|
SP(Set Point)类 DA(fc=SP)的读写管理:
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_s_setting_register(settings, count, callback):
|
||||||
|
→ 对每个 SP DA:
|
||||||
|
- IedServer_setWriteAccessPolicy(deny) // 全局拒绝 SP 写入
|
||||||
|
- IedServer_handleWriteAccess 安装 writeAccessHandler:
|
||||||
|
→ 校验 min/max/step
|
||||||
|
→ 允许后调用 p_setting_callback(saddr, data)
|
||||||
|
- sgcb 定值区变化时同步 SP 值
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. 文件服务(`mms_s_file.cpp`)
|
||||||
|
|
||||||
|
```
|
||||||
|
file_init(server):
|
||||||
|
→ IedServer_setFilestoreBasepath(path)
|
||||||
|
→ IedServer_setFileServiceHandler:
|
||||||
|
- rename: 一律拒绝
|
||||||
|
- read/write/delete: 允许
|
||||||
|
- IEDSERVER.BIN 文件保护: 拒绝任何操作
|
||||||
|
→ IedServer_installConnectionHandler(connectionHandler):
|
||||||
|
记录连接建立/断开日志
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. DA 值初始化(`mms_s_value.cpp`)
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_s_values_init(param, model):
|
||||||
|
// 作为 model->initializer 在 IedServer_create 时被调用
|
||||||
|
→ 遍历 LD → LN → DO → SDO → DA 树
|
||||||
|
→ 按 bType 初始化 MmsValue:
|
||||||
|
BOOLEAN → MmsValue_newBoolean(false)
|
||||||
|
INT32 → MmsValue_newInteger(0)
|
||||||
|
INT32U → MmsValue_newUnsigned(0)
|
||||||
|
FLOAT32 → MmsValue_newFloat(0.0)
|
||||||
|
VisString* → MmsValue_newVisibleString("")
|
||||||
|
Unicode* → MmsValue_newUnicodeString("")
|
||||||
|
Timestamp → MmsValue_newUtcTime(0)
|
||||||
|
Dbpos → MmsValue_newDbpos()
|
||||||
|
Enum → 按 OrderedEnum 表现(有 ord → INTEGER, 无 → 第一枚举值)
|
||||||
|
→ MmsValue_setDeletable(value) // 标记为模型可管理生命周期
|
||||||
|
|
||||||
|
mms_s_value_update(saddr, value_str):
|
||||||
|
→ 按 bType 从字符串解析值 → 更新 MmsValue
|
||||||
|
→ IedServer_updateAttributeValue(server, node, value) // 触发 RCB 报告
|
||||||
|
→ 自动更新 t(时标)和 q(品质)字段
|
||||||
|
```
|
||||||
|
|
||||||
|
## 10. 关键日志宏
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#define MMS_S_LOG_E(fmt, ...) LOG_E("[MMS_S][%s:%d]" fmt, __FILENAME__, __LINE__, ##__VA_ARGS__)
|
||||||
|
#define MMS_S_LOG_W(fmt, ...) LOG_W("[MMS_S][%s:%d]" fmt, __FILENAME__, __LINE__, ##__VA_ARGS__)
|
||||||
|
#define MMS_S_LOG_I(fmt, ...) LOG_I("[MMS_S][%s:%d]" fmt, __FILENAME__, __LINE__, ##__VA_ARGS__)
|
||||||
|
```
|
||||||
|
|
||||||
|
带颜色、时间戳、文件名和行号。
|
||||||
|
|
@ -0,0 +1,201 @@
|
||||||
|
# libweb_server 模块分析
|
||||||
|
|
||||||
|
**日期**: 2026-06-12
|
||||||
|
**基于源码**: `src/system/libweb_server/`(5个文件,约1500行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 模块定位
|
||||||
|
|
||||||
|
`libweb_server` 是 RTU 的嵌入式 Web 服务器,作为 `app_web_server` 线程(9个应用线程之第7号)运行。基于 Mongoose v7.x,提供:
|
||||||
|
|
||||||
|
- HTTP 静态文件服务(内嵌前端工程)
|
||||||
|
- WebSocket 实时数据通道(JSON 格式)
|
||||||
|
- 多客户端并发支持(per-connection session)
|
||||||
|
- 信号增量推送(仅变更时发送)
|
||||||
|
|
||||||
|
### 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/system/libweb_server/
|
||||||
|
├── inc/
|
||||||
|
│ ├── web_server.h # 模块头文件
|
||||||
|
│ └── ws_method.h # WebSocket 消息处理接口
|
||||||
|
└── src/
|
||||||
|
├── web_server.cpp # HTTP/WS 服务器 + Mongoose 事件循环
|
||||||
|
├── ws_method.cpp # WebSocket 命令解析 + JSON 推送
|
||||||
|
└── packed_fs.c # 前端文件嵌入(自动生成)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. 架构设计
|
||||||
|
|
||||||
|
### 2.1 Mongoose 集成
|
||||||
|
|
||||||
|
Mongoose 是单线程事件驱动的网络库。RTU 的 Web 服务器以独立线程运行,集成方式:
|
||||||
|
|
||||||
|
```
|
||||||
|
app_web_server_init1:
|
||||||
|
→ mg_mgr_init(&mgr) // 初始化事件管理器
|
||||||
|
→ 加载 packed_fs(前端文件) // 如果定义了 USE_PACKED_FS
|
||||||
|
|
||||||
|
app_web_server_init2:
|
||||||
|
→ mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL)
|
||||||
|
→ 启动事件循环
|
||||||
|
|
||||||
|
app_web_server 线程:
|
||||||
|
while(1):
|
||||||
|
task_event_recv(EV_TIMER1 | EV_TIMER2 | EV_TIMER3)
|
||||||
|
EV_TIMER1 (10ms): mg_mgr_poll(&mgr, 0) // 非阻塞轮询
|
||||||
|
EV_TIMER2 (100ms): ws_task() // WebSocket 推送
|
||||||
|
EV_TIMER3 (1000ms): 空闲
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 事件处理函数 `fn`
|
||||||
|
|
||||||
|
统一的 HTTP + WebSocket 事件处理:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static void fn(struct mg_connection *c, int ev, void *ev_data):
|
||||||
|
switch(ev):
|
||||||
|
MG_EV_HTTP_MSG:
|
||||||
|
→ mg_http_get_header(hm, "Sec-WebSocket-Key") 存在?
|
||||||
|
YES → mg_ws_upgrade(c, hm, NULL) // WebSocket 握手
|
||||||
|
NO → mg_http_serve_dir / mg_http_serve_packed // 静态文件
|
||||||
|
|
||||||
|
MG_EV_WS_MSG:
|
||||||
|
→ ws_recv(c, wm->data.buf, wm->data.len) // WebSocket 消息
|
||||||
|
|
||||||
|
MG_EV_CLOSE:
|
||||||
|
→ ws_session_destroy(c) // 释放 per-connection 资源
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 多连接管理
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
LOCAL std::vector<struct mg_connection *> g_ws_conns; // 所有 WS 连接
|
||||||
|
LOCAL pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
```
|
||||||
|
|
||||||
|
- 连接建立时将 `mg_connection*` 加入 `g_ws_conns`
|
||||||
|
- 断开时从 `g_ws_conns` 移除并调用 `ws_session_destroy`
|
||||||
|
- 所有对 `g_ws_conns` 的访问受互斥锁保护
|
||||||
|
|
||||||
|
## 3. 前端嵌入式部署(packed_fs.c)
|
||||||
|
|
||||||
|
`packed_fs.c` 由构建工具自动生成,将前端文件(HTML/CSS/JS)以 `unsigned char` 数组嵌入:
|
||||||
|
|
||||||
|
```c
|
||||||
|
const struct mg_mem_file mg_packed_files[] = {
|
||||||
|
{"/css/style.css", v3, sizeof(v3) - 1, 1781072856},
|
||||||
|
{"/index.html", v4, sizeof(v4) - 1, 1781073518},
|
||||||
|
{"/js/app.js", v5, sizeof(v5) - 1, 1781073909},
|
||||||
|
{"/js/monitor.js", v6, sizeof(v6) - 1, 1781072788},
|
||||||
|
{"/js/pages.js", v7, sizeof(v7) - 1, 1781073958},
|
||||||
|
{"/js/ws-client.js", v8, sizeof(v8) - 1, 1781071573},
|
||||||
|
{NULL, NULL, 0, 0}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Mongoose 通过 `mg_http_serve_packed` 直接返回内嵌文件,无需外部 `web_root` 目录。
|
||||||
|
|
||||||
|
## 4. WebSocket 消息处理(ws_method.cpp)
|
||||||
|
|
||||||
|
### 4.1 消息格式
|
||||||
|
|
||||||
|
客户端发送 JSON 命令:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"cmd": "subscribe", // 命令类型
|
||||||
|
"type": "out,ao,param", // 订阅的信号类型
|
||||||
|
"signals": ["saddr1", "saddr2"], // 指定信号(空=全部)
|
||||||
|
"conn_id": 12345 // 连接标识
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 命令分发
|
||||||
|
|
||||||
|
`ws_recv` 解析 JSON → 提取 `cmd` 字段 → 路由到处理函数:
|
||||||
|
|
||||||
|
| cmd | 功能 | 处理函数 |
|
||||||
|
|-----|------|---------|
|
||||||
|
| `subscribe` | 订阅信号推送 | `ws_handle_subscribe` |
|
||||||
|
| `unsubscribe` | 取消订阅 | `ws_handle_unsubscribe` |
|
||||||
|
| `read_all` | 读取全部当前值 | `ws_handle_read_all` |
|
||||||
|
| `set_value` | 设置 AO/Param 值 | `ws_handle_set_value` |
|
||||||
|
| `yk_control` | 遥控操作 | `ws_handle_yk_control` |
|
||||||
|
|
||||||
|
### 4.3 信号订阅与增量推送
|
||||||
|
|
||||||
|
每个连接维护独立的 per-connection session:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct ws_session {
|
||||||
|
unsigned long conn_id;
|
||||||
|
std::set<std::string> subscribed_signals; // 已订阅的信号 saddr 集合
|
||||||
|
std::set<std::string> subscribed_types; // 已订阅的信号类型(out/ao/param)
|
||||||
|
// ...
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
订阅流程:
|
||||||
|
```
|
||||||
|
客户端发送 {cmd: "subscribe", type: "out,ao", signals: ["sig1","sig2"]}
|
||||||
|
→ ws_handle_subscribe(c, json):
|
||||||
|
获取或创建 session
|
||||||
|
将信号 saddr 加入 subscribed_signals
|
||||||
|
将类型加入 subscribed_types
|
||||||
|
回复 {status: "ok"}
|
||||||
|
```
|
||||||
|
|
||||||
|
推送流程 (`ws_task`, 每 100ms 执行):
|
||||||
|
```
|
||||||
|
ws_task():
|
||||||
|
遍历 g_ws_conns:
|
||||||
|
对每个连接:
|
||||||
|
收集该连接订阅的信号中发生了变更的
|
||||||
|
构建 JSON: {signals: [{saddr, value, time, quality}, ...]}
|
||||||
|
通过 mg_ws_send 发送
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 数据广播
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void ws_send_all(const char *p_tx, uint16_t tx_len):
|
||||||
|
pthread_mutex_lock(&g_ws_conns_mutex)
|
||||||
|
for each c in g_ws_conns:
|
||||||
|
mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT)
|
||||||
|
pthread_mutex_unlock(&g_ws_conns_mutex)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 单连接发送
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void ws_send_one(unsigned long conn_id, const char *p_tx, uint16_t tx_len):
|
||||||
|
在 g_ws_conns 中查找 mg_connection.conn_id == conn_id
|
||||||
|
找到 → mg_ws_send
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. HTTP API
|
||||||
|
|
||||||
|
除了 WebSocket,还提供 RESTful HTTP 接口(通过 `MG_EV_HTTP_MSG` 处理):
|
||||||
|
|
||||||
|
| 方法 | 路径 | 功能 |
|
||||||
|
|------|------|------|
|
||||||
|
| GET | `/` | 静态首页 |
|
||||||
|
| GET | `/api/status` | 设备状态 |
|
||||||
|
| GET | `/api/signals` | 信号列表快照 |
|
||||||
|
| POST | `/api/control` | 控制命令 |
|
||||||
|
|
||||||
|
## 6. 线程安全
|
||||||
|
|
||||||
|
- Mongoose 事件循环在 `app_web_server` 线程中执行
|
||||||
|
- `ws_task()` 在 `EV_TIMER2`(100ms)中执行,与事件循环同线程
|
||||||
|
- `g_ws_conns` 访问受 `g_ws_conns_mutex` 保护
|
||||||
|
- 通过任务事件机制与 datacenter 交互(信号值读取通过 `dc_get_signal_val` 等线程安全 API)
|
||||||
|
|
||||||
|
## 7. 已知问题
|
||||||
|
|
||||||
|
1. **mg_mgr_poll 非阻塞模式**: `mg_mgr_poll(&mgr, 0)` 零超时,高频率轮询可能消耗 CPU。Mongoose 连接数少时不明显
|
||||||
|
2. **packed_fs 更新**: 修改前端后需重新生成 `packed_fs.c` 并重新编译
|
||||||
|
3. **广播效率**: `ws_send_all` 逐连接发送,大量连接时效率低。当前场景(嵌入式 RTU)连接数通常较少
|
||||||
|
|
@ -0,0 +1,267 @@
|
||||||
|
# WebSocket 服务端分析
|
||||||
|
|
||||||
|
**日期**: 2026-06-12
|
||||||
|
**基于源码**: `src/protocol/libmongoose/src/mongoose.c`(ws.c 部分约 300 行)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 协议概述
|
||||||
|
|
||||||
|
WebSocket 是 RFC 6455 定义的全双工通信协议,通过 HTTP Upgrade 从 HTTP 升级到持久 TCP 连接。RTU 使用 Mongoose 7.x 内置的 WebSocket 实现提供实时数据推送。
|
||||||
|
|
||||||
|
### 1.1 帧操作码
|
||||||
|
|
||||||
|
| 操作码 | 值 | 说明 |
|
||||||
|
|--------|---|------|
|
||||||
|
| CONTINUE | 0x0 | 分片帧的后续帧 |
|
||||||
|
| TEXT | 0x1 | UTF-8 文本帧 |
|
||||||
|
| BINARY | 0x2 | 二进制帧 |
|
||||||
|
| CLOSE | 0x8 | 关闭连接 |
|
||||||
|
| PING | 0x9 | 心跳请求 |
|
||||||
|
| PONG | 0xA | 心跳响应 |
|
||||||
|
|
||||||
|
## 2. Mongoose WebSocket 实现
|
||||||
|
|
||||||
|
### 2.1 核心结构
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 消息结构(零拷贝,引用 c->recv 缓冲区)
|
||||||
|
struct mg_ws_message {
|
||||||
|
struct mg_str data; // 消息数据
|
||||||
|
uint8_t flags; // 高4位=FIN标志,低4位=操作码
|
||||||
|
};
|
||||||
|
|
||||||
|
// 内部帧解析结构
|
||||||
|
struct ws_msg {
|
||||||
|
uint8_t flags;
|
||||||
|
size_t header_len; // 帧头长度(含掩码key)
|
||||||
|
size_t data_len; // 数据长度
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 帧格式(RFC 6455)
|
||||||
|
|
||||||
|
```
|
||||||
|
字节0: [FIN(1)] [RSV(3)] [OPCODE(4)]
|
||||||
|
字节1: [MASK(1)] [PAYLOAD_LEN(7)]
|
||||||
|
字节2-3: [扩展长度16位] (如果 PAYLOAD_LEN == 126)
|
||||||
|
字节2-9: [扩展长度64位] (如果 PAYLOAD_LEN == 127)
|
||||||
|
字节n: [掩码Key 4字节] (如果 MASK == 1)
|
||||||
|
字节n+4: [Payload Data]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 帧头构建
|
||||||
|
|
||||||
|
```c
|
||||||
|
static size_t mkhdr(size_t len, int op, bool is_client, uint8_t *buf):
|
||||||
|
buf[0] = op | 128; // FIN=1, Opcode=op
|
||||||
|
|
||||||
|
if (len < 126):
|
||||||
|
buf[1] = len, n = 2 // 7位直接编码
|
||||||
|
else if (len < 65536):
|
||||||
|
buf[1] = 126, n = 4 // 16位扩展长度(大端)
|
||||||
|
else:
|
||||||
|
buf[1] = 127, n = 10 // 64位扩展长度(大端)
|
||||||
|
|
||||||
|
if (is_client):
|
||||||
|
buf[1] |= 0x80 // 设置MASK位
|
||||||
|
mg_random(&buf[n], 4) // 随机掩码key
|
||||||
|
n += 4
|
||||||
|
|
||||||
|
return n
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键**: 服务端发送的帧不设 MASK 位。只有客户端发往服务端的帧才需要掩码(防缓存投毒攻击)。
|
||||||
|
|
||||||
|
### 2.4 帧解析
|
||||||
|
|
||||||
|
```c
|
||||||
|
static size_t ws_process(uint8_t *buf, size_t len, struct ws_msg *msg):
|
||||||
|
→ 解析字节0: msg->flags
|
||||||
|
→ 解析字节1: payload_len (低7位) + mask_len (MASK位→4或0)
|
||||||
|
→ 根据 payload_len 读取扩展长度
|
||||||
|
→ 安全检查: data_len 不能超过 1GB
|
||||||
|
→ 如果有掩码: payload[i] ^= masking_key[i % 4] // XOR解码
|
||||||
|
→ 返回 header_len + data_len
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 握手流程
|
||||||
|
|
||||||
|
### 3.1 HTTP Upgrade
|
||||||
|
|
||||||
|
```
|
||||||
|
客户端 → 服务端:
|
||||||
|
GET /ws HTTP/1.1
|
||||||
|
Upgrade: websocket
|
||||||
|
Connection: Upgrade
|
||||||
|
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
|
||||||
|
Sec-WebSocket-Version: 13
|
||||||
|
|
||||||
|
服务端 → 客户端:
|
||||||
|
HTTP/1.1 101 Switching Protocols
|
||||||
|
Upgrade: websocket
|
||||||
|
Connection: Upgrade
|
||||||
|
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 Accept 值计算
|
||||||
|
|
||||||
|
```
|
||||||
|
1. SHA1( Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" )
|
||||||
|
2. Base64 编码 SHA1 结果
|
||||||
|
3. 魔数是 RFC 6455 §4.2.2 固定值,防跨协议攻击
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 Mongoose API
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 服务端升级
|
||||||
|
void mg_ws_upgrade(struct mg_connection *c, struct mg_http_message *hm, const char *fmt, ...);
|
||||||
|
// 内部: 计算Accept → 发送101 → 切换c->pfn = mg_ws_cb → c->is_websocket = 1
|
||||||
|
|
||||||
|
// 客户端连接
|
||||||
|
struct mg_connection *mg_ws_connect(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data, const char *fmt, ...);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 协议处理器 `mg_ws_cb`
|
||||||
|
|
||||||
|
WebSocket 连接的核心处理器,在握手完成后注册为 `c->pfn`:
|
||||||
|
|
||||||
|
```
|
||||||
|
mg_ws_cb (MG_EV_READ 处理):
|
||||||
|
|
||||||
|
解析帧 → opcode 分类:
|
||||||
|
TEXT/BINARY + FIN=1 (完整帧):
|
||||||
|
→ 触发 MG_EV_WS_MSG(用户在此接收消息)
|
||||||
|
|
||||||
|
TEXT/BINARY + FIN=0 (分片开始):
|
||||||
|
→ 保留flags,累积数据,等待后续帧
|
||||||
|
|
||||||
|
CONTINUE:
|
||||||
|
→ 剥离帧头追加到累积缓冲区
|
||||||
|
→ FIN=1: 触发 MG_EV_WS_MSG,从缓冲区首字节恢复原始opcode
|
||||||
|
|
||||||
|
CLOSE (0x8):
|
||||||
|
→ 触发 MG_EV_WS_CTL
|
||||||
|
→ 回显CLOSE帧给对端
|
||||||
|
→ c->is_draining = 1(优雅关闭)
|
||||||
|
|
||||||
|
PING (0x9):
|
||||||
|
→ 自动回复PONG
|
||||||
|
→ 触发 MG_EV_WS_CTL
|
||||||
|
|
||||||
|
PONG (0xA):
|
||||||
|
→ 触发 MG_EV_WS_CTL(用户可检测心跳响应)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.1 分片帧处理
|
||||||
|
|
||||||
|
```
|
||||||
|
客户端发送 3 条分片:
|
||||||
|
Frame1: FIN=0, op=TEXT, data="Hello "
|
||||||
|
Frame2: FIN=0, op=CONTINUE, data="World"
|
||||||
|
Frame3: FIN=1, op=CONTINUE, data="!"
|
||||||
|
|
||||||
|
内部处理:
|
||||||
|
Frame1: 在c->recv中保存flags(0x01) + 数据 → "\x01Hello "
|
||||||
|
Frame2: 剥离帧头追加 → "\x01Hello World"
|
||||||
|
Frame3: 剥离帧头追加 → "\x01Hello World!" → FIN=1触发
|
||||||
|
→ m.flags = c->recv.buf[0] (TEXT)
|
||||||
|
→ m.data = "Hello World!" (跳过标记字节)
|
||||||
|
→ 删除已处理数据
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 发送函数
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 发送一条完整消息(构建帧头+掩码+发送)
|
||||||
|
size_t mg_ws_send(struct mg_connection *c, const void *buf, size_t len, int op);
|
||||||
|
|
||||||
|
// 格式化发送(mg_vxprintf → mg_ws_wrap)
|
||||||
|
size_t mg_ws_printf(struct mg_connection *c, int op, const char *fmt, ...);
|
||||||
|
|
||||||
|
// 为已在c->send中的数据添加WS帧头(内部)
|
||||||
|
size_t mg_ws_wrap(struct mg_connection *c, size_t len, int op);
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 服务端最佳实践
|
||||||
|
|
||||||
|
### 6.1 标准事件处理模板
|
||||||
|
|
||||||
|
```c
|
||||||
|
static void fn(struct mg_connection *c, int ev, void *ev_data) {
|
||||||
|
// WebSocket 握手
|
||||||
|
if (ev == MG_EV_HTTP_MSG) {
|
||||||
|
struct mg_http_message *hm = (struct mg_http_message *) ev_data;
|
||||||
|
if (mg_http_get_header(hm, "Sec-WebSocket-Key")) {
|
||||||
|
mg_ws_upgrade(c, hm, NULL);
|
||||||
|
} else {
|
||||||
|
mg_http_reply(c, 200, "", "Use WebSocket\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接建立
|
||||||
|
if (ev == MG_EV_WS_OPEN) {
|
||||||
|
// 注入 per-connection 资源
|
||||||
|
}
|
||||||
|
|
||||||
|
// 接收消息
|
||||||
|
if (ev == MG_EV_WS_MSG) {
|
||||||
|
struct mg_ws_message *wm = (struct mg_ws_message *) ev_data;
|
||||||
|
// 处理文本/二进制消息
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接关闭
|
||||||
|
if (ev == MG_EV_CLOSE) {
|
||||||
|
// 释放 per-connection 资源
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 广播
|
||||||
|
|
||||||
|
```c
|
||||||
|
void broadcast(struct mg_mgr *mgr, const char *msg, size_t len):
|
||||||
|
for each c in mgr->conns:
|
||||||
|
if c->is_websocket:
|
||||||
|
mg_ws_send(c, msg, len, WEBSOCKET_OP_TEXT)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 心跳检测
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 定时发送 PING(通过 mg_timer_add)
|
||||||
|
mg_ws_send(c, NULL, 0, WEBSOCKET_OP_PING);
|
||||||
|
|
||||||
|
// 在 MG_EV_WS_CTL 中检测 PONG 确认存活
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. 服务端 vs 客户端差异
|
||||||
|
|
||||||
|
| 特性 | 服务端 | 客户端 |
|
||||||
|
|------|--------|--------|
|
||||||
|
| 帧掩码 | 不掩码 | 必须掩码(4字节随机key + XOR) |
|
||||||
|
| 握手 | 接收Key,计算Accept | 生成随机Key,验证Accept |
|
||||||
|
| API | `mg_ws_upgrade()` | `mg_ws_connect()` |
|
||||||
|
| `is_client` | `false` | `true` |
|
||||||
|
|
||||||
|
## 8. RTU 中的应用
|
||||||
|
|
||||||
|
RTU 的 `libweb_server` 模块通过 Mongoose WebSocket 实现:
|
||||||
|
|
||||||
|
- **实时数据推送**: 客户端订阅信号 → `ws_task` 每 100ms 推送变更的 JSON 数据
|
||||||
|
- **前端交互**: 内嵌 SPA 通过 WebSocket 接收遥测/遥信实时更新
|
||||||
|
- **命令下发**: 前端通过 WebSocket 发送控制命令(遥控/定值修改)
|
||||||
|
- **多浏览器**: per-connection session 隔离,各浏览器独立订阅
|
||||||
|
|
||||||
|
```
|
||||||
|
浏览器 ──WebSocket──→ Mongoose (mg_mgr_poll, 10ms)
|
||||||
|
↓ MG_EV_WS_MSG
|
||||||
|
ws_recv() → 命令分发
|
||||||
|
↓
|
||||||
|
datacenter API
|
||||||
|
↓
|
||||||
|
ws_task() → 增量 JSON 推送 (100ms)
|
||||||
|
↓ mg_ws_send
|
||||||
|
浏览器更新
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue