一、前言:为什么是104?

        在电力系统的神经网络中,调度主站与变电站子站之间的通信是整个电网安全运行的命脉。IEC 60870-5-104(以下简称"104协议")是IEC60870-5-101(串口远动规约)在TCP/IP网络上的延伸版本,它继承了101协议完整的应用层语义,同时借助以太网实现了更远的传输距离和更高的可靠性。

        本文以 iec_104_reporter目录下的一套真实工业EMS(储能能量管理系统)C语言实现为蓝本,从帧结构解析到连接管理、从遥信遥测到遥控对时,逐层拆解104协议的工程细节,并在文末探讨如何用AI对104数据流做实时电网异常检测。

        
二、104协议帧结构:三类帧的完整解析

2.1 APDU总体结构

        104协议的传输单元称为 APDU(Application Protocol Data Unit,应用规约数据单元),其最大长度为255字节。APDU由两部分组成:

  ┌───────────────────────────────────────────────────────────
  │  APCI(应用规约控制信息,6字节固定头)                         │
  │  ASDU(应用服务数据单元,可变长度)                             │
  └───────────────────────────────────────────────────────────

  APCI的结构如下:

  字节0:0x68                  起始字符(固定)
  字节1:APDU长度        从字节2开始到APDU结束的字节数
  字节2-5:控制域            决定帧类型(I/S/U)

        在代码的 rawMessageHandler 回调中,可以看到系统对每一帧的原始字节进行了十六进制打印与比较:

  // iec_104_handler.c - rawMessageHandler
  static void rawMessageHandler(void* parameter, IMasterConnection conneciton,
                                 uint8_t* msg, int msgSize, bool sent)
  {
      int i;
      for (i = 0; i < msgSize; i++)

      {
          nOffset += sprintf(szDump + nOffset, "%02X", msg[i]);
      }
      // 与上次接收数据对比,避免重复记录
      if(strncmp(&szDump[12], &pIEC104->szLastRecvData[12], (nOffset-12)) == 0)
          return;
      memcpy(pIEC104->szLastRecvData, szDump, nOffset);
      LOG_INFO("IEC_104 type=[%s], Data=\n[%s]", (sent == TRUE) ? "WRITE" : "READ", szDump);
  }

        注意第12字节偏移(跳过前6字节APCI头和6字节序号域)的比较逻辑:这是为了过滤S帧等纯控制帧的重复日志,因为S帧数据体完全相同但序号不同。

  2.2 I帧(Information Frame)——数据传输帧

        I帧用于携带ASDU进行实际数据传输,是三类帧中最复杂、信息量最大的一种。

  控制域结构(4字节):
  字节2: N(S) 低7位 + LSB=0(标识为I帧)
  字节3: N(S) 高8位
  字节4: N(R) 低7位 + LSB=0
  字节5: N(R) 高8位

        其中 N(S) 是发送序列号,N(R) 是接收序列号(同时也是对对端的确认)。两者范围均为 0~32767(15位),超过后归零循环。

  代码注释中保留了真实抓包的I帧示例:
  从站发送:
  68 16 92 01 00 00 0b 83 01 01 01 00 01 40 00 73 00 00 7e 00 00 89 00 80
  解析如下:
  - 68:起始字节
  - 16:APDU长度=22字节
  - 92 01 00 00:I帧,N(S)=0xC9=201,N(R)=0
  - 0b:TI=11(M_ME_NB_1,带品质描述的标度化测量值)
  - 83:VSQ=0x83,SQ=1(顺序地址),个数=3
  - 01 01:COT=0x0101,传送原因=1(周期/循环)

  2.3 S帧(Supervisory Frame)——监视帧

  S帧仅用于确认(ACK),不携带ASDU数据。

  控制域结构:
  字节2: 0x01(固定,标识为S帧)
  字节3: 0x00(固定)
  字节4: N(R) 低7位 + LSB=0
  字节5: N(R) 高8位

  典型的S帧如:68 04 01 00 d8 03(长度4,N(R)=0x01EC=492)。这意味着子站确认已收到主站发送序号 < 492 的所有I帧。

  S帧是104的"滑动窗口"机制的核心。协议参数 k(最大未确认I帧数)和 w(最大收到未确认I帧数)控制着发送节奏:

  // 代码中的APCI参数获取(iec_104_handler.c)
  CS104_APCIParameters apciParams = CS104_Slave_getConnectionParameters(slave);
  // 默认参数:t0=10s t1=15s t2=10s t3=20s k=18 w=12

  2.4 U帧(Unnumbered Frame)——无编号控制帧

        U帧用于连接控制,分为三对命令/确认:

  ┌─────────────┬─────────────────┬──────────────────┐
  │  命令类型                 │ 控制域(字节2)                │       用途                               │
  ├─────────────┼─────────────────┼──────────────────┤
  │ STARTDT ACT         │ 0x07                                  │ 启动数据传输激活                 │
  ├─────────────┼─────────────────┼──────────────────┤
  │ STARTDT CON        │ 0x0B                                  │ 启动数据传输确认                │
  ├─────────────┼─────────────────┼──────────────────┤
  │ STOPDT ACT          │ 0x13                                   │ 停止数据传输激活                 │
  ├─────────────┼─────────────────┼──────────────────┤
  │ STOPDT CON         │ 0x17                                   │ 停止数据传输确认                 │
  ├─────────────┼─────────────────┼──────────────────┤
  │ TESTFR ACT           │ 0x43                                   │ 测试帧激活                           │
  ├─────────────┼─────────────────┼──────────────────┤
  │ TESTFR CON          │ 0x83                                   │ 测试帧确认                           │
  └─────────────┴─────────────────┴──────────────────┘

  U帧格式简洁:68 04 [控制字节] 00 00 00,总长6字节,无序列号、无ASDU。

        
  三、连接管理:STARTDT/STOPDT/TESTFR握手机制

  3.1 连接状态机

  在 iec_104.h 中,工程师定义了清晰的连接状态枚举:

  typedef enum tagIEC104ConnStatus

