新增 scene.signal 信号交互插件,落地 PLC 握手与扫车放行。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ykkokluo
2026-08-14 14:43:11 +08:00
co-authored by Cursor
parent 1e780c2b65
commit 12c7c1d518
29 changed files with 2425 additions and 1 deletions
+183
View File
@@ -0,0 +1,183 @@
using System;
using System.Linq;
using SimpleCore.Library;
using StandardScene.Signal.Model;
namespace StandardScene.Signal.Plc
{
/// <summary>读写字节转换与四套自动离站,对齐反编译 PLCManager。</summary>
public static class PlcStationLogic
{
public static SignalReadValue ConvertRead(byte val) => val switch
{
1 => SignalReadValue.,
2 => SignalReadValue.,
_ => SignalReadValue.
};
public static SignalWriteValue ConvertWrite(byte val) => val switch
{
1 => SignalWriteValue.,
2 => SignalWriteValue.,
3 => SignalWriteValue.,
4 => SignalWriteValue.,
_ => SignalWriteValue.
};
public static bool ContainsSite(string csv, int siteId)
{
if (string.IsNullOrWhiteSpace(csv) || siteId <= 0)
return false;
return csv.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Any(s => int.TryParse(s.Trim(), out var n) && n == siteId);
}
public static void AutoLeave(PlcStationRuntime rt)
{
if (rt?.Config == null) return;
switch (rt.Config.LineType)
{
case SignalLineType.线:
AutoLeaveUpLine(rt);
break;
case SignalLineType.线:
AutoLeaveDownLine(rt, pin2: false);
break;
case SignalLineType.线:
AutoLeaveDownLine(rt, pin2: true);
break;
case SignalLineType.:
AutoLeaveWaitSit(rt);
break;
}
}
private static void ClearLeft(PlcStationRuntime rt, bool fromReadback)
{
rt.WriteSignal = SignalWriteValue.;
rt.InAgvNo = 0;
if (rt.RequestInAgvNo > 0)
{
Diagnosis.Log(
$"{rt.Config.JgName} {(fromReadback ? "" : "")},清空请求进入车 {rt.RequestInAgvNo}",
"JGControl", true);
rt.RequestInAgvNo = 0;
}
else
Diagnosis.Log($"{rt.Config.JgName} 已离开,清空信号", "JGControl", true);
}
private static void AutoLeaveUpLine(PlcStationRuntime rt)
{
if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.)
ClearLeft(rt, fromReadback: false);
if (rt.ReadWriteSignal == SignalWriteValue.)
ClearLeft(rt, fromReadback: true);
if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.)
{
var car = SignalCarAdapter.Find(rt.RequestInAgvNo);
if (car == null) return;
if (!ContainsSite(rt.Config.JgInContains, SignalCarAdapter.PositionId(car)))
{
rt.WriteSignal = SignalWriteValue.;
rt.InAgvNo = car.id;
rt.RequestInAgvNo = 0;
Diagnosis.Log($"AGV{car.id} 已到达机构 {rt.Config.JgName}", "JGControl", true);
}
}
else if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.)
{
var car = SignalCarAdapter.Find(rt.InAgvNo);
if (car == null || SignalCarAdapter.IsRunning(car))
rt.WriteSignal = SignalWriteValue.;
}
else if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.)
{
var car = SignalCarAdapter.Find(rt.InAgvNo);
if (car == null || !ContainsSite(rt.Config.JgContains, SignalCarAdapter.PositionId(car)))
rt.WriteSignal = SignalWriteValue.;
}
}
private static void AutoLeaveDownLine(PlcStationRuntime rt, bool pin2)
{
if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.)
{
ClearLeft(rt, fromReadback: false);
return;
}
if (rt.ReadWriteSignal == SignalWriteValue.)
ClearLeft(rt, fromReadback: true);
if (rt.InAgvNo == 0) return;
var car = SignalCarAdapter.Find(rt.InAgvNo);
if (car == null) return;
var pos = SignalCarAdapter.PositionId(car);
if (rt.ReadSignal == SignalReadValue. && pos == rt.Config.JgPointId)
{
if (rt.WriteSignal == SignalWriteValue.) return;
var pinOk = SignalCarAdapter.IsActionFinish(car, "SetPin1Up")
&& (!pin2 || SignalCarAdapter.IsActionFinish(car, "SetPin2Up"))
&& SignalCarAdapter.IsActionFinish(car, "StopCharge");
if (pinOk)
{
SignalCarAdapter.StopCharge(car);
SignalCarAdapter.Start(car);
rt.WriteSignal = SignalWriteValue.;
Diagnosis.Log($"{rt.Config.JgName} 允许离开,通知 AGV{car.id} 启动,写离开中", "JGControl", true);
}
else
{
SignalCarAdapter.Control(car, "SetPin1Up");
if (pin2) SignalCarAdapter.Control(car, "SetPin2Up");
SignalCarAdapter.Control(car, "StopCharge");
}
}
else if (!string.IsNullOrEmpty(rt.Config.JgContains)
&& !ContainsSite(rt.Config.JgContains, pos)
&& rt.ReadWriteSignal != SignalWriteValue.)
{
rt.WriteSignal = SignalWriteValue.;
}
}
private static void AutoLeaveWaitSit(PlcStationRuntime rt)
{
if (rt.WriteSuccess && rt.WriteSignal == SignalWriteValue.)
{
ClearLeft(rt, fromReadback: false);
return;
}
if (rt.ReadWriteSignal == SignalWriteValue.)
ClearLeft(rt, fromReadback: true);
if (rt.InAgvNo == 0) return;
var car = SignalCarAdapter.Find(rt.InAgvNo);
if (car == null) return;
var pos = SignalCarAdapter.PositionId(car);
if (rt.ReadSignal == SignalReadValue. && pos == rt.Config.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);
}
else
SignalCarAdapter.Control(car, "StopCharge");
}
else if (!string.IsNullOrEmpty(rt.Config.JgContains)
&& !ContainsSite(rt.Config.JgContains, pos)
&& rt.ReadWriteSignal != SignalWriteValue.)
{
rt.WriteSignal = SignalWriteValue.;
}
}
}
}
@@ -0,0 +1,79 @@
using System;
using SimpleCore.Library;
using StandardScene.Signal.Model;
namespace StandardScene.Signal.Plc
{
/// <summary>机构运行态。写信号赋值会清 WriteSuccess,驱动 Worker 重写 PLC。</summary>
public sealed class PlcStationRuntime
{
private SignalWriteValue _writeSignal;
private SignalReadValue _readSignal;
private SignalWriteValue _readWriteSignal;
public PlcStationRuntime(PlcStationModel config)
{
Config = config ?? throw new ArgumentNullException(nameof(config));
}
public PlcStationModel Config { get; }
/// <summary>扫车线程与 PLC Worker 共用运行态,写入时加此锁。</summary>
public object Sync { get; } = new object();
public SignalReadValue ReadSignal
{
get => _readSignal;
set
{
if (_readSignal != value)
Diagnosis.Log($"{Config.JgName} 读信号 {_readSignal} → {value}", "PLC", true);
_readSignal = value;
}
}
public SignalWriteValue WriteSignal
{
get => _writeSignal;
set
{
if (_writeSignal != value)
Diagnosis.Log($"{Config.JgName} 写信号 {_writeSignal} → {value}", "PLC", true);
_writeSignal = value;
WriteSuccess = false;
}
}
public SignalWriteValue ReadWriteSignal
{
get => _readWriteSignal;
set
{
if (_readWriteSignal != value)
Diagnosis.Log($"{Config.JgName} 回读写值 {_readWriteSignal} → {value}", "PLC", true);
_readWriteSignal = value;
}
}
public int InAgvNo { get; set; }
public int RequestInAgvNo { get; set; }
public bool IsConnected { get; set; }
public bool WriteSuccess { get; set; }
public bool Heart { get; set; }
public string LastError { get; set; } = "";
public PlcStationSnapshot ToSnapshot() => new PlcStationSnapshot
{
JgName = Config.JgName,
IsConnected = IsConnected,
ReadSignal = ReadSignal.ToString(),
WriteSignal = WriteSignal.ToString(),
ReadWriteSignal = ReadWriteSignal.ToString(),
InAgvNo = InAgvNo,
RequestInAgvNo = RequestInAgvNo,
WriteSuccess = WriteSuccess,
Heart = Heart,
LastError = LastError ?? ""
};
}
}
@@ -0,0 +1,155 @@
using System;
using System.Threading;
using SimpleCore.Library;
using StandardScene.Signal.Model;
namespace StandardScene.Signal.Plc
{
/// <summary>
/// 单机构 200ms 循环:心跳、读允许进出、回读写值、按需写入、自动离站。
/// 对齐反编译 <c>PLCManager.ExcuteThreadMethod</c>。
/// </summary>
public sealed class PlcStationWorker : IDisposable
{
private readonly SiemensPlcSession _session;
private readonly int _pollMs;
private readonly Action _onTick;
private CancellationTokenSource _cts;
private Thread _thread;
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;
}
public PlcStationRuntime Runtime { get; }
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);
}
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; }
}
}
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}");
}
private void LogFail(string msg)
{
if ((DateTime.Now - _lastFailLog).TotalSeconds < 5) return;
_lastFailLog = DateTime.Now;
Diagnosis.Log(msg, "PLC", true);
}
}
}
@@ -0,0 +1,152 @@
using System;
using System.Text.RegularExpressions;
using IoTClient.Clients.PLC;
using IoTClient.Common.Enums;
namespace StandardScene.Signal.Plc
{
/// <summary>西门子 S7 长连接。地址兼容 Hsl 的 DB1.0 与 IoTClient 的 DB1.DBB0。</summary>
public sealed class SiemensPlcSession : IDisposable
{
private static readonly Regex BareDbByte = new Regex(@"^DB(\d+)\.(\d+)$", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private readonly SiemensVersion _version;
private readonly string _ip;
private readonly int _port;
private readonly byte _slot;
private SiemensClient _client;
private readonly object _sync = new object();
public SiemensPlcSession(string version, string ip, int port, int slot)
{
_version = ParseVersion(version);
_ip = ip ?? "";
_port = port > 0 ? port : 102;
_slot = (byte)Math.Max(0, slot);
}
public bool Connected
{
get
{
lock (_sync) return _client?.Connected == true;
}
}
public bool EnsureOpen()
{
lock (_sync)
{
try
{
if (_client == null)
_client = new SiemensClient(_version, _ip, _port, _slot);
if (_client.Connected)
return true;
var r = _client.Open();
return r != null && r.IsSucceed;
}
catch
{
return false;
}
}
}
public bool TryReadByte(string address, out byte value)
{
value = 0;
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
return false;
try
{
var r = _client.ReadByte(NormalizeByteAddress(address));
if (r == null || !r.IsSucceed)
return false;
value = r.Value;
return true;
}
catch
{
return false;
}
}
public bool TryWriteByte(string address, byte value)
{
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
return false;
try
{
var r = _client.Write(NormalizeByteAddress(address), value);
return r != null && r.IsSucceed;
}
catch
{
return false;
}
}
public bool TryReadBool(string address, out bool value)
{
value = false;
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
return false;
try
{
var r = _client.ReadBoolean(address.Trim());
if (r == null || !r.IsSucceed)
return false;
value = r.Value;
return true;
}
catch
{
return false;
}
}
public bool TryWriteBool(string address, bool value)
{
if (string.IsNullOrWhiteSpace(address) || !EnsureOpen())
return false;
try
{
var r = _client.Write(address.Trim(), value);
return r != null && r.IsSucceed;
}
catch
{
return false;
}
}
public void Close()
{
lock (_sync)
{
try { _client?.Close(); }
catch { }
_client = null;
}
}
public void Dispose() => Close();
internal static string NormalizeByteAddress(string address)
{
var addr = address.Trim();
var m = BareDbByte.Match(addr);
return m.Success ? $"DB{m.Groups[1].Value}.DBB{m.Groups[2].Value}" : addr;
}
internal static SiemensVersion ParseVersion(string version)
{
if (string.IsNullOrWhiteSpace(version))
return SiemensVersion.S7_1500;
return Enum.TryParse(version.Replace("-", "_"), true, out SiemensVersion parsed)
? parsed
: SiemensVersion.S7_1500;
}
}
}
@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using SimpleCore;
using SimpleCore.PropType;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using StandardScene.Signal.Site;
namespace StandardScene.Signal.Plc
{
/// <summary>
/// 不引用 Magnetic.dll:按方法名反射 Start/Stop/牵引棒。
/// MagCar / Mag2Car 有对应 MethodMember 时生效;没有则视为动作已完成。
/// </summary>
public static class SignalCarAdapter
{
public static IEnumerable<Car> All()
{
try
{
return SimpleLib.GetAllCars().OfType<Car>();
}
catch
{
return Array.Empty<Car>();
}
}
public static Car Find(int id)
{
if (id <= 0) return null;
return SimpleLib.GetCar(id) as Car;
}
public static int PositionId(Car car)
{
if (car == null) return 0;
try
{
if (car.siteID > 0) return car.siteID;
var last = car.GetLastSite();
return last > 0 ? last : 0;
}
catch
{
return 0;
}
}
public static string GetState(Car car)
{
if (car?.status?.enums != null &&
car.status.enums.TryGetValue("State", out var state) &&
!string.IsNullOrWhiteSpace(state))
return state;
return "";
}
public static bool IsMoving(Car car)
{
var state = GetState(car);
return state == "运行中" ||
string.Equals(state, "Running", StringComparison.OrdinalIgnoreCase);
}
/// <summary>对齐反编译 RunStatus==停止:非运行中即视为停稳,含充电中。</summary>
public static bool IsStopped(Car car)
{
if (car == null) return false;
if (IsMoving(car)) return false;
var state = GetState(car);
if (state == "停止中" || state == "充电中" ||
string.Equals(state, "Stopping", StringComparison.OrdinalIgnoreCase) ||
string.Equals(state, "Charging", StringComparison.OrdinalIgnoreCase) ||
string.Equals(state, "WaitPass", StringComparison.OrdinalIgnoreCase))
return true;
return !IsRunning(car);
}
public static bool IsCharging(Car car)
{
var state = GetState(car);
return state == "充电中" ||
string.Equals(state, "Charging", StringComparison.OrdinalIgnoreCase);
}
public static bool IsRunning(Car car)
{
if (car == null) return false;
if (IsMoving(car)) return true;
if (car.status?.pendingLocks != null && car.status.pendingLocks.Length > 0)
return true;
if (car.tags != null && car.tags.Contains("occupied"))
return true;
return false;
}
public static double Soc(Car car)
{
if (car?.status?.enums != null &&
car.status.enums.TryGetValue("Soc", out var soc) &&
double.TryParse(soc, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
return value;
return 0;
}
public static string GroupId(Car car)
{
if (car?.fields == null) return "";
if (car.fields.TryGetValue("group", out var g) && !string.IsNullOrWhiteSpace(g))
return g;
if (car.fields.TryGetValue("GroupID", out var g2) && !string.IsNullOrWhiteSpace(g2))
return g2;
return "";
}
public static IReadOnlyList<Car> CarsAt(IEnumerable<int> siteIds)
{
var set = new HashSet<int>(siteIds ?? Array.Empty<int>());
if (set.Count == 0)
return Array.Empty<Car>();
return All().Where(c => set.Contains(PositionId(c))).ToList();
}
public static IReadOnlyList<Car> CarsAt(int siteId)
{
if (siteId <= 0) return Array.Empty<Car>();
return CarsAt(new[] { siteId });
}
public static bool TryReadSiteField(int siteId, string key, out string value)
{
value = "";
if (siteId <= 0 || string.IsNullOrWhiteSpace(key))
return false;
try
{
var site = SimpleLib.GetSite(siteId);
if (site?.fields == null)
return false;
if (!site.fields.TryGetValue(key, out var raw) || string.IsNullOrWhiteSpace(raw))
return false;
value = raw;
return true;
}
catch
{
return false;
}
}
public static bool TryReadSignalStop(int siteId, out string signalType, out string signalParams)
{
signalType = "";
signalParams = "";
if (!TryReadSiteField(siteId, SignalSiteFields.SignalType, out signalType))
return false;
TryReadSiteField(siteId, SignalSiteFields.SignalParams, out signalParams);
return true;
}
public static int ChargeSeconds(Car car, IDictionary<int, DateTime> startedAt)
{
if (car == null) return 0;
var mi = car.GetType().GetMethod("GetChargeTime", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (mi != null)
{
try
{
var r = mi.Invoke(car, null);
if (r is int i) return i;
if (r is double d) return (int)d;
if (r is TimeSpan ts) return (int)ts.TotalSeconds;
}
catch
{
}
}
if (IsCharging(car))
{
if (!startedAt.ContainsKey(car.id))
startedAt[car.id] = DateTime.Now;
return (int)(DateTime.Now - startedAt[car.id]).TotalSeconds;
}
startedAt.Remove(car.id);
return 0;
}
public static void Start(Car car)
{
if (car == null) return;
if (TryInvoke(car, "StartMag2Car") || TryInvoke(car, "StartMagCar"))
return;
TryInvoke(car, "SendStart", new object[] { (ushort)0 });
}
public static void Stop(Car car)
{
if (car == null) return;
if (TryInvoke(car, "StopMag2Car") || TryInvoke(car, "StopMagCar"))
return;
TryInvoke(car, "SendStop", new object[] { (ushort)0 });
}
public static void StopCharge(Car car)
{
if (car == null) return;
TryInvoke(car, "StopCharge");
}
public static bool IsActionFinish(Car car, string action)
{
if (car == null) return false;
if (string.IsNullOrWhiteSpace(action)) return true;
var mi = car.GetType().GetMethod("IsActionFinish", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (mi == null)
return true;
try
{
var r = mi.Invoke(car, new object[] { action });
return r is true;
}
catch
{
return true;
}
}
public static void Control(Car car, string action)
{
if (car == null || string.IsNullOrWhiteSpace(action)) return;
if (TryInvoke(car, action)) return;
TryInvoke(car, "ImmediateCommand", new object[] { action });
}
private static bool TryInvoke(object target, string name, object[] args = null)
{
var type = target.GetType();
var methods = type.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
.Where(m => string.Equals(m.Name, name, StringComparison.Ordinal));
foreach (var mi in methods)
{
var ps = mi.GetParameters();
if (args == null)
{
if (ps.Length != 0) continue;
try { mi.Invoke(target, null); return true; }
catch { return false; }
}
if (ps.Length != args.Length) continue;
try { mi.Invoke(target, args); return true; }
catch { return false; }
}
return false;
}
}
}