using System; using System.Threading; using SimpleCore.Library; using StandardScene.Signal.Model; namespace StandardScene.Signal.Plc { /// /// 单机构 200ms 循环:心跳、读允许进出、回读写值、按需写入、自动离站。 /// 对齐反编译 PLCManager.ExcuteThreadMethod。 /// /// /// 一拍顺序固定,失败则本拍标断线并 return,避免用脏数据推进状态机: /// 1. 翻转内存心跳标志;若配置了 HeartDb,读当前位再写反码(PLC 侧看翻转判断调度活着)。 /// 2. 读 ReadDb → 。 /// 3. 回读 WriteDb → ReadWriteSignal;与目标 WriteSignal 一致则 WriteSuccess=true。 /// 4. 目标与回读不一致且尚未成功时写入目标字节。 /// 5. 。 /// 读写均在 内,与扫车 Handler 互斥。 /// public sealed class PlcStationWorker : IDisposable { private readonly SiemensPlcSession _session; private readonly int _pollMs; private readonly Action _onTick; private CancellationTokenSource _cts; private Thread _thread; /// 断线日志节流起点,避免 200ms 打满诊断。 private DateTime _lastFailLog = DateTime.MinValue; public PlcStationWorker(PlcStationRuntime runtime, SiemensPlcSession session, int pollMs, Action onTick = null) { Runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); _session = session ?? throw new ArgumentNullException(nameof(session)); _pollMs = Math.Max(50, pollMs); _onTick = onTick; } /// 本机构运行态,扫车线程按 JgName 查找后加 Sync 读写。 public PlcStationRuntime Runtime { get; } /// 启动后台线程。重复 Start 若仍存活则忽略。 public void Start() { if (_thread != null && _thread.IsAlive) return; _cts = new CancellationTokenSource(); _thread = new Thread(Loop) { IsBackground = true, Name = $"PlcStation:{Runtime.Config.JgName}" }; _thread.Start(); Diagnosis.Log($"{Runtime.Config.JgName} Worker 已启动 → {Runtime.Config.Ip}:{Runtime.Config.Port}", "PLC", true); } /// 取消循环、最多等 2s、关闭 S7 连接并标断线。 public void Stop() { try { _cts?.Cancel(); } catch { } try { _thread?.Join(2000); } catch { } _thread = null; _session.Close(); Runtime.IsConnected = false; Diagnosis.Log($"{Runtime.Config.JgName} Worker 已停止", "PLC", true); } public void Dispose() => Stop(); private void Loop() { var token = _cts.Token; while (!token.IsCancellationRequested) { try { lock (Runtime.Sync) Tick(); } catch (Exception ex) { Runtime.LastError = ex.Message; Runtime.IsConnected = false; LogFail($"{Runtime.Config.JgName} 周期异常 {ex.Message}"); } try { _onTick?.Invoke(); } catch { } try { Thread.Sleep(_pollMs); } catch (ThreadInterruptedException) { break; } } } /// 单拍 PLC 交互。心跳地址为空则跳过心跳;读写地址为空则跳过对应步骤。 internal void Tick() { var cfg = Runtime.Config; Runtime.Heart = !Runtime.Heart; if (!string.IsNullOrWhiteSpace(cfg.HeartDb)) { if (_session.TryReadBool(cfg.HeartDb, out var bit)) { _session.TryWriteBool(cfg.HeartDb, !bit); Thread.Sleep(20); } else { MarkDisconnected("心跳读写失败"); return; } } if (!string.IsNullOrWhiteSpace(cfg.ReadDb)) { if (_session.TryReadByte(cfg.ReadDb, out var rb)) Runtime.ReadSignal = PlcStationLogic.ConvertRead(rb); else { MarkDisconnected("读地址失败"); return; } } if (!string.IsNullOrWhiteSpace(cfg.WriteDb)) { if (_session.TryReadByte(cfg.WriteDb, out var wb)) { Runtime.ReadWriteSignal = PlcStationLogic.ConvertWrite(wb); if (Runtime.ReadWriteSignal == Runtime.WriteSignal) Runtime.WriteSuccess = true; } else { MarkDisconnected("写地址回读失败"); return; } if (Runtime.WriteSignal != Runtime.ReadWriteSignal && !Runtime.WriteSuccess) { Diagnosis.Log( $"AGV→机构 {cfg.JgName} 写入【{Runtime.WriteSignal}】请求进入{Runtime.RequestInAgvNo} 到达{Runtime.InAgvNo}", "JGControl", true); _session.TryWriteByte(cfg.WriteDb, (byte)Runtime.WriteSignal); } } Runtime.IsConnected = true; Runtime.LastError = ""; PlcStationLogic.AutoLeave(Runtime); } private void MarkDisconnected(string reason) { Runtime.IsConnected = false; Runtime.LastError = reason; LogFail($"{Runtime.Config.JgName} {reason}"); } /// 同一机构失败日志最少间隔 5 秒。 private void LogFail(string msg) { if ((DateTime.Now - _lastFailLog).TotalSeconds < 5) return; _lastFailLog = DateTime.Now; Diagnosis.Log(msg, "PLC", true); } } }