{
      STAT_Connect_Opened  = 0,  // TCP连接已建立,但应用层未激活
      STAT_Connect_Closed,        // TCP连接关闭
      STAT_Activated,             // 应用层已激活(STARTDT握手完成)
      STAT_DeActivated,           // 应用层已停止
      STAT_DisConnected,          // 完全断开
  } IEC104_CONN_STATUS;

  对应的状态转换处理在 connectionEventHandler 中实现:

  // iec_104_handler.c
  void connectionEventHandler(void* parameter, IMasterConnection con,
                               CS104_PeerConnectionEvent event)
  {
      IEC104_DATA_ROUGH* pIEC104 = (IEC104_DATA_ROUGH*)parameter;

      if (event == CS104_CON_EVENT_CONNECTION_OPENED) {
          pIEC104->em104ConnStatus = STAT_Connect_Opened;
      } else if (event == CS104_CON_EVENT_CONNECTION_CLOSED) {
          pIEC104->em104ConnStatus = STAT_Connect_Closed;
      } else if (event == CS104_CON_EVENT_ACTIVATED) {
          pIEC104->em104ConnStatus = STAT_Activated;
          pIEC104->bTrigger = TRUE;  // ★ 激活后立即触发全数据上送
      } else if (event == CS104_CON_EVENT_DEACTIVATED) {
          pIEC104->em104ConnStatus = STAT_DeActivated;
      }
  }

        STAT_Activated 时设置的 bTrigger = TRUE 是关键:它触发主线程立即将所有遥信/遥测数据推送给新激活的主站连接,相当于完成了"连接初始化上送"。

  3.2 STARTDT握手完整时序

  主站(Client)                              子站(Server/Slave)
      |                                                             |
      |-------- TCP SYN ----------------------->    |
      |<------- TCP SYN-ACK --------------------|
      |-------- TCP ACK ----------------------->     |  TCP连接建立
      |                                                             |
      |--- 68 04 07 00 00 00 (STARTDT ACT) ---->|  启动数据传输
      |<-- 68 04 0B 00 00 00 (STARTDT CON) -----|  确认启动
      |                                                                      |  ← STAT_Activated
      |  开始正常I帧数据交换                                   |
      |<-- 68 xx [I帧 遥信/遥测数据] ------------          |
      |--- 68 04 01 00 xx xx (S帧 确认) ------->        |
      |                                                                      |
      |--- 68 xx 67 01 06 00 [时钟同步] ------->        |  对时
      |<-- 68 xx 67 01 07 00 [时钟确认] --------        |
      |                                                                      |
      |--- 68 04 43 00 00 00 (TESTFR ACT) ----->  |  心跳测试
      |<-- 68 04 83 00 00 00 (TESTFR CON) ------ |  心跳确认

  3.3 服务器初始化与线程架构

  // iec_104_handler.c - _ThreadEntry_IEC104Comm
  static DWORD _ThreadEntry_IEC104Comm(void* pArg)
  {
      CS104_Slave slave = CS104_Slave_create(50, 50); // 低优先级队列50, 高优先级队列50
      CS104_Slave_setLocalAddress(slave, "0.0.0.0");   // 监听所有网卡

      // 支持多主站并发连接(每个连接独立事件队列)
      CS104_Slave_setServerMode(slave, CS104_MODE_CONNECTION_IS_REDUNDANCY_GROUP);
      CS104_Slave_setMaxOpenConnections(slave, 5);  // 最多5个主站同时连接

      // 注册回调
      CS104_Slave_setClockSyncHandler(slave, clockSyncHandler, pIEC104);
      CS104_Slave_setInterrogationHandler(slave, interrogationHandler, pIEC104);
      CS104_Slave_setASDUHandler(slave, asduHandler, pIEC104);
      CS104_Slave_setConnectionRequestHandler(slave, connectionRequestHandler, pIEC104);
      CS104_Slave_setConnectionEventHandler(slave, connectionEventHandler, pIEC104);
      CS104_Slave_setRawMessageHandler(slave, rawMessageHandler, pIEC104);

      CS104_Slave_start(slave);  // 启动后台监听线程

      while (!pReporter->bExit) {
          Handle_IEC104CommStatus(pIEC104);  // 监控连接状态变化

          if (!Wait_IEC104_CommReady(pReporter)) {
              Sleep(1000);
              continue;  // 未激活则等待
          }

          Refresh_YC_YX_SigMapValue(pIEC104);       // 刷新实时数据
          Handle_YC_YX_EnqueueASDU(pIEC104, slave, alParams);  // 定时上送
          Sleep(2000);  // 2秒轮询周期
      }

      CS104_Slave_stop(slave);
      CS104_Slave_destroy(slave);
  }

        这里采用了 CS104_MODE_CONNECTION_IS_REDUNDANCY_GROUP 模式(2026年3月18日由George更新),支持多个主站并发连接,每个连接有独立的事件队列,适合多个调度系统同时接入同一EMS子站的场景。


  四、遥信、遥测、遥控全类型深度解析

  4.1 遥信(YX)——状态量上送

        遥信(Teleinformation)反映开关、告警等二值状态,对应104协议的 M_SP_NA_1(TI=1,单点遥信)。

