172 lines
6.3 KiB
C#
172 lines
6.3 KiB
C#
using System;
|
|
using System.Threading;
|
|
using SimpleCore.Library;
|
|
using StandardScene.Signal.Model;
|
|
|
|
namespace StandardScene.Signal.Plc
|
|
{
|
|
/// <summary>
|
|
/// 单机构 200ms 循环:心跳、读允许进出、回读写值、按需写入、自动离站。
|
|
/// 对齐反编译 <c>PLCManager.ExcuteThreadMethod</c>。
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 一拍顺序固定,失败则本拍标断线并 return,避免用脏数据推进状态机:
|
|
/// 1. 翻转内存心跳标志;若配置了 HeartDb,读当前位再写反码(PLC 侧看翻转判断调度活着)。
|
|
/// 2. 读 ReadDb → <see cref="PlcStationRuntime.ReadSignal"/>。
|
|
/// 3. 回读 WriteDb → ReadWriteSignal;与目标 WriteSignal 一致则 WriteSuccess=true。
|
|
/// 4. 目标与回读不一致且尚未成功时写入目标字节。
|
|
/// 5. <see cref="PlcStationLogic.AutoLeave"/>。
|
|
/// 读写均在 <see cref="PlcStationRuntime.Sync"/> 内,与扫车 Handler 互斥。
|
|
/// </remarks>
|
|
public sealed class PlcStationWorker : IDisposable
|
|
{
|
|
private readonly SiemensPlcSession _session;
|
|
private readonly int _pollMs;
|
|
private readonly Action _onTick;
|
|
private CancellationTokenSource _cts;
|
|
private Thread _thread;
|
|
|
|
/// <summary>断线日志节流起点,避免 200ms 打满诊断。</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>本机构运行态,扫车线程按 JgName 查找后加 Sync 读写。</summary>
|
|
public PlcStationRuntime Runtime { get; }
|
|
|
|
/// <summary>启动后台线程。重复 Start 若仍存活则忽略。</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>取消循环、最多等 2s、关闭 S7 连接并标断线。</summary>
|
|
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; }
|
|
}
|
|
}
|
|
|
|
/// <summary>单拍 PLC 交互。心跳地址为空则跳过心跳;读写地址为空则跳过对应步骤。</summary>
|
|
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}");
|
|
}
|
|
|
|
/// <summary>同一机构失败日志最少间隔 5 秒。</summary>
|
|
private void LogFail(string msg)
|
|
{
|
|
if ((DateTime.Now - _lastFailLog).TotalSeconds < 5) return;
|
|
_lastFailLog = DateTime.Now;
|
|
Diagnosis.Log(msg, "PLC", true);
|
|
}
|
|
}
|
|
}
|