From e18f239d77ca9087a0ea60abbc1886dfc093a066 Mon Sep 17 00:00:00 2001 From: ypc <15051963820@163.com> Date: Wed, 10 Jun 2026 10:44:51 +0800 Subject: [PATCH] =?UTF-8?q?<=E6=96=B0=E5=A2=9E>=201=E3=80=81=E5=B0=86claud?= =?UTF-8?q?e=E7=94=9F=E6=88=90=E7=9A=84=E6=96=87=E4=BB=B6=E4=BF=9D?= =?UTF-8?q?=E7=95=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- claude/mid/RCB订阅编号可配置化.md | 88 ++ .../工程/libiec61850_MMS客户端API开发手册.md | 1207 +++++++++++++++ .../工程/libiec61850_MMS服务端API开发手册.md | 1294 +++++++++++++++++ claude/工程/libiec61850m模块分析.md | 354 +++++ claude/工程/libmms_m模块分析.md | 671 +++++++++ claude/问题处理文档.md | 22 + 6 files changed, 3636 insertions(+) create mode 100644 claude/mid/RCB订阅编号可配置化.md create mode 100644 claude/工程/libiec61850_MMS客户端API开发手册.md create mode 100644 claude/工程/libiec61850_MMS服务端API开发手册.md create mode 100644 claude/工程/libiec61850m模块分析.md create mode 100644 claude/工程/libmms_m模块分析.md create mode 100644 claude/问题处理文档.md diff --git a/claude/mid/RCB订阅编号可配置化.md b/claude/mid/RCB订阅编号可配置化.md new file mode 100644 index 0000000..fdb4d0a --- /dev/null +++ b/claude/mid/RCB订阅编号可配置化.md @@ -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 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 ` 头文件 + - 新增 `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 rcb_numbers` | +| `mms_m.cpp` | 7 | 新增 `#include ` | +| `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")` | diff --git a/claude/工程/libiec61850_MMS客户端API开发手册.md b/claude/工程/libiec61850_MMS客户端API开发手册.md new file mode 100644 index 0000000..85288c7 --- /dev/null +++ b/claude/工程/libiec61850_MMS客户端API开发手册.md @@ -0,0 +1,1207 @@ +# libiec61850-1.5.3 MMS 客户端 API 开发手册 + +> 本文档基于 libiec61850 v1.5.3 源码,覆盖 MMS 客户端全部公开 C API,达到开发者脱离源码即可进行项目开发的标准。 + +--- + +## 目录 + +1. [架构概述](#1-架构概述) +2. [核心数据类型](#2-核心数据类型) +3. [连接管理](#3-连接管理) +4. [数据读写](#4-数据读写) +5. [报告服务(Report)](#5-报告服务report) +6. [控制服务(Control)](#6-控制服务control) +7. [数据集服务(DataSet)](#7-数据集服务dataset) +8. [文件服务(File)](#8-文件服务file) +9. [日志服务(Log)](#9-日志服务log) +10. [模型发现服务](#10-模型发现服务) +11. [SV/GOOSE 控制块处理](#11-svgoose-控制块处理) +12. [MSO 传输层访问](#12-mms低层传输层访问) +13. [错误码完整参考](#13-错误码完整参考) +14. [运行模式:线程模式 vs 非线程模式](#14-运行模式线程模式-vs-非线程模式) +15. [与 RTU 项目对照](#15-与-rtu-项目对照) + +--- + +## 1. 架构概述 + +libiec61850 客户端 API 提供两个抽象层级: + +| 层级 | API 头文件 | 描述 | +|------|-----------|------| +| **高层 IEC 61850** | `iec61850_client.h` | 封装 IEC 61850 语义(FC, LD/LN/DO/DA),推荐使用 | +| **底层 MMS** | `mms_client_connection.h` | 直接操作 MMS 协议 domain/item,一般不需要直接使用 | + +客户端核心不透明句柄为 `IedConnection`,所有操作围绕它展开。 + +### 协议栈层次 + +``` +┌─────────────────────────────────────────────────────┐ +│ 高层 IEC 61850 API (iec61850_client.h) │ +│ IedConnection / ClientReport / ControlObjectClient │ +├─────────────────────────────────────────────────────┤ +│ 底层 MMS API (mms_client_connection.h) │ +│ MmsConnection / MmsValue │ +├─────────────────────────────────────────────────────┤ +│ ISO 传输层 (ACSE + Presentation + Session) │ +├─────────────────────────────────────────────────────┤ +│ ASN.1 BER 编解码 + TCP/IP │ +└─────────────────────────────────────────────────────┘ +``` + +### 两类运行模式 + +| 模式 | 创建方式 | 消息处理 | 适用场景 | +|------|---------|---------|---------| +| **线程模式**(默认) | `IedConnection_create()` | 库内部后台线程自动处理 | 大多数场景 | +| **非线程模式** | `IedConnection_createEx(tlsConfig, false)` | 用户周期性调用 `IedConnection_tick()` | 需要精确控制线程的嵌入式系统 | + +在非线程模式下,禁止使用同步(阻塞)API,必须使用 `*Async` 异步版本。 + +--- + +## 2. 核心数据类型 + +### 2.1 主要不透明句柄 + +```c +typedef struct sIedConnection* IedConnection; // 客户端连接 +typedef struct sClientReportControlBlock* ClientReportControlBlock; // RCB 本地对象 +typedef struct sClientReport* ClientReport; // 接收到的报告 +typedef struct sClientDataSet* ClientDataSet; // 数据集本地对象 +typedef struct sControlObjectClient* ControlObjectClient; // 控制对象 +typedef struct sClientSVControlBlock* ClientSVControlBlock; // SV控制块 +typedef struct sClientGooseControlBlock* ClientGooseControlBlock; // GOOSE控制块 +``` + +### 2.2 连接状态 + +```c +typedef enum { + IED_STATE_CLOSED = 0, // 空闲/关闭 + IED_STATE_CONNECTING = 1, // 连接中 + IED_STATE_CONNECTED = 2, // 已连接 + IED_STATE_CLOSING = 3 // 关闭中 +} IedConnectionState; +``` + +### 2.3 功能约束(FunctionalConstraint) + +客户端读写数据时,**必须指定 FC**: + +| FC 枚举 | 值 | 含义 | 典型使用 | +|---------|----|------|---------| +| `IEC61850_FC_ST` | 0 | 状态信息 | 读遥信状态 | +| `IEC61850_FC_MX` | 1 | 测量值 | 读遥测值 | +| `IEC61850_FC_SP` | 2 | 设定值 | 读设定点 | +| `IEC61850_FC_SV` | 3 | 替代值 | 读替代值 | +| `IEC61850_FC_CF` | 4 | 配置 | 读配置 | +| `IEC61850_FC_DC` | 5 | 描述 | 读描述 | +| `IEC61850_FC_SG` | 6 | 定值组 | 读定值组活跃值 | +| `IEC61850_FC_SE` | 7 | 可编辑定值组 | 读/写编辑定值组 | +| `IEC61850_FC_CO` | 12 | 控制 | 写控制命令 | +| `IEC61850_FC_RP` | 15 | 非缓存报告 | RCB 操作 | +| `IEC61850_FC_BR` | 16 | 缓存报告 | BRCB 操作 | + +**引用格式**:`"LD名/LN名.DO名.DA名"`,如 `"simpleIOGenericIO/GGIO1.ST.Ind1.stVal"`。 + +### 2.4 控制模型 + +```c +typedef enum { + CONTROL_MODEL_STATUS_ONLY = 0, // 只读,不支持控制 + CONTROL_MODEL_DIRECT_NORMAL = 1, // 直控-普通安全 + CONTROL_MODEL_SBO_NORMAL = 2, // 选控-普通安全(需Select再Operate) + CONTROL_MODEL_DIRECT_ENHANCED = 3,// 直控-增强安全(含CommandTermination) + CONTROL_MODEL_SBO_ENHANCED = 4 // 选控-增强安全(SelectWithValue再Operate) +} ControlModel; +``` + +### 2.5 Quality 质量 + +```c +typedef uint16_t Quality; + +// 有效性(低2位) +#define QUALITY_VALIDITY_GOOD 0 +#define QUALITY_VALIDITY_INVALID 2 +#define QUALITY_VALIDITY_QUESTIONABLE 3 + +// 详情标志 +#define QUALITY_DETAIL_OVERFLOW 4 // 溢出 +#define QUALITY_DETAIL_OUT_OF_RANGE 8 // 超量程 +#define QUALITY_DETAIL_BAD_REFERENCE 16 // 坏基准值 +#define QUALITY_DETAIL_OSCILLATORY 32 // 振荡 +#define QUALITY_DETAIL_FAILURE 64 // 故障 +#define QUALITY_DETAIL_OLD_DATA 128 // 旧数据 +#define QUALITY_DETAIL_INCONSISTENT 256 // 不一致 +#define QUALITY_DETAIL_INACCURATE 512 // 不精确 +#define QUALITY_SOURCE_SUBSTITUTED 1024 // 替代值 +#define QUALITY_TEST 2048 // 测试 +#define QUALITY_OPERATOR_BLOCKED 4096 // 操作员闭锁 + +// Quality 操作函数 +Validity Quality_getValidity(Quality* self); +void Quality_setValidity(Quality* self, Validity validity); +void Quality_setFlag(Quality* self, int flag); +void Quality_unsetFlag(Quality* self, int flag); +bool Quality_isFlagSet(Quality* self, int flag); +Quality Quality_fromMmsValue(const MmsValue* mmsValue); +MmsValue* Quality_toMmsValue(Quality* self, MmsValue* mmsValue); +``` + +### 2.6 Timestamp + +```c +typedef union { + uint8_t val[8]; +} Timestamp; + +Timestamp* Timestamp_create(void); +Timestamp* Timestamp_createFromByteArray(const uint8_t* byteArray); +void Timestamp_destroy(Timestamp* self); +void Timestamp_clearFlags(Timestamp* self); +uint32_t Timestamp_getTimeInSeconds(Timestamp* self); // 秒级时间戳 +uint64_t Timestamp_getTimeInMs(Timestamp* self); // 毫秒级时间戳 +uint64_t Timestamp_getTimeInNs(Timestamp* self); // 纳秒级时间戳 +bool Timestamp_isLeapSecondKnown(Timestamp* self); +void Timestamp_setLeapSecondKnown(Timestamp* self, bool value); +bool Timestamp_hasClockFailure(Timestamp* self); +void Timestamp_setClockFailure(Timestamp* self, bool value); +bool Timestamp_isClockNotSynchronized(Timestamp* self); +void Timestamp_setClockNotSynchronized(Timestamp* self, bool value); +int Timestamp_getSubsecondPrecision(Timestamp* self); +void Timestamp_setSubsecondPrecision(Timestamp* self, int subsecondPrecision); +void Timestamp_setTimeInSeconds(Timestamp* self, uint32_t secondsSinceEpoch); +void Timestamp_setTimeInMilliseconds(Timestamp* self, uint64_t msTime); +void Timestamp_setTimeInNanoseconds(Timestamp* self, uint64_t nsTime); +void Timestamp_setByMmsUtcTime(Timestamp* self, const MmsValue* mmsValue); +MmsValue* Timestamp_toMmsValue(Timestamp* self, MmsValue* mmsValue); +Timestamp* Timestamp_fromMmsValue(Timestamp* self, MmsValue* mmsValue); +``` + +### 2.7 回调函数类型速查 + +```c +// 通用服务回调(写操作完成等) +typedef void (*IedConnection_GenericServiceHandler)(uint32_t invokeId, void* parameter, IedClientError err); + +// 连接状态变化回调 +typedef void (*IedConnection_StateChangedHandler)(void* parameter, IedConnection connection, IedConnectionState newState); + +// 读对象回调 +typedef void (*IedConnection_ReadObjectHandler)(uint32_t invokeId, void* parameter, IedClientError err, MmsValue* value); + +// 报告接收回调 +typedef void (*ReportCallbackFunction)(void* parameter, ClientReport report); + +// 控制操作完成回调 +typedef void (*ControlObjectClient_ControlActionHandler)(uint32_t invokeId, void* parameter, IedClientError err, ControlActionType type, bool success); + +// 命令终止回调 +typedef void (*CommandTerminationHandler)(void* parameter, ControlObjectClient controlClient); + +// 文件下载数据回调 +typedef bool (*IedClientGetFileHandler)(void* parameter, uint8_t* buffer, uint32_t bytesRead); +``` + +### 2.8 MmsValue 基础操作 + +`MmsValue` 是所有客户端数据交换的统一容器类型。每个 `MmsValue` 有一个 `MmsType`: + +```c +typedef enum { + MMS_ARRAY = 0, // 数组 + MMS_STRUCTURE = 1, // 结构体 + MMS_BOOLEAN = 2, // 布尔 + MMS_BIT_STRING = 3, // 位串 + MMS_INTEGER = 4, // 有符号整数 + MMS_UNSIGNED = 5, // 无符号整数 + MMS_FLOAT = 6, // 浮点数 + MMS_OCTET_STRING = 7, // 字节串 + MMS_VISIBLE_STRING = 8, // 可见字符串 + MMS_STRING = 9, // MMS 字符串 + MMS_UTC_TIME = 10, // UTC 时间 + MMS_DATA_ACCESS_ERROR = 11, +} MmsType; +``` + +**构造**(仅在需要主动创建值时使用,读取操作由库自动创建): + +```c +MmsValue* MmsValue_newBoolean(bool boolean); +MmsValue* MmsValue_newFloat(float value); +MmsValue* MmsValue_newDouble(double value); +MmsValue* MmsValue_newIntegerFromInt32(int32_t integer); +MmsValue* MmsValue_newIntegerFromInt64(int64_t integer); +MmsValue* MmsValue_newUnsignedFromUint32(uint32_t integer); +MmsValue* MmsValue_newVisibleString(const char* string); +MmsValue* MmsValue_newUtcTime(uint32_t timeval); // 秒级时间戳 +MmsValue* MmsValue_newUtcTimeByMsTime(uint64_t timeval); // 毫秒级时间戳 +MmsValue* MmsValue_newOctetString(int size, int maxSize); +MmsValue* MmsValue_newBitString(int bitSize); +MmsValue* MmsValue_newStructure(const MmsVariableSpecification* typeSpec); +MmsValue* MmsValue_createEmptyStructure(int size); +MmsValue* MmsValue_createEmptyArray(int size); +``` + +**取值**: + +```c +bool MmsValue_getBoolean(const MmsValue* value); +float MmsValue_toFloat(const MmsValue* self); +double MmsValue_toDouble(const MmsValue* self); +int32_t MmsValue_toInt32(const MmsValue* value); +int64_t MmsValue_toInt64(const MmsValue* self); +uint32_t MmsValue_toUint32(const MmsValue* value); +uint64_t MmsValue_getUtcTimeInMs(const MmsValue* value); +uint64_t MmsValue_getUtcTimeInMsWithUs(const MmsValue* self, uint32_t* usec); +uint32_t MmsValue_toUnixTimestamp(const MmsValue* self); +const char* MmsValue_toString(MmsValue* self); // 对 VISIBLE_STRING/STRING 类型 + +// 数组/结构体 +MmsValue* MmsValue_getElement(const MmsValue* array, int index); +uint32_t MmsValue_getArraySize(const MmsValue* self); + +// 位串 +bool MmsValue_getBitStringBit(const MmsValue* self, int bitPos); +int MmsValue_getBitStringSize(const MmsValue* self); +uint32_t MmsValue_getBitStringAsInteger(const MmsValue* self); +uint32_t MmsValue_getBitStringAsIntegerBigEndian(const MmsValue* self); + +// 字节串 +uint16_t MmsValue_getOctetStringSize(const MmsValue* self); +uint8_t* MmsValue_getOctetStringBuffer(MmsValue* self); +uint8_t MmsValue_getOctetStringOctet(MmsValue* self, int octetPos); +``` + +**赋值**: + +```c +void MmsValue_setBoolean(MmsValue* value, bool boolValue); +void MmsValue_setFloat(MmsValue* self, float newFloatValue); +void MmsValue_setDouble(MmsValue* self, double newFloatValue); +void MmsValue_setInt32(MmsValue* self, int32_t integer); +void MmsValue_setInt64(MmsValue* value, int64_t integer); +void MmsValue_setUint32(MmsValue* value, uint32_t integer); +void MmsValue_setVisibleString(MmsValue* self, const char* string); +MmsValue* MmsValue_setUtcTime(MmsValue* self, uint32_t timeval); // 秒 +MmsValue* MmsValue_setUtcTimeMs(MmsValue* self, uint64_t timeval); // 毫秒 +void MmsValue_setOctetString(MmsValue* self, const uint8_t* buf, int size); +void MmsValue_setBitStringBit(MmsValue* self, int bitPos, bool value); +void MmsValue_setElement(MmsValue* complexValue, int index, MmsValue* elementValue); +``` + +**生命周期**: + +```c +MmsValue* MmsValue_clone(const MmsValue* self); // 深拷贝,调用者负责释放 +void MmsValue_delete(MmsValue* self); // 递归删除所有子元素 +bool MmsValue_update(MmsValue* self, const MmsValue* source); +bool MmsValue_equals(const MmsValue* self, const MmsValue* otherValue); +MmsType MmsValue_getType(const MmsValue* self); +``` + +--- + +## 3. 连接管理 + +### 3.1 完整连接生命周期 + +```c +// 步骤1:创建连接 +IedConnection conn = IedConnection_create(); +// 或高级版本:IedConnection_createEx(tlsConfig, useThreads); + +// 步骤2(可选):设置参数 +IedConnection_setLocalAddress(conn, "0.0.0.0", 0); // 本地绑定地址和端口,0=自动分配 +IedConnection_setConnectTimeout(conn, 5000); // 连接超时(ms),须在connect前调用 +IedConnection_setRequestTimeout(conn, 3000); // 请求超时(ms),可随时调用 +IedConnection_setTimeQuality(conn, true, false, false, 10); // 时间品质 + +// 步骤3:连接服务器(阻塞版本) +IedClientError err; +IedConnection_connect(conn, &err, "192.168.1.100", 102); +if (err != IED_ERROR_OK) { /* 处理错误 */ } + +// 步骤3':连接服务器(异步版本) +IedConnection_connectAsync(conn, &err, "192.168.1.100", 102); +// 轮询 IedConnection_getState(conn) 或安装状态回调来等待连接完成 + +// 步骤4:安装连接状态回调(可选) +void stateHandler(void* param, IedConnection connection, IedConnectionState newState) { + switch (newState) { + case IED_STATE_CONNECTED: /* 连接成功 */ break; + case IED_STATE_CLOSED: /* 连接关闭 */ break; + // ... + } +} +IedConnection_installStateChangedHandler(conn, stateHandler, NULL); + +// 步骤5:进行业务操作... + +// 步骤6:关闭连接(三选一) +IedConnection_close(conn); // 直接关闭 TCP(最常用) +IedConnection_abort(conn, &err); // 发送 ACSE Abort 后关闭 +IedConnection_release(conn, &err); // 发送 MMS Conclude(优雅关闭) + +// 步骤7:释放资源 +IedConnection_destroy(conn); +``` + +### 3.2 连接状态查询 + +```c +IedConnectionState IedConnection_getState(IedConnection self); + +// 状态流转: +// CLOSED → connect() → CONNECTING → 连接成功 → CONNECTED +// CONNECTED → close()/abort() → CLOSING → TCP断开 → CLOSED +``` + +### 3.3 连接参数 + +| API | 说明 | +|-----|------| +| `IedConnection_setLocalAddress(conn, ip, port)` | 绑定本地地址(可选,不调用由OS自动分配) | +| `IedConnection_setConnectTimeout(conn, ms)` | 连接超时,须在connect前调用 | +| `IedConnection_setRequestTimeout(conn, ms)` | 请求超时,可随时调整 | +| `IedConnection_getRequestTimeout(conn)` | 获取当前请求超时值 | +| `IedConnection_setTimeQuality(conn, a,b,c,d)` | 设置本连接生成的所有时间戳的品质 | + +### 3.4 获取底层连接 + +```c +MmsConnection IedConnection_getMmsConnection(IedConnection self); +``` +返回底层 `MmsConnection` 句柄,可用于直接调用低层 MMS API。 + +--- + +## 4. 数据读写 + +### 4.1 通用读写(通过 FCDA 引用 + FC) + +```c +// 同步读:返回 MmsValue*,失败返回 NULL +MmsValue* val = IedConnection_readObject(conn, &err, + "simpleIOGenericIO/GGIO1.ST.Ind1.stVal", IEC61850_FC_ST); +if (val) { + float f = MmsValue_toFloat(val); + // 注意:不要手动 delete val,生命周期由库管理 +} + +// 同步写 +MmsValue* writeVal = MmsValue_newBoolean(true); +IedConnection_writeObject(conn, &err, + "simpleIOGenericIO/GGIO1.SP.Pos1.ctlVal", IEC61850_FC_CO, writeVal); +MmsValue_delete(writeVal); + +// 异步读 +void readHandler(uint32_t invokeId, void* param, IedClientError err, MmsValue* value) { + if (err == IED_ERROR_OK) { /* 使用 value */ } +} +uint32_t id = IedConnection_readObjectAsync(conn, &err, "ref", IEC61850_FC_ST, + readHandler, myParam); + +// 异步写 +uint32_t id = IedConnection_writeObjectAsync(conn, &err, "ref", IEC61850_FC_CO, + value, genericHandler, myParam); +``` + +### 4.2 便捷类型读写(推荐用于简单类型) + +**读**: + +```c +bool v = IedConnection_readBooleanValue(conn, &err, "ref", IEC61850_FC_ST); +float v = IedConnection_readFloatValue(conn, &err, "ref", IEC61850_FC_MX); +int32_t v = IedConnection_readInt32Value(conn, &err, "ref", IEC61850_FC_ST); +int64_t v = IedConnection_readInt64Value(conn, &err, "ref", IEC61850_FC_ST); +uint32_t v = IedConnection_readUnsigned32Value(conn, &err, "ref", IEC61850_FC_ST); +char* v = IedConnection_readStringValue(conn, &err, "ref", IEC61850_FC_CF); // 需手动 free! +Quality v = IedConnection_readQualityValue(conn, &err, "ref", IEC61850_FC_ST); +Timestamp* v = IedConnection_readTimestampValue(conn, &err, "ref", IEC61850_FC_ST, NULL); // 需手动 free! +``` + +**注意**:`readStringValue` 返回的 `char*` 由库动态分配,调用者必须手动 `free()`。 + +`readTimestampValue` 如果传入 `NULL` 则库分配新 Timestamp 对象,调用者负责 `Timestamp_destroy()`;如果传入已有的 Timestamp 指针则复用。 + +**写**: + +```c +IedConnection_writeBooleanValue(conn, &err, "ref", IEC61850_FC_CO, true); +IedConnection_writeFloatValue(conn, &err, "ref", IEC61850_FC_CO, 1.5f); +IedConnection_writeInt32Value(conn, &err, "ref", IEC61850_FC_CO, 42); +IedConnection_writeUnsigned32Value(conn, &err, "ref", IEC61850_FC_CO, 100); +IedConnection_writeVisibleStringValue(conn, &err, "ref", IEC61850_FC_CF, "hello"); +IedConnection_writeOctetString(conn, &err, "ref", IEC61850_FC_CF, data, len); +``` + +--- + +## 5. 报告服务(Report) + +报告是 IEC 61850 客户端接收服务端主动上送数据的机制。 + +### 5.1 RCB 类型 + +| 类型 | 引用名特征 | 行为 | +|------|-----------|------| +| **URCB** (Unbuffered) | 含 `RP`(如 `LLN0.RP.EventsRCB01`) | 不缓存,断连后丢失 | +| **BRCB** (Buffered) | 含 `BR`(如 `LLN0.BR.EventsBRCB01`) | 缓存报告,断连后可重传 | + +### 5.2 RCB 元素掩码(setRCBValues 时指定要写入哪些字段) + +```c +#define RCB_ELEMENT_RPT_ID 1 // 报告ID +#define RCB_ELEMENT_RPT_ENA 2 // 报告使能 +#define RCB_ELEMENT_RESV 4 // 预留(仅URCB) +#define RCB_ELEMENT_DATSET 8 // 数据集 +#define RCB_ELEMENT_CONF_REV 16 // 配置版本 +#define RCB_ELEMENT_OPT_FLDS 32 // 选项字段 +#define RCB_ELEMENT_BUF_TM 64 // 缓冲时间 +#define RCB_ELEMENT_SQ_NUM 128 // 序列号 +#define RCB_ELEMENT_TRG_OPS 256 // 触发选项 +#define RCB_ELEMENT_INTG_PD 512 // 完整性周期 +#define RCB_ELEMENT_GI 1024 // 总召 +#define RCB_ELEMENT_PURGE_BUF 2048 // 清除缓冲区(仅BRCB) +#define RCB_ELEMENT_ENTRY_ID 4096 // 条目ID(仅BRCB) +#define RCB_ELEMENT_TIME_OF_ENTRY 8192 // 条目时间(仅BRCB) +#define RCB_ELEMENT_RESV_TMS 16384 // 预留时间(仅BRCB) +#define RCB_ELEMENT_OWNER 32768 // 所有者 +``` + +### 5.3 触发选项(TrgOps) + +```c +#define TRG_OPT_DATA_CHANGED 1 // 数据变化触发 +#define TRG_OPT_QUALITY_CHANGED 2 // 品质变化触发 +#define TRG_OPT_DATA_UPDATE 4 // 数据更新触发 +#define TRG_OPT_INTEGRITY 8 // 周期性触发 +#define TRG_OPT_GI 16 // 总召触发 +#define TRG_OPT_TRANSIENT 128 // 仅上升沿触发(瞬态) +``` + +### 5.4 报告选项字段(OptFlds)— 决定报告包含哪些信息 + +```c +#define RPT_OPT_SEQ_NUM 1 // 序列号 +#define RPT_OPT_TIME_STAMP 2 // 时标 +#define RPT_OPT_REASON_FOR_INCLUSION 4 // 包含原因 +#define RPT_OPT_DATA_SET 8 // 数据集名 +#define RPT_OPT_DATA_REFERENCE 16 // 数据引用 +#define RPT_OPT_BUFFER_OVERFLOW 32 // 缓冲区溢出标志 +#define RPT_OPT_ENTRY_ID 64 // 条目ID +#define RPT_OPT_CONF_REV 128 // 配置版本 +``` + +### 5.5 完整报告配置与接收流程 + +```c +// 步骤1:获取 RCB(读取服务端当前RCB值) +ClientReportControlBlock rcb = IedConnection_getRCBValues(conn, &err, + "simpleIOGenericIO/LLN0.RP.EventsRCB01", NULL); +if (!rcb || err != IED_ERROR_OK) { /* 错误处理 */ } + +// 或者异步获取 +uint32_t id = IedConnection_getRCBValuesAsync(conn, &err, rcbRef, NULL, handler, param); + +// 步骤2:配置 RCB 参数 +ClientReportControlBlock_setRptEna(rcb, true); // 使能报告 +ClientReportControlBlock_setDataSetReference(rcb, + "simpleIOGenericIO/LLN0$Events"); // 设置数据集 +ClientReportControlBlock_setOptFlds(rcb, + RPT_OPT_SEQ_NUM | RPT_OPT_TIME_STAMP | + RPT_OPT_REASON_FOR_INCLUSION | RPT_OPT_DATA_SET | + RPT_OPT_DATA_REFERENCE | RPT_OPT_BUF_OVERFLOW | RPT_OPT_CONF_REV); +ClientReportControlBlock_setTrgOps(rcb, + TRG_OPT_DATA_CHANGED | TRG_OPT_INTEGRITY | TRG_OPT_GI); +ClientReportControlBlock_setBufTm(rcb, 100); // 缓冲时间 100ms +ClientReportControlBlock_setIntgPd(rcb, 60000); // 完整性周期 60s + +// 步骤3:将配置写入服务端 +IedConnection_setRCBValues(conn, &err, rcb, + RCB_ELEMENT_RPT_ENA | RCB_ELEMENT_OPT_FLDS | RCB_ELEMENT_TRG_OPS | + RCB_ELEMENT_DATSET | RCB_ELEMENT_BUF_TM | RCB_ELEMENT_INTG_PD, + false); // false=多变量合并单次MMS写请求(通常建议false) + +// 异步版本 +IedConnection_setRCBValuesAsync(conn, &err, rcb, mask, false, handler, param); + +// 步骤4:安装报告回调 +IedConnection_installReportHandler(conn, + "simpleIOGenericIO/LLN0.RP.EventsRCB01", // RCB引用 + "EventsRpt", // 报告标识符(RptID) + rcbHandler, // 回调函数 + myParam); // 用户参数 + +// 步骤5:报告回调处理 +void rcbHandler(void* parameter, ClientReport report) { + // 获取数据集值(MmsValue 数组) + MmsValue* values = ClientReport_getDataSetValues(report); + int size = MmsValue_getArraySize(values); + + // 遍历数据集成员 + for (int i = 0; i < size; i++) { + ReasonForInclusion reason = ClientReport_getReasonForInclusion(report, i); + const char* ref = ClientReport_getDataReference(report, i); + MmsValue* element = MmsValue_getElement(values, i); + + // 根据类型处理值... + } + + // 报告元数据 + if (ClientReport_hasTimestamp(report)) + uint64_t ts = ClientReport_getTimestamp(report); + if (ClientReport_hasSeqNum(report)) + uint16_t seq = ClientReport_getSeqNum(report); + if (ClientReport_hasBufOvfl(report)) + bool ovfl = ClientReport_getBufOvfl(report); + if (ClientReport_hasConfRev(report)) + uint32_t cr = ClientReport_getConfRev(report); + if (ClientReport_hasSubSeqNum(report)) { // 分段报告 + uint16_t subSeq = ClientReport_getSubSeqNum(report); + bool more = ClientReport_getMoreSegmentsFollow(report); + } +} + +// 步骤6:触发总召(可选) +ClientReportControlBlock_setGI(rcb, true); +IedConnection_setRCBValues(conn, &err, rcb, RCB_ELEMENT_GI, false); + +// 步骤7:注销报告 +IedConnection_uninstallReportHandler(conn, + "simpleIOGenericIO/LLN0.RP.EventsRCB01"); + +// 步骤8:释放 RCB 对象 +ClientReportControlBlock_destroy(rcb); +``` + +### 5.6 包含原因枚举 + +```c +typedef int ReasonForInclusion; +#define IEC61850_REASON_NOT_INCLUDED 0 // 未包含 +#define IEC61850_REASON_DATA_CHANGE 1 // 数据变化 +#define IEC61850_REASON_QUALITY_CHANGE 2 // 品质变化 +#define IEC61850_REASON_DATA_UPDATE 4 // 数据更新 +#define IEC61850_REASON_INTEGRITY 8 // 周期性上送 +#define IEC61850_REASON_GI 16 // 总召 +#define IEC61850_REASON_UNKNOWN 32 // 原因未知 +``` + +### 5.7 ClientReport API 完整列表 + +```c +const char* ClientReport_getDataSetName(ClientReport self); +MmsValue* ClientReport_getDataSetValues(ClientReport self); // 仅回调内有效 +char* ClientReport_getRcbReference(ClientReport self); +char* ClientReport_getRptId(ClientReport self); +ReasonForInclusion ClientReport_getReasonForInclusion(ClientReport self, int elementIndex); +MmsValue* ClientReport_getEntryId(ClientReport self); +bool ClientReport_hasTimestamp(ClientReport self); +uint64_t ClientReport_getTimestamp(ClientReport self); +bool ClientReport_hasSeqNum(ClientReport self); +uint16_t ClientReport_getSeqNum(ClientReport self); +bool ClientReport_hasDataSetName(ClientReport self); +bool ClientReport_hasReasonForInclusion(ClientReport self); +bool ClientReport_hasConfRev(ClientReport self); +uint32_t ClientReport_getConfRev(ClientReport self); +bool ClientReport_hasBufOvfl(ClientReport self); +bool ClientReport_getBufOvfl(ClientReport self); +bool ClientReport_hasDataReference(ClientReport self); +const char* ClientReport_getDataReference(ClientReport self, int elementIndex); +bool ClientReport_hasSubSeqNum(ClientReport self); // 分段报告 +uint16_t ClientReport_getSubSeqNum(ClientReport self); +bool ClientReport_getMoreSegmentsFollow(ClientReport self); +char* ReasonForInclusion_getValueAsString(ReasonForInclusion reasonCode); +``` + +### 5.8 ClientReportControlBlock API 完整列表 + +```c +ClientReportControlBlock ClientReportControlBlock_create(const char* rcbReference); +void ClientReportControlBlock_destroy(ClientReportControlBlock self); +char* ClientReportControlBlock_getObjectReference(ClientReportControlBlock self); +bool ClientReportControlBlock_isBuffered(ClientReportControlBlock self); + +// Getter/Setter +const char* ClientReportControlBlock_getRptId(ClientReportControlBlock self); +void ClientReportControlBlock_setRptId(ClientReportControlBlock self, const char* rptId); +bool ClientReportControlBlock_getRptEna(ClientReportControlBlock self); +void ClientReportControlBlock_setRptEna(ClientReportControlBlock self, bool rptEna); +bool ClientReportControlBlock_getResv(ClientReportControlBlock self); +void ClientReportControlBlock_setResv(ClientReportControlBlock self, bool resv); +const char* ClientReportControlBlock_getDataSetReference(ClientReportControlBlock self); +void ClientReportControlBlock_setDataSetReference(ClientReportControlBlock self, const char* dataSetReference); +uint32_t ClientReportControlBlock_getConfRev(ClientReportControlBlock self); +int ClientReportControlBlock_getOptFlds(ClientReportControlBlock self); +void ClientReportControlBlock_setOptFlds(ClientReportControlBlock self, int optFlds); +uint32_t ClientReportControlBlock_getBufTm(ClientReportControlBlock self); +void ClientReportControlBlock_setBufTm(ClientReportControlBlock self, uint32_t bufTm); +uint16_t ClientReportControlBlock_getSqNum(ClientReportControlBlock self); +int ClientReportControlBlock_getTrgOps(ClientReportControlBlock self); +void ClientReportControlBlock_setTrgOps(ClientReportControlBlock self, int trgOps); +uint32_t ClientReportControlBlock_getIntgPd(ClientReportControlBlock self); +void ClientReportControlBlock_setIntgPd(ClientReportControlBlock self, uint32_t intgPd); +bool ClientReportControlBlock_getGI(ClientReportControlBlock self); +void ClientReportControlBlock_setGI(ClientReportControlBlock self, bool gi); +bool ClientReportControlBlock_getPurgeBuf(ClientReportControlBlock self); +void ClientReportControlBlock_setPurgeBuf(ClientReportControlBlock self, bool purgeBuf); +bool ClientReportControlBlock_hasResvTms(ClientReportControlBlock self); +int16_t ClientReportControlBlock_getResvTms(ClientReportControlBlock self); +void ClientReportControlBlock_setResvTms(ClientReportControlBlock self, int16_t resvTms); +MmsValue* ClientReportControlBlock_getEntryId(ClientReportControlBlock self); +void ClientReportControlBlock_setEntryId(ClientReportControlBlock self, MmsValue* entryId); +uint64_t ClientReportControlBlock_getEntryTime(ClientReportControlBlock self); +MmsValue* ClientReportControlBlock_getOwner(ClientReportControlBlock self); +``` + +--- + +## 6. 控制服务(Control) + +客户端控制功能用于向服务端发送遥控命令。支持直控(Direct)和选控(SBO,Select Before Operate)。 + +### 6.1 控制对象生命周期 + +```c +// 创建(同步版本:会请求服务端获取 ctlModel 等信息,可能阻塞) +ControlObjectClient ctl = ControlObjectClient_create( + "simpleIOGenericIO/GGIO1.SP.Pos1", conn); + +// 创建(扩展版本:不阻塞,需要已知 ctlModel 和 controlObjectSpec) +ControlObjectClient ctl = ControlObjectClient_createEx( + objRef, conn, CONTROL_MODEL_SBO_NORMAL, controlObjectSpec); + +// 销毁 +ControlObjectClient_destroy(ctl); +``` + +### 6.2 同步控制操作 + +```c +// === 直控-普通安全 (DIRECT_NORMAL) === +// 直接发送 operate +MmsValue* ctlVal = MmsValue_newBoolean(true); +bool success = ControlObjectClient_operate(ctl, ctlVal, 0); // operTime=0 立即执行 +MmsValue_delete(ctlVal); + +// === 选控-普通安全 (SBO_NORMAL) === +// 第一步:select +bool ok = ControlObjectClient_select(ctl); +// 第二步:operate +bool ok = ControlObjectClient_operate(ctl, ctlVal, 0); + +// === 选控-增强安全 (SBO_ENHANCED) === +// 第一步:select with value +bool ok = ControlObjectClient_selectWithValue(ctl, ctlVal); +// 第二步:operate +bool ok = ControlObjectClient_operate(ctl, ctlVal, 0); + +// === 取消操作 === +bool ok = ControlObjectClient_cancel(ctl); +``` + +### 6.3 异步控制操作 + +```c +void actionHandler(uint32_t invokeId, void* param, IedClientError err, + ControlActionType type, bool success) { + switch (type) { + case CONTROL_ACTION_TYPE_SELECT: /* select 结果 */ break; + case CONTROL_ACTION_TYPE_OPERATE: /* operate 结果 */ break; + case CONTROL_ACTION_TYPE_CANCEL: /* cancel 结果 */ break; + } +} + +uint32_t id; +id = ControlObjectClient_operateAsync(ctl, &err, ctlVal, 0, actionHandler, myParam); +id = ControlObjectClient_selectAsync(ctl, &err, actionHandler, myParam); +id = ControlObjectClient_selectWithValueAsync(ctl, &err, ctlVal, actionHandler, myParam); +id = ControlObjectClient_cancelAsync(ctl, &err, actionHandler, myParam); +``` + +### 6.4 命令终止回调(增强安全模式) + +```c +void terminationHandler(void* parameter, ControlObjectClient controlClient) { + LastApplError lastErr = ControlObjectClient_getLastApplError(controlClient); + if (lastErr.error == CONTROL_ERROR_NO_ERROR) { + // CommandTermination+ (成功) + } else { + // CommandTermination- (失败) + } +} +ControlObjectClient_setCommandTerminationHandler(ctl, terminationHandler, param); +``` + +### 6.5 控制对象其他 API + +```c +const char* ControlObjectClient_getObjectReference(ControlObjectClient self); +ControlModel ControlObjectClient_getControlModel(ControlObjectClient self); +void ControlObjectClient_setControlModel(ControlObjectClient self, ControlModel ctlModel); +void ControlObjectClient_changeServerControlModel(ControlObjectClient self, ControlModel ctlModel); +MmsType ControlObjectClient_getCtlValType(ControlObjectClient self); +IedClientError ControlObjectClient_getLastError(ControlObjectClient self); +LastApplError ControlObjectClient_getLastApplError(ControlObjectClient self); + +// 控制参数设置 +void ControlObjectClient_setTestMode(ControlObjectClient self, bool value); +void ControlObjectClient_setOrigin(ControlObjectClient self, const char* orIdent, int orCat); +void ControlObjectClient_setInterlockCheck(ControlObjectClient self, bool value); +void ControlObjectClient_setSynchroCheck(ControlObjectClient self, bool value); +void ControlObjectClient_useConstantT(ControlObjectClient self, bool useConstantT); +``` + +**Originator 类别(orCat)**: + +```c +#define CONTROL_ORCAT_NOT_SUPPORTED 0 +#define CONTROL_ORCAT_BAY_CONTROL 1 // 间隔层操作员 +#define CONTROL_ORCAT_STATION_CONTROL 2 // 站控层操作员 +#define CONTROL_ORCAT_REMOTE_CONTROL 3 // 远方控制 +#define CONTROL_ORCAT_AUTOMATIC_BAY 4 // 间隔层自动 +#define CONTROL_ORCAT_AUTOMATIC_STATION 5 // 站控层自动 +#define CONTROL_ORCAT_AUTOMATIC_REMOTE 6 // 远方自动 +#define CONTROL_ORCAT_MAINTENANCE 7 // 维护工具 +#define CONTROL_ORCAT_PROCESS 8 // 过程层 +``` + +--- + +## 7. 数据集服务(DataSet) + +数据集是一组 FCD/FCDA 引用的集合,用于报告、日志等。 + +### 7.1 读取数据集 + +```c +// 同步 +ClientDataSet ds = IedConnection_readDataSetValues(conn, &err, + "simpleIOGenericIO/LLN0$Events", NULL); // NULL=创建新实例 +if (ds) { + MmsValue* values = ClientDataSet_getValues(ds); // MMS_ARRAY + int size = ClientDataSet_getDataSetSize(ds); + char* ref = ClientDataSet_getReference(ds); + ClientDataSet_destroy(ds); +} + +// 异步 +IedConnection_readDataSetValuesAsync(conn, &err, dsRef, NULL, handler, param); +// 回调: void handler(uint32_t invokeId, void* param, IedClientError err, ClientDataSet dataSet) +``` + +### 7.2 创建/删除数据集 + +```c +// 创建 +LinkedList members = LinkedList_create(); +LinkedList_add(members, strdup("simpleIOGenericIO/GGIO1.ST.Ind1.stVal[ST]")); +LinkedList_add(members, strdup("simpleIOGenericIO/GGIO1.ST.Ind2.stVal[ST]")); +IedConnection_createDataSet(conn, &err, "simpleIOGenericIO/LLN0.MyDS", members); +LinkedList_destroyDeep(members, free); + +// 异步创建 +IedConnection_createDataSetAsync(conn, &err, ref, members, handler, param); + +// 删除 +bool deleted = IedConnection_deleteDataSet(conn, &err, "simpleIOGenericIO/LLN0.MyDS"); + +// 异步删除 +IedConnection_deleteDataSetAsync(conn, &err, ref, handler, param); +``` + +### 7.3 获取数据集目录 + +```c +// 同步:返回 LinkedList 所有成员引用 +bool isDeletable; +LinkedList elements = IedConnection_getDataSetDirectory(conn, &err, ref, &isDeletable); + +// 异步 +IedConnection_getDataSetDirectoryAsync(conn, &err, ref, handler, param); +// 回调: void handler(uint32_t id, void* param, IedClientError err, LinkedList dir, bool isDeletable) +``` + +### 7.4 写入数据集 + +```c +LinkedList values = LinkedList_create(); +LinkedList_add(values, MmsValue_newFloat(1.5f)); +LinkedList_add(values, MmsValue_newBoolean(true)); +LinkedList accessResults = NULL; +IedConnection_writeDataSetValues(conn, &err, ref, values, &accessResults); + +// 异步 +IedConnection_writeDataSetValuesAsync(conn, &err, ref, values, handler, param); +// 回调: void handler(uint32_t id, void* param, IedClientError err, LinkedList accessResults) +``` + +### 7.5 ClientDataSet API + +```c +void ClientDataSet_destroy(ClientDataSet self); +MmsValue* ClientDataSet_getValues(ClientDataSet self); // MMS_ARRAY +char* ClientDataSet_getReference(ClientDataSet self); +int ClientDataSet_getDataSetSize(ClientDataSet self); // 成员数 +``` + +--- + +## 8. 文件服务(File) + +### 8.1 获取文件目录 + +```c +// 获取根目录 +LinkedList /**/ dir = IedConnection_getFileDirectory(conn, &err, NULL); +if (dir) { + // 遍历 + LinkedList element = LinkedList_getNext(dir); + while (element) { + FileDirectoryEntry entry = (FileDirectoryEntry) element->data; + const char* name = FileDirectoryEntry_getFileName(entry); + uint32_t size = FileDirectoryEntry_getFileSize(entry); + uint64_t mtime = FileDirectoryEntry_getLastModified(entry); + element = LinkedList_getNext(element); + } + LinkedList_destroyDeep(dir, (LinkedListValueDeleteFunction)FileDirectoryEntry_destroy); +} + +// 扩展版本(支持分页) +bool moreFollows; +LinkedList dir = IedConnection_getFileDirectoryEx(conn, &err, NULL, NULL, &moreFollows); +// 如果 moreFollows=true,用最后一个文件名做 continueAfter 继续获取 + +// 异步版本 +IedConnection_getFileDirectoryAsyncEx(conn, &err, dirName, continueAfter, handler, param); +``` + +### 8.2 下载文件(GetFile) + +```c +// 同步 +bool fileHandler(void* parameter, uint8_t* buffer, uint32_t bytesRead) { + // 将 buffer 中的数据写入本地文件 + fwrite(buffer, 1, bytesRead, (FILE*)parameter); + return true; // 返回 true 继续下载 +} +uint32_t totalBytes = IedConnection_getFile(conn, &err, "remoteFile.txt", fileHandler, fp); + +// 异步 +bool asyncFileHandler(uint32_t invokeId, void* parameter, IedClientError err, + uint32_t originalInvokeId, uint8_t* buffer, uint32_t bytesRead, bool moreFollows) { + if (err != IED_ERROR_OK) { /* 错误处理 */ return false; } + fwrite(buffer, 1, bytesRead, (FILE*)parameter); + return moreFollows; // 还有更多数据 +} +uint32_t id = IedConnection_getFileAsync(conn, &err, "remoteFile.txt", asyncFileHandler, fp); +``` + +### 8.3 上传文件(SetFile)/ 删除文件 + +```c +// 上传 +IedConnection_setFile(conn, &err, "localFile.txt", "remoteFile.txt"); +// 异步 +IedConnection_setFileAsync(conn, &err, src, dst, handler, param); + +// 删除 +IedConnection_deleteFile(conn, &err, "remoteFile.txt"); +// 异步 +IedConnection_deleteFileAsync(conn, &err, filename, handler, param); +``` + +--- + +## 9. 日志服务(Log) + +```c +// 按时间范围查询日志 +bool moreFollows; +LinkedList /* */ entries = IedConnection_queryLogByTime( + conn, &err, "LDName/LNName$LogName", startTime, endTime, &moreFollows); + +// 按条目ID查询后续日志 +LinkedList entries = IedConnection_queryLogAfter( + conn, &err, "LDName/LNName$LogName", entryID, timeStamp, &moreFollows); + +// 异步版本 +IedConnection_queryLogByTimeAsync(conn, &err, ref, start, end, handler, param); +IedConnection_queryLogAfterAsync(conn, &err, ref, entryID, ts, handler, param); +``` + +--- + +## 10. 模型发现服务 + +用于动态发现服务器端设备模型结构。 + +### 10.1 获取完整设备模型 + +```c +IedConnection_getDeviceModelFromServer(conn, &err); +// 此调用会缓存模型,后续的查询基于缓存,不再产生网络请求 +``` + +### 10.2 目录查询 + +```c +// 获取逻辑设备列表 +LinkedList /* */ lds = IedConnection_getLogicalDeviceList(conn, &err); +// 或:IedConnection_getServerDirectory(conn, &err, false); + +// 获取逻辑节点列表 +LinkedList lns = IedConnection_getLogicalDeviceDirectory(conn, &err, "LDName"); + +// 获取逻辑节点目录(按ACSI类别过滤) +LinkedList items = IedConnection_getLogicalNodeDirectory(conn, &err, + "LDName/LLN0", ACSI_CLASS_DATA_SET); + +// 获取数据对象(DO)的子元素 +LinkedList das = IedConnection_getDataDirectory(conn, &err, "LDName/GGIO1.ST.Ind1"); +LinkedList das = IedConnection_getDataDirectoryFC(conn, &err, "LDName/GGIO1.ST.Ind1"); // 带 FC 后缀 +LinkedList das = IedConnection_getDataDirectoryByFC(conn, &err, "LDName/GGIO1.ST", IEC61850_FC_ST); + +// 获取变量规格 +MmsVariableSpecification* spec = IedConnection_getVariableSpecification(conn, &err, ref, fc); + +// 获取逻辑设备所有MMS变量名 +LinkedList vars = IedConnection_getLogicalDeviceVariables(conn, &err, "LDName"); +// 获取逻辑设备所有数据集名 +LinkedList dss = IedConnection_getLogicalDeviceDataSets(conn, &err, "LDName"); +``` + +### 10.3 ACSI 类别枚举 + +```c +typedef enum { + ACSI_CLASS_DATA_OBJECT, + ACSI_CLASS_DATA_SET, + ACSI_CLASS_BRCB, + ACSI_CLASS_URCB, + ACSI_CLASS_LCB, + ACSI_CLASS_LOG, + ACSI_CLASS_SGCB, + ACSI_CLASS_GoCB, + ACSI_CLASS_GsCB, + ACSI_CLASS_MSVCB, + ACSI_CLASS_USVCB +} ACSIClass; +``` + +### 10.4 异步模型发现 + +```c +IedConnection_getServerDirectoryAsync(conn, &err, NULL, NULL, handler, param); +IedConnection_getLogicalDeviceVariablesAsync(conn, &err, ld, NULL, NULL, handler, param); +IedConnection_getLogicalDeviceDataSetsAsync(conn, &err, ld, NULL, NULL, handler, param); +IedConnection_getVariableSpecificationAsync(conn, &err, ref, fc, handler, param); +``` + +--- + +## 11. SV/GOOSE 控制块处理 + +### 11.1 SV 控制块 + +```c +ClientSVControlBlock svcb = ClientSVControlBlock_create(conn, "ref"); +bool ena = ClientSVControlBlock_getSvEna(svcb); +ClientSVControlBlock_setSvEna(svcb, true); +char* msvID = ClientSVControlBlock_getMsvID(svcb); +char* ds = ClientSVControlBlock_getDatSet(svcb); // 需手动 free +uint32_t confRev = ClientSVControlBlock_getConfRev(svcb); +uint16_t smpRate = ClientSVControlBlock_getSmpRate(svcb); +uint8_t smpMod = ClientSVControlBlock_getSmpMod(svcb); +int noASDU = ClientSVControlBlock_getNoASDU(svcb); +PhyComAddress addr = ClientSVControlBlock_getDstAddress(svcb); +int optFlds = ClientSVControlBlock_getOptFlds(svcb); +bool mcast = ClientSVControlBlock_isMulticast(svcb); +IedClientError err = ClientSVControlBlock_getLastComError(svcb); +ClientSVControlBlock_destroy(svcb); +``` + +### 11.2 GOOSE 控制块 + +```c +ClientGooseControlBlock gcb = ClientGooseControlBlock_create("ref"); + +// 读取服务端GoCB +IedConnection_getGoCBValues(conn, &err, "simpleIOGenericIO/LLN0.gcbEvents", gcb); +// 或传入 NULL 创建新实例:gcb = IedConnection_getGoCBValues(conn, &err, ref, NULL); + +// 读写属性 +bool ena = ClientGooseControlBlock_getGoEna(gcb); +ClientGooseControlBlock_setGoEna(gcb, true); +const char* goID = ClientGooseControlBlock_getGoID(gcb); +ClientGooseControlBlock_setGoID(gcb, "newID"); +const char* ds = ClientGooseControlBlock_getDatSet(gcb); +ClientGooseControlBlock_setDatSet(gcb, "LD/LLN0$ds1"); +uint32_t confRev = ClientGooseControlBlock_getConfRev(gcb); +bool ndsComm = ClientGooseControlBlock_getNdsComm(gcb); +uint32_t minTime = ClientGooseControlBlock_getMinTime(gcb); +uint32_t maxTime = ClientGooseControlBlock_getMaxTime(gcb); +bool fixedOffs = ClientGooseControlBlock_getFixedOffs(gcb); +PhyComAddress addr = ClientGooseControlBlock_getDstAddress(gcb); +ClientGooseControlBlock_setDstAddress(gcb, value); + +// 写入服务端 +IedConnection_setGoCBValues(conn, &err, gcb, + GOCB_ELEMENT_GO_ENA | GOCB_ELEMENT_DATSET, false); + +ClientGooseControlBlock_destroy(gcb); +``` + +**GoCB 元素掩码**: + +```c +#define GOCB_ELEMENT_GO_ENA 1 +#define GOCB_ELEMENT_GO_ID 2 +#define GOCB_ELEMENT_DATSET 4 +#define GOCB_ELEMENT_CONF_REV 8 +#define GOCB_ELEMENT_NDS_COMM 16 +#define GOCB_ELEMENT_DST_ADDRESS 32 +#define GOCB_ELEMENT_MIN_TIME 64 +#define GOCB_ELEMENT_MAX_TIME 128 +#define GOCB_ELEMENT_FIXED_OFFS 256 +#define GOCB_ELEMENT_ALL 511 +``` + +--- + +## 12. MMS低层传输层访问 + +当高层 API 不够用时,可通过 `IedConnection_getMmsConnection()` 获取底层 `MmsConnection`,直接使用 `mms_client_connection.h` 中的低层 API: + +```c +// 获取底层连接 +MmsConnection mms = IedConnection_getMmsConnection(conn); + +// 低层API包含: +// - 直接域名(domain)和变量名(item)的读写 +// - 命名变量列表操作 +// - 文件服务(底层的FileOpen/Read/Close等) +// - 日志/Journal读取 +``` + +一般开发不需要使用低层 API,只有在高层 API 不支持的特殊场景(如非标准服务器)才需要。 + +--- + +## 13. 错误码完整参考 + +```c +typedef enum { + IED_ERROR_OK = 0, // 成功 + IED_ERROR_NOT_CONNECTED = 1, // 未连接 + IED_ERROR_ALREADY_CONNECTED = 2, // 已连接(重复连接) + IED_ERROR_CONNECTION_LOST = 3, // 连接丢失 + IED_ERROR_SERVICE_NOT_SUPPORTED = 4, // 服务不支持 + IED_ERROR_CONNECTION_REJECTED = 5, // 连接被拒绝 + IED_ERROR_OUTSTANDING_CALL_LIMIT_REACHED = 6, // 未完成调用达上限 + IED_ERROR_USER_PROVIDED_INVALID_ARGUMENT = 10,// 无效参数 + IED_ERROR_ENABLE_REPORT_FAILED_DATASET_MISMATCH = 11, // 报告使能失败(数据集不匹配) + IED_ERROR_OBJECT_REFERENCE_INVALID = 12, // 对象引用无效 + IED_ERROR_UNEXPECTED_VALUE_RECEIVED = 13, // 收到意外类型值 + IED_ERROR_TIMEOUT = 20, // 超时 + IED_ERROR_ACCESS_DENIED = 21, // 访问被拒绝 + IED_ERROR_OBJECT_DOES_NOT_EXIST = 22, // 对象不存在 + IED_ERROR_OBJECT_EXISTS = 23, // 对象已存在 + IED_ERROR_OBJECT_ACCESS_UNSUPPORTED = 24, // 访问方式不支持 + IED_ERROR_TYPE_INCONSISTENT = 25, // 类型不一致 + IED_ERROR_TEMPORARILY_UNAVAILABLE = 26, // 临时不可用 + IED_ERROR_OBJECT_UNDEFINED = 27, // 对象未定义 + IED_ERROR_INVALID_ADDRESS = 28, // 无效地址 + IED_ERROR_HARDWARE_FAULT = 29, // 硬件故障 + IED_ERROR_TYPE_UNSUPPORTED = 30, // 类型不支持 + IED_ERROR_OBJECT_ATTRIBUTE_INCONSISTENT = 31, // 属性不一致 + IED_ERROR_OBJECT_VALUE_INVALID = 32, // 对象值无效 + IED_ERROR_OBJECT_INVALIDATED = 33, // 对象已失效 + IED_ERROR_MALFORMED_MESSAGE = 34, // 畸形消息 + IED_ERROR_SERVICE_NOT_IMPLEMENTED = 98, // 服务未实现 + IED_ERROR_UNKNOWN = 99 // 未知错误 +} IedClientError; +``` + +--- + +## 14. 运行模式:线程模式 vs 非线程模式 + +### 线程模式(默认) + +```c +IedConnection conn = IedConnection_create(); +// 库内部创建后台线程自动处理 MMS 消息 +// 可以使用所有同步/异步 API +IedConnection_connect(conn, &err, host, port); +IedConnection_destroy(conn); +``` + +### 非线程模式 + +```c +IedConnection conn = IedConnection_createEx(NULL, false); // useThreads=false + +// 必须使用异步 API +IedConnection_connectAsync(conn, &err, host, port); + +// 周期性轮询 +while (running) { + bool canPause = IedConnection_tick(conn); + // canPause=true: 当前无活跃请求,调用线程可以挂起 + // canPause=false: 忙,应尽快再次调用 tick + + if (canPause) { + usleep(10000); // 10ms + } +} + +// 警告:不要使用同步 API!它会永远阻塞(因为没有后台线程驱动) +``` + +--- + +## 15. 与 RTU 项目对照 + +RTU 项目中,`src/protocol/libmms_m/` 封装了客户端 API,核心数据结构为 `stru_mms_m_obj`: + +| RTU 封装层 | libiec61850 API | +|-----------|----------------| +| `stru_mms_m_obj.run.con` | `IedConnection` | +| `stru_mms_m_obj.run.con_state` | `IedConnection_getState()` | +| `stru_ln_rpt.rcb` | `ClientReportControlBlock` | +| `stru_ld_dataset.ln_datasets[]` | `IedConnection_readDataSetValues()` | +| `stru_mms_m_obj.run.out_status_cb` | `IedConnection_installStateChangedHandler()` | +| `mms_m_sg.cpp` (定值组读写) | `IedConnection_readObject`/`writeObject` with FC=SG/SE | +| `mms_m_file.cpp` (文件传输) | `IedConnection_getFile`/`setFile` | + +**线程模型**:RTU 客户端使用独立线程(`pthread_task`),通过 `sem` 信号量进行同步。运行间隔为 `MMS_M_THREAD_RUN_TM = 300ms`。 + +**关键使用模式**(参考 RTU 代码): + +```cpp +// RTU 中的典型初始化流程 +stru_mms_m_obj* obj = new stru_mms_m_obj; +obj->run.con = IedConnection_create(); +IedConnection_setConnectTimeout(obj->run.con, obj->connectionTimeout); + +// 连接 +IedClientError err; +IedConnection_connect(obj->run.con, &err, obj->run.ip.c_str(), obj->run.port); +obj->run.con_state = IedConnection_getState(obj->run.con); + +// 安装报告回调 +IedConnection_installReportHandler(obj->run.con, + rcbRef.c_str(), rptId.c_str(), reportCallback, obj); +``` + +--- + +> 本文档基于 `libiec61850-1.5.3/src/iec61850/inc/iec61850_client.h` (3091行) 和 `src/mms/inc/mms_value.h` (1062行) 完整归纳,覆盖所有公开 C API。 diff --git a/claude/工程/libiec61850_MMS服务端API开发手册.md b/claude/工程/libiec61850_MMS服务端API开发手册.md new file mode 100644 index 0000000..cb7c53b --- /dev/null +++ b/claude/工程/libiec61850_MMS服务端API开发手册.md @@ -0,0 +1,1294 @@ +# libiec61850-1.5.3 MMS 服务端 API 开发手册 + +> 本文档基于 libiec61850 v1.5.3 源码,覆盖 IEC 61850 服务端全部公开 C API,达到开发者脱离源码即可进行项目开发的标准。 + +--- + +## 目录 + +1. [架构概述](#1-架构概述) +2. [数据模型创建(动态模型)](#2-数据模型创建动态模型) +3. [服务器配置(IedServerConfig)](#3-服务器配置iedserverconfig) +4. [服务器生命周期管理](#4-服务器生命周期管理) +5. [数据值读取与更新](#5-数据值读取与更新) +6. [控制模型回调](#6-控制模型回调) +7. [报告控制块(RCB)事件处理](#7-报告控制块rcb事件处理) +8. [读写访问控制](#8-读写访问控制) +9. [定值组(SGCB)处理](#9-定值组sgcb处理) +10. [GOOSE 发布](#10-goose-发布) +11. [SV 控制块](#11-sv-控制块) +12. [连接管理与认证](#12-连接管理与认证) +13. [日志服务](#13-日志服务) +14. [运行模式:线程模式 vs 非线程模式](#14-运行模式线程模式-vs-非线程模式) +15. [与 RTU 项目对照](#15-与-rtu-项目对照) +16. [附录:公共数据类型速查](#16-附录公共数据类型速查) + +--- + +## 1. 架构概述 + +服务端 API 提供两个抽象层级: + +| 层级 | API 头文件 | 描述 | +|------|-----------|------| +| **高层 IEC 61850** | `iec61850_server.h` | 封装 IEC 61850 语义(LD/LN/DO/DA/RCB/SGCB),推荐使用 | +| **底层 MMS** | `mms_server.h` | MMS 协议层服务器,一般不需要直接使用 | + +核心不透明句柄为 `IedServer`,所有服务端操作围绕它展开。 + +### 关键头文件依赖 + +``` +iec61850_server.h + ├── iec61850_dynamic_model.h ← 动态创建数据模型(IedModel/LD/LN/DO/DA/RCB/DataSet) + ├── iec61850_model.h ← 静态模型(自动生成) + ├── iec61850_common.h ← FC/Quality/Timestamp/ControlModel + ├── mms_server.h ← 底层 MMS 服务器 + ├── mms_value.h ← MmsValue 数据类型 + └── iso_connection_parameters.h ← ISO 连接参数 +``` + +### 服务端完整工作流 + +``` +1. 创建数据模型 (IedModel → LD → LN → DO → DA → RCB → DataSet) +2. 创建配置 (IedServerConfig) +3. 创建服务器 (IedServer_createWithConfig) +4. 设置各种回调 (控制/RCB/写访问/读访问/连接认证) +5. 启动服务器 (IedServer_start) 或非线程模式启动 (IedServer_startThreadless) +6. 运行时更新数据值 (IedServer_update*AttributeValue) +7. 停止/销毁 (IedServer_stop + IedServer_destroy) +``` + +--- + +## 2. 数据模型创建(动态模型) + +### 2.1 模型层级结构 + +``` +IedModel (IED) + └─ LogicalDevice (LD) "TEMPLATE" + ├─ LogicalNode (LN) "LLN0" ← 每个LD必须包含LLN0 + │ ├─ DataObject (DO) "Mod" ← 模式 + │ ├─ DataObject (DO) "Beh" ← 行为 + │ ├─ DataObject (DO) "Health" + │ ├─ ReportControlBlock (RCB) + │ ├─ DataSet + │ └─ SettingGroupControlBlock (SGCB) + └─ LogicalNode (LN) "GGIO1" ← 通用IO + ├─ DataObject (DO) "Ind1" (array=0) + │ ├─ DataAttribute (DA) "stVal" [FC=ST, type=FLOAT32, trgOps=dchg] + │ └─ DataAttribute (DA) "q" [FC=ST, type=QUALITY] + └─ DataObject (DO) "SPCSO1" + ├─ DataAttribute (DA) "ctlVal" [FC=CO, type=BOOLEAN] + └─ DataAttribute (DA) "stVal" [FC=ST, type=BOOLEAN, trgOps=dchg] +``` + +### 2.2 完整创建流程 + +```c +// 1. 创建 IED 模型 +IedModel* model = IedModel_create("MyIED"); +IedModel_setIedNameForDynamicModel(model, "RTU"); // 必须在 IedServer_create 前调用 + +// 2. 创建逻辑设备 +LogicalDevice* ld = LogicalDevice_create("TEMPLATE", model); + +// 3. 创建 LLN0 逻辑节点(每个 LD 必须包含) +LogicalNode* ln0 = LogicalNode_create("LLN0", ld); + +// 4. 创建 LLN0 下的常用数据对象 +DataObject* mod = DataObject_create("Mod", ln0, 0); +DataObject* beh = DataObject_create("Beh", ln0, 0); +DataObject* health = DataObject_create("Health", ln0, 0); + +// 5. 创建应用逻辑节点 GGIO1 +LogicalNode* ggio1 = LogicalNode_create("GGIO1", ld); + +// 6. 创建数据对象 Ind1(单点遥信) +DataObject* ind1 = DataObject_create("Ind1", ggio1, 0); + +// 7. 创建数据属性 stVal(状态值)+ q(品质)+ t(时标) +// DataAttribute_create(名称, 父节点, 类型, FC, 触发选项, 数组大小, 短地址) +DataAttribute* stVal = DataAttribute_create("stVal", ind1, + IEC61850_FLOAT32, // 类型 + IEC61850_FC_ST, // 功能约束 + TRG_OPT_DATA_CHANGED, // 数据变化时触发报告 + 0, // 非数组 + NULL); // 无短地址 + +DataAttribute* q = DataAttribute_create("q", ind1, + IEC61850_QUALITY, IEC61850_FC_ST, 0, 0, NULL); + +DataAttribute* t = DataAttribute_create("t", ind1, + IEC61850_TIMESTAMP, IEC61850_FC_ST, 0, 0, NULL); + +// 8. 创建可控数据对象(遥控) +DataObject* spcso1 = DataObject_create("SPCSO1", ggio1, 0); +DataAttribute* ctlVal = DataAttribute_create("ctlVal", spcso1, + IEC61850_BOOLEAN, IEC61850_FC_CO, 0, 0, NULL); +DataAttribute* stVal2 = DataAttribute_create("stVal", spcso1, + IEC61850_BOOLEAN, IEC61850_FC_ST, TRG_OPT_DATA_CHANGED, 0, NULL); +DataAttribute* q2 = DataAttribute_create("q", spcso1, + IEC61850_QUALITY, IEC61850_FC_ST, 0, 0, NULL); +DataAttribute* t2 = DataAttribute_create("t", spcso1, + IEC61850_TIMESTAMP, IEC61850_FC_ST, 0, 0, NULL); +``` + +### 2.3 创建报告控制块(RCB) + +```c +// 在 LLN0 下创建 RCB +ReportControlBlock* rcb = ReportControlBlock_create( + "EventsRCB01", // 名称(会变成 LLN0.RP.EventsRCB01 或 .BR.) + ln0, // 父节点 LLN0 + NULL, // rptId,NULL = 使用默认(对象引用) + false, // isBuffered: false=URCB, true=BRCB + "TEMPLATE/LLN0$Events", // 数据集引用 + 1, // confRef: 配置版本 + TRG_OPT_DATA_CHANGED | TRG_OPT_INTEGRITY | TRG_OPT_GI, // trgOps + RPT_OPT_SEQ_NUM | RPT_OPT_TIME_STAMP | + RPT_OPT_REASON_FOR_INCLUSION | RPT_OPT_DATA_SET | + RPT_OPT_DATA_REFERENCE, // options (OptFlds) + 100, // bufTm: 缓冲时间 100ms + 60000 // intgPd: 完整性周期 60s +); + +// 设置预配置客户端(可选,用于指定哪些客户端可以使用此RCB) +// uint8_t ipv4[4] = {192, 168, 1, 100}; +// ReportControlBlock_setPreconfiguredClient(rcb, 4, ipv4); + +// RCB 信息查询 +const char* name = ReportControlBlock_getName(rcb); +bool isBuffered = ReportControlBlock_isBuffered(rcb); // true=BRCB, false=URCB +LogicalNode* parent = ReportControlBlock_getParent(rcb); +char* rptId = ReportControlBlock_getRptID(rcb); // 需手动 free +bool ena = ReportControlBlock_getRptEna(rcb); // 当前是否使能 +char* ds = ReportControlBlock_getDataSet(rcb); // 需手动 free +uint32_t confRev = ReportControlBlock_getConfRev(rcb); +uint32_t optFlds = ReportControlBlock_getOptFlds(rcb); +uint32_t bufTm = ReportControlBlock_getBufTm(rcb); +uint16_t sqNum = ReportControlBlock_getSqNum(rcb); // 当前序列号 +uint32_t trgOps = ReportControlBlock_getTrgOps(rcb); +uint32_t intgPd = ReportControlBlock_getIntgPd(rcb); +bool gi = ReportControlBlock_getGI(rcb); +bool purgeBuf = ReportControlBlock_getPurgeBuf(rcb); +MmsValue* entryId = ReportControlBlock_getEntryId(rcb); +uint64_t timeOfEntry = ReportControlBlock_getTimeofEntry(rcb); +int16_t resvTms = ReportControlBlock_getResvTms(rcb); +bool resv = ReportControlBlock_getResv(rcb); +MmsValue* owner = ReportControlBlock_getOwner(rcb); +``` + +### 2.4 创建数据集(DataSet) + +```c +// 创建数据集 +DataSet* ds = DataSet_create("Events", ln0); // 名称 + LLN0 父节点 + +// 添加数据集成员(FCDA 引用) +// DataSetEntry_create(数据集, 变量名, 索引, 组件名) +// 变量名格式: "LN名$FC$DO名$DA名"(用$而非.分隔,不要LD名前缀) +DataSetEntry_create(ds, "GGIO1$ST$Ind1$stVal", -1, NULL); +DataSetEntry_create(ds, "GGIO1$ST$Ind1$q", -1, NULL); +DataSetEntry_create(ds, "GGIO1$ST$Ind1$t", -1, NULL); + +// 数据集查询 +const char* dsName = DataSet_getName(ds); +int dsSize = DataSet_getSize(ds); // 成员数 +DataSetEntry* first = DataSet_getFirstEntry(ds); +DataSetEntry* next = DataSetEntry_getNext(first); +``` + +### 2.5 设置数据属性默认值 + +```c +// 在创建 IedServer 之前可以设置默认值 +MmsValue* defaultVal = MmsValue_newFloat(0.0f); +DataAttribute_setValue(stVal, defaultVal); +MmsValue_delete(defaultVal); +``` + +### 2.6 DA 属性查询 + +```c +DataAttributeType DataAttribute_getType(DataAttribute* self); +FunctionalConstraint DataAttribute_getFC(DataAttribute* self); +uint8_t DataAttribute_getTrgOps(DataAttribute* self); +``` + +### 2.7 创建其他控制块 + +```c +// 定值组控制块 +SettingGroupControlBlock* sgcb = SettingGroupControlBlock_create( + ln0, // 父节点 LLN0 + 1, // actSG: 启动时活跃定值组 + 8); // numOfSGs: 定值组数量 + +// GOOSE 控制块 +GSEControlBlock* gcb = GSEControlBlock_create( + "gcbEvents", ln0, "appId", "TEMPLATE/LLN0$GooseDS", + 1, // confRev + false, // fixedOffs + -1, // minTime,-1=使用默认 + -1); // maxTime,-1=使用默认 +GSEControlBlock_addPhyComAddress(gcb, phyComAddress); + +// Sampled Values 控制块 +SVControlBlock* svcb = SVControlBlock_create( + "MSVCB01", ln0, "svID", "TEMPLATE/LLN0$SvDS", + 1, // confRev + IEC61850_SV_SMPMOD_SAMPLES_PER_PERIOD, // smpMod + 80, // smpRate (如 80 采样/周期) + IEC61850_SV_OPT_REFRESH_TIME | IEC61850_SV_OPT_SAMPLE_SYNC, // optFlds + false); // isUnicast: false=multicast +SVControlBlock_addPhyComAddress(svcb, phyComAddress); + +// 日志控制块 +LogControlBlock* lcb = LogControlBlock_create( + "LogCB01", ln0, "TEMPLATE/LLN0$Events", + "TEMPLATE/LLN0$MyLog", TRG_OPT_DATA_CHANGED, 60000, false, true); + +// 日志对象 +Log* log = Log_create("MyLog", ln0); + +// PhyComAddress +uint8_t mac[6] = {0x01, 0x0C, 0xCD, 0x01, 0x00, 0x01}; +PhyComAddress* addr = PhyComAddress_create( + 4, // vlanPriority + 100, // vlanId + 0x4000, // appId + mac); // dstAddress +``` + +### 2.8 模型销毁 + +```c +// 销毁动态创建的数据模型 +// 注意:一定要在 IedServer_destroy 之后调用,否则会导致资源泄漏 +IedModel_destroy(model); +``` + +### 2.9 数据属性类型枚举(DataAttributeType) + +创建 DA 时使用的类型常量: + +```c +IEC61850_BOOLEAN // MMS_BOOLEAN +IEC61850_INT8 // MMS_INTEGER (8bit) +IEC61850_INT16 // MMS_INTEGER (16bit) +IEC61850_INT32 // MMS_INTEGER (32bit) +IEC61850_INT64 // MMS_INTEGER (64bit) +IEC61850_INT8U // MMS_UNSIGNED (8bit) +IEC61850_INT16U // MMS_UNSIGNED (16bit) +IEC61850_INT32U // MMS_UNSIGNED (32bit) +IEC61850_FLOAT32 // MMS_FLOAT (32bit) +IEC61850_FLOAT64 // MMS_FLOAT (64bit) +IEC61850_QUALITY // MMS_BIT_STRING (13bit, 品质) +IEC61850_TIMESTAMP // MMS_UTC_TIME +IEC61850_VISSTRING32 // MMS_VISIBLE_STRING (max 32) +IEC61850_VISSTRING64 // MMS_VISIBLE_STRING (max 64) +IEC61850_VISSTRING129 // MMS_VISIBLE_STRING (max 129) +IEC61850_VISSTRING255 // MMS_VISIBLE_STRING (max 255) +IEC61850_DBPOS // MMS_BIT_STRING (双点位置) +IEC61850_CONSTRUCTED // 复合类型(如 AnalogueValue) +``` + +--- + +## 3. 服务器配置(IedServerConfig) + +### 3.1 配置结构体字段 + +```c +typedef struct sIedServerConfig { + int reportBufferSize; // BRCB 报告缓冲区大小 + int reportBufferSizeURCBs; // URCB 报告缓冲区大小 + char* fileServiceBasepath; // 文件服务根目录 + bool enableFileService; // 是否启用文件服务 + bool enableDynamicDataSetService; // 是否允许动态数据集 + int maxAssociationSpecificDataSets; // 每连接最大关联数据集数 + int maxDomainSpecificDataSets; // 最大域数据集数 + int maxDataSetEntries; // 数据集最大条目数 + bool enableLogService; // 是否启用日志服务 + bool useIntegratedGoosePublisher; // 是否使用内置 GOOSE 发布器 + uint8_t edition; // IEC 61850 版本:0=E1, 1=E2, 2=E2.1 + int maxMmsConnections; // 最大 MMS 连接数 + bool enableEditSG; // 是否允许 EditSG 服务 + bool enableResvTmsForSGCB; // SGCB 是否可见 ResvTms + bool enableResvTmsForBRCB; // BRCB 是否可见 ResvTms + bool enableOwnerForRCB; // RCB 是否可见 owner 属性 + bool syncIntegrityReportTimes; // 是否同步完整性报告时间 + uint8_t reportSettingsWritable; // 哪些报告设置可写(位掩码) +} IedServerConfig; +``` + +### 3.2 配置 API + +```c +IedServerConfig config = IedServerConfig_create(); + +// 标准配置 +IedServerConfig_setReportBufferSize(config, 100); // BRCB 缓冲区 +IedServerConfig_setReportBufferSizeForURCBs(config, 10); // URCB 缓冲区 +IedServerConfig_setMaxMmsConnections(config, 5); // 最大连接数 +IedServerConfig_setFileServiceBasePath(config, "./files"); +IedServerConfig_enableFileService(config, true); +IedServerConfig_setEdition(config, IEC_61850_EDITION_2); // 版本 + +// 动态数据集 +IedServerConfig_enableDynamicDataSetService(config, true); +IedServerConfig_setMaxAssociationSpecificDataSets(config, 10); +IedServerConfig_setMaxDomainSpecificDataSets(config, 5); +IedServerConfig_setMaxDataSetEntries(config, 128); + +// 日志 +IedServerConfig_enableLogService(config, false); + +// 定值组 +IedServerConfig_enableEditSG(config, true); +IedServerConfig_enableResvTmsForSGCB(config, true); + +// RCB +IedServerConfig_enableResvTmsForBRCB(config, true); +IedServerConfig_enableOwnerForRCB(config, false); + +// GOOSE +IedServerConfig_useIntegratedGoosePublisher(config, true); + +// 完整性报告时间同步 +IedServerConfig_setSyncIntegrityReportTimes(config, false); + +// 报告设置可写性(IEC61850_REPORTSETTINGS_* 常量组合) +IedServerConfig_setReportSetting(config, IEC61850_REPORTSETTINGS_RPT_ID, true); // 允许客户端修改 RptID +IedServerConfig_setReportSetting(config, IEC61850_REPORTSETTINGS_DATSET, true); // 允许客户端修改数据集 + +// 销毁配置 +IedServerConfig_destroy(config); +``` + +**报告设置可写性常量**: + +```c +#define IEC61850_REPORTSETTINGS_RPT_ID 1 +#define IEC61850_REPORTSETTINGS_BUF_TIME 2 +#define IEC61850_REPORTSETTINGS_DATSET 4 +#define IEC61850_REPORTSETTINGS_TRG_OPS 8 +#define IEC61850_REPORTSETTINGS_OPT_FIELDS 16 +#define IEC61850_REPORTSETTINGS_INTG_PD 32 +``` + +### 3.3 查询配置 + +```c +uint8_t edition = IedServerConfig_getEdition(config); +int bufSize = IedServerConfig_getReportBufferSize(config); +int urcbBufSize = IedServerConfig_getReportBufferSizeForURCBs(config); +int maxConn = IedServerConfig_getMaxMmsConnections(config); +const char* path = IedServerConfig_getFileServiceBasePath(config); +bool fsEnabled = IedServerConfig_isFileServiceEnabled(config); +bool dsEnabled = IedServerConfig_isDynamicDataSetServiceEnabled(config); +int maxAssocDS = IedServerConfig_getMaxAssociationSpecificDataSets(config); +int maxDomainDS = IedServerConfig_getMaxDomainSpecificDataSets(config); +int maxDsEntries = IedServerConfig_getMaxDatasSetEntries(config); +bool logEnabled = IedServerConfig_isLogServiceEnabled(config); +bool syncRt = IedServerConfig_getSyncIntegrityReportTimes(config); +bool resvTmsBRCB = IedServerConfig_isResvTmsForBRCBEnabled(config); +bool ownerRCB = IedServerConfig_isOwnerForRCBEnabled(config); +bool reportSetting = IedServerConfig_getReportSetting(config, IEC61850_REPORTSETTINGS_TRG_OPS); +``` + +--- + +## 4. 服务器生命周期管理 + +### 4.1 创建服务器 + +```c +// 简单创建(仅需数据模型) +IedServer server = IedServer_create(model); + +// TLS 支持 +IedServer server = IedServer_createWithTlsSupport(model, tlsConfig); + +// 完整配置创建(推荐) +IedServerConfig config = IedServerConfig_create(); +// ... 配置 config ... +IedServer server = IedServer_createWithConfig(model, NULL, config); // NULL=无TLS +// IedServerConfig_destroy(config); // 创建后可销毁 config +``` + +### 4.2 附加访问点 + +```c +// 为服务器添加额外的监听地址(可在 start 前多次添加) +// 返回 true 成功,false 失败 +IedServer_addAccessPoint(server, "192.168.2.1", 102, NULL); +``` + +### 4.3 设置服务器属性 + +```c +// 设置监听地址(默认监听所有接口) +IedServer_setLocalIpAddress(server, "0.0.0.0"); + +// 设置 MMS identify 服务响应(需要 CONFIG_IEC61850_SUPPORT_SERVER_IDENTITY) +IedServer_setServerIdentity(server, "VendorName", "ModelName", "1.0"); + +// 设置运行时文件服务根目录(需要 CONFIG_SET_FILESTORE_BASEPATH_AT_RUNTIME) +IedServer_setFilestoreBasepath(server, "/data/files"); + +// 设置时间品质(可在运行时更新) +IedServer_setTimeQuality(server, + true, // leapSecondKnown 位7: 闰秒已知 + false, // clockFailure 位6: 时钟故障 + false, // clockNotSynchronized 位5: 时钟未同步 + 10); // subsecondPrecision 位0-4: 亚秒精度(fractionOfSecond 有效位数) +``` + +### 4.4 启动/停止(线程模式) + +```c +// 启动(线程模式)—— 内部创建后台线程 +IedServer_start(server, 102); // 102 = MMS 默认端口,-1 = 使用默认端口 + +// 检查运行状态 +bool running = IedServer_isRunning(server); + +// 获取当前连接数 +int connections = IedServer_getNumberOfOpenConnections(server); + +// 停止 +IedServer_stop(server); + +// 销毁 +IedServer_destroy(server); +// 注意:需要单独销毁数据模型 IedModel_destroy(model) +``` + +### 4.5 启动/停止(非线程模式) + +```c +// 启动(非线程模式)—— 用户驱动消息循环 +IedServer_startThreadless(server, 102); + +// 主循环 +while (running) { + // 等待连接就绪(可选,类似 select) + int ready = IedServer_waitReady(server, 100); // 超时 100ms + if (ready != 0) { + // 处理收到的数据 + IedServer_processIncomingData(server); + } + // 执行周期性任务(报告生成、超时检查等) + IedServer_performPeriodicTasks(server); +} + +// 停止 +IedServer_stopThreadless(server); + +// 销毁 +IedServer_destroy(server); +``` + +### 4.6 底层访问 + +```c +// 获取数据模型 +IedModel* model = IedServer_getDataModel(server); + +// 获取底层 MmsServer(谨慎使用:直接操作可能干扰 IedServer) +MmsServer mmsServer = IedServer_getMmsServer(server); +``` + +--- + +## 5. 数据值读取与更新 + +### 5.1 读取属性值 + +```c +// 通用读取(返回 MmsValue*) +MmsValue* val = IedServer_getAttributeValue(server, stVal); + +// 类型便捷读取 +bool b = IedServer_getBooleanAttributeValue(server, da); +float f = IedServer_getFloatAttributeValue(server, da); +int32_t i32 = IedServer_getInt32AttributeValue(server, da); +int64_t i64 = IedServer_getInt64AttributeValue(server, da); +uint32_t u32 = IedServer_getUInt32AttributeValue(server, da); +uint64_t utc = IedServer_getUTCTimeAttributeValue(server, da); // ms +uint32_t bs = IedServer_getBitStringAttributeValue(server, da); +const char* s = IedServer_getStringAttributeValue(server, da); // VISIBLE_STRING/STRING + +// 获取某个 FC 下的 FCD 对象(绕过报告通知机制,直接操作底层值) +MmsValue* fcd = IedServer_getFunctionalConstrainedData(server, dataObject, IEC61850_FC_ST); +// 警告:直接操作 FCD 不会触发报告,需谨慎使用 +``` + +### 5.2 更新属性值(自动触发报告和 GOOSE) + +```c +// 通用更新 +IedServer_updateAttributeValue(server, da, mmsValue); + +// 类型便捷更新 — 这些函数会自动检查触发条件(dchg/qchg/dupd)并触发报告 +IedServer_updateFloatAttributeValue(server, da, 35.5f); +IedServer_updateInt32AttributeValue(server, da, 42); +IedServer_updateInt64AttributeValue(server, da, 12345678901234LL); +IedServer_updateUnsignedAttributeValue(server, da, 100); +IedServer_updateBooleanAttributeValue(server, da, true); +IedServer_updateVisibleStringAttributeValue(server, da, "Hello"); +IedServer_updateBitStringAttributeValue(server, da, bitStringInt); +IedServer_updateUTCTimeAttributeValue(server, da, msTimestamp); +IedServer_updateTimestampAttributeValue(server, da, timestamp); +IedServer_updateDbposValue(server, da, DBPOS_ON); // 双点:ON/OFF/中间态/坏态 +IedServer_updateQuality(server, da, quality); // 品质更新(触发 qchg) +``` + +### 5.3 批量更新(加锁) + +```c +// 更新多个值时加锁以提高效率 +IedServer_lockDataModel(server); + +IedServer_updateFloatAttributeValue(server, da1, 1.5f); +IedServer_updateFloatAttributeValue(server, da2, 2.5f); +IedServer_updateFloatAttributeValue(server, da3, 3.5f); + +IedServer_unlockDataModel(server); // 解锁后将触发一次通知 + +// 警告:绝对不要在库回调函数内部调用 lockDataModel! +// 库回调中数据模型已经锁定,再次锁定会导致死锁 +``` + +### 5.4 更新控制模型 + +```c +// 更新可控数据对象的控制模型 +IedServer_updateCtlModel(server, ctlObject, CONTROL_MODEL_SBO_NORMAL); +// 注意:对应的控制结构必须在数据模型中存在 +``` + +--- + +## 6. 控制模型回调 + +服务端通过三个层级的回调实现 IEC 61850 的遥控机制。 + +### 6.1 控制操作完整流程 + +``` +客户端发送控制命令 + ↓ +PerformCheck 回调(静态测试:联锁、权限等) + ↓ (通过) +WaitForExecution 回调(动态测试:同步检查等) + ↓ (通过) +Control 回调(实际执行:操作继电器等) + ↓ +CommandTermination 响应返回客户端 +``` + +### 6.2 回调返回值 + +```c +// PerformCheck 回调返回值 +typedef enum { + CONTROL_ACCEPTED = -1, // 检查通过 + CONTROL_WAITING_FOR_SELECT = 0, // 选择进行中(稍后重试) + CONTROL_HARDWARE_FAULT = 1, // 硬件故障 + CONTROL_TEMPORARILY_UNAVAILABLE = 2,// 暂时不可用(已选中或正操作) + CONTROL_OBJECT_ACCESS_DENIED = 3, // 拒绝访问 + CONTROL_OBJECT_UNDEFINED = 4, // 对象未定义 + CONTROL_VALUE_INVALID = 11 // ctlVal 超出范围 +} CheckHandlerResult; + +// Control / WaitForExecution 回调返回值 +typedef enum { + CONTROL_RESULT_FAILED = 0, // 失败 + CONTROL_RESULT_OK = 1, // 成功 + CONTROL_RESULT_WAITING = 2 // 等待中(异步执行,稍后再次调用) +} ControlHandlerResult; +``` + +### 6.3 ControlAction 上下文信息 + +```c +typedef void* ControlAction; + +// 设置错误附加信息(在回调中使用) +void ControlAction_setError(ControlAction self, ControlLastApplError error); +void ControlAction_setAddCause(ControlAction self, ControlAddCause addCause); + +// 获取客户端上下文 +int ControlAction_getOrCat(ControlAction self); // 发起者类别 +uint8_t* ControlAction_getOrIdent(ControlAction self, int* size); // 发起者标识 +int ControlAction_getCtlNum(ControlAction self); // 控制序号 +bool ControlAction_getSynchroCheck(ControlAction self); // 同步检查位 +bool ControlAction_getInterlockCheck(ControlAction self); // 联锁检查位 +bool ControlAction_isSelect(ControlAction self); // 是否是 Select 操作 +ClientConnection ControlAction_getClientConnection(ControlAction self); // 客户端连接 +DataObject* ControlAction_getControlObject(ControlAction self); // 控制对象 +uint64_t ControlAction_getControlTime(ControlAction self); // 时间激活控制时间(0=立即) +``` + +### 6.4 注册控制回调 + +```c +// 实际执行回调(必须注册) +CheckHandlerResult myPerformCheck(ControlAction action, void* param, + MmsValue* ctlVal, bool test, bool interlockCheck) { + // 静态测试:检查联锁条件、访问权限等 + if (!interlock_ok) { + ControlAction_setAddCause(action, ADD_CAUSE_BLOCKED_BY_INTERLOCKING); + return CONTROL_OBJECT_ACCESS_DENIED; + } + return CONTROL_ACCEPTED; +} + +ControlHandlerResult myWaitForExecution(ControlAction action, void* param, + MmsValue* ctlVal, bool test, bool synchroCheck) { + // 动态测试:检查同步条件等 + // 如果测试不能立即完成,返回 CONTROL_RESULT_WAITING,稍后会再次调用 + if (needs_wait) return CONTROL_RESULT_WAITING; + return CONTROL_RESULT_OK; +} + +ControlHandlerResult myControl(ControlAction action, void* param, + MmsValue* ctlVal, bool test) { + // 实际执行:控制继电器、输出信号等 + bool val = MmsValue_getBoolean(ctlVal); + if (test) { + // 测试模式:不影响实际物理过程 + return CONTROL_RESULT_OK; + } + set_output(val); // 控制硬件 + return CONTROL_RESULT_OK; +} + +// 注册三个层级的回调 +IedServer_setControlHandler(server, spcso1, myControl, myParam); +IedServer_setPerformCheckHandler(server, spcso1, myPerformCheck, myParam); +IedServer_setWaitForExecutionHandler(server, spcso1, myWaitForExecution, myParam); +``` + +### 6.5 Select 状态变化回调 + +```c +typedef enum { + SELECT_STATE_REASON_SELECTED, // 被选中 + SELECT_STATE_REASON_CANCELED, // 取消 + SELECT_STATE_REASON_TIMEOUT, // 超时(sboTimeout) + SELECT_STATE_REASON_OPERATED, // 操作成功 + SELECT_STATE_REASON_OPERATE_FAILED, // 操作失败 + SELECT_STATE_REASON_DISCONNECTED // 选中客户端断开连接 +} SelectStateChangedReason; + +void mySelectStateChanged(ControlAction action, void* param, + bool isSelected, SelectStateChangedReason reason) { + if (isSelected) { + // 被客户端选中 + } else { + // 取消选中(原因见 reason) + } +} + +IedServer_setSelectStateChangedHandler(server, spcso1, mySelectStateChanged, myParam); +``` + +### 6.6 AddCause 完整枚举 + +```c +typedef enum { + ADD_CAUSE_UNKNOWN = 0, + ADD_CAUSE_NOT_SUPPORTED = 1, + ADD_CAUSE_BLOCKED_BY_SWITCHING_HIERARCHY = 2, + ADD_CAUSE_SELECT_FAILED = 3, + ADD_CAUSE_INVALID_POSITION = 4, + ADD_CAUSE_POSITION_REACHED = 5, + ADD_CAUSE_PARAMETER_CHANGE_IN_EXECUTION = 6, + ADD_CAUSE_STEP_LIMIT = 7, + ADD_CAUSE_BLOCKED_BY_MODE = 8, + ADD_CAUSE_BLOCKED_BY_PROCESS = 9, + ADD_CAUSE_BLOCKED_BY_INTERLOCKING = 10, + ADD_CAUSE_BLOCKED_BY_SYNCHROCHECK = 11, + ADD_CAUSE_COMMAND_ALREADY_IN_EXECUTION = 12, + ADD_CAUSE_BLOCKED_BY_HEALTH = 13, + ADD_CAUSE_1_OF_N_CONTROL = 14, + ADD_CAUSE_ABORTION_BY_CANCEL = 15, + ADD_CAUSE_TIME_LIMIT_OVER = 16, + ADD_CAUSE_ABORTION_BY_TRIP = 17, + ADD_CAUSE_OBJECT_NOT_SELECTED = 18, + ADD_CAUSE_OBJECT_ALREADY_SELECTED = 19, + ADD_CAUSE_NO_ACCESS_AUTHORITY = 20, + ADD_CAUSE_ENDED_WITH_OVERSHOOT = 21, + ADD_CAUSE_ABORTION_DUE_TO_DEVIATION = 22, + ADD_CAUSE_ABORTION_BY_COMMUNICATION_LOSS = 23, + ADD_CAUSE_ABORTION_BY_COMMAND = 24, + ADD_CAUSE_NONE = 25, + ADD_CAUSE_INCONSISTENT_PARAMETERS = 26, + ADD_CAUSE_LOCKED_BY_OTHER_CLIENT = 27 +} ControlAddCause; +``` + +--- + +## 7. 报告控制块(RCB)事件处理 + +### 7.1 RCB 事件类型 + +```c +typedef enum { + RCB_EVENT_GET_PARAMETER, // 参数被读取(暂未实现) + RCB_EVENT_SET_PARAMETER, // 参数被客户端设置 + RCB_EVENT_UNRESERVED, // 取消预留 + RCB_EVENT_RESERVED, // 被预留 + RCB_EVENT_ENABLE, // 被使能 + RCB_EVENT_DISABLE, // 被停用 + RCB_EVENT_GI, // 总召触发 + RCB_EVENT_PURGEBUF, // 清除缓冲区 + RCB_EVENT_OVERFLOW, // 报告缓冲区溢出 + RCB_EVENT_REPORT_CREATED // 新报告创建并插入缓冲区 +} IedServer_RCBEventType; +``` + +### 7.2 完整 RCB 事件回调 + +```c +void rcbEventHandler(void* parameter, ReportControlBlock* rcb, + ClientConnection connection, IedServer_RCBEventType event, + const char* parameterName, MmsDataAccessError serviceError) { + + const char* rcbName = ReportControlBlock_getName(rcb); + char* rptId = ReportControlBlock_getRptID(rcb); + char* dataSet = ReportControlBlock_getDataSet(rcb); + + switch (event) { + case RCB_EVENT_ENABLE: + // 客户端使能了 RCB → 可以开始上送数据 + printf("RCB %s (RptID=%s) 已使能\n", rcbName, rptId); + break; + + case RCB_EVENT_DISABLE: + // 客户端停用了 RCB → 暂停上送 + break; + + case RCB_EVENT_RESERVED: + // 客户端独占了 URCB + break; + + case RCB_EVENT_UNRESERVED: + // 客户端释放了 URCB + break; + + case RCB_EVENT_GI: + // 客户端触发了总召 + // 库会自动执行总召,这里可以记录日志 + break; + + case RCB_EVENT_SET_PARAMETER: + if (serviceError != DATA_ACCESS_ERROR_SUCCESS) { + // 参数设置失败 + printf("RCB 参数设置失败: param=%s, err=%d\n", + parameterName, serviceError); + } else { + // 参数设置成功 + if (parameterName && strcmp(parameterName, "RptEna") == 0) { + bool ena = ReportControlBlock_getRptEna(rcb); + // ... + } + if (parameterName && strcmp(parameterName, "DatSet") == 0) { + // 客户端改变了数据集 → 需要更新本地 report 数据结构 + // 重新调用 IedConnection_installReportHandler(客户端场景) + } + } + break; + + case RCB_EVENT_OVERFLOW: + // 报告缓冲区溢出(仅 BRCB) + // 可能需要重新配置缓冲区大小或检查周期性积分周期 + break; + + case RCB_EVENT_REPORT_CREATED: + // 新报告已创建(可用于调试/监控) + break; + + case RCB_EVENT_PURGEBUF: + // 客户端清除了缓冲区 + break; + + default: + break; + } + + free(rptId); + free(dataSet); +} + +// 注册 +IedServer_setRCBEventHandler(server, rcbEventHandler, myParam); +``` + +--- + +## 8. 读写访问控制 + +### 8.1 写访问控制(单个数据属性) + +```c +// 写访问回调 +MmsDataAccessError myWriteAccessHandler( + DataAttribute* dataAttribute, MmsValue* value, + ClientConnection connection, void* parameter) { + + // 检查客户端是否有写权限 + if (!client_has_permission(connection)) { + return DATA_ACCESS_ERROR_OBJECT_ACCESS_DENIED; + } + + // 检查值是否在允许范围内(可选) + if (MmsValue_toFloat(value) > 100.0f) { + return DATA_ACCESS_ERROR_OBJECT_VALUE_INVALID; + } + + return DATA_ACCESS_ERROR_SUCCESS; // 接受,库自动更新值 + // 或 return DATA_ACCESS_ERROR_SUCCESS_NO_UPDATE; // 接受但库不更新(自定义逻辑) +} + +// 为单个数据属性注册写访问控制 +IedServer_handleWriteAccess(server, stVal, myWriteAccessHandler, myParam); + +// 为复合属性及其所有子属性注册写访问控制 +IedServer_handleWriteAccessForComplexAttribute(server, complexDA, myWriteAccessHandler, myParam); +``` + +### 8.2 全局写访问策略 + +```c +typedef enum { + ACCESS_POLICY_ALLOW, // 允许 + ACCESS_POLICY_DENY // 拒绝 +} AccessPolicy; + +// 设置某个 FC 的全局默认写访问策略 +IedServer_setWriteAccessPolicy(server, IEC61850_FC_SP, ACCESS_POLICY_DENY); +IedServer_setWriteAccessPolicy(server, IEC61850_FC_SE, ACCESS_POLICY_ALLOW); +``` + +### 8.3 读访问控制(全局) + +```c +// 全局读访问回调(对每个读请求调用) +MmsDataAccessError myReadAccessHandler( + LogicalDevice* ld, LogicalNode* ln, DataObject* dataObject, + FunctionalConstraint fc, ClientConnection connection, void* parameter) { + + // 示例:禁止特定客户端读取 CO 数据 + if (fc == IEC61850_FC_CO && is_restricted_client(connection)) { + return DATA_ACCESS_ERROR_OBJECT_ACCESS_DENIED; + } + + return DATA_ACCESS_ERROR_SUCCESS; +} + +IedServer_setReadAccessHandler(server, myReadAccessHandler, myParam); +``` + +--- + +## 9. 定值组(SGCB)处理 + +### 9.1 内部切换定值组 + +```c +// 内部事件导致活跃定值组变化 +IedServer_changeActiveSettingGroup(server, sgcb, 3); // 切换到第3组 + +// 获取当前活跃定值组号 +uint8_t activeSG = IedServer_getActiveSettingGroup(server, sgcb); +``` + +### 9.2 定值组变化回调 + +```c +// 活跃定值组切换前回调(可拒绝切换) +bool myActiveSGChanged(void* parameter, SettingGroupControlBlock* sgcb, + uint8_t newActSg, ClientConnection connection) { + if (newActSg > 8) return false; // 拒绝无效的定值组 + // 更新 SG 相关数据属性(FC=SG) + return true; // 接受 +} +IedServer_setActiveSettingGroupChangedHandler(server, sgcb, myActiveSGChanged, param); + +// 编辑定值组切换前回调 +bool myEditSGChanged(void* parameter, SettingGroupControlBlock* sgcb, + uint8_t newEditSg, ClientConnection connection) { + // 更新 SE 数据属性 + return true; +} +IedServer_setEditSettingGroupChangedHandler(server, sgcb, myEditSGChanged, param); + +// 编辑定值组确认回调 +void myEditSGConfirmed(void* parameter, SettingGroupControlBlock* sgcb, uint8_t editSg) { + // 编辑组已确认,将 SG 数据复制到 SE 组 +} +IedServer_setEditSettingGroupConfirmationHandler(server, sgcb, myEditSGConfirmed, param); +``` + +--- + +## 10. GOOSE 发布 + +### 10.1 启用/禁用 GOOSE + +```c +// 批量启用所有 GoCB +IedServer_enableGoosePublishing(server); + +// 批量禁用所有 GoCB +IedServer_disableGoosePublishing(server); + +// 设置 GOOSE 网络接口(操作系统相关) +IedServer_setGooseInterfaceId(server, "eth0"); + +// 设置特定 GoCB 的网络接口 +IedServer_setGooseInterfaceIdEx(server, someLN, "gcbEvents", "eth1"); + +// 启用/禁用 VLAN 标签(全局或特定 GoCB) +IedServer_useGooseVlanTag(server, NULL, NULL, true); // 全部启用 +IedServer_useGooseVlanTag(server, someLN, "gcbEvents", false); // 特定 GoCB 禁用 +``` + +### 10.2 GoCB 事件回调 + +```c +void goCBEventHandler(MmsGooseControlBlock goCb, int event, void* parameter) { + if (event == IEC61850_GOCB_EVENT_ENABLE) { + char* name = MmsGooseControlBlock_getName(goCb); + LogicalNode* ln = MmsGooseControlBlock_getLogicalNode(goCb); + DataSet* ds = MmsGooseControlBlock_getDataSet(goCb); + bool enabled = MmsGooseControlBlock_getGoEna(goCb); + int minTime = MmsGooseControlBlock_getMinTime(goCb); + int maxTime = MmsGooseControlBlock_getMaxTime(goCb); + bool fixedOffs = MmsGooseControlBlock_getFixedOffs(goCb); + bool ndsCom = MmsGooseControlBlock_getNdsCom(goCb); + free(name); + } + // event == IEC61850_GOCB_EVENT_DISABLE 停用 +} + +IedServer_setGoCBHandler(server, goCBEventHandler, myParam); +``` + +### 10.3 GoCB 信息查询 + +```c +char* MmsGooseControlBlock_getName(MmsGooseControlBlock self); +LogicalNode* MmsGooseControlBlock_getLogicalNode(MmsGooseControlBlock self); +DataSet* MmsGooseControlBlock_getDataSet(MmsGooseControlBlock self); +bool MmsGooseControlBlock_getGoEna(MmsGooseControlBlock self); +int MmsGooseControlBlock_getMinTime(MmsGooseControlBlock self); +int MmsGooseControlBlock_getMaxTime(MmsGooseControlBlock self); +bool MmsGooseControlBlock_getFixedOffs(MmsGooseControlBlock self); +bool MmsGooseControlBlock_getNdsCom(MmsGooseControlBlock self); +``` + +--- + +## 11. SV 控制块 + +```c +// SV 控制块回调 +void svcBEventHandler(SVControlBlock* svcb, int event, void* parameter) { + if (event == IEC61850_SVCB_EVENT_ENABLE) { + // SV 被客户端使能 + } + // event == IEC61850_SVCB_EVENT_DISABLE 停用 +} + +IedServer_setSVCBHandler(server, svcb, svcBEventHandler, myParam); +``` + +--- + +## 12. 连接管理与认证 + +### 12.1 连接认证 + +```c +// ACSE 认证回调 +bool myAuthenticator(void* parameter, AcseAuthenticationParameter* authParameter) { + // 从 authParameter 中提取认证信息 + // 返回 true 接受连接,false 拒绝连接 + return true; +} + +IedServer_setAuthenticator(server, myAuthenticator, authParam); +``` + +### 12.2 连接状态变化回调 + +```c +void connHandler(IedServer self, ClientConnection connection, bool connected, void* parameter) { + const char* peer = ClientConnection_getPeerAddress(connection); + const char* local = ClientConnection_getLocalAddress(connection); + void* token = ClientConnection_getSecurityToken(connection); // 认证令牌 + + if (connected) { + printf("新客户端连接: %s\n", peer); + } else { + printf("客户端断开: %s\n", peer); + } +} + +IedServer_setConnectionIndicationHandler(server, connHandler, myParam); +``` + +### 12.3 ClientConnection API + +```c +const char* ClientConnection_getPeerAddress(ClientConnection self); // 客户端IP +const char* ClientConnection_getLocalAddress(ClientConnection self); // 服务端本地IP +void* ClientConnection_getSecurityToken(ClientConnection self); // 认证令牌 +``` + +--- + +## 13. 日志服务 + +```c +// 设置日志存储(将日志控制块关联到日志存储实例) +IedServer_setLogStorage(server, "TEMPLATE/LLN0$MyLog", logStorage); +``` + +--- + +## 14. 运行模式:线程模式 vs 非线程模式 + +### 线程模式(默认,推荐) + +```c +IedServer server = IedServer_createWithConfig(model, NULL, config); +IedServer_start(server, 102); +// 库内部创建线程处理所有客户端连接和周期性任务 +// 用户只需调用 update 系列函数更新数据 + +IedServer_updateFloatAttributeValue(server, da, value); // 安全,随时可调用 +``` + +### 非线程模式(嵌入式/特殊场景) + +```c +IedServer server = IedServer_createWithConfig(model, NULL, config); +IedServer_startThreadless(server, 102); + +while (running) { + int ready = IedServer_waitReady(server, 100); + if (ready) { + IedServer_processIncomingData(server); + } + IedServer_performPeriodicTasks(server); // 报告、超时等 +} + +IedServer_stopThreadless(server); +``` + +**非线程模式的限制**: +- 必须周期性调用 `processIncomingData` 和 `performPeriodicTasks` +- 这些函数的调用频率直接影响响应速度和定时精度 +- 报告周期精度取决于 `performPeriodicTasks` 的调用间隔 + +--- + +## 15. 与 RTU 项目对照 + +RTU 项目在 `src/protocol/libmms_s/` 中封装了服务端 API。 + +### 核心对应关系 + +| RTU 封装层 | libiec61850 API | +|-----------|----------------| +| `mms_s_init(icd_path, port)` | 完整初始化流程 | +| `mms_s_get_ied_server_ptr()` → `gp_iedServer` | `IedServer` 全局句柄 | +| `mms_s_model.cpp : model_init()` | `IedModel_create()` → `LogicalDevice_create()` → ... | +| `mms_s_value.cpp : mms_s_values_init()` | `IedServer` initializer 回调 → 遍历 DA 设置默认值 | +| `mms_s_control.cpp : control_init()` | `IedServer_setControlHandler()` | +| `mms_s_param.cpp : param_init()` | `IedServer_setActiveSettingGroupChangedHandler()` + EditSG | +| `mms_s_setting.cpp : setting_init()` | 初始化定值组数据 | +| `mms_s_file.cpp : file_init()` | `IedServerConfig_enableFileService()` | +| `rcbEventHandler()` (在 mms_s.cpp 中) | `IedServer_setRCBEventHandler()` | +| `mms_s_run_task()` 后台线程 | 线程模式下自动处理(RTU 仅用锁持有维持运行) | +| `mms_s_dbg_switch()` | `LOG_I` 条件输出开关 | + +### RTU 初始化流程(对应 libiec61850 API 调用) + +```cpp +// RTU 中的 mms_s_init() 流程 +// 1. icd_parse(icd_path) → 解析 ICD XML 文件为 stru_icd 结构 +// 2. model_init(*gp_icd) → IedModel_create() + 遍历 ICD 创建 LD/LN/DO/DA/RCB/DataSet +// 3. IedServerConfig_create() → IedServerConfig_enableResvTmsForSGCB(true) +// 4. IedServer_createWithConfig(iedModel, NULL, serverConfig) +// 5. IedServer_setTimeQuality(server, true, false, false, 10) +// 6. control_init() → 遍历可控 DO,安装 ControlHandler +// 7. param_init() → 安装 SGCB 回调 +// 8. file_init() → 配置文件服务 +// 9. IedServer_setRCBEventHandler(server, rcbEventHandler, NULL) +// 10. IedServer_start(server, port) +// 11. setting_init() → 初始化定值组数据 +// 12. Thread_create(mms_s_run_task, iedModel, false) → 后台线程 +``` + +### RTU 后台线程的特殊处理 + +```cpp +// RTU 使用线程模式,但创建了额外的后台线程来持有锁: +void* mms_s_run_task(void* parameter) { + while(g_running) { + IedServer_lockDataModel(gp_iedServer); + IedServer_unlockDataModel(gp_iedServer); + Thread_sleep(100); + } + IedServer_stop(gp_iedServer); + IedServer_destroy(gp_iedServer); + IedModel_destroy(iedModel); // 销毁数据模型 + return NULL; +} +``` + +--- + +## 16. 附录:公共数据类型速查 + +### 16.1 Quality 操作 + +```c +typedef uint16_t Quality; + +// 有效性 +#define QUALITY_VALIDITY_GOOD 0 +#define QUALITY_VALIDITY_INVALID 2 +#define QUALITY_VALIDITY_QUESTIONABLE 3 + +// 详情标志 +#define QUALITY_DETAIL_OVERFLOW 4 +#define QUALITY_DETAIL_OUT_OF_RANGE 8 +#define QUALITY_DETAIL_FAILURE 64 +#define QUALITY_DETAIL_OLD_DATA 128 +#define QUALITY_SOURCE_SUBSTITUTED 1024 +#define QUALITY_TEST 2048 +#define QUALITY_OPERATOR_BLOCKED 4096 + +Validity Quality_getValidity(Quality* self); +void Quality_setValidity(Quality* self, Validity validity); +void Quality_setFlag(Quality* self, int flag); +void Quality_unsetFlag(Quality* self, int flag); +bool Quality_isFlagSet(Quality* self, int flag); +``` + +### 16.2 Timestamp 操作 + +```c +typedef union { uint8_t val[8]; } Timestamp; + +Timestamp* Timestamp_create(void); +void Timestamp_destroy(Timestamp* self); +uint32_t Timestamp_getTimeInSeconds(Timestamp* self); +uint64_t Timestamp_getTimeInMs(Timestamp* self); +void Timestamp_setTimeInMilliseconds(Timestamp* self, uint64_t msTime); +void Timestamp_setLeapSecondKnown(Timestamp* self, bool value); +void Timestamp_setClockFailure(Timestamp* self, bool value); +void Timestamp_setClockNotSynchronized(Timestamp* self, bool value); +void Timestamp_setSubsecondPrecision(Timestamp* self, int precision); +``` + +### 16.3 Dbpos(双点位置) + +```c +typedef enum { + DBPOS_INTERMEDIATE_STATE = 0, // 中间态 + DBPOS_OFF = 1, // 分 + DBPOS_ON = 2, // 合 + DBPOS_BAD_STATE = 3 // 坏态 +} Dbpos; +``` + +### 16.4 触发选项 + +```c +#define TRG_OPT_DATA_CHANGED 1 // 数据变化触发 +#define TRG_OPT_QUALITY_CHANGED 2 // 品质变化触发 +#define TRG_OPT_DATA_UPDATE 4 // 数据更新触发 +#define TRG_OPT_INTEGRITY 8 // 周期性触发 +#define TRG_OPT_GI 16 // 总召触发 +#define TRG_OPT_TRANSIENT 128 // 仅上升沿触发 +``` + +### 16.5 报告选项 + +```c +#define RPT_OPT_SEQ_NUM 1 +#define RPT_OPT_TIME_STAMP 2 +#define RPT_OPT_REASON_FOR_INCLUSION 4 +#define RPT_OPT_DATA_SET 8 +#define RPT_OPT_DATA_REFERENCE 16 +#define RPT_OPT_BUFFER_OVERFLOW 32 +#define RPT_OPT_ENTRY_ID 64 +#define RPT_OPT_CONF_REV 128 +``` + +### 16.6 控制模型 + +```c +typedef enum { + CONTROL_MODEL_STATUS_ONLY = 0, + CONTROL_MODEL_DIRECT_NORMAL = 1, + CONTROL_MODEL_SBO_NORMAL = 2, + CONTROL_MODEL_DIRECT_ENHANCED = 3, + CONTROL_MODEL_SBO_ENHANCED = 4 +} ControlModel; +``` + +### 16.7 MmsValue 基础构造(服务端常用) + +```c +MmsValue* MmsValue_newBoolean(bool); +MmsValue* MmsValue_newFloat(float); +MmsValue* MmsValue_newDouble(double); +MmsValue* MmsValue_newIntegerFromInt32(int32_t); +MmsValue* MmsValue_newIntegerFromInt64(int64_t); +MmsValue* MmsValue_newUnsignedFromUint32(uint32_t); +MmsValue* MmsValue_newVisibleString(const char*); +MmsValue* MmsValue_newBitString(int bitSize); +MmsValue* MmsValue_newUtcTimeByMsTime(uint64_t msTime); +void MmsValue_delete(MmsValue*); +``` + +### 16.8 数据访问错误 + +```c +typedef enum { + DATA_ACCESS_ERROR_SUCCESS_NO_UPDATE = -3, + DATA_ACCESS_ERROR_NO_RESPONSE = -2, + DATA_ACCESS_ERROR_SUCCESS = -1, + DATA_ACCESS_ERROR_OBJECT_INVALIDATED = 0, + DATA_ACCESS_ERROR_HARDWARE_FAULT = 1, + DATA_ACCESS_ERROR_TEMPORARILY_UNAVAILABLE = 2, + DATA_ACCESS_ERROR_OBJECT_ACCESS_DENIED = 3, + DATA_ACCESS_ERROR_OBJECT_UNDEFINED = 4, + DATA_ACCESS_ERROR_INVALID_ADDRESS = 5, + DATA_ACCESS_ERROR_TYPE_UNSUPPORTED = 6, + DATA_ACCESS_ERROR_TYPE_INCONSISTENT = 7, + DATA_ACCESS_ERROR_OBJECT_ATTRIBUTE_INCONSISTENT = 8, + DATA_ACCESS_ERROR_OBJECT_ACCESS_UNSUPPORTED = 9, + DATA_ACCESS_ERROR_OBJECT_NONE_EXISTENT = 10, + DATA_ACCESS_ERROR_OBJECT_VALUE_INVALID = 11, + DATA_ACCESS_ERROR_UNKNOWN = 12 +} MmsDataAccessError; +``` + +--- + +> 本文档基于 `libiec61850-1.5.3/src/iec61850/inc/iec61850_server.h` (1896行)、`iec61850_dynamic_model.h` (539行)、`iec61850_common.h` (548行) 和 `mms_value.h` (1062行) 完整归纳,覆盖所有公开 C API。 diff --git a/claude/工程/libiec61850m模块分析.md b/claude/工程/libiec61850m模块分析.md new file mode 100644 index 0000000..cf25621 --- /dev/null +++ b/claude/工程/libiec61850m模块分析.md @@ -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 + + + + + + + + + + + + + + + + + + + + +``` + +### 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 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 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 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 # 手动遥控(框架已有,部分代码注释) +iec61850m set # 手动设值(框架已有) +``` + +注册方式:`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 │ + └─────────────────────────────────────┘ +``` diff --git a/claude/工程/libmms_m模块分析.md b/claude/工程/libmms_m模块分析.md new file mode 100644 index 0000000..49ba06e --- /dev/null +++ b/claude/工程/libmms_m模块分析.md @@ -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 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 ldevs; // 逻辑设备树(LD→LN→DO→Point) + std::vector 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 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,其他编号直接丢弃 diff --git a/claude/问题处理文档.md b/claude/问题处理文档.md new file mode 100644 index 0000000..6b2cd25 --- /dev/null +++ b/claude/问题处理文档.md @@ -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` + +---