信号点映射表设计:

  // iec_104.h - 信号映射结构
  typedef struct tagIEC104sigMap {
      UINT    nSigIdx;    // 内部信号索引
      UINT    nIOA;       // 信息对象地址(IEC104点号)
      QualityDescriptorP emQuality;  // 品质描述(IV/NT/SB/BL/OV)
      OBJVALUE_DATATYPE  emDataType; // 数据类型
      UINT    nScale;     // 缩放因子
      void*   ptrValue;   // 直接指向内存中的实时值
  } IEC104_CONFIG_SIGMAP;

遥信数据上送代码:

  // iec_104_handler.c - Create_YX_EnqueueASDU
  static void Create_YX_EnqueueASDU(IEC104_DATA_ROUGH* pIEC104,
                                      CS104_Slave slave,
                                      CS101_AppLayerParameters alParams)
  {
      // 创建ASDU:TI=M_SP_NA_1,COT=1(周期循环),IOA起始地址=0x01
      CS101_ASDU newAsdu = CS101_ASDU_create(alParams, true,
          CS101_COT_PERIODIC, 0x01, 1, false, false);

      IEC104_CONFIG_SIGMAP* pYXItem;
      for (n = 0; n < pIEC104->nYXNum; n++) {
          pYXItem = pIEC104->pYXEle + n;
          int tempVal = *((int*)pYXItem->ptrValue);  // 直接读取内存中的BOOL值

          // 创建单点信息对象
          InformationObject io = (InformationObject)
              SinglePointInformation_create(NULL, pYXItem->nIOA,
                                            tempVal, pYXItem->emQuality);
          CS101_ASDU_addInformationObject(newAsdu, io);
          InformationObject_destroy(io);
      }
      CS104_Slave_enqueueASDU(slave, newAsdu);
      CS101_ASDU_destroy(newAsdu);
  }

        关键设计要点:ptrValue 是直接指向内存中实时变量的指针,实现了信号表与实时数据的"零拷贝"映射。当系统底层(BMS/PCS)更新变量时,104上送自动反映最新值,无需额外同步。

品质描述字段(Quality Descriptor)解析:

代码注释中的抓包数据揭示了品质描述的实际含义:
变位有效 IV=0  当前值 NT=0  未被取代 SB=0  未被封锁 BL=0 点号=0 分
变位有效 IV=0  当前值 NT=0  未被取代 SB=0  未被封锁 BL=0 点号=1 合
变位无效 IV=1  当前值 NT=0  未被取代 SB=0  未被封锁 BL=0 点号=2 分

IV=1 表示该点通信中断或传感器异常,调度系统应忽略此值——这正是代码中通信失败时直接 return 不上送的原因:

  static void Create_YCCabMeter_EnqueueASDU(...) {
      if(pIEC104->strYC_SysInfo.emCabMeter_CommStatu == 1) // 通信故障
          return;  // 通信失败时不上送,避免主站读到错误数据
      // ...
  }

BMS告警遥信的分层设计:

储能系统的告警点多达数百个,代码采用分层结构体映射:

  // iec_104.h - BMS告警Part1,共127个布尔点
  typedef struct strYXBmsAlmPart1SigMap {
      BOOL bBmsNotPowerOn;        // BMS未上电
      BOOL bAL_Over_Ucell;        // 单体过压告警
      BOOL bAL_Under_Ucell;       // 单体欠压告警
      BOOL bAL_Over_Tcell;        // 单体过温告警
      BOOL bFireAlm;              // 火灾告警
      BOOL bFire_Lv1_Alm;         // 火灾一级告警
      // ... 共127个告警点
  } STR_YX_BMS_ALM_PART1_SIGMAP;

  按电池组分组上送,IOA地址分配为:
  - 第1组 BMS Part1:起始 0x1001
  - 第2组 BMS Part1:起始 0x1101(偏移 0x100)
  - 第1组 BMS Part2:起始 0x1081

4.2 遥测(YC)——模拟量上送

  遥测(Telemetry)上送模拟量测量值,本工程使用了两种类型:

4.2.1 M_ME_NB_1(TI=11)——归一化/标度化测量值

