578 lines
23 KiB
C#
578 lines
23 KiB
C#
using IoTClient.Clients.PLC;
|
||
using IoTClient.Common.Enums;
|
||
using LessokajiWeaverUtilities.Utilities;
|
||
using SimpleLite.RCS;
|
||
using SimpleCore.Library;
|
||
using SimpleCore.PropType;
|
||
using StandardScene;
|
||
using StandardScene.CarTypes;
|
||
using StandardScene.Scheduler;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using SimpleLite.RCS.CarTypes;
|
||
|
||
namespace StandardScene.Scheduler.FassEvent
|
||
{
|
||
/// <summary>
|
||
/// FASS 车辆事件核心处理器,逻辑对齐 <c>EventCarService</c>。
|
||
/// <para>处理流程:TriggerHandler(触发匹配)→ ConditionHandler(条件 AND)→ ExecuteTask(Start/Stop)。</para>
|
||
/// </summary>
|
||
internal sealed class FassEventCarHandler
|
||
{
|
||
/// <summary>
|
||
/// 防重复执行任务的车辆标签名。同一地标周期内只执行一次,换地标后由 EventCarMission 清除。
|
||
/// 标签值为 <see cref="BuildTaskKey"/> 生成的任务指纹。
|
||
/// </summary>
|
||
internal const string ActiveTaskTag = "FassEvent:Active";
|
||
|
||
private readonly EventCarMission _mission;
|
||
|
||
/// <summary>PLC 读写串行锁,避免多车并发条件中同时 Open/Close 同一连接。</summary>
|
||
private readonly object _plcLock = new object();
|
||
|
||
/// <summary>JSON ConditionMethodName → 内置条件方法 映射表。</summary>
|
||
private readonly Dictionary<string, Func<FassEventCarSnapshot, FassConditionEventConfig, bool>> _eventMethods;
|
||
|
||
public FassEventCarHandler(EventCarMission mission)
|
||
{
|
||
_mission = mission;
|
||
_eventMethods = BuildEventMethods();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 单次 TimerTick 对一辆车的处理入口(通常在 Task.Run 中调用)。
|
||
/// 仅处理 TriggerEvent=TimerTick 的配置项。
|
||
/// </summary>
|
||
public void HandleCarTimerTick(Car car, FassEventCarSnapshot snapshot, IReadOnlyList<FassEventCarConfig> eventCars)
|
||
{
|
||
foreach (var eventConfig in eventCars.Where(item => string.Equals(item.TriggerEvent, "TimerTick", StringComparison.OrdinalIgnoreCase)))
|
||
{
|
||
if (!TriggerHandler(snapshot, eventConfig))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!ConditionHandler(car, snapshot, eventConfig))
|
||
{
|
||
continue;
|
||
}
|
||
|
||
if (!eventConfig.TaskEnable)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
ExecuteTask(car, snapshot, eventConfig);
|
||
}
|
||
}
|
||
|
||
/// <summary>注册与 FASS EventCarService.InitEventMethod 一致的条件方法名(保留历史拼写)。</summary>
|
||
private Dictionary<string, Func<FassEventCarSnapshot, FassConditionEventConfig, bool>> BuildEventMethods()
|
||
{
|
||
return new Dictionary<string, Func<FassEventCarSnapshot, FassConditionEventConfig, bool>>(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
["NodeAllUnlockedNotCar"] = NodeAllUnlockedNotCar,
|
||
["NodeAllUnlocked"] = NodeAllUnlocked,
|
||
["NodeAnyLockedNotCar"] = NodeAnyLockedNotCar,
|
||
["NodeAnyLocked"] = NodeAnyLocked,
|
||
["NodeAllUnlockedNotCarNotEndCarState"] = NodeAllUnlockedNotCarNotEndCarState,
|
||
["requestStaion"] = RequestStation,
|
||
["clearStaion"] = ClearStation,
|
||
["EmegerStop"] = EmergencyStop,
|
||
["EmegerStart"] = EmergencyStart,
|
||
["IsExistRunningStateCar"] = IsExistRunningStateCar,
|
||
["ChargeStart"] = ChargeStart
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 触发器匹配:车辆编号、前后地标、状态、角度等 CSV 过滤。
|
||
/// 配置项为空表示“不限制”;车辆对应字段为空则无法匹配非空配置。
|
||
/// </summary>
|
||
private bool TriggerHandler(FassEventCarSnapshot snapshot, FassEventCarConfig config)
|
||
{
|
||
if (!MatchesCsvFilter(config.TriggerCarCode, snapshot.Code))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!MatchesCsvFilter(config.TriggerCarPrevNodeCode, snapshot.PrevNodeCode))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!MatchesCsvFilter(config.TriggerCarCurrentNodeCode, snapshot.CurrentNodeCode))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!MatchesCsvFilter(config.TriggerCarNextNodeCode, snapshot.NextNodeCode))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!MatchesCsvFilter(config.TriggerCarState, snapshot.State))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!MatchesCsvFilter(config.TriggerCarAngle, snapshot.Angle.ToString(CultureInfo.InvariantCulture)))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>所有 ConditionEvents 必须全部通过(逻辑与)。</summary>
|
||
private bool ConditionHandler(Car car, FassEventCarSnapshot snapshot, FassEventCarConfig config)
|
||
{
|
||
if (config.ConditionEvents == null || config.ConditionEvents.Count == 0)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
foreach (var condition in config.ConditionEvents)
|
||
{
|
||
if (!SingleConditionHandler(car, snapshot, condition))
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>单条条件分发:Method / Assembly / Plugin。</summary>
|
||
private bool SingleConditionHandler(Car car, FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
if (!condition.ConditionEnable)
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var conditionType = condition.ConditionType ?? "Method";
|
||
if (string.Equals(conditionType, "Method", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return MethodInvoke(snapshot, condition);
|
||
}
|
||
|
||
if (string.Equals(conditionType, "Assembly", StringComparison.OrdinalIgnoreCase) ||
|
||
string.Equals(conditionType, "Plugin", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
LogSignal($"暂不支持的 ConditionType={conditionType}, Method={condition.ConditionMethodName}");
|
||
return false;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>按方法名调用内置条件实现。</summary>
|
||
private bool MethodInvoke(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(condition.ConditionMethodName))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (_eventMethods.TryGetValue(condition.ConditionMethodName, out var handler))
|
||
{
|
||
return handler(snapshot, condition);
|
||
}
|
||
|
||
LogSignal($"未注册的条件方法:{condition.ConditionMethodName}");
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行任务:MagCar 调用 SendStart/SendStop;其他车型仅打日志。
|
||
/// 通过 ActiveTaskTag 防止同一地标周期内重复下发。
|
||
/// </summary>
|
||
private void ExecuteTask(Car car, FassEventCarSnapshot snapshot, FassEventCarConfig config)
|
||
{
|
||
if (car.tags != null && car.tags.Contains(ActiveTaskTag))
|
||
{
|
||
return;
|
||
}
|
||
|
||
var taskType = config.TaskType ?? "Start";
|
||
if (car is MagCar magCar)
|
||
{
|
||
switch (taskType)
|
||
{
|
||
case "Start":
|
||
magCar.SendStart((ushort)Math.Round(magCar.th));
|
||
break;
|
||
case "Stop":
|
||
magCar.SendStop(0);
|
||
break;
|
||
default:
|
||
LogTraffic($"车辆[{snapshot.Code}] 收到任务类型[{taskType}],当前仅显式处理 Start/Stop");
|
||
break;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
LogTraffic($"车辆[{snapshot.Code}] 非 MagCar,任务类型[{taskType}] 仅记录标签");
|
||
}
|
||
|
||
var taskKey = BuildTaskKey(snapshot, config);
|
||
Commons.AddOrUpdateTag(car.tags, ActiveTaskTag, taskKey);
|
||
LogTraffic($"车辆[{snapshot.Code}] 触发事件任务:{taskType} @ node={snapshot.CurrentNodeCode}, state={snapshot.State}");
|
||
}
|
||
|
||
/// <summary>生成任务指纹,写入 ActiveTaskTag 的值,便于日志追溯。</summary>
|
||
private static string BuildTaskKey(FassEventCarSnapshot snapshot, FassEventCarConfig config)
|
||
{
|
||
return $"FassEvent:{snapshot.CurrentNodeCode}:{config.TriggerCarState}:{config.TaskType}:{string.Join("|", config.ConditionEvents?.Select(item => item.ConditionMethodName) ?? Array.Empty<string>())}";
|
||
}
|
||
|
||
#region 交管条件(节点锁)
|
||
|
||
/// <summary>指定节点全部未被其他车辆占用(排除自身)。参数:逗号分隔节点号。</summary>
|
||
private bool NodeAllUnlockedNotCar(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
return FassEventNodeHelper.NodeAllUnlockedNotCar(snapshot.Code, SplitArgs(condition.ConditionMethodArgs));
|
||
}
|
||
|
||
/// <summary>指定节点全部空闲(含自身锁也计入)。</summary>
|
||
private bool NodeAllUnlocked(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
return FassEventNodeHelper.NodeAllUnlocked(SplitArgs(condition.ConditionMethodArgs));
|
||
}
|
||
|
||
/// <summary>指定节点中存在被其他车占用的点。</summary>
|
||
private bool NodeAnyLockedNotCar(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
return FassEventNodeHelper.NodeAnyLockedNotCar(snapshot.Code, SplitArgs(condition.ConditionMethodArgs));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 指定节点存在占用时先 SendStart 再判断(与 FASS NodeAnyLocked 行为一致)。
|
||
/// </summary>
|
||
private bool NodeAnyLocked(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
var car = SimpleCore.SimpleLib.GetCar(snapshot.CarId) as MagCar;
|
||
car?.SendStart((ushort)Math.Round(car.th));
|
||
return FassEventNodeHelper.NodeAnyLocked(SplitArgs(condition.ConditionMethodArgs));
|
||
}
|
||
|
||
/// <summary>节点全解锁且路径上无其他运行中车辆。</summary>
|
||
private bool NodeAllUnlockedNotCarNotEndCarState(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
return FassEventNodeHelper.NodeAllUnlockedNotCarNotEndCarState(snapshot.Code, "Runing", SplitArgs(condition.ConditionMethodArgs));
|
||
}
|
||
|
||
/// <summary>判断指定节点范围是否不存在其他“运行中”车辆(返回 true 表示可以放行)。</summary>
|
||
private bool IsExistRunningStateCar(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
var param = SplitArgs(condition.ConditionMethodArgs);
|
||
LogTraffic($"IsExistRunningStateCar 参数:{condition.ConditionMethodArgs}");
|
||
return !FassEventNodeHelper.ExistsRunningCarOnNodes(snapshot.Code, "Runing", param);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 电量与安全
|
||
|
||
/// <summary>
|
||
/// 电量达到阈值方可放行。阈值优先取 ConditionMethodArgs 首参,否则用 Mission.ChargeStartThreshold。
|
||
/// </summary>
|
||
private bool ChargeStart(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
var threshold = _mission.ChargeStartThreshold;
|
||
var configured = SplitArgs(condition.ConditionMethodArgs).FirstOrDefault();
|
||
if (!string.IsNullOrWhiteSpace(configured) &&
|
||
double.TryParse(configured, NumberStyles.Float, CultureInfo.InvariantCulture, out var eventThreshold))
|
||
{
|
||
threshold = eventThreshold;
|
||
}
|
||
|
||
var result = snapshot.Charge >= threshold;
|
||
if (result)
|
||
{
|
||
LogSignal($"ChargeStart 车辆[{snapshot.Code}] 电量[{snapshot.Charge}] >= 阈值[{threshold}]");
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设备安全急停:读 PLC 布尔位,false 表示故障需停车(条件返回 true 以触发 Stop 任务)。
|
||
/// 参数 param[0]:PLC 地址。
|
||
/// </summary>
|
||
private bool EmergencyStop(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
return ExecutePlcCondition(snapshot, condition, 1, (client, param) =>
|
||
{
|
||
var allowIn = client.ReadBoolean(param[0]).Value;
|
||
if (!allowIn)
|
||
{
|
||
LogSignal($"设备安全故障急停:{allowIn}");
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设备安全恢复:PLC 布尔位为 true 时允许 Start。
|
||
/// </summary>
|
||
private bool EmergencyStart(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
return ExecutePlcCondition(snapshot, condition, 1, (client, param) =>
|
||
{
|
||
var allowIn = client.ReadBoolean(param[0]).Value;
|
||
if (allowIn)
|
||
{
|
||
LogSignal($"设备安全恢复放行:{allowIn}");
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
});
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region ME11 下料站 PLC 交互
|
||
|
||
/// <summary>
|
||
/// 清除站点到位与离开中信号。
|
||
/// 参数:param[0] 到位信号地址,param[1] 离开中信号地址。
|
||
/// </summary>
|
||
private bool ClearStation(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
var context = BuildStationLogContext(nameof(ClearStation), snapshot, condition);
|
||
LogSignal($"{context} 开始清除站点到位/离开中信号");
|
||
if (!_mission.IsEnableMEll)
|
||
{
|
||
LogSignal($"车辆[{snapshot.Code}] ME11 未启用,直接返回 true");
|
||
return true;
|
||
}
|
||
|
||
return ExecutePlcCondition(snapshot, condition, 2, (client, param) =>
|
||
{
|
||
var afterClear = WriteAndRead(client, param[0], false, "清除到位信号", context);
|
||
WriteStationSignal(client, param[1], false, "清除离开中信号", context);
|
||
var leavingClear = client.ReadBoolean(param[1]).Value;
|
||
var result = !afterClear && !leavingClear;
|
||
LogSignal($"{context} 清除站点信号结束,返回[{result}]");
|
||
return result;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 请求进入工位:读“请求进入”,为 true 时写“正在进入中”,并以进入中信号作为放行条件结果。
|
||
/// 参数:param[0] 请求进入,param[1] 正在进入中。
|
||
/// </summary>
|
||
private bool RequestStation(FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
var context = BuildStationLogContext(nameof(RequestStation), snapshot, condition);
|
||
LogSignal($"{context} 开始请求进入信号判断");
|
||
if (!_mission.IsEnableMEll)
|
||
{
|
||
LogSignal($"车辆[{snapshot.Code}] ME11 未启用,直接返回 true");
|
||
return true;
|
||
}
|
||
|
||
return ExecutePlcCondition(snapshot, condition, 2, (client, param) =>
|
||
{
|
||
var requestIn = ReadStationSignal(client, param[0], "请求进入", context);
|
||
if (requestIn)
|
||
{
|
||
WriteStationSignal(client, param[1], true, "正在进入中", context);
|
||
}
|
||
|
||
var result = ReadStationSignal(client, param[1], "正在进入中", context);
|
||
LogSignal($"{context} 请求进入判断结束,返回[{result}]");
|
||
return result;
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 到位与允许离开交互(ME11 下料核心逻辑)。
|
||
/// <para>
|
||
/// 参数:param[0] 到位,param[1] 允许离开,param[2] 正在进入中,param[3] 正在离开中。
|
||
/// ME11 车型:气缸伸出(Read3&Read5)后写到位;读到允许离开后切换为放行车型。
|
||
/// 放行车型:气缸收回(Read4&Read6)后写离开中、清除到位。
|
||
/// </para>
|
||
/// </summary>
|
||
|
||
|
||
/// <summary>将车辆 fields["CarModel"] 切换为放行车型,供后续触发器匹配。</summary>
|
||
private void SaveCarModel(int carId, string carModel, string reason)
|
||
{
|
||
var car = SimpleCore.SimpleLib.GetCar(carId) as Car;
|
||
if (car == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
car.fields ??= new Dictionary<string, string>();
|
||
if (string.Equals(FassEventCarSnapshotFactory.GetCarModel(car), carModel, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return;
|
||
}
|
||
|
||
car.fields["CarModel"] = carModel;
|
||
LogSignal($"{reason}:车辆[{FassEventCarSnapshotFactory.GetCarCode(car)}] 车型切换为 [{carModel}]");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region PLC 与工具方法
|
||
|
||
/// <summary>在 PLC 锁内打开连接、执行读写、关闭连接。</summary>
|
||
private bool ExecutePlcCondition(
|
||
FassEventCarSnapshot snapshot,
|
||
FassConditionEventConfig condition,
|
||
int minArgs,
|
||
Func<SiemensClient, string[], bool> action)
|
||
{
|
||
if (!TryGetConditionArgs(condition, minArgs, out var param))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
lock (_plcLock)
|
||
{
|
||
SiemensClient client = null;
|
||
try
|
||
{
|
||
client = CreatePlcClient();
|
||
client.Open();
|
||
return action(client, param);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
LogSignalError(ex, "交互站点报错");
|
||
return false;
|
||
}
|
||
finally
|
||
{
|
||
client?.Close();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>根据 Mission 配置创建西门子 PLC 客户端。</summary>
|
||
private SiemensClient CreatePlcClient()
|
||
{
|
||
var version = ParsePlcVersion(_mission.PlcVersion);
|
||
return new SiemensClient(version, _mission.PlcIpAddress, _mission.PlcPort, 0);
|
||
}
|
||
|
||
private static SiemensVersion ParsePlcVersion(string version)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(version))
|
||
{
|
||
return SiemensVersion.S7_1500;
|
||
}
|
||
|
||
return Enum.TryParse(version, true, out SiemensVersion parsed) ? parsed : SiemensVersion.S7_1500;
|
||
}
|
||
|
||
/// <summary>拼接站点交互日志上下文,便于现场对照 PLC 地址排查。</summary>
|
||
private string BuildStationLogContext(string methodName, FassEventCarSnapshot snapshot, FassConditionEventConfig condition)
|
||
{
|
||
return $"{methodName} 车辆[{snapshot.Code}] 状态[{snapshot.State}] 当前地标[{snapshot.CurrentNodeCode}] 下一地标[{snapshot.NextNodeCode}] 车型[{snapshot.CarModel}] 参数[{condition.ConditionMethodArgs}]";
|
||
}
|
||
|
||
private bool ReadStationSignal(SiemensClient client, string address, string signalName, string context)
|
||
{
|
||
var value = client.ReadBoolean(address).Value;
|
||
LogSignal($"{context} 读取信号[{signalName}] 地址[{address}] 值[{value}]");
|
||
return value;
|
||
}
|
||
|
||
private void WriteStationSignal(SiemensClient client, string address, bool value, string signalName, string context)
|
||
{
|
||
LogSignal($"{context} 写入信号[{signalName}] 地址[{address}] 值[{value}]");
|
||
client.Write(address, value);
|
||
var readBackValue = client.ReadBoolean(address).Value;
|
||
LogSignal($"{context} 回读信号[{signalName}] 地址[{address}] 值[{readBackValue}]");
|
||
}
|
||
|
||
private bool WriteAndRead(SiemensClient client, string address, bool value, string signalName, string context)
|
||
{
|
||
WriteStationSignal(client, address, value, signalName, context);
|
||
return client.ReadBoolean(address).Value;
|
||
}
|
||
|
||
/// <summary>校验条件参数个数是否满足 PLC 方法要求。</summary>
|
||
private static bool TryGetConditionArgs(FassConditionEventConfig condition, int minLength, out string[] args)
|
||
{
|
||
args = SplitArgs(condition.ConditionMethodArgs);
|
||
return args.Length >= minLength;
|
||
}
|
||
|
||
/// <summary>将逗号分隔的配置参数字符串拆分为数组。</summary>
|
||
private static string[] SplitArgs(string args)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(args))
|
||
{
|
||
return Array.Empty<string>();
|
||
}
|
||
|
||
return args.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||
.Select(item => item.Trim())
|
||
.ToArray();
|
||
}
|
||
|
||
/// <summary>
|
||
/// CSV 过滤器:filter 为空则通过;否则 value 必须等于 filter 中某项(忽略大小写)。
|
||
/// 用于 TriggerCarState 等多值匹配,如 "Stopping,Charging"。
|
||
/// </summary>
|
||
private static bool MatchesCsvFilter(string filter, string value)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(filter))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(value))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
return filter.Split(',').Select(item => item.Trim()).Any(item => string.Equals(item, value, StringComparison.OrdinalIgnoreCase));
|
||
}
|
||
|
||
private void LogTraffic(string message) => Diagnosis.Post(message, "Fass交管", true);
|
||
|
||
private void LogSignal(string message) => Diagnosis.Post(message, "Fass信号交互", true);
|
||
|
||
private void LogSignalError(Exception ex, string message) =>
|
||
Diagnosis.Post($"{message}: {ExceptionFormatter.FormatEx(ex)}", "Fass信号交互", true);
|
||
|
||
#endregion
|
||
}
|
||
|
||
/// <summary>从磁盘加载 EventCar JSON 配置数组。</summary>
|
||
internal static class FassEventConfigLoader
|
||
{
|
||
/// <summary>
|
||
/// 读取并反序列化配置文件。
|
||
/// </summary>
|
||
/// <param name="configPath">JSON 文件路径。</param>
|
||
/// <returns>事件配置列表;文件内容为空数组时返回空列表。</returns>
|
||
/// <exception cref="FileNotFoundException">文件不存在时抛出。</exception>
|
||
public static List<FassEventCarConfig> Load(string configPath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(configPath) || !File.Exists(configPath))
|
||
{
|
||
throw new FileNotFoundException($"未找到 FASS 车辆事件配置文件:{configPath}");
|
||
}
|
||
|
||
var json = File.ReadAllText(configPath);
|
||
return Newtonsoft.Json.JsonConvert.DeserializeObject<List<FassEventCarConfig>>(json)
|
||
?? new List<FassEventCarConfig>();
|
||
}
|
||
}
|
||
}
|