磁导航1.0内部交管和信号交互

This commit is contained in:
ykkokluo
2026-08-24 14:55:35 +08:00
parent cacc5fea5c
commit 244b0af6c8
60 changed files with 6395 additions and 294 deletions
+63
View File
@@ -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
};
}
}
+15 -15
View File
@@ -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.;
+25 -28
View File
@@ -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(),
+199 -64
View File
@@ -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);
}
+21 -1
View File
@@ -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)
{