用于EMS调度参数等整数类型遥测:

  // 创建标度化测量值(整数型,有缩放因子)
  InformationObject io = (InformationObject)
      MeasuredValueScaled_create(NULL,
          pYCItem_Ems->nIOA,              // 信息对象地址
          tempVal * (int)pYCItem_Ems->nScale,  // 值 × 缩放因子
          pYCItem_Ems->emQuality);        // 品质描述

  // 对应ASDU示例(实际抓包):
  // TI=11 M_ME_NB_1 点号=16385 OV=0 未溢出 值=115
  // 点号=16386 值=126,点号=16387 IV=1(无效)值=137

缩放因子 nScale 实现了工程量与协议值的转换:例如SOC百分比×100传输,主站收到后÷100还原。

  4.2.2 M_ME_NC_1(TI=13)——短浮点数测量值

用于电表、BMS等需要小数精度的遥测:

  // 创建短浮点测量值(IEEE 754单精度浮点)
  InformationObject io = (InformationObject)
      MeasuredValueShort_create(NULL,
          pYCItem_CabMeter->nIOA,
          tempVal,          // float,保留小数精度
          pYCItem_CabMeter->emQuality);

遥测数据的完整IOA地址规划:

  4.2.3 电芯数据的分块传输策略

        240个电芯的单体数据(电压+温度)共480个浮点值,每个I帧最大只能容纳约48个信息体(受255字节APDU限制),因此代码实现了分块循环上送:

  // iec_104_handler.c - Create_YCCellGrp_EnqueueASDU
  // 注释:240个电芯的单体信息:温度+电压=480个,float型占4字节,
  // 信息体243/(4+1)=48,需10个APDU传送,需要循环创建
  static void Create_YCCellGrp_EnqueueASDU(...) {
      int i;
      for(i = 0; i < 10; i++) {         // 10个APDU批次
          Create_YCCellG1_EnqueueASDU(pIEC104, slave, alParams, i);
          Create_YCCellG2_EnqueueASDU(pIEC104, slave, alParams, i);
          Create_YCCellG3_EnqueueASDU(pIEC104, slave, alParams, i);
          Create_YCCellG4_EnqueueASDU(pIEC104, slave, alParams, i);
      }
  }

  // 每个批次:
  static void Create_YCCellG1_EnqueueASDU(..., int id) {
      int offset = id * 48;  // 每批48个点
      CS101_ASDU newAsdu = CS101_ASDU_create(...,
          0xA001 + offset, ...);  // IOA地址随批次偏移

      for (n = 0; (((n + offset) < pIEC104->nYCNum_CellG1) && (n < 48)); n++) {
          // 每批最多48个信息体
      }
  }

  4.3 遥控(YK)——控制命令

遥控是104协议中最敏感的功能,本工程实现了单点遥控(C_SC_NA_1,TI=45)。

