(* ============================================================ *)
(* DiagOB_Types —— 诊断 OB 错误捕获：数据类型 + 共享 DB         *)
(* S7-1200/1500 TIA Portal SCL（导入用，GB2312/ANSI 编码）      *)
(* 导入：项目树->外部源文件->添加新外部源文件->从源生成块       *)
(* 本文件含 2 个数据类型 + 1 个 DB，必须第一个导入。            *)
(* 不要先手动新建同名对象。完整说明见 UTF-8 主版本。            *)
(* 导入顺序：本文件 -> FC_DiagOBReport -> FB_DiagOB。           *)
(* SerialGen 必须最后更新，否则 OB1 会读到半截事件。            *)
(* 版本：v1 2026-10-07                                          *)
(* ============================================================ *)

TYPE "UDT_DiagEvent"
VERSION : 0.1
   STRUCT
      Serial   : DWord;      // 流水号，1 起单调递增；0 = 这条是空槽位
      OBNr     : Word;       // 事件来自哪个 OB：80/82/83/86/121/122
                             //   16#FFFF = 本地错误通道上报的（不是 OB 报的）
      EvClass  : Byte;       // 事件类别 Event_Class（OB83/OB86 有）
      FaultId  : Byte;       // 故障标识符 Fault_ID（OB83/OB86 有）
      IOstate  : Word;       // IO 状态字（OB82 有）：位4=有错、位5=组态错、位7=访问错
      Laddr    : Word;       // 出错对象的硬件标识符 LADDR
      Channel  : Word;       // 通道号（OB82 有）
      Leaving  : Bool;       // FALSE = 到达事件（故障发生）；TRUE = 离去事件（故障消了）
      Stamp    : DTL;        // 发生时刻（本地时间）。CPU 时钟没设过的话这是垃圾值，
                             //   可信与否看 FB_DiagOB 的 bClockValid 输出
   END_STRUCT;
END_TYPE

TYPE "UDT_DiagState"
VERSION : 0.1
   STRUCT
      (* -------------------- 版本与流水号发生器 -------------------- *)
      VER          : Byte;                      // 版本指纹：在线监视 16#01 即本版
      SerialGen    : DWord;                     // 流水号发生器。只有 FC_DiagOBReport 写它，
                                                //   并且必须最后写；FB 侧从不写它

      (* ------------------------- 累计计数 ------------------------- *)
      EvtTotal     : DWord;                     // 事件总数（到达 + 离去都算）
      RepeatTotal  : DWord;                     // 同一个来源在去重窗口内反复触发的次数
      CntOB80      : DWord;                     // OB80  循环时间超限
      CntOB82      : DWord;                     // OB82  模块诊断中断（断线/短路/超量程…）
      CntOB83      : DWord;                     // OB83  分布式模块拔插
      CntOB86      : DWord;                     // OB86  PROFINET IO 设备 / DP 从站故障
      CntOB121     : DWord;                     // OB121 编程错误（仅 S7-1500）
      CntOB122     : DWord;                     // OB122 I/O 访问错误（仅 S7-1500）
      CntLocalErr  : DWord;                     // 本地错误通道上报次数（GET_ERR_ID）
      CntUnknown   : DWord;                     // OB 号不是上面六个：调用方填错了，
                                                //   单独记账，好让你一眼看出接口接错

      (* ------------------------- 事件快照 ------------------------- *)
      Last         : "UDT_DiagEvent";           // 最近一条事件（不管哪一类）
      LastHw       : "UDT_DiagEvent";           // 最近一条**硬件类**错误（82/83/86 到达）
      LastProg     : "UDT_DiagEvent";           // 最近一条**程序类**错误（121/122 + 本地通道）
      FirstSinceRst: "UDT_DiagEvent";           // 复位后第一条事件。现场最值钱的就是它：
                                                //   后面那一大串往往都是它的跟班症状

      (* ------------------------- 环形历史 ------------------------- *)
      Hist         : Array[0..15] of "UDT_DiagEvent";   // 16 条滚动历史，越界自动覆盖最老的
      HistWptr     : UInt;                      // 下一个要写的槽位
      HistCount    : UInt;                      // 已存条数，满 16 就不再涨

      (* --------------------- 硬件故障配平计数 --------------------- *)
      uHwOpen      : UInt;                      // 还没等到配对离去事件的硬件类"到达"条数。
                                                //   HwFault 就是"这个数 > 0"，由 FB 每周期重算。
                                                //   为什么不用一个简单的 BOOL：一个工程里少则
                                                //   三五个、多则几十个分布式设备，用 BOOL 的话
                                                //   任何一个恢复就把全局清了，剩下还在报错的那
                                                //   些就凭空消失 —— 现场最怕这种假"已恢复"

      (* ------------------------ 锁存故障位 ------------------------ *)
      HwFault      : Bool;                      // 硬件类故障未确认
      ProgFault    : Bool;                      // 程序类故障未确认
      CycFault     : Bool;                      // 循环时间类故障未确认
      LocalFault   : Bool;                      // 本地错误通道上报过未确认
   END_STRUCT;
