2026-08-14 14:43:11 +08:00
|
|
|
|
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>
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// 信号插件访问 AGV / 站点的适配层。不引用 Magnetic.dll,按方法名反射启停和牵引棒。
|
2026-08-14 14:43:11 +08:00
|
|
|
|
/// </summary>
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// MagCar / Mag2Car 上有对应 MethodMember(StartMag2Car、SetPin1Up 等)时生效;
|
|
|
|
|
|
/// 没有 <c>IsActionFinish</c> 则视为动作已完成,避免非磁导航车卡在销钉等待。
|
|
|
|
|
|
/// 启停回退顺序:StartMag2Car / StartMagCar → SendStart(0)。
|
|
|
|
|
|
/// </remarks>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public static class SignalCarAdapter
|
|
|
|
|
|
{
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>当前内核里的全部车;SimpleLib 异常时返回空,避免扫车线程崩掉。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public static IEnumerable<Car> All()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
return SimpleLib.GetAllCars().OfType<Car>();
|
|
|
|
|
|
}
|
|
|
|
|
|
catch
|
|
|
|
|
|
{
|
|
|
|
|
|
return Array.Empty<Car>();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>按车号取车;id≤0 返回 null。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public static Car Find(int id)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (id <= 0) return null;
|
|
|
|
|
|
return SimpleLib.GetCar(id) as Car;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 当前站点:优先 <c>siteID</c>,否则 <c>GetLastSite()</c>。
|
|
|
|
|
|
/// 握手 / 放行 / 限流都用这个号去对 JSON 里的 Sit。
|
|
|
|
|
|
/// </summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>读车 status.enums["State"],没有则空串。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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 "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>State 为「运行中」或 Running。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public static bool IsMoving(Car car)
|
|
|
|
|
|
{
|
|
|
|
|
|
var state = GetState(car);
|
|
|
|
|
|
return state == "运行中" ||
|
|
|
|
|
|
string.Equals(state, "Running", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 对齐反编译 <c>RunStatus==停止</c>:非运行中即视为可握手,含充电中 / 停止中 / WaitPass。
|
|
|
|
|
|
/// 充电点必须把「充电中」算停稳,否则 TimedCharging 不会进 Handler。
|
|
|
|
|
|
/// </summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>正在充电(中英文 State)。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public static bool IsCharging(Car car)
|
|
|
|
|
|
{
|
|
|
|
|
|
var state = GetState(car);
|
|
|
|
|
|
return state == "充电中" ||
|
|
|
|
|
|
string.Equals(state, "Charging", StringComparison.OrdinalIgnoreCase);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 车仍占资源:运行中,或有 pendingLocks,或 tags 含 occupied。
|
|
|
|
|
|
/// 上线机构「到位后开始离开」用这个判断,比单纯看 State 更早切到离开中。
|
|
|
|
|
|
/// </summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>电量百分比,读 enums["Soc"];解析失败当 0。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>限流用的车组:fields["group"] 或 ["GroupID"]。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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 "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>当前停在给定站点集合上的全部车(按 PositionId)。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>当前停在单个站点上的车。下线 RequestIn 用 JgPointId 查机构内是否已有车。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public static IReadOnlyList<Car> CarsAt(int siteId)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (siteId <= 0) return Array.Empty<Car>();
|
|
|
|
|
|
return CarsAt(new[] { siteId });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>读站点 fields 指定键。站点不存在或值为空返回 false。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 地图点是否自带 SignalStop(SignalType 字段存在且非空)。
|
|
|
|
|
|
/// SignalParams 可缺省。有类型则扫车不再查 handshake-points.json。
|
|
|
|
|
|
/// </summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 已充电秒数。优先反射 GetChargeTime(int/double/TimeSpan);
|
|
|
|
|
|
/// 没有则从首次看到充电中的时刻算,离开充电态时清字典。
|
|
|
|
|
|
/// </summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 14:55:35 +08:00
|
|
|
|
/// <summary>类型名恰好为 MagCar(不含 Mag2Car)。磁条交管只放行这种车。</summary>
|
|
|
|
|
|
public static bool IsMagCar(Car car)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (car == null) return false;
|
|
|
|
|
|
return string.Equals(car.GetType().Name, "MagCar", StringComparison.Ordinal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>放行:Mag2 → Mag → SendStart(0)。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public static void Start(Car car)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (car == null) return;
|
|
|
|
|
|
if (TryInvoke(car, "StartMag2Car") || TryInvoke(car, "StartMagCar"))
|
2026-08-24 14:55:35 +08:00
|
|
|
|
return;
|
|
|
|
|
|
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"))
|
2026-08-14 14:43:11 +08:00
|
|
|
|
return;
|
|
|
|
|
|
TryInvoke(car, "SendStart", new object[] { (ushort)0 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>急停:Mag2 → Mag → SendStop(0)。机构未允许进入时调用。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>停充。握手放行前通常先停充再启动。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
public static void StopCharge(Car car)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (car == null) return;
|
|
|
|
|
|
TryInvoke(car, "StopCharge");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 销钉/停充等动作是否完成。车类型没有 IsActionFinish 时返回 true(非磁导航不阻塞)。
|
|
|
|
|
|
/// 反射异常也当完成,避免把产线卡死。
|
|
|
|
|
|
/// </summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>下发动作:先按动作名直接调 MethodMember,没有再 ImmediateCommand(action)。</summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 15:20:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 按名称找实例方法。无参与带参分开匹配,重载参数个数不对就跳过。
|
|
|
|
|
|
/// 调用抛错返回 false,让上层试下一个名字。
|
|
|
|
|
|
/// </summary>
|
2026-08-14 14:43:11 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|