(* ============================================================ *)
(* ModbusWriteCache_Types —— 写缓存模块：数据类型 + 缓存 DB     *)
(* S7-1200/1500 TIA Portal SCL（导入用，GB2312/ANSI 编码）      *)
(* 导入：项目树->外部源文件->添加新外部源文件->从源生成块       *)
(* 本文件含 1 个数据类型 + 1 个 DB：须先导它，再导两个 FC。     *)
(* 不要先手动新建同名对象。完整说明见 UTF-8 主版本。            *)
(* DB 的"优化的块访问"必须保持关闭，否则 MB_CLIENT 报 16#818C。 *)
(* 版本：v2.1 2026-10-23（前一版 v2.0.1 2026-09-29）            *)
(* 修订：v2.0 漏了 MB_CLIENT_W_DATA 字段，本版补回。            *)
(* 只有本文件变了，两个 FC 没动，不必重导。                     *)
(* ============================================================ *)

TYPE "UDT_ModbusWriteTask"
VERSION : 0.2
   STRUCT
      SlaveID : Byte;    // 从站/单元号 -> MB_UNIT_ID，1..247；0 是广播，本模块拒收
      RegAddr : UDInt;   // 地址 -> MB_DATA_ADDR。注意存的是"实际发出去的值"，
                         //   已把 DB 里的 ADDR_OFFSET 加进去了，不再是调用方原值
      RegValue : Word;   // 要写入的值
      RetryCnt : Int;    // 剩余重试次数
      TaskValid : Bool;  // 任务有效标记
      SeqNo : UDInt;     // 入队序号（单调递增）。现场看它是否在变，就知道队列有没有在动
   END_STRUCT;

END_TYPE

DATA_BLOCK "DB_ModbusWriteCache"
{ S7_Optimized_Access := 'FALSE' }   // 千万别改成 TRUE，见文件头第 1 条
VERSION : 0.3
NON_RETAIN                          // 见文件头第 2 条
   STRUCT
      (* ---------------- 组态区：这些是你要改的东西 ---------------- *)
      QUEUE_MAX_LEN : Int;      // 队列物理长度，必须等于 TaskQueue 的数组长度
      QUEUE_CAPACITY : Int;     // 实际可用容量 = QUEUE_MAX_LEN - 1（环形队列留一空位区分空/满）
      RETRY_MAX : Int;          // 单条任务最多尝试几次（含第一次）：3 = 首发 1 次 + 重试 2 次。
                                //   沿用旧版语义（旧版也是拿它当"总尝试次数"用），别理解成"重试 3 次"
      ENABLE_WRITE : Bool;      // 写总闸：FALSE = 只入队不发送（调试/检修用）
      RETRY_ON_PROTO_ERR : Bool;// 从站返回异常码(838x)时是否也重试。FALSE=直接丢弃并报警
      ADDR_OFFSET : UDInt;      // 地址统一偏移。填 0 = 原样透传（默认，与旧版一致）；
                                //   你的地址习惯是"0 基寄存器号"就填 40001
      TRIG_MODE : Int;          // 【自 v2.1 起已废弃，FC 内不再读这个字段】
                                //   v2.0 用它切"块内取沿 / 调用方给脉冲"两种模式。
                                //   现在 AddTrigger 一律按单周期脉冲处理，填 0 填 1 都一样。
                                //   留着字段只是为了避免重导 DB —— 重导会把在线整定过的
                                //   T_ACCEPT / T_REQ / RETRY_MAX 一起重置
      T_ACCEPT : Time;          // 置 REQ 后等"受理(BUSY)"的超时
      T_REQ : Time;             // 受理后等 DONE/ERROR 的超时（兜底，防状态机卡死）
      T_DISC : Time;            // 断线(DISCONNECT)保持时长
      T_RECONN : Time;          // 重连冷却时长

      (* -------------------------- 队列区 -------------------------- *)
      TaskQueue : Array[0..99] of "UDT_ModbusWriteTask";  // 任务环形队列
      QueueWrPtr : Int;         // 写指针（下一个空槽位）
      QueueRdPtr : Int;         // 读指针（正在处理 / 下一个要处理的槽位）
      QueueEmpty : Bool;        // 队列空
      QueueFull : Bool;         // 队列满
      QueueCount : Int;         // 当前条目数。每周期从两个指针重算，不靠推算

      (* ------------------------- 状态机区 ------------------------- *)
      MB_State : Int;           // 0 空闲 / 10 等受理 / 20 等结果 / 30 判定 / 60 断线 / 65 冷却
                                //   成功与丢弃不单占状态，就地出队（每任务少花一拍）
      CurTask : "UDT_ModbusWriteTask";   // 当前正在处理的任务（队列槽位的副本）
      Busy : Bool;              // 有任务在处理中
      Err : Bool;               // 最近一次出队是失败
      Execute : Bool;           // 给 MB_CLIENT 的 REQ
      Disc : Bool;              // 给 MB_CLIENT 的 DISCONNECT
      LastTrigger : Bool;       // 上一周期最后一次调用时的 AddTrigger 值。
                                //   v2.1 起只用于 TrigHoldWarn（判断触发位是否连续两拍为 TRUE），
                                //   不再参与入队判定 —— FC 没有静态区，一份共享历史撑不住多次调用
      PutCallCnt : Int;         // 本扫描周期内 PutTask 被调了几次。v2.1 起"调多次"是正常用法，
                                //   这个计数只用于认出"本周期第一次调用"和 MultiCall 诊断。
                                //   由 FC_ModbusWrite_CacheRun 每周期末尾清零
      WriteLocked : Bool;       // 队列里有任务但写总闸没开（稳态诊断量）

      (* ------------------------ 通信缓冲区 ------------------------ *)
      MB_CLIENT_W_DATA : Word;  // 写数据缓冲区。MB_CLIENT 的 MB_DATA_PTR 指这里。
                                //   名字沿用 v0.1 原稿，别改名 —— 改了引用它的
                                //   块会一起断（v2.0 就是漏了这个字段才编译不过）。
                                //   必须是本非优化 DB 里的普通变量，见文件头第 1 条；
                                //   指向优化 DB 会稳定报 16#818C，一条都发不出去。
                                //   想一次写多个寄存器：换成 Array[0..122] OF Word，
                                //   并把 CacheRun 里的 MB_DATA_LEN 一起改（FC16 上限 123）

      (* ----------------------- 统计与诊断区 ----------------------- *)
      VER : Byte;               // 版本指纹：在线监视到 16#21 说明跑的是 v2.0.1 这套代码
      Nonce : UDInt;            // 入队序号发生器
      OkCount : UDInt;          // 成功写入次数
      RetryCount : UDInt;       // 累计重试次数
      ErrCount : UDInt;         // 最终失败（重试耗尽或被判不可重试）次数
      DropCount : UDInt;        // 因队列满被拒绝入队的次数
      RejectCount : UDInt;      // 因参数非法被拒绝入队的次数
      TimeoutCount : UDInt;     // 发生兜底超时 / 未受理的次数
      AddrWarnCount : UDInt;    // 地址落在"非保持寄存器区"而被警示的次数
      MB_StatusLive : Word;     // MB_CLIENT 原始实时 STATUS（16#7000 空闲 / 16#7005 发送中…）
      MB_StatusLatched : Word;  // 锁存的错误码；成功时清零。DONE/ERROR 只亮一个周期，
                                //   不锁存的话等你去监视表看的时候早就没了
      LastErrStatus : Word;     // 最近一次失败的现场状态码（含本块自定义码）
      LastErrTask : "UDT_ModbusWriteTask";  // 最近一次失败的任务快照：哪个从站、哪个地址、什么值

      (* -------------------------- 定时器 -------------------------- *)
      TAcceptTmr : TON_TIME;    // 等受理
      TReqTmr : TON_TIME;       // 等结果
      TDiscTmr : TON_TIME;      // 断线保持
      TReconnTmr : TON_TIME;    // 重连冷却
   END_STRUCT;


BEGIN
   (* 组态默认值。改这里只会影响"首次下载 / 掉电重启后"的值，
      在线改的话直接改监视表里的值即可（本 DB 非保持）。 *)
   QUEUE_MAX_LEN := 100;
   QUEUE_CAPACITY := 99;
   RETRY_MAX := 3;
   ENABLE_WRITE := TRUE;         (* 默认开：本模块的职责就是往从站写，
                                   置 FALSE 会让现场"莫名不写了"，所以默认沿用旧版行为 *)
   RETRY_ON_PROTO_ERR := FALSE;  (* 从站明确回异常码，说明这条请求本身有问题，重试是白等 *)
   ADDR_OFFSET := 0;             (* 默认不改地址，与旧版行为完全一致 *)
   TRIG_MODE := 0;               (* 自 v2.1 起被 FC 忽略，填什么值都不影响行为 *)
   T_ACCEPT := T#1s;
   T_REQ := T#6s;                (* 要比 MB_CLIENT 自己在背景 DB 里的响应超时(默认 5s)长，
                                   否则本块会先于 MB_CLIENT 报超时，拿不到真正的错误码 *)
   T_DISC := T#300ms;
   T_RECONN := T#500ms;

   (* 版本指纹：在线监视到 16#21 说明下进去的是 v2.0.1             *)
   VER := 16#21;

   (* 通信缓冲区                                                   *)
   MB_CLIENT_W_DATA := 0;

   (* 队列与状态机                                                 *)
   QueueWrPtr := 0;
   QueueRdPtr := 0;
   QueueEmpty := TRUE;
   QueueFull := FALSE;
   QueueCount := 0;
   MB_State := 0;
   Busy := FALSE;
   Err := FALSE;
   Execute := FALSE;
   Disc := FALSE;
   LastTrigger := FALSE;
   PutCallCnt := 0;
   WriteLocked := FALSE;

   (* 统计清零                                                     *)
   Nonce := 0;
   OkCount := 0;
   RetryCount := 0;
   ErrCount := 0;
   DropCount := 0;
   RejectCount := 0;
   TimeoutCount := 0;
   AddrWarnCount := 0;
   MB_StatusLive := 16#7000;
   MB_StatusLatched := 0;
   LastErrStatus := 0;

END_DATA_BLOCK