遥控处理流程(在 asduHandler 中):

  bool asduHandler(void* parameter, IMasterConnection connection, CS101_ASDU asdu)
  {
      if (CS101_ASDU_getTypeID(asdu) == C_SC_NA_1) {  // 单点遥控
          if (CS101_ASDU_getCOT(asdu) == CS101_COT_ACTIVATION) {  // 激活命令
              InformationObject io = CS101_ASDU_getElement(asdu, 0);
              SingleCommand sc = (SingleCommand)io;
              BOOL bCtrlState = SingleCommand_getState(sc);  // 0=分, 1=合
              int nIOA = InformationObject_getObjectAddress(io);

              if (nIOA == 0x6001) {         // 系统锁定/解锁
                  CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON);
                  On_Hander_Lock(pIEC104, bCtrlState);
              } else if (nIOA == 0x6002) {  // 系统复位
                  CS101_ASDU_setCOT(asdu, CS101_COT_ACTIVATION_CON);
                  On_Hander_Reset(pIEC104, bCtrlState);
              } else {
                  CS101_ASDU_setCOT(asdu, CS101_COT_UNKNOWN_IOA);  // 未知点号
              }
              IMasterConnection_sendASDU(connection, asdu);  // 回复确认
          }
      }
      else if (CS101_ASDU_getTypeID(asdu) == C_SE_NB_1) {  // 标度化设定命令
          // IOA 0x6101-0x6105:上报间隔参数设定
          // IOA 0x6201-0x623D:EMS调度计划设定(12个时段×5参数)
      }
      else if (CS101_ASDU_getTypeID(asdu) == C_SE_NC_1) {  // 短浮点设定命令
          // IOA 0x6191/0x6391:REMS下发充放电功率指令(+充-放/+放-充两种约定)
      }
  }

  关键的遥控响应流程:

  主站(Client)                                                                子站(Server)
      |                                                                                    |
      |-- C_SC_NA_1, COT=6(ACT), IOA=0x6001, S=1 --> |  发送遥控命令
      |                                                                                     |  ← 执行控制操作
      |<- C_SC_NA_1, COT=7(ACT_CON), IOA=0x6001 ----|  返回执行确认
      |                                                                                      |

        注意:本工程没有实现Select-Execute两步遥控(先选择后执行),而是直接执行单步遥控。这在储能EMS领域较为常见——调度主站下发充放电功率指令,EMS直接执行,然后主动上报当前状态。真实变电站的一次设备控制(断路器分合)才必须
        实现Select-Execute以防止误操作。


  五、CP56Time2a精确对时设计

  5.1 时间格式解析

  CP56Time2a是104协议使用的7字节时间戳格式,精度达到毫秒级:

  字节0-1:毫秒(0~59999,包含秒值)
  字节2:  分钟(0~59)+ IV位(时间有效性)+ RES1
  字节3:  小时(0~23)+ RES2 + SU(夏令时标志)
  字节4:  星期(1~7)+ 日(1~31)
  字节5:  月(1~12)+ RES3
  字节6:  年(0~99,加2000为完整年份)

  代码中的时间解析与转换:

  // iec_104_handler.c - CP56Time2a_toSecTimestamp
  time_t CP56Time2a_toSecTimestamp(CP56Time2a time)
  {
      struct tm tmTime;
      tmTime.tm_year = CP56Time2a_getYear(time) + 100;  // +100因为tm_year是从1900开始
      tmTime.tm_mon  = CP56Time2a_getMonth(time) - 1;   // tm_mon是0-11
      tmTime.tm_mday = CP56Time2a_getDayOfMonth(time);
      tmTime.tm_hour = CP56Time2a_getHour(time);
      tmTime.tm_min  = CP56Time2a_getMinute(time);
      tmTime.tm_sec  = CP56Time2a_getSecond(time);       // 毫秒部分÷1000

      time_t timestamp = mktime(&tmTime);  // 转换为Unix时间戳
      return timestamp;
  }

  5.2 对时处理流程

  // iec_104_handler.c - clockSyncHandler
  bool clockSyncHandler(void* parameter, IMasterConnection connection,
                         CS101_ASDU asdu, CP56Time2a newTime)
  {
      time_t newSystemTimeInSec = CP56Time2a_toSecTimestamp(newTime);

      // 用当前系统时间更新ACT_CON响应中的时间戳
      CP56Time2a_setFromMsTimestamp(newTime, Hal_getTimeInMs());

      // 仅当时间差超过5秒时才执行对时(防止频繁调整)
      if (pIEC104->lcCfg.lcType == LC_TYPE_1st)  // 一级控制器才执行系统对时
      {
          if (ABS(newSystemTimeInSec - _NOW_) > 5)
          {
              VAR_VALUE varV;
              varV.dtValue = newSystemTimeInSec;
              pReporter->pDAI->Set((HANDLE)pReporter, DAITYPE_SETT_PARAMVALUE_RW,
                                   1, 1, &varV, 4, 0);  // 写入系统时钟
          }
      }
      return true;  // 返回true触发库自动发送ACT_CON
  }

  工程细节:
  - LC_TYPE_1st 判断:在主从式多机架构中,只有主控器(1st级)执行系统对时,避免从机互相干扰
  - 5秒阈值:小于5秒的偏差忽略,防止频繁写入RTC造成系统抖动
  - 对时报文示例(来自代码注释):
  主站发送时钟同步(COT=6激活):
  68 14 00 00 18 00 67 01 06 00 01 00 00 00 00 70 d0 1f 10 04 06 17
  = TI=103(C_CS_NA_1) 360ms 53秒 31分 16时 4日 6月 2023年

  子站响应(COT=7激活确认):
  68 14 18 00 02 00 67 01 07 00 01 00 00 00 00 33 81 1f 10 04 06 17
  = TI=103 75ms 33秒 31分 16时 4日 6月 2023年(响应时刻)

  5.3 总召(Station Interrogation)中的时间戳规则

        值得注意的是,代码遵循了104协议的严格规定:总召(GI)响应的ASDU不允许携带时间戳。代码中总召响应一律使用 CS101_COT_INTERROGATED_BY_STATION(COT=20),且创建ASDU时 isSequence=true 以顺序地址方式减少帧数。


  六、主站-子站断线重连的工程实现

  6.1 断线检测机制

  // iec_104_handler.c - Handle_IEC104CommStatus
  static void Handle_IEC104CommStatus(IEC104_DATA_ROUGH* pIEC104)
  {
      static IEC104_CONN_STATUS emLastCommStatu = STAT_DisConnected;

      if(pIEC104->em104ConnStatus != emLastCommStatu) {
          // 检测到从激活状态变为非激活状态
          if (emLastCommStatu == STAT_Activated &&
              pIEC104->em104ConnStatus != STAT_Activated)
          {
              pIEC104->tmLastDisconnect = _NOW_;  // 记录断线时刻
              pIEC104->bDisconTrigger = TRUE;     // 设置断线触发标志
          }
          emLastCommStatu = pIEC104->em104ConnStatus;

          // 向上层报告通信状态
          VAR_VALUE varV;
          varV.emValue = (pIEC104->em104ConnStatus == STAT_Activated) ? 0 : 1;
          pReporter->pDAI->Set(pReporter, DAITYPE_VIRTUAL_PARAMVALUE_W, 1, 5, &varV, 4, 100);
      }
  }

  6.2 断线后的优雅降级

        断线后,REMS(远端能量管理系统)控制权需要自动回退到本地控制,以确保储能系统安全运行:

  // iec_104_handler.c - _ThreadEntry_IEC104Comm主循环
  while (!pReporter->bExit) {
      Handle_IEC104CommStatus(pIEC104);

      if (pIEC104->enableREMSCtrl &&
          pIEC104->bDisconTrigger &&           // 曾经断过线
          pIEC104->em104ConnStatus != STAT_Activated)  // 当前仍是断线
      {
          // 断线超过 nTDisconDelayIOA6191 秒后,清零控制指令并切回本地控制
          if (abs(_NOW_ - pIEC104->tmLastDisconnect) > pIEC104->nTDisconDelayIOA6191)
          {
              Handle_ParamStat(pIEC104, 0x6191, 0);  // 清零功率指令

              if (pIEC104->emEMSCtrlMode_REMS == EMSCtrlMode_Remote) {
                  // 强制切换回本地控制模式
                  varV.emValue = EMSCtrlMode_Local;
                  pReporter->pDAI->Set(pReporter, DAITYPE_VIRTUAL_PARAMVALUE_W,
                                       1001, 3902, &varV, 4, 100);
                  pIEC104->emEMSCtrlMode_REMS = EMSCtrlMode_Local;
              }
              pIEC104->bDisconTrigger = FALSE;
          }
      }
      // ...
  }

  断线重连时序:

  REMS主站                                                 EMS子站
      |                                                                    |
      | ×× TCP断开(网络故障/主站重启)××      |
      |                                                                    | bDisconTrigger=TRUE
      |                                                                    | tmLastDisconnect=now
      |  (等待 nTDisconDelayIOA6191 秒)       |
      |                                                                    | 功率指令清零
      |                                                                    | 切回本地控制(安全状态)
      |                                                                    |
      |-------- TCP重连 ----------------------->            |
      |--- STARTDT ACT ------------------------->      |
      |<-- STARTDT CON --------------------------     |  重新激活
      |                                                                    | bTrigger=TRUE(触发全量上送)
      |<-- 全量遥信/遥测推送 ------------------          |
      |                                                                    |
      |--- 重新下发 C_SE_NC_1 控制指令 -------> |  重建控制

        延时时间 nTDisconDelayIOA6191 可通过IOA 0x6191 动态配置,对应代码中的参数存储:1001, 3300(设备ID=1001, 参数SubID=3300)。


  七、AI结合:基于104数据流的电网异常实时检测

  7.1 数据特征工程

        104协议数据流天然具备时序性和多维性,非常适合机器学习异常检测。从本工程的数据结构可以提取以下特征维度:

