对齐 FairyView 宿主命名。环线/充电/按钮/门进程的 Stop 改为 override,避免隐藏基类;Kiva.ForceStop 改为 override。 Co-authored-by: Cursor <cursoragent@cursor.com>
2234 lines
83 KiB
C#
2234 lines
83 KiB
C#
using LoopViewerApp;
|
||
using Newtonsoft.Json;
|
||
using Simple3.RCS;
|
||
using Simple3.RCS.CarTypes;
|
||
using SimpleCore;
|
||
using SimpleCore.Compiler;
|
||
using SimpleCore.Library;
|
||
using SimpleCore.PropType;
|
||
using StandardScene.CarTypes;
|
||
using StandardScene.Chained.Loop;
|
||
using StandardScene.Model;
|
||
using StandardScene.Utils;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace StandardScene.Chained
|
||
{
|
||
/// <summary>
|
||
/// 抽象环线任务基类
|
||
/// <para>核心功能:</para>
|
||
/// <list type="bullet">
|
||
/// <item><description>支持多种启动类型:AutoLoop(自动循环)、Api、Plc、ButtonBox(外部触发)</description></item>
|
||
/// <item><description>支持多种任务类别:Loop(普通循环)、BranchPoint(分流点)、JoinPoint(汇合点)</description></item>
|
||
/// <item><description>支持流量控制:限制目标站点的最大车辆数</description></item>
|
||
/// <item><description>支持优先级调度:高优先级任务优先处理</description></item>
|
||
/// <item><description>支持配置热更新:tasklist.json 文件变更自动刷新</description></item>
|
||
/// <item><description>外部触发接口:子类可重写 OnApiTrigger/OnPlcTrigger/OnButtonTrigger 实现业务逻辑</description></item>
|
||
/// </list>
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// <para>子类实现示例:</para>
|
||
/// <code>
|
||
/// public class MyLoopMission : AbstractLoopMission
|
||
/// {
|
||
/// protected override ExternalTriggerResult OnApiTrigger(int currentSiteId, LoopTask task, Car car)
|
||
/// {
|
||
/// // 返回 UseTarget(目标站点) - 使用子类指定的目标
|
||
/// // 返回 UseConfigTarget() - 使用配置文件目标
|
||
/// // 返回 Fail() - 不分配任务
|
||
/// return ExternalTriggerResult.UseConfigTarget();
|
||
/// }
|
||
/// }
|
||
/// </code>
|
||
/// </remarks>
|
||
public abstract class AbstractLoopMission : Mission, IDisposable
|
||
{
|
||
#region 常量定义
|
||
|
||
/// <summary>策略同步线程轮询间隔(毫秒)</summary>
|
||
private const int STRATEGY_SYNC_INTERVAL_MS = 1000;
|
||
|
||
/// <summary>业务逻辑线程轮询间隔(毫秒)</summary>
|
||
private const int LOGIC_LOOP_INTERVAL_MS = 300;
|
||
|
||
/// <summary>文件变更防抖延迟(毫秒)</summary>
|
||
private const int FILE_CHANGE_DEBOUNCE_MS = 50;
|
||
|
||
/// <summary>异常恢复等待时间(毫秒)</summary>
|
||
private const int ERROR_RECOVERY_DELAY_MS = 3000;
|
||
|
||
/// <summary>脚本异常检测触发延迟(毫秒)</summary>
|
||
private const int SCRIPT_ERROR_TRIGGER_DELAY_MS = 3000;
|
||
|
||
#endregion
|
||
|
||
#region 外部触发结果类
|
||
|
||
/// <summary>
|
||
/// 外部触发处理结果类
|
||
/// <para>用于子类重写外部触发接口时返回处理结果,决定目标站点的分配方式</para>
|
||
/// </summary>
|
||
/// <example>
|
||
/// 使用示例:
|
||
/// <code>
|
||
/// // 方式1:使用子类指定的目标站点
|
||
/// return ExternalTriggerResult.UseTarget(200);
|
||
///
|
||
/// // 方式2:使用配置文件中的目标站点(相当于返回 true)
|
||
/// return ExternalTriggerResult.UseConfigTarget();
|
||
///
|
||
/// // 方式3:业务失败,不分配任务
|
||
/// return ExternalTriggerResult.Fail();
|
||
/// </code>
|
||
/// </example>
|
||
public class ExternalTriggerResult
|
||
{
|
||
/// <summary>
|
||
/// 是否处理成功
|
||
/// <para>true = 成功,将根据 CustomTargetSiteId 分配目标站点</para>
|
||
/// <para>false = 失败或跳过,不分配任务</para>
|
||
/// </summary>
|
||
public bool Success { get; set; }
|
||
|
||
/// <summary>
|
||
/// 子类指定的目标站点ID
|
||
/// <para>有值时使用此目标站点</para>
|
||
/// <para>null 时使用配置文件中的 TargetStationId</para>
|
||
/// </summary>
|
||
public int? CustomTargetSiteId { get; set; }
|
||
|
||
/// <summary>
|
||
/// 创建成功结果,使用子类指定的目标站点
|
||
/// </summary>
|
||
/// <param name="targetSiteId">目标站点ID</param>
|
||
/// <returns>包含指定目标的成功结果</returns>
|
||
public static ExternalTriggerResult UseTarget(int targetSiteId)
|
||
{
|
||
return new ExternalTriggerResult
|
||
{
|
||
Success = true,
|
||
CustomTargetSiteId = targetSiteId
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建成功结果,使用配置文件中的目标站点
|
||
/// <para>等同于业务处理成功返回 true 的效果</para>
|
||
/// </summary>
|
||
/// <returns>使用配置目标的成功结果</returns>
|
||
public static ExternalTriggerResult UseConfigTarget()
|
||
{
|
||
return new ExternalTriggerResult
|
||
{
|
||
Success = true,
|
||
CustomTargetSiteId = null
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建失败结果,不分配任务
|
||
/// <para>等同于业务处理失败返回 false 的效果</para>
|
||
/// </summary>
|
||
/// <returns>失败结果</returns>
|
||
public static ExternalTriggerResult Fail()
|
||
{
|
||
return new ExternalTriggerResult { Success = false };
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 状态与字段
|
||
|
||
/// <summary>
|
||
/// 环线任务状态类
|
||
/// </summary>
|
||
public class AbstractLoopMissionStatus : MissionStatus
|
||
{
|
||
/// <summary>当前任务数量</summary>
|
||
public int TaskCount { get; set; }
|
||
|
||
/// <summary>最后一次错误信息</summary>
|
||
public string LastError { get; set; } = "/";
|
||
}
|
||
|
||
/// <summary>任务状态对象</summary>
|
||
/* [JsonIgnore]
|
||
public override MissionStatus status { get; set; } = new LoopMissionStatus();*/
|
||
|
||
/// <summary>策略同步线程</summary>
|
||
[JsonIgnore]
|
||
private Thread _strategyThread;
|
||
|
||
/// <summary>策略同步线程运行标志</summary>
|
||
[JsonIgnore]
|
||
private volatile bool _strategyRunning;
|
||
|
||
/// <summary>业务逻辑线程</summary>
|
||
[JsonIgnore]
|
||
private Thread _logicThread;
|
||
|
||
/// <summary>业务逻辑线程运行标志</summary>
|
||
[JsonIgnore]
|
||
private volatile bool _logicRunning;
|
||
|
||
/// <summary>任务集合同步锁</summary>
|
||
[JsonIgnore]
|
||
private readonly object _tasksLock = new object();
|
||
|
||
/// <summary>任务列表(线程安全访问需使用 _tasksLock)</summary>
|
||
[JsonIgnore]
|
||
protected readonly List<LoopTask> Tasks = new List<LoopTask>();
|
||
|
||
/// <summary>触发器适配器列表</summary>
|
||
[JsonIgnore]
|
||
private readonly List<ITriggerAdapter> _adapters = new List<ITriggerAdapter>();
|
||
|
||
/// <summary>任务策略实例</summary>
|
||
private ITaskStrategy _taskStrategy;
|
||
|
||
/// <summary>
|
||
/// 任务策略属性
|
||
/// <para>支持热更新,设置时自动订阅/取消订阅变更事件</para>
|
||
/// </summary>
|
||
public ITaskStrategy TaskStrategy
|
||
{
|
||
get => _taskStrategy;
|
||
set
|
||
{
|
||
// 取消旧策略的事件订阅
|
||
if (_taskStrategy is JsonFileTaskStrategy oldStrategy)
|
||
{
|
||
oldStrategy.Changed -= OnTaskStrategyChanged;
|
||
oldStrategy.DisposeWatcher();
|
||
}
|
||
|
||
// 设置新策略(null 时使用默认策略)
|
||
_taskStrategy = value ?? new JsonFileTaskStrategy();
|
||
|
||
// 订阅新策略的变更事件
|
||
if (_taskStrategy is JsonFileTaskStrategy newStrategy)
|
||
{
|
||
newStrategy.Changed += OnTaskStrategyChanged;
|
||
newStrategy.EnsureWatcher();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>任务列表变更事件</summary>
|
||
public event EventHandler<TaskListChangedEventArgs> TaskListChanged;
|
||
|
||
#endregion
|
||
|
||
#region 构造函数
|
||
|
||
/// <summary>
|
||
/// 初始化抽象环线任务基类
|
||
/// <para>自动使用默认的 JSON 文件策略并加载初始任务配置</para>
|
||
/// </summary>
|
||
protected AbstractLoopMission()
|
||
{
|
||
TaskStrategy = new JsonFileTaskStrategy();
|
||
ApplyTaskStrategy();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 任务管理方法
|
||
|
||
/// <summary>
|
||
/// 获取当前任务列表的只读副本(线程安全)
|
||
/// </summary>
|
||
/// <returns>任务列表快照</returns>
|
||
public IReadOnlyList<LoopTask> GetTasks()
|
||
{
|
||
lock (_tasksLock)
|
||
{
|
||
return Tasks.ToArray();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加任务到列表末尾
|
||
/// </summary>
|
||
/// <param name="task">要添加的任务</param>
|
||
/// <returns>新任务的索引</returns>
|
||
/// <exception cref="ArgumentNullException">task 为 null 时抛出</exception>
|
||
public int EnqueueTask(LoopTask task)
|
||
{
|
||
if (task == null)
|
||
throw new ArgumentNullException(nameof(task));
|
||
|
||
lock (_tasksLock)
|
||
{
|
||
Tasks.Add(task);
|
||
int index = Tasks.Count - 1;
|
||
|
||
// 触发任务添加事件
|
||
TaskListChanged?.Invoke(this, new TaskListChangedEventArgs(TaskChangeType.Added, index, task));
|
||
|
||
// 更新状态中的任务计数
|
||
((AbstractLoopMissionStatus)status).TaskCount = Tasks.Count;
|
||
|
||
return index;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新指定索引的任务
|
||
/// </summary>
|
||
/// <param name="index">任务索引</param>
|
||
/// <param name="task">新的任务数据</param>
|
||
/// <returns>是否更新成功</returns>
|
||
public bool UpdateTask(int index, LoopTask task)
|
||
{
|
||
lock (_tasksLock)
|
||
{
|
||
if (index < 0 || index >= Tasks.Count)
|
||
return false;
|
||
|
||
Tasks[index] = task;
|
||
|
||
// 触发任务更新事件
|
||
TaskListChanged?.Invoke(this, new TaskListChangedEventArgs(TaskChangeType.Updated, index, task));
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移除指定索引的任务
|
||
/// </summary>
|
||
/// <param name="index">任务索引</param>
|
||
/// <returns>是否移除成功</returns>
|
||
public bool RemoveTask(int index)
|
||
{
|
||
lock (_tasksLock)
|
||
{
|
||
if (index < 0 || index >= Tasks.Count)
|
||
return false;
|
||
|
||
LoopTask task = Tasks[index];
|
||
Tasks.RemoveAt(index);
|
||
|
||
// 触发任务移除事件
|
||
TaskListChanged?.Invoke(this, new TaskListChangedEventArgs(TaskChangeType.Removed, index, task));
|
||
|
||
// 更新状态中的任务计数
|
||
((AbstractLoopMissionStatus)status).TaskCount = Tasks.Count;
|
||
|
||
return true;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 车辆查找辅助方法
|
||
|
||
/// <summary>
|
||
/// 查找恰好停在指定站点的车辆
|
||
/// <para>条件:车辆的 holdingLocks 只包含该站点(表示车辆已到达并停稳)</para>
|
||
/// </summary>
|
||
/// <param name="siteId">站点ID</param>
|
||
/// <returns>找到的车辆,未找到返回 null</returns>
|
||
protected virtual Car FindCarArrivedAtSite(int siteId)
|
||
{
|
||
try
|
||
{
|
||
return SimpleLib.GetAllCars()
|
||
.OfType<Car>()
|
||
.FirstOrDefault(car =>
|
||
car?.status?.holdingLocks != null &&
|
||
car.status.holdingLocks.Length == 1 &&
|
||
car.status.holdingLocks[0] == siteId);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[AbstractLoopMission] FindCarArrivedAtSite({siteId}) error: {ex.Message}");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前在站或正在前往指定站点的所有车辆
|
||
/// <para>用于流量控制统计</para>
|
||
/// </summary>
|
||
/// <param name="siteId">站点ID</param>
|
||
/// <returns>车辆集合</returns>
|
||
protected IEnumerable<Car> GetCarsAtOrHeadingToSite(int siteId)
|
||
{
|
||
try
|
||
{
|
||
return SimpleLib.GetAllCars()
|
||
.OfType<Car>()
|
||
.Where(car =>
|
||
{
|
||
if (car?.status == null)
|
||
return false;
|
||
|
||
// 检查是否在站:holdingLocks 包含该站点
|
||
if (car.status.holdingLocks?.Contains(siteId) == true)
|
||
return true;
|
||
|
||
// 检查是否前往:pendingLocks 最后一个锁定点为该站点
|
||
if (car.status.pendingLocks.LastOrDefault()==siteId)
|
||
{
|
||
return true;
|
||
}
|
||
// 检查是否前往:goalSite 标签指向该站点
|
||
/* if (car.tags?.TryGetValue("goalSite", out var destStr) == true &&
|
||
int.TryParse(destStr, out int destId) &&
|
||
destId == siteId)
|
||
return true;*/
|
||
|
||
return false;
|
||
})
|
||
.ToList();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[AbstractLoopMission] GetCarsAtOrHeadingToSite({siteId}) error: {ex.Message}");
|
||
return Array.Empty<Car>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 统计当前在站或正在前往指定站点的车辆数量
|
||
/// </summary>
|
||
/// <param name="siteId">站点ID</param>
|
||
/// <returns>车辆数量</returns>
|
||
protected int CountCarsAtOrHeadingToSite(int siteId)
|
||
{
|
||
return GetCarsAtOrHeadingToSite(siteId).Count();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查车辆是否空闲(未被占用、未有目标、未执行任务)
|
||
/// </summary>
|
||
/// <param name="car">车辆</param>
|
||
/// <returns>是否空闲</returns>
|
||
protected bool IsCarIdle(Car car)
|
||
{
|
||
if (car?.tags == null)
|
||
return false;
|
||
|
||
try
|
||
{
|
||
// 检查是否存在表示忙碌状态的标签
|
||
if (car.tags.ContainsKey("occupied")) return false;
|
||
if (car.tags.ContainsKey("dest")) return false;
|
||
if (car.tags.ContainsKey("deliver")) return false;
|
||
|
||
return true;
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 流量控制检查
|
||
|
||
/// <summary>
|
||
/// 检查目标站点是否满足流量控制条件
|
||
/// <para>统计当前在站或正在前往该站点的车辆数量,与最大允许数比较</para>
|
||
/// </summary>
|
||
/// <param name="targetSiteId">目标站点ID</param>
|
||
/// <param name="maxAllowed">最大允许车辆数(0或负数表示不限制)</param>
|
||
/// <returns>true=可以发车,false=已达上限需等待</returns>
|
||
protected bool CheckTrafficControl(int targetSiteId, int maxAllowed)
|
||
{
|
||
// 不限制流量的情况
|
||
if (maxAllowed <= 0)
|
||
return true;
|
||
|
||
int currentCount = CountCarsAtOrHeadingToSite(targetSiteId);
|
||
return currentCount < maxAllowed;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 外部触发接口(子类重写)
|
||
|
||
/// <summary>
|
||
/// API 触发处理接口
|
||
/// <para>当任务的 StartType 为 Api 时,车辆到达当前站点后会调用此方法</para>
|
||
/// <para>子类重写此方法实现 API 触发的业务逻辑</para>
|
||
/// </summary>
|
||
/// <param name="currentSiteId">当前站点ID</param>
|
||
/// <param name="task">匹配的任务配置</param>
|
||
/// <param name="car">到达的车辆</param>
|
||
/// <returns>
|
||
/// 处理结果:
|
||
/// <list type="bullet">
|
||
/// <item><description><see cref="ExternalTriggerResult.UseTarget"/> - 使用子类指定的目标站点</description></item>
|
||
/// <item><description><see cref="ExternalTriggerResult.UseConfigTarget"/> - 使用配置文件中的目标站点</description></item>
|
||
/// <item><description><see cref="ExternalTriggerResult.Fail"/> - 业务失败,不分配任务</description></item>
|
||
/// </list>
|
||
/// </returns>
|
||
protected virtual ExternalTriggerResult OnApiTrigger(int currentSiteId, LoopTask task, Car car)
|
||
{
|
||
// 默认返回失败,子类需重写以启用 API 触发功能
|
||
return ExternalTriggerResult.Fail();
|
||
}
|
||
|
||
/// <summary>
|
||
/// PLC 信号触发处理接口
|
||
/// <para>当任务的 StartType 为 Plc 时,车辆到达当前站点后会调用此方法</para>
|
||
/// <para>子类重写此方法实现 PLC 触发的业务逻辑</para>
|
||
/// </summary>
|
||
/// <param name="currentSiteId">当前站点ID</param>
|
||
/// <param name="task">匹配的任务配置</param>
|
||
/// <param name="car">到达的车辆</param>
|
||
/// <returns>处理结果</returns>
|
||
protected virtual ExternalTriggerResult OnPlcTrigger(int currentSiteId, LoopTask task, Car car)
|
||
{
|
||
// 默认返回失败,子类需重写以启用 PLC 触发功能
|
||
return ExternalTriggerResult.Fail();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 充电条件信号触发处理接口
|
||
/// <para>当任务的 StartType 为 Plc 时,车辆到达当前站点后会调用此方法</para>
|
||
/// <para>子类重写此方法实现 PLC 触发的业务逻辑</para>
|
||
/// </summary>
|
||
/// <param name="currentSiteId">当前站点ID</param>
|
||
/// <param name="task">匹配的任务配置</param>
|
||
/// <param name="car">到达的车辆</param>
|
||
/// <returns>处理结果</returns>
|
||
protected virtual ExternalTriggerResult OnChargeTrigger(int currentSiteId, LoopTask task, Car car)
|
||
{
|
||
// 默认返回失败,子类需重写已启用去充电触发功能
|
||
return ExternalTriggerResult.Fail();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按钮盒触发处理接口
|
||
/// <para>当任务的 StartType 为 ButtonBox 时,车辆到达当前站点后会调用此方法</para>
|
||
/// <para>子类重写此方法实现按钮盒触发的业务逻辑</para>
|
||
/// </summary>
|
||
/// <param name="currentSiteId">当前站点ID</param>
|
||
/// <param name="task">匹配的任务配置</param>
|
||
/// <param name="car">到达的车辆</param>
|
||
/// <returns>处理结果</returns>
|
||
protected virtual ExternalTriggerResult OnButtonTrigger(int currentSiteId, LoopTask task, Car car)
|
||
{
|
||
// 默认返回失败,子类需重写以启用按钮盒触发功能
|
||
return ExternalTriggerResult.Fail();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据启动类型调用对应的子类触发接口
|
||
/// </summary>
|
||
/// <param name="startType">启动类型</param>
|
||
/// <param name="currentSiteId">当前站点ID</param>
|
||
/// <param name="task">匹配的任务配置</param>
|
||
/// <param name="car">到达的车辆</param>
|
||
/// <returns>子类返回的处理结果</returns>
|
||
private ExternalTriggerResult InvokeExternalTrigger(TaskStartType startType, int currentSiteId, LoopTask task, Car car)
|
||
{
|
||
return startType switch
|
||
{
|
||
TaskStartType.Api => OnApiTrigger(currentSiteId, task, car),
|
||
TaskStartType.Plc => OnPlcTrigger(currentSiteId, task, car),
|
||
TaskStartType.ButtonBox => OnButtonTrigger(currentSiteId, task, car),
|
||
TaskStartType.Charge => OnChargeTrigger(currentSiteId, task, car),
|
||
_ => ExternalTriggerResult.Fail()
|
||
};
|
||
}
|
||
|
||
|
||
|
||
#endregion
|
||
|
||
#region 任务策略管理
|
||
|
||
/// <summary>
|
||
/// 从任务策略刷新并应用任务列表
|
||
/// <para>读取配置文件内容并替换当前任务列表</para>
|
||
/// </summary>
|
||
public void ApplyTaskStrategy()
|
||
{
|
||
try
|
||
{
|
||
// 刷新策略数据
|
||
TaskStrategy?.Refresh();
|
||
var sourceTasks = TaskStrategy?.GetTasks();
|
||
|
||
if (sourceTasks == null)
|
||
return;
|
||
|
||
lock (_tasksLock)
|
||
{
|
||
// 清空并重新加载任务
|
||
Tasks.Clear();
|
||
foreach (var task in sourceTasks)
|
||
{
|
||
Tasks.Add(task);
|
||
}
|
||
|
||
// 触发任务替换事件
|
||
TaskListChanged?.Invoke(this, new TaskListChangedEventArgs(TaskChangeType.Replaced, -1, null));
|
||
|
||
// 更新状态中的任务计数
|
||
((AbstractLoopMissionStatus)status).TaskCount = Tasks.Count;
|
||
}
|
||
|
||
System.Diagnostics.Debug.WriteLine($"[AbstractLoopMission] ApplyTaskStrategy: 加载了 {Tasks.Count} 个任务");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 记录错误信息
|
||
((AbstractLoopMissionStatus)status).LastError = ex.Message;
|
||
System.Diagnostics.Debug.WriteLine($"[AbstractLoopMission] ApplyTaskStrategy error: {ex}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 任务策略配置文件变更处理
|
||
/// </summary>
|
||
private void OnTaskStrategyChanged(object sender, EventArgs e)
|
||
{
|
||
try
|
||
{
|
||
ApplyTaskStrategy();
|
||
System.Diagnostics.Debug.WriteLine("[AbstractLoopMission] OnTaskStrategyChanged: 配置文件已更新");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[AbstractLoopMission] OnTaskStrategyChanged error: {ex}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 核心业务逻辑
|
||
|
||
/// <summary>
|
||
/// 业务逻辑主循环
|
||
/// <para>在 _logicThread 线程中执行,每 500ms 处理一次任务调度</para>
|
||
/// </summary>
|
||
protected virtual void ExecuteLogicLoop()
|
||
{
|
||
while (_logicRunning)
|
||
{
|
||
try
|
||
{
|
||
ProcessAutoLoopTasks();
|
||
Thread.Sleep(LOGIC_LOOP_INTERVAL_MS);
|
||
}
|
||
catch (ThreadAbortException)
|
||
{
|
||
break;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[AbstractLoopMission] ExecuteLogicLoop error: {ex}");
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理所有任务(主调度入口)
|
||
/// <para>按顺序处理:自动循环任务 → 外部触发任务 → 分流点任务 → 汇合点任务 → 循环启动动作</para>
|
||
/// </summary>
|
||
protected virtual void ProcessAutoLoopTasks()
|
||
{
|
||
// 获取任务快照(避免长时间持锁)
|
||
List<LoopTask> snapshot;
|
||
lock (_tasksLock)
|
||
{
|
||
snapshot = Tasks.ToList();
|
||
}
|
||
|
||
// 1. 处理自动循环任务(StartType=AutoLoop, Kind=Loop)
|
||
ProcessAutoLoopTypeTasks(snapshot);
|
||
|
||
// 2. 处理外部触发任务(StartType=Api/Plc/ButtonBox/charge)
|
||
ProcessExternalTriggerTasks(snapshot);
|
||
|
||
// 3. 处理自动分流点任务(StartType=AutoLoop, Kind=BranchPoint)
|
||
ProcessBranchPointTasks(snapshot);
|
||
|
||
// 4. 处理自动汇合点任务(StartType=AutoLoop, Kind=JoinPoint)
|
||
ProcessJoinPointTasks(snapshot);
|
||
|
||
// 5. 执行循环启动动作(处理站点默认目标、执行导航、异常恢复等)
|
||
LoopStartAction();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理自动循环类型任务
|
||
/// <para>筛选 StartType=AutoLoop 且 Kind=Loop 的任务,按优先级处理</para>
|
||
/// </summary>
|
||
/// <param name="snapshot">任务快照</param>
|
||
private void ProcessAutoLoopTypeTasks(List<LoopTask> snapshot)
|
||
{
|
||
// 筛选自动循环任务并按优先级排序
|
||
var autoTasks = snapshot
|
||
.Where(t => t.StartType == TaskStartType.AutoLoop && t.Kind == TaskKind.Loop)
|
||
.OrderByDescending(t => t.Priority)
|
||
.ToList();
|
||
|
||
foreach (var task in autoTasks)
|
||
{
|
||
try
|
||
{
|
||
// 查找到达当前站点的车辆
|
||
var car = FindCarArrivedAtSite(task.CurrentStationId);
|
||
if (car == null)
|
||
continue;
|
||
|
||
// 检查车辆是否可用(SelectCar 返回 1 表示可用)
|
||
if (Commons.SelectCar(car) != 1)
|
||
continue;
|
||
|
||
// 流量控制检查
|
||
if (!CheckTrafficControl(task.TargetStationId, task.TrafficControl))
|
||
{
|
||
|
||
Diagnosis.Post($"[AbstractLoopMission] AutoLoop: 目标站点 {task.TargetStationId} 流量已满,车辆 {car.name} 等待", "Loop-AutoLoop", true);
|
||
continue;
|
||
}
|
||
|
||
// 分配目标站点
|
||
AssignCarToTarget(car, task.TargetStationId);
|
||
Diagnosis.Post($"[AbstractLoopMission] AutoLoop: 车辆 {car.name} 从站点 {task.CurrentStationId} 自动分配到目标站点 {task.TargetStationId}", "Loop-AutoLoop", true);
|
||
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Post($"[AbstractLoopMission] ProcessAutoLoopTypeTasks error: {ex}", "Loop-AutoLoop", true);
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理外部触发类型任务(API/PLC/ButtonBox)
|
||
/// <para>依次处理各类型的外部触发任务</para>
|
||
/// </summary>
|
||
/// <param name="snapshot">任务快照</param>
|
||
private void ProcessExternalTriggerTasks(List<LoopTask> snapshot)
|
||
{
|
||
// 依次处理 API、PLC、ButtonBox 类型任务
|
||
ProcessExternalTasksByType(snapshot, TaskStartType.Api);
|
||
ProcessExternalTasksByType(snapshot, TaskStartType.Plc);
|
||
ProcessExternalTasksByType(snapshot, TaskStartType.ButtonBox);
|
||
ProcessExternalTasksByType(snapshot, TaskStartType.Charge);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按启动类型处理外部触发任务
|
||
/// <para>处理普通循环任务、分流点任务、汇合点任务</para>
|
||
/// </summary>
|
||
/// <param name="snapshot">任务快照</param>
|
||
/// <param name="startType">启动类型</param>
|
||
private void ProcessExternalTasksByType(List<LoopTask> snapshot, TaskStartType startType)
|
||
{
|
||
// 筛选指定启动类型的普通循环任务
|
||
var externalTasks = snapshot
|
||
.Where(t => t.StartType == startType && t.Kind == TaskKind.Loop)
|
||
.OrderByDescending(t => t.Priority)
|
||
.ToList();
|
||
|
||
foreach (var task in externalTasks)
|
||
{
|
||
try
|
||
{
|
||
// 查找到达当前站点的车辆
|
||
var car = FindCarArrivedAtSite(task.CurrentStationId);
|
||
if (car == null)
|
||
continue;
|
||
|
||
// 检查车辆是否可用
|
||
if (Commons.SelectCar(car) != 1)
|
||
continue;
|
||
|
||
// 调用子类接口获取处理结果
|
||
ExternalTriggerResult result = InvokeExternalTrigger(startType, task.CurrentStationId, task, car);
|
||
|
||
// 子类返回失败则跳过
|
||
if (result?.Success != true)
|
||
continue;
|
||
|
||
// 确定目标站点:子类指定 > 配置文件
|
||
int targetSiteId = result.CustomTargetSiteId ?? task.TargetStationId;
|
||
|
||
// 流量控制检查
|
||
if (!CheckTrafficControl(targetSiteId, task.TrafficControl))
|
||
{
|
||
Diagnosis.Post($"[AbstractLoopMission] {startType}: 目标站点 {targetSiteId} 流量已满,车辆 {car.name} 等待", $"Loop-{startType}", true);
|
||
|
||
continue;
|
||
}
|
||
|
||
// 分配目标站点
|
||
AssignCarToTarget(car, targetSiteId);
|
||
Diagnosis.Post($"[AbstractLoopMission] {startType}: 车辆 {car.name} 从站点 {task.CurrentStationId} 分配到目标站点 {targetSiteId}", $"Loop-{startType}", true);
|
||
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Post($"[AbstractLoopMission] ProcessExternalTasksByType({startType}) error: {ex}", $"Loop-{startType}", true);
|
||
|
||
}
|
||
}
|
||
|
||
// 处理分流点类型的外部触发任务
|
||
ProcessExternalBranchTasks(snapshot, startType);
|
||
|
||
// 处理汇合点类型的外部触发任务
|
||
ProcessExternalJoinTasks(snapshot, startType);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理外部触发的分流点任务
|
||
/// <para>按当前站点分组,优先级排序,调用子类接口后分配目标</para>
|
||
/// </summary>
|
||
/// <param name="snapshot">任务快照</param>
|
||
/// <param name="startType">启动类型</param>
|
||
private void ProcessExternalBranchTasks(List<LoopTask> snapshot, TaskStartType startType)
|
||
{
|
||
// 按当前站点分组
|
||
var branchGroups = snapshot
|
||
.Where(t => t.Kind == TaskKind.BranchPoint && t.StartType == startType)
|
||
.GroupBy(t => t.CurrentStationId)
|
||
.ToList();
|
||
|
||
foreach (var group in branchGroups)
|
||
{
|
||
int currentSiteId = group.Key;
|
||
|
||
// 查找到达当前站点的车辆
|
||
var car = FindCarArrivedAtSite(currentSiteId);
|
||
if (car == null || Commons.SelectCar(car) != 1)
|
||
continue;
|
||
|
||
// 按优先级排序
|
||
var sortedTasks = group.OrderByDescending(t => t.Priority).ToList();
|
||
|
||
foreach (var task in sortedTasks)
|
||
{
|
||
// 调用子类接口
|
||
ExternalTriggerResult result = InvokeExternalTrigger(startType, currentSiteId, task, car);
|
||
|
||
if (result?.Success != true)
|
||
continue;
|
||
|
||
// 确定目标:子类指定 > 按优先级选择流量允许的目标
|
||
int targetSiteId = result.CustomTargetSiteId ?? SelectBranchTarget(task, snapshot);
|
||
|
||
if (CheckTrafficControl(targetSiteId, task.TrafficControl))
|
||
{
|
||
AssignCarToTarget(car, targetSiteId);
|
||
Diagnosis.Post($"[AbstractLoopMission] {startType} BranchPoint: 车辆 {car.name} 从分流点 {currentSiteId} 分配到目标 {targetSiteId}", $"BranchPoint-{startType}", true);
|
||
|
||
break; // 分流点只分配一个目标
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// 处理外部触发的汇合点任务
|
||
/// <para>按目标站点分组(汇合点),每组中的策略按优先级排列,排列后每个策略根据流量判断,最后采用接口返回结果</para>
|
||
/// </summary>
|
||
/// <param name="snapshot">任务快照</param>
|
||
/// <param name="startType">启动类型</param>
|
||
private void ProcessExternalJoinTasks(List<LoopTask> snapshot, TaskStartType startType)
|
||
{
|
||
// 按目标站点分组(汇合点)
|
||
var joinGroups = snapshot
|
||
.Where(t => t.Kind == TaskKind.JoinPoint && t.StartType == startType)
|
||
.GroupBy(t => t.TargetStationId)
|
||
.ToList();
|
||
|
||
foreach (var group in joinGroups)
|
||
{
|
||
int targetSiteId = group.Key;
|
||
|
||
// 按优先级降序排列组内任务
|
||
var sortedTasks = group.OrderByDescending(t => t.Priority).ToList();
|
||
|
||
// 遍历排序后的任务,根据流量和接口返回结果处理
|
||
foreach (var task in sortedTasks)
|
||
{
|
||
try
|
||
{
|
||
|
||
// 查找到达当前站点的车辆
|
||
var car = FindCarArrivedAtSite(task.CurrentStationId);
|
||
if (car == null)
|
||
continue;
|
||
|
||
// 检查车辆是否可用
|
||
if (Commons.SelectCar(car) != 1)
|
||
continue;
|
||
|
||
|
||
// 流量控制检查(优先判断,避免无效的接口调用)
|
||
if (!CheckTrafficControl(targetSiteId, task.TrafficControl))
|
||
{
|
||
Diagnosis.Post($"[AbstractLoopMission] {startType} JoinPoint: 目标站点 {targetSiteId} 流量已满,跳过任务 {task.Id}", $"JoinPoint-{startType}", true);
|
||
continue;
|
||
}
|
||
|
||
|
||
// 调用子类接口获取处理结果
|
||
ExternalTriggerResult result = InvokeExternalTrigger(startType, task.CurrentStationId, task, car);
|
||
|
||
// 子类返回失败则跳过当前任务
|
||
if (result?.Success != true)
|
||
continue;
|
||
|
||
// 确定最终目标:子类指定 > 配置文件
|
||
int finalTarget = result.CustomTargetSiteId ?? targetSiteId;
|
||
|
||
// 如果子类指定了不同的目标,需要再次检查流量
|
||
if (finalTarget != targetSiteId && !CheckTrafficControl(finalTarget, task.TrafficControl))
|
||
{
|
||
Diagnosis.Post($"[AbstractLoopMission] {startType} JoinPoint: 子类指定目标站点 {finalTarget} 流量已满,车辆 {car.name} 等待", $"JoinPoint-{startType}", true);
|
||
continue;
|
||
}
|
||
|
||
// 分配目标站点
|
||
AssignCarToTarget(car, finalTarget);
|
||
Diagnosis.Post($"[AbstractLoopMission] {startType} JoinPoint: 车辆 {car.name} 从站点 {task.CurrentStationId} 汇入目标 {finalTarget}(优先级 {task.Priority})", $"JoinPoint-{startType}", true);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Post($"[AbstractLoopMission] ProcessExternalJoinTasks({startType}) task {task.Id} error: {ex.Message}", $"JoinPoint-{startType}", true);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 循环启动动作
|
||
/// <para>处理站点配置的默认目标、执行路径规划导航、检测并处理脚本异常</para>
|
||
/// <para>注意:此方法不拆分,保持原有逻辑完整性</para>
|
||
/// </summary>
|
||
private void LoopStartAction()
|
||
{
|
||
try
|
||
{
|
||
foreach (var car in SimpleLib.GetAllCars())
|
||
{
|
||
// 如果车辆可用且没有目标,尝试从站点配置读取默认目标
|
||
if (Commons.SelectCar(car) == 1 && !car.tags.Contains("goalSite"))
|
||
{
|
||
var carSite = SimpleLib.GetSite(car.GetLastSite());
|
||
if (carSite != null && carSite.fields.TryGetValue("goSite", out var dst))
|
||
{
|
||
Commons.AddOrUpdateTag(car.tags, "goalSite", dst);
|
||
}
|
||
if (carSite != null && !car.tags.Contains("goalSite"))
|
||
{
|
||
var taskLine = FindBestTaskForCar((Car)car);
|
||
if (taskLine!=null && !taskLine.IsAtStartPoint && !taskLine.IsAtEndPoint)
|
||
{
|
||
Commons.AddOrUpdateTag(car.tags, "goalSite", taskLine.TargetSiteId.ToString());
|
||
}
|
||
}
|
||
}
|
||
|
||
// 如果车辆可用且有目标,执行导航
|
||
if (Commons.SelectCar(car) == 1 && car.tags.Contains("goalSite"))
|
||
{
|
||
var goalSite = SimpleLib.GetAllSites()
|
||
.FirstOrDefault(s => car.tags.IsEqual("goalSite", s.id.ToString()));
|
||
var holdingSite=car.status.holdingLocks.FirstOrDefault();
|
||
if (goalSite != null && holdingSite!=goalSite.id )
|
||
{
|
||
_=GoSite(car, goalSite);
|
||
|
||
}
|
||
else if (goalSite != null && holdingSite == goalSite.id)
|
||
{
|
||
// 已到达目标,清理 goalSite 标签
|
||
car.tags.Remove("goalSite");
|
||
car.tags.Remove("loopAssigned");
|
||
}
|
||
|
||
}
|
||
|
||
// 以下处理声明了脚本异常自恢复能力的车辆(IScriptErrorRecoverable,当前为 Kiva)
|
||
if (!(car is IScriptErrorRecoverable recoverable) || !(car is Car mcar))
|
||
continue;
|
||
|
||
// 检测脚本异常状态:车辆被占用但脚本状态为 Error 或 Bad
|
||
LadderLogic.TriggerOnce(
|
||
mcar.tags.Contains("occupied") &&
|
||
(mcar.status.programs.printStatus().Contains("Error") || mcar.status.programs.printStatus().Contains("Bad")),
|
||
SCRIPT_ERROR_TRIGGER_DELAY_MS,
|
||
() =>
|
||
{
|
||
// 获取车辆最后持有的站点锁
|
||
var lastHoldingLocks = car.status.holdingLocks.First();
|
||
var site = SimpleLib.GetSite(lastHoldingLocks);
|
||
|
||
// 标记站点不可用
|
||
site.MakeAllCarsUnavailable();
|
||
|
||
// 禁止车辆调度
|
||
mcar.NoSchedule(true);
|
||
Diagnosis.Post($"errStatus ", "NoSchedule", true);
|
||
|
||
// 重置车辆站点ID
|
||
mcar.siteID = -1;
|
||
|
||
// 更新车辆使用状态
|
||
mcar.status.usage.AddUsage("base", new CarUsage.CarUsageInfo()
|
||
{
|
||
scheduling = false,
|
||
refreshing = false
|
||
});
|
||
|
||
// 设置车辆状态为返厂检修
|
||
mcar.lstatus = "返厂检修";
|
||
|
||
// 清理车辆标签
|
||
Commons.DeleteTag(mcar.tags, "occupied");
|
||
// Commons.DeleteTag(mcar.tags, "goalSite");
|
||
|
||
// 执行车辆重置
|
||
((Car)car).Intercept((_) =>
|
||
{
|
||
recoverable.RecoverReset(lastHoldingLocks);
|
||
});
|
||
},
|
||
mcar.id
|
||
);
|
||
}
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Diagnosis.Post($"loop mission error {ExceptionFormatter.FormatEx(e)}");
|
||
}
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 导航车辆到目标站点
|
||
/// <para>创建路径规划并执行移动任务</para>
|
||
/// </summary>
|
||
/// <param name="car">车辆</param>
|
||
/// <param name="targetSite">目标站点</param>
|
||
/// <param name="action">动作参数,默认为 "/"</param>
|
||
/// <param name="reverse">是否倒车,默认为 false</param>
|
||
public async Task GoSite(AbstractCar car, Site targetSite, string action = "/", bool reverse = false)
|
||
{
|
||
try
|
||
{
|
||
// 创建路径规划
|
||
var plan = new SegmentPlan() { usingCar = car };
|
||
plan.fields["reverse"] = reverse.ToString();
|
||
plan.fields["action"] = action;
|
||
plan.fields["allow_destination_on_route"] = "true";
|
||
|
||
// 查找从当前位置到目标站点的路径
|
||
plan.FindRoute(SimpleLib.GetSite(car.GetLastSite()), targetSite);
|
||
|
||
// 编译并执行移动脚本(默认 Forecast)
|
||
var program = plan.Compile("move");
|
||
|
||
// 标记车辆为占用状态
|
||
car.tags.Add("occupied", $"go{targetSite.id}");
|
||
|
||
Console.WriteLine($">>Script:{program.script}");
|
||
|
||
// 等待任务完成
|
||
var tsk = program.Queue();
|
||
await tsk;
|
||
|
||
// 任务完成后清理标签
|
||
car.tags.Remove("occupied");
|
||
car.tags.Remove("goalSite");
|
||
car.tags.Remove("loopAssigned");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
try
|
||
{
|
||
car.tags.Remove("occupied");
|
||
if (car is Car trafficCar && car.GetLastSite() > 0)
|
||
{
|
||
var currentSite = SimpleLib.GetSite(car.GetLastSite());
|
||
if (currentSite != null)
|
||
{
|
||
trafficCar.TrafficReset(currentSite, true, strict: false);
|
||
}
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
}
|
||
|
||
// 记录异常并等待后重试
|
||
Diagnosis.Post(
|
||
$"{car.name}循环任务报错{ExceptionFormatter.FormatEx(ex)}--去往循环点id:{targetSite.id},name:{targetSite.name}",
|
||
$"重新执行循环任务",
|
||
true
|
||
);
|
||
Thread.Sleep(ERROR_RECOVERY_DELAY_MS);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理自动分流点任务
|
||
/// <para>筛选 StartType=AutoLoop 且 Kind=BranchPoint 的任务</para>
|
||
/// <para>按当前站点分组,优先级排序,选择流量允许的目标</para>
|
||
/// </summary>
|
||
/// <param name="allTasks">任务列表</param>
|
||
protected virtual void ProcessBranchPointTasks(List<LoopTask> allTasks)
|
||
{
|
||
// 按当前站点分组
|
||
var branchGroups = allTasks
|
||
.Where(t => t.Kind == TaskKind.BranchPoint && t.StartType == TaskStartType.AutoLoop)
|
||
.GroupBy(t => t.CurrentStationId)
|
||
.ToList();
|
||
|
||
foreach (var group in branchGroups)
|
||
{
|
||
int currentSiteId = group.Key;
|
||
|
||
// 查找到达当前站点的车辆
|
||
var car = FindCarArrivedAtSite(currentSiteId);
|
||
if (car == null || Commons.SelectCar(car) != 1)
|
||
continue;
|
||
|
||
// 按优先级排序,选择流量允许的目标
|
||
var sortedTasks = group.OrderByDescending(t => t.Priority).ToList();
|
||
|
||
foreach (var task in sortedTasks)
|
||
{
|
||
if (CheckTrafficControl(task.TargetStationId, task.TrafficControl))
|
||
{
|
||
AssignCarToTarget(car, task.TargetStationId);
|
||
Diagnosis.Post($"[AbstractLoopMission] BranchPoint: 车辆 {car.name}-{car.id} 从分流点 {currentSiteId} 分配到目标 {task.TargetStationId}(优先级 {task.Priority})", $"BranchPoint-AutoLoop", true);
|
||
|
||
break; // 分流点只分配一个目标
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理自动汇合点任务
|
||
/// <para>筛选 StartType=AutoLoop 且 Kind=JoinPoint 的任务</para>
|
||
/// <para>按目标站点分组,优先级排序,选择有车在站的最高优先级任务</para>
|
||
/// </summary>
|
||
/// <param name="allTasks">任务列表</param>
|
||
protected virtual void ProcessJoinPointTasks(List<LoopTask> allTasks)
|
||
{
|
||
// 按目标站点分组(汇合点)
|
||
var joinGroups = allTasks
|
||
.Where(t => t.Kind == TaskKind.JoinPoint && t.StartType == TaskStartType.AutoLoop)
|
||
.GroupBy(t => t.TargetStationId)
|
||
.ToList();
|
||
|
||
foreach (var group in joinGroups)
|
||
{
|
||
int targetSiteId = group.Key;
|
||
|
||
// 检查汇合点流量控制
|
||
var firstTask = group.First();
|
||
if (!CheckTrafficControl(targetSiteId, firstTask.TrafficControl))
|
||
continue;
|
||
|
||
// 按优先级排序,选择有车在站的最高优先级任务
|
||
var sortedTasks = group.OrderByDescending(t => t.Priority).ToList();
|
||
|
||
foreach (var task in sortedTasks)
|
||
{
|
||
var car = FindCarArrivedAtSite(task.CurrentStationId);
|
||
|
||
if (car != null && Commons.SelectCar(car) == 1)
|
||
{
|
||
AssignCarToTarget(car, targetSiteId);
|
||
Diagnosis.Post($"[AbstractLoopMission] JoinPoint: 车辆 {car.name} 从站点 {task.CurrentStationId} 汇入目标 {targetSiteId}(优先级 {task.Priority})", $"JoinPoint-AutoLoop", true);
|
||
|
||
//break; // 汇合点一次只放行一辆车
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 分流点目标选择
|
||
/// <para>按优先级选择流量允许的目标站点</para>
|
||
/// </summary>
|
||
/// <param name="branchTask">当前分流任务</param>
|
||
/// <param name="allTasks">所有任务列表</param>
|
||
/// <returns>选中的目标站点ID</returns>
|
||
protected virtual int SelectBranchTarget(LoopTask branchTask, List<LoopTask> allTasks)
|
||
{
|
||
// 查找同一当前站点的所有分流目标
|
||
var branches = allTasks
|
||
.Where(t => t.Kind == TaskKind.BranchPoint && t.CurrentStationId == branchTask.CurrentStationId)
|
||
.OrderByDescending(t => t.Priority)
|
||
.ToList();
|
||
|
||
// 按优先级选择流量允许的目标
|
||
foreach (var branch in branches)
|
||
{
|
||
if (CheckTrafficControl(branch.TargetStationId, branch.TrafficControl))
|
||
{
|
||
return branch.TargetStationId;
|
||
}
|
||
}
|
||
|
||
// 默认返回原任务目标
|
||
return branchTask.TargetStationId;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 汇合点通行判断
|
||
/// <para>检查当前任务是否有最高优先级通行权(是否有更高优先级的车在等待)</para>
|
||
/// </summary>
|
||
/// <param name="joinTask">当前汇合任务</param>
|
||
/// <param name="allTasks">所有任务列表</param>
|
||
/// <returns>true=可以通行,false=需要等待更高优先级的车</returns>
|
||
protected virtual bool CanJoinPass(LoopTask joinTask, List<LoopTask> allTasks)
|
||
{
|
||
// 查找同一目标站点的所有汇合来源
|
||
var joinSources = allTasks
|
||
.Where(t => t.Kind == TaskKind.JoinPoint && t.TargetStationId == joinTask.TargetStationId)
|
||
.OrderByDescending(t => t.Priority)
|
||
.ToList();
|
||
|
||
// 检查是否有更高优先级的任务且其站点有空闲车等待
|
||
foreach (var source in joinSources)
|
||
{
|
||
if (source.Priority > joinTask.Priority)
|
||
{
|
||
var higherPriorityCar = FindCarArrivedAtSite(source.CurrentStationId);
|
||
if (higherPriorityCar != null && IsCarIdle(higherPriorityCar))
|
||
{
|
||
return false; // 有更高优先级的车等待,当前车需要让行
|
||
}
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为车辆分配目标站点
|
||
/// <para>设置车辆的 goalSite 标签和 loopAssigned 时间戳</para>
|
||
/// </summary>
|
||
/// <param name="car">车辆</param>
|
||
/// <param name="targetSiteId">目标站点ID</param>
|
||
protected virtual void AssignCarToTarget(Car car, int targetSiteId)
|
||
{
|
||
if (car == null)
|
||
return;
|
||
|
||
try
|
||
{
|
||
string targetIdStr = targetSiteId.ToString();
|
||
|
||
// 设置或更新目标站点标签
|
||
if (car.tags.ContainsKey("goalSite"))
|
||
car.tags["goalSite"] = targetIdStr;
|
||
else
|
||
car.tags.Add("goalSite", targetIdStr);
|
||
|
||
// 记录分配时间(用于调试和监控)
|
||
if (!car.tags.ContainsKey("loopAssigned"))
|
||
car.tags.Add("loopAssigned", DateTime.Now.ToString("HH:mm:ss"));
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[AbstractLoopMission] AssignCarToTarget error: {ex}");
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 线程生命周期
|
||
|
||
/// <summary>
|
||
/// 启动策略同步线程
|
||
/// <para>每秒同步一次配置文件,自动更新任务列表</para>
|
||
/// </summary>
|
||
public virtual void StartLoop()
|
||
{
|
||
if (_strategyRunning)
|
||
return;
|
||
|
||
_strategyRunning = true;
|
||
_strategyThread = new Thread(() =>
|
||
{
|
||
while (_strategyRunning)
|
||
{
|
||
try
|
||
{
|
||
ApplyTaskStrategy();
|
||
Thread.Sleep(STRATEGY_SYNC_INTERVAL_MS);
|
||
}
|
||
catch (ThreadAbortException)
|
||
{
|
||
break;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[AbstractLoopMission] Strategy loop error: {ex}");
|
||
}
|
||
}
|
||
})
|
||
{
|
||
Name = "AbstractLoopMission_Strategy",
|
||
IsBackground = true
|
||
};
|
||
_strategyThread.Start();
|
||
|
||
status.status = "策略同步已启动";
|
||
System.Diagnostics.Debug.WriteLine("[AbstractLoopMission] StartLoop: 策略同步线程已启动");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动业务逻辑线程
|
||
/// <para>每 500ms 处理一次任务调度</para>
|
||
/// </summary>
|
||
public virtual void StartLogicLoop()
|
||
{
|
||
if (_logicRunning)
|
||
return;
|
||
|
||
_logicRunning = true;
|
||
_logicThread = new Thread(ExecuteLogicLoop)
|
||
{
|
||
Name = "AbstractLoopMission_Logic",
|
||
IsBackground = true
|
||
};
|
||
_logicThread.Start();
|
||
|
||
status.status = "业务逻辑已启动";
|
||
System.Diagnostics.Debug.WriteLine("[AbstractLoopMission] StartLogicLoop: 业务逻辑线程已启动");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止策略同步线程
|
||
/// </summary>
|
||
public virtual void StopLoop()
|
||
{
|
||
try
|
||
{
|
||
_strategyRunning = false;
|
||
_strategyThread?.Join(2000);
|
||
_strategyThread = null;
|
||
System.Diagnostics.Debug.WriteLine("[AbstractLoopMission] StopLoop: 策略同步线程已停止");
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止业务逻辑线程
|
||
/// </summary>
|
||
public virtual void StopLogicLoop()
|
||
{
|
||
try
|
||
{
|
||
_logicRunning = false;
|
||
_logicThread?.Join(2000);
|
||
_logicThread = null;
|
||
System.Diagnostics.Debug.WriteLine("[AbstractLoopMission] StopLogicLoop: 业务逻辑线程已停止");
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 启动所有线程(策略同步 + 业务逻辑)
|
||
/// </summary>
|
||
public virtual void StartAll()
|
||
{
|
||
// ApplyTaskStrategy();
|
||
MarkTaskStartSitesAsTerminal();
|
||
StartLoop();
|
||
StartLogicLoop();
|
||
status.status = "已启动";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将任务策略中的起始站点标记为 terminal=true
|
||
/// </summary>
|
||
private void MarkTaskStartSitesAsTerminal()
|
||
{
|
||
List<int> startSiteIds;
|
||
lock (_tasksLock)
|
||
{
|
||
startSiteIds = Tasks
|
||
.Select(t => t.CurrentStationId)
|
||
.Where(id => id > 0)
|
||
.Distinct()
|
||
.ToList();
|
||
}
|
||
|
||
foreach (var siteId in startSiteIds)
|
||
{
|
||
var site = SimpleLib.GetSite(siteId);
|
||
if (site == null)
|
||
continue;
|
||
|
||
Commons.AddOrUpdateSiteField(site, "terminal", "true");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止所有线程
|
||
/// </summary>
|
||
public virtual void StopAll()
|
||
{
|
||
StopLoop();
|
||
StopLogicLoop();
|
||
status.status = "已停止";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放资源
|
||
/// <para>清理触发器适配器、停止所有线程、取消策略订阅</para>
|
||
/// </summary>
|
||
public void Dispose()
|
||
{
|
||
// 清理触发器适配器
|
||
foreach (var adapter in _adapters.ToArray())
|
||
{
|
||
try
|
||
{
|
||
adapter?.Dispose();
|
||
}
|
||
catch { }
|
||
}
|
||
_adapters.Clear();
|
||
|
||
// 停止所有线程
|
||
StopAll();
|
||
|
||
// 取消策略订阅
|
||
if (_taskStrategy is JsonFileTaskStrategy jsonStrategy)
|
||
{
|
||
jsonStrategy.Changed -= OnTaskStrategyChanged;
|
||
jsonStrategy.DisposeWatcher();
|
||
}
|
||
|
||
System.Diagnostics.Debug.WriteLine("[AbstractLoopMission] Dispose: 资源已释放");
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 辅助类型
|
||
|
||
/// <summary>
|
||
/// 任务列表变更事件参数
|
||
/// </summary>
|
||
public class TaskListChangedEventArgs : EventArgs
|
||
{
|
||
/// <summary>变更类型</summary>
|
||
public TaskChangeType ChangeType { get; }
|
||
|
||
/// <summary>变更的任务索引(Replaced 时为 -1)</summary>
|
||
public int Index { get; }
|
||
|
||
/// <summary>变更的任务(Replaced 时为 null)</summary>
|
||
public LoopTask Task { get; }
|
||
|
||
/// <summary>
|
||
/// 创建任务列表变更事件参数
|
||
/// </summary>
|
||
/// <param name="changeType">变更类型</param>
|
||
/// <param name="index">任务索引</param>
|
||
/// <param name="task">任务对象</param>
|
||
public TaskListChangedEventArgs(TaskChangeType changeType, int index, LoopTask task)
|
||
{
|
||
ChangeType = changeType;
|
||
Index = index;
|
||
Task = task;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 任务变更类型枚举
|
||
/// </summary>
|
||
public enum TaskChangeType
|
||
{
|
||
/// <summary>添加任务</summary>
|
||
Added,
|
||
/// <summary>更新任务</summary>
|
||
Updated,
|
||
/// <summary>移除任务</summary>
|
||
Removed,
|
||
/// <summary>替换全部任务</summary>
|
||
Replaced
|
||
}
|
||
|
||
/// <summary>
|
||
/// 循环点配置类
|
||
/// <para>用于定义循环点的规则和属性</para>
|
||
/// </summary>
|
||
public class LoopPoint
|
||
{
|
||
/// <summary>唯一标识</summary>
|
||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||
|
||
/// <summary>名称</summary>
|
||
public string Name { get; set; } = string.Empty;
|
||
|
||
/// <summary>自定义标签集合</summary>
|
||
public Dictionary<string, object> Tags { get; } = new Dictionary<string, object>();
|
||
|
||
/// <summary>进入规则</summary>
|
||
public IEnterRule EnterRule { get; set; }
|
||
|
||
/// <summary>退出规则</summary>
|
||
public IExitRule ExitRule { get; set; }
|
||
|
||
/// <summary>汇合规则</summary>
|
||
public IJoinRule JoinRule { get; set; }
|
||
|
||
/// <summary>分流规则</summary>
|
||
public IBranchRule BranchRule { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// JSON 文件任务策略类
|
||
/// <para>从 tasklist.json 读取任务配置,支持文件变更自动刷新</para>
|
||
/// </summary>
|
||
public class JsonFileTaskStrategy : ITaskStrategy, IDisposable
|
||
{
|
||
/// <summary>JSON 文件路径</summary>
|
||
public string JsonPath { get; set; }
|
||
|
||
/// <summary>任务缓存列表</summary>
|
||
private List<LoopTask> _cache = new List<LoopTask>();
|
||
|
||
/// <summary>文件系统监听器</summary>
|
||
private FileSystemWatcher _watcher;
|
||
|
||
/// <summary>配置变更事件</summary>
|
||
public event EventHandler Changed;
|
||
|
||
/// <summary>
|
||
/// 创建 JSON 文件任务策略
|
||
/// </summary>
|
||
/// <param name="jsonPath">JSON 文件路径(null 时使用默认路径 tasklist.json)</param>
|
||
public JsonFileTaskStrategy(string jsonPath = null)
|
||
{
|
||
JsonPath = string.IsNullOrWhiteSpace(jsonPath)
|
||
? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tasklist.json")
|
||
: jsonPath;
|
||
|
||
EnsureWatcher();
|
||
Refresh();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取任务列表
|
||
/// </summary>
|
||
/// <returns>任务列表副本</returns>
|
||
public IEnumerable<LoopTask> GetTasks()
|
||
{
|
||
return _cache.ToArray();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新任务列表(从文件重新读取)
|
||
/// </summary>
|
||
public void Refresh()
|
||
{
|
||
try
|
||
{
|
||
if (!File.Exists(JsonPath))
|
||
{
|
||
_cache = new List<LoopTask>();
|
||
return;
|
||
}
|
||
|
||
string json = File.ReadAllText(JsonPath);
|
||
_cache = JsonConvert.DeserializeObject<List<LoopTask>>(json) ?? new List<LoopTask>();
|
||
}
|
||
catch
|
||
{
|
||
_cache = new List<LoopTask>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保文件监听器已启动
|
||
/// </summary>
|
||
public void EnsureWatcher()
|
||
{
|
||
try
|
||
{
|
||
if (_watcher != null)
|
||
return;
|
||
|
||
string directory = Path.GetDirectoryName(JsonPath);
|
||
string fileName = Path.GetFileName(JsonPath);
|
||
|
||
if (string.IsNullOrEmpty(directory))
|
||
return;
|
||
|
||
_watcher = new FileSystemWatcher(directory, fileName)
|
||
{
|
||
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.FileName
|
||
};
|
||
|
||
_watcher.Changed += (s, e) => OnFileChanged();
|
||
_watcher.Renamed += (s, e) => OnFileChanged();
|
||
_watcher.EnableRaisingEvents = true;
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
private void OnFileChanged()
|
||
{
|
||
try
|
||
{
|
||
Thread.Sleep(50);
|
||
Refresh();
|
||
Changed?.Invoke(this, EventArgs.Empty);
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
public void DisposeWatcher()
|
||
{
|
||
try
|
||
{
|
||
if (_watcher != null)
|
||
{
|
||
_watcher.EnableRaisingEvents = false;
|
||
_watcher.Dispose();
|
||
_watcher = null;
|
||
}
|
||
}
|
||
catch { }
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
DisposeWatcher();
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 路径查找辅助方法
|
||
|
||
/// <summary>
|
||
/// 查找从起点到终点之间的所有站点(基于轨道连接的广度优先搜索)
|
||
/// </summary>
|
||
/// <param name="startSiteId">起点站点ID</param>
|
||
/// <param name="endSiteId">终点站点ID</param>
|
||
/// <returns>从起点到终点的站点ID列表(包含起点和终点),未找到路径返回空列表</returns>
|
||
/// <example>
|
||
/// <code>
|
||
/// // 查找站点100到站点200之间的所有站点
|
||
/// var sites = GetSitesBetween(100, 200);
|
||
/// // 返回: [100, 150, 180, 200]
|
||
/// </code>
|
||
/// </example>
|
||
protected List<int> GetSitesBetween(int startSiteId, int endSiteId)
|
||
{
|
||
if (startSiteId == endSiteId)
|
||
return new List<int> { startSiteId };
|
||
|
||
try
|
||
{
|
||
// 构建站点邻接表
|
||
var adjacency = BuildSiteAdjacency();
|
||
|
||
// 广度优先搜索
|
||
var visited = new HashSet<int>();
|
||
var queue = new Queue<List<int>>();
|
||
queue.Enqueue(new List<int> { startSiteId });
|
||
visited.Add(startSiteId);
|
||
|
||
while (queue.Count > 0)
|
||
{
|
||
var currentPath = queue.Dequeue();
|
||
int currentSite = currentPath[currentPath.Count - 1];
|
||
|
||
// 找到终点,返回路径
|
||
if (currentSite == endSiteId)
|
||
return currentPath;
|
||
|
||
// 获取相邻站点
|
||
if (!adjacency.TryGetValue(currentSite, out var neighbors))
|
||
continue;
|
||
|
||
foreach (var neighbor in neighbors)
|
||
{
|
||
if (visited.Contains(neighbor))
|
||
continue;
|
||
|
||
visited.Add(neighbor);
|
||
var newPath = new List<int>(currentPath) { neighbor };
|
||
queue.Enqueue(newPath);
|
||
}
|
||
}
|
||
|
||
// 未找到路径
|
||
return new List<int>();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
System.Diagnostics.Debug.WriteLine($"[AbstractLoopMission] GetSitesBetween({startSiteId}, {endSiteId}) error: {ex.Message}");
|
||
return new List<int>();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查找从起点到终点之间的所有站点对象
|
||
/// </summary>
|
||
/// <param name="startSiteId">起点站点ID</param>
|
||
/// <param name="endSiteId">终点站点ID</param>
|
||
/// <returns>从起点到终点的站点对象列表</returns>
|
||
protected List<Site> GetSiteObjectsBetween(int startSiteId, int endSiteId)
|
||
{
|
||
var siteIds = GetSitesBetween(startSiteId, endSiteId);
|
||
var result = new List<Site>();
|
||
|
||
foreach (var siteId in siteIds)
|
||
{
|
||
var site = SimpleLib.GetSite(siteId);
|
||
if (site != null)
|
||
result.Add(site);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建站点邻接表(基于轨道连接,考虑轨道方向)
|
||
/// </summary>
|
||
/// <returns>邻接表:站点ID -> 可达的相邻站点ID列表</returns>
|
||
private Dictionary<int, HashSet<int>> BuildSiteAdjacency()
|
||
{
|
||
var adjacency = new Dictionary<int, HashSet<int>>();
|
||
|
||
foreach (var track in SimpleLib.GetAllTracks())
|
||
{
|
||
int siteA = track.siteA;
|
||
int siteB = track.siteB;
|
||
|
||
// 根据轨道方向添加邻接关系
|
||
// direction: 0=双向, 1=A->B, 2=B->A
|
||
switch (track.direction)
|
||
{
|
||
case 0: // 双向
|
||
AddAdjacency(adjacency, siteA, siteB);
|
||
AddAdjacency(adjacency, siteB, siteA);
|
||
break;
|
||
case 1: // A -> B
|
||
AddAdjacency(adjacency, siteA, siteB);
|
||
break;
|
||
case 2: // B -> A
|
||
AddAdjacency(adjacency, siteB, siteA);
|
||
break;
|
||
default:
|
||
AddAdjacency(adjacency, siteA, siteB);
|
||
AddAdjacency(adjacency, siteB, siteA);
|
||
break;
|
||
}
|
||
}
|
||
|
||
return adjacency;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 向邻接表添加边
|
||
/// </summary>
|
||
private void AddAdjacency(Dictionary<int, HashSet<int>> adjacency, int from, int to)
|
||
{
|
||
if (!adjacency.ContainsKey(from))
|
||
adjacency[from] = new HashSet<int>();
|
||
|
||
adjacency[from].Add(to);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查路径上是否有其他车辆(不包含起点和终点)
|
||
/// </summary>
|
||
/// <param name="startSiteId">起点站点ID</param>
|
||
/// <param name="endSiteId">终点站点ID</param>
|
||
/// <param name="excludeCar">排除的车辆(通常是当前车辆)</param>
|
||
/// <returns>true=路径上有车辆,false=路径畅通</returns>
|
||
protected bool HasCarOnPath(int startSiteId, int endSiteId, Car excludeCar = null)
|
||
{
|
||
var pathSites = GetSitesBetween(startSiteId, endSiteId);
|
||
|
||
// 排除起点和终点
|
||
var middleSites = pathSites
|
||
.Skip(1)
|
||
.Take(pathSites.Count - 2)
|
||
.ToList();
|
||
|
||
if (middleSites.Count == 0)
|
||
return false;
|
||
|
||
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
|
||
{
|
||
if (excludeCar != null && car == excludeCar)
|
||
continue;
|
||
|
||
if (car?.status?.holdingLocks == null)
|
||
continue;
|
||
|
||
// 检查车辆是否在路径中间站点
|
||
foreach (var siteId in middleSites)
|
||
{
|
||
if (car.status.holdingLocks.Contains(siteId))
|
||
return true;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取路径上的所有车辆
|
||
/// </summary>
|
||
/// <param name="startSiteId">起点站点ID</param>
|
||
/// <param name="endSiteId">终点站点ID</param>
|
||
/// <param name="includeEndpoints">是否包含起点和终点的车辆</param>
|
||
/// <returns>路径上的车辆列表</returns>
|
||
protected List<Car> GetCarsOnPath(int startSiteId, int endSiteId, bool includeEndpoints = false)
|
||
{
|
||
var pathSites = GetSitesBetween(startSiteId, endSiteId);
|
||
|
||
if (!includeEndpoints && pathSites.Count > 2)
|
||
{
|
||
pathSites = pathSites
|
||
.Skip(1)
|
||
.Take(pathSites.Count - 2)
|
||
.ToList();
|
||
}
|
||
|
||
var carsOnPath = new List<Car>();
|
||
|
||
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
|
||
{
|
||
if (car?.status?.holdingLocks == null)
|
||
continue;
|
||
|
||
foreach (var siteId in pathSites)
|
||
{
|
||
if (car.status.holdingLocks.Contains(siteId))
|
||
{
|
||
carsOnPath.Add(car);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
return carsOnPath;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 计算两站点之间的路径长度(经过的站点数)
|
||
/// </summary>
|
||
/// <param name="startSiteId">起点站点ID</param>
|
||
/// <param name="endSiteId">终点站点ID</param>
|
||
/// <returns>路径长度(站点数),未找到路径返回 -1</returns>
|
||
protected int GetPathLength(int startSiteId, int endSiteId)
|
||
{
|
||
var path = GetSitesBetween(startSiteId, endSiteId);
|
||
return path.Count > 0 ? path.Count : -1;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 任务策略匹配
|
||
|
||
/// <summary>
|
||
/// 路径缓存(避免重复计算)
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
private readonly Dictionary<(int, int), List<int>> _pathCache = new Dictionary<(int, int), List<int>>();
|
||
|
||
/// <summary>
|
||
/// 路径缓存锁
|
||
/// </summary>
|
||
[JsonIgnore]
|
||
private readonly object _pathCacheLock = new object();
|
||
|
||
/// <summary>
|
||
/// 根据车辆当前站点匹配最优任务策略
|
||
/// <para>匹配逻辑:</para>
|
||
/// <list type="number">
|
||
/// <item>遍历所有任务策略,获取每个策略从当前点到目标点的完整路径</item>
|
||
/// <item>检查车辆当前站点是否在该路径上</item>
|
||
/// <item>返回包含当前站点且优先级最高的策略</item>
|
||
/// </list>
|
||
/// </summary>
|
||
/// <param name="car">车辆</param>
|
||
/// <returns>最优匹配结果,无匹配返回 null</returns>
|
||
protected TaskPathMatchResult FindBestTaskForCar(Car car)
|
||
{
|
||
if (car?.status?.holdingLocks == null || car.status.holdingLocks.Length == 0)
|
||
return null;
|
||
|
||
int currentSiteId = car.status.holdingLocks[0];
|
||
return FindBestTaskForSite(currentSiteId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据站点ID匹配最优任务策略
|
||
/// <para>遍历所有任务,找出包含该站点的路径,返回优先级最高的任务</para>
|
||
/// </summary>
|
||
/// <param name="siteId">当前站点ID</param>
|
||
/// <returns>最优匹配结果,无匹配返回 null</returns>
|
||
protected TaskPathMatchResult FindBestTaskForSite(int siteId)
|
||
{
|
||
List<LoopTask> snapshot;
|
||
lock (_tasksLock)
|
||
{
|
||
snapshot = Tasks.Where(T=>T.IsViaPoint).ToList();
|
||
}
|
||
|
||
var matchResults = new List<TaskPathMatchResult>();
|
||
|
||
foreach (var task in snapshot)
|
||
{
|
||
// 获取任务的完整路径(从当前点到目标点)
|
||
var fullPath = GetSitesBetweenCached(task.CurrentStationId, task.TargetStationId);
|
||
|
||
if (fullPath.Count == 0)
|
||
continue;
|
||
|
||
// 检查站点是否在路径上
|
||
int indexInPath = fullPath.IndexOf(siteId);
|
||
if (indexInPath < 0)
|
||
continue;
|
||
|
||
|
||
// 创建匹配结果
|
||
var result = new TaskPathMatchResult
|
||
{
|
||
Task = task,
|
||
CurrentSiteId = siteId,
|
||
FullPath = fullPath,
|
||
IndexInPath = indexInPath,
|
||
RemainingPath = fullPath.Skip(indexInPath).ToList(),
|
||
DistanceToTarget = fullPath.Count - indexInPath - 1,
|
||
IsAtStartPoint = (indexInPath == 0),
|
||
IsAtEndPoint = (indexInPath == fullPath.Count - 1),
|
||
IsOnMiddlePath = (indexInPath > 0 && indexInPath < fullPath.Count - 1)
|
||
};
|
||
|
||
matchResults.Add(result);
|
||
}
|
||
|
||
if (matchResults.Count == 0)
|
||
return null;
|
||
|
||
// 按匹配优先级排序:
|
||
// 1. 起点匹配优先
|
||
// 2. 任务优先级高的优先
|
||
// 3. 距离目标近的优先
|
||
var bestMatch = matchResults.Where(r=>!r.IsAtEndPoint && !r.IsAtStartPoint )
|
||
.OrderByDescending(r => r.IsAtStartPoint ? 1 : 0) // 起点匹配优先
|
||
.ThenByDescending(r => r.Task.Priority) // 任务优先级高的优先
|
||
.ThenBy(r => r.DistanceToTarget) // 距离目标近的优先
|
||
.FirstOrDefault();
|
||
Diagnosis.Post($"[AbstractLoopMission] 小车获取到的任务策略 {bestMatch.ToString()} )", $"路径中间点初始化", true);
|
||
return bestMatch;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据站点ID查找所有匹配的任务策略
|
||
/// <para>返回所有包含该站点的任务,按优先级排序</para>
|
||
/// </summary>
|
||
/// <param name="siteId">站点ID</param>
|
||
/// <returns>匹配结果列表(按优先级降序)</returns>
|
||
protected List<TaskPathMatchResult> FindAllTasksForSite(int siteId)
|
||
{
|
||
List<LoopTask> snapshot;
|
||
lock (_tasksLock)
|
||
{
|
||
snapshot = Tasks.ToList();
|
||
}
|
||
|
||
var matchResults = new List<TaskPathMatchResult>();
|
||
|
||
foreach (var task in snapshot)
|
||
{
|
||
// 获取任务的完整路径
|
||
var fullPath = GetSitesBetweenCached(task.CurrentStationId, task.TargetStationId);
|
||
|
||
if (fullPath.Count == 0)
|
||
continue;
|
||
|
||
// 检查站点是否在路径上
|
||
int indexInPath = fullPath.IndexOf(siteId);
|
||
if (indexInPath < 0)
|
||
continue;
|
||
|
||
var result = new TaskPathMatchResult
|
||
{
|
||
Task = task,
|
||
CurrentSiteId = siteId,
|
||
FullPath = fullPath,
|
||
IndexInPath = indexInPath,
|
||
RemainingPath = fullPath.Skip(indexInPath).ToList(),
|
||
DistanceToTarget = fullPath.Count - indexInPath - 1,
|
||
IsAtStartPoint = (indexInPath == 0),
|
||
IsAtEndPoint = (indexInPath == fullPath.Count - 1),
|
||
IsOnMiddlePath = (indexInPath > 0 && indexInPath < fullPath.Count - 1)
|
||
};
|
||
|
||
matchResults.Add(result);
|
||
}
|
||
|
||
// 按优先级排序
|
||
return matchResults
|
||
.OrderByDescending(r => r.IsAtStartPoint ? 1 : 0)
|
||
.ThenByDescending(r => r.Task.Priority)
|
||
.ThenBy(r => r.DistanceToTarget)
|
||
.ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取车辆应该前往的下一个目标站点
|
||
/// <para>根据最优匹配策略返回目标站点ID</para>
|
||
/// </summary>
|
||
/// <param name="car">车辆</param>
|
||
/// <returns>目标站点ID,无匹配返回 -1</returns>
|
||
protected int GetRecommendedTargetForCar(Car car)
|
||
{
|
||
var match = FindBestTaskForCar(car);
|
||
return match?.Task?.TargetStationId ?? -1;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取车辆的下一个中间站点
|
||
/// <para>返回当前位置的下一个路径节点</para>
|
||
/// </summary>
|
||
/// <param name="car">车辆</param>
|
||
/// <returns>下一站点ID,无匹配返回 -1</returns>
|
||
protected int GetNextSiteForCar(Car car)
|
||
{
|
||
var match = FindBestTaskForCar(car);
|
||
if (match?.RemainingPath == null || match.RemainingPath.Count < 2)
|
||
return -1;
|
||
|
||
return match.RemainingPath[1];
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取缓存的路径(避免重复计算)
|
||
/// </summary>
|
||
private List<int> GetSitesBetweenCached(int startSiteId, int endSiteId)
|
||
{
|
||
var key = (startSiteId, endSiteId);
|
||
|
||
lock (_pathCacheLock)
|
||
{
|
||
if (_pathCache.TryGetValue(key, out var cached))
|
||
return new List<int>(cached);
|
||
|
||
var path = GetSitesBetween(startSiteId, endSiteId);
|
||
_pathCache[key] = path;
|
||
return new List<int>(path);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除路径缓存
|
||
/// <para>当任务策略变更或地图变更时调用</para>
|
||
/// </summary>
|
||
protected void ClearPathCache()
|
||
{
|
||
lock (_pathCacheLock)
|
||
{
|
||
_pathCache.Clear();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 根据任务ID查找任务
|
||
/// </summary>
|
||
/// <param name="taskId">任务ID</param>
|
||
/// <returns>任务对象,未找到返回 null</returns>
|
||
protected LoopTask FindTaskById(int taskId)
|
||
{
|
||
lock (_tasksLock)
|
||
{
|
||
return Tasks.FirstOrDefault(t => t.Id == taskId);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 判断车辆是否在指定任务的路径上
|
||
/// </summary>
|
||
/// <param name="car">车辆</param>
|
||
/// <param name="task">任务</param>
|
||
/// <returns>是否在路径上</returns>
|
||
protected bool IsCarOnTaskPath(Car car, LoopTask task)
|
||
{
|
||
if (car?.status?.holdingLocks == null || car.status.holdingLocks.Length == 0)
|
||
return false;
|
||
|
||
int currentSiteId = car.status.holdingLocks[0];
|
||
var path = GetSitesBetweenCached(task.CurrentStationId, task.TargetStationId);
|
||
|
||
return path.Contains(currentSiteId);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 任务匹配结果类型
|
||
|
||
/// <summary>
|
||
/// 任务路径匹配结果
|
||
/// </summary>
|
||
public class TaskPathMatchResult
|
||
{
|
||
/// <summary>匹配的任务</summary>
|
||
public LoopTask Task { get; set; }
|
||
|
||
/// <summary>车辆当前站点ID</summary>
|
||
public int CurrentSiteId { get; set; }
|
||
|
||
/// <summary>完整路径(从任务起点到终点)</summary>
|
||
public List<int> FullPath { get; set; } = new List<int>();
|
||
|
||
/// <summary>当前站点在路径中的索引</summary>
|
||
public int IndexInPath { get; set; }
|
||
|
||
/// <summary>剩余路径(从当前站点到终点)</summary>
|
||
public List<int> RemainingPath { get; set; } = new List<int>();
|
||
|
||
/// <summary>到目标的距离(剩余站点数)</summary>
|
||
public int DistanceToTarget { get; set; }
|
||
|
||
/// <summary>是否在任务起点</summary>
|
||
public bool IsAtStartPoint { get; set; }
|
||
|
||
/// <summary>是否在任务终点</summary>
|
||
public bool IsAtEndPoint { get; set; }
|
||
|
||
/// <summary>是否在路径中间</summary>
|
||
public bool IsOnMiddlePath { get; set; }
|
||
|
||
/// <summary>任务ID</summary>
|
||
public int TaskId => Task?.Id ?? 0;
|
||
|
||
/// <summary>任务优先级</summary>
|
||
public int TaskPriority => Task?.Priority ?? 0;
|
||
|
||
/// <summary>目标站点ID</summary>
|
||
public int TargetSiteId => Task?.TargetStationId ?? -1;
|
||
|
||
/// <summary>下一个站点ID(当前站点的下一跳)</summary>
|
||
public int NextSiteId => RemainingPath.Count > 1 ? RemainingPath[1] : -1;
|
||
|
||
/// <summary>路径进度百分比</summary>
|
||
public double ProgressPercent => FullPath.Count > 1
|
||
? (double)IndexInPath / (FullPath.Count - 1) * 100
|
||
: 0;
|
||
|
||
/// <summary>
|
||
/// 获取匹配信息描述
|
||
/// </summary>
|
||
public override string ToString()
|
||
{
|
||
string position = IsAtStartPoint ? "起点" : (IsAtEndPoint ? "终点" : "中间");
|
||
return $"任务[ID={TaskId}, {Task?.CurrentStationId}->{Task?.TargetStationId}] " +
|
||
$"优先级={TaskPriority}, 当前={CurrentSiteId}({position}), " +
|
||
$"进度={ProgressPercent:F1}%, 剩余={DistanceToTarget}站, " +
|
||
$"下一站={NextSiteId}";
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
private LoopViewer lv;
|
||
|
||
|
||
[MethodMember(Name = "任务管理", Description = "任务管理")]
|
||
public void Print()
|
||
{
|
||
lv = new LoopViewer();
|
||
lv.Show();
|
||
}
|
||
|
||
[MethodMember(Name = "启动环线", Description = "启动环线")]
|
||
public void Start()
|
||
{
|
||
StartAll();
|
||
}
|
||
|
||
[MethodMember(Name = "停止环线", Description = "停止环线")]
|
||
public override void Stop()
|
||
{
|
||
StopAll();
|
||
}
|
||
|
||
[MethodMember(Name = "一键上线所有AGV", Description = "将所有已初始化的正常 AGV 标记为在线",
|
||
RequiresPlatformConfirm = true,
|
||
ConfirmMessage = "确认将所有已初始化的正常 AGV 一键上线?")]
|
||
public void OnlineAllCars()
|
||
{
|
||
// 确认由 MethodMember.ConfirmMessage + ObjectEditorParity/SafeUI 居中弹出,此处勿再套 FairyUiHelper。
|
||
Task.Run(OnlineAllCarsCore);
|
||
}
|
||
|
||
private void OnlineAllCarsCore()
|
||
{
|
||
int count = 0;
|
||
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
|
||
{
|
||
if (car.GetLastSite() == -1 && Commons.GetVehicleStatus(car) == VehicleStatus.Normal)
|
||
{
|
||
Commons.AddOrUpdateTag(car.tags, "Online", "true");
|
||
car.Reset();
|
||
count++;
|
||
}
|
||
}
|
||
|
||
status.status = $"已上线 {count} 台AGV";
|
||
Diagnosis.Post($"[AbstractLoopMission] 一键上线所有AGV:已上线 {count} 台", "Loop-OnlineAll", true);
|
||
}
|
||
|
||
[MethodMember(Name = "一键结束所有AGV任务", Description = "停止环线调度并清除所有 AGV 的环线任务分配",
|
||
RequiresPlatformConfirm = true,
|
||
ConfirmMessage = "确认停止环线调度并结束所有 AGV 的环线任务?")]
|
||
public void StopAllCarTasks()
|
||
{
|
||
// 确认由 MethodMember.ConfirmMessage + ObjectEditorParity/SafeUI 居中弹出,此处勿再套 FairyUiHelper。
|
||
Task.Run(StopAllCarTasksCore);
|
||
}
|
||
|
||
private void StopAllCarTasksCore()
|
||
{
|
||
|
||
int count = 0;
|
||
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
|
||
{
|
||
Commons.DeleteTag(car.tags, "goalSite");
|
||
Commons.DeleteTag(car.tags, "loopAssigned");
|
||
Commons.DeleteTag(car.tags, "occupied");
|
||
car.ForceStop();
|
||
count++;
|
||
}
|
||
|
||
status.status = $"已结束 {count} 台AGV任务";
|
||
Diagnosis.Post($"[AbstractLoopMission] 一键结束所有AGV任务:已停止环线并清理 {count} 台 AGV 的任务标签", "Loop-StopAllTasks", true);
|
||
}
|
||
|
||
}
|
||
}
|