RTU/claude/工程/libmms_s模块分析.md

380 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 等)更新时会被跳过。