时序特征:
  - 遥测采样频率(本工程BMS每2~5秒一帧,电表每2~5秒一帧)
  - 品质描述变化(IV位突变往往预示传感器故障)
  - 连接状态事件时间分布(断连频率异常)

  物理约束特征:
  # 储能系统物理约束检测示例
  def detect_physical_violation(bms_data):
      violations = []

      # 约束1:SOC超限(0~100%)
      if not (0 <= bms_data['soc'] <= 100):
          violations.append(('SOC_OUT_OF_RANGE', bms_data['soc']))

      # 约束2:功率守恒(PCS输出功率不能超过电池允许功率)
      if bms_data['pcs_output'] > bms_data['max_allow_chg_pwr'] * 1.1:
          violations.append(('POWER_OVERCHARGE', bms_data['pcs_output']))

      # 约束3:单体电压一致性(极差超阈值=电芯内阻失衡)
      cell_diff = bms_data['max_cell_volt'] - bms_data['min_cell_volt']
      if cell_diff > 0.05:  # 50mV阈值
          violations.append(('CELL_IMBALANCE', cell_diff))

      return violations

  7.2 基于Isolation Forest的无监督异常检测

  import numpy as np
  from sklearn.ensemble import IsolationForest
  from collections import deque

  class IEC104AnomalyDetector:
      """
      基于104实时数据流的储能系统异常检测器
      对应iec_104_reporter中上送的遥测数据
      """

      def __init__(self, window_size=300, contamination=0.01):
          self.window_size = window_size  # 300个采样点=约600秒历史
          self.model = IsolationForest(
              n_estimators=100,
              contamination=contamination,  # 预期异常比例1%
              random_state=42
          )
          self.buffer = deque(maxlen=window_size)
          self.is_trained = False

      def extract_features(self, iec104_frame):
          """
          从104上送的遥测数据提取特征向量
          对应iec_104.h中的STR_YC_PARAM_BMS/STR_YC_PARAM_METER结构
          """
          features = [
              # BMS特征(来自IOA 0x4301 电池组数据)
              iec104_frame['bms']['soc'],                    # 荷电状态
              iec104_frame['bms']['rack_volt'],              # 总压
              iec104_frame['bms']['rack_curr'],              # 总流
              iec104_frame['bms']['max_cell_temp'],          # 最高单体温度
              iec104_frame['bms']['max_cell_volt'],          # 最高单体电压
              iec104_frame['bms']['min_cell_volt'],          # 最低单体电压
              # 温差和电压差(衍生特征)
              iec104_frame['bms']['max_cell_temp'] - iec104_frame['bms']['min_cell_temp'],
              iec104_frame['bms']['max_cell_volt'] - iec104_frame['bms']['min_cell_volt'],

              # 电表特征(来自IOA 0x4101/0x4201)
              iec104_frame['meter']['active_power'],         # 有功功率
              iec104_frame['meter']['reactive_power'],        # 无功功率
              iec104_frame['meter']['freq'],                 # 电网频率
              iec104_frame['meter']['power_factor'],         # 功率因数

              # 通信质量特征
              iec104_frame['quality_invalid_count'],          # 本帧无效点数量
              iec104_frame['frame_interval_ms'],             # 帧间隔(正常应稳定)
          ]
          return np.array(features)

      def update(self, iec104_frame):
          features = self.extract_features(iec104_frame)
          self.buffer.append(features)

          # 收集足够数据后训练模型
          if len(self.buffer) >= self.window_size and not self.is_trained:
              X = np.array(list(self.buffer))
              self.model.fit(X)
              self.is_trained = True
              print("Anomaly detector trained on historical data")

          # 在线推理
          if self.is_trained:
              score = self.model.decision_function([features])[0]
              is_anomaly = self.model.predict([features])[0] == -1

              if is_anomaly:
                  self.trigger_alert(features, score, iec104_frame)
              return is_anomaly, score

          return False, 0.0

      def trigger_alert(self, features, score, raw_frame):
          """
          异常触发:可对接告警系统或反向通过104协议通知主站
          """
          alert = {
              'timestamp': raw_frame['timestamp'],
              'anomaly_score': score,
              'raw_soc': features[0],
              'cell_volt_diff': features[7],   # 电芯电压差
              'power_imbalance': abs(features[8]),  # 功率异常
          }
          print(f"[ALERT] Anomaly detected at {alert['timestamp']}: "
                f"score={score:.3f}, SOC={features[0]:.1f}%, "
                f"cell_diff={features[7]*1000:.1f}mV")
          # 可在此处调用 pReporter->pDAI->Set 触发告警遥信上送

  7.3 基于LSTM的时序预测异常检测

        对于SOC和温度等具有强时序规律的量,LSTM预测偏差检测效果更好:

  import torch
  import torch.nn as nn

  class SOC_Predictor(nn.Module):
      """
      基于LSTM的SOC预测模型
      若实测值与预测值偏差超过阈值,触发异常
      """
      def __init__(self, input_size=8, hidden_size=64, num_layers=2):
          super().__init__()
          self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True)
          self.fc = nn.Linear(hidden_size, 1)

      def forward(self, x):
          out, _ = self.lstm(x)
          return self.fc(out[:, -1, :])

  class PredictiveAnomalyDetector:
      """
      预测偏差型异常检测:
      利用历史数据预测下一时

● 刻的SOC值,若实测与预测偏差过大则报警
      """
      def init(self, threshold=3.0):
          self.model = SOC_Predictor()
          self.threshold = threshold  # 偏差阈值(%SOC)
          self.seq_len = 30           # 使用过去30个采样点(约60秒)预测

  def detect(self, soc_sequence):
      # soc_sequence: shape (seq_len, features)
      x = torch.FloatTensor(soc_sequence).unsqueeze(0)
      with torch.no_grad():
          predicted_soc = self.model(x).item()

      actual_soc = soc_sequence[-1][0]  # 最新实测SOC
      deviation = abs(actual_soc - predicted_soc)

      if deviation > self.threshold:
          return True, deviation, predicted_soc
      return False, deviation, predicted_soc

 7.4 三类典型异常的104数据特征

  结合本工程实际上送的数据,总结三种高频电网/储能异常:

        异常类型1:电芯失衡(Cell Imbalance)

  在 `STR_YC_PARAM_BMS` 结构和 IOA `0xA001`~`0xA5A1` 的电芯单体数据中可观测到:

  检测规则:
    max_cell_volt - min_cell_volt > 50mV(正常运行期间)
    → 预警电芯失衡,需要均衡充电

  104数据来源:
    IOA 0x4301+12: fMax_CellVolt(最高单体电压)
    IOA 0x4301+13: fMin_CellVolt(最低单体电压)
    IOA 0xA001起:  各电芯实际电压(分10批APDU上送)

