光伏逆变器多协议接入——Modbus RTU 在新能源设备集成中的实践

0 阅读18分钟

光伏逆变器多协议接入——Modbus RTU 在新能源设备集成中的实践

▎  前言:一个不得不解决的碎片化问题

做储能能量管理系统(EMS)的人,绕不开一个现实:光伏侧的逆变器品牌五花八门。一个工商业储能项目里,业主可能已经部署了古瑞瓦特(Growatt)的组串机,下一期又采购了阳光电源(Sungrow),隔壁厂房的屋顶是锦浪(Ginlong/Solis),园区总配那台大机器是华为 FusionSolar。四个品牌,四套 Modbus 寄存器地址,四种状态字编码,四种功率控制命令语义——你必须把它们统一接进来,让 EMS看到的是一个抽象的"光伏出力值",而不是品牌的乱麻。

这篇文章是我在开发项目中,主要负责光伏采样模块时的实践总结。核心入口是 pv_inverter_main.c,四个品牌的具体实现分别在pv_inverter_growatt.cpv_inverter_huawei.cpv_inverter_ginlong.cpv_inverter_sungrow.c,Modbus RTU 的底层收发封装在 pv_modbus_rtu.c 里。


一、架构决策:为什么不用现成的 libmodbus?

项目里确实引入了 libmodbus(include/3rd_party/modbus/),但光伏采样部分并没有使用它,而是自己实现了一套精简的 Modbus RTU 收发层(pv_modbus_rtu.c)。原因很直接:

libmodbus 的上下文模型是阻塞的。它在建立 modbus_t 上下文时绑定了一个串口,后续所有操作都是同步阻塞的。而我们的场景是:同一个物理串口上挂了多台逆变器(RS485多机),同一时刻只能有一台设备在通信,必须轮询且严格串行。与此同时,我们还有多个串口(RS485_1、RS485_2,加上最多 4 路串口服务器 TCP 透传),需要并发采样。

这种"串口内串行、串口间并发"的需求,配合 libmodbus 的 API 很别扭。自己实现一套薄薄的 RTU 层,反而更好控制:帧打包、CRC 计算、收发超时、多次读取拼包——全部在 pv_modbus_rtu.c 的约 500 行代码里搞定。


二、Modbus RTU 底层实现细节

2.1 帧结构与 CRC

标准 Modbus RTU 请求帧(读寄存器):

  [SlaveAddr 1B][FuncCode 1B][RegAddr_Hi 1B][RegAddr_Lo 1B][RegNum_Hi 1B][RegNum_Lo 1B][CRC_Lo 1B][CRC_Hi 1B]

  注意 CRC 是小端序——低字节在前,高字节在后。这是 Modbus RTU 规范的要求,也是新手最容易踩的坑。packModbusRtuDataToSend() 里这样处理:

  USHORT usCRC = Modbus_CRC16(pSendBuf, 6);
  *pBuf++ = (BYTE)(usCRC & 0xFF);   // 低字节先
  *pBuf++ = (BYTE)(usCRC >> 8);     // 高字节后

  CRC16 采用标准 Modbus 多项式 0xA001(反射多项式),初始值 0xFFFF:

  USHORT Modbus_CRC16(const BYTE *data, USHORT len) {
      USHORT crc = 0xFFFF;
      while (len--) {
          crc ^= *data++;
          for (int i = 0; i < 8; i++) {
              if (crc & 0x0001)
                  crc = (crc >> 1) ^ 0xA001;
              else
                  crc >>= 1;
          }
      }
      return crc;
  }

2.2 分段接收与超时处理

RS485 串口通信中,由于波特率、串口驱动缓冲区的原因,一个完整的响应帧可能分多次 Read 才能收全。代码里用了一个简单但有效的策略:重试 3 次,每次间隔 20ms,累积拼包:

  int nDynamicPos = 0;
  BYTE byRecvBufDynamic[MAX_RECEIVE_BUF_LEN] = {0};
  for (n = 0; n < 3; n++) {
      Sleep(20);
      nReadBytes = pCOMPort->Read(hPort, byRecvBuf, MAX_RECEIVE_BUF_LEN);
      if (nReadBytes < iExpectRecvLen) {
          memcpy(byRecvBufDynamic + nDynamicPos, byRecvBuf, nReadBytes);
          nDynamicPos += nReadBytes;
      } else {
          memcpy(byRecvBufDynamic, byRecvBuf, nReadBytes);
          nDynamicPos = nReadBytes;
          break;
      }
  }

