磁导航1.0内部交管和信号交互
This commit is contained in:
@@ -139,13 +139,26 @@ namespace StandardScene.Signal.Logic
|
||||
release = _release;
|
||||
}
|
||||
|
||||
PlcStationRuntime Find(string name)
|
||||
PlcStationRuntime Find(string name, string dockId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
return null;
|
||||
var list = _runtimes() ?? Array.Empty<PlcStationRuntime>();
|
||||
return list.FirstOrDefault(x =>
|
||||
string.Equals(x.Config.JgName, name, StringComparison.OrdinalIgnoreCase));
|
||||
var ofStation = list.Where(x =>
|
||||
string.Equals(x.JgName, name, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (ofStation.Count == 0)
|
||||
return null;
|
||||
if (!string.IsNullOrWhiteSpace(dockId))
|
||||
{
|
||||
var hit = ofStation.FirstOrDefault(x =>
|
||||
string.Equals(x.DockId, dockId.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||
if (hit != null)
|
||||
return hit;
|
||||
}
|
||||
|
||||
return ofStation.FirstOrDefault(x =>
|
||||
string.Equals(x.DockId, PlcConfigLoader.DefaultDockId, StringComparison.OrdinalIgnoreCase))
|
||||
?? ofStation[0];
|
||||
}
|
||||
|
||||
foreach (var car in SignalCarAdapter.All())
|
||||
@@ -192,7 +205,7 @@ namespace StandardScene.Signal.Logic
|
||||
/// 停稳分发。TimedCharging 优先:充电点不应再走机构逻辑。
|
||||
/// 未识别的 SignalType 按站点节流打日志,避免刷屏。
|
||||
/// </summary>
|
||||
private void DispatchStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
|
||||
private void DispatchStopped(Car car, SignalStopSpec spec, Func<string, string, PlcStationRuntime> find)
|
||||
{
|
||||
if (spec.Is("TimedCharging"))
|
||||
{
|
||||
|
||||
@@ -30,11 +30,12 @@ namespace StandardScene.Signal.Logic
|
||||
public static class JgControlHandler
|
||||
{
|
||||
/// <summary>停稳:可选限流 → RequestIn 或 Arrived。</summary>
|
||||
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find, IDictionary<string, int> rateFlowLog)
|
||||
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, string, PlcStationRuntime> find, IDictionary<string, int> rateFlowLog)
|
||||
{
|
||||
var jgName = spec.Part(0).Trim();
|
||||
var action = spec.Part(1).Trim();
|
||||
var rateFlow = spec.Part(2).Trim();
|
||||
var jgName = spec.JgName;
|
||||
var action = spec.Action;
|
||||
var dockId = spec.DockId;
|
||||
var rateFlow = spec.ExtraParams;
|
||||
if (string.IsNullOrEmpty(jgName))
|
||||
{
|
||||
Diagnosis.Log($"AGV{car.id}停在地标{SignalCarAdapter.PositionId(car)},读取配置机构名称为空", "JGControl", true);
|
||||
@@ -48,29 +49,25 @@ namespace StandardScene.Signal.Logic
|
||||
}
|
||||
|
||||
if (string.Equals(action, "RequestIn", StringComparison.OrdinalIgnoreCase))
|
||||
RequestIn(jgName, car, find);
|
||||
RequestIn(jgName, dockId, car, find);
|
||||
else if (string.Equals(action, "Arrived", StringComparison.OrdinalIgnoreCase))
|
||||
Arrived(jgName, car, find);
|
||||
Arrived(jgName, dockId, car, find);
|
||||
}
|
||||
|
||||
/// <summary>行驶中只做 RequestIn 安全闸:未允许进入且不是本车占用请求槽则停车。</summary>
|
||||
public static void OnMoving(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
|
||||
public static void OnMoving(Car car, SignalStopSpec spec, Func<string, string, PlcStationRuntime> find)
|
||||
{
|
||||
var jgName = spec.Part(0).Trim();
|
||||
var action = spec.Part(1).Trim();
|
||||
var jgName = spec.JgName;
|
||||
var action = spec.Action;
|
||||
if (string.IsNullOrEmpty(jgName))
|
||||
return;
|
||||
if (string.Equals(action, "RequestIn", StringComparison.OrdinalIgnoreCase))
|
||||
RequestInJudge(jgName, car, find);
|
||||
RequestInJudge(jgName, spec.DockId, car, find);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 请求进入。锁机构运行态,避免与 PLC Worker 同时改写字节 / 车号。
|
||||
/// 后半段与 <see cref="RequestInJudge"/> 相同,覆盖「刚放行后又被调度层启动」的竞态。
|
||||
/// </summary>
|
||||
private static void RequestIn(string jgName, Car car, Func<string, PlcStationRuntime> find)
|
||||
private static void RequestIn(string jgName, string dockId, Car car, Func<string, string, PlcStationRuntime> find)
|
||||
{
|
||||
var rt = find(jgName);
|
||||
var rt = find(jgName, dockId);
|
||||
if (rt == null)
|
||||
{
|
||||
Diagnosis.Log($"{jgName} 未能找到机构", "JGControl", true);
|
||||
@@ -83,19 +80,19 @@ namespace StandardScene.Signal.Logic
|
||||
rt.RequestInAgvNo == 0 &&
|
||||
rt.ReadWriteSignal == SignalWriteValue.无状态)
|
||||
{
|
||||
if (rt.Config.LineType is SignalLineType.单牵引下线机构 or SignalLineType.双牵引下线机构)
|
||||
if (rt.Station.LineType is SignalLineType.单牵引下线机构 or SignalLineType.双牵引下线机构)
|
||||
{
|
||||
var inside = SignalCarAdapter.CarsAt(rt.Config.JgPointId);
|
||||
var inside = SignalCarAdapter.CarsAt(rt.Dock.JgPointId);
|
||||
if (inside.Count > 0)
|
||||
{
|
||||
Diagnosis.Log(
|
||||
$"{jgName} 机构内有AGV【{string.Join(",", inside.Select(x => x.id))}】暂不放行AGV{car.id}",
|
||||
$"{rt.DisplayName} 机构内有AGV【{string.Join(",", inside.Select(x => x.id))}】暂不放行AGV{car.id}",
|
||||
"JGControl", true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Diagnosis.Log($"{jgName} 机构允许进入,通知AGV{car.id}启动,通知机构AGV进入中", "JGControl", true);
|
||||
Diagnosis.Log($"{rt.DisplayName} 机构允许进入,通知AGV{car.id}启动,通知机构AGV进入中", "JGControl", true);
|
||||
rt.WriteSignal = SignalWriteValue.进入中;
|
||||
rt.RequestInAgvNo = car.id;
|
||||
SignalCarAdapter.StopCharge(car);
|
||||
@@ -108,15 +105,14 @@ namespace StandardScene.Signal.Logic
|
||||
SignalCarAdapter.IsMoving(car))
|
||||
{
|
||||
SignalCarAdapter.Stop(car);
|
||||
Diagnosis.Log($"{jgName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
|
||||
Diagnosis.Log($"{rt.DisplayName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>行驶中的 RequestIn 安全闸,条件与 RequestIn 后半段一致。</summary>
|
||||
private static void RequestInJudge(string jgName, Car car, Func<string, PlcStationRuntime> find)
|
||||
private static void RequestInJudge(string jgName, string dockId, Car car, Func<string, string, PlcStationRuntime> find)
|
||||
{
|
||||
var rt = find(jgName);
|
||||
var rt = find(jgName, dockId);
|
||||
if (rt == null)
|
||||
{
|
||||
Diagnosis.Log($"{jgName} 未能找到机构", "JGControl", true);
|
||||
@@ -130,17 +126,14 @@ namespace StandardScene.Signal.Logic
|
||||
SignalCarAdapter.IsMoving(car))
|
||||
{
|
||||
SignalCarAdapter.Stop(car);
|
||||
Diagnosis.Log($"{jgName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
|
||||
Diagnosis.Log($"{rt.DisplayName} 机构不允许进入,监控到AGV启动,暂停AGV{car.id}", "JGControl", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 到位。InAgvNo 已是本车则认为已报到,避免每秒重复写「到位」把 WriteSuccess 清掉。
|
||||
/// </summary>
|
||||
private static void Arrived(string jgName, Car car, Func<string, PlcStationRuntime> find)
|
||||
private static void Arrived(string jgName, string dockId, Car car, Func<string, string, PlcStationRuntime> find)
|
||||
{
|
||||
var rt = find(jgName);
|
||||
var rt = find(jgName, dockId);
|
||||
if (rt == null)
|
||||
{
|
||||
Diagnosis.Log($"{jgName} 未能找到机构", "JGControl", true);
|
||||
@@ -151,7 +144,7 @@ namespace StandardScene.Signal.Logic
|
||||
{
|
||||
if (rt.InAgvNo == car.id)
|
||||
return;
|
||||
Diagnosis.Log($"AGV{car.id}已到达机构{jgName}", "JGControl", true);
|
||||
Diagnosis.Log($"AGV{car.id}已到达机构{rt.DisplayName}", "JGControl", true);
|
||||
rt.WriteSignal = SignalWriteValue.到位;
|
||||
rt.InAgvNo = car.id;
|
||||
rt.RequestInAgvNo = 0;
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 磁条管控区占用判定:他车占用「当前策略」管控站点列表的数量 ≥ 容量则不放行;本车不计入。
|
||||
/// 磁条交管进程只对停在触发点的 MagCar 调用,满足后发启动。
|
||||
/// </summary>
|
||||
public static class MagControlAreaGate
|
||||
{
|
||||
public const byte StartStopPass = 1;
|
||||
public const byte StartStopControlStart = 11;
|
||||
|
||||
private static readonly char[] SiteIdSeparators = { ',', ';', '|', ' ', '\t', '[', ']', '(', ')', '{', '}' };
|
||||
|
||||
public static byte NormalizeReleaseStartStop(byte value)
|
||||
{
|
||||
if (value == StartStopPass || value == StartStopControlStart)
|
||||
return value;
|
||||
return StartStopControlStart;
|
||||
}
|
||||
|
||||
public static int NormalizeCapacity(int capacity)
|
||||
{
|
||||
return capacity < 1 ? 1 : capacity;
|
||||
}
|
||||
|
||||
public static int[] ParseSiteIds(string text, int fallbackSiteId)
|
||||
{
|
||||
var ids = new List<int>();
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
var parts = text.Split(SiteIdSeparators, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (int.TryParse(part.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var id) &&
|
||||
id > 0 && !ids.Contains(id))
|
||||
{
|
||||
ids.Add(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ids.Count == 0 && fallbackSiteId > 0)
|
||||
ids.Add(fallbackSiteId);
|
||||
|
||||
return ids.ToArray();
|
||||
}
|
||||
|
||||
public static string NormalizeCompatGroup(string value)
|
||||
{
|
||||
return (value ?? string.Empty).Trim();
|
||||
}
|
||||
|
||||
public static bool SitesIntersect(IReadOnlyList<int> a, IReadOnlyList<int> b)
|
||||
{
|
||||
if (a == null || b == null || a.Count == 0 || b.Count == 0)
|
||||
return false;
|
||||
for (var i = 0; i < a.Count; i++)
|
||||
{
|
||||
var left = a[i];
|
||||
if (left <= 0)
|
||||
continue;
|
||||
for (var j = 0; j < b.Count; j++)
|
||||
{
|
||||
if (b[j] == left)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static MagControlReleaseDecision Evaluate(
|
||||
int selfCarId,
|
||||
int triggerSit,
|
||||
IReadOnlyList<int> areaSiteIds,
|
||||
int capacity,
|
||||
byte releaseStartStop,
|
||||
IReadOnlyList<(int CarId, IReadOnlyList<int> OccupiedSiteIds)> occupancies)
|
||||
{
|
||||
var sites = areaSiteIds == null || areaSiteIds.Count == 0
|
||||
? (triggerSit > 0 ? new[] { triggerSit } : Array.Empty<int>())
|
||||
: areaSiteIds;
|
||||
capacity = NormalizeCapacity(capacity);
|
||||
releaseStartStop = NormalizeReleaseStartStop(releaseStartStop);
|
||||
|
||||
var others = 0;
|
||||
if (occupancies != null)
|
||||
{
|
||||
foreach (var occupancy in occupancies)
|
||||
{
|
||||
if (occupancy.CarId == selfCarId)
|
||||
continue;
|
||||
if (OccupiesListedSites(occupancy.OccupiedSiteIds, sites))
|
||||
others++;
|
||||
}
|
||||
}
|
||||
|
||||
var canRelease = others < capacity;
|
||||
var areaId = string.Join(",", sites);
|
||||
return new MagControlReleaseDecision
|
||||
{
|
||||
RuleFound = true,
|
||||
CanRelease = canRelease,
|
||||
AreaId = areaId,
|
||||
Capacity = capacity,
|
||||
OthersInArea = others,
|
||||
ReleaseStartStop = releaseStartStop,
|
||||
Reason = canRelease
|
||||
? "area clear"
|
||||
: $"area occupied cars={others}, capacity={capacity}"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>统计占用管控区内部站点的车辆数,可排除排队车和在途车。</summary>
|
||||
public static int CountOccupyingCars(
|
||||
IReadOnlyList<(int CarId, IReadOnlyList<int> OccupiedSiteIds)> occupancies,
|
||||
IReadOnlyList<int> areaSiteIds,
|
||||
HashSet<int> excludeCarIds)
|
||||
{
|
||||
if (occupancies == null || occupancies.Count == 0 ||
|
||||
areaSiteIds == null || areaSiteIds.Count == 0)
|
||||
return 0;
|
||||
|
||||
var others = 0;
|
||||
foreach (var occupancy in occupancies)
|
||||
{
|
||||
if (excludeCarIds != null && excludeCarIds.Contains(occupancy.CarId))
|
||||
continue;
|
||||
if (OccupiesListedSites(occupancy.OccupiedSiteIds, areaSiteIds))
|
||||
others++;
|
||||
}
|
||||
|
||||
return others;
|
||||
}
|
||||
|
||||
public static bool OccupiesListedSites(IReadOnlyList<int> occupiedSiteIds, IReadOnlyList<int> areaSiteIds)
|
||||
{
|
||||
if (occupiedSiteIds == null || occupiedSiteIds.Count == 0 ||
|
||||
areaSiteIds == null || areaSiteIds.Count == 0)
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < occupiedSiteIds.Count; i++)
|
||||
{
|
||||
var occupied = occupiedSiteIds[i];
|
||||
if (occupied <= 0)
|
||||
continue;
|
||||
for (var j = 0; j < areaSiteIds.Count; j++)
|
||||
{
|
||||
if (areaSiteIds[j] == occupied)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>统计一辆车占用的站点:物理 siteID + holdingLocks。</summary>
|
||||
public static IReadOnlyList<int> CollectOccupiedSiteIds(Car car)
|
||||
{
|
||||
var ids = new List<int>();
|
||||
if (car == null)
|
||||
return ids;
|
||||
try
|
||||
{
|
||||
if (car.siteID > 0)
|
||||
ids.Add(car.siteID);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
var holding = car.status?.holdingLocks;
|
||||
if (holding == null)
|
||||
return ids;
|
||||
foreach (var lockId in holding)
|
||||
{
|
||||
if (lockId > 0 && !ids.Contains(lockId))
|
||||
ids.Add(lockId);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,479 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using SimpleCore.Library;
|
||||
using SimpleLite.RCS.CarTypes;
|
||||
using StandardScene.Signal.Model;
|
||||
using StandardScene.Signal.Plc;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 磁条交管运行态:按管控区名称分组排队;放行按「本线 + 冲突对向线」站点并集查当前占用。
|
||||
/// 不维护 pending 在途占额。
|
||||
/// </summary>
|
||||
public sealed class MagControlRuntime
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private Dictionary<int, MagControlAreaModel> _byTrigger = new Dictionary<int, MagControlAreaModel>();
|
||||
private readonly Dictionary<int, DateTime> _releaseLog = new Dictionary<int, DateTime>();
|
||||
private readonly Dictionary<int, Arrival> _arrivals = new Dictionary<int, Arrival>();
|
||||
|
||||
public int RuleCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
return _byTrigger.Count;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>已不再使用 pending;保留属性避免旧 UI 绑定报错,恒为 0。</summary>
|
||||
public int PendingCount => 0;
|
||||
|
||||
public void Replace(IEnumerable<MagControlAreaModel> rows)
|
||||
{
|
||||
var map = (rows ?? Array.Empty<MagControlAreaModel>())
|
||||
.Where(x => x != null && x.TriggerSit > 0 && x.IsUse)
|
||||
.GroupBy(x => x.TriggerSit)
|
||||
.ToDictionary(g => g.Key, g => g.First());
|
||||
lock (_lock)
|
||||
{
|
||||
_byTrigger = map;
|
||||
_arrivals.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 同名区排队;放行前查本线 + 冲突对向线站点并集上的当前占用。
|
||||
/// 另计:已放行仍停在冲突触发点上的运行中车辆(避免 site 未进区时空窗连放)。
|
||||
/// </summary>
|
||||
public (int Waiting, int Released) ScanAndRelease()
|
||||
{
|
||||
List<MagControlAreaModel> rules;
|
||||
lock (_lock)
|
||||
rules = _byTrigger.Values.ToList();
|
||||
|
||||
if (rules.Count == 0)
|
||||
return (0, 0);
|
||||
|
||||
var snapshots = SnapshotCars();
|
||||
var occupancies = snapshots
|
||||
.Select(s => (s.CarId, s.OccupiedSiteIds))
|
||||
.ToList();
|
||||
|
||||
var groups = rules
|
||||
.GroupBy(AreaKey, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(g => BuildGroup(g.Key, g.ToList()))
|
||||
.ToList();
|
||||
|
||||
lock (_lock)
|
||||
SweepArrivals(snapshots);
|
||||
|
||||
var waiting = 0;
|
||||
var released = 0;
|
||||
foreach (var group in groups)
|
||||
{
|
||||
var waiters = CollectWaiters(group, snapshots);
|
||||
waiting += waiters.Count;
|
||||
if (waiters.Count == 0)
|
||||
continue;
|
||||
|
||||
// 停稳排队的车不计入区内占用;本拍已放行的车单独记冲突占额
|
||||
var exclude = new HashSet<int>(waiters.Select(w => w.CarId));
|
||||
var releasedThisScan = new List<Waiter>();
|
||||
|
||||
waiters.Sort((a, b) => CompareWaiters(a, b, group.WaitSeconds));
|
||||
foreach (var waiter in waiters)
|
||||
{
|
||||
var checkSites = BuildConflictSites(group, waiter);
|
||||
var capacity = MagControlAreaGate.NormalizeCapacity(waiter.Capacity);
|
||||
var others = CountConflictingOccupancy(
|
||||
group, waiter, checkSites, occupancies, snapshots, exclude, releasedThisScan);
|
||||
if (others >= capacity)
|
||||
continue;
|
||||
|
||||
SignalCarAdapter.StopCharge(waiter.Car);
|
||||
SignalCarAdapter.StartMagCar(waiter.Car);
|
||||
released++;
|
||||
exclude.Add(waiter.CarId);
|
||||
releasedThisScan.Add(waiter);
|
||||
NoteRelease(waiter, group, others, capacity, checkSites);
|
||||
}
|
||||
}
|
||||
|
||||
return (waiting, released);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 冲突占用 = 并集站点上的当前车
|
||||
/// + 本区冲突触发点上仍「运行中」的车(刚放行、site 尚未进 ControlArea)
|
||||
/// + 本拍已放行且与本线冲突的车。
|
||||
/// </summary>
|
||||
private static int CountConflictingOccupancy(
|
||||
AreaGroup group,
|
||||
Waiter waiter,
|
||||
IReadOnlyList<int> checkSites,
|
||||
List<(int CarId, IReadOnlyList<int> OccupiedSiteIds)> occupancies,
|
||||
List<CarSnapshot> snapshots,
|
||||
HashSet<int> exclude,
|
||||
List<Waiter> releasedThisScan)
|
||||
{
|
||||
var others = MagControlAreaGate.CountOccupyingCars(occupancies, checkSites, exclude);
|
||||
var counted = new HashSet<int>();
|
||||
|
||||
foreach (var snap in snapshots)
|
||||
{
|
||||
if (snap == null || exclude.Contains(snap.CarId) || counted.Contains(snap.CarId))
|
||||
continue;
|
||||
if (!snap.IsMagCar)
|
||||
continue;
|
||||
|
||||
// 已在并集站点上:CountOccupyingCars 已计,这里补「运行中仍在冲突触发点」
|
||||
if (snap.IsStopped)
|
||||
continue;
|
||||
if (snap.Sit <= 0 || !group.Triggers.Contains(snap.Sit))
|
||||
continue;
|
||||
if (!group.RouteByTrigger.TryGetValue(snap.Sit, out var route))
|
||||
continue;
|
||||
if (!Conflicts(waiter.CompatGroup, waiter.AreaSites, route.CompatGroup, route.AreaSites))
|
||||
continue;
|
||||
|
||||
// 若 site 已在 checkSites 里,上面 others 可能已计(触发点一般不在列表)
|
||||
if (MagControlAreaGate.OccupiesListedSites(snap.OccupiedSiteIds, checkSites))
|
||||
continue;
|
||||
|
||||
counted.Add(snap.CarId);
|
||||
others++;
|
||||
}
|
||||
|
||||
foreach (var releasedCar in releasedThisScan)
|
||||
{
|
||||
if (releasedCar == null || counted.Contains(releasedCar.CarId))
|
||||
continue;
|
||||
if (!Conflicts(
|
||||
waiter.CompatGroup, waiter.AreaSites,
|
||||
releasedCar.CompatGroup, releasedCar.AreaSites))
|
||||
continue;
|
||||
counted.Add(releasedCar.CarId);
|
||||
others++;
|
||||
}
|
||||
|
||||
return others;
|
||||
}
|
||||
|
||||
/// <summary>单车判定:本线 + 同名区内冲突对向线站点并集的当前占用。</summary>
|
||||
public MagControlReleaseDecision Decide(int carId, int triggerSit)
|
||||
{
|
||||
MagControlAreaModel rule;
|
||||
List<MagControlAreaModel> sameArea;
|
||||
lock (_lock)
|
||||
{
|
||||
if (triggerSit <= 0 || !_byTrigger.TryGetValue(triggerSit, out rule) || rule == null)
|
||||
{
|
||||
return new MagControlReleaseDecision
|
||||
{
|
||||
RuleFound = false,
|
||||
Reason = "no mag-control rule"
|
||||
};
|
||||
}
|
||||
|
||||
var key = AreaKey(rule);
|
||||
sameArea = _byTrigger.Values
|
||||
.Where(r => string.Equals(AreaKey(r), key, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
var group = BuildGroup(AreaKey(rule), sameArea);
|
||||
if (!group.RouteByTrigger.TryGetValue(triggerSit, out var route))
|
||||
{
|
||||
return new MagControlReleaseDecision
|
||||
{
|
||||
RuleFound = false,
|
||||
Reason = "no mag-control route"
|
||||
};
|
||||
}
|
||||
|
||||
var waiter = new Waiter
|
||||
{
|
||||
CarId = carId,
|
||||
TriggerSit = triggerSit,
|
||||
CompatGroup = route.CompatGroup,
|
||||
AreaSites = route.AreaSites,
|
||||
Capacity = route.Capacity
|
||||
};
|
||||
var checkSites = BuildConflictSites(group, waiter);
|
||||
var occupancies = new List<(int CarId, IReadOnlyList<int> OccupiedSiteIds)>();
|
||||
foreach (var car in SignalCarAdapter.All())
|
||||
{
|
||||
if (car == null)
|
||||
continue;
|
||||
occupancies.Add((car.id, MagControlAreaGate.CollectOccupiedSiteIds(car)));
|
||||
}
|
||||
|
||||
return MagControlAreaGate.Evaluate(
|
||||
carId,
|
||||
triggerSit,
|
||||
checkSites,
|
||||
rule.ControlCapacity,
|
||||
rule.ControlReleaseStartStop,
|
||||
occupancies);
|
||||
}
|
||||
|
||||
internal static string AreaKey(MagControlAreaModel rule)
|
||||
{
|
||||
if (rule == null)
|
||||
return "#sit:0";
|
||||
var name = (rule.AreaName ?? string.Empty).Trim();
|
||||
return name.Length > 0 ? name : "#sit:" + rule.TriggerSit;
|
||||
}
|
||||
|
||||
internal static int CompareWaiters(Waiter a, Waiter b, int waitSeconds)
|
||||
{
|
||||
if (waitSeconds > 0)
|
||||
{
|
||||
if (a.Priority < b.Priority &&
|
||||
(b.ArrivedAt - a.ArrivedAt).TotalSeconds > waitSeconds)
|
||||
return -1;
|
||||
if (b.Priority < a.Priority &&
|
||||
(a.ArrivedAt - b.ArrivedAt).TotalSeconds > waitSeconds)
|
||||
return 1;
|
||||
}
|
||||
|
||||
var byPriority = b.Priority.CompareTo(a.Priority);
|
||||
if (byPriority != 0)
|
||||
return byPriority;
|
||||
var byArrival = a.ArrivedAt.CompareTo(b.ArrivedAt);
|
||||
if (byArrival != 0)
|
||||
return byArrival;
|
||||
return a.CarId.CompareTo(b.CarId);
|
||||
}
|
||||
|
||||
internal static bool Conflicts(
|
||||
string compatA,
|
||||
IReadOnlyList<int> sitesA,
|
||||
string compatB,
|
||||
IReadOnlyList<int> sitesB)
|
||||
{
|
||||
if (string.Equals(compatA ?? "", compatB ?? "", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
return MagControlAreaGate.SitesIntersect(sitesA, sitesB);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 本线站点 + 同名区内所有与本线冲突的对向/交叉线路站点并集。
|
||||
/// 例如西环 NS-W 会并入东/北/南列表,100/101/104 有车时都能挡住触发点 4。
|
||||
/// </summary>
|
||||
private static IReadOnlyList<int> BuildConflictSites(AreaGroup group, Waiter waiter)
|
||||
{
|
||||
var set = new HashSet<int>();
|
||||
if (waiter?.AreaSites != null)
|
||||
{
|
||||
foreach (var site in waiter.AreaSites)
|
||||
{
|
||||
if (site > 0)
|
||||
set.Add(site);
|
||||
}
|
||||
}
|
||||
|
||||
if (group?.RouteByTrigger == null)
|
||||
return set.OrderBy(x => x).ToList();
|
||||
|
||||
foreach (var route in group.RouteByTrigger.Values)
|
||||
{
|
||||
if (route?.AreaSites == null)
|
||||
continue;
|
||||
if (!Conflicts(waiter.CompatGroup, waiter.AreaSites, route.CompatGroup, route.AreaSites))
|
||||
continue;
|
||||
foreach (var site in route.AreaSites)
|
||||
{
|
||||
if (site > 0)
|
||||
set.Add(site);
|
||||
}
|
||||
}
|
||||
|
||||
return set.OrderBy(x => x).ToList();
|
||||
}
|
||||
|
||||
private static AreaGroup BuildGroup(string key, List<MagControlAreaModel> rows)
|
||||
{
|
||||
var ordered = rows.OrderBy(r => r.TriggerSit).ToList();
|
||||
var triggers = new HashSet<int>(ordered.Select(r => r.TriggerSit));
|
||||
var waitSeconds = 0;
|
||||
var routeByTrigger = new Dictionary<int, RouteInfo>();
|
||||
foreach (var row in ordered)
|
||||
{
|
||||
waitSeconds = Math.Max(waitSeconds, row.WaitSeconds);
|
||||
var sites = MagControlAreaGate.ParseSiteIds(row.ControlArea, row.TriggerSit);
|
||||
routeByTrigger[row.TriggerSit] = new RouteInfo
|
||||
{
|
||||
CompatGroup = MagControlAreaGate.NormalizeCompatGroup(row.CompatGroup),
|
||||
Capacity = MagControlAreaGate.NormalizeCapacity(row.ControlCapacity),
|
||||
AreaSites = sites,
|
||||
Priority = row.Priority
|
||||
};
|
||||
}
|
||||
|
||||
return new AreaGroup
|
||||
{
|
||||
Key = key,
|
||||
WaitSeconds = waitSeconds,
|
||||
Triggers = triggers,
|
||||
RouteByTrigger = routeByTrigger
|
||||
};
|
||||
}
|
||||
|
||||
private List<CarSnapshot> SnapshotCars()
|
||||
{
|
||||
var list = new List<CarSnapshot>();
|
||||
foreach (var car in SignalCarAdapter.All())
|
||||
{
|
||||
if (car == null)
|
||||
continue;
|
||||
list.Add(new CarSnapshot
|
||||
{
|
||||
Car = car,
|
||||
CarId = car.id,
|
||||
OccupiedSiteIds = MagControlAreaGate.CollectOccupiedSiteIds(car),
|
||||
IsMagCar = SignalCarAdapter.IsMagCar(car),
|
||||
IsStopped = SignalCarAdapter.IsStopped(car),
|
||||
Sit = SignalCarAdapter.PositionId(car)
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private List<Waiter> CollectWaiters(AreaGroup group, List<CarSnapshot> snapshots)
|
||||
{
|
||||
var waiters = new List<Waiter>();
|
||||
var now = DateTime.Now;
|
||||
foreach (var snap in snapshots)
|
||||
{
|
||||
if (!snap.IsMagCar || !snap.IsStopped)
|
||||
continue;
|
||||
if (snap.Sit <= 0 || !group.Triggers.Contains(snap.Sit))
|
||||
continue;
|
||||
|
||||
DateTime arrivedAt;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_arrivals.TryGetValue(snap.CarId, out var arrival) || arrival.TriggerSit != snap.Sit)
|
||||
{
|
||||
arrival = new Arrival { TriggerSit = snap.Sit, ArrivedAt = now };
|
||||
_arrivals[snap.CarId] = arrival;
|
||||
}
|
||||
|
||||
arrivedAt = arrival.ArrivedAt;
|
||||
}
|
||||
|
||||
if (!group.RouteByTrigger.TryGetValue(snap.Sit, out var route))
|
||||
continue;
|
||||
|
||||
waiters.Add(new Waiter
|
||||
{
|
||||
Car = snap.Car,
|
||||
CarId = snap.CarId,
|
||||
TriggerSit = snap.Sit,
|
||||
AreaKey = group.Key,
|
||||
CompatGroup = route.CompatGroup,
|
||||
Capacity = route.Capacity,
|
||||
AreaSites = route.AreaSites,
|
||||
Priority = route.Priority,
|
||||
ArrivedAt = arrivedAt
|
||||
});
|
||||
}
|
||||
|
||||
return waiters;
|
||||
}
|
||||
|
||||
private void SweepArrivals(List<CarSnapshot> snapshots)
|
||||
{
|
||||
var present = new HashSet<int>();
|
||||
foreach (var snap in snapshots)
|
||||
{
|
||||
if (!snap.IsMagCar || !snap.IsStopped || snap.Sit <= 0)
|
||||
continue;
|
||||
if (_arrivals.TryGetValue(snap.CarId, out var arrival) && arrival.TriggerSit == snap.Sit)
|
||||
present.Add(snap.CarId);
|
||||
}
|
||||
|
||||
var stale = _arrivals.Keys.Where(id => !present.Contains(id)).ToList();
|
||||
foreach (var id in stale)
|
||||
_arrivals.Remove(id);
|
||||
}
|
||||
|
||||
private void NoteRelease(
|
||||
Waiter waiter,
|
||||
AreaGroup group,
|
||||
int others,
|
||||
int capacity,
|
||||
IReadOnlyList<int> checkSites)
|
||||
{
|
||||
var groupName = string.IsNullOrEmpty(waiter.CompatGroup) ? "-" : waiter.CompatGroup;
|
||||
var carName = waiter.Car?.name;
|
||||
var namePart = string.IsNullOrWhiteSpace(carName) ? "" : $" name={carName}";
|
||||
var sites = waiter.AreaSites == null || waiter.AreaSites.Count == 0
|
||||
? "-"
|
||||
: string.Join(",", waiter.AreaSites);
|
||||
var conflict = checkSites == null || checkSites.Count == 0
|
||||
? "-"
|
||||
: string.Join(",", checkSites);
|
||||
var message =
|
||||
$"放行 MagCar={waiter.CarId}{namePart} 区={group.Key} 组={groupName} 触发点={waiter.TriggerSit} " +
|
||||
$"本线={sites} 冲突并集={conflict} 当前占用={others}/{capacity}";
|
||||
|
||||
MagTrafficFileLogger.Write(carName, waiter.CarId, message);
|
||||
|
||||
if (_releaseLog.TryGetValue(waiter.CarId, out var last) && (DateTime.Now - last).TotalSeconds < 5)
|
||||
return;
|
||||
_releaseLog[waiter.CarId] = DateTime.Now;
|
||||
Diagnosis.Log($"磁条交管{message}", "MagTraffic", true);
|
||||
}
|
||||
|
||||
internal sealed class Waiter
|
||||
{
|
||||
public Car Car;
|
||||
public int CarId;
|
||||
public int TriggerSit;
|
||||
public string AreaKey;
|
||||
public string CompatGroup;
|
||||
public int Capacity;
|
||||
public IReadOnlyList<int> AreaSites;
|
||||
public int Priority;
|
||||
public DateTime ArrivedAt;
|
||||
}
|
||||
|
||||
private sealed class Arrival
|
||||
{
|
||||
public int TriggerSit;
|
||||
public DateTime ArrivedAt;
|
||||
}
|
||||
|
||||
private sealed class RouteInfo
|
||||
{
|
||||
public string CompatGroup;
|
||||
public int Capacity;
|
||||
public IReadOnlyList<int> AreaSites;
|
||||
public int Priority;
|
||||
}
|
||||
|
||||
private sealed class AreaGroup
|
||||
{
|
||||
public string Key;
|
||||
public int WaitSeconds;
|
||||
public HashSet<int> Triggers;
|
||||
public Dictionary<int, RouteInfo> RouteByTrigger;
|
||||
}
|
||||
|
||||
private sealed class CarSnapshot
|
||||
{
|
||||
public Car Car;
|
||||
public int CarId;
|
||||
public IReadOnlyList<int> OccupiedSiteIds;
|
||||
public bool IsMagCar;
|
||||
public bool IsStopped;
|
||||
public int Sit;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 磁条交管放行留档,按车名分目录:log/mag-traffic/car{name}/release_yyyyMMdd.log
|
||||
/// 进程启停等写到 log/mag-traffic/_sys/sys_yyyyMMdd.log
|
||||
/// </summary>
|
||||
public static class MagTrafficFileLogger
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, object> CarLocks = new ConcurrentDictionary<string, object>(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly object SysLock = new object();
|
||||
private static readonly char[] InvalidNameChars = Path.GetInvalidFileNameChars();
|
||||
private static string _baseDirectory = "log";
|
||||
private static bool _enabled = true;
|
||||
|
||||
public static void Configure(string baseDirectory, bool enabled)
|
||||
{
|
||||
_baseDirectory = string.IsNullOrWhiteSpace(baseDirectory) ? "log" : baseDirectory.Trim();
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
public static string GetLogDirectory()
|
||||
{
|
||||
return Path.Combine(ResolveBaseDirectory(), "mag-traffic");
|
||||
}
|
||||
|
||||
/// <summary>目录名 car{name};名称为空时回退 car{id}。</summary>
|
||||
public static string GetCarFolderKey(string carName, int carId)
|
||||
{
|
||||
var safe = SanitizeName(carName);
|
||||
if (string.IsNullOrWhiteSpace(safe))
|
||||
{
|
||||
safe = carId > 0 ? carId.ToString() : "unknown";
|
||||
}
|
||||
|
||||
return "car" + safe;
|
||||
}
|
||||
|
||||
public static string GetCarLogDirectory(string carName, int carId = 0)
|
||||
{
|
||||
return Path.Combine(GetLogDirectory(), GetCarFolderKey(carName, carId));
|
||||
}
|
||||
|
||||
public static string GetCurrentCarLogFilePath(string carName, int carId = 0)
|
||||
{
|
||||
return Path.Combine(GetCarLogDirectory(carName, carId), $"release_{DateTime.Now:yyyyMMdd}.log");
|
||||
}
|
||||
|
||||
public static string GetCurrentSysLogFilePath()
|
||||
{
|
||||
return Path.Combine(GetLogDirectory(), "_sys", $"sys_{DateTime.Now:yyyyMMdd}.log");
|
||||
}
|
||||
|
||||
/// <summary>按车名写入放行记录。</summary>
|
||||
public static void Write(string carName, int carId, string message)
|
||||
{
|
||||
if (!_enabled || string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var folderKey = GetCarFolderKey(carName, carId);
|
||||
var carLock = CarLocks.GetOrAdd(folderKey, _ => new object());
|
||||
lock (carLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.Combine(GetLogDirectory(), folderKey);
|
||||
Directory.CreateDirectory(directory);
|
||||
var path = Path.Combine(directory, $"release_{DateTime.Now:yyyyMMdd}.log");
|
||||
File.AppendAllText(
|
||||
path,
|
||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}",
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>进程启停、重载配置等非单车事件。</summary>
|
||||
public static void WriteSystem(string message)
|
||||
{
|
||||
if (!_enabled || string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (SysLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = GetCurrentSysLogFilePath();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? GetLogDirectory());
|
||||
File.AppendAllText(
|
||||
path,
|
||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}] {message}{Environment.NewLine}",
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false));
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string SanitizeName(string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var chars = name.Trim().Select(ch => InvalidNameChars.Contains(ch) || ch <= 31 ? '_' : ch).ToArray();
|
||||
var safe = new string(chars).Trim('.', ' ');
|
||||
while (safe.Contains("__"))
|
||||
{
|
||||
safe = safe.Replace("__", "_");
|
||||
}
|
||||
|
||||
return safe.Length > 80 ? safe.Substring(0, 80) : safe;
|
||||
}
|
||||
|
||||
private static string ResolveBaseDirectory()
|
||||
{
|
||||
return Path.IsPathRooted(_baseDirectory)
|
||||
? _baseDirectory
|
||||
: Path.Combine(AppContext.BaseDirectory, _baseDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,10 @@ namespace StandardScene.Signal.Logic
|
||||
public static class PointControlHandler
|
||||
{
|
||||
/// <summary>停稳:先判机构是否允许离开,再下发销钉,全部到位后启动。</summary>
|
||||
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
|
||||
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, string, PlcStationRuntime> find)
|
||||
{
|
||||
var action = spec.Part(0).Trim();
|
||||
var jgName = spec.Part(1).Trim();
|
||||
var action = spec.Action;
|
||||
var jgName = spec.JgName;
|
||||
if (string.IsNullOrEmpty(action))
|
||||
{
|
||||
Diagnosis.Log($"AGV{car.id}停在地标{SignalCarAdapter.PositionId(car)},读取配置特殊点名称为空", "SetPin", true);
|
||||
@@ -31,7 +31,7 @@ namespace StandardScene.Signal.Logic
|
||||
var waitLeave = false;
|
||||
if (!string.IsNullOrEmpty(jgName))
|
||||
{
|
||||
var rt = find(jgName);
|
||||
var rt = find(jgName, spec.DockId);
|
||||
if (rt != null)
|
||||
{
|
||||
lock (rt.Sync)
|
||||
|
||||
@@ -4,30 +4,24 @@ using StandardScene.Signal.Model;
|
||||
namespace StandardScene.Signal.Logic
|
||||
{
|
||||
/// <summary>
|
||||
/// 一条握手点配置的解析结果。参数格式与反编译 <c>SignalStop.SignalParams</c> 一致。
|
||||
/// 一条握手点配置的解析结果。新列(机构/工位/动作/附加参数)优先,旧 SignalParams 作回退。
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="SignalParams"/> 按分号切成 <see cref="Parts"/>,缺段时 <see cref="Part"/> 返回空串。
|
||||
/// 各类型约定:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>JGControl</c>:机构名;RequestIn|Arrived;[区域-车组-限额-点列表]</item>
|
||||
/// <item><c>PointControl</c>:UP1|UP2|Down1|Down2;机构名</item>
|
||||
/// <item><c>TimedCharging</c>:最长秒数;上限电量;下限电量</item>
|
||||
/// <item><c>RateFlowControl</c>:区域;车组;限额;点1,点2</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public sealed class SignalStopSpec
|
||||
{
|
||||
/// <summary>握手类型名,与 Handler 的 <see cref="Is"/> 比较时忽略大小写。</summary>
|
||||
public string SignalType { get; init; } = "";
|
||||
|
||||
/// <summary>原始参数串,限流独立类型会整段交给 <see cref="RateFlowHandler"/>。</summary>
|
||||
public string SignalParams { get; init; } = "";
|
||||
|
||||
/// <summary>按 ';' 切分,保留空段(与 Split None 一致,方便 Part(n) 按下标取)。</summary>
|
||||
public string[] Parts { get; init; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>取第 <paramref name="index"/> 段;越界或 null 返回空串。</summary>
|
||||
public string JgName { get; init; } = "";
|
||||
|
||||
public string DockId { get; init; } = "";
|
||||
|
||||
public string Action { get; init; } = "";
|
||||
|
||||
public string ExtraParams { get; init; } = "";
|
||||
|
||||
public string Part(int index)
|
||||
{
|
||||
if (index < 0 || index >= Parts.Length)
|
||||
@@ -35,29 +29,102 @@ namespace StandardScene.Signal.Logic
|
||||
return Parts[index] ?? "";
|
||||
}
|
||||
|
||||
/// <summary>从类型名和参数串构造;类型会 Trim,参数保持原样以便日志对照现场 JSON。</summary>
|
||||
public static SignalStopSpec Parse(string signalType, string signalParams)
|
||||
{
|
||||
var type = (signalType ?? "").Trim();
|
||||
var raw = signalParams ?? "";
|
||||
return new SignalStopSpec
|
||||
var parsed = new SignalStopSpec
|
||||
{
|
||||
SignalType = type,
|
||||
SignalParams = raw,
|
||||
Parts = raw.Split(new[] { ';' }, StringSplitOptions.None)
|
||||
};
|
||||
return EnrichFromParts(parsed);
|
||||
}
|
||||
|
||||
/// <summary>从握手点 Model 构造;model 为 null 时返回 null。</summary>
|
||||
public static SignalStopSpec From(HandshakePointModel model)
|
||||
{
|
||||
if (model == null)
|
||||
return null;
|
||||
return Parse(model.SignalType, model.SignalParams);
|
||||
|
||||
var type = (model.SignalType ?? "").Trim();
|
||||
if (string.Equals(type, "TimedCharging", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(type, "RateFlowControl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var raw = First(model.ExtraParams, model.SignalParams);
|
||||
return Parse(type, raw);
|
||||
}
|
||||
|
||||
var parsed = Parse(type, model.SignalParams ?? "");
|
||||
var jg = First(model.JgName, parsed.JgName);
|
||||
var action = First(model.Action, parsed.Action);
|
||||
var dock = First(model.DockId, parsed.DockId);
|
||||
var extra = First(model.ExtraParams, parsed.ExtraParams);
|
||||
return new SignalStopSpec
|
||||
{
|
||||
SignalType = parsed.SignalType,
|
||||
SignalParams = parsed.SignalParams,
|
||||
Parts = parsed.Parts,
|
||||
JgName = jg,
|
||||
DockId = dock,
|
||||
Action = action,
|
||||
ExtraParams = extra
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>类型名是否匹配(忽略大小写)。</summary>
|
||||
public bool Is(string type) =>
|
||||
string.Equals(SignalType, type, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static SignalStopSpec EnrichFromParts(SignalStopSpec parsed)
|
||||
{
|
||||
var type = parsed.SignalType;
|
||||
if (string.Equals(type, "TimedCharging", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(type, "RateFlowControl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new SignalStopSpec
|
||||
{
|
||||
SignalType = parsed.SignalType,
|
||||
SignalParams = parsed.SignalParams,
|
||||
Parts = parsed.Parts,
|
||||
ExtraParams = parsed.SignalParams
|
||||
};
|
||||
}
|
||||
|
||||
var point = string.Equals(type, "PointControl", StringComparison.OrdinalIgnoreCase);
|
||||
var jg = point ? parsed.Part(1).Trim() : parsed.Part(0).Trim();
|
||||
var action = point ? parsed.Part(0).Trim() : parsed.Part(1).Trim();
|
||||
var dock = "";
|
||||
var extra = "";
|
||||
var p2 = parsed.Part(2).Trim();
|
||||
if (LooksLikeDock(p2))
|
||||
dock = p2;
|
||||
else if (LooksLikeRateFlow(p2))
|
||||
extra = p2;
|
||||
if (string.IsNullOrEmpty(extra))
|
||||
extra = parsed.Part(3).Trim();
|
||||
return new SignalStopSpec
|
||||
{
|
||||
SignalType = parsed.SignalType,
|
||||
SignalParams = parsed.SignalParams,
|
||||
Parts = parsed.Parts,
|
||||
JgName = jg,
|
||||
DockId = dock,
|
||||
Action = action,
|
||||
ExtraParams = extra
|
||||
};
|
||||
}
|
||||
|
||||
private static string First(string a, string b)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(a))
|
||||
return a.Trim();
|
||||
return (b ?? "").Trim();
|
||||
}
|
||||
|
||||
private static bool LooksLikeRateFlow(string s) =>
|
||||
!string.IsNullOrEmpty(s) && s.IndexOf('-') >= 0;
|
||||
|
||||
private static bool LooksLikeDock(string s) =>
|
||||
!string.IsNullOrEmpty(s) && s.IndexOf('-') < 0 && s.IndexOf(',') < 0;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user