(* ============================================================ *)
(* FC_ModbusWrite_CacheRun —— Modbus TCP 写缓存：出队 + 状态机  *)
(* S7-1200/1500 TIA Portal SCL（导入用，GB2312/ANSI 编码）      *)
(* 导入：项目树->外部源文件->添加新外部源文件->从源生成块       *)
(* 本文件只含 FC，须先导 ModbusWriteCache_Types（UDT 与 DB）。  *)
(* 本文件故意不含 MB_CLIENT_DB / MB_CLIENT_PARA，免得覆盖你现场 *)
(* 的 IP 与连接号；那两个 DB 你的工程里已经有了。               *)
(* 每周期调用一次，全工程只能调一次。详见 UTF-8 主版本。        *)
(* 版本：v2.0 2026-09-29                                        *)
(* ============================================================ *)

FUNCTION "FC_ModbusWrite_CacheRun" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.2
   VAR_INPUT 
      InterfaceId : HW_ANY;   // 硬件标识符（PLC 的 PROFINET 口）
      ID : CONN_OUC;          // 连接号，必须唯一，且运行中不能改
      ADDR1 : Byte;           // 从站 IP 第 1 段
      ADDR2 : Byte;           // 从站 IP 第 2 段
      ADDR3 : Byte;           // 从站 IP 第 3 段
      ADDR4 : Byte;           // 从站 IP 第 4 段
      RemotePort : UInt;      // 端口号（Modbus TCP 标准 502）
   END_VAR

   VAR_OUTPUT 
      Busy : Bool;            // TRUE = 队列里有任务正在处理
      Err : Bool;             // TRUE = 最近一次出队是失败
      Done : Bool;            // 单周期脉冲：本次出队成功
      Dropped : Bool;         // 单周期脉冲：本次出队失败被丢弃
      QueueCount : Int;       // 当前队列条目数
      QueueFull : Bool;       // 队列满
      WriteLocked : Bool;     // 队列里有任务但写总闸没开（配置状态，不是故障）
      Status : Word;          // 锁存的错误码，0 = 当前健康。等你去监视表看的时候它还在
      StatusLive : Word;      // MB_CLIENT 原始实时状态（16#7000 空闲 / 7005 发送中…）
   END_VAR

   VAR_TEMP 
      iPop : Int;             // 出队记账：0=不出队 1=出队不记账 2=出队记成功 3=出队记失败
      iQueueLen : Int;        // 本周期现算出来的队列条目数
      wStat : Word;           // 本次失败的码，用来分类
      bNoRetry : Bool;        // TRUE = 重试没意义，直接丢
      bNeedReconn : Bool;     // TRUE = 先断开重连再试
   END_VAR