帧校验在 checkModbusRtuFrameValid() 里完成:从站地址、功能码、字节数、帧总长度、CRC 逐一验证,有任何一项不对就返回 FALSE,触发上层的通信失败计数。

2.3 写操作:FC06 与 FC16

功率控制下发用到两种写功能码:

  • FC06(Write Single Register):用于单寄存器写,如开关机命令、功率百分比设置。响应帧应与请求帧相同,用 memcmp 校验。
  • FC16(Write Multiple Registers):用于时间同步等需要一次写多个寄存器的场景,如 Growatt 的时间同步写 6 个连续寄存器(年月日时分秒)。

三、四大品牌寄存器地址差异对比

这是调试过程中最耗时的部分。各品牌的寄存器地址规划差异极大,必须逐一核对协议文档与实测数据。下表是从代码中提炼出的关键寄存器对比:

  ┌──────────────┬─────────────────────────────────────┬──────────────────────────────────────────────┬───────────────────────────────────────┬────────────────────────────────────┐
     数据字段                   Growatt                             Huawei FusionSolar                           锦浪 Ginlong                        阳光电源 Sungrow          
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   功能码(读)   FC04 + FC03                          FC03                                          FC04                                   FC04                               
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   额定功率       FC03: Reg 6~7(×0.1W)               FC03: Reg 30073(×1 KW)                      FC04: Reg 3109(×10W)                 FC04: Reg 5000(×0.1)             
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   工作状态       FC04: Reg 0(0=等待,1=运行,3=故障)  FC03: Reg 32089(状态字 0x0200~0x0202=运行)  FC04: Reg 3043(0x0003或0x0000=运行)                                    
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   输出有功功率   FC04: Reg 35~36(×0.1W)             FC03: Reg 32080(×1 KW)                      FC04: Reg 3004~3005(×1W)             FC04: Reg 5030~5031(×1W)         
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   线电压 AB      FC04: Reg 50(×0.1V)                FC03: Reg 32066(×0.1V)                      FC04: Reg 3033(×0.1V)                FC04: Reg 5018(×0.1V)            
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   相电流 A       FC04: Reg 39(×0.1A)                FC03: Reg 32072(×0.001A,4字节)             FC04: Reg 3036(×0.1A)                FC04: Reg 5021(×0.1A)            
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   电网频率       FC04: Reg 37(×0.01Hz)              FC03: Reg 32085(×0.01Hz)                    FC04: Reg 3042(×0.01Hz)              FC04: Reg 5035(×0.1Hz,精度更低) 
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   当日发电量     FC04: Reg 53~54(×0.1 kWh)          FC03: Reg 32114(×0.01 kWh)                  FC04: Reg 3014(×0.1 kWh)             FC04: Reg 5002(×0.1 kWh)         
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   总发电量       FC04: Reg 55~56(×0.1 kWh)          FC03: Reg 32106(×0.01 kWh)                  FC04: Reg 3008~3009                    FC04: Reg 5003~5004                
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   开关机控制     FC06: Reg 0(1=开,0=关)             独立命令                                      FC06: Reg 3006(0xBE=开,0xDE=关)                                        
  ├──────────────┼─────────────────────────────────────┼──────────────────────────────────────────────┼───────────────────────────────────────┼────────────────────────────────────┤
   功率限制控制   FC06: Reg 308(0~1000,千分比)      FC06: Reg 40119                               FC06: Reg 3051(0~10000,万分比)                                        
  └──────────────┴─────────────────────────────────────┴──────────────────────────────────────────────┴───────────────────────────────────────┴────────────────────────────────────┘

