新增1.0和2.0两种协议车型车型
This commit is contained in:
@@ -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)→ 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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>场景内车辆 ID(SimpleLib)。</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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user