Files
StandardSence/StandardScene.Signal/Plc/SignalCarAdapter.cs
T

326 lines
12 KiB
C#
Raw Blame History

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