几个值得注意的细节:

  1. Sungrow 寄存器地址需要减 1。协议文档里写的地址与实际帧中的地址偏移 1,代码注释里明确标注了这一点:// 阳光电源协议所有寄存器地址在使用时需减1访问。这导致文档写着读 5000,实际请求帧里写的是4999,坑过不少人。

  2. 锦浪同样存在地址偏移。协议文档地址与帧中地址差 1,所以 modbusRtuReqInfo 里起始地址是 2999(对应文档的 3000),函数名也用了 _2999 来标注起始地址,而非文档地址。

  3. 华为的电流精度是 0.001A,其他品牌是 0.1A。华为相电流用了 4 字节(两个寄存器),精度更高,解析时要用 getFourBytes() 而非 getTwoBytes()。

  4. Growatt 的功率限制用千分比,锦浪用万分比。下发相同功率目标时,换算系数不同:Growatt 写 percent * 10(01000),锦浪写 power * 10000 / rated_power(010000)。

  5. 华为的状态字是枚举值,其他品牌是简单整数。华为状态 0x02000x0202 表示并网运行,0x03000x0308 表示各种关机状态,需要范围判断而非精确匹配。


四、RS485 多机挂接的电气规范

写软件的人经常忽略硬件层面的问题,但 RS485 多机组网是影响通信稳定性的根本因素。项目部署时遇到过几次通信不稳定的问题,最后都定位到电气施工问题上。

4.1 终端电阻

RS485 是差分信号传输,总线两端必须各接一个 120Ω的终端电阻(匹配电缆特性阻抗)。不接终端电阻,信号在总线末端反射,高速通信(115200bps)下会产生严重的码间干扰。工程上常见的错误是只接了一端,或者把终端电阻加在中间节点上。

典型配置:

  • 主机端(数据采集器/EMS 网关):120Ω
  • 总线末端最后一台设备:120Ω
  • 中间各节点:不接终端电阻,T 型分支越短越好(建议 < 1m)

4.2 偏置电阻(上下拉)

当总线空闲或没有设备在发送时,RS485 的 A、B 两线处于高阻态,差分电压不确定,接收端可能误判为数据。偏置电阻(Bias Resistor)的作用是在空闲时将 A 线拉高(上拉到 VCC)、B 线拉低(下拉到GND),保证差分电压满足 V_A - V_B > 200mV,使接收端稳定在逻辑高电平。

典型阻值:560Ω 上拉 + 560Ω 下拉(与 120Ω 终端电阻配合,等效负载约 100Ω,仍满足 RS485 规范的 54Ω 最小负载要求)。大多数工业 RS485 收发器内置了偏置电阻,可通过跳线启用。

4.3 最大节点数与电缆长度

RS485 理论上支持 32 个单位负载(Unit Load)。现代低功耗收发器通常是 1/8 UL 甚至 1/32 UL,因此实际可挂节点数远超 32。但工程上建议控制在 32 个节点以内,主要考虑:

  • 总线负载增加导致信号摆幅下降
  • 每个节点引入寄生电容,影响波特率上限
  • 电缆总长度(9600bps 下理论最大 1200m,115200bps 下建议不超过 200m)

代码中配置的波特率是 9600bps("9600,N,8,1"),这是光伏逆变器 Modbus 的业界主流配置,稳定性优先。对于长距离拉线或设备台数多的场景,不建议贸然提升到 115200bps(代码中也支持该选项,通过ENU_INVERTER_BAUD_RATE 枚举配置)。


五、多串口并发采样的线程模型

5.1 设计动机

最初版本的实现是单线程轮询:主线程按顺序挨个读所有逆变器,读完一台再读下一台。问题在于,不同逆变器挂在不同串口(RS485_1 和 RS485_2),串行化之后总采样周期 = ΣN台 × (每台读取时间)。当台数增多,整体采样延迟迅速超过 EMS 的要求(通常 ≤ 5s 完成一轮)。

重构后的方案:一个物理端口对应一个独立线程,不同端口的采样完全并发,同一端口内的多台设备严格串行(RS485 总线的物理约束决定了这一点)。

5.2 数据结构设计

