新增信号交互进程插件

This commit is contained in:
ykkokluo
2026-08-17 15:20:27 +08:00
parent 12c7c1d518
commit cacc5fea5c
25 changed files with 937 additions and 369 deletions
+46 -1
View File
@@ -9,17 +9,43 @@ using StandardScene.Signal.Plc;
namespace StandardScene.Signal.Logic
{
/// <summary>1s 扫全场车,对齐反编译 DefaultPlugin.ExcuteThreadMethod + StarSignManage。</summary>
/// <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
{
/// <summary>取当前全部机构运行态;由 Mission 提供,内部会拷贝列表。</summary>
private readonly Func<IReadOnlyList<PlcStationRuntime>> _runtimes;
/// <summary>扫车间隔,下限 100ms,默认跟进程 TickMs=1000。</summary>
private readonly int _tickMs;
/// <summary>保护握手点 / 放行点字典,热替换配置时与扫车拍互斥。</summary>
private readonly object _configLock = new object();
/// <summary>站点 → 握手点。同一站点多条配置只留第一条。</summary>
private Dictionary<int, HandshakePointModel> _handshake = new Dictionary<int, HandshakePointModel>();
/// <summary>站点 → 放行点。同一站点多条配置只留第一条。</summary>
private Dictionary<int, ReleasePointModel> _release = new Dictionary<int, ReleasePointModel>();
/// <summary>限流区域上次记日志的车号,避免同一区域每秒刷屏。</summary>
private readonly Dictionary<string, int> _rateFlowLog = new Dictionary<string, int>();
/// <summary>车辆没有 GetChargeTime 时,用首次看到「充电中」的时刻估算秒数。</summary>
private readonly Dictionary<int, DateTime> _chargeStarted = new Dictionary<int, DateTime>();
/// <summary>未知 SignalType 的日志节流(同站点 10s 一次)。</summary>
private readonly Dictionary<int, DateTime> _unknownTypeLog = new Dictionary<int, DateTime>();
private CancellationTokenSource _cts;
private Thread _thread;
private int _tickCount;
@@ -30,8 +56,13 @@ namespace StandardScene.Signal.Logic
_tickMs = Math.Max(100, tickMs);
}
/// <summary>已完成的扫车拍数,写入进程 status 供监视。</summary>
public int TickCount => _tickCount;
/// <summary>
/// 热替换握手点 / 放行点。过滤掉站点号无效或信号类型为空的握手点。
/// 不中断线程,下一拍生效。
/// </summary>
public void ReplaceConfig(IEnumerable<HandshakePointModel> handshake, IEnumerable<ReleasePointModel> release)
{
lock (_configLock)
@@ -47,6 +78,7 @@ namespace StandardScene.Signal.Logic
}
}
/// <summary>启动后台扫车线程。重复 Start 若线程仍活则忽略。</summary>
public void Start()
{
if (_thread != null && _thread.IsAlive) return;
@@ -60,6 +92,7 @@ namespace StandardScene.Signal.Logic
Diagnosis.Log("扫车握手线程已启动", "Signal", true);
}
/// <summary>取消令牌并最多等 2s 退出。不抛异常,供进程停止时调用。</summary>
public void Stop()
{
try { _cts?.Cancel(); }
@@ -92,6 +125,10 @@ namespace StandardScene.Signal.Logic
}
}
/// <summary>
/// 一拍:拷贝配置字典 → 按机构名查找闭包 → 遍历全部车。
/// 查找机构时忽略大小写,与 JSON 里 JgName 对齐。
/// </summary>
internal void ScanOnce()
{
Dictionary<int, HandshakePointModel> handshake;
@@ -130,6 +167,10 @@ namespace StandardScene.Signal.Logic
}
}
/// <summary>
/// 站点 fields 有 SignalType 则用地图点配置(老 SignalStop);
/// 否则用 handshake-points.json。fields 存在但类型为空视为本站不握手。
/// </summary>
private SignalStopSpec ResolveSpec(int sit, IReadOnlyDictionary<int, HandshakePointModel> handshake)
{
if (sit <= 0) return null;
@@ -147,6 +188,10 @@ namespace StandardScene.Signal.Logic
return null;
}
/// <summary>
/// 停稳分发。TimedCharging 优先:充电点不应再走机构逻辑。
/// 未识别的 SignalType 按站点节流打日志,避免刷屏。
/// </summary>
private void DispatchStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
{
if (spec.Is("TimedCharging"))
+29 -1
View File
@@ -8,9 +8,28 @@ using StandardScene.Signal.Plc;
namespace StandardScene.Signal.Logic
{
/// <summary>对齐反编译 DefaultPlugin.JGControl / JGJudge / RequestIn / Arrived。</summary>
/// <summary>
/// 机构进出站握手。对齐反编译 <c>DefaultPlugin.JGControl</c> / <c>JGJudge</c> / <c>RequestIn</c> / <c>Arrived</c>。
/// </summary>
/// <remarks>
/// <para>
/// <c>SignalParams</c> 分号三段:<c>机构名;动作;限流</c>。
/// 动作 <c>RequestIn</c>(请求进入,配在机构口外等待点)或 <c>Arrived</c>(到位,配在机构内地标)。
/// 第三段可选,格式与独立限流不同:<c>区域-车组-限额-点1,点2</c>(短横分隔,见 <see cref="RateFlowHandler"/>)。
/// </para>
/// <para>
/// RequestIn 放行条件(需同时满足):PLC 读字节=允许进入、尚无请求进入车、写字节=无状态。
/// 下线机构额外要求 <c>JgPointId</c> 上没有其它车。通过后写「进入中」、记下 RequestInAgvNo、停充并启动。
/// </para>
/// <para>
/// 车已在跑但本车不是请求进入车、且 PLC 不是允许进入时,立即停车(停稳拍与行驶拍都会判)。
/// Arrived:若 InAgvNo 还不是本车,写「到位」、登记 InAgvNo、清空 RequestInAgvNo。
/// 之后的「离开中 / 已离开」由 PLC Worker 的 <see cref="PlcStationLogic.AutoLeave"/> 推进。
/// </para>
/// </remarks>
public static class JgControlHandler
{
/// <summary>停稳:可选限流 → RequestIn 或 Arrived。</summary>
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find, IDictionary<string, int> rateFlowLog)
{
var jgName = spec.Part(0).Trim();
@@ -34,6 +53,7 @@ namespace StandardScene.Signal.Logic
Arrived(jgName, car, find);
}
/// <summary>行驶中只做 RequestIn 安全闸:未允许进入且不是本车占用请求槽则停车。</summary>
public static void OnMoving(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
{
var jgName = spec.Part(0).Trim();
@@ -44,6 +64,10 @@ namespace StandardScene.Signal.Logic
RequestInJudge(jgName, car, find);
}
/// <summary>
/// 请求进入。锁机构运行态,避免与 PLC Worker 同时改写字节 / 车号。
/// 后半段与 <see cref="RequestInJudge"/> 相同,覆盖「刚放行后又被调度层启动」的竞态。
/// </summary>
private static void RequestIn(string jgName, Car car, Func<string, PlcStationRuntime> find)
{
var rt = find(jgName);
@@ -89,6 +113,7 @@ namespace StandardScene.Signal.Logic
}
}
/// <summary>行驶中的 RequestIn 安全闸,条件与 RequestIn 后半段一致。</summary>
private static void RequestInJudge(string jgName, Car car, Func<string, PlcStationRuntime> find)
{
var rt = find(jgName);
@@ -110,6 +135,9 @@ namespace StandardScene.Signal.Logic
}
}
/// <summary>
/// 到位。InAgvNo 已是本车则认为已报到,避免每秒重复写「到位」把 WriteSuccess 清掉。
/// </summary>
private static void Arrived(string jgName, Car car, Func<string, PlcStationRuntime> find)
{
var rt = find(jgName);
@@ -6,9 +6,18 @@ using StandardScene.Signal.Plc;
namespace StandardScene.Signal.Logic
{
/// <summary>对齐反编译 DefaultPlugin.PointControl:牵引棒升/降到位后再放行。</summary>
/// <summary>
/// 销钉 / 牵引棒升降后再放行。对齐反编译 <c>DefaultPlugin.PointControl</c>。
/// </summary>
/// <remarks>
/// <c>SignalParams</c><c>动作;机构名</c>。
/// 动作:<c>UP1</c> 升销钉1<c>UP2</c> 升销钉1+2<c>Down1</c> / <c>Down2</c> 对应下降。
/// 机构名可选:填了则必须等该机构读字节=允许离开才启动;空则只等销钉动作完成。
/// 销钉指令通过 <see cref="SignalCarAdapter.Control"/> 反射调用车体 MethodMember(如 SetPin1Up)。
/// </remarks>
public static class PointControlHandler
{
/// <summary>停稳:先判机构是否允许离开,再下发销钉,全部到位后启动。</summary>
public static void OnStopped(Car car, SignalStopSpec spec, Func<string, PlcStationRuntime> find)
{
var action = spec.Part(0).Trim();
@@ -48,6 +57,10 @@ namespace StandardScene.Signal.Logic
}
}
/// <summary>
/// 任一销钉未完成则本拍只下发指令、不启动;
/// 全部完成且(无需等离开 或 已允许离开)才 Start。
/// </summary>
private static void PinThenGo(Car car, int pos, bool waitLeave, string jgName, params string[] actions)
{
var done = true;
+11 -2
View File
@@ -8,11 +8,17 @@ using StandardScene.Signal.Plc;
namespace StandardScene.Signal.Logic
{
/// <summary>
/// 对齐反编译 DefaultPlugin.RateFlowControl。
/// 独立类型用分号:区域;车组;限额;点1,点2。JGControl 第三段用短横:区域-车组-限额-点1,点2。
/// 区域车组限流。对齐反编译 <c>DefaultPlugin.RateFlowControl</c>
/// </summary>
/// <remarks>
/// 独立握手类型 <c>RateFlowControl</c> 用分号:<c>区域;车组;限额;点1,点2</c>。
/// 挂在 JGControl 第三段时用短横:<c>区域-车组-限额-点1,点2</c>(因为前两段机构名/动作已经占用分号)。
/// 统计「站点列表上、同车组、且不是本车」的数量,小于限额才放行。
/// 解析异常时放行(return true),避免配错把产线卡死。
/// </remarks>
public static class RateFlowHandler
{
/// <summary>独立限流点:通过则停充并启动。</summary>
public static void OnStopped(Car car, SignalStopSpec spec, IDictionary<string, int> logByArea)
{
if (AllowPass(car, spec.SignalParams, logByArea, sep: ';'))
@@ -22,9 +28,11 @@ namespace StandardScene.Signal.Logic
}
}
/// <summary>JGControl 第三段限流,短横分隔。不通过时调用方应直接 return,不启动。</summary>
public static bool AllowPass(Car car, string rateFlowString, IDictionary<string, int> logByArea) =>
AllowPass(car, rateFlowString, logByArea, sep: '-');
/// <param name="sep">独立类型用 ';'JGControl 挂载用 '-'。</param>
private static bool AllowPass(Car car, string raw, IDictionary<string, int> logByArea, char sep)
{
try
@@ -71,6 +79,7 @@ namespace StandardScene.Signal.Logic
}
}
/// <summary>同一限流区域仅在「决策车号变化」时再打一条带原始参数的日志。</summary>
private static void Note(IDictionary<string, int> logByArea, string area, Car car, string raw)
{
if (logByArea == null) return;
@@ -5,9 +5,17 @@ using StandardScene.Signal.Plc;
namespace StandardScene.Signal.Logic
{
/// <summary>对齐反编译 StarSignManage:配置点停稳则停充并放行。</summary>
/// <summary>
/// 无条件放行点。对齐反编译 <c>StarSignManage</c> / <c>StartSignModel</c>。
/// </summary>
/// <remarks>
/// 车在配置站点停稳且 <see cref="ReleasePointModel.IsUse"/> 为 true 时:停充并启动,不读 PLC。
/// 用于任务在某点停住、充完电或人工暂停后需要自动续跑的站点。
/// 机构口应配握手点,不要同时配放行点——扫车线程在握手 Handler 之后仍会执行本方法,可能提前开走。
/// </remarks>
public static class ReleasePointHandler
{
/// <summary>按当前站点号查放行表;未配置或未启用则什么都不做。</summary>
public static void OnStopped(Car car, int sit, IReadOnlyDictionary<int, ReleasePointModel> points)
{
if (points == null || !points.TryGetValue(sit, out var p) || p == null || !p.IsUse)
+22 -1
View File
@@ -3,13 +3,31 @@ using StandardScene.Signal.Model;
namespace StandardScene.Signal.Logic
{
/// <summary>握手点解析结果。参数格式与反编译 SignalStop.SignalParams 一致。</summary>
/// <summary>
/// 一条握手点配置的解析结果。参数格式与反编译 <c>SignalStop.SignalParams</c> 一致。
/// </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 Part(int index)
{
if (index < 0 || index >= Parts.Length)
@@ -17,6 +35,7 @@ namespace StandardScene.Signal.Logic
return Parts[index] ?? "";
}
/// <summary>从类型名和参数串构造;类型会 Trim,参数保持原样以便日志对照现场 JSON。</summary>
public static SignalStopSpec Parse(string signalType, string signalParams)
{
var type = (signalType ?? "").Trim();
@@ -29,6 +48,7 @@ namespace StandardScene.Signal.Logic
};
}
/// <summary>从握手点 Model 构造;model 为 null 时返回 null。</summary>
public static SignalStopSpec From(HandshakePointModel model)
{
if (model == null)
@@ -36,6 +56,7 @@ namespace StandardScene.Signal.Logic
return Parse(model.SignalType, model.SignalParams);
}
/// <summary>类型名是否匹配(忽略大小写)。</summary>
public bool Is(string type) =>
string.Equals(SignalType, type, StringComparison.OrdinalIgnoreCase);
}
@@ -6,9 +6,18 @@ using StandardScene.Signal.Plc;
namespace StandardScene.Signal.Logic
{
/// <summary>对齐反编译 DefaultPlugin.TimedCharging:时长 / 上限电量 / 下限电量。</summary>
/// <summary>
/// 站点定时充电。对齐反编译 <c>DefaultPlugin.TimedCharging</c>。
/// </summary>
/// <remarks>
/// <c>SignalParams</c><c>最长秒数;上限电量;下限电量</c>,某段填 0 表示该条件不启用。
/// 正在充电:电量 ≥ 上限则停充;时长到且电量已过下限则停充;时长到但电量仍 ≤ 下限则继续充。
/// 已停充且车仍停着:视为充电流程结束,启动放行。
/// 充电秒数优先反射 <c>GetChargeTime</c>,没有则用 <paramref name="chargeStarted"/> 字典估算。
/// </remarks>
public static class TimedChargeHandler
{
/// <summary>停稳且本站是 TimedCharging 时由扫车线程调用。</summary>
public static void OnStopped(Car car, SignalStopSpec spec, IDictionary<int, DateTime> chargeStarted)
{
int.TryParse(spec.Part(0), out var maxSeconds);