using IoTClient.Clients.PLC; using IoTClient.Common.Enums; using LessokajiWeaverUtilities.Utilities; using Simple3.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 Simple3.RCS.CarTypes; namespace StandardScene.Scheduler.FassEvent { /// /// FASS 车辆事件核心处理器,逻辑对齐 EventCarService。 /// 处理流程:TriggerHandler(触发匹配)→ ConditionHandler(条件 AND)→ ExecuteTask(Start/Stop)。 /// internal sealed class FassEventCarHandler { /// /// 防重复执行任务的车辆标签名。同一地标周期内只执行一次,换地标后由 EventCarMission 清除。 /// 标签值为 生成的任务指纹。 /// internal const string ActiveTaskTag = "FassEvent:Active"; private readonly EventCarMission _mission; /// PLC 读写串行锁,避免多车并发条件中同时 Open/Close 同一连接。 private readonly object _plcLock = new object(); /// JSON ConditionMethodName → 内置条件方法 映射表。 private readonly Dictionary> _eventMethods; public FassEventCarHandler(EventCarMission mission) { _mission = mission; _eventMethods = BuildEventMethods(); } /// /// 单次 TimerTick 对一辆车的处理入口(通常在 Task.Run 中调用)。 /// 仅处理 TriggerEvent=TimerTick 的配置项。 /// public void HandleCarTimerTick(Car car, FassEventCarSnapshot snapshot, IReadOnlyList 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); } } /// 注册与 FASS EventCarService.InitEventMethod 一致的条件方法名(保留历史拼写)。 private Dictionary> BuildEventMethods() { return new Dictionary>(StringComparer.OrdinalIgnoreCase) { ["NodeAllUnlockedNotCar"] = NodeAllUnlockedNotCar, ["NodeAllUnlocked"] = NodeAllUnlocked, ["NodeAnyLockedNotCar"] = NodeAnyLockedNotCar, ["NodeAnyLocked"] = NodeAnyLocked, ["NodeAllUnlockedNotCarNotEndCarState"] = NodeAllUnlockedNotCarNotEndCarState, ["requestStaion"] = RequestStation, ["clearStaion"] = ClearStation, ["EmegerStop"] = EmergencyStop, ["EmegerStart"] = EmergencyStart, ["IsExistRunningStateCar"] = IsExistRunningStateCar, ["ChargeStart"] = ChargeStart }; } /// /// 触发器匹配:车辆编号、前后地标、状态、角度等 CSV 过滤。 /// 配置项为空表示“不限制”;车辆对应字段为空则无法匹配非空配置。 /// 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; } /// 所有 ConditionEvents 必须全部通过(逻辑与)。 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; } /// 单条条件分发:Method / Assembly / Plugin。 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; } /// 按方法名调用内置条件实现。 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; } /// /// 执行任务:MagCar 调用 SendStart/SendStop;其他车型仅打日志。 /// 通过 ActiveTaskTag 防止同一地标周期内重复下发。 /// 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}"); } /// 生成任务指纹,写入 ActiveTaskTag 的值,便于日志追溯。 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())}"; } #region 交管条件(节点锁) /// 指定节点全部未被其他车辆占用(排除自身)。参数:逗号分隔节点号。 private bool NodeAllUnlockedNotCar(FassEventCarSnapshot snapshot, FassConditionEventConfig condition) { return FassEventNodeHelper.NodeAllUnlockedNotCar(snapshot.Code, SplitArgs(condition.ConditionMethodArgs)); } /// 指定节点全部空闲(含自身锁也计入)。 private bool NodeAllUnlocked(FassEventCarSnapshot snapshot, FassConditionEventConfig condition) { return FassEventNodeHelper.NodeAllUnlocked(SplitArgs(condition.ConditionMethodArgs)); } /// 指定节点中存在被其他车占用的点。 private bool NodeAnyLockedNotCar(FassEventCarSnapshot snapshot, FassConditionEventConfig condition) { return FassEventNodeHelper.NodeAnyLockedNotCar(snapshot.Code, SplitArgs(condition.ConditionMethodArgs)); } /// /// 指定节点存在占用时先 SendStart 再判断(与 FASS NodeAnyLocked 行为一致)。 /// 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)); } /// 节点全解锁且路径上无其他运行中车辆。 private bool NodeAllUnlockedNotCarNotEndCarState(FassEventCarSnapshot snapshot, FassConditionEventConfig condition) { return FassEventNodeHelper.NodeAllUnlockedNotCarNotEndCarState(snapshot.Code, "Runing", SplitArgs(condition.ConditionMethodArgs)); } /// 判断指定节点范围是否不存在其他“运行中”车辆(返回 true 表示可以放行)。 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 电量与安全 /// /// 电量达到阈值方可放行。阈值优先取 ConditionMethodArgs 首参,否则用 Mission.ChargeStartThreshold。 /// 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; } /// /// 设备安全急停:读 PLC 布尔位,false 表示故障需停车(条件返回 true 以触发 Stop 任务)。 /// 参数 param[0]:PLC 地址。 /// 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; }); } /// /// 设备安全恢复:PLC 布尔位为 true 时允许 Start。 /// 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 交互 /// /// 清除站点到位与离开中信号。 /// 参数:param[0] 到位信号地址,param[1] 离开中信号地址。 /// 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; }); } /// /// 请求进入工位:读“请求进入”,为 true 时写“正在进入中”,并以进入中信号作为放行条件结果。 /// 参数:param[0] 请求进入,param[1] 正在进入中。 /// 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; }); } /// /// 到位与允许离开交互(ME11 下料核心逻辑)。 /// /// 参数:param[0] 到位,param[1] 允许离开,param[2] 正在进入中,param[3] 正在离开中。 /// ME11 车型:气缸伸出(Read3&Read5)后写到位;读到允许离开后切换为放行车型。 /// 放行车型:气缸收回(Read4&Read6)后写离开中、清除到位。 /// /// /// 将车辆 fields["CarModel"] 切换为放行车型,供后续触发器匹配。 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(); if (string.Equals(FassEventCarSnapshotFactory.GetCarModel(car), carModel, StringComparison.OrdinalIgnoreCase)) { return; } car.fields["CarModel"] = carModel; LogSignal($"{reason}:车辆[{FassEventCarSnapshotFactory.GetCarCode(car)}] 车型切换为 [{carModel}]"); } #endregion #region PLC 与工具方法 /// 在 PLC 锁内打开连接、执行读写、关闭连接。 private bool ExecutePlcCondition( FassEventCarSnapshot snapshot, FassConditionEventConfig condition, int minArgs, Func 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(); } } } /// 根据 Mission 配置创建西门子 PLC 客户端。 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; } /// 拼接站点交互日志上下文,便于现场对照 PLC 地址排查。 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; } /// 校验条件参数个数是否满足 PLC 方法要求。 private static bool TryGetConditionArgs(FassConditionEventConfig condition, int minLength, out string[] args) { args = SplitArgs(condition.ConditionMethodArgs); return args.Length >= minLength; } /// 将逗号分隔的配置参数字符串拆分为数组。 private static string[] SplitArgs(string args) { if (string.IsNullOrWhiteSpace(args)) { return Array.Empty(); } return args.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(item => item.Trim()) .ToArray(); } /// /// CSV 过滤器:filter 为空则通过;否则 value 必须等于 filter 中某项(忽略大小写)。 /// 用于 TriggerCarState 等多值匹配,如 "Stopping,Charging"。 /// 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 } /// 从磁盘加载 EventCar JSON 配置数组。 internal static class FassEventConfigLoader { /// /// 读取并反序列化配置文件。 /// /// JSON 文件路径。 /// 事件配置列表;文件内容为空数组时返回空列表。 /// 文件不存在时抛出。 public static List 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>(json) ?? new List(); } } }