磁导航1.0内部交管和信号交互
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
using System;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace StandardScene.Signal.Plc
|
||||
{
|
||||
/// <summary>西门子 DB 字节/位地址拼装与缓冲区位操作。</summary>
|
||||
internal static class PlcAddress
|
||||
{
|
||||
private static readonly Regex Addr = new Regex(
|
||||
@"^DB(\d+)\.(?:DBB|DBX|DBW)?(\d+)(?:\.(\d+))?$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public static string Bit(int db, int absByte, int bit) =>
|
||||
$"DB{Math.Max(1, db)}.DBX{Math.Max(0, absByte)}.{ClampBit(bit)}";
|
||||
|
||||
public static string Byte(int db, int absByte) =>
|
||||
$"DB{Math.Max(1, db)}.DBB{Math.Max(0, absByte)}";
|
||||
|
||||
public static int ClampBit(int bit)
|
||||
{
|
||||
if (bit < 0) return 0;
|
||||
if (bit > 7) return 7;
|
||||
return bit;
|
||||
}
|
||||
|
||||
public static bool TryParse(string address, out int db, out int absByte, out int bit)
|
||||
{
|
||||
db = 0;
|
||||
absByte = 0;
|
||||
bit = 0;
|
||||
if (string.IsNullOrWhiteSpace(address))
|
||||
return false;
|
||||
var m = Addr.Match(address.Trim());
|
||||
if (!m.Success)
|
||||
return false;
|
||||
db = int.Parse(m.Groups[1].Value);
|
||||
absByte = int.Parse(m.Groups[2].Value);
|
||||
if (m.Groups[3].Success)
|
||||
bit = ClampBit(int.Parse(m.Groups[3].Value));
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool GetBit(byte[] buffer, int originByte, int absByte, int bit)
|
||||
{
|
||||
var i = absByte - originByte;
|
||||
if (buffer == null || i < 0 || i >= buffer.Length)
|
||||
return false;
|
||||
return (buffer[i] & (1 << ClampBit(bit))) != 0;
|
||||
}
|
||||
|
||||
public static void SetBit(byte[] buffer, int originByte, int absByte, int bit, bool value)
|
||||
{
|
||||
var i = absByte - originByte;
|
||||
if (buffer == null || i < 0 || i >= buffer.Length)
|
||||
return;
|
||||
var mask = (byte)(1 << ClampBit(bit));
|
||||
if (value)
|
||||
buffer[i] |= mask;
|
||||
else
|
||||
buffer[i] = (byte)(buffer[i] & ~mask);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using StandardScene.Signal.Model;
|
||||
|
||||
namespace StandardScene.Signal.Plc
|
||||
{
|
||||
/// <summary>
|
||||
/// 加载机构 + 工位。没有工位表时,用旧机构行的 ReadDb/WriteDb/地标合成 default 工位(一字节枚举)。
|
||||
/// </summary>
|
||||
internal static class PlcConfigLoader
|
||||
{
|
||||
public const string DefaultDockId = "default";
|
||||
|
||||
public sealed class StationBundle
|
||||
{
|
||||
public PlcStationModel Station { get; init; }
|
||||
public List<PlcStationDockModel> Docks { get; init; }
|
||||
}
|
||||
|
||||
public static List<StationBundle> Load(string stationsPath, string docksPath)
|
||||
{
|
||||
var stations = SignalConfigStore.Load<PlcStationModel>(stationsPath)
|
||||
.Where(s => s != null && !string.IsNullOrWhiteSpace(s.JgName))
|
||||
.ToList();
|
||||
var docks = SignalConfigStore.Load<PlcStationDockModel>(docksPath)
|
||||
.Where(d => d != null && !string.IsNullOrWhiteSpace(d.JgName))
|
||||
.ToList();
|
||||
|
||||
var byName = docks
|
||||
.GroupBy(d => d.JgName.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.ToList(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var list = new List<StationBundle>();
|
||||
foreach (var station in stations)
|
||||
{
|
||||
if (!byName.TryGetValue(station.JgName.Trim(), out var rows) || rows.Count == 0)
|
||||
rows = HasLegacyIo(station) ? new List<PlcStationDockModel> { Synthesize(station) } : new List<PlcStationDockModel>();
|
||||
list.Add(new StationBundle { Station = station, Docks = rows });
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public static int ResolveDb(PlcStationModel station)
|
||||
{
|
||||
if (station == null)
|
||||
return 100;
|
||||
if (station.DbNumber > 0)
|
||||
return station.DbNumber;
|
||||
if (PlcAddress.TryParse(station.HeartDb, out var db, out _, out _))
|
||||
return db;
|
||||
if (PlcAddress.TryParse(station.ReadDb, out db, out _, out _))
|
||||
return db;
|
||||
if (PlcAddress.TryParse(station.WriteDb, out db, out _, out _))
|
||||
return db;
|
||||
return 100;
|
||||
}
|
||||
|
||||
public static string ResolveHeartAddress(PlcStationModel station)
|
||||
{
|
||||
if (station == null)
|
||||
return "";
|
||||
if (!string.IsNullOrWhiteSpace(station.HeartDb))
|
||||
return station.HeartDb.Trim();
|
||||
return PlcAddress.Bit(ResolveDb(station), station.HeartByte, station.HeartBit);
|
||||
}
|
||||
|
||||
private static bool HasLegacyIo(PlcStationModel station) =>
|
||||
station != null &&
|
||||
(!string.IsNullOrWhiteSpace(station.ReadDb) ||
|
||||
!string.IsNullOrWhiteSpace(station.WriteDb) ||
|
||||
station.JgPointId > 0 ||
|
||||
!string.IsNullOrWhiteSpace(station.JgContains));
|
||||
|
||||
private static PlcStationDockModel Synthesize(PlcStationModel station) => new PlcStationDockModel
|
||||
{
|
||||
JgName = station.JgName,
|
||||
DockId = DefaultDockId,
|
||||
UseByteIo = true,
|
||||
ReadDb = station.ReadDb,
|
||||
WriteDb = station.WriteDb,
|
||||
JgInPointId = station.JgInPointId,
|
||||
JgInContains = station.JgInContains,
|
||||
JgPointId = station.JgPointId,
|
||||
JgContains = station.JgContains
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,8 @@ namespace StandardScene.Signal.Plc
|
||||
/// <summary>按 <see cref="PlcStationModel.LineType"/> 分发四套离站逻辑;未知类型不处理。</summary>
|
||||
public static void AutoLeave(PlcStationRuntime rt)
|
||||
{
|
||||
if (rt?.Config == null) return;
|
||||
switch (rt.Config.LineType)
|
||||
if (rt?.Station == null || rt.Dock == null) return;
|
||||
switch (rt.Station.LineType)
|
||||
{
|
||||
case SignalLineType.上线机构:
|
||||
AutoLeaveUpLine(rt);
|
||||
@@ -77,12 +77,12 @@ namespace StandardScene.Signal.Plc
|
||||
if (rt.RequestInAgvNo > 0)
|
||||
{
|
||||
Diagnosis.Log(
|
||||
$"{rt.Config.JgName} {(fromReadback ? "回读已离开" : "写已离开成功")},清空请求进入车 {rt.RequestInAgvNo}",
|
||||
$"{rt.DisplayName} {(fromReadback ? "回读已离开" : "写已离开成功")},清空请求进入车 {rt.RequestInAgvNo}",
|
||||
"JGControl", true);
|
||||
rt.RequestInAgvNo = 0;
|
||||
}
|
||||
else
|
||||
Diagnosis.Log($"{rt.Config.JgName} 已离开,清空信号", "JGControl", true);
|
||||
Diagnosis.Log($"{rt.DisplayName} 已离开,清空信号", "JGControl", true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -102,12 +102,12 @@ namespace StandardScene.Signal.Plc
|
||||
{
|
||||
var car = SignalCarAdapter.Find(rt.RequestInAgvNo);
|
||||
if (car == null) return;
|
||||
if (!ContainsSite(rt.Config.JgInContains, SignalCarAdapter.PositionId(car)))
|
||||
if (!ContainsSite(rt.Dock.JgInContains, SignalCarAdapter.PositionId(car)))
|
||||
{
|
||||
rt.WriteSignal = SignalWriteValue.到位;
|
||||
rt.InAgvNo = car.id;
|
||||
rt.RequestInAgvNo = 0;
|
||||
Diagnosis.Log($"AGV{car.id} 已到达机构 {rt.Config.JgName}", "JGControl", true);
|
||||
Diagnosis.Log($"AGV{car.id} 已到达机构 {rt.DisplayName}", "JGControl", true);
|
||||
}
|
||||
}
|
||||
else if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.到位)
|
||||
@@ -119,7 +119,7 @@ namespace StandardScene.Signal.Plc
|
||||
else if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.离开中)
|
||||
{
|
||||
var car = SignalCarAdapter.Find(rt.InAgvNo);
|
||||
if (car == null || !ContainsSite(rt.Config.JgContains, SignalCarAdapter.PositionId(car)))
|
||||
if (car == null || !ContainsSite(rt.Dock.JgContains, SignalCarAdapter.PositionId(car)))
|
||||
rt.WriteSignal = SignalWriteValue.已离开;
|
||||
}
|
||||
}
|
||||
@@ -145,7 +145,7 @@ namespace StandardScene.Signal.Plc
|
||||
if (car == null) return;
|
||||
|
||||
var pos = SignalCarAdapter.PositionId(car);
|
||||
if (rt.ReadSignal == SignalReadValue.允许离开 && pos == rt.Config.JgPointId)
|
||||
if (rt.ReadSignal == SignalReadValue.允许离开 && pos == rt.Dock.JgPointId)
|
||||
{
|
||||
if (rt.WriteSignal == SignalWriteValue.离开中) return;
|
||||
var pinOk = SignalCarAdapter.IsActionFinish(car, "SetPin1Up")
|
||||
@@ -156,7 +156,7 @@ namespace StandardScene.Signal.Plc
|
||||
SignalCarAdapter.StopCharge(car);
|
||||
SignalCarAdapter.Start(car);
|
||||
rt.WriteSignal = SignalWriteValue.离开中;
|
||||
Diagnosis.Log($"{rt.Config.JgName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
|
||||
Diagnosis.Log($"{rt.DisplayName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -165,8 +165,8 @@ namespace StandardScene.Signal.Plc
|
||||
SignalCarAdapter.Control(car, "StopCharge");
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(rt.Config.JgContains)
|
||||
&& !ContainsSite(rt.Config.JgContains, pos)
|
||||
else if (!string.IsNullOrEmpty(rt.Dock.JgContains)
|
||||
&& !ContainsSite(rt.Dock.JgContains, pos)
|
||||
&& rt.ReadWriteSignal != SignalWriteValue.已离开)
|
||||
{
|
||||
rt.WriteSignal = SignalWriteValue.已离开;
|
||||
@@ -193,20 +193,20 @@ namespace StandardScene.Signal.Plc
|
||||
if (car == null) return;
|
||||
|
||||
var pos = SignalCarAdapter.PositionId(car);
|
||||
if (rt.ReadSignal == SignalReadValue.允许离开 && pos == rt.Config.JgPointId)
|
||||
if (rt.ReadSignal == SignalReadValue.允许离开 && pos == rt.Dock.JgPointId)
|
||||
{
|
||||
if (rt.WriteSignal == SignalWriteValue.离开中) return;
|
||||
if (SignalCarAdapter.IsActionFinish(car, "StopCharge"))
|
||||
{
|
||||
SignalCarAdapter.Start(car);
|
||||
rt.WriteSignal = SignalWriteValue.离开中;
|
||||
Diagnosis.Log($"{rt.Config.JgName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
|
||||
Diagnosis.Log($"{rt.DisplayName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
|
||||
}
|
||||
else
|
||||
SignalCarAdapter.Control(car, "StopCharge");
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(rt.Config.JgContains)
|
||||
&& !ContainsSite(rt.Config.JgContains, pos)
|
||||
else if (!string.IsNullOrEmpty(rt.Dock.JgContains)
|
||||
&& !ContainsSite(rt.Dock.JgContains, pos)
|
||||
&& rt.ReadWriteSignal != SignalWriteValue.已离开)
|
||||
{
|
||||
rt.WriteSignal = SignalWriteValue.已离开;
|
||||
|
||||
@@ -5,91 +5,88 @@ using StandardScene.Signal.Model;
|
||||
namespace StandardScene.Signal.Plc
|
||||
{
|
||||
/// <summary>
|
||||
/// 单个机构的运行态(不进 JSON)。扫车线程改写目标写字节,PLC Worker 负责真正下发。
|
||||
/// 单个工位运行态(不进 JSON)。扫车改写目标写状态,PLC Worker 负责下发 BOOL(或旧一字节枚举)。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 赋值 <see cref="WriteSignal"/> 会把 <see cref="WriteSuccess"/> 置 false,下一拍 Worker 发现
|
||||
/// 目标与回读不一致就会重写 PLC。回读与目标一致后再标成功,AutoLeave 才继续往下一状态走。
|
||||
/// 所有读写应在 <see cref="Sync"/> 内进行。
|
||||
/// </remarks>
|
||||
public sealed class PlcStationRuntime
|
||||
{
|
||||
private SignalWriteValue _writeSignal;
|
||||
private SignalReadValue _readSignal;
|
||||
private SignalWriteValue _readWriteSignal;
|
||||
|
||||
public PlcStationRuntime(PlcStationModel config)
|
||||
public PlcStationRuntime(PlcStationModel station, PlcStationDockModel dock, object sync = null)
|
||||
{
|
||||
Config = config ?? throw new ArgumentNullException(nameof(config));
|
||||
Station = station ?? throw new ArgumentNullException(nameof(station));
|
||||
Dock = dock ?? throw new ArgumentNullException(nameof(dock));
|
||||
Sync = sync ?? new object();
|
||||
}
|
||||
|
||||
/// <summary>本机构静态配置(IP、DB 地址、地标、机构类型)。</summary>
|
||||
public PlcStationModel Config { get; }
|
||||
public PlcStationModel Station { get; }
|
||||
|
||||
/// <summary>扫车线程与 PLC Worker 共用运行态,写入时加此锁。</summary>
|
||||
public object Sync { get; } = new object();
|
||||
public PlcStationDockModel Dock { get; }
|
||||
|
||||
/// <summary>兼容旧代码:机构连接配置(不含工位地标)。</summary>
|
||||
public PlcStationModel Config => Station;
|
||||
|
||||
public object Sync { get; }
|
||||
|
||||
public string JgName => Station.JgName ?? "";
|
||||
|
||||
public string DockId => Dock.ResolvedDockId;
|
||||
|
||||
public string DisplayName =>
|
||||
string.IsNullOrEmpty(DockId) || string.Equals(DockId, PlcConfigLoader.DefaultDockId, StringComparison.OrdinalIgnoreCase)
|
||||
? JgName
|
||||
: $"{JgName}/{DockId}";
|
||||
|
||||
/// <summary>PLC→调度:允许进入 / 允许离开 / 无状态。变化时打诊断。</summary>
|
||||
public SignalReadValue ReadSignal
|
||||
{
|
||||
get => _readSignal;
|
||||
set
|
||||
{
|
||||
if (_readSignal != value)
|
||||
Diagnosis.Log($"{Config.JgName} 读信号 {_readSignal} → {value}", "PLC", true);
|
||||
Diagnosis.Log($"{DisplayName} 读信号 {_readSignal} → {value}", "PLC", true);
|
||||
_readSignal = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调度→PLC 的目标写字节。赋值即视为尚未写成功,驱动 Worker 重写。
|
||||
/// </summary>
|
||||
public SignalWriteValue WriteSignal
|
||||
{
|
||||
get => _writeSignal;
|
||||
set
|
||||
{
|
||||
if (_writeSignal != value)
|
||||
Diagnosis.Log($"{Config.JgName} 写信号 {_writeSignal} → {value}", "PLC", true);
|
||||
Diagnosis.Log($"{DisplayName} 写信号 {_writeSignal} → {value}", "PLC", true);
|
||||
_writeSignal = value;
|
||||
WriteSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从写地址回读到的当前值,用来确认 PLC 是否已吃进目标字节。</summary>
|
||||
public SignalWriteValue ReadWriteSignal
|
||||
{
|
||||
get => _readWriteSignal;
|
||||
set
|
||||
{
|
||||
if (_readWriteSignal != value)
|
||||
Diagnosis.Log($"{Config.JgName} 回读写值 {_readWriteSignal} → {value}", "PLC", true);
|
||||
Diagnosis.Log($"{DisplayName} 回读写值 {_readWriteSignal} → {value}", "PLC", true);
|
||||
_readWriteSignal = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>已报到在机构内的 AGV 号;0 表示没有。Arrived / 上线自动到位 时写入。</summary>
|
||||
public int InAgvNo { get; set; }
|
||||
|
||||
/// <summary>已获准进入、正在往机构开的 AGV 号;到位或离站清空后为 0。</summary>
|
||||
public int RequestInAgvNo { get; set; }
|
||||
|
||||
/// <summary>最近一拍 PLC 读写是否成功。心跳或读写失败会立刻变 false。</summary>
|
||||
public bool IsConnected { get; set; }
|
||||
|
||||
/// <summary>目标写字节是否已与 PLC 回读一致。改 WriteSignal 时自动清掉。</summary>
|
||||
public bool WriteSuccess { get; set; }
|
||||
|
||||
/// <summary>内存侧心跳翻转标志,供监视面板看到 Worker 在跑;真正写 PLC 的是 HeartDb 位翻转。</summary>
|
||||
public bool Heart { get; set; }
|
||||
|
||||
/// <summary>最近一次断线或周期异常原因,连接正常时清空。</summary>
|
||||
public string LastError { get; set; } = "";
|
||||
|
||||
/// <summary>拷贝一份给 MissionStatus / 监视列表,避免 UI 直接绑运行态。</summary>
|
||||
public PlcStationSnapshot ToSnapshot() => new PlcStationSnapshot
|
||||
{
|
||||
JgName = Config.JgName,
|
||||
JgName = DisplayName,
|
||||
IsConnected = IsConnected,
|
||||
ReadSignal = ReadSignal.ToString(),
|
||||
WriteSignal = WriteSignal.ToString(),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace StandardScene.Signal.Plc
|
||||
private SiemensClient _client;
|
||||
private readonly object _sync = new object();
|
||||
|
||||
/// <param name="version">进程字段 PlcVersion,如 S7_1500。</param>
|
||||
/// <param name="version">西门子系列名,如 S7_1500;空或无法解析时默认 S7_1500。</param>
|
||||
/// <param name="ip">机构 IP。</param>
|
||||
/// <param name="port">S7 端口,通常 102。</param>
|
||||
/// <param name="slot">机架槽号,S7-1500 常见 1。</param>
|
||||
@@ -103,6 +103,26 @@ namespace StandardScene.Signal.Plc
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>从起始字节连续读 length 字节,用于工位 BOOL 拆位。</summary>
|
||||
public bool TryReadBytes(string startAddress, ushort length, out byte[] data)
|
||||
{
|
||||
data = Array.Empty<byte>();
|
||||
if (string.IsNullOrWhiteSpace(startAddress) || length == 0 || !EnsureOpen())
|
||||
return false;
|
||||
try
|
||||
{
|
||||
var r = _client.Read(NormalizeByteAddress(startAddress), length, false);
|
||||
if (r == null || !r.IsSucceed || r.Value == null)
|
||||
return false;
|
||||
data = r.Value;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>读心跳位。地址不做 DBB 转换。</summary>
|
||||
public bool TryReadBool(string address, out bool value)
|
||||
{
|
||||
|
||||
@@ -224,6 +224,13 @@ namespace StandardScene.Signal.Plc
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>类型名恰好为 MagCar(不含 Mag2Car)。磁条交管只放行这种车。</summary>
|
||||
public static bool IsMagCar(Car car)
|
||||
{
|
||||
if (car == null) return false;
|
||||
return string.Equals(car.GetType().Name, "MagCar", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>放行:Mag2 → Mag → SendStart(0)。</summary>
|
||||
public static void Start(Car car)
|
||||
{
|
||||
@@ -233,6 +240,15 @@ namespace StandardScene.Signal.Plc
|
||||
TryInvoke(car, "SendStart", new object[] { (ushort)0 });
|
||||
}
|
||||
|
||||
/// <summary>仅 MagCar 发 FASS 1.0 启动(0x01)。不调 StartMag2Car。</summary>
|
||||
public static void StartMagCar(Car car)
|
||||
{
|
||||
if (!IsMagCar(car)) return;
|
||||
if (TryInvoke(car, "StartMagCar"))
|
||||
return;
|
||||
TryInvoke(car, "SendStart", new object[] { (ushort)0 });
|
||||
}
|
||||
|
||||
/// <summary>急停:Mag2 → Mag → SendStop(0)。机构未允许进入时调用。</summary>
|
||||
public static void Stop(Car car)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user