PORT_THREAD_INFO 是每个端口线程的上下文:

  typedef struct tagPortThreadInfo {
      PHY_INTER_TYPE   ePortType;        // 端口类型
      BOOL             bIsOpened;        // 端口是否打开
      HANDLE           hPort;            // 端口句柄
      COMMPORT*        pCommPort;        // 通信端口对象
      char             szPortConfig[64]; // 配置字符串("9600,N,8,1" 或 "IP:Port")

      pthread_t        threadId;         // 线程 ID
      BOOL             bThreadRunning;   // 线程运行标志
      BOOL             bThreadShouldExit;// 退出信号
      pthread_mutex_t  dataMutex;        // 数据互斥锁

      struct stPvRoughData* pPvRoughData;// 指向主数据区
      UINT             uSampleCount;     // 累计采样次数
      UINT             uCommFailCount;   // 通信失败次数
  } PORT_THREAD_INFO;

dataMutex 同时有两个用途:一是保护采样线程与 Modbus 读写操作之间的互斥(sampleModbusRtuData 进入时 lock,返回后 unlock),二是保护采样结果写入主数据区时与主线程(Sample()/Push_AllData())之间的互斥。

5.3 线程生命周期

Initialize() 时,根据配置中各逆变器指定的物理接口,动态决定需要启动哪些端口线程:

  // 遍历所有逆变器配置,标记需要哪些端口
  for (i = 0; i < MAX_PVInverter_NUM; i++) {
      if (pPV_Rough->stPVSettingParam[i].eInvertType != INVERTER_TYPE_NOT_INSTALL) {
          PHY_INTER_TYPE ePort = pPV_Rough->stPVSettingParam[i].ePhyInterface;
          bNeedPorts[ePort] = TRUE;
      }
  }
  // 只启动需要的端口线程
  for (i = 0; i < _MAX_PHY_INTER_TYPE; i++) {
      if (bNeedPorts[i]) {
          pthread_create(&pPortInfo->threadId, NULL, Port_Sample_Thread, pPortInfo);
          pthread_detach(pPortInfo->threadId);  // 分离态,自动回收
      }
  }

线程设置为分离态(pthread_detach),不需要 pthread_join 回收。退出时通过 bThreadShouldExit 标志通知线程自行退出,主线程最多等待 10 秒(100 × 100ms)。

5.4 采样主线程的角色转变

重构后,Sample()(主循环)不再直接做 Modbus 通信,只做三件事:

  1. Refresh_PV_Toatl_Info():汇总所有逆变器的数据,计算总功率、总发电量、整体状态
  2. DisPatch_PV_Output():根据 EMS 下发的控制指令,将功率目标分配给各台逆变器并调用控制命令
  3. Push_AllData():把汇总后的数据推送到数据总线(由 DATA_PROCESSOR 接口完成)

加互斥锁(hSamplerLock,200ms 超时)保护这三个操作,防止与控制命令的并发写冲突。

5.5 端口类型扩展:串口服务器

除了本地 RS485(ttyS3/ttyS4),还支持最多 4 路串口服务器(UART-over-TCP)。串口服务器把远端的 RS485 透传成 TCP 连接,EMS 网关通过 TCP 连接到服务器的 IP+Port,就像操作本地串口一样。

  // PHY_UART_SERVER 到 PHY_UART_SERVER_4,端口号按序递增
  int portOffset = ePortType - PHY_UART_SERVER;
  int targetPort = pPV_Rough->uartServerCfg.iStartPort + portOffset;
  sprintf(szServerAddr, "%s:%d", pPV_Rough->uartServerCfg.szIP, targetPort);

这使得系统可以接入更多逆变器(本地 2 路 + 串口服务器 4 路 = 6 路并发),理论上每路挂 32 台,一套系统管理 192 台逆变器没有问题。


六、光伏出力对 EMS 决策的价值

6.1 EMS 需要什么光伏数据

从代码的 PV_INVERTER_DATA 结构来看,每台逆变器采集的核心数据包括:

