新增1.0和2.0两种协议车型车型

This commit is contained in:
ykkokluo
2026-07-17 13:36:01 +08:00
parent a0dc1e6cd0
commit a28bc68e23
62 changed files with 12638 additions and 203 deletions
@@ -0,0 +1,577 @@
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>();
}
}
}