Files
StandardSence/StandardScene.Signal/Logic/CarScanWorker.cs
T

242 lines
9.6 KiB
C#
Raw Normal View History

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using SimpleCore.Library;
using SimpleLite.RCS.CarTypes;
using StandardScene.Signal.Model;
using StandardScene.Signal.Plc;
namespace StandardScene.Signal.Logic
{
2026-08-17 15:20:27 +08:00
/// <summary>
/// 1s 扫全场车,对齐反编译 <c>DefaultPlugin.ExcuteThreadMethod</c> + <c>StarSignManage</c>。
/// </summary>
/// <remarks>
/// 每辆车每拍只看当前站点:
/// <list type="number">
/// <item>停稳:先按握手点类型分发 Handler,再执行放行点(放行点不读 PLC,可能把还在等允许进入的车开走,站点不要两边都配)。</item>
/// <item>行驶中:仅 JGControl + RequestIn 调用 <c>RequestInJudge</c>PLC 未允许进入则停车。</item>
/// </list>
/// 握手点解析优先站点 fields<see cref="SignalCarAdapter.TryReadSignalStop"/>),没有再查 JSON。
/// </remarks>
public sealed class CarScanWorker : IDisposable
{
2026-08-17 15:20:27 +08:00
/// <summary>取当前全部机构运行态;由 Mission 提供,内部会拷贝列表。</summary>
private readonly Func<IReadOnlyList<PlcStationRuntime>> _runtimes;
2026-08-17 15:20:27 +08:00
/// <summary>扫车间隔,下限 100ms,默认跟进程 TickMs=1000。</summary>
private readonly int _tickMs;
2026-08-17 15:20:27 +08:00
/// <summary>保护握手点 / 放行点字典,热替换配置时与扫车拍互斥。</summary>
private readonly object _configLock = new object();
2026-08-17 15:20:27 +08:00
/// <summary>站点 → 握手点。同一站点多条配置只留第一条。</summary>
private Dictionary<int, HandshakePointModel> _handshake = new Dictionary<int, HandshakePointModel>();
2026-08-17 15:20:27 +08:00
/// <summary>站点 → 放行点。同一站点多条配置只留第一条。</summary>
private Dictionary<int, ReleasePointModel> _release = new Dictionary<int, ReleasePointModel>();
2026-08-17 15:20:27 +08:00
/// <summary>限流区域上次记日志的车号,避免同一区域每秒刷屏。</summary>
private readonly Dictionary<string, int> _rateFlowLog = new Dictionary<string, int>();
2026-08-17 15:20:27 +08:00
/// <summary>车辆没有 GetChargeTime 时,用首次看到「充电中」的时刻估算秒数。</summary>
private readonly Dictionary<int, DateTime> _chargeStarted = new Dictionary<int, DateTime>();
2026-08-17 15:20:27 +08:00
/// <summary>未知 SignalType 的日志节流(同站点 10s 一次)。</summary>
private readonly Dictionary<int, DateTime> _unknownTypeLog = new Dictionary<int, DateTime>();
2026-08-17 15:20:27 +08:00
private CancellationTokenSource _cts;
private Thread _thread;
private int _tickCount;
public CarScanWorker(Func<IReadOnlyList<PlcStationRuntime>> runtimes, int tickMs)
{
_runtimes = runtimes ?? throw new ArgumentNullException(nameof(runtimes));
_tickMs = Math.Max(100, tickMs);
}
2026-08-17 15:20:27 +08:00
/// <summary>已完成的扫车拍数,写入进程 status 供监视。</summary>
public int TickCount => _tickCount;
2026-08-17 15:20:27 +08:00
/// <summary>
/// 热替换握手点 / 放行点。过滤掉站点号无效或信号类型为空的握手点。
/// 不中断线程,下一拍生效。
/// </summary>
public void ReplaceConfig(IEnumerable<HandshakePointModel> handshake, IEnumerable<ReleasePointModel> release)
{
lock (_configLock)
{
_handshake = (handshake ?? Array.Empty<HandshakePointModel>())
.Where(x => x != null && x.Sit > 0 && !string.IsNullOrWhiteSpace(x.SignalType))
.GroupBy(x => x.Sit)
.ToDictionary(g => g.Key, g => g.First());
_release = (release ?? Array.Empty<ReleasePointModel>())
.Where(x => x != null && x.Sit > 0)
.GroupBy(x => x.Sit)
.ToDictionary(g => g.Key, g => g.First());
}
}
2026-08-17 15:20:27 +08:00
/// <summary>启动后台扫车线程。重复 Start 若线程仍活则忽略。</summary>
public void Start()
{
if (_thread != null && _thread.IsAlive) return;
_cts = new CancellationTokenSource();
_thread = new Thread(Loop)
{
IsBackground = true,
Name = "SignalScan"
};
_thread.Start();
Diagnosis.Log("扫车握手线程已启动", "Signal", true);
}
2026-08-17 15:20:27 +08:00
/// <summary>取消令牌并最多等 2s 退出。不抛异常,供进程停止时调用。</summary>
public void Stop()
{
try { _cts?.Cancel(); }
catch { }
try { _thread?.Join(2000); }
catch { }
_thread = null;
Diagnosis.Log("扫车握手线程已停止", "Signal", true);
}
public void Dispose() => Stop();
private void Loop()
{
var token = _cts.Token;
while (!token.IsCancellationRequested)
{
try
{
ScanOnce();
Interlocked.Increment(ref _tickCount);
}
catch (Exception ex)
{
Diagnosis.Log($"检测信号停止的AGV是否可以放行异常:{ex.Message}", "Signal", true);
}
try { Thread.Sleep(_tickMs); }
catch (ThreadInterruptedException) { break; }
}
}
2026-08-17 15:20:27 +08:00
/// <summary>
/// 一拍:拷贝配置字典 → 按机构名查找闭包 → 遍历全部车。
/// 查找机构时忽略大小写,与 JSON 里 JgName 对齐。
/// </summary>
internal void ScanOnce()
{
Dictionary<int, HandshakePointModel> handshake;
Dictionary<int, ReleasePointModel> release;
lock (_configLock)
{
handshake = _handshake;
release = _release;
}
2026-08-24 14:55:35 +08:00
PlcStationRuntime Find(string name, string dockId)
{
if (string.IsNullOrWhiteSpace(name))
return null;
var list = _runtimes() ?? Array.Empty<PlcStationRuntime>();
2026-08-24 14:55:35 +08:00
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())
{
if (car == null) continue;
var sit = SignalCarAdapter.PositionId(car);
var spec = ResolveSpec(sit, handshake);
if (SignalCarAdapter.IsStopped(car))
{
if (spec != null)
DispatchStopped(car, spec, Find);
ReleasePointHandler.OnStopped(car, sit, release);
}
else if (spec != null && spec.Is("JGControl"))
{
JgControlHandler.OnMoving(car, spec, Find);
}
}
}
2026-08-17 15:20:27 +08:00
/// <summary>
/// 站点 fields 有 SignalType 则用地图点配置(老 SignalStop);
/// 否则用 handshake-points.json。fields 存在但类型为空视为本站不握手。
/// </summary>
private SignalStopSpec ResolveSpec(int sit, IReadOnlyDictionary<int, HandshakePointModel> handshake)
{
if (sit <= 0) return null;
if (SignalCarAdapter.TryReadSignalStop(sit, out var type, out var parms))
{
if (string.IsNullOrWhiteSpace(type))
return null;
return SignalStopSpec.Parse(type, parms);
}
if (handshake != null && handshake.TryGetValue(sit, out var model))
return SignalStopSpec.From(model);
return null;
}
2026-08-17 15:20:27 +08:00
/// <summary>
/// 停稳分发。TimedCharging 优先:充电点不应再走机构逻辑。
/// 未识别的 SignalType 按站点节流打日志,避免刷屏。
/// </summary>
2026-08-24 14:55:35 +08:00
private void DispatchStopped(Car car, SignalStopSpec spec, Func<string, string, PlcStationRuntime> find)
{
if (spec.Is("TimedCharging"))
{
TimedChargeHandler.OnStopped(car, spec, _chargeStarted);
return;
}
if (spec.Is("JGControl"))
{
JgControlHandler.OnStopped(car, spec, find, _rateFlowLog);
return;
}
if (spec.Is("PointControl"))
{
PointControlHandler.OnStopped(car, spec, find);
return;
}
if (spec.Is("RateFlowControl"))
{
RateFlowHandler.OnStopped(car, spec, _rateFlowLog);
return;
}
var sit = SignalCarAdapter.PositionId(car);
if (_unknownTypeLog.TryGetValue(sit, out var last) && (DateTime.Now - last).TotalSeconds < 10)
return;
_unknownTypeLog[sit] = DateTime.Now;
Diagnosis.Log($"信号类型【{spec.SignalType}】未找到匹配的业务处理模块", "Signal", true);
}
}
}