异常类型2:并网点功率越限(Grid Power Violation)**

  检测规则:
    abs(meter_active_power) > sys_max_power * 1.05
    → 系统实发功率超出合同限值5%

  104数据来源:
    IOA 0x4201+6: fActive_Power(并网点有功功率)
    IOA 0x4051+3: nActvPwrLmt(系统功率限值,由REMS通过0x6101下发)

异常类型3:通信链路质量劣化(Link Quality Degradation)**

  ```python
  def detect_link_degradation(frame_history, window=60):
      """
      统计窗口内品质描述无效帧(IV=1)占比
      对应 rawMessageHandler 中记录的帧质量信息
      """
      recent = frame_history[-window:]
      invalid_ratio = sum(1 for f in recent if f['has_invalid_quality']) / len(recent)

      # 连接状态异常频率
      disconnect_count = sum(1 for f in recent if f['conn_event'] == 'CLOSED')

      if invalid_ratio > 0.1:  # 10%帧含无效品质描述
          return 'LINK_DEGRADED', invalid_ratio
      if disconnect_count > 3:  # 1分钟内断连超3次
          return 'LINK_UNSTABLE', disconnect_count
      return 'OK', 0

  7.5 AI检测结果的反向推送

        检测到异常后,可利用现有104基础设施反向通知主站——在 connectionEventHandler 感知到主站在线后,构造自发性ASDU(COT=3,突发上送):

  // 新增:AI告警结果上送
  static void Push_AIAlarm_ASDU(IEC104_DATA_ROUGH* pIEC104,
                                 CS104_Slave slave,
                                 CS101_AppLayerParameters alParams,
                                 int nAlarmIOA, float fScore)
  {
      // 使用 CS101_COT_SPONTANEOUS(COT=3,自发/突发)
      CS101_ASDU newAsdu = CS101_ASDU_create(alParams, false,
          CS101_COT_SPONTANEOUS, 0x7001, 1, false, false);

      // 将AI异常评分作为浮点遥测上送给主站
      InformationObject io = (InformationObject)
          MeasuredValueShort_create(NULL, nAlarmIOA, fScore,
                                     IEC60870_QUALITY_GOOD);
      CS101_ASDU_addInformationObject(newAsdu, io);
      InformationObject_destroy(io);

      CS104_Slave_enqueueASDU(slave, newAsdu);
      CS101_ASDU_destroy(newAsdu);
  }

        
  八、工程级性能优化与注意事项

  8.1 总召期间的数据保护

        代码中有一个关键的并发保护设计,避免总召期间的周期数据与总召响应数据交织:

  // iec_104_handler.c - Handle_YC_YX_EnqueueASDU
  static void Handle_YC_YX_EnqueueASDU(...) {
      // 总召期间,停止周期上送
      if(pIEC104->bOn_Interrogating == TRUE) {
          TRACE("Warning:--- On_Interrogating ---\n");
          return;
      }
      // ...周期数据处理
  }

  // interrogationHandler中:
  bool interrogationHandler(...) {
      pIEC104->bOn_Interrogating = TRUE;   // 上锁
      // ...发送所有总召响应数据...
      IMasterConnection_sendACT_TERM(connection, asdu);
      pIEC104->bOn_Interrogating = FALSE;  // 解锁
  }

  8.2 差异化上送周期

  不同数据源的变化频率不同,工程中实现了分级上送策略:

  // 系统状态:1秒上送(最快,反映实时状态)
  if (ABS(tmNow - pIEC104->tmLastSent_Sys) >= 1)
      Create_YCSysInfo_EnqueueASDU(...);

  // 电表数据:max(2, nCmd_107_Interval)秒上送
  if (ABS(tmNow - pIEC104->tmLastSent_Meter) >= MAX(2, nCmd_107_Interval))
      Create_YCCabMeter_EnqueueASDU(...);

  // BMS数据:max(2, nCmd_103_Interval)秒上送
  if (ABS(tmNow - pIEC104->tmLastSent_BMS) >= MAX(2, nCmd_103_Interval))
      Create_YCBatG1_EnqueueASDU(...);

  // 电量统计:max(10, nCmd_204_Interval)秒上送(变化慢)
  if (ABS(tmNow - pIEC104->tmLastSent_KWh) >= MAX(10, nCmd_204_Interval))
      Create_YCKWh_EnqueueASDU(...);

  // 电芯单体:20秒上送(数据量大,不宜频繁)
  if (pIEC104->bNeedUploadCellInfo && ABS(tmNow - pIEC104->tmLastSent_Cell) >= 20)
      Create_YCCellGrp_EnqueueASDU(...);

  上送间隔还支持主站通过遥控命令动态调整(IOA 0x6102~`0x6105`),使主站可以根据网络状况和业务需要灵活控制子站的数据上报频率。

  8.3 bTrigger触发机制

  bTrigger 标志是系统中的"脏标记",触发条件包括:
  - 主站连接激活(STAT_Activated)
  - 配置参数变更(EVENT_TYPE_CFG_CHANGED)
  - EMS计划下发完成
  - REMS控制模式切换

        触发后,下一个2秒轮询周期立即上送全量数据(遥信+遥测),而不是等待各自的差异化周期到来——这保证了主站在任何时刻都能获得最新完整数据快照。

        
  九、总结

        本文以一套真实储能EMS的104协议C语言实现为主线,完整剖析了:

  1. 帧结构:I/S/U三类帧的字节级解析,以及从实际抓包数据中还原协议语义的方法
  2. 连接管理:STARTDT握手→激活上送→TESTFR心跳→断线降级的完整生命周期状态机
  3. 四遥实现:
    - 遥信:单点信息(M_SP_NA_1)的指针映射与品质描述管理
    - 遥测:标度化值(M_ME_NB_1)与短浮点(M_ME_NC_1)双模式,及电芯数据分块策略
    - 遥控:C_SC_NA_1单步遥控与C_SE_NC_1浮点设定的IOA分段处理
  4. 对时:CP56Time2a 7字节毫秒级时标的解析与系统时钟同步
  5. 断线重连:延时降级策略保障储能系统在主站失联时的本地安全运行
  6. AI融合:Isolation Forest无监督检测与LSTM预测偏差两种范式,及三类典型异常的特征工程方案

        104协议在技术细节上并不"时髦",却在每一个电网边缘节点默默守护着数据的准确传递。真正的工程价值,往往就藏在这些看似朴素的字节序列和状态机跳转之中。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