新增 scene.signal 信号交互插件,落地 PLC 握手与扫车放行。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user