磁导航1.0内部交管和信号交互
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using SimpleCore.Library;
|
||||
using StandardScene.Signal.Model;
|
||||
@@ -6,41 +8,43 @@ using StandardScene.Signal.Model;
|
||||
namespace StandardScene.Signal.Plc
|
||||
{
|
||||
/// <summary>
|
||||
/// 单机构 200ms 循环:心跳、读允许进出、回读写值、按需写入、自动离站。
|
||||
/// 对齐反编译 <c>PLCManager.ExcuteThreadMethod</c>。
|
||||
/// 单机构 200ms 循环:心跳、按工位读 BOOL(或旧一字节)、回写变化位、自动离站。
|
||||
/// </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 readonly PlcStationModel _station;
|
||||
private readonly List<PlcStationRuntime> _docks;
|
||||
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)
|
||||
public PlcStationWorker(
|
||||
PlcStationModel station,
|
||||
IEnumerable<PlcStationRuntime> docks,
|
||||
SiemensPlcSession session,
|
||||
object sync,
|
||||
int pollMs,
|
||||
Action onTick = null)
|
||||
{
|
||||
Runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_station = station ?? throw new ArgumentNullException(nameof(station));
|
||||
_docks = (docks ?? Array.Empty<PlcStationRuntime>()).Where(d => d != null).ToList();
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
Sync = sync ?? new object();
|
||||
_pollMs = Math.Max(50, pollMs);
|
||||
_onTick = onTick;
|
||||
foreach (var d in _docks)
|
||||
d.Heart = false;
|
||||
}
|
||||
|
||||
/// <summary>本机构运行态,扫车线程按 JgName 查找后加 Sync 读写。</summary>
|
||||
public PlcStationRuntime Runtime { get; }
|
||||
public object Sync { get; }
|
||||
|
||||
public IReadOnlyList<PlcStationRuntime> Docks => _docks;
|
||||
|
||||
public PlcStationRuntime Runtime => _docks.Count > 0 ? _docks[0] : null;
|
||||
|
||||
/// <summary>启动后台线程。重复 Start 若仍存活则忽略。</summary>
|
||||
public void Start()
|
||||
{
|
||||
if (_thread != null && _thread.IsAlive) return;
|
||||
@@ -48,13 +52,12 @@ namespace StandardScene.Signal.Plc
|
||||
_thread = new Thread(Loop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = $"PlcStation:{Runtime.Config.JgName}"
|
||||
Name = $"PlcStation:{_station.JgName}"
|
||||
};
|
||||
_thread.Start();
|
||||
Diagnosis.Log($"{Runtime.Config.JgName} Worker 已启动 → {Runtime.Config.Ip}:{Runtime.Config.Port}", "PLC", true);
|
||||
Diagnosis.Log($"{_station.JgName} Worker 已启动 → {_station.Ip}:{_station.Port} 工位{_docks.Count}", "PLC", true);
|
||||
}
|
||||
|
||||
/// <summary>取消循环、最多等 2s、关闭 S7 连接并标断线。</summary>
|
||||
public void Stop()
|
||||
{
|
||||
try { _cts?.Cancel(); }
|
||||
@@ -63,8 +66,9 @@ namespace StandardScene.Signal.Plc
|
||||
catch { }
|
||||
_thread = null;
|
||||
_session.Close();
|
||||
Runtime.IsConnected = false;
|
||||
Diagnosis.Log($"{Runtime.Config.JgName} Worker 已停止", "PLC", true);
|
||||
foreach (var d in _docks)
|
||||
d.IsConnected = false;
|
||||
Diagnosis.Log($"{_station.JgName} Worker 已停止", "PLC", true);
|
||||
}
|
||||
|
||||
public void Dispose() => Stop();
|
||||
@@ -76,14 +80,17 @@ namespace StandardScene.Signal.Plc
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (Runtime.Sync)
|
||||
lock (Sync)
|
||||
Tick();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Runtime.LastError = ex.Message;
|
||||
Runtime.IsConnected = false;
|
||||
LogFail($"{Runtime.Config.JgName} 周期异常 {ex.Message}");
|
||||
foreach (var d in _docks)
|
||||
{
|
||||
d.LastError = ex.Message;
|
||||
d.IsConnected = false;
|
||||
}
|
||||
LogFail($"{_station.JgName} 周期异常 {ex.Message}");
|
||||
}
|
||||
|
||||
try { _onTick?.Invoke(); }
|
||||
@@ -94,17 +101,18 @@ namespace StandardScene.Signal.Plc
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>单拍 PLC 交互。心跳地址为空则跳过心跳;读写地址为空则跳过对应步骤。</summary>
|
||||
internal void Tick()
|
||||
{
|
||||
var cfg = Runtime.Config;
|
||||
Runtime.Heart = !Runtime.Heart;
|
||||
var heart = !_docks.Select(d => d.Heart).FirstOrDefault();
|
||||
foreach (var d in _docks)
|
||||
d.Heart = heart;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cfg.HeartDb))
|
||||
var heartAddr = PlcConfigLoader.ResolveHeartAddress(_station);
|
||||
if (!string.IsNullOrWhiteSpace(heartAddr))
|
||||
{
|
||||
if (_session.TryReadBool(cfg.HeartDb, out var bit))
|
||||
if (_session.TryReadBool(heartAddr, out var bit))
|
||||
{
|
||||
_session.TryWriteBool(cfg.HeartDb, !bit);
|
||||
_session.TryWriteBool(heartAddr, !bit);
|
||||
Thread.Sleep(20);
|
||||
}
|
||||
else
|
||||
@@ -114,56 +122,183 @@ namespace StandardScene.Signal.Plc
|
||||
}
|
||||
}
|
||||
|
||||
var boolDocks = _docks.Where(d => !d.Dock.UseByteIo).ToList();
|
||||
var byteDocks = _docks.Where(d => d.Dock.UseByteIo).ToList();
|
||||
|
||||
if (boolDocks.Count > 0 && !TickBoolDocks(boolDocks))
|
||||
return;
|
||||
|
||||
foreach (var rt in byteDocks)
|
||||
{
|
||||
if (!TickByteDock(rt))
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var rt in _docks)
|
||||
{
|
||||
rt.IsConnected = true;
|
||||
rt.LastError = "";
|
||||
PlcStationLogic.AutoLeave(rt);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TickBoolDocks(List<PlcStationRuntime> docks)
|
||||
{
|
||||
var db = PlcConfigLoader.ResolveDb(_station);
|
||||
var bytes = new List<int>();
|
||||
foreach (var rt in docks)
|
||||
CollectBytes(rt.Dock, bytes);
|
||||
if (bytes.Count == 0)
|
||||
return true;
|
||||
|
||||
var origin = bytes.Min();
|
||||
var last = bytes.Max();
|
||||
var len = (ushort)(last - origin + 1);
|
||||
if (!_session.TryReadBytes(PlcAddress.Byte(db, origin), len, out var buffer) ||
|
||||
buffer == null || buffer.Length < len)
|
||||
{
|
||||
MarkDisconnected("BOOL 块读失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
var changedBytes = new HashSet<int>();
|
||||
foreach (var rt in docks)
|
||||
{
|
||||
var dock = rt.Dock;
|
||||
var allowIn = PlcAddress.GetBit(buffer, origin, dock.AllowInByte, dock.AllowInBit);
|
||||
var allowOut = PlcAddress.GetBit(buffer, origin, dock.AllowOutByte, dock.AllowOutBit);
|
||||
rt.ReadSignal = allowIn
|
||||
? SignalReadValue.允许进入
|
||||
: allowOut
|
||||
? SignalReadValue.允许离开
|
||||
: SignalReadValue.无状态;
|
||||
|
||||
var readWrite = DecodeWrite(buffer, origin, dock);
|
||||
rt.ReadWriteSignal = readWrite;
|
||||
if (readWrite == rt.WriteSignal)
|
||||
rt.WriteSuccess = true;
|
||||
|
||||
if (rt.WriteSignal != readWrite && !rt.WriteSuccess)
|
||||
{
|
||||
Diagnosis.Log(
|
||||
$"AGV→机构 {rt.DisplayName} 写入【{rt.WriteSignal}】请求进入{rt.RequestInAgvNo} 到达{rt.InAgvNo}",
|
||||
"JGControl", true);
|
||||
EncodeWrite(buffer, origin, dock, rt.WriteSignal, changedBytes);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var abs in changedBytes)
|
||||
{
|
||||
var i = abs - origin;
|
||||
if (i < 0 || i >= buffer.Length)
|
||||
continue;
|
||||
if (!_session.TryWriteByte(PlcAddress.Byte(db, abs), buffer[i]))
|
||||
{
|
||||
MarkDisconnected("BOOL 写失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TickByteDock(PlcStationRuntime rt)
|
||||
{
|
||||
var cfg = rt.Dock;
|
||||
if (!string.IsNullOrWhiteSpace(cfg.ReadDb))
|
||||
{
|
||||
if (_session.TryReadByte(cfg.ReadDb, out var rb))
|
||||
Runtime.ReadSignal = PlcStationLogic.ConvertRead(rb);
|
||||
rt.ReadSignal = PlcStationLogic.ConvertRead(rb);
|
||||
else
|
||||
{
|
||||
MarkDisconnected("读地址失败");
|
||||
return;
|
||||
MarkDisconnected($"{rt.DisplayName} 读地址失败");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(cfg.WriteDb))
|
||||
if (string.IsNullOrWhiteSpace(cfg.WriteDb))
|
||||
return true;
|
||||
|
||||
if (!_session.TryReadByte(cfg.WriteDb, out var wb))
|
||||
{
|
||||
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);
|
||||
}
|
||||
MarkDisconnected($"{rt.DisplayName} 写地址回读失败");
|
||||
return false;
|
||||
}
|
||||
|
||||
Runtime.IsConnected = true;
|
||||
Runtime.LastError = "";
|
||||
PlcStationLogic.AutoLeave(Runtime);
|
||||
rt.ReadWriteSignal = PlcStationLogic.ConvertWrite(wb);
|
||||
if (rt.ReadWriteSignal == rt.WriteSignal)
|
||||
rt.WriteSuccess = true;
|
||||
|
||||
if (rt.WriteSignal != rt.ReadWriteSignal && !rt.WriteSuccess)
|
||||
{
|
||||
Diagnosis.Log(
|
||||
$"AGV→机构 {rt.DisplayName} 写入【{rt.WriteSignal}】请求进入{rt.RequestInAgvNo} 到达{rt.InAgvNo}",
|
||||
"JGControl", true);
|
||||
_session.TryWriteByte(cfg.WriteDb, (byte)rt.WriteSignal);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void CollectBytes(PlcStationDockModel dock, List<int> bytes)
|
||||
{
|
||||
if (dock == null) return;
|
||||
bytes.Add(dock.AllowInByte);
|
||||
bytes.Add(dock.AllowOutByte);
|
||||
bytes.Add(dock.EnteringByte);
|
||||
bytes.Add(dock.ArrivedByte);
|
||||
bytes.Add(dock.LeavingByte);
|
||||
bytes.Add(dock.LeftByte);
|
||||
}
|
||||
|
||||
private static SignalWriteValue DecodeWrite(byte[] buffer, int origin, PlcStationDockModel dock)
|
||||
{
|
||||
if (PlcAddress.GetBit(buffer, origin, dock.LeftByte, dock.LeftBit))
|
||||
return SignalWriteValue.已离开;
|
||||
if (PlcAddress.GetBit(buffer, origin, dock.LeavingByte, dock.LeavingBit))
|
||||
return SignalWriteValue.离开中;
|
||||
if (PlcAddress.GetBit(buffer, origin, dock.ArrivedByte, dock.ArrivedBit))
|
||||
return SignalWriteValue.到位;
|
||||
if (PlcAddress.GetBit(buffer, origin, dock.EnteringByte, dock.EnteringBit))
|
||||
return SignalWriteValue.进入中;
|
||||
return SignalWriteValue.无状态;
|
||||
}
|
||||
|
||||
private static void EncodeWrite(
|
||||
byte[] buffer,
|
||||
int origin,
|
||||
PlcStationDockModel dock,
|
||||
SignalWriteValue value,
|
||||
HashSet<int> changedBytes)
|
||||
{
|
||||
void Set(int abs, int bit, bool on)
|
||||
{
|
||||
var before = PlcAddress.GetBit(buffer, origin, abs, bit);
|
||||
if (before == on) return;
|
||||
PlcAddress.SetBit(buffer, origin, abs, bit, on);
|
||||
changedBytes.Add(abs);
|
||||
}
|
||||
|
||||
Set(dock.EnteringByte, dock.EnteringBit, value == SignalWriteValue.进入中);
|
||||
Set(dock.ArrivedByte, dock.ArrivedBit, value == SignalWriteValue.到位);
|
||||
Set(dock.LeavingByte, dock.LeavingBit, value == SignalWriteValue.离开中);
|
||||
Set(dock.LeftByte, dock.LeftBit, value == SignalWriteValue.已离开);
|
||||
}
|
||||
|
||||
private void MarkDisconnected(string reason)
|
||||
{
|
||||
Runtime.IsConnected = false;
|
||||
Runtime.LastError = reason;
|
||||
LogFail($"{Runtime.Config.JgName} {reason}");
|
||||
foreach (var d in _docks)
|
||||
{
|
||||
d.IsConnected = false;
|
||||
d.LastError = reason;
|
||||
}
|
||||
LogFail($"{_station.JgName} {reason}");
|
||||
}
|
||||
|
||||
/// <summary>同一机构失败日志最少间隔 5 秒。</summary>
|
||||
private void LogFail(string msg)
|
||||
{
|
||||
if ((DateTime.Now - _lastFailLog).TotalSeconds < 5) return;
|
||||
if ((DateTime.Now - _lastFailLog).TotalSeconds < 5)
|
||||
return;
|
||||
_lastFailLog = DateTime.Now;
|
||||
Diagnosis.Log(msg, "PLC", true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user