380 lines
17 KiB
Markdown
380 lines
17 KiB
Markdown
# libmms_s 模块分析
|
||
|
||
**日期**:2026-06-10
|
||
|
||
---
|
||
|
||
## 1. 模块概览
|
||
|
||
`libmms_s` 是 IEC 61850 MMS 服务端的封装库,基于 `libiec61850` v1.5.3 的服务端 API,提供 ICD 文件解析、动态数据模型创建、IedServer 生命周期管理,以及控制/定值/参数/文件等子系统的初始化。
|
||
|
||
### 目录结构
|
||
|
||
```
|
||
src/protocol/libmms_s/
|
||
├── inc/
|
||
│ ├── mms_s.h # 核心头文件:日志宏、全局访问接口、类型转换
|
||
│ ├── mms_s_icd.h # ICD 解析数据结构(完整 SCL 类型体系)
|
||
│ ├── mms_s_model.h # 动态模型创建接口
|
||
│ ├── mms_s_value.h # DA 初始值设定
|
||
│ ├── mms_s_control.h # 控制功能接口
|
||
│ ├── mms_s_param.h # 定值组参数接口
|
||
│ ├── mms_s_setting.h # 定值(SP)接口
|
||
│ └── mms_s_file.h # 文件传输服务接口
|
||
└── src/
|
||
├── mms_s.cpp # 核心:init/run/task/类型转换
|
||
├── mms_s_icd.cpp # ICD XML 解析器(tinyxml2)
|
||
├── mms_s_model.cpp # 动态 IedModel 创建
|
||
├── mms_s_control.cpp # 控制(SBO/直控)处理
|
||
├── mms_s_param.cpp # 定值组(SG/SE)管理
|
||
├── mms_s_setting.cpp # 定值(SP)管理
|
||
├── mms_s_value.cpp # 数据属性初始值设定
|
||
└── mms_s_file.cpp # MMS 文件传输服务
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 核心架构
|
||
|
||
### 2.1 单例全局状态
|
||
|
||
```cpp
|
||
LOCAL IedServer gp_iedServer = NULL; // 全局 IED 服务器(单例)
|
||
LOCAL stru_icd *gp_icd = NULL; // 全局 ICD 数据(单例)
|
||
LOCAL int g_running = 0; // 线程运行标志
|
||
LOCAL bool g_dbg_switch = false; // 调试输出开关
|
||
```
|
||
|
||
整个库采用单例模式,全局只有一个 IedServer 和一个 ICD 数据结构。
|
||
|
||
### 2.2 模块分层
|
||
|
||
```
|
||
┌──────────────────────────────────────────┐
|
||
│ myMms_s.h │ ← 公共 API + 类型定义
|
||
├──────────────────────────────────────────┤
|
||
│ mms_s.cpp (init / run / 类型转换) │ ← 库入口
|
||
├──────────────────────────────────────────┤
|
||
│ mms_s_icd.cpp │ mms_s_model.cpp │ ← ICD 解析 → 模型构建
|
||
├──────────────────────────────────────────┤
|
||
│ mms_s_value.cpp (DA 初始值) │ ← 模型回调
|
||
├──────────────────────────────────────────┤
|
||
│ mms_s_control.cpp │ mms_s_param.cpp │ ← 功能模块
|
||
│ mms_s_setting.cpp │ mms_s_file.cpp │
|
||
├──────────────────────────────────────────┤
|
||
│ libiec61850 (libiec61850.a) │ ← 底层协议栈
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 完整初始化流程
|
||
|
||
`mms_s_init()` 的执行顺序([mms_s.cpp:261](src/protocol/libmms_s/src/mms_s.cpp#L261)):
|
||
|
||
```
|
||
1. icd_parse(icd_path) → 解析 ICD 文件 → stru_icd
|
||
2. model_init(*gp_icd) → 创建动态数据模型 → IedModel
|
||
3. IedServerConfig_create() → 创建服务器配置
|
||
4. IedServer_createWithConfig() → 创建 IedServer 实例
|
||
5. control_init() → 安装控制回调
|
||
6. param_init() → 安装定值组回调
|
||
7. file_init() → 安装文件服务
|
||
8. IedServer_setRCBEventHandler() → 注册 RCB 事件监听
|
||
9. IedServer_start(port) → 启动服务器(默认端口 102)
|
||
10. setting_init() → 初始化定值
|
||
11. IedServer_isRunning() → 检查运行状态
|
||
12. Thread_create(mms_s_run_task) → 启动后台线程
|
||
```
|
||
|
||
---
|
||
|
||
## 4. ICD 解析子系统
|
||
|
||
### 4.1 数据结构体系([mms_s_icd.h](src/protocol/libmms_s/inc/mms_s_icd.h))
|
||
|
||
对应 IEC 61850-6 SCL 标准的完整类型体系:
|
||
|
||
**数据类型模板层(DataTypeTemplates)**:
|
||
| 结构 | 说明 | 关键字段 |
|
||
|------|------|---------|
|
||
| `stru_LNodeType` | 逻辑节点类型 | lnClass, vec_do, map_do(key:name) |
|
||
| `stru_DO` | 数据对象定义 | desc, type(→ DOType id) |
|
||
| `stru_DOType` | 数据对象类型 | cdc, vec_da, map_da, vec_sdo, map_sdo |
|
||
| `stru_DA` | 数据属性定义 | bType, type, dchg, qchg, fc, text |
|
||
| `stru_DAType` | 数据属性类型 | vec_bda, map_bda(key:name) |
|
||
| `stru_BDA` | 基本数据属性 | bType, type, fc, dchg, qchg |
|
||
| `stru_EnumType` | 枚举类型 | vec_ord, map_enumVal(key:ord) |
|
||
|
||
**实例化模型层(IED/Server)**:
|
||
| 结构 | 说明 |
|
||
|------|------|
|
||
| `stru_ied` | IED 顶层模型(含 model, name, 各逻辑设备) |
|
||
| `stru_LDevice` | 逻辑设备(含 Ldevice 指针, sgcb, ln0, vec_ln) |
|
||
| `stru_LN0` | LLN0(含 数据集, 报告控制, 定值控制) |
|
||
| `stru_LN` | 普通逻辑节点 |
|
||
| `stru_DOI` | 数据对象实例(含 SDI, DAI) |
|
||
| `stru_SDI` | 子数据对象实例(可递归嵌套) |
|
||
| `stru_DAI` | 数据属性实例(含 sAddr, val) |
|
||
| `stru_DataSet` | 数据集(含 FCDA 列表) |
|
||
| `stru_FCDA` | 功能约束数据属性(ldInst/lnClass/doName/daName/fc) |
|
||
| `stru_ReportControl` | 报告控制块(含 TrgOps, OptFields, RptEnabled_max) |
|
||
| `stru_SettingControl` | 定值控制块(actSG, numOfSGs) |
|
||
|
||
**运行时辅助结构**:
|
||
| 结构 | 说明 |
|
||
|------|------|
|
||
| `stru_all_DO` | DO 运行时树(含 libiec61850 DataObject 指针, map_all_sdo, map_all_da) |
|
||
| `stru_all_DA` | DA 运行时树(含 libiec61850 DataAttribute 指针, map_all_da 子节点) |
|
||
| `stru_contorl_DO` | 控制 DO 定位信息(ldevice_inst/ln_class/ln_inst/do_name/p_all_do) |
|
||
| `stru_saddr_point` | sAddr 到模型节点的映射(node, da_t, da_q) |
|
||
| `stru_setting_info` | 定值信息(DA 指针, 类型, 值) |
|
||
|
||
### 4.2 解析流程([mms_s_icd.cpp](src/protocol/libmms_s/src/mms_s_icd.cpp))
|
||
|
||
```
|
||
icd_parse(icd_file)
|
||
├── XMLDocument.LoadFile()
|
||
├── parse_ied() → 解析 IED 实例
|
||
│ ├── parse_LDevice() → 每个逻辑设备
|
||
│ │ ├── parse_LN0() → LLN0(数据集/报告控制/定值控制/DOI)
|
||
│ │ │ ├── parse_DataSet()
|
||
│ │ │ ├── parse_ReportControl()
|
||
│ │ │ │ ├── parse_ReportControl_TrgOps()
|
||
│ │ │ │ ├── parse_ReportControl_OptFields()
|
||
│ │ │ │ └── parse_ReportControl_RptEnable() → RptEnabled_max
|
||
│ │ │ ├── parse_DOI() → parse_SDI() / parse_DAI()
|
||
│ │ │ └── parse_SettingControl()
|
||
│ │ └── parse_LN() → 普通 LN + DOI
|
||
│ └── (沿 SCL > IED > AccessPoint > Server > LDevice 路径)
|
||
├── parse_dataTypeTemplates() → 解析模板
|
||
│ ├── parse_LNodeType()
|
||
│ ├── parse_DOType()
|
||
│ ├── parse_DAType()
|
||
│ ├── parse_EnumType()
|
||
│ └── parse_BDA_fc() ×2 → 传播 FC/dchg/qchg 到嵌套 BDA
|
||
└── check_ied_dataTypeTemplates() → 一致性校验
|
||
└── 递归验证每个实例节点都能在模板中找到定义
|
||
```
|
||
|
||
### 4.3 BDA 属性传播机制
|
||
|
||
关键设计:当 DA 的 type 指向 DAType(Struct 类型)时,`parse_BDA_fc()` 将父级 DA 的 fc/dchg/qchg 属性向下传播到 DAType 中所有 BDA,确保嵌套 Struct 的底层 BDA 也能继承正确的 FC 约束。
|
||
|
||
---
|
||
|
||
## 5. 动态模型创建子系统
|
||
|
||
### 5.1 model_init() 主流程([mms_s_model.cpp:1319](src/protocol/libmms_s/src/mms_s_model.cpp#L1319))
|
||
|
||
```
|
||
1. IedModel_create(name)
|
||
2. model_ldevice_init()
|
||
├── LogicalDevice_create()
|
||
├── model_ln0_init()
|
||
│ ├── LogicalNode_create("LLN0")
|
||
│ ├── model_dataobject_init() → 创建所有 DO/SDO/DA
|
||
│ ├── model_DataSet_init() → 创建数据集+条目
|
||
│ ├── model_ReportControlBlock_init() → 创建 RCB
|
||
│ └── SettingGroupControlBlock_create()
|
||
└── model_LNodes_init() → 创建普通 LN
|
||
3. model_sync_ied_default_values() → 同步 ICD DAI 中的 sAddr/val
|
||
4. model_search_control_DataObjects() → 搜索 ctlModel→vec_control_do
|
||
5. icd.ied.model->initializer = mms_s_values_init → 注册回调
|
||
```
|
||
|
||
### 5.2 类型映射表
|
||
|
||
**bType → DataAttributeType**([mms_s_model.cpp:43](src/protocol/libmms_s/src/mms_s_model.cpp#L43)):
|
||
`BOOLEAN → IEC61850_BOOLEAN`, `INT32 → IEC61850_INT32`, `FLOAT32 → IEC61850_FLOAT32`, `Struct → IEC61850_CONSTRUCTED`, `Quality → IEC61850_QUALITY`, `Timestamp → IEC61850_TIMESTAMP`, `Dbpos → IEC61850_GENERIC_BITSTRING` 等,共约 25 种映射。
|
||
|
||
**FC → FunctionalConstraint**([mms_s_model.cpp:86](src/protocol/libmms_s/src/mms_s_model.cpp#L86)):
|
||
`ST/MX/SP/SV/CF/DC/SG/SE/SR/OR/BL/EX/CO/US/MS/RP/BR/LG/GO` → 对应 IEC61850 枚举。
|
||
|
||
### 5.3 SG/SE 双数据属性设计
|
||
|
||
当 DA 的 FC=SG 时,除了创建 FC=SG 的 DA 外,还会额外创建一个 FC=SE 的同名 DA(key 加 `_SE` 后缀)。反之 FC=SE 同理。sAddr 映射时也会对应添加 `_SG` / `_SE` 后缀区分。
|
||
|
||
这是一个独创设计——在同一个 DO 下同时暴露 SG(当前激活值)和 SE(编辑缓冲区值),使得外部系统可以同时访问定值的"当前生效值"和"正在编辑中的值"。
|
||
|
||
### 5.4 sAddr 映射机制
|
||
|
||
ICD 文件中 DAI 元素的 `sAddr` 属性定义了该数据点与外部数据源的绑定关系。`model_sync_ied_default_values()` 将 sAddr 写入模型节点的 `sAddr` 字段,同时建立:
|
||
- `icd.ied.vec_saddr` — sAddr 列表
|
||
- `icd.ied.map_saddr_point` — sAddr → ModelNode 映射
|
||
|
||
对于 FC=SG 的点,sAddr 后缀 `_SG`;FC=SE 的点后缀 `_SE`。
|
||
|
||
---
|
||
|
||
## 6. 控制子系统([mms_s_control.cpp](src/protocol/libmms_s/src/mms_s_control.cpp))
|
||
|
||
### 6.1 双回调模式
|
||
|
||
| 回调 | 阶段 | 职能 |
|
||
|------|------|------|
|
||
| `check_handler()` | Select / Interlock | 权限校验,匹配合法 DO 则返回 CONTROL_ACCEPTED |
|
||
| `control_handler()` | Operate | 执行控制命令,更新 t(时间戳)+ stVal,触发外部回调 |
|
||
|
||
### 6.2 控制执行流程
|
||
|
||
```
|
||
客户端 → Select Request → check_handler() → CONTROL_ACCEPTED
|
||
客户端 → Operate Request → check_handler() (interlock check) → control_handler()
|
||
├── IedServer_updateUTCTimeAttributeValue(t)
|
||
├── 匹配 sAddr → cb_control() → 通知应用层
|
||
└── 根据 stVal 类型:
|
||
├── BOOLEAN → IedServer_updateAttributeValue()
|
||
└── Dbpos → IedServer_updateDbposValue()
|
||
```
|
||
|
||
### 6.3 控制 DO 发现
|
||
|
||
`model_search_control_DataObjects()` 遍历模型树,找到所有包含 `ctlModel` DA 的 DO,存入 `icd.ied.vec_control_do`。`control_init()` 遍历该列表,为每个安装 `control_handler` 和 `check_handler`。
|
||
|
||
---
|
||
|
||
## 7. 定值组子系统([mms_s_param.cpp](src/protocol/libmms_s/src/mms_s_param.cpp))
|
||
|
||
### 7.1 SG vs SE
|
||
|
||
| FC | 含义 | mmsValue 加载时机 |
|
||
|----|------|------------------|
|
||
| SG | Setting Group — 当前激活定值区的实际值 | 激活定值组切换时加载 |
|
||
| SE | Setting Editable — 编辑缓冲区值 | 编辑定值组切换时加载 |
|
||
|
||
### 7.2 回调链
|
||
|
||
```
|
||
激活定值组切换 → param_active_sg_changed_handler() → param_load_active_sg_values()
|
||
编辑定值组切换 → param_edit_sg_changed_handler() → param_load_edit_sg_values()
|
||
编辑确认 → edit_sg_confirmation_handler() → 回读 SE 值 → cb_param()
|
||
```
|
||
|
||
### 7.3 值的加载与校验
|
||
|
||
- 加载:根据 sAddr+"_SG"/"_SE" 在 `map_saddr_point` 中查找节点,按 MMS 类型分发更新函数
|
||
- 回读:`edit_sg_confirmation_handler()` 读取 SE DA 当前值,做 min/max/step 校验,通过后触发外部回调 `g_param_cb`
|
||
- 类型分发表:`g_param_update_funcs[]`(写)和 `g_param_get_funcs[]`(读),覆盖 BOOLEAN/INT/UINT/FLOAT/STRING
|
||
|
||
---
|
||
|
||
## 8. 定值子系统([mms_s_setting.cpp](src/protocol/libmms_s/src/mms_s_setting.cpp))
|
||
|
||
### 8.1 与参数的差异
|
||
|
||
定值(FC=SP)不参与定值组切换,是单一设置值。
|
||
|
||
### 8.2 写保护机制
|
||
|
||
1. 全局访问策略:`IedServer_setWriteAccessPolicy(IEC61850_FC_SP, ACCESS_POLICY_DENY)`
|
||
2. 精确放行:通过 `IedServer_handleWriteAccess()` 为已注册 sAddr 安装 `writeAccessHandler`
|
||
3. `writeAccessHandler` 做值校验(范围+步长),通过后触发外部回调 `cb_setting`
|
||
|
||
### 8.3 校验流程
|
||
|
||
```
|
||
客户端写 SP 值 → writeAccessHandler()
|
||
├── 匹配 sAddr
|
||
├── 类型分发 (setting_get_BOOLEAN/INT/UINT/FLOAT/STRING)
|
||
│ └── setting_min_max_step_get() → 读取同级 minVal/maxVal/stepSize
|
||
│ └── 范围校验 + 步长校验
|
||
├── cb_setting() → 通知应用层
|
||
└── return DATA_ACCESS_ERROR_SUCCESS → libiec61850 更新 mmsValue
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 数据属性值初始化([mms_s_value.cpp](src/protocol/libmms_s/src/mms_s_value.cpp))
|
||
|
||
`mms_s_values_init()` 作为 `IedModel.initializer` 回调,在 IedServer 创建过程中由 libiec61850 自动调用。
|
||
|
||
**初始化类型覆盖**:BOOLEAN, INT8U/32, FLOAT32, VisString*, Unicode*, Timestamp, Dbpos, Enum(特殊:按文本值或序号查找枚举值)。
|
||
|
||
**Dbpos 特殊处理**:`mms_s_da_value_init_Dbpos()` 按 val 值映射到 DBPOS_INTERMEDIATE_STATE(0)/OFF(1)/ON(2)/BAD_STATE(>=3)。
|
||
|
||
### 9.1 值更新路径
|
||
|
||
`mms_s_value_update()` 是预留的值更新函数,通过 `mms_s_value_update_register()` 注册给外部。当数据中心信号变化时,上层调用此函数更新模型中的 DA 值,同时自动更新父 DO 的 `t`(时间戳)和 `q`(品质)。
|
||
|
||
---
|
||
|
||
## 10. 文件传输服务([mms_s_file.cpp](src/protocol/libmms_s/src/mms_s_file.cpp))
|
||
|
||
提供 MMS 文件传输基础能力:
|
||
- 设置文件存储根目录 `IedServer_setFilestoreBasepath()`
|
||
- 访问控制:禁止重命名、禁止删除 `IEDSERVER.BIN`
|
||
- 连接事件日志
|
||
|
||
---
|
||
|
||
## 11. 后台运行线程
|
||
|
||
```cpp
|
||
LOCAL void *mms_s_run_task(void *parameter)
|
||
{
|
||
while(g_running) {
|
||
IedServer_lockDataModel(gp_iedServer);
|
||
IedServer_unlockDataModel(gp_iedServer);
|
||
Thread_sleep(100); // 100ms 周期
|
||
}
|
||
IedServer_stop(gp_iedServer);
|
||
IedServer_destroy(gp_iedServer);
|
||
IedModel_destroy(iedModel);
|
||
}
|
||
```
|
||
|
||
线程负责保护 IedServer 生命周期,通过 lock/unlock 持有模型锁保持服务活跃。收到 SIGINT 后 `g_running=0`,线程退出并销毁资源。
|
||
|
||
---
|
||
|
||
## 12. RCB 事件监听
|
||
|
||
`rcbEventHandler()` 监听客户端的报告控制块操作([mms_s.cpp:48](src/protocol/libmms_s/src/mms_s.cpp#L48)):
|
||
|
||
| 事件 | 含义 |
|
||
|------|------|
|
||
| `RCB_EVENT_ENABLE` | 客户端使能报告 |
|
||
| `RCB_EVENT_DISABLE` | 客户端关闭报告 |
|
||
| `RCB_EVENT_RESERVED` | 客户端预订 RCB |
|
||
| `RCB_EVENT_UNRESERVED` | 客户端释放预订 |
|
||
| `RCB_EVENT_GI` | 总召触发 |
|
||
| `RCB_EVENT_SET_PARAMETER` | 客户端设置 RCB 参数(如 TrgOps 等) |
|
||
| `RCB_EVENT_GET_PARAMETER` | 客户端获取 RCB 参数 |
|
||
|
||
**特殊处理**:当客户端设置 `TrgOps` 参数时,服务器强制追加 `dchg`(`rcb->trgOps |= 0x01`),确保数据变化一定能触发报告上送。
|
||
|
||
---
|
||
|
||
## 13. 对外 API 汇总
|
||
|
||
| API | 文件 | 说明 |
|
||
|-----|------|------|
|
||
| `mms_s_init(icd_path, port)` | mms_s.cpp | 完整初始化 MMS 服务器 |
|
||
| `mms_s_dbg_switch(on)` | mms_s.cpp | 调试开关 |
|
||
| `mms_s_get_icd_ptr()` | mms_s.cpp | 获取 ICD 数据 |
|
||
| `mms_s_get_ied_server_ptr()` | mms_s.cpp | 获取 IedServer |
|
||
| `mms_s_control_register(...)` | mms_s_control.cpp | 注册控制点 |
|
||
| `mms_s_setting_register(...)` | mms_s_setting.cpp | 注册定值 |
|
||
| `mms_s_param_register(...)` | mms_s_param.cpp | 注册参数(定值组) |
|
||
| `mms_s_file_path_set(path)` | mms_s_file.cpp | 设置文件根路径 |
|
||
| `mms_s_value_update_register(cb)` | mms_s_value.cpp | 注册值更新回调 |
|
||
| `mms_s_get_string_by_mms_type(...)` | mms_s.cpp | MMS 类型→字符串 |
|
||
| `mms_s_get_string_by_type(...)` | mms_s.cpp | 自定义类型→字符串 |
|
||
|
||
---
|
||
|
||
## 14. 已知问题
|
||
|
||
### 14.1 check_handler 未调用 cb_interlock
|
||
|
||
`mms_s_control.h` 中声明了 `mms_s_interlock_cb` 和 `mms_s_set_interlock_cb()`,但 `check_handler()` 中仅检查 ICD 中是否存在对应 DO,`cb_interlock` 未被实际调用。外部注册的联锁检查回调不会生效。
|
||
|
||
### 14.2 param_min_max_step_get 实现不一致
|
||
|
||
`mms_s_param.cpp` 的 `param_min_max_step_get()` 通过遍历 ModelNode 父子关系查找 minVal/maxVal/stepSize(向上找到 DataObject 再遍历 firstChild),而 `mms_s_setting.cpp` 的 `setting_min_max_step_get()` 是通过 parent->firstChild 直接遍历。两者逻辑不同,前者更复杂(向上一级再到 DO),可能是修正后的版本,但后者仍保留了旧逻辑。
|
||
|
||
### 14.3 mms_s_value_update 未使用 g_iec61850s_value_init_cb_map
|
||
|
||
`mms_s_value_update()` 使用 `g_iec61850s_value_update_cb_map`(按 DataAttributeType 索引),但表中大部分类型回调为 NULL,仅 BOOLEAN/INT32/INT32U/FLOAT32/Enum/VisString32/Unicode255/Timestamp 有实际实现。其他类型(如 INT8/INT16/INT64/FLOAT64 等)更新时会被跳过。
|