Files
StandardSence/StandardScene.Magnetic/Scheduler/FassEvent/FassEventCarHandler.cs
T

578 lines
23 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 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)→ ExecuteTaskStart/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&amp;Read5)后写到位;读到允许离开后切换为放行车型。
/// 放行车型:气缸收回(Read4&amp;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>();
}
}
}