252 lines
9.6 KiB
C#
252 lines
9.6 KiB
C#
using LessokajiWeaverUtilities.Utilities;
|
||
using Newtonsoft.Json;
|
||
using SimpleLite.RCS;
|
||
using SimpleLite.Props;
|
||
using SimpleCore;
|
||
using SimpleCore.Library;
|
||
using SimpleCore.PropType;
|
||
using StandardScene;
|
||
using StandardScene.Scheduler.FassEvent;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using SimpleLite.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>进程运行状态扩展字段。</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 秒)。</summary>
|
||
[MethodMember(Name = "停止进程", Description = "停止 FASS 车辆事件轮询")]
|
||
public void Stop()
|
||
{
|
||
_started = false;
|
||
try
|
||
{
|
||
_workerThread?.Join(2000);
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
|
||
status.status = "已停止";
|
||
Diagnosis.Post("FASS车辆事件进程已停止", "FassEventCar", true);
|
||
}
|
||
|
||
/// <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,构建快照并异步调用 Handler(与 FASS Task.Run 行为一致)。
|
||
/// <para>
|
||
/// 跳过 status 为空或 usage.refreshing=false 的车辆(未参与调度刷新的车不处理)。
|
||
/// 当地标变化时清除 <see cref="FassEventCarHandler.ActiveTaskTag"/>,允许在新地标重新触发事件。
|
||
/// </para>
|
||
/// </summary>
|
||
private void ProcessAllCars()
|
||
{
|
||
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));
|
||
|
||
Task.Run(() => _handler.HandleCarTimerTick(car, snapshot, _eventCars));
|
||
|
||
if (!string.Equals(prevNode, currentNode, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
_prevNodeByCarId[car.id] = currentNode;
|
||
ClearTaskTags(car);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <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];
|
||
}
|
||
}
|
||
}
|