│             字段              │            含义             │        EMS 用途        │
├───────────────────────────────┼────────────────────────────┼────────────────────────┤
│ fOutputActivePower            │ 当前交流有功出力(W)        │ 实时功率平衡计算       │
├───────────────────────────────┼────────────────────────────┼────────────────────────┤
│ iRatedPower                   │ 额定功率(W)               │ 最大可用功率估算       │
├───────────────────────────────┼────────────────────────────┼────────────────────────┤
│ fPVInputTotalPower            │ 直流侧总功率(W)            │ 与交流侧对比,监测效率 │
├───────────────────────────────┼────────────────────────────┼────────────────────────┤
│ fDailyPowerGeneration         │ 当日发电量(kWh)           │ 日内电量预测基准       │
├───────────────────────────────┼────────────────────────────┼────────────────────────┤
│ iWorkMode                     │ 工作状态(运行/等待/故障)   │ 可用容量判断           │
├───────────────────────────────┼────────────────────────────┼────────────────────────┤
│ fPVGroupVolt[]/fPVGroupCurr[] │ 各路 MPPT 电压电流          │ 辐照度反演,精细化预测 │
├───────────────────────────────┼────────────────────────────┼────────────────────────┤
│ fInnerTemperature             │ 机内温度                    │ 高温降额提前预警       │
└───────────────────────────────┴────────────────────────────┴────────────────────────┘

采样频率:当前代码的采样周期是 1 秒(端口线程的 Sleep(1000)),主线程也是 1 秒一个周期。这对于 EMS 的实时控制(防逆流、削峰填谷)已经足够。对于光伏出力预测,1 分钟甚至 5 分钟的聚合数据更有意义——可以在数据总线层做下采样,不需要修改采样层的频率。

6.2 防逆流控制的实现

代码中 DisPatch_PV_Output() 实现了两种控制模式:

CTRL_BY_AEMS(EMS 主控):EMS 通过 Control() 接口(参数 ID 534)下发功率目标,代码将总功率按台数均分,换算成百分比后通过 Modbus FC06 写入各台逆变器的功率限制寄存器。

CTRL_BY_DATA_ACQUISITION(本地数采对接):外部数采系统通过 DO 信号控制权的交接(DO_Ctrl()),决定由谁来做防逆流控制。这种模式下,如果外部数采没有接管,本系统自动将逆变器设为不限功率输出。

这种双模控制设计在工程上很实用——新站点投运初期,可以先用本地数采模式验证通信,确认没问题后再切换到 EMS 主控模式。


七、AI 结合:光伏出力短期预测(CNN + 气象数据融合)

采到了数据,下一步是让数据产生更大的价值。EMS 的调度决策质量很大程度上取决于对未来 15 分钟~2 小时光伏出力的预测准确性。纯粹靠实时采样数据被动响应,始终比主动预测慢半拍。

7.1 为什么用 CNN 而不是 LSTM

光伏出力的时序特性有其独特性:它不是纯粹的时序序列问题,而是时序 × 气象特征的二维结构。比如,"今天 10:00 的出力"与"同一地点昨天 10:00 的出力"的相关性,远比"今天 9:55 的出力"更强(天气的日周期性)。CNN 的卷积核天然擅长提取这种局部时空模式,而 LSTM 的记忆机制在这里反而引入了不必要的长程依赖。

实践中表现最好的是 1D CNN + 气象特征拼接 的结构:

输入:过去 N 个时间步的逆变器数据 + 对应时刻的气象数据

气象数据:来自气象 API 或现场气象站(辐照度、温度、云层覆盖、湿度)

inputs_pv = Input(shape=(N_steps, pv_features)) # 采样数据 inputs_weather = Input(shape=(weather_features,)) # 气象特征

  ### 用 1D CNN 提取时序模式
  x = Conv1D(64, kernel_size=3, activation='relu')(inputs_pv)
  x = Conv1D(64, kernel_size=3, activation='relu')(x)
  x = GlobalMaxPooling1D()(x)

  ### 拼接气象特征
  x = Concatenate()([x, inputs_weather])
  x = Dense(64, activation='relu')(x)
  output = Dense(H_horizon)(x)  # 输出未来 H 个时间步的出力预测

7.2 关键输入特征

