From f4b84506a62c973487fcafd7e01d84b2e2500ea4 Mon Sep 17 00:00:00 2001 From: ypc <15051963820@163.com> Date: Fri, 12 Jun 2026 16:48:42 +0800 Subject: [PATCH] =?UTF-8?q?<=E4=BF=AE=E6=94=B9>=201=E3=80=81=E5=88=9B?= =?UTF-8?q?=E5=BB=BAmimo=E5=88=86=E6=94=AF=EF=BC=8C=E5=90=8E=E7=BB=AD?= =?UTF-8?q?=E6=AD=A4=E5=88=86=E6=94=AF=E4=BD=BF=E7=94=A8=E5=B0=8F=E7=B1=B3?= =?UTF-8?q?mimo=20code=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/memory/MEMORY.md | 3 - .claude/memory/code-style.md | 79 - .claude/memory/project-rules.md | 24 - .../工程/libiec61850_MMS客户端API开发手册.md | 1207 --------------- .../工程/libiec61850_MMS服务端API开发手册.md | 1294 ----------------- claude/工程/libiec61850m模块分析.md | 354 ----- claude/工程/libiec61850s模块分析.md | 273 ---- claude/工程/libmms_m模块分析.md | 671 --------- claude/工程/libmms_s模块分析.md | 379 ----- claude/工程/libweb_server模块分析.md | 217 --- claude/工程/websocket-server.md | 598 -------- mimo/MEMORY.md | 118 ++ .../user_language.md => mimo/global-MEMORY.md | 13 +- mimo/plan/RTU_Claude到Mimo文档迁移.md | 70 + .../中间文档}/RCB订阅编号可配置化.md | 0 .../mid => mimo/中间文档}/Tab命令补全功能.md | 0 .../中间文档}/app_cmd_CPU108问题修复.md | 0 mimo/工程/libiec61850_MMS客户端API开发手册.md | 304 ++++ mimo/工程/libiec61850_MMS服务端API开发手册.md | 266 ++++ mimo/工程/libiec61850m模块分析.md | 194 +++ mimo/工程/libiec61850s模块分析.md | 214 +++ mimo/工程/libmms_m模块分析.md | 224 +++ mimo/工程/libmms_s模块分析.md | 283 ++++ mimo/工程/libweb_server模块分析.md | 201 +++ mimo/工程/websocket-server.md | 267 ++++ {claude => mimo}/问题处理文档.md | 0 26 files changed, 2146 insertions(+), 5107 deletions(-) delete mode 100644 .claude/memory/MEMORY.md delete mode 100644 .claude/memory/code-style.md delete mode 100644 .claude/memory/project-rules.md delete mode 100644 claude/工程/libiec61850_MMS客户端API开发手册.md delete mode 100644 claude/工程/libiec61850_MMS服务端API开发手册.md delete mode 100644 claude/工程/libiec61850m模块分析.md delete mode 100644 claude/工程/libiec61850s模块分析.md delete mode 100644 claude/工程/libmms_m模块分析.md delete mode 100644 claude/工程/libmms_s模块分析.md delete mode 100644 claude/工程/libweb_server模块分析.md delete mode 100644 claude/工程/websocket-server.md create mode 100644 mimo/MEMORY.md rename .claude/memory/user_language.md => mimo/global-MEMORY.md (52%) create mode 100644 mimo/plan/RTU_Claude到Mimo文档迁移.md rename {claude/mid => mimo/中间文档}/RCB订阅编号可配置化.md (100%) rename {claude/mid => mimo/中间文档}/Tab命令补全功能.md (100%) rename {claude/mid => mimo/中间文档}/app_cmd_CPU108问题修复.md (100%) create mode 100644 mimo/工程/libiec61850_MMS客户端API开发手册.md create mode 100644 mimo/工程/libiec61850_MMS服务端API开发手册.md create mode 100644 mimo/工程/libiec61850m模块分析.md create mode 100644 mimo/工程/libiec61850s模块分析.md create mode 100644 mimo/工程/libmms_m模块分析.md create mode 100644 mimo/工程/libmms_s模块分析.md create mode 100644 mimo/工程/libweb_server模块分析.md create mode 100644 mimo/工程/websocket-server.md rename {claude => mimo}/问题处理文档.md (100%) diff --git a/.claude/memory/MEMORY.md b/.claude/memory/MEMORY.md deleted file mode 100644 index b80da06..0000000 --- a/.claude/memory/MEMORY.md +++ /dev/null @@ -1,3 +0,0 @@ -- [user-language](user_language.md) — 用户要求所有对话和思考过程使用中文显示 -- [project-rules](project-rules.md) — 项目开发规范:编译/运行路径、排查流程、文档管理、plan归档到mid目录 -- [code-style](code-style.md) — C/C++代码格式规范:Tab缩进、Allman括号、Yoda条件、空行规则 diff --git a/.claude/memory/code-style.md b/.claude/memory/code-style.md deleted file mode 100644 index 2f05c97..0000000 --- a/.claude/memory/code-style.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -name: code-style -description: 项目 C/C++ 代码格式规范 — 缩进、括号、空格、空行、条件写法 -metadata: - type: feedback ---- - -## 缩进 - -使用 **Tab 字符**缩进(显示宽度 4),不使用空格缩进。 - -## 括号风格(Allman 风格) - -所有大括号 **独占一行**,包括函数、if、for、while、struct: - -```cpp -LOCAL void check_sg_zone_change() -{ - if(g_sg_zone_saddr.empty()) - { - return; - } - - for(auto &setting : g_vec_setting) - { - if(strcmp(setting.base.saddr, zone_saddr) == 0) - { - break; - } - } -} -``` - -单条语句也保留大括号,不省略。 - -## 空格规则 - -- 关键字与括号之间 **不加空格**:`if(`, `for(`, `while(`, `switch(` -- 函数名与括号之间 **不加空格**:`func(args)` -- 指针声明:`type *name`(`*` 前有空格,后无空格) -- 引用声明:`type &name` - -## Yoda 条件 - -常量写在比较运算符左侧: - -```cpp -if(NULL == ptr) // ✓ 正确 -if(0 != ret) // ✓ 正确 -if(ptr == NULL) // ✗ 不符合项目风格 -``` - -## 空行 - -- 函数之间:两行空行 -- 逻辑块之间:一行空行 -- `#include` 区块末尾:一行空行 - -## typedef struct - -```cpp -typedef struct -{ - uint32_t field1; - uint8_t field2; -}stru_type_name; -``` - -结构体成员无缩进额外层级(与 `{` 对齐)。 - -## 变量声明 - -- 局部静态变量用 `LOCAL` 宏(= `static`) -- 全局变量用 `g_` 前缀 -- 结构体用 `stru_` 前缀 -- 枚举用 `enum_` 前缀,枚举值用 `ENUM_` 前缀 - -**Why:** 从项目中多个核心源文件(iec61850s.cpp、self_ptl.cpp、ws_method.cpp、dc_signal.cpp)提取的共识格式规范 -**How to apply:** 新增代码和修改现有代码时必须遵守此格式。本次仅统一缩进/括号/空格/空行,命名问题暂不处理。 diff --git a/.claude/memory/project-rules.md b/.claude/memory/project-rules.md deleted file mode 100644 index 4789f8c..0000000 --- a/.claude/memory/project-rules.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -name: project-rules -description: 项目开发规范 — 编译/运行路径、排查流程、文档管理、plan归档要求 -metadata: - node_type: memory - type: feedback - originSessionId: 5e65af11-b197-479e-a82f-c2e5b475a8c3 ---- - -## 核心规则 - -1. **对话语言**:全程使用中文显示 -2. **编译路径**:`./release/build.sh`(x86)或 `./release/build.sh arm`(ARM交叉编译) -3. **执行路径**:`./test/RTU` -4. **排查问题流程**:先重新读取对应位置的源码,再对照问题或打印信息排查,不要依赖记忆中的旧代码 -5. **问题记录**:每次解决的问题都追加到 `claude/问题处理文档.md` -6. **文档目录**: - - `claude/mid/` — 存放中间文档、plan 文档 - - `claude/工程/` — 存放按模块记录的项目工程文档 -7. **每次读取项目工程时**,把读到的东西按模块生成文档记录到 `claude/工程/` 文件夹 -8. **解决问题的 plan**:每次制定并执行 plan 解决问题后,将 plan 内容整理为文档存入 `claude/mid/` 目录 - -**Why:** 用户明确要求的项目开发规范和工作流程 -**How to apply:** 每次操作前检查这些规则,特别是编译/运行路径和问题排查流程。问题解决后必须追加到问题处理文档,同时将 plan 归档到 mid 目录。 diff --git a/claude/工程/libiec61850_MMS客户端API开发手册.md b/claude/工程/libiec61850_MMS客户端API开发手册.md deleted file mode 100644 index 85288c7..0000000 --- a/claude/工程/libiec61850_MMS客户端API开发手册.md +++ /dev/null @@ -1,1207 +0,0 @@ -# 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 deleted file mode 100644 index cb7c53b..0000000 --- a/claude/工程/libiec61850_MMS服务端API开发手册.md +++ /dev/null @@ -1,1294 +0,0 @@ -# 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 deleted file mode 100644 index cf25621..0000000 --- a/claude/工程/libiec61850m模块分析.md +++ /dev/null @@ -1,354 +0,0 @@ -# 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/工程/libiec61850s模块分析.md b/claude/工程/libiec61850s模块分析.md deleted file mode 100644 index 04d61cf..0000000 --- a/claude/工程/libiec61850s模块分析.md +++ /dev/null @@ -1,273 +0,0 @@ -# libiec61850s 模块分析 - -**日期**:2026-06-10 - ---- - -## 1. 模块概览 - -`libiec61850s` 是 IEC 61850 MMS 服务端的应用线程模块(`app_iec61850s`),属于系统层的第 9 号线程。它作为桥接层,连接三个子系统: - -``` -┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ DataCenter │ ←→ │ libiec61850s │ ←→ │ libmms_s │ -│ (信号数据中心) │ │ (应用线程/桥接层) │ │ (MMS 服务端封装) │ -└─────────────────┘ └──────────────────┘ └─────────────────┘ - ↕ - ┌──────────────────┐ - │ mms_s.xml 配置 │ - └──────────────────┘ -``` - -### 目录结构 - -``` -src/system/libiec61850s/ -├── inc/ -│ ├── iec61850s.h # 头文件(包含 mySystem/myMms_s 等) -│ └── parse_xml.h # XML 配置解析类型 + 接口 -└── src/ - ├── iec61850s.cpp # 应用线程主逻辑(~487 行) - └── parse_xml.cpp # mms_s.xml 解析(~129 行) -``` - ---- - -## 2. 应用线程模型 - -与所有 app 线程一致,libiec61850s 遵循 `init1 → init2 → fun_cb` 三段式: - -| 阶段 | 函数 | 主要工作 | -|------|------|---------| -| init1 | `app_iec61850s_init1()` | 解析配置、注册回调 | -| init2 | `app_iec61850s_init2()` | 初始化信号、启动 MMS 服务器 | -| fun_cb | `app_iec61850s()` | 主循环(3 定时器,当前空闲) | - -### 2.1 init1 流程([iec61850s.cpp:392](src/system/libiec61850s/src/iec61850s.cpp#L392)) - -``` -1. get_base_path() → 获取进程目录 -2. parse_mms_xml("config/MMS/mms_s.xml") → 解析 XML 配置 -3. mms_s_dbg_switch(false) → 关闭调试 -4. mms_s_file_path_set(base_path) → 设置文件根目录 -5. mms_s_value_update_register(&cb) → 注册值更新回调 -``` - -### 2.2 init2 流程([iec61850s.cpp:421](src/system/libiec61850s/src/iec61850s.cpp#L421)) - -``` -1. iec61850s_signals_init() → 初始化五类信号 -2. mms_s_init("config/MMS/PCS.icd", 102) → 启动 MMS 服务器 -``` - -### 2.3 主循环([iec61850s.cpp:446](src/system/libiec61850s/src/iec61850s.cpp#L446)) - -三个定时器事件(`EV_TIMER1/2/3`)当前均为空闲,仅 `EV_TIMER3` 做 `run_cnt++`。信号驱动的工作全部通过 libmms_s 的内部线程和 DataCenter 回调完成——本线程主要作为容器,维护 MMS 服务器的生命周期。 - ---- - -## 3. 配置解析子系统([parse_xml.cpp](src/system/libiec61850s/src/parse_xml.cpp)) - -### 3.1 XML 结构(mms_s.xml) - -```xml - - - - - - - - - - - - - - - - - -``` - -### 3.2 数据结构 - -```cpp -typedef struct { - std::vector vec_st; // 遥信 - std::vector vec_mx; // 遥测 - std::vector vec_co; // 遥控 - std::vector vec_ao; // 定值 - std::vector vec_param; // 参数 -} stru_mms_cfg; -``` - -每个 Signal 只需要 `link`(sAddr)属性,`type` 和 `ctrl_model` 在后续从 DataCenter 获取。 - ---- - -## 4. 五类信号初始化 - -`iec61850s_signals_init()` 按顺序初始化五类信号([iec61850s.cpp:347](src/system/libiec61850s/src/iec61850s.cpp#L347)): - -### 4.1 遥信(ST)初始化 - -``` -for each st signal: - dc_signal_out_link_with_callback(saddr, &p_data, iec61850s_st_mx_change_callback) -``` - -将 DataCenter 的 `out` 信号与本地指针绑定,注册变化回调 `iec61850s_st_mx_change_callback`。 - -### 4.2 遥测(MX)初始化 - -逻辑与 ST 完全一致,共用同一个回调函数。 - -### 4.3 ST/MX 变化回调 - -```cpp -iec61850s_st_mx_change_callback(saddr, type, p_data, p_last_data) - ↓ -dc_get_signal_val(p_data, type) → 获取当前值字符串 - ↓ -g_mms_s_value_update_cb(saddr, val) → 更新 MMS 模型中的 DA 值 -``` - -数据流向:**DataCenter 信号变化 → 回调 → libmms_s::mms_s_value_update() → IedServer 模型更新** - -### 4.4 遥控(CO)初始化 - -``` -for each co signal: - dc_get_yk_signal_info(saddr, desc, type, ctrl_model, &p_data) - ↓ -mms_s_control_register(&g_vec_control, iec61850s_control_callback) -``` - -控制执行时,`mms_s_control.cpp` 的 `control_handler()` 会触发 `iec61850s_control_callback()`,根据 `ctrl_model` 调用 DataCenter 的 `dc_signal_yk_set_status()`。 - -**ctrl_model 映射**: -| MMS Control Model | DataCenter 动作 | -|-------------------|----------------| -| DIRECT_NORMAL / DIRECT_ENHANCED | `SIGNAL_CTRL_TYPE::DIRECT_NORMAL` → `dc_signal_yk_set_status(DIRECT)` | -| SBO_NORMAL / SBO_ENHANCED | `SIGNAL_CTRL_TYPE::SBO_NORMAL` → select + direct 两步 | -| STATUS_ONLY | 仅日志,不操作 | - -### 4.5 定值(AO)初始化 - -``` -for each ao signal: - dc_get_ao_signal_info(saddr, desc, type, null, ctrl_model, &p_data, null) - ↓ -mms_s_setting_register(&g_vec_setting, iec61850s_setting_callback) -``` - -客户端写定值时,`mms_s_setting.cpp` 的 `writeAccessHandler()` 校验通过后触发 `iec61850s_setting_callback()`,调用 `dc_signal_ao_set_val()`。 - -### 4.6 参数(Param)初始化 - -``` -for each param signal: - dc_get_param_signal_info(saddr, desc, type, null, ctrl_model, &p_data_vec, null) - ↓ -mms_s_param_register(&g_vec_param, iec61850s_param_callback) -``` - -参数支持多定值区(`p_data[MMS_S_PARAM_MAX]`,最大 16 组)。客户端确认编辑后,`edit_sg_confirmation_handler()` 触发 `iec61850s_param_callback()`,调用 `dc_signal_param_set_val()`,传入 `setting_zone` 索引。 - ---- - -## 5. 完整数据流向 - -### 5.1 上行数据(RTU → 客户端) - -``` -DataCenter 信号变化 - → iec61850s_st_mx_change_callback() - → g_mms_s_value_update_cb(saddr, val) - → mms_s_value_update() [mms_s_value.cpp] - → IedServer_update*AttributeValue() - → libiec61850 内部触发报告(根据 TrgOps 配置) - → MMS 报告上送到客户端 -``` - -### 5.2 下行数据(客户端 → RTU) - -**控制**: -``` -客户端 Select/Operate - → libiec61850 → control_handler() [mms_s_control.cpp] - → iec61850s_control_callback() - → dc_signal_yk_set_status() -``` - -**定值**: -``` -客户端 Write SP - → libiec61850 → writeAccessHandler() [mms_s_setting.cpp] - → 值校验(范围/步长) - → iec61850s_setting_callback() - → dc_signal_ao_set_val() -``` - -**参数(定值组)**: -``` -客户端切换定值组 - → libiec61850 → param_active_sg_changed_handler() [mms_s_param.cpp] - → param_load_active_sg_values() → 加载 SG DA -客户端编辑确认 - → edit_sg_confirmation_handler() [mms_s_param.cpp] - → iec61850s_param_callback() - → dc_signal_param_set_val(setting_zone) -``` - ---- - -## 6. 数据结构 - -### 6.1 本地信号存储 - -| 类型 | 容器 | 数据结构 | -|------|------|---------| -| 遥信 | `g_vec_st` | `vector` — {base, p_data} | -| 遥测 | `g_vec_mx` | `vector` — {base, p_data} | -| 遥控 | `g_vec_control` | `vector` — {base, p_data} | -| 定值 | `g_vec_setting` | `vector` — {base, p_data} | -| 参数 | `g_vec_param` | `vector` — {base, param_num, p_data[]} | - -### 6.2 公共信号基类([myMms_s.h](release/inc/myMms_s.h)) - -```cpp -typedef struct { - char saddr[MMS_S_STR_LEN]; // 短地址(如 "st.0", "mx.5") - char desc[MMS_S_STR_LEN]; // 描述 - uint8_t type; // 数据类型(DATA_TYPE_*) - uint8_t ctrl_model; // 控制模型 -} stru_mms_s_signal_base; -``` - ---- - -## 7. 与 libmms_m / libiec61850m 的对比 - -| 维度 | MMS 客户端 (m) | MMS 服务端 (s) | -|------|---------------|---------------| -| **角色** | 主动连接 IED,订阅报告 | 被动等待客户端连接 | -| **模型来源** | 从 IED 发现 | ICD 文件预定义 | -| **数据方向** | 先订阅 RCB,再接收报告 | 收到控制/写请求后更新 DataCenter | -| **线程模型** | libmms_m 启动 pthread,libiec61850m 做桥接 | libmms_s 启动 pthread,libiec61850s 做桥接 | -| **桥接层复杂度** | 复杂:RCB 发现/匹配/订阅/GI 触发/重连 | 相对简单:注册信号+回调,libmms_s 处理细节 | -| **配置方式** | mms_m.xml(IED 连接参数) | mms_s.xml(信号映射)+ PCS.icd(模型定义) | - ---- - -## 8. 对外接口 - -| 函数 | 说明 | -|------|------| -| `app_iec61850s_init1(void *arg)` | 第一段初始化:解析 XML、注册值更新回调 | -| `app_iec61850s_init2(void *arg)` | 第二段初始化:初始化信号、启动 MMS 服务器 | -| `app_iec61850s(void *arg)` | 主线程循环 | -| `parse_mms_xml(path)` | 解析 mms_s.xml 配置 | -| `mms_cfg_ptr_get()` | 获取配置数据指针 | -| `show_mms_xml(cfg)` | 打印配置内容(调试) | diff --git a/claude/工程/libmms_m模块分析.md b/claude/工程/libmms_m模块分析.md deleted file mode 100644 index 49ba06e..0000000 --- a/claude/工程/libmms_m模块分析.md +++ /dev/null @@ -1,671 +0,0 @@ -# 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/工程/libmms_s模块分析.md b/claude/工程/libmms_s模块分析.md deleted file mode 100644 index 9571c7d..0000000 --- a/claude/工程/libmms_s模块分析.md +++ /dev/null @@ -1,379 +0,0 @@ -# libmms_s 模块分析 - -**日期**:2026-06-10 - ---- - -## 1. 模块概览 - -`libmms_s` 是 IEC 61850 MMS 服务端的封装库,基于 `libiec61850` v1.5.3 的服务端 API,提供 ICD 文件解析、动态数据模型创建、IedServer 生命周期管理,以及控制/定值/参数/文件等子系统的初始化。 - -### 目录结构 - -``` -src/protocol/libmms_s/ -├── inc/ -│ ├── mms_s.h # 核心头文件:日志宏、全局访问接口、类型转换 -│ ├── mms_s_icd.h # ICD 解析数据结构(完整 SCL 类型体系) -│ ├── mms_s_model.h # 动态模型创建接口 -│ ├── mms_s_value.h # DA 初始值设定 -│ ├── mms_s_control.h # 控制功能接口 -│ ├── mms_s_param.h # 定值组参数接口 -│ ├── mms_s_setting.h # 定值(SP)接口 -│ └── mms_s_file.h # 文件传输服务接口 -└── src/ - ├── mms_s.cpp # 核心:init/run/task/类型转换 - ├── mms_s_icd.cpp # ICD XML 解析器(tinyxml2) - ├── mms_s_model.cpp # 动态 IedModel 创建 - ├── mms_s_control.cpp # 控制(SBO/直控)处理 - ├── mms_s_param.cpp # 定值组(SG/SE)管理 - ├── mms_s_setting.cpp # 定值(SP)管理 - ├── mms_s_value.cpp # 数据属性初始值设定 - └── mms_s_file.cpp # MMS 文件传输服务 -``` - ---- - -## 2. 核心架构 - -### 2.1 单例全局状态 - -```cpp -LOCAL IedServer gp_iedServer = NULL; // 全局 IED 服务器(单例) -LOCAL stru_icd *gp_icd = NULL; // 全局 ICD 数据(单例) -LOCAL int g_running = 0; // 线程运行标志 -LOCAL bool g_dbg_switch = false; // 调试输出开关 -``` - -整个库采用单例模式,全局只有一个 IedServer 和一个 ICD 数据结构。 - -### 2.2 模块分层 - -``` -┌──────────────────────────────────────────┐ -│ myMms_s.h │ ← 公共 API + 类型定义 -├──────────────────────────────────────────┤ -│ mms_s.cpp (init / run / 类型转换) │ ← 库入口 -├──────────────────────────────────────────┤ -│ mms_s_icd.cpp │ mms_s_model.cpp │ ← ICD 解析 → 模型构建 -├──────────────────────────────────────────┤ -│ mms_s_value.cpp (DA 初始值) │ ← 模型回调 -├──────────────────────────────────────────┤ -│ mms_s_control.cpp │ mms_s_param.cpp │ ← 功能模块 -│ mms_s_setting.cpp │ mms_s_file.cpp │ -├──────────────────────────────────────────┤ -│ libiec61850 (libiec61850.a) │ ← 底层协议栈 -└──────────────────────────────────────────┘ -``` - ---- - -## 3. 完整初始化流程 - -`mms_s_init()` 的执行顺序([mms_s.cpp:261](src/protocol/libmms_s/src/mms_s.cpp#L261)): - -``` -1. icd_parse(icd_path) → 解析 ICD 文件 → stru_icd -2. model_init(*gp_icd) → 创建动态数据模型 → IedModel -3. IedServerConfig_create() → 创建服务器配置 -4. IedServer_createWithConfig() → 创建 IedServer 实例 -5. control_init() → 安装控制回调 -6. param_init() → 安装定值组回调 -7. file_init() → 安装文件服务 -8. IedServer_setRCBEventHandler() → 注册 RCB 事件监听 -9. IedServer_start(port) → 启动服务器(默认端口 102) -10. setting_init() → 初始化定值 -11. IedServer_isRunning() → 检查运行状态 -12. Thread_create(mms_s_run_task) → 启动后台线程 -``` - ---- - -## 4. ICD 解析子系统 - -### 4.1 数据结构体系([mms_s_icd.h](src/protocol/libmms_s/inc/mms_s_icd.h)) - -对应 IEC 61850-6 SCL 标准的完整类型体系: - -**数据类型模板层(DataTypeTemplates)**: -| 结构 | 说明 | 关键字段 | -|------|------|---------| -| `stru_LNodeType` | 逻辑节点类型 | lnClass, vec_do, map_do(key:name) | -| `stru_DO` | 数据对象定义 | desc, type(→ DOType id) | -| `stru_DOType` | 数据对象类型 | cdc, vec_da, map_da, vec_sdo, map_sdo | -| `stru_DA` | 数据属性定义 | bType, type, dchg, qchg, fc, text | -| `stru_DAType` | 数据属性类型 | vec_bda, map_bda(key:name) | -| `stru_BDA` | 基本数据属性 | bType, type, fc, dchg, qchg | -| `stru_EnumType` | 枚举类型 | vec_ord, map_enumVal(key:ord) | - -**实例化模型层(IED/Server)**: -| 结构 | 说明 | -|------|------| -| `stru_ied` | IED 顶层模型(含 model, name, 各逻辑设备) | -| `stru_LDevice` | 逻辑设备(含 Ldevice 指针, sgcb, ln0, vec_ln) | -| `stru_LN0` | LLN0(含 数据集, 报告控制, 定值控制) | -| `stru_LN` | 普通逻辑节点 | -| `stru_DOI` | 数据对象实例(含 SDI, DAI) | -| `stru_SDI` | 子数据对象实例(可递归嵌套) | -| `stru_DAI` | 数据属性实例(含 sAddr, val) | -| `stru_DataSet` | 数据集(含 FCDA 列表) | -| `stru_FCDA` | 功能约束数据属性(ldInst/lnClass/doName/daName/fc) | -| `stru_ReportControl` | 报告控制块(含 TrgOps, OptFields, RptEnabled_max) | -| `stru_SettingControl` | 定值控制块(actSG, numOfSGs) | - -**运行时辅助结构**: -| 结构 | 说明 | -|------|------| -| `stru_all_DO` | DO 运行时树(含 libiec61850 DataObject 指针, map_all_sdo, map_all_da) | -| `stru_all_DA` | DA 运行时树(含 libiec61850 DataAttribute 指针, map_all_da 子节点) | -| `stru_contorl_DO` | 控制 DO 定位信息(ldevice_inst/ln_class/ln_inst/do_name/p_all_do) | -| `stru_saddr_point` | sAddr 到模型节点的映射(node, da_t, da_q) | -| `stru_setting_info` | 定值信息(DA 指针, 类型, 值) | - -### 4.2 解析流程([mms_s_icd.cpp](src/protocol/libmms_s/src/mms_s_icd.cpp)) - -``` -icd_parse(icd_file) -├── XMLDocument.LoadFile() -├── parse_ied() → 解析 IED 实例 -│ ├── parse_LDevice() → 每个逻辑设备 -│ │ ├── parse_LN0() → LLN0(数据集/报告控制/定值控制/DOI) -│ │ │ ├── parse_DataSet() -│ │ │ ├── parse_ReportControl() -│ │ │ │ ├── parse_ReportControl_TrgOps() -│ │ │ │ ├── parse_ReportControl_OptFields() -│ │ │ │ └── parse_ReportControl_RptEnable() → RptEnabled_max -│ │ │ ├── parse_DOI() → parse_SDI() / parse_DAI() -│ │ │ └── parse_SettingControl() -│ │ └── parse_LN() → 普通 LN + DOI -│ └── (沿 SCL > IED > AccessPoint > Server > LDevice 路径) -├── parse_dataTypeTemplates() → 解析模板 -│ ├── parse_LNodeType() -│ ├── parse_DOType() -│ ├── parse_DAType() -│ ├── parse_EnumType() -│ └── parse_BDA_fc() ×2 → 传播 FC/dchg/qchg 到嵌套 BDA -└── check_ied_dataTypeTemplates() → 一致性校验 - └── 递归验证每个实例节点都能在模板中找到定义 -``` - -### 4.3 BDA 属性传播机制 - -关键设计:当 DA 的 type 指向 DAType(Struct 类型)时,`parse_BDA_fc()` 将父级 DA 的 fc/dchg/qchg 属性向下传播到 DAType 中所有 BDA,确保嵌套 Struct 的底层 BDA 也能继承正确的 FC 约束。 - ---- - -## 5. 动态模型创建子系统 - -### 5.1 model_init() 主流程([mms_s_model.cpp:1319](src/protocol/libmms_s/src/mms_s_model.cpp#L1319)) - -``` -1. IedModel_create(name) -2. model_ldevice_init() - ├── LogicalDevice_create() - ├── model_ln0_init() - │ ├── LogicalNode_create("LLN0") - │ ├── model_dataobject_init() → 创建所有 DO/SDO/DA - │ ├── model_DataSet_init() → 创建数据集+条目 - │ ├── model_ReportControlBlock_init() → 创建 RCB - │ └── SettingGroupControlBlock_create() - └── model_LNodes_init() → 创建普通 LN -3. model_sync_ied_default_values() → 同步 ICD DAI 中的 sAddr/val -4. model_search_control_DataObjects() → 搜索 ctlModel→vec_control_do -5. icd.ied.model->initializer = mms_s_values_init → 注册回调 -``` - -### 5.2 类型映射表 - -**bType → DataAttributeType**([mms_s_model.cpp:43](src/protocol/libmms_s/src/mms_s_model.cpp#L43)): -`BOOLEAN → IEC61850_BOOLEAN`, `INT32 → IEC61850_INT32`, `FLOAT32 → IEC61850_FLOAT32`, `Struct → IEC61850_CONSTRUCTED`, `Quality → IEC61850_QUALITY`, `Timestamp → IEC61850_TIMESTAMP`, `Dbpos → IEC61850_GENERIC_BITSTRING` 等,共约 25 种映射。 - -**FC → FunctionalConstraint**([mms_s_model.cpp:86](src/protocol/libmms_s/src/mms_s_model.cpp#L86)): -`ST/MX/SP/SV/CF/DC/SG/SE/SR/OR/BL/EX/CO/US/MS/RP/BR/LG/GO` → 对应 IEC61850 枚举。 - -### 5.3 SG/SE 双数据属性设计 - -当 DA 的 FC=SG 时,除了创建 FC=SG 的 DA 外,还会额外创建一个 FC=SE 的同名 DA(key 加 `_SE` 后缀)。反之 FC=SE 同理。sAddr 映射时也会对应添加 `_SG` / `_SE` 后缀区分。 - -这是一个独创设计——在同一个 DO 下同时暴露 SG(当前激活值)和 SE(编辑缓冲区值),使得外部系统可以同时访问定值的"当前生效值"和"正在编辑中的值"。 - -### 5.4 sAddr 映射机制 - -ICD 文件中 DAI 元素的 `sAddr` 属性定义了该数据点与外部数据源的绑定关系。`model_sync_ied_default_values()` 将 sAddr 写入模型节点的 `sAddr` 字段,同时建立: -- `icd.ied.vec_saddr` — sAddr 列表 -- `icd.ied.map_saddr_point` — sAddr → ModelNode 映射 - -对于 FC=SG 的点,sAddr 后缀 `_SG`;FC=SE 的点后缀 `_SE`。 - ---- - -## 6. 控制子系统([mms_s_control.cpp](src/protocol/libmms_s/src/mms_s_control.cpp)) - -### 6.1 双回调模式 - -| 回调 | 阶段 | 职能 | -|------|------|------| -| `check_handler()` | Select / Interlock | 权限校验,匹配合法 DO 则返回 CONTROL_ACCEPTED | -| `control_handler()` | Operate | 执行控制命令,更新 t(时间戳)+ stVal,触发外部回调 | - -### 6.2 控制执行流程 - -``` -客户端 → Select Request → check_handler() → CONTROL_ACCEPTED -客户端 → Operate Request → check_handler() (interlock check) → control_handler() - ├── IedServer_updateUTCTimeAttributeValue(t) - ├── 匹配 sAddr → cb_control() → 通知应用层 - └── 根据 stVal 类型: - ├── BOOLEAN → IedServer_updateAttributeValue() - └── Dbpos → IedServer_updateDbposValue() -``` - -### 6.3 控制 DO 发现 - -`model_search_control_DataObjects()` 遍历模型树,找到所有包含 `ctlModel` DA 的 DO,存入 `icd.ied.vec_control_do`。`control_init()` 遍历该列表,为每个安装 `control_handler` 和 `check_handler`。 - ---- - -## 7. 定值组子系统([mms_s_param.cpp](src/protocol/libmms_s/src/mms_s_param.cpp)) - -### 7.1 SG vs SE - -| FC | 含义 | mmsValue 加载时机 | -|----|------|------------------| -| SG | Setting Group — 当前激活定值区的实际值 | 激活定值组切换时加载 | -| SE | Setting Editable — 编辑缓冲区值 | 编辑定值组切换时加载 | - -### 7.2 回调链 - -``` -激活定值组切换 → param_active_sg_changed_handler() → param_load_active_sg_values() -编辑定值组切换 → param_edit_sg_changed_handler() → param_load_edit_sg_values() -编辑确认 → edit_sg_confirmation_handler() → 回读 SE 值 → cb_param() -``` - -### 7.3 值的加载与校验 - -- 加载:根据 sAddr+"_SG"/"_SE" 在 `map_saddr_point` 中查找节点,按 MMS 类型分发更新函数 -- 回读:`edit_sg_confirmation_handler()` 读取 SE DA 当前值,做 min/max/step 校验,通过后触发外部回调 `g_param_cb` -- 类型分发表:`g_param_update_funcs[]`(写)和 `g_param_get_funcs[]`(读),覆盖 BOOLEAN/INT/UINT/FLOAT/STRING - ---- - -## 8. 定值子系统([mms_s_setting.cpp](src/protocol/libmms_s/src/mms_s_setting.cpp)) - -### 8.1 与参数的差异 - -定值(FC=SP)不参与定值组切换,是单一设置值。 - -### 8.2 写保护机制 - -1. 全局访问策略:`IedServer_setWriteAccessPolicy(IEC61850_FC_SP, ACCESS_POLICY_DENY)` -2. 精确放行:通过 `IedServer_handleWriteAccess()` 为已注册 sAddr 安装 `writeAccessHandler` -3. `writeAccessHandler` 做值校验(范围+步长),通过后触发外部回调 `cb_setting` - -### 8.3 校验流程 - -``` -客户端写 SP 值 → writeAccessHandler() -├── 匹配 sAddr -├── 类型分发 (setting_get_BOOLEAN/INT/UINT/FLOAT/STRING) -│ └── setting_min_max_step_get() → 读取同级 minVal/maxVal/stepSize -│ └── 范围校验 + 步长校验 -├── cb_setting() → 通知应用层 -└── return DATA_ACCESS_ERROR_SUCCESS → libiec61850 更新 mmsValue -``` - ---- - -## 9. 数据属性值初始化([mms_s_value.cpp](src/protocol/libmms_s/src/mms_s_value.cpp)) - -`mms_s_values_init()` 作为 `IedModel.initializer` 回调,在 IedServer 创建过程中由 libiec61850 自动调用。 - -**初始化类型覆盖**:BOOLEAN, INT8U/32, FLOAT32, VisString*, Unicode*, Timestamp, Dbpos, Enum(特殊:按文本值或序号查找枚举值)。 - -**Dbpos 特殊处理**:`mms_s_da_value_init_Dbpos()` 按 val 值映射到 DBPOS_INTERMEDIATE_STATE(0)/OFF(1)/ON(2)/BAD_STATE(>=3)。 - -### 9.1 值更新路径 - -`mms_s_value_update()` 是预留的值更新函数,通过 `mms_s_value_update_register()` 注册给外部。当数据中心信号变化时,上层调用此函数更新模型中的 DA 值,同时自动更新父 DO 的 `t`(时间戳)和 `q`(品质)。 - ---- - -## 10. 文件传输服务([mms_s_file.cpp](src/protocol/libmms_s/src/mms_s_file.cpp)) - -提供 MMS 文件传输基础能力: -- 设置文件存储根目录 `IedServer_setFilestoreBasepath()` -- 访问控制:禁止重命名、禁止删除 `IEDSERVER.BIN` -- 连接事件日志 - ---- - -## 11. 后台运行线程 - -```cpp -LOCAL void *mms_s_run_task(void *parameter) -{ - while(g_running) { - IedServer_lockDataModel(gp_iedServer); - IedServer_unlockDataModel(gp_iedServer); - Thread_sleep(100); // 100ms 周期 - } - IedServer_stop(gp_iedServer); - IedServer_destroy(gp_iedServer); - IedModel_destroy(iedModel); -} -``` - -线程负责保护 IedServer 生命周期,通过 lock/unlock 持有模型锁保持服务活跃。收到 SIGINT 后 `g_running=0`,线程退出并销毁资源。 - ---- - -## 12. RCB 事件监听 - -`rcbEventHandler()` 监听客户端的报告控制块操作([mms_s.cpp:48](src/protocol/libmms_s/src/mms_s.cpp#L48)): - -| 事件 | 含义 | -|------|------| -| `RCB_EVENT_ENABLE` | 客户端使能报告 | -| `RCB_EVENT_DISABLE` | 客户端关闭报告 | -| `RCB_EVENT_RESERVED` | 客户端预订 RCB | -| `RCB_EVENT_UNRESERVED` | 客户端释放预订 | -| `RCB_EVENT_GI` | 总召触发 | -| `RCB_EVENT_SET_PARAMETER` | 客户端设置 RCB 参数(如 TrgOps 等) | -| `RCB_EVENT_GET_PARAMETER` | 客户端获取 RCB 参数 | - -**特殊处理**:当客户端设置 `TrgOps` 参数时,服务器强制追加 `dchg`(`rcb->trgOps |= 0x01`),确保数据变化一定能触发报告上送。 - ---- - -## 13. 对外 API 汇总 - -| API | 文件 | 说明 | -|-----|------|------| -| `mms_s_init(icd_path, port)` | mms_s.cpp | 完整初始化 MMS 服务器 | -| `mms_s_dbg_switch(on)` | mms_s.cpp | 调试开关 | -| `mms_s_get_icd_ptr()` | mms_s.cpp | 获取 ICD 数据 | -| `mms_s_get_ied_server_ptr()` | mms_s.cpp | 获取 IedServer | -| `mms_s_control_register(...)` | mms_s_control.cpp | 注册控制点 | -| `mms_s_setting_register(...)` | mms_s_setting.cpp | 注册定值 | -| `mms_s_param_register(...)` | mms_s_param.cpp | 注册参数(定值组) | -| `mms_s_file_path_set(path)` | mms_s_file.cpp | 设置文件根路径 | -| `mms_s_value_update_register(cb)` | mms_s_value.cpp | 注册值更新回调 | -| `mms_s_get_string_by_mms_type(...)` | mms_s.cpp | MMS 类型→字符串 | -| `mms_s_get_string_by_type(...)` | mms_s.cpp | 自定义类型→字符串 | - ---- - -## 14. 已知问题 - -### 14.1 check_handler 未调用 cb_interlock - -`mms_s_control.h` 中声明了 `mms_s_interlock_cb` 和 `mms_s_set_interlock_cb()`,但 `check_handler()` 中仅检查 ICD 中是否存在对应 DO,`cb_interlock` 未被实际调用。外部注册的联锁检查回调不会生效。 - -### 14.2 param_min_max_step_get 实现不一致 - -`mms_s_param.cpp` 的 `param_min_max_step_get()` 通过遍历 ModelNode 父子关系查找 minVal/maxVal/stepSize(向上找到 DataObject 再遍历 firstChild),而 `mms_s_setting.cpp` 的 `setting_min_max_step_get()` 是通过 parent->firstChild 直接遍历。两者逻辑不同,前者更复杂(向上一级再到 DO),可能是修正后的版本,但后者仍保留了旧逻辑。 - -### 14.3 mms_s_value_update 未使用 g_iec61850s_value_init_cb_map - -`mms_s_value_update()` 使用 `g_iec61850s_value_update_cb_map`(按 DataAttributeType 索引),但表中大部分类型回调为 NULL,仅 BOOLEAN/INT32/INT32U/FLOAT32/Enum/VisString32/Unicode255/Timestamp 有实际实现。其他类型(如 INT8/INT16/INT64/FLOAT64 等)更新时会被跳过。 diff --git a/claude/工程/libweb_server模块分析.md b/claude/工程/libweb_server模块分析.md deleted file mode 100644 index 2b019eb..0000000 --- a/claude/工程/libweb_server模块分析.md +++ /dev/null @@ -1,217 +0,0 @@ -# libweb_server 模块分析 - -**日期**:2026-06-10 - ---- - -## 1. 模块概览 - -`libweb_server` 是 RTU 的嵌入式 Web 服务器模块(`app_web_server` 线程),属于系统层的第 7 号线程。基于 Mongoose v7.21,提供 HTTP 静态文件服务 + WebSocket 实时数据通道。 - -### 目录结构 - -``` -src/system/libweb_server/ -├── inc/ -│ ├── web_server.h # 对外接口(app_web_server_init1/2, app_web_server) -│ └── ws_method.h # WebSocket 消息处理方法声明 -└── src/ - ├── web_server.cpp # HTTP/WS 服务器 + mongoose 事件循环 - └── ws_method.cpp # WebSocket 命令解析 + JSON 数据推送 -``` - ---- - -## 2. 架构 - -### 2.1 线程模型 - -``` -┌─────────────────────────────────────────────────┐ -│ app_web_server 线程 (RTU 9号线程) │ -│ │ -│ init1: web_server_init() │ -│ → mg_mgr_init + mg_http_listen(8000) │ -│ → pthread_create → web_server_run (独立线程) │ -│ │ -│ init2: (空) │ -│ │ -│ fun_cb: 事件循环 (3个定时器) │ -│ EV_TIMER3 → ws_task() (每秒推送数据) │ -└─────────────────────────────────────────────────┘ - │ - │ g_ws_conns (连接列表) + g_ws_sessions (会话资源) - │ -┌─────────────────────────────────────────────────┐ -│ web_server_run 线程 (mongoose 事件线程) │ -│ │ -│ while(1) mg_mgr_poll(500ms) │ -│ → web_server_task() 回调 │ -│ MG_EV_HTTP_MSG → WS升级 / 静态文件 │ -│ MG_EV_WS_MSG → 接收命令 │ -│ MG_EV_CLOSE → 断连清理 │ -└─────────────────────────────────────────────────┘ -``` - -### 2.2 会话隔离模型 - -每个 WebSocket 连接拥有独立的 `stru_ws_session`,包含自己的五类信号集。连接建立时开辟、断开时释放。 - -``` -客户端A (WebSocket) ─→ session_A: {out_signals_A, in_signals_A, yk_signals_A, ...} -客户端B (WebSocket) ─→ session_B: {out_signals_B, in_signals_B, yk_signals_B, ...} -``` - -`ws_task()` 遍历所有 session,为每个 session 构建独立的 JSON 并发送到对应连接。 - -### 2.3 数据流 - -``` -浏览器 ←── HTTP ──→ mongoose ←── 静态文件 (web_root/) -浏览器 ←── WS ────→ mongoose ←── JSON 命令/数据 ←── ws_method.cpp - ↕ - DataCenter -``` - ---- - -## 3. web_server.cpp 核心逻辑 - -### 3.1 全局状态 - -```cpp -g_web_root → 静态文件根目录(进程目录 + "web_root") -mgr → mongoose 事件管理器 -g_ws_conns → vector 活跃 WebSocket 连接列表 -g_ws_conns_mutex → pthread 互斥锁,保护 g_ws_conns -g_ws_sessions → map 每连接独立信号资源(ws_method.cpp) -g_ws_session_mutex → pthread 互斥锁,保护 g_ws_sessions -``` - -### 3.2 事件处理 (web_server_task) - -| 事件 | 处理 | -|------|------| -| `MG_EV_HTTP_MSG` | URI=`/ws` → `mg_ws_upgrade()` 升级为 WebSocket;其他 → `mg_http_serve_dir()` 静态文件 | -| `MG_EV_WS_MSG` | 将连接加入 `g_ws_conns`,消息经 `std::string` 安全复制后传给 `ws_recv()` | -| `MG_EV_WS_CTL` | CLOSE 帧 → 调用 `ws_session_destroy()` + 从 `g_ws_conns` 移除 | -| `MG_EV_CLOSE` | 调用 `ws_session_destroy()` + 从 `g_ws_conns` 移除 | - -### 3.3 ws_send_all / ws_send_one - -- `ws_send_all()`:遍历 `g_ws_conns` 广播,跳过死连接 -- `ws_send_one(conn_id, ...)`:按 `c->id` 精确定位单连接发送 - -均加 `g_ws_conns_mutex` 锁保护。 - ---- - -## 4. ws_method.cpp 命令处理 - -### 4.1 会话结构 - -```cpp -struct stru_ws_session { - vector out_signals; // 本连接订阅的遥信 - vector in_signals; // 本连接订阅的遥测 - vector yk_signals; // 本连接订阅的遥控 - vector ao_signals; // 本连接订阅的定值 - vector param_signals; // 本连接订阅的参数 -}; -``` - -全局 `g_ws_sessions: map` 管理所有连接的独立资源。`g_ws_session_mutex` 保护。 - -### 4.2 信号模型 - -每个连接 `add` 信号时仅影响自己的 session,不同客户端之间完全隔离。 - -| 类型 | session 字段 | 支持操作 | -|------|-------------|---------| -| out (遥信) | `session.out_signals` | add / del / set | -| in (遥测输入) | `session.in_signals` | add / del | -| yk (遥控) | `session.yk_signals` | add / del / set (含 SBO) | -| ao (定值) | `session.ao_signals` | add / del / set (含 SBO) | -| param (参数) | `session.param_signals` | add / del / set (含 SBO, 多定值区) | - -### 4.2 WebSocket 上行命令格式 (JSON) - -```json -{ - "curd": "add|del|set", - "signal_type": "out|in|yk|ao|param", - "saddr": "st.0", - "signal_data": "1.5", - "setting_zone": "0" -} -``` - -### 4.3 下行数据格式 (JSON) - -每秒 (EV_TIMER3) 推送完整快照,五类信号分数组。修复后仅在值变化时推送。 - -### 4.4 SBO 控制流程 - -遥控/定值/参数支持 `DIRECT_NORMAL`(直接执行)和 `SBO_NORMAL`(选择-执行两步),通过 DataCenter 的 `dc_signal_yk_set_status` / `dc_signal_ao_set_val` / `dc_signal_param_set_val` 执行。 - ---- - -## 5. 缺陷与修复 - -### 5.1 缺陷清单(修复前) - -| # | 严重度 | 位置 | 问题 | -|---|--------|------|------| -| 1 | 严重 | web_server.cpp L14 | `p_conn` 单指针,仅支持一个 WS 客户端 | -| 2 | 严重 | web_server.cpp | 无 `MG_EV_CLOSE` 处理,断连后悬空指针 | -| 3 | 严重 | web_server.cpp | `p_conn` 无锁保护,多线程竞态 | -| 4 | 严重 | web_server.cpp L66, ws_method.cpp L519 | `mg_str` 非 null-terminated 传给 `printf`/`cJSON_Parse`(UB) | -| 5 | 高危 | web_server.cpp L66 | 调试 `printf` 遗留 | -| 6 | 高危 | ws_method.cpp L386 | SBO `task_sleep_ms(1000)` 阻塞事件循环 | -| 7 | 中危 | — | 无心跳保活机制 | -| 8 | 中危 | ws_method.cpp | 每秒全量推送,数据不变也发送 | -| 9 | 中危 | ws_method.cpp L400,L502 | `LOG_E("%d")` 缺少对应参数 | -| 10 | 中危 | web_server.cpp L22 | `ws_send` 未校验 `is_websocket`/`is_draining` | -| 11 | 严重 | ws_method.cpp L21-25 | 多客户端共享同一套全局信号资源,客户端之间信号干扰 | - -### 5.2 修复措施 - -**第一轮(2026-06-10)**: - -| # | 修复 | -|---|------| -| 1 | `p_conn` → `g_ws_conns: vector`,遍历广播 | -| 2 | 新增 `MG_EV_CLOSE` + `MG_EV_WS_CTL(CLOSE)` 从列表移除 | -| 3 | 新增 `pthread_mutex_t g_ws_mutex`,列表读写前加锁 | -| 4 | `std::string(wm->data.buf, wm->data.len)` 安全复制后再用 | -| 5 | `printf` → `LOG_I` | -| 6 | 删除 `task_sleep_ms(1000)` | -| 7 | 当前 500ms poll 周期可替代心跳,mg_timer 需配合 wakeup_init 略复杂暂不引入 | -| 8 | `stru_ws_signal` 新增 `last_val`,`ws_task()` 仅在值变化时发送 | -| 9 | 补全 `p_signal->ctrl_type` 参数 | -| 10 | `ws_send()` 循环中增加 `c->is_websocket && !c->is_draining` 检查 | - -**第二轮(2026-06-10):会话隔离重构**: - -| # | 修复 | -|---|------| -| 11 | 引入 `stru_ws_session` 每连接独立信号集,全局 `g_ws_sessions: map` 管理 | -| — | `ws_recv(c, ...)` 增加连接参数,操作仅影响对应 session | -| — | `ws_task()` 遍历 sessions,为每个连接构建独立 JSON → `ws_send_one(conn_id, ...)` | -| — | `ws_session_destroy(c)` 在 `MG_EV_CLOSE`/`WS_CTL(CLOSE)` 时释放该连接所有信号资源 | -| — | 所有 `add/del/set/make` 函数改为接受 `stru_ws_session&` 参数,无状态纯函数 | - ---- - -## 6. 对外接口 - -| 函数 | 文件 | 说明 | -|------|------|------| -| `app_web_server_init1(arg)` | web_server.cpp | 第一段初始化,启动 HTTP/WS 服务器 | -| `app_web_server_init2(arg)` | web_server.cpp | 第二段初始化(空) | -| `app_web_server(arg)` | web_server.cpp | 主线程循环,定时器驱动 `ws_task()` | -| `ws_send_all(p_tx, tx_len)` | web_server.cpp | 向所有 WS 客户端广播相同数据 | -| `ws_send_one(conn_id, p_tx, tx_len)` | web_server.cpp | 向单个 WS 连接发送数据 | -| `ws_recv(c, p_rx, rx_len)` | ws_method.cpp | 解析 WS 命令 JSON,操作仅在 c 对应 session 内生效 | -| `ws_task()` | ws_method.cpp | 遍历所有 session,各自构建增量 JSON 并推送 | -| `ws_session_destroy(c)` | ws_method.cpp | 释放连接对应的所有信号资源 | diff --git a/claude/工程/websocket-server.md b/claude/工程/websocket-server.md deleted file mode 100644 index 815bd11..0000000 --- a/claude/工程/websocket-server.md +++ /dev/null @@ -1,598 +0,0 @@ -# WebSocket 服务端深度分析 - -## 一、概述 - -Mongoose 的 WebSocket 实现位于 `src/ws.c`(~302 行),基于 RFC 6455 规范,同时支持服务端和客户端。本文档聚焦**服务端**的使用和内部实现。 - -**核心流程**: - -``` -HTTP 请求到达 (Upgrade: websocket) - → 用户处理器检测到 WebSocket 升级请求 - → 调用 mg_ws_upgrade() 完成握手 - → pfn 切换为 mg_ws_cb(WebSocket 协议处理器) - → 后续消息通过 MG_EV_WS_MSG 事件传递 - → mg_ws_send() 发送帧 -``` - -## 二、数据结构 - -### 帧操作码(Opcode) - -```c -// 定义在 ws.h -#define WEBSOCKET_OP_CONTINUE 0 // 分片帧的后续帧 -#define WEBSOCKET_OP_TEXT 1 // 文本帧(UTF-8) -#define WEBSOCKET_OP_BINARY 2 // 二进制帧 -#define WEBSOCKET_OP_CLOSE 8 // 关闭连接 -#define WEBSOCKET_OP_PING 9 // 心跳请求 -#define WEBSOCKET_OP_PONG 10 // 心跳响应 -``` - -### WebSocket 消息结构 - -```c -// 定义在 ws.h 第 12-15 行 -struct mg_ws_message { - struct mg_str data; // 消息数据(引用 c->recv 缓冲区,零拷贝) - uint8_t flags; // 帧标志字节(FIN + Opcode) -}; -``` - -`flags` 字节的高位是 FIN 标志(bit 7),低 4 位是操作码: - -``` -flags = 0b1xxx_xxxx → FIN=1(最后一帧) -flags = 0b0xxx_xxxx → FIN=0(还有后续帧) -flags & 15 → 操作码(0-15) -``` - -### 内部帧解析结构(ws.c 第 12-16 行) - -```c -struct ws_msg { - uint8_t flags; // 帧标志 - size_t header_len; // 帧头长度(含掩码 key) - size_t data_len; // 数据长度 -}; -``` - -## 三、WebSocket 服务端完整使用流程 - -### 3.1 最小示例 - -```c -#include "mongoose.h" - -// 统一的 HTTP + WebSocket 事件处理函数 -static void fn(struct mg_connection *c, int ev, void *ev_data) { - if (ev == MG_EV_HTTP_MSG) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - // 判断是否是 WebSocket 升级请求 - if (mg_http_get_header(hm, "Sec-WebSocket-Key")) { - mg_ws_upgrade(c, hm, NULL); // 执行握手,切换到 WS 模式 - } else { - // 普通 HTTP 请求处理 - mg_http_reply(c, 200, "", "hello\n"); - } - } else if (ev == MG_EV_WS_MSG) { - // WebSocket 消息到达(握手完成后) - struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; - // 判断消息类型 - if (wm->flags & WEBSOCKET_OP_TEXT) { - // 文本消息:回显 - mg_ws_send(c, wm->data.buf, wm->data.len, WEBSOCKET_OP_TEXT); - } else if (wm->flags & WEBSOCKET_OP_BINARY) { - // 二进制消息处理 - } - } else if (ev == MG_EV_CLOSE) { - // 连接关闭(包括 WebSocket 关闭) - } -} - -int main() { - struct mg_mgr mgr; - mg_mgr_init(&mgr); - mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL); - for (;;) mg_mgr_poll(&mgr, 1000); -} -``` - -### 3.2 完整事件流 - -``` -连接建立: - MG_EV_OPEN → 连接创建 - -HTTP 阶段: - MG_EV_ACCEPT → 连接被接受(可在此时初始化 TLS) - MG_EV_READ → 数据到达 - MG_EV_HTTP_MSG → HTTP 请求完整接收 - → 用户检测 Sec-WebSocket-Key 头部 - → 调用 mg_ws_upgrade(c, hm, NULL) - -WebSocket 握手: - mg_ws_upgrade() 内部: - → 计算 Sec-WebSocket-Accept - → 发送 HTTP 101 响应 - → c->pfn = mg_ws_cb - → c->is_websocket = 1 - → 触发 MG_EV_WS_OPEN - -WebSocket 通信阶段: - MG_EV_WS_MSG → 文本/二进制消息到达 - MG_EV_WS_CTL → 控制帧到达(Ping/Pong/Close) - MG_EV_READ → 原始数据到达(ws_cb 内部处理) - MG_EV_WRITE → 数据写入完成 - -连接关闭: - MG_EV_CLOSE → 连接关闭 -``` - -## 四、握手过程详解 (`mg_ws_upgrade`) - -### 4.1 函数签名(ws.c 第 269 行) - -```c -void mg_ws_upgrade(struct mg_connection *c, struct mg_http_message *hm, - const char *fmt, ...); -``` - -### 4.2 内部实现 - -``` -mg_ws_upgrade(): - 1. 从 HTTP 头部提取 "Sec-WebSocket-Key" - → 如果不存在,返回 426 Upgrade Required 错误 - 2. 可选:提取 "Sec-WebSocket-Protocol"(子协议协商) - 3. 调用 ws_handshake() 生成握手响应 - 4. 设置 c->pfn = mg_ws_cb(切换协议处理器) - 5. 设置 c->is_websocket = 1 - 6. 设置 c->is_resp = 0(标记响应完成) - 7. 触发 MG_EV_WS_OPEN 事件 -``` - -### 4.3 握手计算 (`ws_handshake`, 第 35 行) - -```c -static void ws_handshake(struct mg_connection *c, const struct mg_str *wskey, - const struct mg_str *wsproto, const char *fmt, - va_list *ap) { - const char *magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // RFC 6455 魔数 - unsigned char sha[20], b64_sha[30]; - - // 1. SHA1(client_key + magic) - mg_sha1_ctx sha_ctx; - mg_sha1_init(&sha_ctx); - mg_sha1_update(&sha_ctx, (unsigned char *) wskey->buf, wskey->len); - mg_sha1_update(&sha_ctx, (unsigned char *) magic, 36); - mg_sha1_final(sha, &sha_ctx); - - // 2. Base64 编码 SHA1 结果 - mg_base64_encode(sha, sizeof(sha), (char *) b64_sha, sizeof(b64_sha)); - - // 3. 构建 HTTP 101 响应 - mg_xprintf(mg_pfn_iobuf, &c->send, - "HTTP/1.1 101 Switching Protocols\r\n" - "Upgrade: websocket\r\n" - "Connection: Upgrade\r\n" - "Sec-WebSocket-Accept: %s\r\n", - b64_sha); - - // 4. 添加用户自定义响应头(通过 fmt 参数) - if (fmt != NULL) mg_vxprintf(mg_pfn_iobuf, &c->send, fmt, &ap); - - // 5. 可选的子协议响应头 - if (wsproto != NULL) { - mg_printf(c, "Sec-WebSocket-Protocol: %.*s\r\n", ...); - } - - // 6. 结束响应头 - mg_send(c, "\r\n", 2); -} -``` - -**关键常量**:魔数 `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` 是 RFC 6455 第 4.2.2 节规定的固定值,用于防止跨协议攻击。 - -### 4.4 握手时的进阶用法 - -**添加自定义响应头**(如 Cookie、Token 等): - -```c -mg_ws_upgrade(c, hm, "Set-Cookie: token=%s\r\nX-User: %s\r\n", token, username); -``` - -**子协议协商**: - -```c -struct mg_str *proto = mg_http_get_header(hm, "Sec-WebSocket-Protocol"); -// Mongoose 会自动回显匹配的协议,也可手动处理 -mg_ws_upgrade(c, hm, NULL); -``` - -## 五、帧格式详解 - -### 5.1 RFC 6455 帧结构 - -``` - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -+-+-+-+-+-------+-+-------------+-------------------------------+ -|F|R|R|R| opcode|M| Payload len | Extended payload length | -|I|S|S|S| (4) |A| (7) | (16/64) | -|N|V|V|V| |S| | (if payload len==126/127) | -| |1|2|3| |K| | | -+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + -| Extended payload length continued, if payload len == 127 | -+ - - - - - - - - - - - - - - - +-------------------------------+ -| |Masking-key, if MASK set to 1 | -+-------------------------------+-------------------------------+ -| Masking-key (continued) | Payload Data | -+-------------------------------- - - - - - - - - - - - - - - - + -: Payload Data continued ... : -+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + -| Payload Data continued ... | -+---------------------------------------------------------------+ -``` - -### 5.2 Mongoose 的帧头构建 (`mkhdr`, 第 96 行) - -```c -static size_t mkhdr(size_t len, int op, bool is_client, uint8_t *buf) { - size_t n = 0; - buf[0] = (uint8_t) (op | 128); // FIN=1, Opcode=op - if (len < 126) { // 7位长度足够 - buf[1] = (unsigned char) len; - n = 2; - } else if (len < 65536) { // 16位扩展长度 - uint16_t tmp = mg_htons((uint16_t) len); - buf[1] = 126; - memcpy(&buf[2], &tmp, sizeof(tmp)); - n = 4; - } else { // 64位扩展长度 - buf[1] = 127; - // 先写高32位,再写低32位 - tmp = mg_htonl((uint32_t) (((uint64_t) len) >> 32)); - memcpy(&buf[2], &tmp, sizeof(tmp)); - tmp = mg_htonl((uint32_t) (len & 0xffffffffU)); - memcpy(&buf[6], &tmp, sizeof(tmp)); - n = 10; - } - // 客户端帧需要掩码 - if (is_client) { - buf[1] |= 1 << 7; // 设置 MASK 位 - mg_random(&buf[n], 4); // 生成随机掩码 key - n += 4; - } - return n; -} -``` - -**注意**:**服务端发送的帧不需要掩码**(RFC 6455 第 5.1 节)。只有客户端发送到服务端的帧才需要掩码。Mongoose 通过 `is_client` 标志来控制。 - -### 5.3 帧解析 (`ws_process`, 第 66 行) - -```c -static size_t ws_process(uint8_t *buf, size_t len, struct ws_msg *msg) { - memset(msg, 0, sizeof(*msg)); - if (len >= 2) { - n = buf[1] & 0x7f; // 7位载荷长度 - mask_len = buf[1] & 128 ? 4 : 0; // MASK 位 → 掩码 key 长度 - msg->flags = buf[0]; - - if (n < 126 && len >= mask_len) { - msg->data_len = n; - msg->header_len = 2 + mask_len; - } else if (n == 126 && len >= 4 + mask_len) { - msg->header_len = 4 + mask_len; - msg->data_len = (((size_t) buf[2]) << 8) | buf[3]; // 16位长度 - } else if (len >= 10 + mask_len) { - msg->header_len = 10 + mask_len; - msg->data_len = be32(buf+2)<<32 | be32(buf+6); // 64位长度 - } - } - // 安全检查:数据长度不能超过 1GB - if (msg->data_len > 1024 * 1024 * 1024) return 0; - if (msg->header_len + msg->data_len > len) return 0; // 数据不完整 - - // 如果有掩码,解码 - if (mask_len > 0) { - uint8_t *p = buf + msg->header_len, *m = p - mask_len; - for (i = 0; i < msg->data_len; i++) p[i] ^= m[i & 3]; // XOR 解码 - } - return msg->header_len + msg->data_len; -} -``` - -### 5.4 掩码处理 - -**为什么需要掩码**:RFC 6455 要求客户端发往服务端的所有帧必须掩码。这是为了防止"缓存投毒攻击"——恶意脚本通过浏览器发送精心构造的 WebSocket 帧,可能被中间代理缓存误解为 HTTP 请求。 - -**解码算法**(异或): - -``` -for (i = 0; i < data_len; i++) - payload[i] = payload[i] ^ masking_key[i % 4] -``` - -**发送时的掩码**(`mg_ws_mask`, ws.c 第 124 行): - -```c -static void mg_ws_mask(struct mg_connection *c, size_t len) { - if (c->is_client && c->send.buf != NULL) { - uint8_t *p = c->send.buf + c->send.len - len, *mask = p - 4; - for (i = 0; i < len; i++) p[i] ^= mask[i & 3]; - } -} -``` - -只在客户端模式(`c->is_client == true`)时对数据执行掩码。服务端不需要。 - -## 六、协议处理器 `mg_ws_cb` 详解(ws.c 第 166 行) - -这是 WebSocket 连接的核心处理器,注册为 `c->pfn`,处理所有接收到的帧: - -``` -mg_ws_cb 处理流程(MG_EV_READ 事件时): - - 1. 客户端模式:检查握手是否完成 - → 未完成:调用 mg_ws_client_handshake() 验证 HTTP 101 响应 - → 完成:继续解析帧 - - 2. 循环解析帧: - while (ws_process() 成功解析一帧) { - - 3. 根据操作码分类处理: - - ┌─ WEBSOCKET_OP_CONTINUE (0): - │ 分片帧 → 触发 MG_EV_WS_CTL - │ - ├─ WEBSOCKET_OP_TEXT (1) / WEBSOCKET_OP_BINARY (2): - │ 如果 FIN=1 (完整帧): - │ → 触发 MG_EV_WS_MSG(用户在此接收消息) - │ 如果 FIN=0 (分片开始): - │ → 不触发事件(等待后续帧组装) - │ - ├─ WEBSOCKET_OP_CLOSE (8): - │ → 触发 MG_EV_WS_CTL - │ → 回显 CLOSE 帧给对端 - │ → 设置 c->is_draining = 1(优雅关闭) - │ - ├─ WEBSOCKET_OP_PING (9): - │ → 自动回复 PONG - │ → 触发 MG_EV_WS_CTL(通知用户) - │ - ├─ WEBSOCKET_OP_PONG (10): - │ → 触发 MG_EV_WS_CTL(用户可检测心跳响应) - │ - └─ 未知操作码: - → mg_error() 关闭连接 - - 4. 分片帧处理(ws.c 第 215-233 行): - 如果 FIN=0 或 op=CONTINUE: - → 第一条分片帧:保留 1 字节 op 标记 - → 后续帧:剥离帧头,保留数据 - → 所有分片数据累积在 c->recv 中 - 如果 FIN=1 且 op=CONTINUE: - → 分片结束,触发 MG_EV_WS_MSG - → 从 c->recv 中删除完整消息 - } -``` - -### 分片帧的处理细节 - -Mongoose 支持分片帧的自动组装: - -``` -客户端发送三条分片帧: - Frame 1: FIN=0, op=TEXT, data="Hello " - Frame 2: FIN=0, op=CONTINUE, data="World" - Frame 3: FIN=1, op=CONTINUE, data="!" - -Mongoose 内部处理: - 1. 收到 Frame 1: - → 保留 flags 在 c->recv 中 (buf[0]=0x01 TEXT) - → 剥离帧头,数据变为 "\x01Hello " - → ofs 追踪到数据末尾 - 2. 收到 Frame 2: - → 剥离帧头,数据追加 → "\x01Hello World" - → ofs 更新 - 3. 收到 Frame 3: - → 剥离帧头,数据追加 → "\x01Hello World!" - → FIN=1, op=CONTINUE: 触发 MG_EV_WS_MSG - → m.flags = c->recv.buf[0] (TEXT) - → m.data = "Hello World!" (跳过第1字节的标记) - → 删除已处理数据 -``` - -## 七、发送函数详解 - -### 7.1 `mg_ws_send()` — 发送一条完整消息(第 132 行) - -```c -size_t mg_ws_send(struct mg_connection *c, const void *buf, size_t len, int op); -``` - -流程: -1. 调用 `mkhdr()` 构建帧头 -2. 发送帧头(通过 `mg_send` 写入 `c->send` 缓冲区) -3. 发送数据 -4. 如果是客户端模式 → 对数据执行掩码 - -```c -// 使用示例 -mg_ws_send(c, "hello", 5, WEBSOCKET_OP_TEXT); // 发送文本 -mg_ws_send(c, data, len, WEBSOCKET_OP_BINARY); // 发送二进制 -mg_ws_send(c, NULL, 0, WEBSOCKET_OP_PING); // 发送 Ping -``` - -### 7.2 `mg_ws_printf()` — 格式化发送(第 26 行) - -```c -size_t mg_ws_printf(struct mg_connection *c, int op, const char *fmt, ...); - -// 使用示例 -mg_ws_printf(c, WEBSOCKET_OP_TEXT, "{\"temp\":%.2f,\"unit\":\"%s\"}", 23.5, "C"); -``` - -内部调用 `mg_vxprintf()` 格式化到 `c->send`,然后调用 `mg_ws_wrap()` 添加帧头。 - -### 7.3 `mg_ws_wrap()` — 为已有数据添加帧头(第 289 行) - -```c -size_t mg_ws_wrap(struct mg_connection *c, size_t len, int op); -``` - -这个是内部函数,用于为已经在 `c->send` 中的数据添加 WebSocket 帧头。适用场景:先写入 JSON 数据到 send 缓冲区,再包装成 WS 帧。 - -```c -// mg_ws_printf 的内部流程: -c->send.len → mg_vxprintf 写入 JSON 数据 - → mg_ws_wrap 在前面插入帧头 - → mg_ws_mask 如果是客户端则掩码 -``` - -## 八、服务端完整事件处理最佳实践 - -### 8.1 标准事件处理模板 - -```c -static void fn(struct mg_connection *c, int ev, void *ev_data) { - // 1. TLS 初始化(如果需要 WSS) - if (ev == MG_EV_ACCEPT) { - struct mg_tls_opts opts = { - .cert = mg_str(s_cert_pem), - .key = mg_str(s_key_pem), - }; - mg_tls_init(c, &opts); - } - - // 2. WebSocket 握手 - if (ev == MG_EV_HTTP_MSG) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - struct mg_str *ws_key = mg_http_get_header(hm, "Sec-WebSocket-Key"); - if (ws_key != NULL) { - // 可以在此做认证 - // struct mg_str *token = mg_http_get_header(hm, "Authorization"); - mg_ws_upgrade(c, hm, NULL); // 升级到 WebSocket - } else { - mg_http_reply(c, 200, "", "Use WebSocket\n"); - } - } - - // 3. WebSocket 连接建立 - if (ev == MG_EV_WS_OPEN) { - struct mg_http_message *hm = (struct mg_http_message *) ev_data; - // hm 是发起升级的原始 HTTP 请求,可以获取 URI、Cookie 等 - MG_INFO(("WebSocket connected, URI: %.*s", hm->uri.len, hm->uri.buf)); - } - - // 4. 接收 WebSocket 消息 - if (ev == MG_EV_WS_MSG) { - struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; - uint8_t op = wm->flags & 15; - if (op == WEBSOCKET_OP_TEXT) { - // 文本消息 - MG_INFO(("TEXT: %.*s", wm->data.len, wm->data.buf)); - // 回显 - mg_ws_send(c, wm->data.buf, wm->data.len, WEBSOCKET_OP_TEXT); - } else if (op == WEBSOCKET_OP_BINARY) { - // 二进制消息 - MG_INFO(("BINARY: %zu bytes", wm->data.len)); - // 处理二进制数据 - } - } - - // 5. 控制帧通知 - if (ev == MG_EV_WS_CTL) { - struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; - uint8_t op = wm->flags & 15; - if (op == WEBSOCKET_OP_PING) { - MG_DEBUG(("Ping received")); - // Ping 已被 Mongoose 自动回复 Pong - } else if (op == WEBSOCKET_OP_PONG) { - MG_DEBUG(("Pong received")); - // 可用于检测客户端存活 - } else if (op == WEBSOCKET_OP_CLOSE) { - MG_INFO(("Client sent close")); - } - } - - // 6. 连接关闭 - if (ev == MG_EV_CLOSE) { - MG_INFO(("WebSocket disconnected")); - } -} -``` - -### 8.2 广播消息给多个客户端 - -```c -static void broadcast(struct mg_mgr *mgr, const char *msg, size_t len) { - struct mg_connection *c; - for (c = mgr->conns; c != NULL; c = c->next) { - if (c->is_websocket && !c->is_listening) { // 只发给 WS 客户端 - mg_ws_send(c, msg, len, WEBSOCKET_OP_TEXT); - } - } -} -``` - -### 8.3 心跳检测 - -```c -// 定时发送 Ping 帧检测客户端是否存活 -static void heartbeat_timer(void *arg) { - struct mg_mgr *mgr = (struct mg_mgr *) arg; - struct mg_connection *c; - for (c = mgr->conns; c != NULL; c = c->next) { - if (c->is_websocket && !c->is_listening) { - mg_ws_send(c, NULL, 0, WEBSOCKET_OP_PING); - } - } -} - -// 在 main 中添加定时器 -mg_timer_add(&mgr, 30000, MG_TIMER_REPEAT, heartbeat_timer, &mgr); - -// 在事件处理器中检测 Pong -if (ev == MG_EV_WS_CTL) { - struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; - if ((wm->flags & 15) == WEBSOCKET_OP_PONG) { - // 客户端存活确认 - } -} -``` - -## 九、服务端 vs 客户端差异总结 - -| 特性 | 服务端 | 客户端 | -|------|--------|--------| -| 帧掩码 | **不掩码** | **必须掩码**(4 字节随机 key + XOR) | -| 握手方式 | `mg_ws_upgrade()` | `mg_ws_connect()` | -| 握手角色 | 接收 `Sec-WebSocket-Key`,计算 `Accept` | 生成随机 `Key`,验证 `Accept` | -| Ping/Pong | 可主动发送 | 可主动发送 | -| 关闭帧 | 收到后回显 | 收到后回显 | -| `is_client` | `false` | `true`(由 `mg_connect` 设置) | - -## 十、调试与诊断 - -### 启用 hex dump - -```c -// 在 MG_EV_ACCEPT 或 MG_EV_WS_OPEN 中设置 -c->is_hexdumping = 1; -``` - -这会将 WebSocket 帧的原始字节打印到日志中(通过 `mg_hexdump()`)。 - -### 常见问题排查 - -1. **客户端收到 "not http" 错误**:握手 URL 没有使用 `http://` 或 `https://` 前缀 -2. **握手被拒绝**:检查 `Sec-WebSocket-Key` 是否存在;URL 路径是否正确 -3. **消息收到乱码**:检查客户端是否正确掩码;操作码是否正确(TEXT=1, BINARY=2) -4. **内存泄漏**:确保 `mg_mgr_poll()` 被循环调用,否则连接不会释放 - -## 十一、MQTT over WebSocket - -Mongoose 支持 MQTT over WebSocket。当 MQTT 连接 URL 使用 `mqtt://` 方案且底层升级为 WebSocket 时,MQTT 模块会自动使用 WebSocket 帧封装 MQTT 包。这是服务端常用的场景——通过 WebSocket 传输 MQTT 协议。 diff --git a/mimo/MEMORY.md b/mimo/MEMORY.md new file mode 100644 index 0000000..719510d --- /dev/null +++ b/mimo/MEMORY.md @@ -0,0 +1,118 @@ +# Project memory +_Durable project-level knowledge. Persists across all sessions in this project. Edit only content under italic instructions._ + +## Project context +_What is this project? What's its goal? High-level identity._ + +**RTU (Remote Terminal Unit)** — 基于 IEC 61850 的智能通信网关,用于电力系统自动化。 + +- **核心协议**: IEC 61850 MMS(服务端/客户端)、IEC 101/104(通过 ICP67 协议) +- **通信方式**: TCP/UART/UDP 多通道、MQTT(mosquitto)、WebSocket +- **数据模型**: 信号数据中心(遥测/遥信/遥控/遥调/参数),支持 XXH128 哈希索引 +- **技术栈**: C/C++(GCC/Linux ARM 交叉编译)、Mongoose 嵌入式 Web 服务器、linenoise CLI +- **编译**: `./release/build.sh`(x86)或 `./release/build.sh arm`(ARM 交叉编译) +- **运行**: `./test/RTU` + +### 模块架构 + +``` +src/ +├── system/RTU/ # 主程序入口、CLI、自点表配置、应用系统调度 +├── system/libdatacenter/ # 数据中心(信号注册/变更检测/SBO控制/事件队列) +├── system/libcom_channel/ # 通信通道管理(TCP服务端/客户端、串口) +├── protocol/libmms_s/ # IEC 61850 MMS 服务端(ICD解析、模型创建、控制/定值/文件) +├── protocol/libmms_m/ # IEC 61850 MMS 客户端(总召/读写/报告订阅) +├── protocol/libicp67/ # ICP67 协议(IEC 101/104 帧编解码) +├── protocol/libmongoose/ # Mongoose HTTP/WebSocket 嵌入式服务器 +├── public/libmy_mosquitto/ # MQTT 客户端库(mosquitto 2.x) +├── public/libcJSON/ # JSON 解析 +└── public/libcomm/ # 统一通信抽象层(TCP/UART/UDP) +``` + +9 个应用线程: `app_sys`, `app_cmd`, `app_comm_channel`, `app_com_scan`, `app_iec`, `app_self_ptl`, `app_web_server`, `app_iec61850m`, `app_iec61850s` + +## Rules +_Hard constraints from user that every session must respect._ + +### 工作流程规则 +1. **对话语言**: 全程使用中文显示 +2. **编译路径**: `./release/build.sh`(x86)或 `./release/build.sh arm`(ARM交叉编译) +3. **执行路径**: `./test/RTU` +4. **排查问题流程**: 先重新读取对应位置的源码,再对照问题或打印信息排查,不要依赖记忆中的旧代码 +5. **问题记录**: 每次解决的问题都追加到 `claude/问题处理文档.md` +6. **文档目录**: + - `claude/mid/` — 存放中间文档、plan 文档 + - `claude/工程/` — 存放按模块记录的项目工程文档 +7. **每次读取项目工程时**: 把读到的东西按模块生成文档记录到 `claude/工程/` 文件夹 +8. **解决问题的 plan**: 每次制定并执行 plan 解决问题后,将 plan 内容整理为文档存入 `claude/mid/` 目录 +9. **Claude→Mimo 记忆转换规则** (2026-06-12, plan 1781250239159-brave-harbor): + - `.claude/memory/MEMORY.md` → Mimo `memory/projects/global/MEMORY.md` + - `.claude/memory/code-style.md` + `project-rules.md` → MEMORY.md §Rules + - `.claude/memory/user_language.md` → Mimo `memory/global/MEMORY.md` + - `claude/mid/*.md` → 提取核心知识 → MEMORY.md §Discovered durable knowledge + - `claude/工程/*.md` → 提取核心知识 → MEMORY.md §Architecture decisions + - 原始 claude 文件保留不动,仅新增 Mimo 记忆文件 + +### C/C++ 代码格式规范 +- **缩进**: 使用 **Tab 字符**缩进(显示宽度 4),不使用空格缩进 +- **括号风格**: Allman 风格 — 所有大括号独占一行(函数、if、for、while、struct),单条语句也保留大括号 +- **空格**: + - 关键字与括号之间不加空格: `if(`, `for(`, `while(`, `switch(` + - 函数名与括号之间不加空格: `func(args)` + - 指针声明: `type *name`(`*` 前有空格,后无空格) + - 引用声明: `type &name` +- **Yoda 条件**: 常量写在比较运算符左侧,`if(NULL == ptr)` 而非 `if(ptr == NULL)` +- **空行**: 函数之间两行空行,逻辑块之间一行空行,`#include` 区块末尾一行空行 +- **typedef struct**: 结构体成员无缩进额外层级(与 `{` 对齐) +- **命名约定**: 局部静态变量用 `LOCAL` 宏(= `static`),全局变量用 `g_` 前缀,结构体用 `stru_` 前缀,枚举用 `enum_` 前缀/`ENUM_` 值前缀 + +## Architecture decisions +_Major design choices with rationale. The "why" matters more than the "what" for future sessions._ + +### 1. 数据中心信号变更检测 +`dc_signal.cpp` 使用 XXH128 哈希对信号快速索引(`signal_out`, `signal_in`, `signal_yk`, `signal_ao`, `signal_param` 五张表)。输出信号变更通过脏队列 + 去重 + 回调循环防护实现增量推送(仅变更时发送),`last_caller_module` 防止同模块回调自循环。 + +### 2. WebSocket 多连接隔离 +每个 WebSocket 连接拥有独立的信号资源 session(per-connection),连接建立时开辟、断开时释放。全局共享方案会导致一个客户端的 add/del 操作影响其他客户端。 + +### 3. MMS 客户端事件驱动模型 +`mms_m.cpp` 使用事件队列 + 状态机模式,定时器驱动周期性操作(T0=120s all-call, T1=60s GI, T2=30s CO, T3=20s param)。RCB 订阅支持可配置化编号过滤。 + +### 4. IEC 61850 服务器模型 +`mms_s_icd.cpp` 解析 SCL XML ICD 文件构建完整数据模型。`mms_s_model.cpp` 动态创建 IedModel(LD/LN/DO/SDO/DA 树)。定值组管理通过 SGCB + SG/SE 镜像 DA 实现编辑区/运行区隔离。 + +### 5. 通信通道统一抽象 +`com_channel.cpp` 管理所有通信通道配置。`libcomm` 提供统一连接/发送/接收/断开接口,按类型分发到 TCP/UART/UDP 实现。ICP66 帧转换为 ICP67 格式。 + +### 6. MQTT 通信 +使用自编译 mosquitto 2.x 客户端库(`libmy_mosquitto`),支持 MQTT v5 特性。 + +## Discovered durable knowledge +_Cross-task facts that survive across sessions. Promoted from session checkpoints' §7 when proven durable._ + +### 已修复的关键问题 + +#### #1 RCB 订阅编号可配置化(2026-06-10) +- **问题**: `mms_m_icd_report_init()` 硬编码只订阅编号 `"01"` 的 RCB +- **修复**: 新增 `mms_m_out_set_rcb_numbers()` API,支持逗号分隔的多编号和通配符 `"*"` +- **文件**: `myMms_m.h`, `mms_m.h`, `mms_m.cpp`, `iec61850m.cpp` + +#### #2 libweb_server 多连接支持(2026-06-10) +- **问题**: 原 WebSocket 服务端仅支持单客户端,存在多线程竞态、悬空指针等 10 个缺陷 +- **修复**: 改为多连接,每个连接独立信号资源 session,断开自动释放 +- **文件**: `web_server.cpp`, `ws_method.h`, `ws_method.cpp` + +#### #3 app_cmd CPU 108% 问题(2026-06-12) +- **根因**: `linenoiseEdit()` 中 `read()` 仅检查 `-1` 未处理 EOF(返回 0),非终端环境下形成死循环 +- **修复**: 改为 `<= 0` 判断 + `enableRawMode()` 返回值检查 + `select()` 非阻塞方案 +- **文件**: `my_cmd.cpp`, `app_cmd.cpp` + +#### #4 Tab 命令补全前缀丢失(2026-06-12) +- **问题**: 子命令 Tab 补全时命令名前缀被覆盖(如 `datacenter param` 变 `param`) +- **修复**: Tab 键处理改为找到最后一个空格,只替换空格之后的当前词 +- **文件**: `my_cmd.cpp` + +#### #5 app_cmd 交互卡顿(2026-06-12) +- **问题**: select 非阻塞 + 100ms 定时器导致回车后回显有卡顿感 +- **修复**: 改为简单阻塞循环 `while(1) { cmd_recv(); }`,linenoise 自管理终端模式 +- **文件**: `app_cmd.cpp` diff --git a/.claude/memory/user_language.md b/mimo/global-MEMORY.md similarity index 52% rename from .claude/memory/user_language.md rename to mimo/global-MEMORY.md index e1598f1..e1cbfcd 100644 --- a/.claude/memory/user_language.md +++ b/mimo/global-MEMORY.md @@ -1,10 +1,7 @@ ---- -name: user-language -description: 用户要求所有对话、思考过程和中间输出均使用中文显示 -metadata: - node_type: memory - type: user - originSessionId: 5e65af11-b197-479e-a82f-c2e5b475a8c3 ---- +# Global memory +_Cross-project user preferences. Persists across all projects. Edit only content under italic instructions._ + +## User language preference +_What language should the assistant use for all conversations, thinking, and visible outputs?_ 用户要求所有对话内容、思考过程(thinking)、以及所有用户能看到的中间过程都使用中文显示。包括但不限于:回复内容、代码注释、任务列表、提交信息等。代码本身(变量名、函数名等)保持英文不变。 diff --git a/mimo/plan/RTU_Claude到Mimo文档迁移.md b/mimo/plan/RTU_Claude到Mimo文档迁移.md new file mode 100644 index 0000000..bf317a1 --- /dev/null +++ b/mimo/plan/RTU_Claude到Mimo文档迁移.md @@ -0,0 +1,70 @@ +# Plan: RTU 项目 Claude → Mimo 文档迁移 + +## 背景 +前序任务(T1-T6)已完成:仓库克隆、源码读取、Claude 记忆文件已提取到 Mimo MEMORY.md。本次将 claude/ 目录下的文档和配置全部迁移到 mimo/。 + +--- + +## 步骤 + +### T7: 保存当前 plan 到 mimo/plan/ +- 创建 `mimo/plan/` 目录 +- 将本 plan 内容保存为 `mimo/plan/RTU_Claude到Mimo文档迁移.md` +- **规则**: 此后每次制定 plan 实施前,都先将 plan 文档存入 `mimo/plan/` + +### T8: 删除 .claude/memory/ 配置 +- 前提确认: Claude 记忆文件内容已全部记录在 `mimo/MEMORY.md` 和规范路径 MEMORY.md 中 +- 删除整个 `.claude/memory/` 目录 + +### T9: 创建 mimo/ 子目录结构 +- 创建 `mimo/工程/` +- 创建 `mimo/中间文档/` + +### T10: 复制 mid/ → 中间文档/ +- 复制 `claude/mid/RCB订阅编号可配置化.md` → `mimo/中间文档/RCB订阅编号可配置化.md` +- 复制 `claude/mid/Tab命令补全功能.md` → `mimo/中间文档/Tab命令补全功能.md` +- 复制 `claude/mid/app_cmd_CPU108问题修复.md` → `mimo/中间文档/app_cmd_CPU108问题修复.md` + +### T11: 复制 问题处理文档 +- 复制 `claude/问题处理文档.md` → `mimo/问题处理文档.md` + +### T12: 重写 工程/ 文档(8篇,每篇对照源码彻底重写) +对 claude/工程/ 下每个文档: +1. 读取原 claude 文档 +2. 读取对应模块的完整源码 +3. 用我的理解与思路重写,形成新文档放入 `mimo/工程/` + +| 原文件 | 对应源码模块 | +|--------|------------| +| `libiec61850_MMS客户端API开发手册.md` | `src/protocol/libmms_m/` + `release/inc/myMms_m.h` | +| `libiec61850_MMS服务端API开发手册.md` | `src/protocol/libmms_s/` | +| `libiec61850m模块分析.md` | `src/system/libiec61850m/` | +| `libiec61850s模块分析.md` | `src/system/libiec61850s/` | +| `libmms_m模块分析.md` | `src/protocol/libmms_m/` | +| `libmms_s模块分析.md` | `src/protocol/libmms_s/` | +| `libweb_server模块分析.md` | `src/system/libweb_server/` | +| `websocket-server.md` | `src/protocol/libmongoose/src/mongoose.c` + `src/system/libweb_server/` | + +### T13: 删除 claude/ 和 .claude/ 全部内容 +- 删除 `claude/` 目录及其所有内容 +- 删除 `.claude/` 目录(已在上一步删除 memory/,确认无残留) + +### T14: 验证 +- 确认 `mimo/` 目录结构完整 +- 确认 `claude/` 和 `.claude/` 已删除 +- 确认 `mimo/MEMORY.md` 中的路径引用更新(如有涉及 claude/ 路径) + +--- + +## 关键文件 +- **源目录**: `/mnt/AI/AI_mimo/claude/`(待删除) +- **源目录**: `/mnt/AI/AI_mimo/.claude/`(待删除) +- **目标目录**: `/mnt/AI/AI_mimo/mimo/plan/` +- **目标目录**: `/mnt/AI/AI_mimo/mimo/工程/` +- **目标目录**: `/mnt/AI/AI_mimo/mimo/中间文档/` +- **目标文件**: `/mnt/AI/AI_mimo/mimo/问题处理文档.md` + +## 验证方式 +1. `tree /mnt/AI/AI_mimo/mimo/` 检查目录结构 +2. 确认 `claude/` 和 `.claude/` 不存在 +3. `git status` 确认变更范围 diff --git a/claude/mid/RCB订阅编号可配置化.md b/mimo/中间文档/RCB订阅编号可配置化.md similarity index 100% rename from claude/mid/RCB订阅编号可配置化.md rename to mimo/中间文档/RCB订阅编号可配置化.md diff --git a/claude/mid/Tab命令补全功能.md b/mimo/中间文档/Tab命令补全功能.md similarity index 100% rename from claude/mid/Tab命令补全功能.md rename to mimo/中间文档/Tab命令补全功能.md diff --git a/claude/mid/app_cmd_CPU108问题修复.md b/mimo/中间文档/app_cmd_CPU108问题修复.md similarity index 100% rename from claude/mid/app_cmd_CPU108问题修复.md rename to mimo/中间文档/app_cmd_CPU108问题修复.md diff --git a/mimo/工程/libiec61850_MMS客户端API开发手册.md b/mimo/工程/libiec61850_MMS客户端API开发手册.md new file mode 100644 index 0000000..a7daeb0 --- /dev/null +++ b/mimo/工程/libiec61850_MMS客户端API开发手册.md @@ -0,0 +1,304 @@ +# libiec61850_MMS客户端API开发手册 + +**日期**: 2026-06-12 +**基于源码**: `release/inc/myMms_m.h`, `src/system/libiec61850m/` + +--- + +## 1. 模块层次 + +RTU 的 MMS 客户端分为两层: + +``` +┌─────────────────────────────┐ +│ iec61850m (系统层) │ ← 本文档描述 +│ - 配置解析、信号初始化 │ +│ - 回调转发给 datacenter │ +├─────────────────────────────┤ +│ libmms_m (协议层) │ ← 底层 API,详见《libmms_m模块分析》 +│ - IED 连接、定时器、事件 │ +│ - MMS 协议编解码 │ +└─────────────────────────────┘ +``` + +`iec61850m` 负责解析 XML 配置(`mms_m.xml`),编排信号初始化流程,将 `libmms_m` 的数据通过回调桥接到数据中心(`datacenter`)。 + +--- + +## 2. 核心 API(`release/inc/myMms_m.h`) + +### 2.1 `mms_m_out_init` + +```c +int mms_m_out_init(stru_cfg *p_cfg, int debug_print_flag, uint32_t connectionTimeout); +``` + +**功能**: 创建一个 IED 连接。 + +| 参数 | 说明 | +|------|------| +| `p_cfg` | 配置文件结构体指针,必须包含 `path`(配置文件路径) | +| `debug_print_flag` | 调试打印开关(1=开, 0=关),开启后打印 ICD 发现详情和 MMS 值 | +| `connectionTimeout` | 连接超时(毫秒) | + +**返回值**: 成功返回 `obj_fd`(>0),失败返回 `-1`。 + +**副作用**: 创建 `stru_mms_m_obj` 实例,启动独立工作线程 `mms_m_run_thread`,开始异步连接远方 IED。 + +--- + +### 2.2 `mms_m_out_get_connect_status` + +```c +int mms_m_out_get_connect_status(int app_fd, mms_m_out_status_cb p_func); +``` + +**功能**: 注册连接状态回调。 + +| 参数 | 说明 | +|------|------| +| `app_fd` | `mms_m_out_init` 返回的句柄 | +| `p_func` | 回调函数 `void(int fd, int status)`,status: `MMS_M_ON_LINE` 或 `MMS_M_OFF_LINE` | + +--- + +### 2.3 `mms_m_out_debug_print_swicth` + +```c +int mms_m_out_debug_print_swicth(int app_fd, int debug_print_flag); +``` + +**功能**: 运行时开关调试打印。 + +--- + +### 2.4 `mms_m_out_do_set_yk` + +```c +int mms_m_out_do_set_yk(stru_mms_m_event *p_event); +``` + +**功能**: 下达遥控命令(Select/Direct/Cancel)。 + +```c +typedef struct +{ + int app_fd; // IED 句柄 + char ied[64]; // IED 名称(app_fd = 0 时按名称匹配) + char saddr[256]; // 信号地址 + uint8_t ctrl_type; // 操作类型 + char data[128]; // 操作数据 + int set_fd; // 操作序号 +}stru_mms_m_event; +``` + +`ctrl_type` 取值: +- `_MMS_M_EVENT_CO_SELECT`: 选择(SBO 模式第一步) +- `_MMS_M_EVENT_CO_DIRECT`: 执行(Direct 或 SBO 第二步) +- `_MMS_M_EVENT_CO_CANCEL`: 取消 + +**注意**: `app_fd > 0` 时按句柄查找;`app_fd = 0` 时按 `ied` 名称查找。 + +--- + +### 2.5 `mms_m_out_read_ao_or_params` + +```c +int mms_m_out_read_ao_or_params(int app_fd, uint8_t type, const char *saddr); +``` + +**功能**: 读取 AO 或 Param 值。 + +| 参数 | 说明 | +|------|------| +| `type` | `_MMS_M_EVENT_AO_READ` 或 `_MMS_M_EVENT_PARAM_READ` | +| `saddr` | 指定信号地址(`nullptr` 时读取全部) | + +--- + +### 2.6 `mms_m_out_get_value` + +```c +int mms_m_out_get_value(int fd, mms_m_out_value_cb p_func); +``` + +**功能**: 注册数据推送回调。所有读取到的 ST/MX/AO/Param 值都通过此回调向外推送。 + +```c +typedef void (*mms_m_out_value_cb)(stru_mms_m_out_value &val); +``` + +`stru_mms_m_out_value` 字段: +```c +typedef struct +{ + int app_fd; // IED 句柄 + char name[256]; // 信号 saddr + char desc[256]; // 信号描述 + char reference[256]; // MMS 对象引用 + void *p_value; // 数据指针 + uint8_t type; // MMS 数据类型(BOOLEAN/INTEGER/UNSIGNED/FLOAT/STRING) + int quality; // 品质位 + stru_mms_m_time time; // 时标 + int reason; // 原因码(ALL_CALL/GI/dchg/qchg 等) +}stru_mms_m_out_value; +``` + +--- + +### 2.7 `mms_m_out_bind_param_zone_signal` + +```c +int mms_m_out_bind_param_zone_signal(int app_fd, const char *zone_saddr); +``` + +**功能**: 绑定定值区号信号。定值区号变化时自动触发参数重读。 + +--- + +### 2.8 `mms_m_out_set_rcb_numbers` + +```c +int mms_m_out_set_rcb_numbers(int app_fd, const char *rcb_numbers); +``` + +**功能**: 设置 RCB 订阅编号。 + +| 输入 | 行为 | +|------|------| +| 不调用 | 默认订阅 `"01"` | +| `"01"` | 只订阅 01 号 | +| `"01,02,03"` | 订阅多个编号 | +| `"*"` | 订阅全部 RCB | +| `"abc"` / `"1"` / `"012"` | 非法,返回 -1 | + +--- + +### 2.9 工具函数 + +```c +void *mms_m_create_data_ptr(uint8_t type); // 创建数据指针(需手动 delete) +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 *s); // 转字符串 +int mms_m_set_data_by_str(void *data, uint8_t type, const char *s); // 从字符串设置 +char *mms_m_out_reason_str(int reason); // 原因码→字符串 +``` + +`type` 取值: `MMS_BOOLEAN`, `MMS_INTEGER`, `MMS_UNSIGNED`, `MMS_FLOAT`, `MMS_STRING` + +--- + +## 3. iec61850m 初始化流程 + +### 3.1 两阶段初始化 + +``` +app_iec61850m_init1: + 1. 获取进程目录 + 2. 解析 config/MMS/mms_m.xml(点表配置) + - St/Mx/Co/Ao/Param 各信号点的 saddr/type/ctrl_model/desc + 3. 为每个 IED 调用 mms_m_out_init → 获取 obj_fd + 4. 注册数据回调: mms_m_out_get_value → 桥接到 datacenter + 5. 注册连接状态回调 + +app_iec61850m_init2: + 1. 调用 mms_m_out_debug_print_swicth(传入调试标志) + 2. 调用 mms_m_out_set_rcb_numbers(传入 RCB 编号配置) + 3. 调用 mms_m_out_bind_param_zone_signal(绑定定值区信号) +``` + +### 3.2 数据回调桥接 + +iec61850m 的数据回调完成 MMS 值 → datacenter 的转换: + +``` +mms_m_out_get_value_callback(out_val): + → 根据 reason 分类处理: + ALL_CALL: dc_set_out_signal_val(name, p_value, type, quality) + GI: dc_set_out_signal_val(name, p_value, type, quality) + dchg/qchg: // 报告触发,同上 + → 对 CO/AO/Param: 转发到对应的 datacenter 写接口 +``` + +--- + +## 4. 配置文件格式(mms_m.xml) + +```xml + + + + + + + + + + + + + + + + + +``` + +- `link`: 信号 saddr,对应 datacenter 中的信号地址 +- `reference`: IEC 61850 对象引用,用于 `IedConnection_readObject` +- `name`: IED 名称,用于连接日志 +- `rcb_numbers`: RCB 编号过滤(逗号分隔,可选) + +--- + +## 5. 常见使用场景 + +### 5.1 读取遥测/遥信 + +``` +// 自动完成 — 定时器 T0 (120s) 触发 All-Call,T1 (60s) 触发 GI +// 数据通过 mms_m_out_get_value 注册的回调推送 +// 用户只需在 iec61850m_init1 中调用 mms_m_out_get_value 注册回调 +``` + +### 5.2 手动读取 AO 值 + +```cpp +// 读取指定 AO +mms_m_out_read_ao_or_params(fd, _MMS_M_EVENT_AO_READ, "AO_Signal_1"); +// 读取全部 AO +mms_m_out_read_ao_or_params(fd, _MMS_M_EVENT_AO_READ, nullptr); +``` + +### 5.3 下达遥控命令 + +```cpp +// SBO 模式 +stru_mms_m_event event = {}; +event.app_fd = fd; +event.ctrl_type = _MMS_M_EVENT_CO_SELECT; +strncpy(event.saddr, "YK_Signal_1", sizeof(event.saddr)); +mms_m_out_do_set_yk(&event); + +// 确认后执行 +event.ctrl_type = _MMS_M_EVENT_CO_DIRECT; +mms_m_out_do_set_yk(&event); +``` + +### 5.4 写入定值 + +```cpp +// 定值写入通过 mms_m_out_read_ao_or_params 触发 O→Param 流程 +// libmms_m 内部自动处理 SG/SE 编辑-确认流程 +``` + +### 5.5 RCB 编号配置 + +```cpp +// 只订阅 01 号 RCB +mms_m_out_set_rcb_numbers(fd, "01"); +// 订阅 01, 02, 03 号 +mms_m_out_set_rcb_numbers(fd, "01,02,03"); +// 订阅全部 +mms_m_out_set_rcb_numbers(fd, "*"); +``` diff --git a/mimo/工程/libiec61850_MMS服务端API开发手册.md b/mimo/工程/libiec61850_MMS服务端API开发手册.md new file mode 100644 index 0000000..9812151 --- /dev/null +++ b/mimo/工程/libiec61850_MMS服务端API开发手册.md @@ -0,0 +1,266 @@ +# libiec61850_MMS服务端API开发手册 + +**日期**: 2026-06-12 +**基于源码**: `src/system/libiec61850s/`, `src/protocol/libmms_s/inc/` + +--- + +## 1. 模块层次 + +``` +┌─────────────────────────────┐ +│ iec61850s (系统层) │ ← 本文档描述 +│ - XML配置解析、信号注册 │ +│ - 回调桥接到 datacenter │ +├─────────────────────────────┤ +│ libmms_s (协议层) │ ← 底层 API,详见《libmms_s模块分析》 +│ - ICD解析、模型创建、IedServer│ +│ - 控制/定值/文件服务 │ +└─────────────────────────────┘ +``` + +--- + +## 2. 核心概念 + +### 2.1 sAddr(信号地址) + +sAddr 是 IEC 61850 标准中的短地址字段,在 ICD 文件中定义。RTU 用它作为 datacenter 中信号的唯一标识,打通 "MMS 数据属性 ↔ datacenter 信号" 的映射。 + +### 2.2 信号类型 + +| 分类 | ICD FC | datacenter 表 | 说明 | +|------|--------|-------------|------| +| ST(状态) | ST | signal_out | 遥信/双点状态 | +| MX(测量) | MX | signal_out | 遥测/测量值 | +| CO(控制) | CO | signal_yk | 遥控输出 | +| AO(模拟输出) | 自定义 | signal_ao | 定值区号/SP定值 | +| Param(参数) | SG/SE | signal_param | 定值组参数(多区) | + +--- + +## 3. 配置文件格式(mms_s.xml) + +```xml + + + + + + + + + + + + + + + + + + +``` + +- `no`: 序号 +- `link`: sAddr,对应 datacenter 中的信号地址和 ICD 中的 sAddr 字段 + +配置解析后存入 `stru_mms_cfg`,包含 `vec_st`, `vec_mx`, `vec_co`, `vec_ao`, `vec_param` 五个向量。 + +--- + +## 4. 初始化流程 + +### 4.1 两阶段初始化 + +``` +app_iec61850s_init1: + 1. 获取进程目录(通过 /proc/self/exe) + 2. 解析 config/MMS/mms_s.xml → stru_mms_cfg(信号点表) + 3. 设置调试开关: mms_s_dbg_switch(false) + 4. 设置文件路径: mms_s_file_path_set(base_path) + 5. 注册值更新回调: mms_s_value_update_register(&cb) + +app_iec61850s_init2: + 1. iec61850s_signals_init: + - iec61850s_st_signals_init: + dc_signal_out_link_with_callback → 获取 ST 信号指针并注册变更回调 + 回调: iec61850s_st_mx_change_callback → mms_s_value_update → 更新 MMS 模型值 + - iec61850s_mx_signals_init: 同上(MX 信号) + - iec61850s_control_signals_init: + dc_get_yk_signal_info → 获取控制信号信息 + mms_s_control_register → 向 libmms_s 注册控制回调 + 回调: iec61850s_control_callback → dc_signal_yk_set_status + - iec61850s_setting_signals_init: + dc_signal_ao_link_with_callback → 绑定 AO 信号 + mms_s_setting_register → 注册 SP 定值回调 + 回调: iec61850s_setting_callback → dc_signal_ao_set_val + - iec61850s_param_signals_init: + dc_get_param_signal_info → 获取 Param 信号指针 + mms_s_param_register → 注册定值组回调 + 回调: iec61850s_param_callback → dc_signal_param_set_val + 2. 解析 ICD 文件: config/MMS/PCS.icd + 3. 调用 mms_s_init(icd_path, 102) 启动 MMS 服务器 + 4. 绑定定值区信号: mms_s_bind_param_zone_signal(zone_saddr, iec61850s_sg_change_callback) +``` + +### 4.2 信号初始化详细流程 + +**ST/MX 信号**(单向:datacenter → MMS): +``` +datacenter 信号值变化 + → iec61850s_st_mx_change_callback(saddr, type, p_data, p_last_data) + → dc_get_signal_val(p_data, type) → 字符串值 + → mms_s_value_update(saddr, val) → 更新 MMS 模型 → 触发 RCB 报告 +``` + +**控制信号**(双向:MMS 客户端 → datacenter): +``` +远方 MMS 客户端发送控制命令 + → libmms_s control_handler → iec61850s_control_callback(control, state) + → 判断 ctrl_model: + DIRECT: dc_signal_yk_set_status(DIRECT) + SBO: dc_signal_yk_set_status(SELECT) + dc_signal_yk_set_status(DIRECT) + STATUS_ONLY: 仅记录日志 +``` + +**AO/SP 定值信号**(双向:MMS → datacenter): +``` +远方客户端修改 SP 定值 + → libmms_s setting writeAccessHandler → iec61850s_setting_callback(setting, data) + → dc_set_signal_val_from_str → 解析字符串 + → dc_signal_ao_set_val(SELECT+DIRECT or DIRECT only) +``` + +**Param 定值组信号**(双向,支持多区): +``` +远方客户端 ConfirmEditSG + → libmms_s confirmEditSG_callback → iec61850s_param_callback(param, data, zone) + → dc_signal_param_set_val(zone, data) +``` + +--- + +## 5. 核心 API + +### 5.1 信号注册(iec61850s 内部) + +```cpp +int iec61850s_signals_init(); +``` +**功能**: 从 XML 配置初始化全部五类信号,注册各类回调到 libmms_s。在 `app_iec61850s_init2` 中调用。 + +### 5.2 值更新(libmms_s 提供) + +```cpp +void mms_s_value_update_register(mms_s_value_update_cb *pp_cb); +int mms_s_value_update(const char *saddr, const char *value_str); +``` +**功能**: 更新 MMS 模型中指定 sAddr 的 DA 值,自动更新时标和品质。 + +### 5.3 控制注册(libmms_s 提供) + +```cpp +int mms_s_control_register(stru_mms_s_control *p_control, int count, mms_s_control_cb p_callback); +``` +**功能**: 注册控制 DO 的回调处理函数。 + +`stru_mms_s_control`: +```c +typedef struct +{ + stru_mms_s_signal_base base; // saddr, desc, type, ctrl_model + void *p_data; // 信号数据指针(指向 datacenter) +}stru_mms_s_control; +``` + +### 5.4 SP 定值注册(libmms_s 提供) + +```cpp +int mms_s_setting_register(stru_mms_s_setting *p_setting, int count, mms_s_setting_cb p_callback); +``` +**功能**: 注册 SP(fc=SP)定值的写入回调。 + +### 5.5 定值组注册(libmms_s 提供) + +```cpp +int mms_s_param_register(stru_mms_s_param *p_param, int count, mms_s_param_cb p_callback); +``` +**功能**: 注册定值组参数(SG/SE)的确认回调。 + +`stru_mms_s_param`: +```c +typedef struct +{ + stru_mms_s_signal_base base; // saddr, desc, type, ctrl_model + void *p_data[MAX_ZONE]; // 各定值区的数据指针 + uint8_t param_num; // 定值区数量 +}stru_mms_s_param; +``` + +### 5.6 其他 libmms_s API + +```cpp +int mms_s_init(const char *icd_path, int stack_size); // 启动 MMS 服务器 +void mms_s_dbg_switch(bool on); // 调试开关 +void mms_s_file_path_set(const char *base_path); // 文件路径 +int mms_s_bind_param_zone_signal(const char *zone_saddr, mms_s_sg_change_cb cb); // 绑定定值区 +struct _sIedModel_wrapper *mms_s_get_icd_ptr(); // 获取 IedModel +IedServer mms_s_get_ied_server_ptr(); // 获取 IedServer +``` + +--- + +## 6. 运行时流程 + +### 6.1 主线程循环 + +```cpp +void *app_iec61850s(void *arg): + while(1): + task_event_recv(p_event, EV_TIMER1 | EV_TIMER2 | EV_TIMER3, ...) + + EV_TIMER1 (10ms): 空闲 + EV_TIMER2 (100ms): 空闲 + EV_TIMER3 (1000ms): + - check_sg_zone_change(): 检测定值区号变化,更新 MMS 模型 + - p_app->run_cnt++: 运行计数 +``` + +### 6.2 定值区切换检测 + +``` +ie61850s_sg_change_callback(zone_saddr, new_act_sg): + → 遍历 g_vec_setting 找到匹配的 saddr + → dc_signal_ao_set_val(SELECT + DIRECT) 通过 datacenter 更新值 + → 设置 g_sg_zone_saddr 和 g_sg_zone_act_sg + +check_sg_zone_change() (EV_TIMER3 中): + → 如果有待更新信号 → mms_s_value_update(saddr, val) + → 更新 MMS 模型,通知所有订阅客户端 +``` + +--- + +## 7. 常见配置场景 + +### 7.1 添加新的遥测信号 + +1. 在 ICD 中定义 DA 的 sAddr +2. 在 `mms_s.xml` 的 `` 中添加 `` +3. 在 datacenter 初始化中注册该信号 +4. 重启 RTU,初始化时自动绑定 + +### 7.2 添加新的遥控信号 + +1. ICD 中定义含 `ctlModel` 的 CO DO +2. `mms_s.xml` 的 `` 中添加信号 +3. datacenter 中注册 yk 信号 +4. `iec61850s_control_signals_init` 自动通过 sAddr 找到对应的 ModelNode 安装控制回调 + +### 7.3 配置多定值区参数 + +1. ICD 中 SG 定义多组 SE +2. `mms_s.xml` 的 `` 中添加信号 +3. datacenter 中注册 param 信号(`dc_signal_param_reg` 需指定定值区数) +4. `iec61850s_param_signals_init` 自动获取各定值区数据指针 diff --git a/mimo/工程/libiec61850m模块分析.md b/mimo/工程/libiec61850m模块分析.md new file mode 100644 index 0000000..8dfa6dd --- /dev/null +++ b/mimo/工程/libiec61850m模块分析.md @@ -0,0 +1,194 @@ +# libiec61850m 模块分析 + +**日期**: 2026-06-12 +**基于源码**: `src/system/libiec61850m/` (iec61850m.cpp, parse_xml.cpp, 共4个文件) + +--- + +## 1. 模块定位 + +`libiec61850m` 位于系统层,是 `libmms_m` 的上层封装。它负责: +- 解析 XML 配置(`mms_m.xml`) +- 编排各 IED 的信号初始化流程 +- 将 MMS 读取的数据通过回调桥接到 `datacenter` +- 接收来自其他模块的遥控命令,转发到 `libmms_m` + +**与其他模块的关系**: +``` +app_sys (调度) → app_iec61850m (线程,EV_TIMER 驱动) + ↓ + libiec61850m (本模块,系统层封装) + ↓ 调用 API + libmms_m (协议层,每个 IED 一个线程) + ↓ IedConnection API + 远方 IED 装置 +``` + +## 2. 配置解析(parse_xml.cpp) + +### 2.1 XML 结构 + +```xml + + + + + + + + + +``` + +### 2.2 解析流程 + +``` +parse_mms_xml(path): + → 加载 XML → 根元素 + → mms_m_parse_para(root, cfg): + 提取全局 rcb_numbers 属性 + → 遍历每个 子元素: + mms_m_parse_base(iedEle, cfg): + 提取 name, ip, port + mms_m_parse_signals: 遍历 St/Mx/Co/Ao/Param + → 每个 Signal: 提取 link(saddr), reference, type, ctrl_model + → 存入 stru_mms_cfg 对应的 vector + 存入 cfg.vec_ieds +``` + +### 2.3 数据结构 + +```cpp +typedef struct +{ + std::string path; // XML 文件路径 + int rcb_numbers_flag; // 是否有全局 rcb_numbers + std::string rcb_numbers; // 全局 RCB 编号(如 "01,02") + std::vector vec_ieds; // 各 IED 的配置 +}stru_mms_cfg; + +typedef struct +{ + std::string name; // IED 名称 + std::string ip; // IP 地址 + int port; // MMS 端口(通常 102) + std::string rcb_numbers; // 此 IED 的 RCB 编号 + std::vector vec_st; + std::vector vec_mx; + std::vector vec_co; + std::vector vec_ao; + std::vector vec_param; +}stru_mms_ied_cfg; +``` + +`stru_mms_signal`(与 `myMms_m.h` 中的 `stru_point_item` 对应): +```cpp +typedef struct +{ + char saddr[MMS_M_STR_LEN]; // 信号地址(link 属性) + char reference[MMS_M_REF_LEN]; // MMS 对象引用 + char desc[MMS_M_STR_LEN]; // 描述 + uint8_t type; // 数据类型 + uint8_t ctrl_model; // 控制模型 +}stru_mms_signal; +``` + +## 3. 两阶段初始化 + +### 3.1 第一阶段:`app_iec61850m_init1` + +``` +1. 获取进程目录(/proc/self/exe → 项目根目录) +2. 解析 config/MMS/mms_m.xml → stru_mms_cfg +3. 为每个 IED: + a. 将信号配置转换为 libmms_m 需要的 stru_cfg 格式 + b. 调用 mms_m_out_init(p_cfg, debug, timeout) → obj_fd + c. 调用 mms_m_out_get_value(obj_fd, iec61850m_value_callback) + 注册数据回调 → 桥接到 datacenter + d. 调用 mms_m_out_get_connect_status(obj_fd, iec61850m_status_callback) + 注册连接状态回调 +4. 保存 obj_fd 列表到全局 +``` + +### 3.2 第二阶段:`app_iec61850m_init2` + +``` +1. 遍历所有 IED 的 obj_fd: + a. mms_m_out_debug_print_swicth(fd, debug_flag) // 设置调试 + b. 如果配置中指定了 rcb_numbers: + mms_m_out_set_rcb_numbers(fd, rcb_numbers) + c. 如果配置了定值区信号: + mms_m_out_bind_param_zone_signal(fd, zone_saddr) +``` + +### 3.3 为什么分两阶段 + +- `init1` 执行时 datacenter 尚未完全初始化 +- `init2` 在 `init1` 之后执行,此时 datacenter 已就绪,可以安全注册信号绑定 + +## 4. 数据回调桥接 + +`iec61850m_value_callback` 是核心桥接函数,将 libmms_m 推送的 `stru_mms_m_out_value` 转换为 datacenter 操作: + +``` +iec61850m_value_callback(out_val): + → 根据 out_val.reason 分类: + MMS_M_REASON_ALL_CALL: // 定时全数据刷新 + MMS_M_REASON_READ_AO: // 主动读取 AO + MMS_M_REASON_READ_PARAM: // 主动读取 Param + IEC61850_REASON_DATA_CHANGE / QUALITY_CHANGE / GI: + // 报告触发 + → 写入 datacenter: + - ST/MX (out): dc_set_out_signal_val(name, p_value, type) + - AO: dc_signal_ao_set_val(...) + - Param: dc_signal_param_set_val(...) + - CO: // CO 由操作完成回调处理,不走数据回调 +``` + +## 5. 遥控命令转发 + +其他模块(如 CLI、self_ptl)通过以下路径下发遥控: + +``` +外部模块 → iec61850m_do_set_yk(event) + → 填充 stru_mms_m_event: + - app_fd: 目标 IED 句柄 + - ctrl_type: SELECT/DIRECT/CANCEL + - saddr: 信号地址 + - data: 操作数据 + → mms_m_out_do_set_yk(&event) → libmms_m 处理 +``` + +## 6. 线程模型 + +``` +app_iec61850m 线程: + while(1): + task_event_recv(EV_TIMER1 | EV_TIMER2 | EV_TIMER3) + EV_TIMER1 (10ms): 空闲 + EV_TIMER2 (100ms): 空闲 + EV_TIMER3 (1000ms): p_app->run_cnt++ +``` + +主工作不在 `app_iec61850m` 线程中,而是在 libmms_m 为每个 IED 创建的独立线程 `mms_m_run_thread` 中。`app_iec61850m` 线程仅用于接收定时器事件和消息队列。 + +## 7. 模块间数据流 + +``` +远方 IED 装置 + ↓ MMS 协议 +libmms_m (mms_m_run_thread) + ↓ mms_m_out_value_cb +iec61850m_value_callback + ↓ dc_set_out_signal_val / dc_signal_ao_set_val / dc_signal_param_set_val +datacenter (signal_out/signal_ao/signal_param 表) + ↓ 变更通知 +其他模块 (self_ptl, com_channel, web_server, iec61850s) + +控制方向(反向): +CLI / self_ptl + ↓ dc_signal_yk_set_status / iec61850m_do_set_yk +libmms_m (事件队列 → mms_m_send_co) + ↓ ControlObjectClient_select/operate/cancel +远方 IED 装置 +``` diff --git a/mimo/工程/libiec61850s模块分析.md b/mimo/工程/libiec61850s模块分析.md new file mode 100644 index 0000000..29a99e4 --- /dev/null +++ b/mimo/工程/libiec61850s模块分析.md @@ -0,0 +1,214 @@ +# libiec61850s 模块分析 + +**日期**: 2026-06-12 +**基于源码**: `src/system/libiec61850s/` (iec61850s.cpp, parse_xml.cpp, 共4个文件) + +--- + +## 1. 模块定位 + +`libiec61850s` 位于系统层,是 `libmms_s` 的上层封装。它负责将 datacenter 中的信号注册到 MMS 服务端模型中,使远方客户端能通过 IEC 61850 MMS 协议读取 RTU 数据并下发控制命令。 + +**与其他模块的关系**: +``` +远方 MMS 客户端 + ↓ MMS 协议 +libmms_s (协议层, IedServer) + ↑↓ 回调 +libiec61850s (本模块) + ↑↓ dc_signal_* API +datacenter (信号存储) +``` + +## 2. 配置解析(parse_xml.cpp) + +### 2.1 XML 结构 + +```xml + + + + + + + + + + + + + + + + + +``` + +解析函数 `parse_mms_xml(path)` 使用 tinyxml2 遍历五类信号元素,提取 `link` 属性存入 `stru_mms_cfg`。 + +### 2.2 数据结构 + +```cpp +typedef struct +{ + char saddr[MMS_S_STR_LEN]; + char desc[MMS_S_STR_LEN]; + uint8_t type; + uint8_t ctrl_model; +}stru_mms_s_signal_base; + +typedef struct +{ + std::vector vec_st; + std::vector vec_mx; + std::vector vec_co; + std::vector vec_ao; + std::vector vec_param; +}stru_mms_cfg; +``` + +配置保存在全局变量 `g_cfg` 中,通过 `mms_cfg_ptr_get()` 获取。 + +## 3. 两阶段初始化 + +### 3.1 `app_iec61850s_init1`(第一阶段) + +``` +1. 获取进程目录 +2. 解析 config/MMS/mms_s.xml → stru_mms_cfg +3. mms_s_dbg_switch(false) // 关闭调试 +4. mms_s_file_path_set(base_path) // 设置文件服务根目录 +5. mms_s_value_update_register(&g_mms_s_value_update_cb) // 注册值更新回调 +``` + +此时仅完成配置解析和底层回调注册,datacenter 尚未就绪。 + +### 3.2 `app_iec61850s_init2`(第二阶段) + +``` +1. iec61850s_signals_init(): // 核心信号初始化 + - iec61850s_st_signals_init: + 对每个 ST 信号调用 dc_signal_out_link_with_callback + → 获取 datacenter 信号指针 + 注册变更回调 + → 回调: st_mx → mms_s_value_update (更新 MMS 模型) + + - iec61850s_mx_signals_init: + 同上(MX 信号) + + - iec61850s_control_signals_init: + 对每个 CO 信号: + dc_get_yk_signal_info → 获取类型和 ctrl_model + mms_s_control_register → 向 libmms_s 注册控制回调 + 回调接收远方控制命令 → dc_signal_yk_set_status + + - iec61850s_setting_signals_init: + 对每个 AO 信号: + dc_signal_ao_link_with_callback → 绑定 SP 定值 + mms_s_setting_register → 注册写入回调 + 回调接收客户端修改 → dc_signal_ao_set_val + + - iec61850s_param_signals_init: + 对每个 Param 信号: + dc_get_param_signal_info → 获取各定值区指针 + mms_s_param_register → 注册 ConfirmEditSG 回调 + 回调接收定值确认 → dc_signal_param_set_val + +2. mms_s_init(PCS.icd, 102): + 解析 ICD → 创建模型 → 启动 IedServer(端口 102) + +3. mms_s_bind_param_zone_signal(zone_saddr, iec61850s_sg_change_callback): + 绑定定值区号 → 定值区切换时自动同步 +``` + +## 4. 信号方向与回调链 + +### 4.1 数据上行(datacenter → MMS 客户端) + +``` +datacenter 信号变化 + → iec61850s_st_mx_change_callback(saddr, type, p_data, p_last_data) + → dc_get_signal_val → 格式化为字符串 + → g_mms_s_value_update_cb(saddr, val_str) // 即 mms_s_value_update + → IedServer_updateAttributeValue → 更新模型值 → 触发 RCB 报告 → 通知订阅客户端 +``` + +### 4.2 控制下行(MMS 客户端 → datacenter) + +``` +远方客户端控制命令 + → libmms_s control_handler + → iec61850s_control_callback(p_control, state): + state == 1 (命令到达): + 根据 ctrl_model: + DIRECT/SBO: dc_signal_yk_set_status(SELECT + DIRECT) + STATUS_ONLY: 日志记录 +``` + +### 4.3 SP 定值修改(MMS 客户端 → datacenter) + +``` +远方客户端修改 SP 定值 + → libmms_s setting writeAccessHandler + → iec61850s_setting_callback(p_setting, p_data): + dc_set_signal_val_from_str → 解析字符串值 + dc_signal_ao_set_val(SELECT + DIRECT or DIRECT only) +``` + +### 4.4 定值组修改(MMS 客户端 → datacenter) + +``` +远方客户端 ConfirmEditSG + → libmms_s confirmEditSG_callback + → iec61850s_param_callback(p_param, p_data, zone): + dc_set_signal_val_from_str → 解析 + dc_signal_param_set_val(zone, data) +``` + +### 4.5 定值区切换同步 + +``` +datacenter 定值区号变化 + → iec61850s_sg_change_callback(zone_saddr, new_act_sg): + 找到对应的 setting 信号 + dc_signal_ao_set_val: 同步新值到 AO 信号 + 设置 g_sg_zone_saddr / g_sg_zone_act_sg + → check_sg_zone_change() (EV_TIMER3 中): + mms_s_value_update: 更新 MMS 模型中 SGCB.ActSG 值 +``` + +## 5. 主线程循环 + +```cpp +void *app_iec61850s(void *arg): + while(1): + task_event_recv(EV_TIMER1 | EV_TIMER2 | EV_TIMER3) + + EV_TIMER1 (10ms): 空闲 + EV_TIMER2 (100ms): 空闲 + EV_TIMER3 (1000ms): + check_sg_zone_change() // 定值区号变更同步到 MMS 模型 + p_app->run_cnt++ +``` + +## 6. 全局变量 + +| 变量 | 类型 | 说明 | +|------|------|------| +| `g_vec_st` | `vector` | ST 信号列表(含 datacenter 指针) | +| `g_vec_mx` | `vector` | MX 信号列表 | +| `g_vec_control` | `vector` | 控制信号列表 | +| `g_vec_setting` | `vector` | SP 定值信号列表 | +| `g_vec_param` | `vector` | 定值组参数列表 | +| `g_mms_s_value_update_cb` | 函数指针 | MMS 值更新回调(= mms_s_value_update) | +| `g_sg_zone_saddr` | string | 待同步的定值区信号地址 | +| `g_sg_zone_act_sg` | string | 待同步的新定值区号 | + +## 7. 与 libiec61850m 的对比 + +| 特性 | libiec61850m (客户端) | libiec61850s (服务端) | +|------|----------------------|----------------------| +| 角色 | 主动读取/控制远方 IED | 被动响应远方客户端请求 | +| 数据方向 | 拉取(All-Call/GI) | 推送(RCB 报告触发) | +| 控制 | 下发控制命令 | 接收并执行控制命令 | +| 连接 | 每个 IED 一个连接 | 监听端口,接受多客户端 | +| 定时器 | T0(120s)/T1(60s)/T2(30s) | EV_TIMER3(1s) 仅定值区同步 | diff --git a/mimo/工程/libmms_m模块分析.md b/mimo/工程/libmms_m模块分析.md new file mode 100644 index 0000000..f436cc5 --- /dev/null +++ b/mimo/工程/libmms_m模块分析.md @@ -0,0 +1,224 @@ +# libmms_m 模块分析 + +**日期**: 2026-06-12 +**基于源码**: `src/protocol/libmms_m/`(8个文件,约3000行) + +--- + +## 1. 模块定位 + +`libmms_m` 是 RTU 的 IEC 61850 MMS 客户端库,位于协议层。它通过 libiec61850 库连接到远方 IED 装置,以事件驱动架构实现遥测/遥信读取(All-Call、GI)、遥控执行(SBO/Direct)、AO/参数读写、报告订阅等全部 MMS 客户端功能。 + +**与上下层关系**: +- **下层**: 依赖 libiec61850 v1.5.x 提供的 IedConnection API +- **上层**: 被 `iec61850m`(系统层封装)通过对外 API 调用 +- **并发**: 每个 IED 连接一个独立线程 `mms_m_run_thread`,通过事件队列 + 信号量与其他线程通信 + +## 2. 核心数据结构 + +### 2.1 主对象 `stru_mms_m_obj` + +每个 IED 连接创建一个 `stru_mms_m_obj` 实例,全局保存在 `g_mms_m_obj_map`(map)中,以 `obj_fd` 为句柄。 + +| 字段 | 用途 | +|------|------| +| `cfg_path` | 配置文件路径,用于错误日志和重复检测 | +| `ied_name` | IED 名称,从配置中提取 | +| `debug_print_flag` | 调试打印开关(`MMS_M_DEBUG_PRINT_ON = 1`) | +| `connectionTimeout` | 连接超时(毫秒) | +| `p_cfg` | 指向 `stru_cfg` 的点表配置(ST/MX/CO/AO/Param) | +| `ldevs` | LDevice 树(LD→LN→DO→point_item 指针),用于 dataset member 匹配 | +| `ld_datasets` | 运行时发现的 LD→Dataset→Member→RCB 完整结构 | +| `rcb_numbers` | 可配置的 RCB 编号列表(如 `{"01","02"}`),空则默认 `"01"` | +| `zone_saddr` | 绑定定值区信号的 saddr,用于检测定值区切换 | +| `current_zone` | 当前定值区号 | +| `obj_fd` | 对象句柄,对外 API 用此标识 | +| `run` | 运行时状态(连接、定时器、事件队列、回调列表) | + +### 2.2 运行时结构 `stru_mms_m_run` + +| 字段 | 用途 | +|------|------| +| `con` | `IedConnection` 句柄 | +| `con_state` / `old_con_state` | 连接状态(前后帧对比检测上线/离线) | +| `ip` / `port` | 远方 IED 的 IP/端口 | +| `running_init` | ICD 初始化是否完成标志 | +| `event_queue` | 环形事件队列(容量 `EVENT_QUEUE_SIZE`) | +| `sem` | 信号量,保护事件队列的读写 | +| `timer[4]` | 四个定时器 T0-T3 | +| `pthread_task` | 工作线程句柄 | +| `out_cb_lists` | 数据回调函数列表,向上层推送读取到的值 | +| `out_status_cb` | 连接状态回调(上线/离线通知) | + +### 2.3 事件类型 `_MMS_M_EVENT` + +``` +_MMS_M_EVENT_ALL_CALL // 全数据读取(ST+MX) +_MMS_M_EVENT_GI_CALL // 总召 +_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 // 写入参数值 +``` + +### 2.4 定时器配置 + +| 定时器 | 间隔 | 触发动作 | +|--------|------|---------| +| T0 | 120s | 发送 All-Call → 全数据刷新 | +| T1 | 60s | 发送 GI → 总召 | +| T2 | 30s | 读取所有 AO + Param | +| T3 | 20s | 预留(未使用) | + +定时器实现为倒计数(`cnt--`),在每次 `mms_m_run` 中检查。到零时触发回调并重置计数。 + +## 3. 核心流程 + +### 3.1 初始化流程 + +``` +上层调用 mms_m_out_init(p_cfg, debug_flag, timeout) + → 检查配置文件是否重复 + → new stru_mms_m_obj,初始化基础字段 + → mms_m_ied_init(obj): + - 提取 ied_name, ip, port + - 遍历 ST/MX/CO/AO/Param 点表 + - 调用 mms_m_add_point_to_ldevs 构建 LD→LN→DO→point 树形索引 + - 用于后续 dataset member 匹配 + → sem_init 信号量 + → pthread_create → mms_m_run_thread 启动工作线程 + → 返回 obj_fd +``` + +### 3.2 连接管理(`mms_m_do_comm`) + +``` +mms_m_run_thread 主循环(每 MMS_M_THREAD_RUN_TM 毫秒): + → mms_m_do_comm: + 比较 con_state 与 old_con_state: + - 非连接状态: IedConnection_connectAsync 异步重连 + - 新上线 (old != CONNECTED, new == CONNECTED): + mms_m_control_init() 重建 ControlObjectClient + 回调 out_status_cb(ON_LINE) + - 新离线 (old == CONNECTED, new != CONNECTED): + running_init = false // RCB 需重新订阅 + 回调 out_status_cb(OFF_LINE) + old_con_state = con_state + → if CONNECTED: mms_m_run(obj) +``` + +### 3.3 运行流程(`mms_m_run`) + +``` +mms_m_run(obj): + → mms_m_run_init(obj): // 仅首次执行 + mms_m_icd_init(obj): + - 遍历 LD → LN(通过 IedConnection_getLogicalDeviceList/Directory) + - 对每个 LN: mms_m_icd_dataset_init(获取所有 DataSet + Members) + - 对每个 LN: mms_m_icd_report_init(URCB + BRCB,按 rcb_numbers 过滤) + mms_m_ld_dataset_match_point_init(obj): + - 遍历所有 dataset member + - 解析 ref 得到 LD/LN/DO 名称 + - 在 ldevs 树中查找匹配的 DO,建立 member.p_do_vec 指针关系 + - 报告回调时通过此关系快速定位到具体信号点 + mms_m_rcb_init(obj): + - 获取 RCB 值(Resv→TrgOps→RptEna→GI) + - 安装 mms_m_report_callback + - 设置 TrgOps = dchg | qchg | GI + → mms_m_do_send(obj): 处理事件队列中的操作 + → mms_m_timer_running(obj): 检查四个定时器 +``` + +### 3.4 All-Call 流程 + +``` +T0 定时器到期 → mms_m_do_call_all: + → 推送 _MMS_M_EVENT_ALL_CALL 事件 + → 重置 T0 + +mms_m_do_send → mms_m_send_call_all: + 遍历 ST 和 MX 点表: + IedConnection_readObject(p_con, point.reference, fc) + → mms_m_get_mmsValue: MMS 类型转换 (BOOLEAN→uint8, INTEGER→int32, UNSIGNED→uint32, FLOAT→float, BIT_STRING→quality) + → 构造 out_val { name, desc, reference, time, value, quality, reason } + → mms_m_put_value: 遍历 out_cb_lists 执行每个回调 +``` + +### 3.5 遥控流程(SBO/Direct) + +``` +上层调用 mms_m_out_do_set_yk(event): + → 按 app_fd 或 ied_name 定位 obj + → mms_m_push_event + +mms_m_do_send → mms_m_send_co: + switch ctrl_type: + _MMS_M_EVENT_CO_SELECT: ControlObjectClient_select + _MMS_M_EVENT_CO_DIRECT: ControlObjectClient_operate + _MMS_M_EVENT_CO_CANCEL: ControlObjectClient_cancel + → mms_m_send_set_callback: 设置操作完成回调 +``` + +### 3.6 AO/Param 读写 + +``` +AO 读取:mms_m_send_read_ao + → 按 saddr 或全表读取 + → IedConnection_readObject → 类型转换 → 回调推送 + +Param 写入:mms_m_send_param_write + → 检测定值区是否切换(对比 current_zone) + → 定值区变化: + 读取 SG 信息 → select EditSG → 写入 SE 值 → confirm CnfEdit + → 定值区不变: + 直接写入 SE 值 → confirm + → 操作完成回调 +``` + +### 3.7 报告回调(`mms_m_report_callback`) + +``` +IedConnection 收到 report → mms_m_report_callback: + → 遍历 dataset members + → 检查 ReasonForInclusion(非 NOT_INCLUDED 才处理) + → mms_m_get_MmsValue 解析数据值和时标 + → 通过 member.p_do_vec 找到所有绑定的信号点 + → 构造 out_val → 遍历 out_cb_lists 推送 +``` + +## 4. 对外 API + +| API | 说明 | +|-----|------| +| `mms_m_out_init(p_cfg, dbg, timeout)` | 创建 IED 连接,返回 obj_fd | +| `mms_m_out_get_connect_status(fd, cb)` | 注册连接状态回调 | +| `mms_m_out_debug_print_swicth(fd, flag)` | 开关调试打印 | +| `mms_m_out_do_set_yk(event)` | 下达遥控命令(Select/Direct/Cancel) | +| `mms_m_out_read_ao_or_params(fd, type, saddr)` | 读取 AO/Param 值 | +| `mms_m_out_get_value(fd, cb)` | 注册数据回调 | +| `mms_m_out_bind_param_zone_signal(fd, saddr)` | 绑定定值区信号 | +| `mms_m_out_set_rcb_numbers(fd, numbers)` | 设置 RCB 订阅编号 | +| `mms_m_create_data_ptr(type)` | 创建类型对应的数据指针 | +| `mms_m_set_data_value(src, dst, type)` | 类型化数据赋值 | +| `mms_m_get_data_value_str(data, type, str)` | 类型化数据转字符串 | +| `mms_m_set_data_by_str(data, type, str)` | 字符串转类型化数据 | +| `mms_m_out_reason_str(reason)` | 原因码转字符串 | +| `mms_m_dbg_get(obj)` | 获取调试标志 | +| `mms_m_get_obj(fd)` | 按句柄获取对象指针 | + +## 5. 线程安全 + +- 事件队列 `push/pop` 受 `sem_wait/sem_post` 保护 +- `out_cb_lists` 在初始化时注册,运行时只读 +- `con_state/old_con_state` 仅在主线程(`mms_m_run_thread`)修改 +- libiec61850 的 report callback 在其他线程触发,通过 `mms_m_push_event` 转入主线程处理 + +## 6. 已知问题 + +1. **定时器倒计数精度**: 定时器基于循环次数×休眠时间,非高精度。取决于 `MMS_M_THREAD_RUN_TM` 大小 +2. **事件队列溢出**: 容量 `EVENT_QUEUE_SIZE`,环形覆盖不报错 +3. **RCB 编号过滤在 report_init 中执行**: 一旦初始化完成,变更 rcb_numbers 需重建连接 +4. **离线时 running_init 重置**: 重连后整个 ICD 发现流程重新执行 diff --git a/mimo/工程/libmms_s模块分析.md b/mimo/工程/libmms_s模块分析.md new file mode 100644 index 0000000..dd18c08 --- /dev/null +++ b/mimo/工程/libmms_s模块分析.md @@ -0,0 +1,283 @@ +# libmms_s 模块分析 + +**日期**: 2026-06-12 +**基于源码**: `src/protocol/libmms_s/`(16个文件,约7500行) + +--- + +## 1. 模块定位 + +`libmms_s` 是 RTU 的 IEC 61850 MMS 服务端库,位于协议层。它解析 ICD(IED Capability Description)XML 文件构建完整的数据模型,创建 `IedServer` 对外提供 MMS 服务,实现控制执行、定值组管理、文件服务等功能。 + +**与上下层关系**: +- **下层**: 依赖 libiec61850 v1.5.x 的 IedServer/IedModel API +- **上层**: 被 `iec61850s`(系统层封装)调用初始化,通过回调机制将控制/定值变更通知上层 +- **数据方向**: 接收远方客户端的 MMS 读写请求,通过回调将控制指令传递给 `iec61850s` → `datacenter` + +## 2. ICD 解析流程(`mms_s_icd.cpp`) + +ICD 文件是 SCL(Substation Configuration Language)格式的 XML。解析流程自顶向下: + +``` +XML 加载 (tinyxml2) + → SCL 根元素 + → DataTypeTemplates 区域: + - 遍历 LNodeType(逻辑节点类型) + - DO → 引用 DOType + - 遍历 DOType(数据对象类型) + - SDO → 递归引用 DOType + - DA → 属性定义(bType, fc, dchg, qchg) + - fc/dchg/qchg 从父级 BDA 向下传播 + - 遍历 DAType(数据属性类型) + - BDA → fc/dchg/qchg 从 BDA 向下传播到子 BDA + - 遍历 EnumType(枚举类型) + - EnumVal → 枚举值映射 + → IED 区域: + - AccessPoint → Server + - Server → LDevice + - LN0 (LLN0): + - DOI/SDI/DAI → 运行时值覆盖 + - DataSet → FCDA 成员 + - ReportControl → TrgOps, OptFields + - SettingControl → numOfSGs, actSG + - LN(逻辑节点)→ DOI/SDI/DAI + → 校验: + - LN/LN0 的 lnType 在 DataTypeTemplates 中存在 + - DataSet 的 FCDA 引用的 DO/SDO/DA 在模型中存在 +``` + +核心数据结构: +- `stru_all_DO`: 包含 `map_all_sdo`(子 SDO)和 `map_all_da`(子 DA),每个 DA 记录 `sAddr`, `bType`, `fc` +- `stru_icd`: 顶层结构,包含 `ied` 对象和 `map_lnode_type`, `map_do_type`, `map_da_type`, `map_enum_type` +- `stru_icd::ied`: 包含 `map_ldevice`(逻辑设备),`vec_all_DO`(所有 DO),`vec_control_do`(控制 DO),`vec_saddr`(信号地址列表),`map_saddr_point`(sAddr→模型节点映射) + +## 3. 动态模型创建(`mms_s_model.cpp`) + +将 ICD 解析的 SCL 结构映射为 libiec61850 的 `IedModel` 对象树。 + +### 3.1 类型映射 + +| ICD bType 字符串 | libiec61850 DataAttributeType | +|-----------------|------------------------------| +| `BOOLEAN` | IEC61850_BOOLEAN | +| `INT8` / `Struct` | IEC61850_INT8 | +| `INT16` | IEC61850_INT16 | +| `INT32` | IEC61850_INT32 | +| `INT64` | IEC61850_INT64 | +| `INT128` | IEC61850_INT128 | +| `INT8U` | IEC61850_INT8U | +| `INT16U` | IEC61850_INT16U | +| `INT24U` | IEC61850_INT24U | +| `INT32U` | IEC61850_INT32U | +| `FLOAT32` | IEC61850_FLOAT32 | +| `FLOAT64` | IEC61850_FLOAT64 | +| `VisString64` 等 | IEC61850_VISIBLE_STRING64 | +| `Unicode255` 等 | IEC61850_UNICODE_STRING255 | +| `Timestamp` | IEC61850_TIMESTAMP | +| `Dbpos` | IEC61850_DBPOS | +| `Quality` | IEC61850_QUALITY | +| `Check` | IEC61850_CHECK | +| `Tcmd` | IEC61850_TCMD | +| `OptFlds` | IEC61850_OPTFLDS | +| `TrgOps` | IEC61850_TRGOPS | +| `EntryID` | IEC61850_ENTRYID | +| `EntryTime` | IEC61850_ENTRYTIME | +| `ObjRef` | IEC61850_OBJREF | +| `PhyComAddr` | IEC61850_PHYCOMADDR | +| `Octet64` | IEC61850_OCTET_STRING_64 | +| `Currency` | IEC61850_CURRENCY | +| `AnalogueValue` | IEC61850_ANALOGUE_VALUE | +| `Unit` | IEC61850_UNIT | +| `Vector` | IEC61850_VECTOR | +| `ValWithTrans` | IEC61850_VAL_WITH_TRANS | +| `CUG` | IEC61850_CUG | + +### 3.2 创建流程 + +``` +model_init(icd): + IedModel_create(ied_name) // 创建根模型 + → model_ldevice_init(model, icd): + 遍历每个 LDevice: + LogicalDevice_create(name, model) + → model_init_lnodes(ld, icd): + 遍历每个 LN/LN0: + LogicalNode_create(name, ld) + → model_init_do(ln, icd): + 遍历 DO/SDO/DA 递归创建 DataObject 和 DataAttribute + 记录 sAddr → ModelNode 映射 + → 对 LN0: + model_init_datasets → 创建 DataSet + FCDA + model_init_rcb → 创建 URCB/BRCB + TrgOps/OptFields + model_init_sgcb → 创建 SGCB + SG/SE 镜像 + → model_sync_ied_default_values: ICD 默认值同步到模型节点 + → model_search_control_DataObjects: 搜索所有含 ctlModel 的 DO + → 设置 model.initializer = mms_s_values_init +``` + +### 3.3 SG/SE 定值组镜像 + +每个 LN0 的 SGCB 下创建 SG(Setting Group)和 SE(Setting Group Edit),SE 是 SG 的镜像: +- SG 的 sAddr = 原始 sAddr(如 `PROT/LLN0$SG$sg1$StrVal`) +- SE 的 sAddr = `SE_` 前缀版本 +- `mms_s_param.cpp` 只操作 SE,协议栈负责 SG↔SE 同步 + +### 3.4 sAddr 映射 + +`sAddr` 是信号地址,IEC 61850 标准字段。解析时保存 `sAddr → (fc, node)` 映射到 `map_saddr_point`,供上层 `iec61850s` 按 sAddr 注册信号回调。 + +## 4. MMS 服务器启动(`mms_s.cpp`) + +``` +mms_s_init(icd_path, stack_size): + → 解析 ICD → 创建 IedModel → model_init + → IedServerConfig 配置: + - 设置 selectLlN0ForUnmatchedCommands = true + - 设置 maxMmsConnections = default + - 设置 reportBufferSize = 8 + → IedServer_create(&model) // 创建服务器 + → mms_s_control_init(server) // 安装控制回调 + → mms_s_param_init(server) // 安装定值组回调 + → mms_s_file_init(server) // 安装文件服务 + → mms_s_setting_init(server) // 安装 SP 定值回调 + → IedServer_start(server, port) + → pthread_create → server_run_thread: + while(1) IedServer_processEvent(server, timeout) +``` + +## 5. 控制服务(`mms_s_control.cpp`) + +### 5.1 控制模型 + +支持三种控制模型(由 ICD 中 DO 的 `ctlModel` 决定): +- `status-only` (0): 仅状态,不参与控制 +- `direct-with-normal-security` (1): 直接执行 +- `sbo-with-normal-security` (2): 选择-执行(Select Before Operate) + +### 5.2 回调链 + +``` +远方客户端发送控制命令 + → IedServer 触发 control_handler(control, ctlVal, oper, interlockCheck): + - oper == OPERATE: 执行操作 + stVal 从 ctlVal 中提取 + 若是 SBO: control.oper() → 更新 t, origin, ctlNum + 若是 Direct: 直接更新 stVal + t + 调用 p_control_callback(state) → 通知 iec61850s + - oper == CANCEL: 取消操作 + → 同时触发 check_handler(control, ctlVal, oper, interlockCheck): + - interlockCheck == true: 检查连锁条件 + - 返回 false 阻止操作执行 +``` + +### 5.3 控制注册 + +``` +mms_s_control_register(controls, count, callback): + 遍历所有控制 DO(由 model_search_control_DataObjects 在 ICD 解析阶段收集) + → 通过 sAddr 在 map_saddr_point 中查找对应的 ModelNode + → IedServer_setControlHandlerEx → 安装 control_handler + check_handler + → 保存 callback 到全局列表 +``` + +## 6. 定值组管理(`mms_s_param.cpp`) + +### 6.1 SG/SE 编辑流程 + +``` +远方客户端编辑定值: + → SGCB.SelectEditSG(zone): 切换到编辑区 + → 加载当前激活 SG 的值到 SE + → 设置 selectEditSG_callback → 通知上层定值区切换 + → SE 各 DA 写入新值: + → writeAccessHandler: 校验 min/max/step + → 通过才允许写入 + → SGCB.ConfirmEditSG(zone): + → confirmEditSG_callback: + 读取 SE 所有值 → 校验范围/步长 + → 调用 p_param_callback(zone, data) → 通知 iec61850s + 返回 true 确认 / false 拒绝 +``` + +### 6.2 值校验 + +每种数据类型对应独立的校验函数: +- BOOLEAN: 直接通过 +- INT8/16/32: `minVal/maxVal/stepSize` 从 min/max/stepSize DA 读取 +- INT8U/16U/32U: 同上(无符号) +- FLOAT32/64: 校验 + epsilon 容差(`fabs(val - min) > 0.000001`) +- VisString/UnicodeString: 长度校验 +- Enum: 枚举值范围校验 + +### 6.3 定值区信号绑定 + +``` +mms_s_bind_param_zone_signal(saddr, callback): + → 在 LN0 的 SGCB 中查找 ActSG DA + → IedServer_handleWriteAccess: 安装 writeAccessHandler + → 当 ActSG 被写入时: + 读取新 actSG 值 + 调用 callback(saddr, new_act_sg) + → iec61850s 通过 datacenter 更新相关信号 +``` + +## 7. 固定定值管理(`mms_s_setting.cpp`) + +SP(Set Point)类 DA(fc=SP)的读写管理: + +``` +mms_s_setting_register(settings, count, callback): + → 对每个 SP DA: + - IedServer_setWriteAccessPolicy(deny) // 全局拒绝 SP 写入 + - IedServer_handleWriteAccess 安装 writeAccessHandler: + → 校验 min/max/step + → 允许后调用 p_setting_callback(saddr, data) + - sgcb 定值区变化时同步 SP 值 +``` + +## 8. 文件服务(`mms_s_file.cpp`) + +``` +file_init(server): + → IedServer_setFilestoreBasepath(path) + → IedServer_setFileServiceHandler: + - rename: 一律拒绝 + - read/write/delete: 允许 + - IEDSERVER.BIN 文件保护: 拒绝任何操作 + → IedServer_installConnectionHandler(connectionHandler): + 记录连接建立/断开日志 +``` + +## 9. DA 值初始化(`mms_s_value.cpp`) + +``` +mms_s_values_init(param, model): + // 作为 model->initializer 在 IedServer_create 时被调用 + → 遍历 LD → LN → DO → SDO → DA 树 + → 按 bType 初始化 MmsValue: + BOOLEAN → MmsValue_newBoolean(false) + INT32 → MmsValue_newInteger(0) + INT32U → MmsValue_newUnsigned(0) + FLOAT32 → MmsValue_newFloat(0.0) + VisString* → MmsValue_newVisibleString("") + Unicode* → MmsValue_newUnicodeString("") + Timestamp → MmsValue_newUtcTime(0) + Dbpos → MmsValue_newDbpos() + Enum → 按 OrderedEnum 表现(有 ord → INTEGER, 无 → 第一枚举值) + → MmsValue_setDeletable(value) // 标记为模型可管理生命周期 + +mms_s_value_update(saddr, value_str): + → 按 bType 从字符串解析值 → 更新 MmsValue + → IedServer_updateAttributeValue(server, node, value) // 触发 RCB 报告 + → 自动更新 t(时标)和 q(品质)字段 +``` + +## 10. 关键日志宏 + +```cpp +#define MMS_S_LOG_E(fmt, ...) LOG_E("[MMS_S][%s:%d]" fmt, __FILENAME__, __LINE__, ##__VA_ARGS__) +#define MMS_S_LOG_W(fmt, ...) LOG_W("[MMS_S][%s:%d]" fmt, __FILENAME__, __LINE__, ##__VA_ARGS__) +#define MMS_S_LOG_I(fmt, ...) LOG_I("[MMS_S][%s:%d]" fmt, __FILENAME__, __LINE__, ##__VA_ARGS__) +``` + +带颜色、时间戳、文件名和行号。 diff --git a/mimo/工程/libweb_server模块分析.md b/mimo/工程/libweb_server模块分析.md new file mode 100644 index 0000000..0e90774 --- /dev/null +++ b/mimo/工程/libweb_server模块分析.md @@ -0,0 +1,201 @@ +# libweb_server 模块分析 + +**日期**: 2026-06-12 +**基于源码**: `src/system/libweb_server/`(5个文件,约1500行) + +--- + +## 1. 模块定位 + +`libweb_server` 是 RTU 的嵌入式 Web 服务器,作为 `app_web_server` 线程(9个应用线程之第7号)运行。基于 Mongoose v7.x,提供: + +- HTTP 静态文件服务(内嵌前端工程) +- WebSocket 实时数据通道(JSON 格式) +- 多客户端并发支持(per-connection session) +- 信号增量推送(仅变更时发送) + +### 目录结构 + +``` +src/system/libweb_server/ +├── inc/ +│ ├── web_server.h # 模块头文件 +│ └── ws_method.h # WebSocket 消息处理接口 +└── src/ + ├── web_server.cpp # HTTP/WS 服务器 + Mongoose 事件循环 + ├── ws_method.cpp # WebSocket 命令解析 + JSON 推送 + └── packed_fs.c # 前端文件嵌入(自动生成) +``` + +## 2. 架构设计 + +### 2.1 Mongoose 集成 + +Mongoose 是单线程事件驱动的网络库。RTU 的 Web 服务器以独立线程运行,集成方式: + +``` +app_web_server_init1: + → mg_mgr_init(&mgr) // 初始化事件管理器 + → 加载 packed_fs(前端文件) // 如果定义了 USE_PACKED_FS + +app_web_server_init2: + → mg_http_listen(&mgr, "http://0.0.0.0:8000", fn, NULL) + → 启动事件循环 + +app_web_server 线程: + while(1): + task_event_recv(EV_TIMER1 | EV_TIMER2 | EV_TIMER3) + EV_TIMER1 (10ms): mg_mgr_poll(&mgr, 0) // 非阻塞轮询 + EV_TIMER2 (100ms): ws_task() // WebSocket 推送 + EV_TIMER3 (1000ms): 空闲 +``` + +### 2.2 事件处理函数 `fn` + +统一的 HTTP + WebSocket 事件处理: + +```cpp +static void fn(struct mg_connection *c, int ev, void *ev_data): + switch(ev): + MG_EV_HTTP_MSG: + → mg_http_get_header(hm, "Sec-WebSocket-Key") 存在? + YES → mg_ws_upgrade(c, hm, NULL) // WebSocket 握手 + NO → mg_http_serve_dir / mg_http_serve_packed // 静态文件 + + MG_EV_WS_MSG: + → ws_recv(c, wm->data.buf, wm->data.len) // WebSocket 消息 + + MG_EV_CLOSE: + → ws_session_destroy(c) // 释放 per-connection 资源 +``` + +### 2.3 多连接管理 + +```cpp +LOCAL std::vector g_ws_conns; // 所有 WS 连接 +LOCAL pthread_mutex_t g_ws_conns_mutex = PTHREAD_MUTEX_INITIALIZER; +``` + +- 连接建立时将 `mg_connection*` 加入 `g_ws_conns` +- 断开时从 `g_ws_conns` 移除并调用 `ws_session_destroy` +- 所有对 `g_ws_conns` 的访问受互斥锁保护 + +## 3. 前端嵌入式部署(packed_fs.c) + +`packed_fs.c` 由构建工具自动生成,将前端文件(HTML/CSS/JS)以 `unsigned char` 数组嵌入: + +```c +const struct mg_mem_file mg_packed_files[] = { + {"/css/style.css", v3, sizeof(v3) - 1, 1781072856}, + {"/index.html", v4, sizeof(v4) - 1, 1781073518}, + {"/js/app.js", v5, sizeof(v5) - 1, 1781073909}, + {"/js/monitor.js", v6, sizeof(v6) - 1, 1781072788}, + {"/js/pages.js", v7, sizeof(v7) - 1, 1781073958}, + {"/js/ws-client.js", v8, sizeof(v8) - 1, 1781071573}, + {NULL, NULL, 0, 0} +}; +``` + +Mongoose 通过 `mg_http_serve_packed` 直接返回内嵌文件,无需外部 `web_root` 目录。 + +## 4. WebSocket 消息处理(ws_method.cpp) + +### 4.1 消息格式 + +客户端发送 JSON 命令: + +```json +{ + "cmd": "subscribe", // 命令类型 + "type": "out,ao,param", // 订阅的信号类型 + "signals": ["saddr1", "saddr2"], // 指定信号(空=全部) + "conn_id": 12345 // 连接标识 +} +``` + +### 4.2 命令分发 + +`ws_recv` 解析 JSON → 提取 `cmd` 字段 → 路由到处理函数: + +| cmd | 功能 | 处理函数 | +|-----|------|---------| +| `subscribe` | 订阅信号推送 | `ws_handle_subscribe` | +| `unsubscribe` | 取消订阅 | `ws_handle_unsubscribe` | +| `read_all` | 读取全部当前值 | `ws_handle_read_all` | +| `set_value` | 设置 AO/Param 值 | `ws_handle_set_value` | +| `yk_control` | 遥控操作 | `ws_handle_yk_control` | + +### 4.3 信号订阅与增量推送 + +每个连接维护独立的 per-connection session: + +```cpp +struct ws_session { + unsigned long conn_id; + std::set subscribed_signals; // 已订阅的信号 saddr 集合 + std::set subscribed_types; // 已订阅的信号类型(out/ao/param) + // ... +}; +``` + +订阅流程: +``` +客户端发送 {cmd: "subscribe", type: "out,ao", signals: ["sig1","sig2"]} + → ws_handle_subscribe(c, json): + 获取或创建 session + 将信号 saddr 加入 subscribed_signals + 将类型加入 subscribed_types + 回复 {status: "ok"} +``` + +推送流程 (`ws_task`, 每 100ms 执行): +``` +ws_task(): + 遍历 g_ws_conns: + 对每个连接: + 收集该连接订阅的信号中发生了变更的 + 构建 JSON: {signals: [{saddr, value, time, quality}, ...]} + 通过 mg_ws_send 发送 +``` + +### 4.4 数据广播 + +```cpp +void ws_send_all(const char *p_tx, uint16_t tx_len): + pthread_mutex_lock(&g_ws_conns_mutex) + for each c in g_ws_conns: + mg_ws_send(c, p_tx, tx_len, WEBSOCKET_OP_TEXT) + pthread_mutex_unlock(&g_ws_conns_mutex) +``` + +### 4.5 单连接发送 + +```cpp +void ws_send_one(unsigned long conn_id, const char *p_tx, uint16_t tx_len): + 在 g_ws_conns 中查找 mg_connection.conn_id == conn_id + 找到 → mg_ws_send +``` + +## 5. HTTP API + +除了 WebSocket,还提供 RESTful HTTP 接口(通过 `MG_EV_HTTP_MSG` 处理): + +| 方法 | 路径 | 功能 | +|------|------|------| +| GET | `/` | 静态首页 | +| GET | `/api/status` | 设备状态 | +| GET | `/api/signals` | 信号列表快照 | +| POST | `/api/control` | 控制命令 | + +## 6. 线程安全 + +- Mongoose 事件循环在 `app_web_server` 线程中执行 +- `ws_task()` 在 `EV_TIMER2`(100ms)中执行,与事件循环同线程 +- `g_ws_conns` 访问受 `g_ws_conns_mutex` 保护 +- 通过任务事件机制与 datacenter 交互(信号值读取通过 `dc_get_signal_val` 等线程安全 API) + +## 7. 已知问题 + +1. **mg_mgr_poll 非阻塞模式**: `mg_mgr_poll(&mgr, 0)` 零超时,高频率轮询可能消耗 CPU。Mongoose 连接数少时不明显 +2. **packed_fs 更新**: 修改前端后需重新生成 `packed_fs.c` 并重新编译 +3. **广播效率**: `ws_send_all` 逐连接发送,大量连接时效率低。当前场景(嵌入式 RTU)连接数通常较少 diff --git a/mimo/工程/websocket-server.md b/mimo/工程/websocket-server.md new file mode 100644 index 0000000..c186561 --- /dev/null +++ b/mimo/工程/websocket-server.md @@ -0,0 +1,267 @@ +# WebSocket 服务端分析 + +**日期**: 2026-06-12 +**基于源码**: `src/protocol/libmongoose/src/mongoose.c`(ws.c 部分约 300 行) + +--- + +## 1. 协议概述 + +WebSocket 是 RFC 6455 定义的全双工通信协议,通过 HTTP Upgrade 从 HTTP 升级到持久 TCP 连接。RTU 使用 Mongoose 7.x 内置的 WebSocket 实现提供实时数据推送。 + +### 1.1 帧操作码 + +| 操作码 | 值 | 说明 | +|--------|---|------| +| CONTINUE | 0x0 | 分片帧的后续帧 | +| TEXT | 0x1 | UTF-8 文本帧 | +| BINARY | 0x2 | 二进制帧 | +| CLOSE | 0x8 | 关闭连接 | +| PING | 0x9 | 心跳请求 | +| PONG | 0xA | 心跳响应 | + +## 2. Mongoose WebSocket 实现 + +### 2.1 核心结构 + +```c +// 消息结构(零拷贝,引用 c->recv 缓冲区) +struct mg_ws_message { + struct mg_str data; // 消息数据 + uint8_t flags; // 高4位=FIN标志,低4位=操作码 +}; + +// 内部帧解析结构 +struct ws_msg { + uint8_t flags; + size_t header_len; // 帧头长度(含掩码key) + size_t data_len; // 数据长度 +}; +``` + +### 2.2 帧格式(RFC 6455) + +``` +字节0: [FIN(1)] [RSV(3)] [OPCODE(4)] +字节1: [MASK(1)] [PAYLOAD_LEN(7)] +字节2-3: [扩展长度16位] (如果 PAYLOAD_LEN == 126) +字节2-9: [扩展长度64位] (如果 PAYLOAD_LEN == 127) +字节n: [掩码Key 4字节] (如果 MASK == 1) +字节n+4: [Payload Data] +``` + +### 2.3 帧头构建 + +```c +static size_t mkhdr(size_t len, int op, bool is_client, uint8_t *buf): + buf[0] = op | 128; // FIN=1, Opcode=op + + if (len < 126): + buf[1] = len, n = 2 // 7位直接编码 + else if (len < 65536): + buf[1] = 126, n = 4 // 16位扩展长度(大端) + else: + buf[1] = 127, n = 10 // 64位扩展长度(大端) + + if (is_client): + buf[1] |= 0x80 // 设置MASK位 + mg_random(&buf[n], 4) // 随机掩码key + n += 4 + + return n +``` + +**关键**: 服务端发送的帧不设 MASK 位。只有客户端发往服务端的帧才需要掩码(防缓存投毒攻击)。 + +### 2.4 帧解析 + +```c +static size_t ws_process(uint8_t *buf, size_t len, struct ws_msg *msg): + → 解析字节0: msg->flags + → 解析字节1: payload_len (低7位) + mask_len (MASK位→4或0) + → 根据 payload_len 读取扩展长度 + → 安全检查: data_len 不能超过 1GB + → 如果有掩码: payload[i] ^= masking_key[i % 4] // XOR解码 + → 返回 header_len + data_len +``` + +## 3. 握手流程 + +### 3.1 HTTP Upgrade + +``` +客户端 → 服务端: + GET /ws HTTP/1.1 + Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== + Sec-WebSocket-Version: 13 + +服务端 → 客户端: + HTTP/1.1 101 Switching Protocols + Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= +``` + +### 3.2 Accept 值计算 + +``` +1. SHA1( Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" ) +2. Base64 编码 SHA1 结果 +3. 魔数是 RFC 6455 §4.2.2 固定值,防跨协议攻击 +``` + +### 3.3 Mongoose API + +```c +// 服务端升级 +void mg_ws_upgrade(struct mg_connection *c, struct mg_http_message *hm, const char *fmt, ...); +// 内部: 计算Accept → 发送101 → 切换c->pfn = mg_ws_cb → c->is_websocket = 1 + +// 客户端连接 +struct mg_connection *mg_ws_connect(struct mg_mgr *mgr, const char *url, mg_event_handler_t fn, void *fn_data, const char *fmt, ...); +``` + +## 4. 协议处理器 `mg_ws_cb` + +WebSocket 连接的核心处理器,在握手完成后注册为 `c->pfn`: + +``` +mg_ws_cb (MG_EV_READ 处理): + +解析帧 → opcode 分类: + TEXT/BINARY + FIN=1 (完整帧): + → 触发 MG_EV_WS_MSG(用户在此接收消息) + + TEXT/BINARY + FIN=0 (分片开始): + → 保留flags,累积数据,等待后续帧 + + CONTINUE: + → 剥离帧头追加到累积缓冲区 + → FIN=1: 触发 MG_EV_WS_MSG,从缓冲区首字节恢复原始opcode + + CLOSE (0x8): + → 触发 MG_EV_WS_CTL + → 回显CLOSE帧给对端 + → c->is_draining = 1(优雅关闭) + + PING (0x9): + → 自动回复PONG + → 触发 MG_EV_WS_CTL + + PONG (0xA): + → 触发 MG_EV_WS_CTL(用户可检测心跳响应) +``` + +### 4.1 分片帧处理 + +``` +客户端发送 3 条分片: + Frame1: FIN=0, op=TEXT, data="Hello " + Frame2: FIN=0, op=CONTINUE, data="World" + Frame3: FIN=1, op=CONTINUE, data="!" + +内部处理: + Frame1: 在c->recv中保存flags(0x01) + 数据 → "\x01Hello " + Frame2: 剥离帧头追加 → "\x01Hello World" + Frame3: 剥离帧头追加 → "\x01Hello World!" → FIN=1触发 + → m.flags = c->recv.buf[0] (TEXT) + → m.data = "Hello World!" (跳过标记字节) + → 删除已处理数据 +``` + +## 5. 发送函数 + +```c +// 发送一条完整消息(构建帧头+掩码+发送) +size_t mg_ws_send(struct mg_connection *c, const void *buf, size_t len, int op); + +// 格式化发送(mg_vxprintf → mg_ws_wrap) +size_t mg_ws_printf(struct mg_connection *c, int op, const char *fmt, ...); + +// 为已在c->send中的数据添加WS帧头(内部) +size_t mg_ws_wrap(struct mg_connection *c, size_t len, int op); +``` + +## 6. 服务端最佳实践 + +### 6.1 标准事件处理模板 + +```c +static void fn(struct mg_connection *c, int ev, void *ev_data) { + // WebSocket 握手 + if (ev == MG_EV_HTTP_MSG) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + if (mg_http_get_header(hm, "Sec-WebSocket-Key")) { + mg_ws_upgrade(c, hm, NULL); + } else { + mg_http_reply(c, 200, "", "Use WebSocket\n"); + } + } + + // 连接建立 + if (ev == MG_EV_WS_OPEN) { + // 注入 per-connection 资源 + } + + // 接收消息 + if (ev == MG_EV_WS_MSG) { + struct mg_ws_message *wm = (struct mg_ws_message *) ev_data; + // 处理文本/二进制消息 + } + + // 连接关闭 + if (ev == MG_EV_CLOSE) { + // 释放 per-connection 资源 + } +} +``` + +### 6.2 广播 + +```c +void broadcast(struct mg_mgr *mgr, const char *msg, size_t len): + for each c in mgr->conns: + if c->is_websocket: + mg_ws_send(c, msg, len, WEBSOCKET_OP_TEXT) +``` + +### 6.3 心跳检测 + +```c +// 定时发送 PING(通过 mg_timer_add) +mg_ws_send(c, NULL, 0, WEBSOCKET_OP_PING); + +// 在 MG_EV_WS_CTL 中检测 PONG 确认存活 +``` + +## 7. 服务端 vs 客户端差异 + +| 特性 | 服务端 | 客户端 | +|------|--------|--------| +| 帧掩码 | 不掩码 | 必须掩码(4字节随机key + XOR) | +| 握手 | 接收Key,计算Accept | 生成随机Key,验证Accept | +| API | `mg_ws_upgrade()` | `mg_ws_connect()` | +| `is_client` | `false` | `true` | + +## 8. RTU 中的应用 + +RTU 的 `libweb_server` 模块通过 Mongoose WebSocket 实现: + +- **实时数据推送**: 客户端订阅信号 → `ws_task` 每 100ms 推送变更的 JSON 数据 +- **前端交互**: 内嵌 SPA 通过 WebSocket 接收遥测/遥信实时更新 +- **命令下发**: 前端通过 WebSocket 发送控制命令(遥控/定值修改) +- **多浏览器**: per-connection session 隔离,各浏览器独立订阅 + +``` +浏览器 ──WebSocket──→ Mongoose (mg_mgr_poll, 10ms) + ↓ MG_EV_WS_MSG + ws_recv() → 命令分发 + ↓ + datacenter API + ↓ + ws_task() → 增量 JSON 推送 (100ms) + ↓ mg_ws_send + 浏览器更新 +``` diff --git a/claude/问题处理文档.md b/mimo/问题处理文档.md similarity index 100% rename from claude/问题处理文档.md rename to mimo/问题处理文档.md