Skip to content

IEC61850 报告 GI 与 DataSet 读取修复 ​

版本: 1.0
日期: 2026-06-22
状态: 已实施

1. 问题背景 ​

在 IEC61850 Reports 功能联调中,LLN0 下多个 BRCB/URCB 报告控制块启用后,先后暴露出以下问题:

现象触发场景影响
BRCB 触发 GI 后回调路由到第一个报告对 brcbDin03 触发总召唤,日志仍显示 brcbDin01前端请求不到目标报告的数据
多报告启用后重复订阅异常同一 LLN0 下多个报告模块启用subscriber is already registered,严重时进程崩溃
GI 回调持续触发总召唤后回调被反复分发缓存与前端数据刷新异常
URCB 触发 GI 后进程卡死对 URCB 写 GI=True 或调用同步 setRCBValues后端进程阻塞
URCB 软件 GI 初始读取 DataSet 为空通过 DataSet 读取当前值写入缓存GI 接口返回 500
逐点读取 fallback 可用但效率低DataSet 批量读取不可用后逐个 readObject大 DataSet 总召唤耗时明显增加

本次修复的目标是让 BRCB/URCB 在多报告场景下稳定启用、准确路由 GI 数据,并让 URCB 在不能安全写 GI=True 时仍可通过 DataSet 批量读取完成软件总召唤。

2. 根因分析 ​

2.1 RptId 与 RCB 引用不是一一对应 ​

同一 LLN0 下多个报告控制块可能共享基础 RptId,例如:

text
PCS001LD0/LLN0.brcbDin01 -> LD0/LLN0$BR$brcbDin
PCS001LD0/LLN0.brcbDin02 -> LD0/LLN0$BR$brcbDin
PCS001LD0/LLN0.brcbDin03 -> LD0/LLN0$BR$brcbDin

01/02/03 后缀用于区分多个 RCB 实例,但报告回调中的 RptId 可能不携带该实例后缀。只按 RptId 建立回调映射时,GI 数据容易落到第一个注册的报告缓存中。

2.2 URCB GI 写入路径存在阻塞风险 ​

BRCB 触发 GI 可通过 RCB 对象写回完成;但 URCB 在当前 pyiec61850 绑定与部分设备实现下,直接写 GI 属性或同步 setRCBValues 存在阻塞/卡死风险。为避免进程挂死,URCB 总召唤需要避开危险写路径。

2.3 IedConnection_readDataSetValues 的 Python 绑定不可直接使用 ​

排查发现当前 pyiec61850 中:

python
IedConnection_readDataSetValues(_self, dataSetReference, dataSet)

第三个参数要求传入非空 ClientDataSet,但 Python 包没有暴露可用的 ClientDataSet_create。传 None 会报:

text
ClientDataSet is NULL

因此不能依赖 IedConnection_readDataSetValues 作为 URCB 软件 GI 的主要读取路径。

2.4 DataSet directory 为空时值被误丢弃 ​

旧逻辑按下面方式遍历 DataSet 值:

python
for i in range(min(array_size, len(members))):
    ...

如果设备返回了 DataSet 值数组,但 browse_dataset_directory() 暂时未拿到成员目录,len(members)=0 会导致循环 0 次,最终误判为 DataSet 为空。

3. 修复方案 ​

3.1 GI 路由增加 pending target ​

在触发 GI 前记录目标 RCB:

text
target = PCS001LD0/LLN0.brcbDin03
rpt_id = LD0/LLN0$BR$brcbDin

回调分发时,如果报告 RptId 与 pending GI 记录匹配,则优先写入触发 GI 的目标 RCB 缓存,而不是默认落到第一个同 RptId 的报告模块。

3.2 保留实例后缀作为缓存归属 ​

urcbAin01、brcbDin03 这样的实例后缀应保留在 RCB 引用中,用于:

  • 区分同一 LLN0 下多个报告实例。
  • 作为前端请求报告数据的缓存 key。
  • 保证 GI 结果写入用户实际触发的报告模块。

读取当前值时不通过该 RCB ref 读取,而是通过 RCB 绑定的 data_set_ref 读取。

3.3 URCB 改为软件 GI ​

URCB 不再强依赖写 GI=True,而是执行软件总召唤:

  1. 根据 RCB 明细取出 data_set_ref。
  2. 读取 DataSet 当前值。
  3. 构造 ReportDataEntry。
  4. 直接追加到对应 RCB 的报告缓存。
  5. 前端继续通过原有 /reports/data 接口读取缓存。

这样避免了 URCB 写 GI 或同步 setRCBValues 导致的进程卡死。

3.4 DataSet 优先使用 MMS NamedVariableList 批量读取 ​

为解决逐点读取效率问题,新增 MMS 层批量读取路径:

python
mms_conn = iec61850.IedConnection_getMmsConnection(conn)
mms_error = iec61850.MmsError_create()
values = iec61850.MmsConnection_readNamedVariableListValues(
    mms_conn,
    mms_error,
    domain_id,
    item_id,
    False,
)

DataSet 引用转换规则:

输入domain_iditem_id
PCS001MEAS/LLN0$dsAinPCS001MEASLLN0$dsAin
PCS001MEAS/LLN0.dsAinPCS001MEASLLN0$dsAin

MMS 批量读取成功后,只需一次请求即可取回整个 DataSet 值数组,再按 DataSet 成员顺序映射成 {fcda_ref: value}。

3.5 保留逐点读取 fallback ​