END_TYPE

DATA_BLOCK "DB_DiagOB"
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
   STRUCT
      st : "UDT_DiagState";   // 整个模块就这一份数据。HMI、OB、OB1 全部对准它
   END_STRUCT;

BEGIN
   (* 只要在 BEGIN 里给值，重新下载时会被刷回初值；要改请用监视表  *)
   st.VER := 16#01;
   st.SerialGen := 0;
   st.HistWptr := 0;
   st.HistCount := 0;
   st.FirstSinceRst.Serial := 0;
END_DATA_BLOCK

(* ============================================================ *)
(* 字段说明速查（这块写在这儿是因为它比任何一个块都重要）       *)
(*                                                              *)
(* 1. Leaving 为什么不用 Event_Class 自己算？                   *)
(* 因为 **OB83 和 OB86 的 38/39 语义是反的**：                  *)
(* OB83：39 = 模块拔掉（故障）；38 = 模块插上（含"插了但        *)
(* 不匹配/参数错/有毛病"这几档，也得算故障）                    *)
(* OB86：39 = IO 设备故障（到达）；38 = 故障恢复（离去），      *)
(* 但 C5~C8 / F9 这几档是"部分恢复、仍有故障"，归 38 却不能当成 *)
(* 彻底好了。所以让块去猜，迟早猜错。本模块的办法是：**判定权交 *)
(* 给调用方** —— 在 OB 模板里按上表把 bLeaving 算好传进来，块只 *)
(* 负责照抄记账。改了一个 OB 的语义也不用动块。                 *)
(*                                                              *)
(* 2. 为什么 FirstSinceRst 单独留一条，不用 Hist[0]？           *)
(* 历史是环形的，"复位后第一条"那个槽位迟早被覆盖掉。而它就是第 *)
(* 一现场，覆盖不起 —— 比如第一次报的下标，后面三千条都是它引起 *)
(* 的，第一现场一丢，现场就只能瞎猜。                           *)
(*                                                              *)
(* 3. RepeatTotal 是干什么的？                                  *)
(* 专治"每个扫描周期都报同一个错"。典型就是一个跑飞的数组下标或 *)
(* 者一段根本没配模块的 I/O 访问：它不会只报一次，而是一直报到  *)
(* 天荒地老。如果只做计数，一小时能涨到几十万，现场看着这个数完 *)
(* 全判断不出"到底发生了几件事"。所以 FB 里另有一套窗口去重（同 *)
(* 类来源在 tDedup 之内再报，只往 RepeatTotal 里加，并点亮      *)
(* bRepeatBurst 稳态位），告诉现场"现在正在被同一个错误刷屏"。  *)
(* ============================================================ *)