BEGIN
	(* ============== 0. 输出清零（每周期第一件事） =============== *)
	(* 单周期脉冲必须在这里清，而且必须在所有"会置起它"的代码之前。
	   顺序反了脉冲会被自己抹掉 —— 别的都对、就那一位永远不亮，最难查。 *)
	#Done := FALSE;
	#Dropped := FALSE;
	#iPop := 0;

	(* ================ 1. 指针自愈 + 队列水位重算 ================ *)
	(* 水位每周期从两个指针现算，不做增量推算，也就不依赖 PutTask 与
	   本 FC 谁先执行。这是原稿的一个隐性耦合：原稿的 QueueFull 是
	   上一周期算出来的，同一周期里刚出队一格却仍然被判满。 *)
	IF ("DB_ModbusWriteCache".QueueRdPtr < 0)
	   OR ("DB_ModbusWriteCache".QueueRdPtr > "DB_ModbusWriteCache".QUEUE_MAX_LEN - 1) THEN
	    "DB_ModbusWriteCache".QueueRdPtr := 0;
	END_IF;

	#iQueueLen := "DB_ModbusWriteCache".QueueWrPtr - "DB_ModbusWriteCache".QueueRdPtr;
	IF #iQueueLen < 0 THEN
	    #iQueueLen := #iQueueLen + "DB_ModbusWriteCache".QUEUE_MAX_LEN;
	END_IF;
	IF (#iQueueLen < 0) OR (#iQueueLen > "DB_ModbusWriteCache".QUEUE_CAPACITY) THEN
	    #iQueueLen := 0;   (* 指针跑飞时不硬撑，当空处理，让 PutTask 那侧的钳位去修 *)
	END_IF;
	"DB_ModbusWriteCache".QueueCount := #iQueueLen;
	"DB_ModbusWriteCache".QueueEmpty := (#iQueueLen = 0);
	"DB_ModbusWriteCache".QueueFull := (#iQueueLen >= "DB_ModbusWriteCache".QUEUE_CAPACITY);

	(* ======================= 2. 连接描述 ======================== *)
	(* 只在没有任务在跑的时候更新。MB_CLIENT 手册明确要求：作业进行中它
	   的输入引脚不能被改动，否则它没法判断是"哪次调用"发起的作业。
	   这几个值是常量，值本身不会变，所以跳过作业窗口不会有任何差异；
	   但如果你真在运行中改 IP，就得等这一轮跑完才会生效。 *)
	IF NOT "DB_ModbusWriteCache".Busy THEN
	    "MB_CLIENT_PARA".MB_CLIENT.InterfaceId := #InterfaceId;
	    "MB_CLIENT_PARA".MB_CLIENT.ID := #ID;
	    "MB_CLIENT_PARA".MB_CLIENT.RemoteAddress.ADDR[1] := #ADDR1;
	    "MB_CLIENT_PARA".MB_CLIENT.RemoteAddress.ADDR[2] := #ADDR2;
	    "MB_CLIENT_PARA".MB_CLIENT.RemoteAddress.ADDR[3] := #ADDR3;
	    "MB_CLIENT_PARA".MB_CLIENT.RemoteAddress.ADDR[4] := #ADDR4;
	    "MB_CLIENT_PARA".MB_CLIENT.RemotePort := #RemotePort;
	END_IF;

	(* ======================== 3. 定时器 ========================= *)
	(* 必须在调用 MB_CLIENT 之前每周期都调一次（异步指令的周期调用要求） *)
	"DB_ModbusWriteCache".TAcceptTmr(IN := ("DB_ModbusWriteCache".MB_State = 10),
	                                 PT := "DB_ModbusWriteCache".T_ACCEPT);
	"DB_ModbusWriteCache".TReqTmr(IN := ("DB_ModbusWriteCache".MB_State = 20),
	                              PT := "DB_ModbusWriteCache".T_REQ);
	"DB_ModbusWriteCache".TDiscTmr(IN := ("DB_ModbusWriteCache".MB_State = 60),
	                               PT := "DB_ModbusWriteCache".T_DISC);
	"DB_ModbusWriteCache".TReconnTmr(IN := ("DB_ModbusWriteCache".MB_State = 65),
	                                 PT := "DB_ModbusWriteCache".T_RECONN);

	(* ======================== 4. 状态机 ========================= *)
	CASE "DB_ModbusWriteCache".MB_State OF

	    0:  (* ---- 空闲 ---- *)
	        "DB_ModbusWriteCache".Execute := FALSE;
	        "DB_ModbusWriteCache".Disc := FALSE;
	        IF NOT "DB_ModbusWriteCache".QueueEmpty THEN
	            (* 先把队首整条复制成 CurTask。用副本而不是直接用队列槽位，
	               是为了让"重试次数递减"不污染队列内容，出问题时现场还能
	               看到这条任务当初是什么样 *)
	            "DB_ModbusWriteCache".CurTask := "DB_ModbusWriteCache".TaskQueue["DB_ModbusWriteCache".QueueRdPtr];
	            IF "DB_ModbusWriteCache".CurTask.TaskValid THEN
	                IF "DB_ModbusWriteCache".ENABLE_WRITE THEN
	                    "DB_ModbusWriteCache".Busy := TRUE;
	                    "DB_ModbusWriteCache".MB_State := 10;
	                END_IF;
	                (* 总闸没开：停在 0，不取出也不下发。任务留在队列里不丢，
	                   闸一开就接着跑。WriteLocked 由末尾统一算 *)
	            ELSE
	                (* 槽位无效（正常情况下不该出现）：跳过                         *)
	                #iPop := 1;
	            END_IF;
	        ELSE
	            "DB_ModbusWriteCache".Busy := FALSE;
	        END_IF;

	    10: (* ---- 已置 REQ，等 MB_CLIENT 受理 ---- *)
	        "DB_ModbusWriteCache".Execute := TRUE;
	        (* 作业参数只在触发前设一次。手册要求作业期间这几个输入不能变   *)
	        "MB_CLIENT_DB".MB_Unit_ID := "DB_ModbusWriteCache".CurTask.SlaveID;
	        "MB_CLIENT_DB".MB_DATA_ADDR := "DB_ModbusWriteCache".CurTask.RegAddr;
	        "MB_CLIENT_DB".MB_DATA_LEN := 1;
	        "DB_ModbusWriteCache".MB_CLIENT_W_DATA := "DB_ModbusWriteCache".CurTask.RegValue;

	        IF "MB_CLIENT_DB".DONE THEN
	            "DB_ModbusWriteCache".MB_StatusLatched := 0;
	            "DB_ModbusWriteCache".Busy := FALSE;
	            "DB_ModbusWriteCache".Err := FALSE;
	            #iPop := 2;
	            "DB_ModbusWriteCache".MB_State := 0;
	        ELSIF "MB_CLIENT_DB".ERROR THEN
	            "DB_ModbusWriteCache".MB_StatusLatched := "MB_CLIENT_DB".STATUS;
	            "DB_ModbusWriteCache".MB_State := 30;
	        ELSIF "MB_CLIENT_DB".BUSY THEN
	            (* 受理了：REQ 从这里落下。若是让 REQ 一直保持高，
	               本次作业一完成 MB_CLIENT 会立刻再发一遍同样的写 *)
	            "DB_ModbusWriteCache".MB_State := 20;
	        ELSIF "DB_ModbusWriteCache".TAcceptTmr.Q THEN
	            (* 受理超时：既没受理也没报错。典型是同一端口上还有别的
	               请求在跑（STATUS=16#8200）或连接正在建立中。
	               原稿在这里靠"STATUS=7004 就再置一次 REQ"想救回来，
	               方向是反的 —— 16#7004 是正常空闲态，见文件头第 1 条 *)
	            "DB_ModbusWriteCache".MB_StatusLatched := 16#FFFE;
	            "DB_ModbusWriteCache".TimeoutCount := "DB_ModbusWriteCache".TimeoutCount + 1;
	            "DB_ModbusWriteCache".MB_State := 30;
	        END_IF;

	    20: (* ---- 已受理，等 DONE / ERROR ---- *)
	        "DB_ModbusWriteCache".Execute := FALSE;   (* REQ 落下，避免完成后自动重发 *)
	        IF "MB_CLIENT_DB".DONE THEN
	            "DB_ModbusWriteCache".MB_StatusLatched := 0;
	            "DB_ModbusWriteCache".Busy := FALSE;
	            "DB_ModbusWriteCache".Err := FALSE;
	            #iPop := 2;
	            "DB_ModbusWriteCache".MB_State := 0;
	        ELSIF "MB_CLIENT_DB".ERROR THEN
	            "DB_ModbusWriteCache".MB_StatusLatched := "MB_CLIENT_DB".STATUS;
	            "DB_ModbusWriteCache".MB_State := 30;
	        ELSIF "DB_ModbusWriteCache".TReqTmr.Q THEN
	            (* 兜底超时：连 MB_CLIENT 自己都不返回结果了。
	               原稿没有这一步，走到这里就是永久卡死且无报警，见文件头第 2 条 *)
	            "DB_ModbusWriteCache".MB_StatusLatched := 16#FFFF;
	            "DB_ModbusWriteCache".TimeoutCount := "DB_ModbusWriteCache".TimeoutCount + 1;
	            "DB_ModbusWriteCache".MB_State := 30;
	        END_IF;

	    30: (* ---- 判定：重试 / 丢弃 / 先重连再重试 ---- *)
	        "DB_ModbusWriteCache".Execute := FALSE;
	        (* 这里保证了 REQ 至少有一拍低电平。少了这一拍，返回 10 时
	           形不成上升沿，MB_CLIENT 不会受理新请求 —— 就是永久卡死 *)
	        #wStat := "DB_ModbusWriteCache".MB_StatusLatched;

	        (* 分类一：请求本身有问题，重试一辈子也不会好                   *)
	        #bNoRetry := FALSE;
	        IF ((#wStat >= 16#8180) AND (#wStat <= 16#818F))
	           OR ((#wStat >= 16#8380) AND (#wStat <= 16#838F)) THEN
	            #bNoRetry := TRUE;      (* 16#818x 我方组态错 / 16#838x 从站回异常码 *)
	        END_IF;
	        IF ((#wStat >= 16#8380) AND (#wStat <= 16#838F))
	           AND "DB_ModbusWriteCache".RETRY_ON_PROTO_ERR THEN
	            #bNoRetry := FALSE;     (* 网关偶发帧错的现场留个口子 *)
	        END_IF;

	        (* 分类二：链路层面的问题，得先断开重连再试                     *)
	        #bNeedReconn := FALSE;
	        IF #wStat >= 16#FF00 THEN
	            #bNeedReconn := TRUE;   (* 16#FFFE/FFFF：本块自定义的超时码，见下 *)
	        END_IF;
	        IF (#wStat >= 16#80C0) AND (#wStat <= 16#80FF) THEN
	            #bNeedReconn := TRUE;   (* 80C8 无响应 / 80D0 收发错 等 TCP 层错误 *)
	        END_IF;

	        IF #bNoRetry THEN
	            (* 参数错、从站明确拒绝：直接丢，别浪费剩下几次重试的时间。
	               顺便把现场锁存下来 —— STATUS 只亮一个周期，
	               不锁存的话等你打开监视表时早就没了 *)
	            "DB_ModbusWriteCache".Busy := FALSE;
	            "DB_ModbusWriteCache".Err := TRUE;
	            "DB_ModbusWriteCache".LastErrStatus := "DB_ModbusWriteCache".MB_StatusLatched;
	            "DB_ModbusWriteCache".LastErrTask := "DB_ModbusWriteCache".CurTask;
	            #iPop := 3;
	            "DB_ModbusWriteCache".MB_State := 0;
	        ELSE
	            "DB_ModbusWriteCache".CurTask.RetryCnt := "DB_ModbusWriteCache".CurTask.RetryCnt - 1;
	            IF "DB_ModbusWriteCache".CurTask.RetryCnt > 0 THEN
	                "DB_ModbusWriteCache".RetryCount := "DB_ModbusWriteCache".RetryCount + 1;
	                IF #bNeedReconn THEN
	                    "DB_ModbusWriteCache".MB_State := 60;
	                ELSE
	                    "DB_ModbusWriteCache".MB_State := 10;   (* 原地重试 *)
	                END_IF;
	            ELSE
	                (* 重试耗尽，同样锁存现场后丢弃                                 *)
	                "DB_ModbusWriteCache".Busy := FALSE;
	                "DB_ModbusWriteCache".Err := TRUE;
	                "DB_ModbusWriteCache".LastErrStatus := "DB_ModbusWriteCache".MB_StatusLatched;
	                "DB_ModbusWriteCache".LastErrTask := "DB_ModbusWriteCache".CurTask;
	                #iPop := 3;
	                "DB_ModbusWriteCache".MB_State := 0;
	            END_IF;
	        END_IF;

	    60: (* ---- 断线：DISCONNECT 保持 T_DISC ---- *)
	        "DB_ModbusWriteCache".Execute := FALSE;
	        "DB_ModbusWriteCache".Disc := TRUE;
	        IF "DB_ModbusWriteCache".TDiscTmr.Q THEN
	            "DB_ModbusWriteCache".MB_State := 65;
	        END_IF;

	    65: (* ---- 释放 DISCONNECT，冷却 T_RECONN 后再重试 ---- *)
	        "DB_ModbusWriteCache".Execute := FALSE;
	        "DB_ModbusWriteCache".Disc := FALSE;
	        IF "DB_ModbusWriteCache".TReconnTmr.Q THEN
	            "DB_ModbusWriteCache".MB_State := 10;
	        END_IF;

	END_CASE;

	(* ======================= 5. 出队收尾 ======================== *)
	(* 三条出队路径（跳过无效槽位 / 成功 / 失败）共用这一处，
	   免得改了一处忘了另外两处 —— 原稿就是把出队代码抄了三遍 *)
	IF #iPop > 0 THEN
	    "DB_ModbusWriteCache".TaskQueue["DB_ModbusWriteCache".QueueRdPtr].TaskValid := FALSE;
	    "DB_ModbusWriteCache".QueueRdPtr := "DB_ModbusWriteCache".QueueRdPtr + 1;
	    IF "DB_ModbusWriteCache".QueueRdPtr >= "DB_ModbusWriteCache".QUEUE_MAX_LEN THEN
	        "DB_ModbusWriteCache".QueueRdPtr := 0;
	    END_IF;

	    IF "DB_ModbusWriteCache".QueueCount > 0 THEN
	        "DB_ModbusWriteCache".QueueCount := "DB_ModbusWriteCache".QueueCount - 1;
	    END_IF;
	    "DB_ModbusWriteCache".QueueEmpty := ("DB_ModbusWriteCache".QueueCount = 0);
	    "DB_ModbusWriteCache".QueueFull := ("DB_ModbusWriteCache".QueueCount >= "DB_ModbusWriteCache".QUEUE_CAPACITY);

	    IF #iPop = 2 THEN
	        "DB_ModbusWriteCache".OkCount := "DB_ModbusWriteCache".OkCount + 1;
	        #Done := TRUE;
	    ELSIF #iPop = 3 THEN
	        "DB_ModbusWriteCache".ErrCount := "DB_ModbusWriteCache".ErrCount + 1;
	        #Dropped := TRUE;
	    END_IF;
	END_IF;

	(* ====================== 6. 稳态诊断量 ======================= *)
	(* 每周期重算。只在某个状态里置一次的标志，生命周期只有一个扫描周期，
	   现场打开监视表那一刻它就没了，等于白做 *)
	"DB_ModbusWriteCache".WriteLocked := (NOT "DB_ModbusWriteCache".QueueEmpty)
	                                     AND (NOT "DB_ModbusWriteCache".ENABLE_WRITE);
	"DB_ModbusWriteCache".VER := 16#20;      (* 版本指纹：v2.0 *)

	#Busy := "DB_ModbusWriteCache".Busy;
	#Err := "DB_ModbusWriteCache".Err;
	#QueueCount := "DB_ModbusWriteCache".QueueCount;
	#QueueFull := "DB_ModbusWriteCache".QueueFull;
	#WriteLocked := "DB_ModbusWriteCache".WriteLocked;
	#Status := "DB_ModbusWriteCache".MB_StatusLatched;
	#StatusLive := "DB_ModbusWriteCache".MB_StatusLive;

	(* ==================== 7. MB_CLIENT 调用 ===================== *)
	(* 每扫描周期无条件调用（异步指令，要一直调到 BUSY 落下）       *)
	(* 注意 MB_DATA_PTR 指向的是"标准访问"DB 里的一个 WORD，这是合法的；
	   若哪天想一次写多个寄存器，要把它换成非优化 DB 里的
	   ARRAY[0..122] OF WORD，并把 MB_DATA_LEN 跟着改（FC16 上限 123） *)
	"MB_CLIENT_DB"(REQ := "DB_ModbusWriteCache".Execute,
	               DISCONNECT := "DB_ModbusWriteCache".Disc,
	               MB_MODE := 1,
	               MB_DATA_PTR := "DB_ModbusWriteCache".MB_CLIENT_W_DATA,
	               CONNECT := "MB_CLIENT_PARA".MB_CLIENT);

	(* 原始 STATUS 每周期刷新一份出来，方便调试时看到"进行中"的那几个码
	   （16#7001 建连 / 16#7005 发送中 / 16#7006 接收中），
	   它们不是故障，别当成错误码去查 *)
	"DB_ModbusWriteCache".MB_StatusLive := "MB_CLIENT_DB".STATUS;

	(* ============ 8. 收尾：把 PutTask 的调用计数清零 ============ *)
	(* 放在最后：本 FC 每周期调一次，所以 PutTask 那一侧
	   "本周期被调了几次"才有了一个可靠的基准 *)
	"DB_ModbusWriteCache".PutCallCnt := 0;

END_FUNCTION

(* ============================================================ *)
(* 调用示例（放在 OB1 里，每周期一次，全工程只调这一次）        *)
(*                                                              *)
(* "FC_ModbusWrite_CacheRun"(InterfaceId := 64,                 *)
(* ID          := 21,                                           *)
(* ADDR1       := 192,                                          *)
(* ADDR2       := 168,                                          *)
(* ADDR3       := 1,                                            *)
(* ADDR4       := 50,                                           *)
(* RemotePort  := 502,                                          *)
(* Busy        => "写缓存忙",                                   *)
(* Err         => "写缓存错",                                   *)
(* Done        => "写成功脉冲",                                 *)
(* Dropped     => "写丢弃脉冲",                                 *)
(* Status      => "锁存错误码");                                *)
(*                                                              *)
(* 输出引脚可以全不接：所有状态在 DB_ModbusWriteCache 里都有    *)
(* 一份（含五类计数和最近失败任务快照），HMI 直接读 DB 更省事。 *)
(*                                                              *)
(* 排错表（StatusLive / Status 两列配合看）：                   *)
(* 16#7000     空闲、无连接          —— 正常                    *)
(* 16#7001/2   正在建立连接          —— 正常，别当故障          *)
(* 16#7004     已连接、无作业处理中  —— 正常空闲态              *)
(* 16#7005/6   正在发送 / 接收       —— 正常                    *)
(*   16#80C8     从站超时无响应        —— ping 从站、查端口/防火墙 *)
(* 16#8200     端口正在处理别的请求  —— 同一 ID 有两处在用      *)
(* 16#818C     MB_DATA_PTR 指向了优化 DB —— 见文件头那条硬约束  *)
(*   16#8380..8F 从站返回异常码        —— 查地址越界 / 功能码不支持 *)
(* 16#8387     连接 ID 与先前请求不同 —— 运行中改了 #ID         *)
(* 16#FFFE     本块自定义：等受理超时                           *)
(* 16#FFFF     本块自定义：等结果兜底超时（从站连 TCP 都不回）  *)
(*                                                              *)
(* 常用整定：                                                   *)
(* T_ACCEPT  T#1s   局域网足够；跨网段/无线可放宽到 T#2s        *)
(* T_REQ     要比 MB_CLIENT 背景 DB 里的响应超时（默认 5s）长， *)
(* 否则本块先报超时，拿不到真正错误码。默认 T#6s。              *)
(* RETRY_MAX 3      从站偶发丢包用 2~3 次够了；再大只是拖时间   *)
(* QUEUE_MAX_LEN 100 一条局域网的写来回约 5~15ms，满队列约 1s   *)
(* ============================================================ *)
