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