新增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,251 @@
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];
}
}
}
@@ -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>();
}
}
}
@@ -0,0 +1,165 @@
using SimpleLite.RCS;
using SimpleCore.PropType;
using StandardScene.CarTypes;
using System;
using System.Globalization;
using SimpleLite.RCS.CarTypes;
namespace StandardScene.Scheduler.FassEvent
{
/// <summary>
/// 将 StandardScene 车辆对象转换为 FASS 事件系统可识别的快照字段。
/// 负责编号、地标、状态、电量等与 <c>EventCarService.TriggerHandler</c> 对齐的映射。
/// </summary>
internal static class FassEventCarSnapshotFactory
{
/// <summary>
/// 获取车辆“下一地标”编码。
/// 优先级:status.enums["NextNode"] → tags["NextNode"] → pendingLocks 首站(TagValue 或站点 ID)。
/// </summary>
public static string GetNextNodeCode(Car car)
{
if (car.status?.enums != null && car.status.enums.TryGetValue("NextNode", out var node) && !string.IsNullOrWhiteSpace(node))
{
return node;
}
if (car.tags != null && car.tags.TryGetValue("NextNode", out var tagNode) && !string.IsNullOrWhiteSpace(tagNode))
{
return tagNode;
}
// 调度 pendingLocks 通常表示车辆即将前往的站点,可近似为 FASS NextNodeCode。
if (car.status?.pendingLocks != null && car.status.pendingLocks.Length > 0)
{
var pendingSiteId = car.status.pendingLocks[0];
var site = SimpleCore.SimpleLib.GetSite(pendingSiteId);
if (site?.fields != null && site.fields.TryGetValue("TagValue", out var tagValue) && !string.IsNullOrWhiteSpace(tagValue))
{
return tagValue;
}
return pendingSiteId.ToString(CultureInfo.InvariantCulture);
}
return string.Empty;
}
/// <summary>
/// 根据车辆与上一周期地标构建完整快照。
/// </summary>
/// <param name="car">场景车辆。</param>
/// <param name="prevNodeCode">上一周期 CurrentNode,由 EventCarMission 维护。</param>
/// <param name="nextNodeCode">下一地标;为空时自动调用 <see cref="GetNextNodeCode"/>。</param>
public static FassEventCarSnapshot Create(Car car, string prevNodeCode, string nextNodeCode)
{
return new FassEventCarSnapshot
{
CarId = car.id,
Code = GetCarCode(car),
CarModel = GetCarModel(car),
PrevNodeCode = prevNodeCode,
CurrentNodeCode = GetCurrentNodeCode(car),
NextNodeCode = string.IsNullOrWhiteSpace(nextNodeCode) ? GetNextNodeCode(car) : nextNodeCode,
State = GetFassState(car),
Angle = (int)Math.Round(car.th),
Charge = GetCharge(car)
};
}
/// <summary>
/// 获取 FASS 车辆编号。MagCar 优先使用 VehicleCode,否则 fields["Code"],最后回退 name。
/// </summary>
public static string GetCarCode(Car car)
{
if (car is MagCar magCar && magCar.VehicleCode > 0)
{
return magCar.VehicleCode.ToString(CultureInfo.InvariantCulture);
}
if (car.fields != null && car.fields.TryGetValue("Code", out var code) && !string.IsNullOrWhiteSpace(code))
{
return code;
}
return car.name;
}
/// <summary>获取车型。下料站 InplaceStaion 根据 CarModel 在 ME11 / 放行车型间切换。</summary>
public static string GetCarModel(Car car)
{
if (car.fields != null && car.fields.TryGetValue("CarModel", out var model) && !string.IsNullOrWhiteSpace(model))
{
return model;
}
return car.GetType().Name;
}
/// <summary>
/// 获取当前地标号。MagCar 上报后写入 status.enums["CurrentNode"]
/// 其他车型可回退到 siteID。
/// </summary>
public static string GetCurrentNodeCode(Car car)
{
if (car.status?.enums != null && car.status.enums.TryGetValue("CurrentNode", out var node) && !string.IsNullOrWhiteSpace(node))
{
return node;
}
return car.siteID > 0 ? car.siteID.ToString(CultureInfo.InvariantCulture) : string.Empty;
}
/// <summary>
/// 将 StandardScene 车辆状态映射为 FASS 英文字符串。
/// <para>
/// 可手动覆盖:tags["FassState"] 或 enums["FassState"]。
/// WaitPass 需在停止中且 tags 含 WaitPass(FASS 中“等待放行”态)。
/// </para>
/// </summary>
public static string GetFassState(Car car)
{
if (car.tags != null && car.tags.TryGetValue("FassState", out var tagState) && !string.IsNullOrWhiteSpace(tagState))
{
return tagState;
}
if (car.status?.enums != null && car.status.enums.TryGetValue("FassState", out var enumState) && !string.IsNullOrWhiteSpace(enumState))
{
return enumState;
}
if (car.status?.enums != null && car.status.enums.TryGetValue("State", out var state))
{
switch (state)
{
case "运行中":
return "Running";
case "停止中":
return car.tags != null && car.tags.ContainsKey("WaitPass") ? "WaitPass" : "Stopping";
case "未准备":
return "NotReady";
case "充电中":
return "Charging";
default:
return state;
}
}
return string.Empty;
}
/// <summary>从 status.enums["Soc"] 解析电量百分比。</summary>
private static double GetCharge(Car car)
{
if (car.status?.enums != null &&
car.status.enums.TryGetValue("Soc", out var soc) &&
double.TryParse(soc, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
{
return value;
}
return 0;
}
}
}
@@ -0,0 +1,132 @@
using System.Collections.Generic;
namespace StandardScene.Scheduler.FassEvent
{
/// <summary>
/// FASS 车辆事件配置项,对应 <c>Configs/EventCar/*.json</c> 数组中的单个元素。
/// 配置格式与 FASS.Scheduler 的 EventCar 模型保持一致,便于直接复用现场 JSON。
/// </summary>
public class FassEventCarConfig
{
/// <summary>是否启用本条事件规则。</summary>
public bool TriggerEnable { get; set; } = true;
/// <summary>
/// 事件来源。StandardScene 进程仅处理值为 <c>Car</c> 的项(车辆定时轮询触发)。
/// </summary>
public string TriggerSource { get; set; } = "Car";
/// <summary>
/// 触发时机。当前实现仅支持 <c>TimerTick</c>(与 FASS <c>EventCarService.CarTimerTick</c> 等价)。
/// </summary>
public string TriggerEvent { get; set; } = "TimerTick";
/// <summary>触发条件:车辆编号(逗号分隔,空表示不限制)。</summary>
public string TriggerCarCode { get; set; }
/// <summary>触发条件:上一地标号(逗号分隔)。</summary>
public string TriggerCarPrevNodeCode { get; set; }
/// <summary>触发条件:当前地标号(逗号分隔)。</summary>
public string TriggerCarCurrentNodeCode { get; set; }
/// <summary>触发条件:下一地标号(逗号分隔)。</summary>
public string TriggerCarNextNodeCode { get; set; }
/// <summary>
/// 触发条件:车辆 FASS 状态(逗号分隔)。
/// 常见值:Running、Stopping、WaitPass、Charging、Stopping,Charging 等。
/// </summary>
public string TriggerCarState { get; set; }
/// <summary>触发条件:车头角度(逗号分隔的整数字符串)。</summary>
public string TriggerCarAngle { get; set; }
/// <summary>
/// 条件事件列表。全部满足后才执行任务;
/// 对应 FASS 中 <c>ConditionHandler</c> 的逐项 AND 逻辑。
/// </summary>
public List<FassConditionEventConfig> ConditionEvents { get; set; } = new List<FassConditionEventConfig>();
/// <summary>条件满足后是否执行任务(false 时仅做条件判断,不下发 Start/Stop)。</summary>
public bool TaskEnable { get; set; } = true;
/// <summary>
/// 任务类型。MagCar 当前支持 <c>Start</c>(放行)与 <c>Stop</c>(急停)。
/// </summary>
public string TaskType { get; set; }
}
/// <summary>
/// 单条条件事件配置,描述一个条件判断(交管、PLC 信号、电量等)。
/// </summary>
public class FassConditionEventConfig
{
/// <summary>是否启用本条件;false 时跳过(视为通过)。</summary>
public bool ConditionEnable { get; set; } = true;
/// <summary>
/// 条件类型:<c>Method</c>(内置方法)、<c>Assembly</c>、<c>Plugin</c>。
/// StandardScene 当前仅实现 Method;后两者会记录日志并返回 false。
/// </summary>
public string ConditionType { get; set; } = "Method";
/// <summary>Assembly 条件:程序集文件路径(未实现)。</summary>
public string ConditionAssemblyFile { get; set; }
/// <summary>Assembly/Plugin 条件:类型名(未实现)。</summary>
public string ConditionTypeName { get; set; }
/// <summary>Assembly/Plugin 条件:构造参数(未实现)。</summary>
public string ConditionTypeArgs { get; set; }
/// <summary>
/// Method 条件:方法名,如 NodeAllUnlockedNotCar、requestStaion、EmegerStop 等。
/// 名称与 FASS <c>EventCarService.InitEventMethod</c> 注册表一致(含历史拼写)。
/// </summary>
public string ConditionMethodName { get; set; }
/// <summary>
/// Method 条件:方法参数,一般为逗号分隔的节点号或 PLC 地址。
/// 例如交管:<c>3,53,21</c>PLC<c>DB5524.0.0,DB5524.0.1</c>。
/// </summary>
public string ConditionMethodArgs { get; set; }
}
/// <summary>
/// 车辆事件处理用的只读快照,从 StandardScene <see cref="SimpleLite.RCS.Car"/> 提取 FASS 语义字段。
/// 每次 TimerTick 为每辆车构建一份,供触发器与条件方法使用。
/// </summary>
public class FassEventCarSnapshot
{
/// <summary>场景内车辆 IDSimpleLib)。</summary>
public int CarId { get; set; }
/// <summary>FASS 车辆编号(MagCar.VehicleCode 或 fields["Code"])。</summary>
public string Code { get; set; }
/// <summary>车型标识,用于 ME11 下料站逻辑(fields["CarModel"])。</summary>
public string CarModel { get; set; }
/// <summary>上一周期记录的地标号,用于 TriggerCarPrevNodeCode 匹配。</summary>
public string PrevNodeCode { get; set; }
/// <summary>当前地标号(status.enums["CurrentNode"] 或 siteID)。</summary>
public string CurrentNodeCode { get; set; }
/// <summary>下一地标号(pendingLocks / tags / enums 推断)。</summary>
public string NextNodeCode { get; set; }
/// <summary>
/// FASS 状态字符串:Running、Stopping、WaitPass、Charging 等。
/// 由 <see cref="FassEventCarSnapshotFactory.GetFassState"/> 从中文状态映射。
/// </summary>
public string State { get; set; }
/// <summary>车头角度(度),对应 FASS Car.Angle。</summary>
public int Angle { get; set; }
/// <summary>电量百分比(Soc)。</summary>
public double Charge { get; set; }
}
}
@@ -0,0 +1,196 @@
using SimpleLite.RCS;
using SimpleCore;
using SimpleCore.PropType;
using System;
using System.Linq;
using SimpleLite.RCS.CarTypes;
namespace StandardScene.Scheduler.FassEvent
{
/// <summary>
/// 节点(地标)交管判断辅助类,替代 FASS <c>AlgorithmService</c> 中与 EventCar 相关的方法。
/// <para>
/// “锁定”判定依据 StandardScene 交通锁:holdingLocks、pendingLocks、aquiringLock
/// 以及站点 tags 中的 unavailable。
/// </para>
/// </summary>
internal static class FassEventNodeHelper
{
/// <summary>指定节点列表是否全部未被任何车辆占用。</summary>
public static bool NodeAllUnlocked(string[] nodeCodes)
{
return nodeCodes == null || nodeCodes.All(node => !IsNodeLocked(node, -1));
}
/// <summary>指定节点列表是否全部未被占用(排除当前车辆自身锁)。</summary>
public static bool NodeAllUnlockedNotCar(string carCode, string[] nodeCodes)
{
var excludeCarId = ResolveCarId(carCode);
return nodeCodes == null || nodeCodes.All(node => !IsNodeLocked(node, excludeCarId));
}
/// <summary>指定节点列表中是否存在任意一个被占用的节点。</summary>
public static bool NodeAnyLocked(string[] nodeCodes)
{
return nodeCodes != null && nodeCodes.Any(node => IsNodeLocked(node, -1));
}
/// <summary>指定节点列表中是否存在被其他车辆占用的节点(排除自身)。</summary>
public static bool NodeAnyLockedNotCar(string carCode, string[] nodeCodes)
{
var excludeCarId = ResolveCarId(carCode);
return nodeCodes != null && nodeCodes.Any(node => IsNodeLocked(node, excludeCarId));
}
/// <summary>
/// 节点全部解锁(排除自身)且指定节点范围内不存在处于运行态的其他车辆。
/// 对应 FASS <c>NodeAllUnlockedNotCarNotEndCarState</c>。
/// </summary>
public static bool NodeAllUnlockedNotCarNotEndCarState(string carCode, string runningState, string[] nodeCodes)
{
if (!NodeAllUnlockedNotCar(carCode, nodeCodes))
{
return false;
}
return !ExistsRunningCarOnNodes(carCode, runningState, nodeCodes);
}
/// <summary>
/// 判断除指定车辆外,是否有车辆在给定节点列表上处于“运行中”状态。
/// 逻辑对齐 FASS <c>IsExistRunningStateCar</c>:支持首参 Contains 匹配及 Skip(1) 尾节点匹配。
/// </summary>
public static bool ExistsRunningCarOnNodes(string excludeCarCode, string runningState, string[] nodeCodes)
{
if (nodeCodes == null || nodeCodes.Length == 0)
{
return false;
}
var excludeCarId = ResolveCarId(excludeCarCode);
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
{
if (car.id == excludeCarId)
{
continue;
}
var currentNode = FassEventCarSnapshotFactory.GetCurrentNodeCode(car);
if (string.IsNullOrWhiteSpace(currentNode))
{
continue;
}
var state = FassEventCarSnapshotFactory.GetFassState(car);
if (!IsRunningState(state, runningState))
{
continue;
}
// 与 FASS 一致:当前地标包含参数首项,或等于后续任一项。
if (nodeCodes.Any(node => string.Equals(node, currentNode, StringComparison.OrdinalIgnoreCase)
|| currentNode.Contains(node)))
{
return true;
}
if (nodeCodes.Length > 1)
{
var tailNodes = nodeCodes.Skip(1).ToArray();
if (tailNodes.Any(node => string.Equals(node, currentNode, StringComparison.OrdinalIgnoreCase)))
{
return true;
}
}
}
return false;
}
/// <summary>
/// 判断单个地标是否被锁定。
/// 节点号可解析为站点 ID,或通过 fields["TagValue"] 匹配地标标签。
/// </summary>
private static bool IsNodeLocked(string nodeCode, int excludeCarId)
{
if (!TryResolveSiteId(nodeCode, out var siteId))
{
return false;
}
var site = SimpleLib.GetSite(siteId);
if (site?.tags?.Contains("unavailable") == true)
{
return true;
}
return SimpleLib.GetAllCars().Any(car =>
car.id != excludeCarId &&
(car.status.holdingLocks?.Contains(siteId) == true ||
car.status.pendingLocks?.Contains(siteId) == true ||
car.status.aquiringLock == siteId));
}
/// <summary>根据 FASS 车辆编号解析场景内 car.id。</summary>
private static int ResolveCarId(string carCode)
{
if (string.IsNullOrWhiteSpace(carCode))
{
return -1;
}
var car = SimpleLib.GetAllCars()
.OfType<Car>()
.FirstOrDefault(item => string.Equals(FassEventCarSnapshotFactory.GetCarCode(item), carCode, StringComparison.OrdinalIgnoreCase));
return car?.id ?? -1;
}
/// <summary>
/// 将地标编码解析为站点 ID:纯数字直接查站;否则按 TagValue 字段匹配。
/// </summary>
private static bool TryResolveSiteId(string nodeCode, out int siteId)
{
siteId = 0;
if (string.IsNullOrWhiteSpace(nodeCode))
{
return false;
}
if (int.TryParse(nodeCode, out siteId))
{
return SimpleLib.GetSite(siteId) != null;
}
var site = SimpleLib.GetAllSites().FirstOrDefault(item =>
item.fields.TryGetValue("TagValue", out var tag) &&
string.Equals(tag, nodeCode, StringComparison.OrdinalIgnoreCase));
if (site == null)
{
return false;
}
siteId = site.id;
return true;
}
/// <summary>
/// 判断状态是否视为“运行中”。兼容 FASS 历史拼写 Runing 与 Running。
/// </summary>
private static bool IsRunningState(string state, string runningState)
{
if (string.IsNullOrWhiteSpace(state))
{
return false;
}
if (string.Equals(state, runningState, StringComparison.OrdinalIgnoreCase))
{
return true;
}
return string.Equals(state, "Running", StringComparison.OrdinalIgnoreCase) &&
(string.Equals(runningState, "Runing", StringComparison.OrdinalIgnoreCase) ||
string.Equals(runningState, "Running", StringComparison.OrdinalIgnoreCase));
}
}
}