<修改> 1、创建mimo分支,后续此分支使用小米mimo code开发

This commit is contained in:
ypc 2026-06-12 16:48:42 +08:00
parent 3bf70c5b6d
commit f4b84506a6
26 changed files with 2146 additions and 5107 deletions

View File

@ -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条件、空行规则

View File

@ -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:** 新增代码和修改现有代码时必须遵守此格式。本次仅统一缩进/括号/空格/空行,命名问题暂不处理。

View File

@ -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

View File

@ -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 │
└─────────────────────────────────────┘
```

View File

@ -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 启动 pthreadlibiec61850m 做桥接 | libmms_s 启动 pthreadlibiec61850s 做桥接 |
| **桥接层复杂度** | 复杂RCB 发现/匹配/订阅/GI 触发/重连 | 相对简单:注册信号+回调libmms_s 处理细节 |
| **配置方式** | mms_m.xmlIED 连接参数) | 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)` | 打印配置内容(调试) |

View File

@ -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: 60sGI 触发)
#define MMS_M_TIMER_T2 (30 * 3) // T2: 30sAO + 参数读取)
#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 阶段一:发现 RCBmms_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/DOmember.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 阶段三:配置并激活 RCBmms_m_rcb_init
对每个已发现的 RCB执行完整的配置和激活流程[mms_m.cpp:1158-1214](src/protocol/libmms_m/src/mms_m.cpp#L1158-L1214)
```
┌─ 步骤1getRCBValues(rcb_ref) 读服务端当前 RCB 值
├─ 步骤2getDataSetReference(rcb) 获取 RCB 当前关联的数据集
│ ↓ ds_ref ← "TEMPLATE/LLN0$Events"
├─ 步骤3匹配本地数据集
│ 遍历 ln_datasets通过 dataset_ref 匹配
│ ln_rpt.p_dataset = &matched_dataset
├─ 步骤4本地设置 RCB 参数
│ setResv(true) 预留 RCBURCB
│ setTrgOps(dchg | qchg | gi) 触发条件:数据变化+品质变化+总召
│ setDataSetReference(ds_ref) 确认数据集引用
│ setRptEna(true) 使能报告
│ ⚠ 未调用 setOptFlds() 完全依赖服务端默认值
├─ 步骤5installReportHandler() 安装报告回调
│ rcbReference = rpt_ref 如 "LD/LLN0.RP.EventsRCB"
│ rptId = 从 RCB 获取
│ handler = mms_m_report_callback
│ parameter = &ln_rpt 回传 RCB 上下文
├─ 步骤6setRCBValues() 写入服务端
│ parametersMask = RCB_ELEMENT_RPT_ENA |
│ RCB_ELEMENT_TRG_OPS |
│ RCB_ELEMENT_INTG_PD |
│ RCB_ELEMENT_GI
│ singleRequest = true
│ ⚠ mask 包含 INTG_PD 但未调用 setIntgPd()
│ ⚠ mask 不含 OPT_FLDSOptFlds 沿用服务端默认
└─ 步骤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 定时触发
在阶段三完成首次订阅后,定时器 T160s周期性触发 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其他编号直接丢弃

View File

@ -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 指向 DATypeStruct 类型)时,`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 的同名 DAkey 加 `_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 等)更新时会被跳过。

View File

@ -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 | 释放连接对应的所有信号资源 |

View File

@ -1,598 +0,0 @@
# WebSocket 服务端深度分析
## 一、概述
Mongoose 的 WebSocket 实现位于 `src/ws.c`~302 行),基于 RFC 6455 规范,同时支持服务端和客户端。本文档聚焦**服务端**的使用和内部实现。
**核心流程**
```
HTTP 请求到达 (Upgrade: websocket)
→ 用户处理器检测到 WebSocket 升级请求
→ 调用 mg_ws_upgrade() 完成握手
→ pfn 切换为 mg_ws_cbWebSocket 协议处理器)
→ 后续消息通过 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 协议。

118
mimo/MEMORY.md Normal file
View File

@ -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 多通道、MQTTmosquitto、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 连接拥有独立的信号资源 sessionper-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` 动态创建 IedModelLD/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`

View File

@ -1,10 +1,7 @@
---
name: user-language
description: 用户要求所有对话、思考过程和中间输出均使用中文显示
metadata:
node_type: memory
type: user
originSessionId: 5e65af11-b197-479e-a82f-c2e5b475a8c3
---
# Global memory
_Cross-project user preferences. Persists across all projects. Edit only content under italic instructions._
## User language preference
_What language should the assistant use for all conversations, thinking, and visible outputs?_
用户要求所有对话内容、思考过程thinking、以及所有用户能看到的中间过程都使用中文显示。包括但不限于回复内容、代码注释、任务列表、提交信息等。代码本身变量名、函数名等保持英文不变。

View File

@ -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` 确认变更范围

View File

@ -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-CallT1 (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, "*");
```

View File

@ -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);
```
**功能**: 注册 SPfc=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` 自动获取各定值区数据指针

View File

@ -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 装置
```

View File

@ -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) 仅定值区同步 |

View File

@ -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_initURCB + 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 发现流程重新执行

View File

@ -0,0 +1,283 @@
# libmms_s 模块分析
**日期**: 2026-06-12
**基于源码**: `src/protocol/libmms_s/`16个文件约7500行
---
## 1. 模块定位
`libmms_s` 是 RTU 的 IEC 61850 MMS 服务端库,位于协议层。它解析 ICDIED Capability DescriptionXML 文件构建完整的数据模型,创建 `IedServer` 对外提供 MMS 服务,实现控制执行、定值组管理、文件服务等功能。
**与上下层关系**:
- **下层**: 依赖 libiec61850 v1.5.x 的 IedServer/IedModel API
- **上层**: 被 `iec61850s`(系统层封装)调用初始化,通过回调机制将控制/定值变更通知上层
- **数据方向**: 接收远方客户端的 MMS 读写请求,通过回调将控制指令传递给 `iec61850s``datacenter`
## 2. ICD 解析流程(`mms_s_icd.cpp`
ICD 文件是 SCLSubstation 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 下创建 SGSetting Group和 SESetting Group EditSE 是 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`
SPSet Point类 DAfc=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__)
```
带颜色、时间戳、文件名和行号。

View File

@ -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连接数通常较少

View File

@ -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
浏览器更新
```