Files
StandardSence/StandardScene.Magnetic/Scheduler/EventCarMission.cs
T

252 lines
9.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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];
}
}
}