当 MMS 批量读取不可用或设备返回错误时,保留逐成员 IedConnection_readObject fallback:

text
MMS DataSet read failed
  -> IedConnection_readDataSetValues 兼容尝试
  -> DataSet directory + readObject 逐点读取

这样既保证常规场景性能,也保证异常设备或绑定差异下功能可用。

3.6 DataSet 成员目录缓存 ​

DataSet 成员目录用于把数组下标映射回 FCDA 引用。为减少重复 GI 时的额外 MMS 请求,读取时增加成员缓存:

  • 首次读取时调用 browse_dataset_directory()。
  • 成功获取成员后按完整 DataSet ref 缓存。
  • 后续 GI 复用成员列表,只读取 DataSet 值数组。
  • 断开或关闭插件时清理缓存。

4. 关键文件 ​

文件变更
src/proto/iec61850/plugins/reports/callback.py增加 GI pending 路由、RptId 别名匹配、缓存追加接口
src/proto/iec61850/plugins/reports/__init__.pyURCB 触发 GI 改为软件 GI,按 DataSet 读取后写入报告缓存
src/proto/iec61850/plugins/reports/brcb.pyBRCB GI 触发前记录 pending 路由,避免同 RptId 多实例串线
src/proto/iec61850/plugins/reports/urcb.py避免 URCB 同步写 GI 导致进程卡死,保留必要兼容逻辑
src/proto/iec61850/plugins/datasets/__init__.py新增 MMS NamedVariableList 批量读取、成员缓存、逐点 fallback

5. 修复后的行为 ​

BRCB ​

  • 多个 BRCB 可同时启用。
  • 对 brcbDin03 触发 GI 后,数据写入 brcbDin03 缓存。
  • 前端按目标 rcb_ref 请求可拿到对应数据。

URCB ​

  • 启用报告仍安装正常报告回调。
  • 触发 GI 时优先执行软件 GI,不再阻塞进程。
  • DataSet 值优先通过 MMS NamedVariableList 一次性读取。
  • 读取成功后写入同一个报告缓存通道,前端无须区分真实回调或软件 GI。

DataSet 读取 ​

  • 首选路径:MmsConnection_readNamedVariableListValues 一次读完整 DataSet。
  • 兼容路径:尝试 IedConnection_readDataSetValues 多种签名。
  • 兜底路径:DataSet directory + IedConnection_readObject 逐点读。
  • 当成员目录为空但值数组存在时,使用 data[0]、data[1] 兜底 key,避免误判整包为空。

6. 验证要点 ​

6.1 BRCB 多实例 GI 路由 ​

请求:

json
{
  "channel_id": 12,
  "rcb_ref": "PCS001LD0/LLN0.brcbDin03"
}

期望日志:

text
GI 待路由已记录: target=PCS001LD0/LLN0.brcbDin03

前端请求 brcbDin03 的报告数据,应能读取到本次 GI 写入的缓存。

6.2 URCB 软件 GI ​

请求:

json
{
  "channel_id": 12,
  "rcb_ref": "PCS001MEAS/LLN0.urcbAin01"
}

期望日志:

text
MMS DataSet read succeeded: ref=PCS001MEAS/LLN0$dsAin, values=...
URCB 软件 GI 已写入缓存: ref=PCS001MEAS/LLN0.urcbAin01

6.3 fallback 验证 ​

当 MMS 批量读取失败时,日志应进入 fallback:

text
MMS DataSet read failed: ...
Read DataSet values fallback succeeded: ref=..., values=...

如果 fallback 仍失败,应根据日志继续定位:

日志含义
fallback failed: no membersDataSet directory 读取失败或设备未暴露成员目录
fallback got no values成员可见,但逐点 readObject 全部失败
MMS DataSet read failed ... error=...MMS NamedVariableList 读取被设备拒绝或引用格式不匹配

7. 风险与注意事项 ​

  1. 软件 GI 与真实设备 GI 语义不同
    软件 GI 是读取当前 DataSet 值后写入本地缓存,不会要求设备主动发送一帧 GI 报告。对前端展示和数据刷新足够,但不等同于设备侧真实 GI 流程。

  2. DataSet 成员顺序必须稳定
    MMS 返回值数组按 DataSet 成员顺序排列。成员缓存假设同一连接生命周期内 DataSet 结构不变。如果设备动态修改 DataSet,应断开重连或重新发现。

  3. 同 RptId 多实例仍依赖 pending GI 路由
    普通周期/变化报告若设备只给共享 RptId,仍可能无法仅凭 RptId 区分实例;本次修复主要保证主动触发 GI 的数据归属准确。

  4. MmsErrror_destroy 拼写保持兼容
    当前 pyiec61850 暴露的析构函数名为 MmsErrror_destroy,代码优先使用该拼写,并兼容未来可能存在的 MmsError_destroy。

8. 总结 ​

本次修复把 IEC61850 报告总召唤从“依赖设备回调和不稳定 SWIG 绑定”调整为“明确路由 + 安全软件 GI + MMS 批量读取”的组合:

  • 多报告实例通过 pending GI 准确写入目标缓存。
  • URCB 避免写 GI 和同步 setRCBValues 引发进程卡死。
  • DataSet 读取优先走 MMS NamedVariableList,一次请求取回完整值数组。
  • 保留逐点读取 fallback,保证设备兼容性。

修复后,BRCB 和 URCB 在多报告场景下都可以稳定完成总召唤,前端按目标 RCB 请求数据时能够获取正确缓存。

Released under the Apache 2.0 License.