上一拍未结束则跳过本拍,避免断网重试堆积;Stop 调用 base.Stop 并清空工作线程。 Co-authored-by: Cursor <cursoragent@cursor.com>
286 lines
11 KiB
C#
286 lines
11 KiB
C#
using LessokajiWeaverUtilities.Utilities;
|
||
using Newtonsoft.Json;
|
||
using Simple3.RCS;
|
||
using Simple3.Props;
|
||
using SimpleCore;
|
||
using SimpleCore.Library;
|
||
using SimpleCore.PropType;
|
||
using StandardScene;
|
||
using StandardScene.Scheduler.FassEvent;
|
||
using System;
|
||
using System.Collections.Concurrent;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Simple3.RCS.CarTypes;
|
||
|
||
|
||
namespace StandardScene.Scheduler
|
||
{
|
||
/// <summary>
|
||
/// FASS 车辆事件调度进程。
|
||
/// <para>
|
||
/// 移植自 <c>fass/FASS.Scheduler/Services/Events/EventCarService.cs</c>:
|
||
/// 按 JSON 配置定时轮询全场车辆,匹配触发条件后执行交管/PLC/电量等条件判断,
|
||
/// 最终对 MagCar 下发 Start(放行)或 Stop(停车)。
|
||
/// </para>
|
||
/// <para>
|
||
/// 使用方式:在场景中添加本 Mission →「启动进程」→ 默认加载 <c>Configs\EventCar\FG2512073.json</c>。
|
||
/// </para>
|
||
/// </summary>
|
||
[MissionType(Name = "FASS车辆事件进程", editor = typeof(EventCarMission))]
|
||
[I18N.DocumentTranslation(Name = "FASS Event Car Mission", locale = "en")]
|
||
public class EventCarMission : Mission
|
||
{
|
||
/// <summary>EventCar JSON 配置文件路径(相对程序目录或绝对路径)。</summary>
|
||
[FieldMember] public string EventConfigPath = "Config\\EventCar\\traffic.json";
|
||
|
||
/// <summary>主循环轮询间隔(毫秒),对应 FASS TimerTick 频率,默认 500ms。</summary>
|
||
[FieldMember] public int TickIntervalMs = 500;
|
||
|
||
/// <summary>与下料站/安全信号交互用的西门子 PLC IP。</summary>
|
||
[FieldMember] public string PlcIpAddress = "127.0.0.1";
|
||
|
||
/// <summary>PLC 端口,S7 默认 102。</summary>
|
||
[FieldMember] public int PlcPort = 102;
|
||
|
||
/// <summary>PLC 型号字符串,如 S7_1500、S7_300,传给 IoTClient SiemensClient。</summary>
|
||
[FieldMember] public string PlcVersion = "S7_1500";
|
||
|
||
/// <summary>是否启用 ME11 下料站 PLC 信号交互(false 时 requestStaion/InplaceStaion 等直接返回 true)。</summary>
|
||
[FieldMember] public bool IsEnableMEll = true;
|
||
|
||
|
||
/// <summary>充电完成放行默认电量阈值(%),可被事件内 ChargeStart 参数覆盖。</summary>
|
||
[FieldMember] public double ChargeStartThreshold = 93;
|
||
|
||
|
||
[JsonIgnore] private bool _started;
|
||
|
||
[JsonIgnore] private Thread _workerThread;
|
||
|
||
[JsonIgnore] private FassEventCarHandler _handler;
|
||
|
||
/// <summary>当前生效的车辆事件配置列表(TriggerSource=Car 且 TriggerEnable=true)。</summary>
|
||
[JsonIgnore] private List<FassEventCarConfig> _eventCars = new List<FassEventCarConfig>();
|
||
|
||
/// <summary>记录每辆车上一周期的 CurrentNode,用于 PrevNode 触发与地标变化时清除任务防重复标记。</summary>
|
||
[JsonIgnore] private readonly Dictionary<int, string> _prevNodeByCarId = new Dictionary<int, string>();
|
||
|
||
/// <summary>每车最多一个在飞 Tick,避免断网重试把 Task.Run 堆爆、同一车并发下发。</summary>
|
||
[JsonIgnore] private readonly ConcurrentDictionary<int, byte> _tickInFlight = new ConcurrentDictionary<int, byte>();
|
||
|
||
/// <summary>进程运行状态扩展字段。</summary>
|
||
public class EventCarMissionStatus : MissionStatus
|
||
{
|
||
/// <summary>主循环已执行次数。</summary>
|
||
public int TickCount { get; set; }
|
||
|
||
/// <summary>已加载的事件规则条数。</summary>
|
||
public int LoadedEventCount { get; set; }
|
||
|
||
/// <summary>实际加载的配置文件绝对/解析路径。</summary>
|
||
public string ConfigPath { get; set; }
|
||
}
|
||
|
||
public override MissionStatus status { get; set; } = new EventCarMissionStatus();
|
||
|
||
public static Mission Create()
|
||
{
|
||
return new EventCarMission();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动后台轮询线程:加载配置 → 创建 Handler → 周期性 ProcessAllCars。
|
||
/// </summary>
|
||
[MethodMember(Name = "启动进程", Description = "加载 FASS EventCar 配置并开始轮询车辆事件")]
|
||
public override void Execute()
|
||
{
|
||
if (_started)
|
||
{
|
||
status.status = "已在运行";
|
||
return;
|
||
}
|
||
|
||
try
|
||
{
|
||
ReloadConfig();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
status.status = "配置加载失败";
|
||
Diagnosis.Post($"FASS车辆事件进程配置加载失败:{ExceptionFormatter.FormatEx(ex)}", "FassEventCar", true);
|
||
throw;
|
||
}
|
||
|
||
_handler = new FassEventCarHandler(this);
|
||
_started = true;
|
||
status.status = "运行中";
|
||
|
||
_workerThread = new Thread(WorkerLoop)
|
||
{
|
||
IsBackground = true,
|
||
Name = "EventCarMission"
|
||
};
|
||
_workerThread.Start();
|
||
Diagnosis.Post($"FASS车辆事件进程已启动,配置数={_eventCars.Count}", "FassEventCar", true);
|
||
}
|
||
|
||
/// <summary>热加载 JSON 配置,运行中也可调用。</summary>
|
||
[MethodMember(Name = "重新加载配置", Description = "重新读取 EventCar JSON 配置")]
|
||
public void ReloadConfig()
|
||
{
|
||
var path = ResolveConfigPath();
|
||
_eventCars = FassEventConfigLoader.Load(path)
|
||
.Where(item => item.TriggerEnable && string.Equals(item.TriggerSource, "Car", StringComparison.OrdinalIgnoreCase))
|
||
.ToList();
|
||
|
||
var missionStatus = (EventCarMissionStatus)status;
|
||
missionStatus.LoadedEventCount = _eventCars.Count;
|
||
missionStatus.ConfigPath = path;
|
||
status.status = _started ? "运行中" : "配置已加载";
|
||
Diagnosis.Post($"FASS EventCar 配置已加载:{path},事件数={_eventCars.Count}", "FassEventCar", true);
|
||
}
|
||
|
||
/// <summary>停止轮询并等待工作线程结束(最多 2 秒)。覆盖基类 Stop,避免全量重载后线程仍在对全场车下发。</summary>
|
||
[MethodMember(Name = "停止进程", Description = "停止 FASS 车辆事件轮询")]
|
||
public override void Stop()
|
||
{
|
||
_started = false;
|
||
try
|
||
{
|
||
_workerThread?.Join(2000);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
|
||
_workerThread = null;
|
||
Diagnosis.Post("FASS车辆事件进程已停止", "FassEventCar", true);
|
||
base.Stop();
|
||
status.status = "已停止";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 后台主循环:每 TickIntervalMs 扫描一次全场车辆。
|
||
/// 单轮异常不会终止进程,仅写诊断日志。
|
||
/// </summary>
|
||
private void WorkerLoop()
|
||
{
|
||
while (_started)
|
||
{
|
||
try
|
||
{
|
||
ProcessAllCars();
|
||
var missionStatus = (EventCarMissionStatus)status;
|
||
missionStatus.TickCount++;
|
||
status.status = $"运行中:{missionStatus.TickCount}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Post($"FASS车辆事件进程循环异常:{ExceptionFormatter.FormatEx(ex)}", "FassEventCar", true);
|
||
}
|
||
|
||
Thread.Sleep(Math.Max(100, TickIntervalMs));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 遍历所有 Car,构建快照后 Task.Run 处理:一车断网不堵其它车。
|
||
/// 同一车上一拍未结束则跳过本拍,禁止无界堆积。
|
||
/// </summary>
|
||
private void ProcessAllCars()
|
||
{
|
||
var handler = _handler;
|
||
var eventCars = _eventCars;
|
||
if (handler == null || eventCars == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
|
||
{
|
||
if (car.status == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var usage = car.status.usage?.Get();
|
||
if (usage != null && usage.refreshing == false)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var currentNode = FassEventCarSnapshotFactory.GetCurrentNodeCode(car);
|
||
_prevNodeByCarId.TryGetValue(car.id, out var prevNode);
|
||
var snapshot = FassEventCarSnapshotFactory.Create(car, prevNode, FassEventCarSnapshotFactory.GetNextNodeCode(car));
|
||
|
||
if (!string.Equals(prevNode, currentNode, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
_prevNodeByCarId[car.id] = currentNode;
|
||
ClearTaskTags(car);
|
||
}
|
||
|
||
if (!_tickInFlight.TryAdd(car.id, 0))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
var carId = car.id;
|
||
Task.Run(() =>
|
||
{
|
||
try
|
||
{
|
||
if (_started)
|
||
handler.HandleCarTimerTick(car, snapshot, eventCars);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Post(
|
||
$"FASS车辆事件处理异常 car={carId}:{ExceptionFormatter.FormatEx(ex)}",
|
||
"FassEventCar",
|
||
true);
|
||
}
|
||
finally
|
||
{
|
||
_tickInFlight.TryRemove(carId, out _);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>车辆换地标后删除防重复执行标签,使同一规则可在新站点再次触发。</summary>
|
||
private static void ClearTaskTags(Car car)
|
||
{
|
||
if (car.tags == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
Commons.DeleteTag(car.tags, FassEventCarHandler.ActiveTaskTag);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析配置文件路径:绝对路径直接用;相对路径依次尝试 BaseDirectory、CurrentDirectory、上级目录。
|
||
/// </summary>
|
||
private string ResolveConfigPath()
|
||
{
|
||
if (Path.IsPathRooted(EventConfigPath))
|
||
{
|
||
return EventConfigPath;
|
||
}
|
||
|
||
var candidates = new[]
|
||
{
|
||
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, EventConfigPath),
|
||
Path.Combine(Directory.GetCurrentDirectory(), EventConfigPath),
|
||
Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "..", EventConfigPath))
|
||
};
|
||
|
||
return candidates.FirstOrDefault(File.Exists) ?? candidates[0];
|
||
}
|
||
}
|
||
}
|