<新增> 1、将claude生成的文件保留
This commit is contained in:
parent
ecd41888ca
commit
e18f239d77
|
|
@ -0,0 +1,88 @@
|
||||||
|
# RCB 订阅编号可配置化
|
||||||
|
|
||||||
|
**日期**:2026-06-10
|
||||||
|
**状态**:已完成
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 问题描述
|
||||||
|
|
||||||
|
`libmms_m` 模块中 `mms_m_icd_report_init()` 硬编码了 `if(0 == rpt_no.compare("01"))`,只订阅编号末尾为 `"01"` 的 RCB 实例,其他编号直接丢弃。当 IED 提供多个 RCB 实例(如 EventsRCB01、EventsRCB02)时,无法灵活订阅。
|
||||||
|
|
||||||
|
## 需求
|
||||||
|
|
||||||
|
1. 灵活的可配置订阅的控制块编号
|
||||||
|
2. `libmms_m` 提供接口,由 `libiec61850m` 调用接口传入
|
||||||
|
3. 可以传入一个或多个,如果不调用,默认订阅 `"01"`
|
||||||
|
4. 如果传入无效数据,打印提示信息,返回失败,初始化流程返回失败
|
||||||
|
|
||||||
|
## 设计方案
|
||||||
|
|
||||||
|
### 涉及文件
|
||||||
|
|
||||||
|
| 文件 | 修改内容 |
|
||||||
|
|------|---------|
|
||||||
|
| `release/inc/myMms_m.h` | 新增 API 声明 |
|
||||||
|
| `src/protocol/libmms_m/inc/mms_m.h` | `stru_mms_m_obj` 新增存储字段 |
|
||||||
|
| `src/protocol/libmms_m/src/mms_m.cpp` | 新增 API 实现 + 修改过滤逻辑 |
|
||||||
|
| `src/system/libiec61850m/src/iec61850m.cpp` | 调用新 API 传入配置 |
|
||||||
|
|
||||||
|
### API 设计
|
||||||
|
|
||||||
|
```c
|
||||||
|
int mms_m_out_set_rcb_numbers(int app_fd, const char *rcb_numbers);
|
||||||
|
```
|
||||||
|
|
||||||
|
参数格式:逗号分隔的两位数字编号,如 `"01,02,03"`,特殊值 `"*"` 表示全部。
|
||||||
|
|
||||||
|
### 输入格式定义
|
||||||
|
|
||||||
|
| 输入 | 行为 |
|
||||||
|
|------|------|
|
||||||
|
| 不调用此 API | 默认订阅 `"01"` |
|
||||||
|
| `"01"` | 订阅编号 `"01"` |
|
||||||
|
| `"01,02,03"` | 订阅 `"01"` `"02"` `"03"` |
|
||||||
|
| `"*"` | 订阅全部 RCB |
|
||||||
|
| `"abc"` / `"1"` / `"012"` | 非法,返回 -1 |
|
||||||
|
|
||||||
|
### 存储设计
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// stru_mms_m_obj 新增字段
|
||||||
|
std::vector<std::string> rcb_numbers; // 空=默认"01"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 匹配函数
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static bool mms_m_rcb_number_match(stru_mms_m_obj &obj, const std::string &rpt_no)
|
||||||
|
{
|
||||||
|
if (obj.rcb_numbers.empty()) return (rpt_no == "01"); // 默认
|
||||||
|
for (auto &n : obj.rcb_numbers)
|
||||||
|
if (rpt_no == n) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现流程
|
||||||
|
|
||||||
|
1. **myMms_m.h**:在 `mms_m_out_bind_param_zone_signal` 声明后新增 `mms_m_out_set_rcb_numbers` API 声明
|
||||||
|
2. **mms_m.h**:在 `stru_mms_m_obj.current_zone` 后新增 `rcb_numbers` 字段
|
||||||
|
3. **mms_m.cpp**:
|
||||||
|
- 新增 `#include <sstream>` 头文件
|
||||||
|
- 新增 `mms_m_rcb_number_match()` 静态匹配函数
|
||||||
|
- 新增 `mms_m_out_set_rcb_numbers()` API 实现(按逗号分割、去空格、校验恰好2位数字)
|
||||||
|
- 修改 `mms_m_icd_report_init()` 中 `rpt_no.compare("01")` → `mms_m_rcb_number_match(obj, rpt_no)`
|
||||||
|
4. **iec61850m.cpp**:在 `iec61850m_init()` 中 `mms_m_out_init()` 之后调用新 API,当前传入 `"01"`
|
||||||
|
|
||||||
|
## 变更记录
|
||||||
|
|
||||||
|
| 文件 | 行号(修改后) | 变更 |
|
||||||
|
|------|--------------|------|
|
||||||
|
| `myMms_m.h` | 170 | 新增 `int mms_m_out_set_rcb_numbers(...)` |
|
||||||
|
| `mms_m.h` | 183 | 新增 `std::vector<std::string> rcb_numbers` |
|
||||||
|
| `mms_m.cpp` | 7 | 新增 `#include <sstream>` |
|
||||||
|
| `mms_m.cpp` | 1342-1355 | 新增 `mms_m_rcb_number_match()` |
|
||||||
|
| `mms_m.cpp` | 1398 | `rpt_no.compare("01")` → `mms_m_rcb_number_match(obj, rpt_no)` |
|
||||||
|
| `mms_m.cpp` | 2097-2162 | 新增 `mms_m_out_set_rcb_numbers()` |
|
||||||
|
| `iec61850m.cpp` | 605-610 | 新增调用 `mms_m_out_set_rcb_numbers(fd, "01")` |
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,354 @@
|
||||||
|
# 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 │
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,671 @@
|
||||||
|
# libmms_m 模块工程文档
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
libmms_m 是 RTU 项目中 IEC 61850 MMS 客户端的**封装库**,位于 `src/protocol/libmms_m/`,负责将 libiec61850 的 C 客户端 API 封装成 RTU 内部的事件驱动架构。
|
||||||
|
|
||||||
|
### 核心职责
|
||||||
|
|
||||||
|
1. **连接管理**:通过异步方式与远端 IED 建立/维护 MMS 连接
|
||||||
|
2. **报告接收**:通过 Report 回调接收 IED 主动上送的变化数据
|
||||||
|
3. **数据操作**:总召、总召遥信遥测、AO 读写、参数(定值)读写
|
||||||
|
4. **控制指令**:遥控(CO)的 Select/Operate/Cancel 操作
|
||||||
|
5. **扩展服务**:文件传输、服务器信息查询、定值组读取
|
||||||
|
|
||||||
|
### 文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
src/protocol/libmms_m/
|
||||||
|
├── inc/
|
||||||
|
│ ├── mms_m.h ← 核心头文件(日志宏、数据结构、API 声明)
|
||||||
|
│ ├── mms_m_ext.h ← 扩展功能接口声明(文件/服务器/定值组)
|
||||||
|
│ └── mms_m_errstr.h ← 错误码转字符串
|
||||||
|
└── src/
|
||||||
|
├── mms_m.cpp ← 核心实现(连接/报告/总召/遥控/参数)
|
||||||
|
├── mms_m_errstr.cpp ← 错误码/控制模型/AddCause 转字符串
|
||||||
|
├── mms_m_file.cpp ← 文件服务实现
|
||||||
|
├── mms_m_server.cpp ← 服务器身份/状态查询
|
||||||
|
└── mms_m_sg.cpp ← 定值组信息读取
|
||||||
|
```
|
||||||
|
|
||||||
|
**基础类型定义**位于 `release/inc/myMms_m.h`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 核心数据结构
|
||||||
|
|
||||||
|
### 1.1 全局对象管理
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static std::map<int, stru_mms_m_obj *> g_mms_m_obj_map;
|
||||||
|
```
|
||||||
|
|
||||||
|
所有客户端实例通过全局 map 管理,key 为 `app_fd`(客户端句柄,从 1 开始自增),value 为对象指针。
|
||||||
|
|
||||||
|
### 1.2 主对象结构 `stru_mms_m_obj`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef struct {
|
||||||
|
int obj_fd; // 客户端句柄(自增)
|
||||||
|
uint32_t connectionTimeout; // 连接超时 [毫秒]
|
||||||
|
std::string cfg_path; // 配置文件路径
|
||||||
|
stru_cfg *p_cfg; // XML 配置解析结果
|
||||||
|
|
||||||
|
int debug_print_flag; // 调试打印开关(0/1)
|
||||||
|
|
||||||
|
std::string ied_name; // IED 名称
|
||||||
|
MmsValue *param_zone; // 定值区选择值(MmsValue, 可复用)
|
||||||
|
MmsValue *set_confirm; // 定值确认值(boolean, 可复用)
|
||||||
|
|
||||||
|
MMS_STR zone_saddr; // 关联定值区的信号 saddr(如 "ao.0")
|
||||||
|
int current_zone; // 当前定值区号
|
||||||
|
|
||||||
|
stru_mms_m_run run; // 运行时数据
|
||||||
|
std::vector<stru_ldev> ldevs; // 逻辑设备树(LD→LN→DO→Point)
|
||||||
|
std::vector<stru_ld_dataset> ld_datasets; // 数据集和报告配置
|
||||||
|
} stru_mms_m_obj;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 运行时结构 `stru_mms_m_run`
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
typedef struct {
|
||||||
|
std::string ip; // 服务器 IP
|
||||||
|
int port; // 服务器端口
|
||||||
|
bool running_init; // 是否已完成初始化
|
||||||
|
|
||||||
|
sem_t sem; // 回调同步信号量
|
||||||
|
pthread_t pthread_task; // 工作线程
|
||||||
|
|
||||||
|
IedConnection con; // libiec61850 连接句柄
|
||||||
|
IedConnectionState con_state; // 当前连接状态
|
||||||
|
IedConnectionState old_con_state; // 上一次连接状态
|
||||||
|
|
||||||
|
stru_mms_m_timer timer[_MMS_M_TIMER_END]; // 4 个定时器
|
||||||
|
|
||||||
|
stru_mms_m_event event; // 当前处理的事件
|
||||||
|
stru_event_queue event_queue; // 事件队列(容量 64)
|
||||||
|
// 事件类型(按枚举):
|
||||||
|
// [select] [operate] [cancel] ─→ 遥控
|
||||||
|
// [mms_m_send_co_select|direct|cancel]
|
||||||
|
// 通过 IedConnection_setRCBValues 发送总召
|
||||||
|
mms_m_out_status_cb out_status_cb; // 连接状态回调
|
||||||
|
std::vector<mms_m_out_value_cb> out_cb_lists; // 数据输出回调列表
|
||||||
|
} stru_mms_m_run;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 数据模型层次结构
|
||||||
|
|
||||||
|
```
|
||||||
|
stru_mms_m_obj
|
||||||
|
├── ldevs[] ← 逻辑设备列表
|
||||||
|
│ └── stru_ldev
|
||||||
|
│ ├── ld_name ← LD 名称(如 "TEMPLATE")
|
||||||
|
│ └── lnodes[] ← 逻辑节点列表
|
||||||
|
│ └── stru_lnode
|
||||||
|
│ ├── ln_name ← LN 名称(如 "GGIO1")
|
||||||
|
│ └── dobjs[] ← 数据对象列表
|
||||||
|
│ └── stru_dobj
|
||||||
|
│ ├── do_name ← DO 名称(如 "Ind1")
|
||||||
|
│ └── p_do_vec[] ← 指向 stru_point_item 的指针集合
|
||||||
|
│
|
||||||
|
└── ld_datasets[] ← 数据集/报告配置
|
||||||
|
└── stru_ld_dataset
|
||||||
|
├── ref ← LD/LN 引用
|
||||||
|
├── ld_name ← LD 名称
|
||||||
|
├── ln_name ← LN 名称
|
||||||
|
├── ln_datasets[] ← 数据集列表
|
||||||
|
│ └── stru_ln_dataset
|
||||||
|
│ ├── dataset_name ← 数据集全名
|
||||||
|
│ ├── dataset_ref ← 数据集引用 ($格式)
|
||||||
|
│ └── members[] ← 成员列表
|
||||||
|
│ └── stru_member
|
||||||
|
│ ├── ref ← FCDA 引用
|
||||||
|
│ ├── reason ← 包含原因
|
||||||
|
│ └── p_do_vec ← 关联的点集合
|
||||||
|
└── ln_rpts[] ← 报告列表
|
||||||
|
└── stru_ln_rpt
|
||||||
|
├── rpt_ref ← RCB 引用
|
||||||
|
├── rpt_ref_with_no ← RCB 引用+编号
|
||||||
|
├── ds_ref ← 关联数据集引用
|
||||||
|
├── type ← URCB 或 BRCB
|
||||||
|
├── rcb ← ClientReportControlBlock 句柄
|
||||||
|
├── p_app ← 指向所属 mms_m_obj
|
||||||
|
└── p_dataset ← 指向关联数据集
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.5 事件与定时器
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 事件类型
|
||||||
|
enum {
|
||||||
|
_MMS_M_EVENT_ALL_CALL, // 总召遥信遥测
|
||||||
|
_MMS_M_EVENT_GI_CALL, // 总召(GI)
|
||||||
|
_MMS_M_EVENT_CO_SELECT, // 遥控-选择
|
||||||
|
_MMS_M_EVENT_CO_DIRECT, // 遥控-操作
|
||||||
|
_MMS_M_EVENT_CO_CANCEL, // 遥控-取消
|
||||||
|
_MMS_M_EVENT_AO_READ, // 读取 AO
|
||||||
|
_MMS_M_EVENT_AO_WRITE, // 写入 AO
|
||||||
|
_MMS_M_EVENT_PARAM_READ, // 读取参数
|
||||||
|
_MMS_M_EVENT_PARAM_WRITE, // 写入参数
|
||||||
|
_MMS_M_EVENT_END
|
||||||
|
};
|
||||||
|
|
||||||
|
// 事件结构
|
||||||
|
typedef struct {
|
||||||
|
int app_fd; // 客户端句柄
|
||||||
|
MMS_STR ied; // IED 名称
|
||||||
|
MMS_STR saddr; // 短地址
|
||||||
|
uint8_t value_type; // MMS 数据类型
|
||||||
|
char val[MMS_M_DATA_STRING_LEN]; // 值(字符串形式)
|
||||||
|
uint8_t ctrl_type; // 事件类型
|
||||||
|
int set_zone; // 定值区号
|
||||||
|
void (*p_func)(void *arg, int ret); // 完成回调
|
||||||
|
} stru_mms_m_event;
|
||||||
|
|
||||||
|
// 事件队列(环形缓冲区,容量 64)
|
||||||
|
typedef struct {
|
||||||
|
uint8_t w_ptr; // 写指针
|
||||||
|
uint8_t r_ptr; // 读指针
|
||||||
|
uint8_t size; // 容量
|
||||||
|
uint8_t num; // 当前数量
|
||||||
|
stru_mms_m_event event[EVENT_QUEUE_SIZE];
|
||||||
|
} stru_event_queue;
|
||||||
|
|
||||||
|
// 4 个定时器
|
||||||
|
enum { _MMS_M_TIMER_T0, _MMS_M_TIMER_T1, _MMS_M_TIMER_T2, _MMS_M_TIMER_T3, _MMS_M_TIMER_END };
|
||||||
|
|
||||||
|
// 定时器时间定义
|
||||||
|
#define MMS_M_THREAD_RUN_TM (100 * 3) // 线程循环间隔:300ms
|
||||||
|
#define MMS_M_TIMER_T0 (120 * 3) // T0: 120s(总召所有数据)
|
||||||
|
#define MMS_M_TIMER_T1 (60 * 3) // T1: 60s(GI 触发)
|
||||||
|
#define MMS_M_TIMER_T2 (30 * 3) // T2: 30s(AO + 参数读取)
|
||||||
|
#define MMS_M_TIMER_T3 (20 * 3) // T3: 20s(预留,未使用)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 线程模型与主流程
|
||||||
|
|
||||||
|
### 2.1 生命周期
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_m_out_init()
|
||||||
|
├── 分配 stru_mms_m_obj
|
||||||
|
├── mms_m_ied_init() ← 构建 ldevs 树
|
||||||
|
├── sem_init() ← 初始化信号量
|
||||||
|
├── pthread_create(mms_m_run_thread) ← 创建工作线程
|
||||||
|
└── 加入 g_mms_m_obj_map[id]
|
||||||
|
|
||||||
|
mms_m_run_thread() ← 工作线程入口
|
||||||
|
├── mms_m_timer_init() ← 初始化4个定时器
|
||||||
|
├── 初始化事件队列
|
||||||
|
├── IedConnection_create()
|
||||||
|
├── IedConnection_installStateChangedHandler()
|
||||||
|
├── IedConnection_connectAsync()
|
||||||
|
└── 主循环(300ms 周期)
|
||||||
|
├── mms_m_do_comm() ← 连接状态机
|
||||||
|
└── if CONNECTED → mms_m_run()
|
||||||
|
├── mms_m_run_init() ← 首次建连时执行
|
||||||
|
├── mms_m_do_send() ← 处理事件队列
|
||||||
|
└── mms_m_timer_running() ← 检查定时器
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 连接状态机 `mms_m_do_comm()`
|
||||||
|
|
||||||
|
```
|
||||||
|
状态检查: IedConnection_getState()
|
||||||
|
|
||||||
|
CLOSED/CLOSING → connectAsync() ← 自动重连
|
||||||
|
CONNECTING → 等待
|
||||||
|
CONNECTED → [首次] mms_m_control_init() ← 创建 ControlObjectClient
|
||||||
|
[首次] 触发 out_status_cb(ON_LINE)
|
||||||
|
|
||||||
|
断连检测: old=CONNECTED, new!=CONNECTED
|
||||||
|
→ running_init = false ← 标记需要重新初始化
|
||||||
|
→ 触发 out_status_cb(OFF_LINE)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 首次连接初始化 `mms_m_run_init()`
|
||||||
|
|
||||||
|
```
|
||||||
|
1. mms_m_icd_init()
|
||||||
|
├── getLogicalDeviceList() ← 获取 LD 列表
|
||||||
|
├── 遍历每对 LD+LN:
|
||||||
|
│ ├── mms_m_icd_dataset_init() ← 读取该 LN 下的所有 DataSet
|
||||||
|
│ │ └── getDataSetDirectory() ← 读数据集成员
|
||||||
|
│ ├── mms_m_icd_report_init(URCB) ← 发现 URCB 报告
|
||||||
|
│ └── mms_m_icd_report_init(BRCB) ← 发现 BRCB 报告
|
||||||
|
└── ld_datasets 构建完成
|
||||||
|
|
||||||
|
2. mms_m_ld_dataset_match_point_init()
|
||||||
|
└── 将数据集成员与 ldevs 中的 point 关联(构建 p_do_vec)
|
||||||
|
|
||||||
|
3. mms_m_rcb_init()
|
||||||
|
├── getRCBValues() ← 获取每个 RCB 当前值
|
||||||
|
├── 匹配数据集引用 → p_dataset
|
||||||
|
├── setResv(true) ← 预留 RCB
|
||||||
|
├── setTrgOps(dchg|qchg|gi)
|
||||||
|
├── setRptEna(true) ← 使能报告
|
||||||
|
├── installReportHandler() ← 安装回调
|
||||||
|
├── setRCBValues() ← 写入配置到服务器
|
||||||
|
└── setGI(true) ← 触发一次总召
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 数据流程
|
||||||
|
|
||||||
|
### 3.1 周期性总召(T0 定时器)
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_m_do_call_all() ← 每 120s 触发
|
||||||
|
└── push _MMS_M_EVENT_ALL_CALL 事件
|
||||||
|
└── mms_m_send_call_all()
|
||||||
|
└── 遍历 ST + MX 数据点
|
||||||
|
├── IedConnection_readObject() ← 逐一读取
|
||||||
|
├── mms_m_get_mmsValue() ← MmsValue→C 类型
|
||||||
|
├── mms_m_put_value() ← 通过回调输出
|
||||||
|
└── MmsValue_delete() ← 清理
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 报告控制块(RCB)订阅全流程
|
||||||
|
|
||||||
|
RCB 订阅是 MMS 客户端最核心的机制,负责接收 IED 主动上送的数据变化。整个流程分为发现→匹配→激活→接收四个阶段。
|
||||||
|
|
||||||
|
#### 3.2.1 阶段一:发现 RCB(mms_m_icd_init → mms_m_icd_report_init)
|
||||||
|
|
||||||
|
连接成功后,遍历所有 LD→LN,对每对调用 `IedConnection_getLogicalNodeDirectory()` 分别查询两类 RCB:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 查询 URCB (unbuffered,引用名含 "RP")
|
||||||
|
mms_m_icd_report_init(obj, ld_dataset, ACSI_CLASS_URCB);
|
||||||
|
// 查询 BRCB (buffered,引用名含 "BR")
|
||||||
|
mms_m_icd_report_init(obj, ld_dataset, ACSI_CLASS_BRCB);
|
||||||
|
```
|
||||||
|
|
||||||
|
`mms_m_icd_report_init()` 的核心筛选逻辑:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::string rpt_ref_str = (URCB == acsiClass) ? "RP" : "BR";
|
||||||
|
|
||||||
|
LinkedList reports = IedConnection_getLogicalNodeDirectory(
|
||||||
|
p_con, &err, ld_ds.ref.c_str(), acsiClass);
|
||||||
|
LinkedList report = LinkedList_getNext(reports);
|
||||||
|
|
||||||
|
while (report != NULL) {
|
||||||
|
std::string rpt_data = (char*) report->data;
|
||||||
|
|
||||||
|
// 提取名称和编号:最后2位是编号,前面是名称
|
||||||
|
rpt_name = rpt_data.substr(0, rpt_data.length() - 2);
|
||||||
|
rpt_no = rpt_data.substr(rpt_data.length() - 2);
|
||||||
|
|
||||||
|
rpt_ref = ld_ds.ref + "." + rpt_ref_str + ".";
|
||||||
|
|
||||||
|
if (0 == rpt_no.compare("01")) // 只取编号 "01"
|
||||||
|
{
|
||||||
|
ln_rpt.rpt_ref = rpt_ref + rpt_name;
|
||||||
|
// 例: "TEMPLATE/LLN0.RP.EventsRCB"
|
||||||
|
ln_rpt.rpt_ref_with_no = rpt_ref + rpt_name + rpt_no;
|
||||||
|
// 例: "TEMPLATE/LLN0.RP.EventsRCB01"
|
||||||
|
ld_ds.ln_rpts.push_back(ln_rpt);
|
||||||
|
}
|
||||||
|
// else: 其他编号直接丢弃
|
||||||
|
|
||||||
|
report = LinkedList_getNext(report);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**限制**:只订阅编号末尾为 `"01"` 的 RCB 实例。
|
||||||
|
|
||||||
|
#### 3.2.2 阶段二:匹配数据集(mms_m_ld_dataset_match_point_init)
|
||||||
|
|
||||||
|
将 XML 配置加载的 `ldevs` 信号树与从 IED 发现的 `ld_datasets` 数据集树进行关联,使得每个 dataset member 知道对应哪些信号点(saddr)。
|
||||||
|
|
||||||
|
```
|
||||||
|
数据集 member 的 FCDA 引用示例: "IEDNAME+TEMPLATE/GGIO1.ST.Ind1.stVal[ST]"
|
||||||
|
↓ 解析
|
||||||
|
ld = "IEDNAME+TEMPLATE" → 去 IED 前缀 → "TEMPLATE"
|
||||||
|
ln = "GGIO1"
|
||||||
|
d_name = "Ind1"
|
||||||
|
↓ 匹配 ldevs 树
|
||||||
|
ldevs[ld="TEMPLATE"] → lnodes[ln="GGIO1"] → dobjs[d_name="Ind1"]
|
||||||
|
↓
|
||||||
|
member.p_do_vec = &dobjs.p_do_vec // 建立关联
|
||||||
|
|
||||||
|
如果匹配不到(找不到对应的 LD/LN/DO),member.p_do_vec 为 NULL,
|
||||||
|
该 member 的报告数据将无法输出到任何信号点。
|
||||||
|
```
|
||||||
|
|
||||||
|
引用字符串解析规则([mms_m.cpp:1228-1257](src/protocol/libmms_m/src/mms_m.cpp#L1228-L1257)):
|
||||||
|
- 第1段(`/` 前):LD 名称,需去除 IED 名前缀
|
||||||
|
- 第2段(`.` 前):LN 名称
|
||||||
|
- 第3段(`[` 前):DO 名称
|
||||||
|
- 不匹配此格式的 member 直接跳过
|
||||||
|
|
||||||
|
#### 3.2.3 阶段三:配置并激活 RCB(mms_m_rcb_init)
|
||||||
|
|
||||||
|
对每个已发现的 RCB,执行完整的配置和激活流程([mms_m.cpp:1158-1214](src/protocol/libmms_m/src/mms_m.cpp#L1158-L1214)):
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─ 步骤1:getRCBValues(rcb_ref) 读服务端当前 RCB 值
|
||||||
|
│
|
||||||
|
├─ 步骤2:getDataSetReference(rcb) 获取 RCB 当前关联的数据集
|
||||||
|
│ ↓ ds_ref ← "TEMPLATE/LLN0$Events"
|
||||||
|
├─ 步骤3:匹配本地数据集
|
||||||
|
│ 遍历 ln_datasets,通过 dataset_ref 匹配
|
||||||
|
│ ln_rpt.p_dataset = &matched_dataset
|
||||||
|
│
|
||||||
|
├─ 步骤4:本地设置 RCB 参数
|
||||||
|
│ setResv(true) 预留 RCB(URCB)
|
||||||
|
│ setTrgOps(dchg | qchg | gi) 触发条件:数据变化+品质变化+总召
|
||||||
|
│ setDataSetReference(ds_ref) 确认数据集引用
|
||||||
|
│ setRptEna(true) 使能报告
|
||||||
|
│ ⚠ 未调用 setOptFlds() 完全依赖服务端默认值
|
||||||
|
│
|
||||||
|
├─ 步骤5:installReportHandler() 安装报告回调
|
||||||
|
│ rcbReference = rpt_ref 如 "LD/LLN0.RP.EventsRCB"
|
||||||
|
│ rptId = 从 RCB 获取
|
||||||
|
│ handler = mms_m_report_callback
|
||||||
|
│ parameter = &ln_rpt 回传 RCB 上下文
|
||||||
|
│
|
||||||
|
├─ 步骤6:setRCBValues() 写入服务端
|
||||||
|
│ parametersMask = RCB_ELEMENT_RPT_ENA |
|
||||||
|
│ RCB_ELEMENT_TRG_OPS |
|
||||||
|
│ RCB_ELEMENT_INTG_PD |
|
||||||
|
│ RCB_ELEMENT_GI
|
||||||
|
│ singleRequest = true
|
||||||
|
│ ⚠ mask 包含 INTG_PD 但未调用 setIntgPd()
|
||||||
|
│ ⚠ mask 不含 OPT_FLDS,OptFlds 沿用服务端默认
|
||||||
|
│
|
||||||
|
└─ 步骤7:触发总召
|
||||||
|
setGI(true) → setRCBValues(mask=RCB_ELEMENT_GI)
|
||||||
|
让 IED 立即上送一次完整数据
|
||||||
|
```
|
||||||
|
|
||||||
|
**步骤 6 的潜在问题**:
|
||||||
|
- `parametersMask` 中包含 `RCB_ELEMENT_INTG_PD`,但本地并未调用 `setIntgPd()`,写入的 IntgPd 值实际是步骤1从服务端读取的原始值
|
||||||
|
- `RCB_ELEMENT_OPT_FLDS` 未包含在 mask 中,意味着不会设置服务端的 OptFlds,完全依赖服务端默认配置 — 可能导致报告缺少 `DataReference`、`ConfRev`、`BufferOverflow` 等字段
|
||||||
|
- 使用 `singleRequest=true`,一次 MMS 写请求携带多个变量;如果服务端兼容性不好,可能需要改为 `false`
|
||||||
|
|
||||||
|
#### 3.2.4 阶段四:报告接收与分发(mms_m_report_callback)
|
||||||
|
|
||||||
|
当 IED 发送报告时,libiec61850 回调 `mms_m_report_callback()`([mms_m.cpp:1079-1153](src/protocol/libmms_m/src/mms_m.cpp#L1079-L1153)):
|
||||||
|
|
||||||
|
```
|
||||||
|
ClientReport 到达
|
||||||
|
│ parameter = &ln_rpt ← 阶段三步骤5传入的回调参数
|
||||||
|
│
|
||||||
|
├── ClientReport_getDataSetValues(report) → MmsValue* (MMS_ARRAY)
|
||||||
|
│
|
||||||
|
└── 遍历 dataset->members(按数据集顺序,index 从 0 递增)
|
||||||
|
│
|
||||||
|
├── reason = ClientReport_getReasonForInclusion(report, index)
|
||||||
|
│
|
||||||
|
├── if (reason == NOT_INCLUDED) → 跳过该成员
|
||||||
|
│
|
||||||
|
├── mms_value = MmsValue_getElement(dataset_value, index)
|
||||||
|
│ │
|
||||||
|
│ └── mms_m_get_MmsValue(point_value, out_value, mms_value, ...)
|
||||||
|
│ 递归解包 MmsValue → C 类型
|
||||||
|
│ ├── MMS_STRUCTURE/ARRAY → 递归展开子元素
|
||||||
|
│ ├── MMS_BOOLEAN → *(uint8_t*)p_val
|
||||||
|
│ ├── MMS_INTEGER → *(int32_t*)p_val
|
||||||
|
│ ├── MMS_UNSIGNED → *(uint32_t*)p_val
|
||||||
|
│ ├── MMS_FLOAT → *(float*)p_val
|
||||||
|
│ ├── MMS_BIT_STRING → MmsValue_getBitStringAsIntegerBigEndian()
|
||||||
|
│ ├── MMS_UTC_TIME → 解析时间 → point_value.time
|
||||||
|
│ └── MMS_BINARY_TIME → 解析时间 → point_value.time
|
||||||
|
│
|
||||||
|
├── 如果 mms_value 含时间戳 → tm_flag=1 → 使用报告中的时间
|
||||||
|
│ 否则 → tm_flag=0 → mms_m_get_local_time() 使用本地时间
|
||||||
|
│
|
||||||
|
└── 通过 member.p_do_vec 遍历关联的信号点
|
||||||
|
for (each point_item in p_do_vec)
|
||||||
|
out_value.name = point.saddr
|
||||||
|
out_value.desc = point.desc
|
||||||
|
out_value.reference = point.reference
|
||||||
|
out_value.reason = reason
|
||||||
|
out_value.p_value = 解析后的值指针
|
||||||
|
out_value.time = 时间(报告时间或本地时间)
|
||||||
|
out_value.app_fd = obj.obj_fd
|
||||||
|
↓
|
||||||
|
mms_m_put_value(obj, out_value) // 回调所有注册的 out_cb
|
||||||
|
```
|
||||||
|
|
||||||
|
**数据输出链路**:`mms_m_put_value()` → 遍历 `run.out_cb_lists` → 回调上层注册的数据处理函数(如 `libiec61850m` 中的 `mms_data_back`)。
|
||||||
|
|
||||||
|
#### 3.2.5 断连后的重新订阅
|
||||||
|
|
||||||
|
连接状态机在检测到断连时([mms_m.cpp:1579-1588](src/protocol/libmms_m/src/mms_m.cpp#L1579-L1588)):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
if (run.con_state != CONNECTED && run.old_con_state == CONNECTED) {
|
||||||
|
obj.run.running_init = false; // 清除已初始化标志
|
||||||
|
out_status_cb(obj.obj_fd, OFF_LINE);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
下一次 `mms_m_run()` 检测到 `running_init==false` 且已重连时,自动**重新执行完整的四阶段订阅流程**(阶段一→二→三),确保断连恢复后重新订阅所有 RCB。
|
||||||
|
|
||||||
|
#### 3.2.6 GI 定时触发
|
||||||
|
|
||||||
|
在阶段三完成首次订阅后,定时器 T1(60s)周期性触发 GI 总召([mms_m.cpp:1636-1649](src/protocol/libmms_m/src/mms_m.cpp#L1636-L1649)):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 每 60s 一次
|
||||||
|
event.ctrl_type = _MMS_M_EVENT_GI_CALL;
|
||||||
|
mms_m_push_event(obj, event);
|
||||||
|
|
||||||
|
// → mms_m_send_set_gi()
|
||||||
|
// 遍历所有 RCB → setGI(true) → setRCBValues(mask=RCB_ELEMENT_GI)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2.7 流程总览图
|
||||||
|
|
||||||
|
```
|
||||||
|
首次连接 / 断连重连
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
阶段一 ✦ 发现 ──────────────────────────────────────
|
||||||
|
mms_m_icd_init() → mms_m_icd_report_init()
|
||||||
|
遍历 LD→LN → getLogicalNodeDirectory(URCB/BRCB)
|
||||||
|
└── 筛选:只取编号末尾为 "01" 的 RCB
|
||||||
|
产出: ld_datasets[].ln_rpts[] (RCB 列表)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
阶段二 ✦ 匹配 ──────────────────────────────────────
|
||||||
|
mms_m_ld_dataset_match_point_init()
|
||||||
|
解析 dataset member 的 FCDA 引用 (LD/LN/DO)
|
||||||
|
└── 匹配 ldevs 信号树 → member.p_do_vec 关联
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
阶段三 ✦ 激活 ──────────────────────────────────────
|
||||||
|
mms_m_rcb_init()
|
||||||
|
对每个 RCB:
|
||||||
|
├── getRCBValues() 读取服务端 RCB
|
||||||
|
├── setResv/setTrgOps/setRptEna 配置参数
|
||||||
|
├── installReportHandler() 安装回调
|
||||||
|
├── setRCBValues() 写入服务端
|
||||||
|
└── setGI(true) 触发总召
|
||||||
|
│
|
||||||
|
▼ (此后异步回调)
|
||||||
|
阶段四 ✦ 接收 ──────────────────────────────────────
|
||||||
|
mms_m_report_callback()
|
||||||
|
ClientReport 到达 → 遍历 dataset members
|
||||||
|
├── 跳过 NOT_INCLUDED 成员
|
||||||
|
├── 递归解包 MmsValue → C 类型
|
||||||
|
├── 解析时间戳
|
||||||
|
└── p_do_vec → mms_m_put_value() → 上层回调
|
||||||
|
|
||||||
|
周期性触发:
|
||||||
|
T1 (60s) → GI ─────────────────────────────────→ 阶段四再次触发
|
||||||
|
T0 (120s) → AllCall 读取全部 ST+MX 值
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 遥控流程
|
||||||
|
|
||||||
|
```
|
||||||
|
外部调用 mms_m_out_do_set_yk(event)
|
||||||
|
└── push 遥控事件到事件队列
|
||||||
|
└── mms_m_send_co() ← 处理遥控事件
|
||||||
|
├── 按 saddr 匹配 co_point
|
||||||
|
├── 首次: ControlObjectClient_create()
|
||||||
|
│ └── setOrigin(ORCAT_STATION_CONTROL)
|
||||||
|
├── 构造 set_value (MmsValue_newBoolean)
|
||||||
|
└── 按 ctrl_type 分发:
|
||||||
|
├── SELECT → mms_m_send_co_select()
|
||||||
|
│ ├── SBO_NORMAL: select()
|
||||||
|
│ └── SBO_ENHANCED: selectWithValue()
|
||||||
|
│ └── 先 setCommandTerminationHandler()
|
||||||
|
├── DIRECT → mms_m_send_co_direct()
|
||||||
|
│ ├── operate()
|
||||||
|
│ └── 按 ctrl_model 检查结果:
|
||||||
|
│ ├── DIRECT_NORMAL: 立即回读 stVal 验证
|
||||||
|
│ ├── DIRECT_ENHANCED: 等待 1s 后回读验证
|
||||||
|
│ └── SBO_ENHANCED: 等待 1s(不验证)
|
||||||
|
└── CANCEL → mms_m_send_co_cancel()
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 参数(定值)写入流程
|
||||||
|
|
||||||
|
```
|
||||||
|
_PARAM_WRITE 事件
|
||||||
|
└── mms_m_send_param_write()
|
||||||
|
├── 匹配参数点
|
||||||
|
├── 构造 EditSG 引用: {LD}/LLN0.SGCB.EditSG
|
||||||
|
├── 构造 CnfEdit 引用: {LD}/LLN0.SGCB.CnfEdit
|
||||||
|
├── 步骤1: writeObject(EditSG, FC=SP) ← 选择编辑定值组
|
||||||
|
├── 步骤2: writeObject(ref, FC=SE) ← 写入新定值
|
||||||
|
└── 步骤3: writeObject(CnfEdit, FC=SP) ← 确认编辑
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 定值区自动切换
|
||||||
|
|
||||||
|
```
|
||||||
|
mms_m_send_read_ao() 中检测:
|
||||||
|
如果某个 AO 信号的 saddr == zone_saddr(绑定的定值区信号)
|
||||||
|
→ 读值后比较 current_zone
|
||||||
|
→ 若变化 (new_zone != current_zone):
|
||||||
|
update current_zone
|
||||||
|
push _MMS_M_EVENT_PARAM_READ 事件 ← 自动触发参数重读
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 对外接口
|
||||||
|
|
||||||
|
### 4.1 myMms_m.h 中声明的主要 API
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// === 生命周期 ===
|
||||||
|
int mms_m_out_init(stru_cfg *p_cfg, int debug_print_flag, uint32_t connectionTimeout);
|
||||||
|
// 返回 app_fd (>0 成功,-1 失败)
|
||||||
|
|
||||||
|
// === 状态回调 ===
|
||||||
|
int mms_m_out_get_connect_status(int fd, mms_m_out_status_cb p_func);
|
||||||
|
// 连接/断开时回调: void (int app_fd, int status) // MMS_M_ON_LINE / MMS_M_OFF_LINE
|
||||||
|
|
||||||
|
// === 数据回调 ===
|
||||||
|
int mms_m_out_get_value(int fd, mms_m_out_value_cb p_func);
|
||||||
|
// 数据到达时回调: void (stru_mms_m_out_value *p_value)
|
||||||
|
|
||||||
|
// === 遥控 ===
|
||||||
|
int mms_m_out_do_set_yk(stru_mms_m_event *p_event);
|
||||||
|
// 发送遥控事件(select/direct/cancel),返回 -1 失败
|
||||||
|
|
||||||
|
// === AO/参数操作 ===
|
||||||
|
int mms_m_out_read_ao_or_params(int app_fd, uint8_t type, const char *saddr);
|
||||||
|
// type: _MMS_M_EVENT_AO_READ 或 _MMS_M_EVENT_PARAM_READ
|
||||||
|
// saddr 为 NULL 则读取全部
|
||||||
|
|
||||||
|
// === 定值区绑定 ===
|
||||||
|
int mms_m_out_bind_param_zone_signal(int app_fd, const char *zone_saddr);
|
||||||
|
// 绑定一个 AO 信号作为定值区指示器
|
||||||
|
|
||||||
|
// === 调试 ===
|
||||||
|
int mms_m_out_debug_print_swicth(int id, int debug_print_flag);
|
||||||
|
|
||||||
|
// === 工具函数 ===
|
||||||
|
void *mms_m_create_data_ptr(uint8_t type); // 按 MMS 类型创建数据指针
|
||||||
|
int mms_m_set_data_value(void *srt, void *dst, uint8_t type); // 复制值
|
||||||
|
int mms_m_get_data_value_str(void *data, uint8_t type, char *str); // 转字符串
|
||||||
|
int mms_m_set_data_by_str(void *data, uint8_t type, const char *str); // 从字符串设置
|
||||||
|
char *mms_m_out_reason_str(int reason); // 原因码转字符串
|
||||||
|
void *mms_m_get_obj(int app_fd); // 获取对象(给扩展模块)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 扩展接口(mms_m_ext.h)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 文件服务
|
||||||
|
typedef void (*mms_m_file_read_cb)(int fd, const char *fn, const uint8_t *d, int len, bool mf, int e);
|
||||||
|
typedef void (*mms_m_file_op_cb)(int fd, const char *fn, int e);
|
||||||
|
int mms_m_read_file(int fd, const char *rf, mms_m_file_read_cb cb);
|
||||||
|
int mms_m_delete_file(int fd, const char *rf, mms_m_file_op_cb cb);
|
||||||
|
|
||||||
|
// 服务器信息查询
|
||||||
|
typedef void (*mms_m_server_cb)(int fd, const char *vendor, const char *model, const char *rev, int log_st, int phy_st, int e);
|
||||||
|
int mms_m_query_server(int fd, mms_m_server_cb cb);
|
||||||
|
|
||||||
|
// 定值组信息
|
||||||
|
typedef void (*mms_m_sg_cb)(int fd, const char *ld, int act_sg, int num_sg, int e);
|
||||||
|
int mms_m_read_sg_info(int fd, const char *ld, mms_m_sg_cb cb);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 扩展回调类型汇总
|
||||||
|
|
||||||
|
| 回调类型 | 签名 | 用途 |
|
||||||
|
|---------|------|------|
|
||||||
|
| `mms_m_out_status_cb` | `void(int fd, int status)` | 连接状态 |
|
||||||
|
| `mms_m_out_value_cb` | `void(stru_mms_m_out_value *p_value)` | 数据到达 |
|
||||||
|
| `mms_m_file_read_cb` | `void(int, const char*, const uint8_t*, int, bool, int)` | 文件分块读取 |
|
||||||
|
| `mms_m_file_op_cb` | `void(int fd, const char* filename, int err)` | 文件删除结果 |
|
||||||
|
| `mms_m_server_cb` | `void(int, const char*, const char*, const char*, int, int, int)` | 服务器信息 |
|
||||||
|
| `mms_m_sg_cb` | `void(int, const char*, int act_sg, int num_sg, int err)` | 定值组信息 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 错误码(mms_m_errstr.cpp)
|
||||||
|
|
||||||
|
提供四种枚举值到字符串的映射:
|
||||||
|
|
||||||
|
| 函数 | 映射 |
|
||||||
|
|------|------|
|
||||||
|
| `mms_m_err_str(IedClientError)` | IED 客户端错误(30 个映射) |
|
||||||
|
| `mms_m_control_err_str(ControlLastApplError)` | 控制应用错误(4 个映射) |
|
||||||
|
| `mms_m_ctl_add_cause_str(ControlAddCause)` | 控制附加原因(28 个映射) |
|
||||||
|
| `mms_m_ctrl_model_str(ControlModel)` | 控制模型(5 个映射) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 文件服务(mms_m_file.cpp)
|
||||||
|
|
||||||
|
### 实现状态
|
||||||
|
|
||||||
|
| 功能 | 状态 | 使用的 API |
|
||||||
|
|------|------|-----------|
|
||||||
|
| 文件打开 | 已实现 | `MmsConnection_fileOpen()` |
|
||||||
|
| 分块异步读取 | 未完成(TODO) | 待实现 `MmsConnection_fileReadAsync()` |
|
||||||
|
| 文件删除 | 已实现 | `MmsConnection_fileDelete()` |
|
||||||
|
|
||||||
|
### 调用方式
|
||||||
|
|
||||||
|
所有文件操作通过 `IedConnection_getMmsConnection()` 获取底层 `MmsConnection` 句柄,然后调用 MMS 低层文件 API。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 设计要点
|
||||||
|
|
||||||
|
1. **事件驱动**:所有对外操作通过事件队列异步执行,避免阻塞调用线程
|
||||||
|
2. **自动重连**:连接断开后自动重试连接,连接恢复后重新初始化报告
|
||||||
|
3. **数据结构三棵树**:
|
||||||
|
- **ldevs**:按 LD→LN→DO 组织的信号点树(用于信号匹配输出)
|
||||||
|
- **ld_datasets/ln_rpts**:数据集和 RCB 树(用于报告接收和 GI 触发)
|
||||||
|
- **g_mms_m_obj_map**:多客户端实例管理
|
||||||
|
4. **定值区自动跟踪**:通过绑定 zone_saddr,自动检测定值区变化并重读参数
|
||||||
|
5. **调试标志**:全局 `debug_print_flag` 控制日志输出详细程度
|
||||||
|
6. **RCB 激活配置缺陷**:
|
||||||
|
- `setRCBValues` 的 `parametersMask` 包含 `RCB_ELEMENT_INTG_PD` 但本地未调用 `setIntgPd()`,写入的是从服务端读回的旧值
|
||||||
|
- `RCB_ELEMENT_OPT_FLDS` 未包含在 mask 中,OptFlds 完全依赖服务端默认值,可能导致报告缺少 `DataReference`、`ConfRev`、`BufferOverflow` 等字段
|
||||||
|
- `mms_m_icd_report_init` 只取编号 `"01"` 的 RCB,其他编号直接丢弃
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
# 问题处理文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2026-06-10
|
||||||
|
|
||||||
|
### #1 libmms_m RCB 订阅编号硬编码
|
||||||
|
|
||||||
|
**问题**:`mms_m_icd_report_init()` 中硬编码 `if(0 == rpt_no.compare("01"))`,只订阅编号为 `"01"` 的 RCB,其他编号被丢弃,无法按需灵活订阅多个 RCB 实例。
|
||||||
|
|
||||||
|
**需求**:
|
||||||
|
1. 灵活可配置订阅的控制块编号
|
||||||
|
2. libmms_m 提供接口,由 libiec61850m 传入
|
||||||
|
3. 可传入一个或多个编号,不传默认 `"01"`
|
||||||
|
4. 无效数据打印错误并返回失败
|
||||||
|
|
||||||
|
**处理计划**:[RCB订阅编号可配置化](./mid/RCB订阅编号可配置化.md)
|
||||||
|
|
||||||
|
**状态**:✅ 已完成
|
||||||
|
**涉及文件**:`myMms_m.h`, `mms_m.h`, `mms_m.cpp`, `iec61850m.cpp`
|
||||||
|
|
||||||
|
---
|
||||||
Loading…
Reference in New Issue