从 EMS 的实际需求出发,预测所需的特征优先级:

  ┌───────────────────────────────────┬──────────────────────────┬────────┐
  │               特征                │           来源           │ 重要性 │
  ├───────────────────────────────────┼──────────────────────────┼────────┤
  │ 水平面总辐照度(GHI)             │ 气象 API / 现场辐照仪    │ ★★★★★  │
  ├───────────────────────────────────┼──────────────────────────┼────────┤
  │ 斜面辐照度(POA)                 │ 由 GHI + 坡度/方位角换算 │ ★★★★★  │
  ├───────────────────────────────────┼──────────────────────────┼────────┤
  │ 环境温度                          │ 气象站                   │ ★★★★   │
  ├───────────────────────────────────┼──────────────────────────┼────────┤
  │ 云层覆盖/云量百分比               │ 气象 API                 │ ★★★★   │
  ├───────────────────────────────────┼──────────────────────────┼────────┤
  │ 历史同时刻出力(昨日、近7天均值) │ EMS 数据库               │ ★★★★   │
  ├───────────────────────────────────┼──────────────────────────┼────────┤
  │ 逆变器机内温度(影响效率)        │ 本系统采样               │ ★★★    │
  ├───────────────────────────────────┼──────────────────────────┼────────┤
  │ 风速(影响组件散热)              │ 气象站                   │ ★★     │
  └───────────────────────────────────┴──────────────────────────┴────────┘

最小可行方案:GHI + 环境温度 + 历史出力,三个特征就能把 MAE 控制在额定功率的 5~8% 以内,满足大多数工商业储能的调度需求。

7.3 训练数据的组织

本系统每台逆变器 1 秒采样一次,对于预测任务,需要先聚合成 5 分钟或 15 分钟的数据点。数据入库时需要打标签:

  -- 按 5 分钟聚合光伏出力
  SELECT
      DATE_TRUNC('minute', ts) -
          INTERVAL '1 second' * (EXTRACT(EPOCH FROM ts)::int % 300) AS bucket,
      AVG(output_active_power) AS avg_power,
      MAX(output_active_power) AS max_power,
      COUNT(*) AS sample_count
  FROM pv_inverter_data
  GROUP BY bucket
  ORDER BY bucket;

训练集建议至少覆盖 1 年的完整数据,以包含四季辐照变化规律。增量训练可以按周更新模型,保持对当地气候的适应性。

7.4 预测结果如何输入 EMS 决策

预测出力曲线可以通过 Control() 接口的拓展参数传入 EMS 的调度层:

  • 当预测未来 30 分钟出力下降(如云层遮挡),EMS 提前给储能充电,为后续负荷高峰储备容量
  • 当预测未来 1 小时出力大幅攀升,EMS 提前降低电网购电,减少需量费
  • 当预测出力超过当前负荷且无法消纳时,提前收紧逆变器功率上限(通过 ‘PV_CMD_ActiveOutputPower’性限制),防逆流触发延迟缩短

这是从"被动采样"升级到"主动预测调度"的关键一步,也是现阶段大多数 EMS 产品的差距所在。


总结

回顾这个模块的开发过程,有几个核心经验值得记录:

  1. 协议碎片化是常态,不是意外。光伏逆变器的 Modbus协议没有行业统一标准,每个品牌都有自己的寄存器地址规划。做多品牌接入时,一定要拿到厂家官方的最新协议文档,不要依赖网上流传的版本,那些版本很可能已经过时。
  2. 地址偏移问题必须测试验证。Sungrow 减 1、Ginlong 减 1 这类偏移问题,纯看代码很难发现,必须用真实设备抓包对比。
  3. 线程模型要与物理总线匹配。RS485 是半双工、总线共享的物理介质,在软件层面必须保证同一串口在任意时刻只有一个 Modbus 请求在进行。dataMutex 不只是保护数据结构,更是在保护物理总线的访问时序。
  4. 通信失败处理要有层次。单次读取失败不立即报警,连续 5 次失败才标记 bIsCommFail;所有设备都失败连续 10 次,才触发端口重开。这种分层设计避免了因瞬间干扰导致的误报。
  5. 采样数据的价值不止于控制。把采样到的历史数据喂给机器学习模型,做短期出力预测,是让 EMS 从反应式控制升级为预测式调度的基础。这条路目前大多数厂家还没做好,也是差异化竞争的机会点。