避免通过基类引用调用时走到空实现,隐藏的 Stop/ForceStop 无法真正停进程。 Co-authored-by: Cursor <cursoragent@cursor.com>
1510 lines
66 KiB
C#
1510 lines
66 KiB
C#
using IoTClient.Common.Helpers;
|
||
using Newtonsoft.Json;
|
||
using Simple3;
|
||
using Simple3.RCS;
|
||
using Simple3.RCS.CarTypes;
|
||
using Simple3.CADTools;
|
||
using Simple3.Props;
|
||
using Simple3.UI;
|
||
using SimpleCore;
|
||
using SimpleCore.Compiler;
|
||
using SimpleCore.Library;
|
||
using SimpleCore.PropType;
|
||
using SimpleCore.Traffic;
|
||
using StandardScene.CarTypes;
|
||
using StandardScene.CommonTools;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.Drawing;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Numerics;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
|
||
namespace StandardScene.Chained
|
||
{
|
||
/// <summary>
|
||
/// 抽象链式搬运任务类
|
||
/// 实现AGV搬运任务的核心调度逻辑,支持任务链式执行、智能调度、路径规划、冲突避让等功能
|
||
/// 可以做取满放空操作
|
||
/// </summary>
|
||
public abstract class ChainedDeliveryMission:Mission
|
||
{
|
||
#region Parameters / Status
|
||
/// <summary>
|
||
/// 链式搬运任务参数配置类
|
||
/// </summary>
|
||
private class ChainedDeliveryParams
|
||
{
|
||
/// <summary>
|
||
/// 是否启用打断正在去避让点的小车功能
|
||
/// </summary>
|
||
public bool EnableBlockStandby { get; } = true;
|
||
|
||
/// <summary>
|
||
/// 是否启用紧密任务链模式
|
||
/// true: 链式搬运任务可以同时启动(前序任务开始时后续任务即可开始),提高效率但可能造成死锁
|
||
/// false: 前序任务必须完成后,后续任务才能开始,更安全但效率较低
|
||
/// </summary>
|
||
public bool TightDeliveryChain { get; } = true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 搬运任务状态统计类
|
||
/// 记录任务执行过程中的各种统计信息
|
||
/// </summary>
|
||
public class DeliveryMissionStatus : MissionStatus
|
||
{
|
||
/// <summary>排队等待执行的任务数量</summary>
|
||
public int Enqueued;
|
||
/// <summary>已完成的任务数量</summary>
|
||
public int Performed;
|
||
/// <summary>每小时作业数(Jobs Per Hour),用于衡量系统吞吐量</summary>
|
||
public double Jph;
|
||
/// <summary>淤积任务信息字符串,格式:任务ID:淤积次数</summary>
|
||
public string StuckInfo = "/";
|
||
/// <summary>当前淤积的任务数量(无法执行的任务)</summary>
|
||
public int StuckNum;
|
||
|
||
/// <summary>
|
||
/// 创建后,等待时间超过该阈值的任务,必须被优先安排执行。单位为秒。
|
||
/// </summary>
|
||
public double MustArrangeThreshold = 300;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 继承自基类的任务状态属性,用于记录搬运任务的执行状态并在界面显示。
|
||
/// </summary>
|
||
public override MissionStatus status { get; set; } = new DeliveryMissionStatus();
|
||
|
||
/// <summary>
|
||
/// 允许任务打断正在去避让点的小车,使其承接该任务。
|
||
/// </summary>
|
||
public bool EnableBlockStandby;
|
||
#endregion
|
||
|
||
#region Delivery model
|
||
|
||
/// <summary>
|
||
/// 搬运任务状态枚举
|
||
/// </summary>
|
||
public enum DeliveryStatus
|
||
{
|
||
/// <summary>等待执行</summary>
|
||
Waiting = 0,
|
||
/// <summary>取货中</summary>
|
||
Fetching = 1,
|
||
/// <summary>放货中</summary>
|
||
Putting = 2,
|
||
/// <summary>已完成</summary>
|
||
Finished = 3,
|
||
/// <summary>已取消</summary>
|
||
Canceled = 4,
|
||
/// <summary>已终止(执行过程中被中断)</summary>
|
||
Terminated = 5,
|
||
/// <summary>错误</summary>
|
||
Error = 6,
|
||
/// <summary>挂起</summary>
|
||
Suspended = 7,
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重构版 Delivery:字段与原 AbstractDelivery 保持接近,便于迁移;
|
||
/// 状态读写统一走 syncStatus 锁,避免竞态。
|
||
/// </summary>
|
||
public abstract class Delivery
|
||
{
|
||
/// <summary>雪花算法 ID 生成器(所有 Delivery 共享一份)</summary>
|
||
private static readonly SnowflakeIdGenerator IdGenerator =
|
||
new SnowflakeIdGenerator(workerId: 1, datacenterId: 1);
|
||
|
||
/// <summary>任务唯一标识ID(雪花算法,Base62编码)</summary>
|
||
public string Id { get; set; } = IdGenerator.NextIdBase62();
|
||
/// <summary>外部系统传入的任务ID</summary>
|
||
public string TaskId = string.Empty;
|
||
|
||
/// <summary>物料长度(毫米),用于包络约束</summary>
|
||
public float MaterialLength = -1;
|
||
/// <summary>物料宽度(毫米),用于包络约束</summary>
|
||
public float MaterialWidth = -1;
|
||
|
||
/// <summary>
|
||
/// 任务启动条件函数,返回true时任务才能开始执行
|
||
/// 可用于实现复杂的前置条件判断
|
||
/// </summary>
|
||
public Func<bool> StartCondition = null;
|
||
|
||
// ========== 任务回调函数 ==========
|
||
/// <summary>任务失败时的回调函数</summary>
|
||
[JsonIgnore] public Action<Delivery> Failed;
|
||
/// <summary>放货完成时的回调函数</summary>
|
||
[JsonIgnore] public Action<Delivery> DonePut;
|
||
/// <summary>取货完成时的回调函数</summary>
|
||
[JsonIgnore] public Action<Delivery> DoneFetch;
|
||
/// <summary>整个任务完成时的回调函数</summary>
|
||
[JsonIgnore] public Action<Delivery> DoneMission;
|
||
/// <summary>任务开始执行时的回调函数</summary>
|
||
[JsonIgnore] public Action<Delivery> OnStart;
|
||
/// <summary>任务终止时的回调函数</summary>
|
||
[JsonIgnore] public Func<Delivery, string, Task<int>> OnTerminated;
|
||
|
||
// ========== 任务回调配置(可持久化) ==========
|
||
/// <summary>是否在任务开始时上报回调</summary>
|
||
public bool ReportOnStarted;
|
||
/// <summary>是否在取货完成时上报回调</summary>
|
||
public bool ReportOnFetched;
|
||
/// <summary>是否在放货完成时上报回调</summary>
|
||
public bool ReportOnPut;
|
||
/// <summary>是否在任务最终完成时上报回调</summary>
|
||
public bool ReportOnFinished;
|
||
/// <summary>是否在任务失败时上报回调</summary>
|
||
public bool ReportOnFailed;
|
||
/// <summary>是否在任务被终止/暂停时上报回调</summary>
|
||
public bool ReportOnTerminated;
|
||
|
||
/// <summary>OnStart 事件绑定的回调 key 列表</summary>
|
||
public List<string> OnStartCallbackKeys { get; set; } = new();
|
||
/// <summary>DoneFetch 事件绑定的回调 key 列表</summary>
|
||
public List<string> DoneFetchCallbackKeys { get; set; } = new();
|
||
/// <summary>DonePut 事件绑定的回调 key 列表</summary>
|
||
public List<string> DonePutCallbackKeys { get; set; } = new();
|
||
/// <summary>DoneMission 事件绑定的回调 key 列表</summary>
|
||
public List<string> DoneMissionCallbackKeys { get; set; } = new();
|
||
/// <summary>Failed 事件绑定的回调 key 列表</summary>
|
||
public List<string> FailedCallbackKeys { get; set; } = new();
|
||
/// <summary>OnTerminated 事件绑定的回调 key 列表</summary>
|
||
public List<string> OnTerminatedCallbackKeys { get; set; } = new();
|
||
|
||
// ========== 任务路径信息 ==========
|
||
/// <summary>取货点站点ID</summary>
|
||
public int Src;
|
||
/// <summary>放货点站点ID</summary>
|
||
public int Dst;
|
||
/// <summary>是否跳过取货步骤(如果小车已在取货点或已载货,可设置为true)</summary>
|
||
public bool SkipFetch;
|
||
/// <summary>是否跳过放货步骤(如果只需要移动到目标点但不放货)</summary>
|
||
public bool SkipPut;
|
||
/// <summary>放货点是否允许查找回路路径</summary>
|
||
public bool DstFindLoop;
|
||
/// <summary>取货点是否允许查找回路路径</summary>
|
||
public bool SrcFindLoop;
|
||
|
||
/// <summary>取货路径规划参数字典(传递给路径规划器)</summary>
|
||
[JsonIgnore] public Dictionary<string, string> FetchPlanInfo = new() { { "action", "fetch" } };
|
||
/// <summary>放货路径规划参数字典(传递给路径规划器)</summary>
|
||
[JsonIgnore] public Dictionary<string, string> PutPlanInfo = new() { { "action", "put" } };
|
||
|
||
/// <summary>前序任务,当前任务需要等待前序任务执行完毕才能开始(任务链依赖关系)</summary>
|
||
[JsonIgnore] public Delivery Former;
|
||
/// <summary>执行当前任务的小车对象</summary>
|
||
[JsonIgnore] public Car UsingCar;
|
||
/// <summary>小车分组筛选条件(用于筛选特定组的小车)</summary>
|
||
[JsonIgnore] public string Group = string.Empty;
|
||
/// <summary>小车筛选键列表(用于多条件筛选)</summary>
|
||
public List<string> SelectorKeys = new();
|
||
/// <summary>允许执行任务的小车类型(如"Forklift"、"Kiva"等)</summary>
|
||
[JsonIgnore] public string CarType = string.Empty;
|
||
/// <summary>
|
||
/// 任务优先级,数值越大,优先级越高
|
||
/// 任务调度时按优先级降序执行,同优先级按创建时间排序
|
||
/// </summary>
|
||
public int Priority = 0;
|
||
|
||
// ========== 时间记录 ==========
|
||
/// <summary>任务创建时间</summary>
|
||
public DateTime CreateTime = DateTime.Now;
|
||
/// <summary>任务开始执行时间</summary>
|
||
public DateTime StartTime;
|
||
/// <summary>任务完成时间</summary>
|
||
public DateTime FinishTime;
|
||
|
||
/// <summary>任务状态同步锁对象,用于多线程环境下保证状态修改的线程安全</summary>
|
||
[JsonIgnore] public readonly object SyncStatus = new object();
|
||
|
||
// ========== 任务内部状态标志 ==========
|
||
/// <summary>任务是否正在执行中</summary>
|
||
internal bool Active;
|
||
/// <summary>任务是否处于放货阶段(true=放货中,false=取货中)</summary>
|
||
internal bool Putting;
|
||
/// <summary>任务是否已完成</summary>
|
||
internal bool Finished;
|
||
/// <summary>任务是否出错</summary>
|
||
internal bool Error;
|
||
/// <summary>任务是否已取消</summary>
|
||
internal bool Canceled;
|
||
/// <summary>任务是否已终止(执行过程中被中断)</summary>
|
||
internal bool Terminated;
|
||
/// <summary>任务被挂起</summary>
|
||
internal bool Suspended;
|
||
|
||
/// <summary>追加的逃逸动作(任务完成后需要执行的额外动作,如去避让点)</summary>
|
||
[JsonIgnore] public Action AppendedEscape;
|
||
|
||
/// <summary>任务淤积次数(连续执行失败的次数,用于监控任务阻塞情况)</summary>
|
||
public int StuckTime;
|
||
/// <summary>任务淤积原因说明(记录为什么无法执行)</summary>
|
||
public string StuckReason = "/";
|
||
|
||
/// <summary>
|
||
/// 获取任务当前状态
|
||
/// 状态优先级:Finished > Canceled > Terminated > Error > Fetching/Putting > Waiting
|
||
/// </summary>
|
||
/// <returns>任务当前状态枚举值</returns>
|
||
public DeliveryStatus GetStatus()
|
||
{
|
||
lock (SyncStatus)
|
||
{
|
||
if (Finished) return DeliveryStatus.Finished;
|
||
if (Canceled) return DeliveryStatus.Canceled;
|
||
if (Terminated) return DeliveryStatus.Terminated;
|
||
if (Suspended) return DeliveryStatus.Suspended;
|
||
if (Error) return DeliveryStatus.Error;
|
||
if (Active) return Putting ? DeliveryStatus.Putting : DeliveryStatus.Fetching;
|
||
return DeliveryStatus.Waiting;
|
||
}
|
||
}
|
||
|
||
public bool IsActive() { lock (SyncStatus) return Active; }
|
||
public bool IsFinished() { lock (SyncStatus) return Finished; }
|
||
public bool IsError() { lock (SyncStatus) return Error; }
|
||
public bool IsCanceled() { lock (SyncStatus) return Canceled; }
|
||
public bool IsTerminated() { lock (SyncStatus) return Terminated; }
|
||
|
||
/// <summary>
|
||
/// 取消任务,当前实现仅占位返回 true,具体取消逻辑可由子类重写
|
||
/// </summary>
|
||
public bool Cancel()
|
||
{
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 用于持久化的任务状态快照
|
||
/// 只包含可序列化、与业务恢复相关的关键信息
|
||
/// </summary>
|
||
protected class DeliveryStateSnapshot
|
||
{
|
||
/// <summary>任务唯一标识ID</summary>
|
||
public string Id { get; set; }
|
||
/// <summary>外部系统传入的任务ID</summary>
|
||
public string TaskId { get; set; }
|
||
/// <summary>取货点站点ID</summary>
|
||
public int Src { get; set; }
|
||
/// <summary>放货点站点ID</summary>
|
||
public int Dst { get; set; }
|
||
/// <summary>是否跳过取货步骤</summary>
|
||
public bool SkipFetch { get; set; }
|
||
/// <summary>是否跳过放货步骤</summary>
|
||
public bool SkipPut { get; set; }
|
||
/// <summary>当前是否处于放货阶段(true=放货中)</summary>
|
||
public bool Putting { get; set; }
|
||
/// <summary>任务状态字符串(Waiting/Fetching/Putting/Finished等)</summary>
|
||
public string Status { get; set; }
|
||
/// <summary>任务创建时间</summary>
|
||
public DateTime CreateTime { get; set; }
|
||
/// <summary>任务开始执行时间</summary>
|
||
public DateTime StartTime { get; set; }
|
||
/// <summary>任务完成时间</summary>
|
||
public DateTime FinishTime { get; set; }
|
||
/// <summary>执行任务的小车ID(-1表示未分配)</summary>
|
||
public int UsingCarId { get; set; }
|
||
/// <summary>允许执行任务的小车类型</summary>
|
||
public string CarType { get; set; }
|
||
/// <summary>是否在任务开始时上报回调</summary>
|
||
public bool ReportOnStarted { get; set; }
|
||
/// <summary>是否在取货完成时上报回调</summary>
|
||
public bool ReportOnFetched { get; set; }
|
||
/// <summary>是否在放货完成时上报回调</summary>
|
||
public bool ReportOnPut { get; set; }
|
||
/// <summary>是否在任务最终完成时上报回调</summary>
|
||
public bool ReportOnFinished { get; set; }
|
||
/// <summary>是否在任务失败时上报回调</summary>
|
||
public bool ReportOnFailed { get; set; }
|
||
/// <summary>是否在任务被终止/暂停时上报回调</summary>
|
||
public bool ReportOnTerminated { get; set; }
|
||
/// <summary>OnStart 事件绑定的回调 key 列表</summary>
|
||
public List<string> OnStartCallbackKeys { get; set; } = new();
|
||
/// <summary>DoneFetch 事件绑定的回调 key 列表</summary>
|
||
public List<string> DoneFetchCallbackKeys { get; set; } = new();
|
||
/// <summary>DonePut 事件绑定的回调 key 列表</summary>
|
||
public List<string> DonePutCallbackKeys { get; set; } = new();
|
||
/// <summary>DoneMission 事件绑定的回调 key 列表</summary>
|
||
public List<string> DoneMissionCallbackKeys { get; set; } = new();
|
||
/// <summary>Failed 事件绑定的回调 key 列表</summary>
|
||
public List<string> FailedCallbackKeys { get; set; } = new();
|
||
/// <summary>OnTerminated 事件绑定的回调 key 列表</summary>
|
||
public List<string> OnTerminatedCallbackKeys { get; set; } = new();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Extensibility hooks
|
||
|
||
/// <summary>
|
||
/// 小车离开充电站时的扩展动作钩子,子类可重写以执行自定义逻辑(如通知、统计等)
|
||
/// </summary>
|
||
public virtual void LeaveChargeAction(Car car, Site site) { }
|
||
/// <summary>
|
||
/// 动态调整任务优先级的钩子,在调度循环中每次迭代前调用,子类可重写实现自定义优先级策略
|
||
/// </summary>
|
||
public virtual void ChangePriority() { }
|
||
/// <summary>
|
||
/// 定义避让点类型字符串,用于路径规划时识别可避让的站点,子类可重写以区分不同避让类型
|
||
/// </summary>
|
||
public virtual string DefineGiveWayType(AbstractCar car) => "giveWay";
|
||
|
||
#endregion
|
||
|
||
#region Queue / persistence (thread-safe)
|
||
|
||
/// <summary>队列与持久化操作的同步锁,保证多线程访问 _allDeliveries 时的线程安全</summary>
|
||
[JsonIgnore] private readonly object _sync = new();
|
||
/// <summary>所有搬运任务的集合(含等待中与执行中的任务)</summary>
|
||
[JsonIgnore] private readonly HashSet<Delivery> _allDeliveries = new();
|
||
|
||
/// <summary>
|
||
/// 将任务加入队列并持久化到文件,线程安全
|
||
/// </summary>
|
||
/// <param name="d">待入队的搬运任务</param>
|
||
public void Enqueue(Delivery d)
|
||
{
|
||
lock (_sync) _allDeliveries.Add(d);
|
||
PersistSingleDelivery(d);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前任务列表,支持按状态过滤(已完成/已终止/错误/已取消),线程安全
|
||
/// </summary>
|
||
/// <param name="includeFinished">是否包含已完成的任务</param>
|
||
/// <param name="includeAborted">是否包含已终止的任务</param>
|
||
/// <param name="includeError">是否包含错误状态的任务</param>
|
||
/// <param name="includeCanceled">是否包含已取消的任务</param>
|
||
/// <returns>去重后按状态排序的任务列表</returns>
|
||
public List<Delivery> GetDeliveries(bool includeFinished = false, bool includeAborted = false,bool includeError = false,bool includeCanceled=false)
|
||
{
|
||
List<Delivery> ds;
|
||
lock (_sync) ds = _allDeliveries.ToList();
|
||
|
||
if (!includeFinished)
|
||
ds = ds.Where(p => p.GetStatus() != DeliveryStatus.Finished).ToList();
|
||
if (!includeAborted)
|
||
ds = ds.Where(p => p.GetStatus() != DeliveryStatus.Terminated).ToList();
|
||
if (!includeError)
|
||
ds = ds.Where(p => p.GetStatus() != DeliveryStatus.Error).ToList();
|
||
if (!includeCanceled)
|
||
ds = ds.Where(p => p.GetStatus() != DeliveryStatus.Canceled).ToList();
|
||
|
||
return System.Linq.Enumerable.DistinctBy(ds, e => e.Id).OrderBy(e => e.GetStatus()).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取持久化文件路径,默认存储在 BaseDirectory/Deliveries/ 下,子类可重写自定义路径
|
||
/// </summary>
|
||
protected virtual string GetDeliveryPersistFilePath()
|
||
{
|
||
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||
var persistDir = Path.Combine(baseDir, "Deliveries");
|
||
Directory.CreateDirectory(persistDir);
|
||
|
||
var fileName = $"{GetType().Name}_Deliveries.json";
|
||
return Path.Combine(persistDir, fileName);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从单个 Delivery 生成持久化快照(仅当状态为未结束时返回非 null)。
|
||
/// 在回调中用于单任务持久化,不修改其他任务记录。
|
||
/// </summary>
|
||
private static DeliveryStateSnapshot SnapshotFromDelivery(Delivery d)
|
||
{
|
||
lock (d.SyncStatus)
|
||
{
|
||
var curStatus = d.GetStatus();
|
||
if (curStatus is DeliveryStatus.Finished or DeliveryStatus.Canceled or DeliveryStatus.Error or DeliveryStatus.Terminated)
|
||
return null;
|
||
return new DeliveryStateSnapshot
|
||
{
|
||
Id = d.Id,
|
||
TaskId = d.TaskId,
|
||
Src = d.Src,
|
||
Dst = d.Dst,
|
||
SkipFetch = d.SkipFetch,
|
||
SkipPut = d.SkipPut,
|
||
Putting = d.Putting,
|
||
Status = curStatus.ToString(),
|
||
CreateTime = d.CreateTime,
|
||
StartTime = d.StartTime,
|
||
FinishTime = d.FinishTime,
|
||
UsingCarId = d.UsingCar?.id ?? -1,
|
||
CarType = d.CarType ?? string.Empty,
|
||
ReportOnStarted = d.ReportOnStarted,
|
||
ReportOnFetched = d.ReportOnFetched,
|
||
ReportOnPut = d.ReportOnPut,
|
||
ReportOnFinished = d.ReportOnFinished,
|
||
ReportOnFailed = d.ReportOnFailed,
|
||
ReportOnTerminated = d.ReportOnTerminated,
|
||
OnStartCallbackKeys = d.OnStartCallbackKeys?.ToList() ?? new List<string>(),
|
||
DoneFetchCallbackKeys = d.DoneFetchCallbackKeys?.ToList() ?? new List<string>(),
|
||
DonePutCallbackKeys = d.DonePutCallbackKeys?.ToList() ?? new List<string>(),
|
||
DoneMissionCallbackKeys = d.DoneMissionCallbackKeys?.ToList() ?? new List<string>(),
|
||
FailedCallbackKeys = d.FailedCallbackKeys?.ToList() ?? new List<string>(),
|
||
OnTerminatedCallbackKeys = d.OnTerminatedCallbackKeys?.ToList() ?? new List<string>()
|
||
};
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 仅持久化当前任务,不更改其他任务记录;高并发下由 CommonTools 原子更新保证数据安全。
|
||
/// </summary>
|
||
private void PersistSingleDelivery(Delivery d)
|
||
{
|
||
try
|
||
{
|
||
var snap = SnapshotFromDelivery(d);
|
||
if (snap == null)
|
||
{
|
||
RemoveSingleDeliveryFromPersist(d.Id);
|
||
return;
|
||
}
|
||
var path = GetDeliveryPersistFilePath();
|
||
AtomicFileUpdateHelper.ExecuteAtomicUpdate(path, current =>
|
||
{
|
||
var list = string.IsNullOrEmpty(current)
|
||
? new List<DeliveryStateSnapshot>()
|
||
: JsonConvert.DeserializeObject<List<DeliveryStateSnapshot>>(current) ?? new List<DeliveryStateSnapshot>();
|
||
var idx = list.FindIndex(x => x.Id == d.Id);
|
||
if (idx >= 0)
|
||
list[idx] = snap;
|
||
else
|
||
list.Add(snap);
|
||
return JsonConvert.SerializeObject(list, Formatting.Indented);
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Log($"PersistSingleDelivery error: {ExceptionFormatter.FormatEx(ex)}", "ChainedDeliveryPersist", true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对外公开的单任务持久化方法,供界面或外部逻辑在修改 Delivery 状态后调用。
|
||
/// </summary>
|
||
/// <param name="d">需要持久化的搬运任务</param>
|
||
public void PersistDelivery(Delivery d)
|
||
{
|
||
if (d == null) return;
|
||
PersistSingleDelivery(d);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从持久化文件中移除指定任务记录,不修改其他任务记录。
|
||
/// </summary>
|
||
private void RemoveSingleDeliveryFromPersist(string deliveryId)
|
||
{
|
||
try
|
||
{
|
||
var path = GetDeliveryPersistFilePath();
|
||
AtomicFileUpdateHelper.ExecuteAtomicUpdate(path, current =>
|
||
{
|
||
if (string.IsNullOrEmpty(current)) return null;
|
||
var list = JsonConvert.DeserializeObject<List<DeliveryStateSnapshot>>(current) ?? new List<DeliveryStateSnapshot>();
|
||
list.RemoveAll(x => x.Id == deliveryId);
|
||
return JsonConvert.SerializeObject(list, Formatting.Indented);
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Log($"RemoveSingleDeliveryFromPersist error: {ExceptionFormatter.FormatEx(ex)}", "ChainedDeliveryPersist", true);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 工厂方法:当内存中尚不存在某个快照对应的 Delivery 时,
|
||
/// 允许子类根据快照信息创建一个新的 Delivery 实例并加入到 _allDeliveries。
|
||
/// 默认返回 null,表示基类不主动创建,具体业务由子类决定。
|
||
/// </summary>
|
||
/// <param name="snap">持久化快照</param>
|
||
/// <returns>新建的 Delivery;返回 null 则跳过该快照</returns>
|
||
protected virtual Delivery CreateDeliveryFromSnapshot(DeliveryStateSnapshot snap)
|
||
{
|
||
// 基类不知道具体业务类型,交由子类重写。
|
||
return null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查小车是否已载货,子类可重写以适配不同车型的载货判断逻辑
|
||
/// </summary>
|
||
/// <param name="car">待检查的小车</param>
|
||
/// <returns>true 表示已载货</returns>
|
||
protected virtual bool CheckCarLoaded(AbstractCar car)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从持久化文件恢复未完成的任务到内存队列,在进程启动时调用
|
||
/// </summary>
|
||
private void RecoverDeliveriesFromPersistedFile()
|
||
{
|
||
try
|
||
{
|
||
var path = GetDeliveryPersistFilePath();
|
||
if (!File.Exists(path)) return;
|
||
|
||
var json = File.ReadAllText(path);
|
||
if (string.IsNullOrWhiteSpace(json)) return;
|
||
|
||
var snapshots = JsonConvert.DeserializeObject<List<DeliveryStateSnapshot>>(json) ?? new List<DeliveryStateSnapshot>();
|
||
if (!snapshots.Any()) return;
|
||
|
||
List<Delivery> ds;
|
||
lock (_sync) ds = _allDeliveries.ToList();
|
||
|
||
foreach (var snap in snapshots)
|
||
{
|
||
// 1. 先尝试在现有内存任务中匹配(优先 TaskId,其次内部 Id)
|
||
var d = ds.FirstOrDefault(x =>
|
||
(!string.IsNullOrEmpty(snap.TaskId) && x.TaskId == snap.TaskId) || x.Id == snap.Id);
|
||
|
||
// 2. 如果内存中没有对应任务,尝试通过工厂方法创建一个新的 Delivery
|
||
if (d == null)
|
||
{
|
||
var created = CreateDeliveryFromSnapshot(snap);
|
||
if (created != null)
|
||
{
|
||
lock (_sync)
|
||
{
|
||
_allDeliveries.Add(created);
|
||
ds.Add(created); // 以便后续快照仍然可以在本次循环中匹配到
|
||
}
|
||
d = created;
|
||
}
|
||
else
|
||
{
|
||
// 子类未提供创建逻辑,则跳过该快照
|
||
continue;
|
||
}
|
||
}
|
||
|
||
lock (d.SyncStatus)
|
||
{
|
||
d.Src = snap.Src;
|
||
d.Dst = snap.Dst;
|
||
d.SkipFetch = snap.SkipFetch;
|
||
d.SkipPut = snap.SkipPut;
|
||
d.Putting = snap.Putting;
|
||
d.CreateTime = snap.CreateTime;
|
||
d.StartTime = snap.StartTime;
|
||
d.FinishTime = snap.FinishTime;
|
||
d.ReportOnStarted = snap.ReportOnStarted;
|
||
d.ReportOnFetched = snap.ReportOnFetched;
|
||
d.ReportOnPut = snap.ReportOnPut;
|
||
d.ReportOnFinished = snap.ReportOnFinished;
|
||
d.ReportOnFailed = snap.ReportOnFailed;
|
||
d.ReportOnTerminated = snap.ReportOnTerminated;
|
||
d.OnStartCallbackKeys = snap.OnStartCallbackKeys ?? new List<string>();
|
||
d.DoneFetchCallbackKeys = snap.DoneFetchCallbackKeys ?? new List<string>();
|
||
d.DonePutCallbackKeys = snap.DonePutCallbackKeys ?? new List<string>();
|
||
d.DoneMissionCallbackKeys = snap.DoneMissionCallbackKeys ?? new List<string>();
|
||
d.FailedCallbackKeys = snap.FailedCallbackKeys ?? new List<string>();
|
||
d.OnTerminatedCallbackKeys = snap.OnTerminatedCallbackKeys ?? new List<string>();
|
||
|
||
d.Active = false;
|
||
d.Finished = false;
|
||
d.Error = false;
|
||
d.Canceled = false;
|
||
d.Terminated = false;
|
||
|
||
if (Enum.TryParse<DeliveryStatus>(snap.Status, out var s))
|
||
{
|
||
switch (s)
|
||
{
|
||
case DeliveryStatus.Fetching:
|
||
d.Putting = false;
|
||
break;
|
||
case DeliveryStatus.Putting:
|
||
d.Putting = true;
|
||
d.SkipFetch = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (snap.UsingCarId >= 0)
|
||
d.UsingCar = SimpleLib.GetCar(snap.UsingCarId) as Car;
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Log($"RecoverDeliveriesFromPersistedFile error: {ExceptionFormatter.FormatEx(ex)}", "ChainedDeliveryPersist", true);
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Lifecycle (background loop with cancellation)
|
||
|
||
/// <summary>用于取消后台调度循环的取消令牌源</summary>
|
||
[JsonIgnore] private CancellationTokenSource _cts;
|
||
/// <summary>后台调度循环任务</summary>
|
||
[JsonIgnore] private Task _loopTask;
|
||
/// <summary>进程是否已启动</summary>
|
||
[JsonIgnore] private volatile bool _started;
|
||
/// <summary>用于在界面绘制调度状态的画刷</summary>
|
||
[JsonIgnore] private readonly SimpleMonitor.Painter _painter = SimpleMonitor.getPainter("cdmPainter_refactored");
|
||
|
||
/// <summary>
|
||
/// 启动搬运任务调度进程:恢复持久化任务、启动后台循环
|
||
/// </summary>
|
||
[MethodMember(Name = "启动进程", Description = "开始处理搬运队列")]
|
||
public override void Execute()
|
||
{
|
||
if (_started) return;
|
||
_started = true;
|
||
status.status = "启动中";
|
||
|
||
RecoverDeliveriesFromPersistedFile();
|
||
|
||
_cts = new CancellationTokenSource();
|
||
_loopTask = Task.Run(() => LoopAsync(_cts.Token), _cts.Token);
|
||
status.status = "已启动";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 关闭调度进程,取消后台循环
|
||
/// </summary>
|
||
[MethodMember(Name = "关闭进程", Description = "关闭任务维护进程")]
|
||
public override void Stop()
|
||
{
|
||
status.status = "关闭中";
|
||
_started = false;
|
||
try
|
||
{
|
||
_cts?.Cancel();
|
||
}
|
||
catch { /* ignore */ }
|
||
status.status = "已关闭";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 后台调度循环:周期性地检查等待中的任务,尝试为其分配小车并执行
|
||
/// 每次迭代:读取配置、调整优先级、对等待任务排序、逐个尝试执行并更新淤积统计
|
||
/// </summary>
|
||
/// <param name="token">取消令牌,用于响应 Stop 关闭请求</param>
|
||
private async Task LoopAsync(CancellationToken token)
|
||
{
|
||
var stat = (DeliveryMissionStatus)status;
|
||
var iteration = 0;
|
||
|
||
while (!token.IsCancellationRequested)
|
||
{
|
||
var checkingTime = new Dictionary<string, string>();
|
||
try
|
||
{
|
||
// 读取任务参数
|
||
var missionField = StringDictConvert<ChainedDeliveryParams>.Convert(fields) ?? new ChainedDeliveryParams();
|
||
var mustArrangeThreshold = ReadMustArrangeThreshold();
|
||
stat.MustArrangeThreshold = mustArrangeThreshold;
|
||
EnableBlockStandby = missionField.EnableBlockStandby;
|
||
var arrangeThresholdTime = TimeSpan.FromSeconds(mustArrangeThreshold);
|
||
|
||
ChangePriority();
|
||
|
||
// 获取所有等待中的任务,按优先级和超时时间排序(超时必须优先安排)
|
||
Delivery[] currentChecking;
|
||
lock (_sync)
|
||
{
|
||
var pendingDs = _allDeliveries.Where(p => p.GetStatus() == DeliveryStatus.Waiting).ToList();
|
||
currentChecking = pendingDs.ToArray();
|
||
currentChecking = currentChecking.OrderByDescending(p =>
|
||
p.Priority + ((DateTime.Now - p.CreateTime) > arrangeThresholdTime ? 1 : 0)).ToArray();
|
||
}
|
||
|
||
stat.Enqueued = currentChecking.Length;
|
||
|
||
var stuckStr = "";
|
||
var stuckedNum = 0;
|
||
|
||
foreach (var d in currentChecking)
|
||
{
|
||
|
||
checkingTime[d.Id] = "处理中";
|
||
UpdateDisplay(iteration, checkingTime);
|
||
var startAt = DateTime.Now;
|
||
|
||
if (!TryExecuteSingle(d, missionField, out var reason))
|
||
{
|
||
d.StuckTime += 1;
|
||
d.StuckReason = reason ?? d.StuckReason;
|
||
stuckedNum += 1;
|
||
stuckStr += $"{d.Id}:{d.StuckTime} ";
|
||
}
|
||
|
||
checkingTime[d.Id] = $"{(DateTime.Now - startAt).TotalSeconds:0.0}s";
|
||
UpdateDisplay(iteration, checkingTime);
|
||
}
|
||
|
||
stat.StuckNum = stuckedNum;
|
||
stat.StuckInfo = stuckStr;
|
||
|
||
status.status = $"已启动-循环{iteration++}";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Log($"CDM(refactored) error: {ExceptionFormatter.FormatEx(ex)}", "error", true);
|
||
}
|
||
|
||
UpdateDisplay(iteration, checkingTime);
|
||
try { await Task.Delay(1000, token); } catch { /* cancelled */ } // 每 1 秒循环一次
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从配置中读取“必须安排”阈值(秒),超过该等待时间的任务将被优先调度
|
||
/// </summary>
|
||
private double ReadMustArrangeThreshold()
|
||
{
|
||
if (!fields.TryGetValue("mustArrangeThreshold", out var str) || !double.TryParse(str, out var v))
|
||
return 300;
|
||
return v;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新界面上的调度状态显示:循环次数及各任务处理耗时
|
||
/// </summary>
|
||
private void UpdateDisplay(int iteration, Dictionary<string, string> checkingTime)
|
||
{
|
||
try
|
||
{
|
||
_painter.clear();
|
||
_painter.drawTextFixed(
|
||
$"CDM循环数:{iteration}\n{string.Join("\n", checkingTime.Select(kvp => $"{kvp.Key}\t{kvp.Value}"))}",
|
||
new SolidBrush(Color.Black),
|
||
VirtualPainter.DrawPosition.RightBottom,
|
||
Color.AliceBlue);
|
||
}
|
||
catch
|
||
{
|
||
// UI绘制失败不影响调度
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Core execution pipeline (refactored)
|
||
|
||
/// <summary>
|
||
/// 尝试执行单个搬运任务:校验前置条件、选车规划取货、规划放货、编译下发、启动异步执行
|
||
/// </summary>
|
||
/// <param name="d">待执行的搬运任务</param>
|
||
/// <param name="missionField">任务参数配置</param>
|
||
/// <param name="stuckReason">若失败,返回淤积原因</param>
|
||
/// <returns>true 表示任务已成功启动执行</returns>
|
||
private bool TryExecuteSingle(Delivery d, ChainedDeliveryParams missionField, out string stuckReason)
|
||
{
|
||
stuckReason = null;
|
||
missionField ??= new ChainedDeliveryParams();
|
||
|
||
// 已在执行中
|
||
if (d.IsActive()) return true;
|
||
|
||
// 依赖关系
|
||
if (d.Former != null && !d.Former.IsActive())
|
||
{
|
||
stuckReason = $"depends on {d.Former.Id} not started";
|
||
return false;
|
||
}
|
||
|
||
if (d.Former != null && !d.Former.IsFinished() && !missionField.TightDeliveryChain)
|
||
{
|
||
stuckReason = $"depends on {d.Former.Id} not finished";
|
||
return false;
|
||
}
|
||
|
||
if (d.StartCondition != null && !d.StartCondition())
|
||
{
|
||
stuckReason = $"{d.Id} does not meet startCondition";
|
||
return false;
|
||
}
|
||
|
||
// 延续任务:已有 fetchPlan/fetchCode,延续任务取消
|
||
//if (d.FetchPlan != null)
|
||
// return TryHandleContinuationFetch(d, out stuckReason);
|
||
|
||
// 非延续:选车并规划取货
|
||
SegmentPlan fetchPlan = null;
|
||
Car usingCar;
|
||
if (!d.SkipFetch)
|
||
{
|
||
if (!TrySelectCarAndPlanFetch(d, out usingCar, out fetchPlan, out stuckReason))
|
||
return false;
|
||
}
|
||
else
|
||
{
|
||
usingCar = d.UsingCar;
|
||
//fetchPlan = d.FetchPlan;
|
||
}
|
||
|
||
if (usingCar == null)
|
||
{
|
||
stuckReason = d.SkipFetch
|
||
? $"{d.Id} skip fetch, but no using car assigned for put"
|
||
: $"ucar null, {d.Src}->{d.Dst}";
|
||
return false;
|
||
}
|
||
|
||
// 规划放货
|
||
if (!TryPlanPut(d, usingCar, fetchPlan != null, out var putPlan, out var escPlan, out stuckReason))
|
||
return false;
|
||
|
||
// 编译与下发(含标签占用)
|
||
if (!TryCompileAndDispatch(d, usingCar, fetchPlan, putPlan, escPlan, out var allCode, out stuckReason))
|
||
return false;
|
||
|
||
// 启动 Runner 异步运行并回收状态
|
||
_ = RunDeliveryAsync(d, usingCar, fetchPlan, escPlan, allCode);
|
||
|
||
return true;
|
||
}
|
||
|
||
#region 延续任务,功能屏蔽
|
||
//private bool TryHandleContinuationFetch(Delivery d, out string stuckReason)
|
||
//{
|
||
// stuckReason = null;
|
||
// var usingCar = (Car)d.FetchPlan.UsingCar;
|
||
|
||
// // redirect_fetched: 0 未开始;1 取货中;3 redirect;333 取货完成等待放货;666 去避让
|
||
// if (d.RedirectFetched == 0)
|
||
// {
|
||
// lock (Commons.PlanSession)
|
||
// {
|
||
// if (usingCar.tags.Contains("occupied"))
|
||
// {
|
||
// stuckReason = $"preassigned using car {usingCar.id} is still occupied";
|
||
// return false;
|
||
// }
|
||
|
||
// d.RedirectFetched = 1;
|
||
// usingCar.tags.Add("occupied", $"DeliverFetch{d.Id}");
|
||
// usingCar.tags.Remove("blocking");
|
||
// usingCar.tags.Remove("idle");
|
||
// usingCar.tags.Remove("charging");
|
||
// }
|
||
|
||
// try
|
||
// {
|
||
// Commons.DeleteTag(tags, "FailCDMToStandby");
|
||
// Commons.DeleteTag(tags, "FailCDMToStandbyTime");
|
||
// var task = d.FetchCode.Queue();
|
||
|
||
// async Task go()
|
||
// {
|
||
// try
|
||
// {
|
||
// await task;
|
||
// d.doneFetch?.Invoke(d);
|
||
// lock (d.SyncStatus) d.Putting = true;
|
||
// }
|
||
// catch (Exception ex)
|
||
// {
|
||
// Diagnosis.Log($"Send Prefetch Script to {usingCar.id} exception:{ExceptionFormatter.FormatEx(ex)}", "error", true);
|
||
// lock (d.SyncStatus) { d.Error = true; d.Active = false; }
|
||
// lock (Commons.PlanSession)
|
||
// {
|
||
// usingCar.tags.Remove("redirect");
|
||
// usingCar.tags.Remove("occupied");
|
||
// usingCar.tags.Remove("dest");
|
||
// usingCar.tags.Add("idle", DateTime.Now.ToString());
|
||
// }
|
||
// d.failed?.Invoke(d);
|
||
// return;
|
||
// }
|
||
|
||
// usingCar.siteID = d.Src;
|
||
// lock (Commons.PlanSession)
|
||
// {
|
||
// usingCar.tags.Remove("occupied");
|
||
// usingCar.tags.Remove("redirect");
|
||
// usingCar.tags.Add("pendingput", d.Id.ToString());
|
||
// }
|
||
// d.RedirectFetched = 333;
|
||
// }
|
||
|
||
// _ = go();
|
||
// }
|
||
// catch (Exception ex)
|
||
// {
|
||
// Diagnosis.Log($"pre-fetching of {d.Id} by {usingCar.id} failed: {ExceptionFormatter.FormatEx(ex)}", "error", true);
|
||
// lock (d.SyncStatus) { d.Error = true; d.Active = false; }
|
||
// lock (Commons.PlanSession)
|
||
// {
|
||
// usingCar.tags.Remove("occupied");
|
||
// usingCar.tags.Remove("redirect");
|
||
// usingCar.tags.Remove("dest");
|
||
// usingCar.tags.Add("idle", DateTime.Now.ToString());
|
||
// }
|
||
// d.failed?.Invoke(d);
|
||
// stuckReason = "prefetch failed";
|
||
// return false;
|
||
// }
|
||
|
||
// stuckReason = "prefetching";
|
||
// return false;
|
||
// }
|
||
|
||
// if (d.RedirectFetched == 1 || d.RedirectFetched == 3)
|
||
// {
|
||
// stuckReason = "redirect fetching or redirect giveWay";
|
||
// return false;
|
||
// }
|
||
|
||
// // 333/666:等待放货阶段,由主流程继续处理(fetchPlan != null 分支会进入 TryPlanPut)
|
||
// stuckReason = "waiting for put after prefetch";
|
||
// return false;
|
||
//}
|
||
#endregion
|
||
|
||
/// <summary>
|
||
/// 从可用小车中选车并规划取货路径:筛选可用车、计算到取货点的路径权重、选择最优车与取货段
|
||
/// </summary>
|
||
/// <param name="d">待执行的搬运任务</param>
|
||
/// <param name="usingCar">输出:选中的小车</param>
|
||
/// <param name="fetchPlan">输出:取货路径规划</param>
|
||
/// <param name="stuckReason">输出:若失败,淤积原因</param>
|
||
/// <returns>true 表示成功选车并完成取货路径规划</returns>
|
||
private bool TrySelectCarAndPlanFetch(Delivery d, out Car usingCar, out SegmentPlan fetchPlan, out string stuckReason)
|
||
{
|
||
usingCar = null;
|
||
fetchPlan = null;
|
||
stuckReason = null;
|
||
|
||
// 统计可用车:排除不可调度、类型不匹配、被 hold 到其他站点的车
|
||
var okCars = new List<Car>();
|
||
if (d.UsingCar == null)
|
||
{
|
||
foreach (var car in SimpleLib.GetAllCars().OfType<Car>())
|
||
{
|
||
if (Commons.SelectCar(car, EnableBlockStandby) < 0) continue;
|
||
if (d.UsingCar != null && d.UsingCar != car) continue;
|
||
if (!MeetSelectorKey(car, d.CarType)) continue;
|
||
if (car.tags.Contains("taskIdsString") && car.tags.TryGetValue("taskIdsString", out var ids) && !ids.Contains(d.TaskId)) continue;
|
||
if (car.tags.TryGetValue("holdCar", out var holdSite) && holdSite != d.Src.ToString()) continue;
|
||
okCars.Add(car);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
usingCar = d.UsingCar;
|
||
if (Commons.SelectCar(usingCar, EnableBlockStandby) < 0)
|
||
{
|
||
stuckReason = $"preassigned using car {usingCar.id} is not available";
|
||
return false;
|
||
}
|
||
fetchPlan = new SegmentPlan { usingCar = usingCar };
|
||
if(!d.SkipPut) fetchPlan.fields["allow_destination_on_route"] = "true";
|
||
fetchPlan.fields["CarLength"] = d.MaterialLength.ToString(CultureInfo.InvariantCulture);
|
||
fetchPlan.fields["CarWidth"] = d.MaterialWidth.ToString(CultureInfo.InvariantCulture);
|
||
fetchPlan.fields["action"] = "fetch";
|
||
foreach (var kv in d.FetchPlanInfo) fetchPlan.fields[kv.Key] = kv.Value;
|
||
fetchPlan.findLoop = d.SrcFindLoop;
|
||
var carSiteId = usingCar.GetLastSite();
|
||
fetchPlan.FindRoute(SimpleLib.GetSite(carSiteId), SimpleLib.GetSite(d.Src));
|
||
return true;
|
||
}
|
||
|
||
if (okCars.Count == 0 && d.UsingCar == null)
|
||
{
|
||
stuckReason = $"no available car, {d.Src}->{d.Dst}";
|
||
return false;
|
||
}
|
||
|
||
|
||
// 遍历可用车,计算每辆车到取货点的路径权重,选择路径最短的车
|
||
var currentWeight = float.MaxValue;
|
||
foreach (var car in okCars)
|
||
{
|
||
var mPlan = new SegmentPlan { usingCar = car };
|
||
if (!d.SkipPut) mPlan.fields["allow_destination_on_route"] = "true";
|
||
mPlan.fields["CarLength"] = d.MaterialLength.ToString(CultureInfo.InvariantCulture);
|
||
mPlan.fields["CarWidth"] = d.MaterialWidth.ToString(CultureInfo.InvariantCulture);
|
||
mPlan.fields["action"] = "fetch";
|
||
foreach (var kv in d.FetchPlanInfo) mPlan.fields[kv.Key] = kv.Value;
|
||
|
||
try
|
||
{
|
||
var fs = car.GetLastSite();
|
||
if (fs == -1) continue;
|
||
mPlan.findLoop = d.SrcFindLoop;
|
||
var w = mPlan.FindRoute(SimpleLib.GetSite(fs), SimpleLib.GetSite(d.Src));
|
||
if (w < currentWeight)
|
||
{
|
||
currentWeight = w;
|
||
usingCar = car;
|
||
fetchPlan = mPlan;
|
||
}
|
||
}
|
||
catch (BasicHeuristics.PlanningConflictException sde)
|
||
{
|
||
Diagnosis.Post($"D{d.Id} {car.name}({car.id}) conflict: {ExceptionFormatter.FormatEx(sde)}");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Diagnosis.Post($"D{d.Id} {car.name}({car.id}) exception: {ExceptionFormatter.FormatEx(e)}");
|
||
}
|
||
}
|
||
|
||
if (usingCar == null)
|
||
{
|
||
stuckReason = $"no car route to fetch src {d.Src}";
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规划放货路径:从取货点/当前载货点到放货点的路径,支持 DestinationOnRouteException 重试
|
||
/// </summary>
|
||
/// <param name="d">搬运任务</param>
|
||
/// <param name="usingCar">执行任务的小车</param>
|
||
/// <param name="prefetch">是否已完成取货(true 表示取货段已规划或已执行)</param>
|
||
/// <param name="putPlan">输出:放货路径规划</param>
|
||
/// <param name="escPlan">输出:逃逸路径(当前实现中为 null)</param>
|
||
/// <param name="stuckReason">输出:若失败,淤积原因</param>
|
||
/// <returns>true 表示成功规划放货路径</returns>
|
||
private bool TryPlanPut(Delivery d, Car usingCar, bool prefetch, out SegmentPlan putPlan, out SegmentPlan escPlan, out string stuckReason)
|
||
{
|
||
putPlan = null;
|
||
escPlan = null;
|
||
stuckReason = null;
|
||
|
||
putPlan = new SegmentPlan { usingCar = usingCar };
|
||
putPlan.fields["action"] = "put";
|
||
foreach (var kv in d.PutPlanInfo) putPlan.fields[kv.Key] = kv.Value;
|
||
putPlan.fields["useMustCanGo"] = prefetch ? "true" : "false";
|
||
putPlan.findLoop = !prefetch || d.DstFindLoop;
|
||
|
||
// 确定放货路径起点:SkipFetch 时从载货站点取,否则从取货点 d.Src
|
||
var srcSiteId = d.Src;
|
||
if (d.SkipFetch)
|
||
{
|
||
srcSiteId = usingCar.status.holdingLocks.FirstOrDefault();
|
||
if (srcSiteId == -1)
|
||
{
|
||
stuckReason = "selected using car is not reset";
|
||
return false;
|
||
}
|
||
}
|
||
|
||
if (d.SkipPut)
|
||
return true;
|
||
|
||
// 规划放货路径,最多重试 2 次以处理 DestinationOnRouteException
|
||
for (var attempt = 0; attempt < 2; attempt++)
|
||
{
|
||
try
|
||
{
|
||
putPlan.fields["allow_destination_on_route"] = "true";
|
||
putPlan.FindRoute(SimpleLib.GetSite(srcSiteId), SimpleLib.GetSite(d.Dst));
|
||
return true;
|
||
}
|
||
catch (BasicHeuristics.DestinationOnRouteException)
|
||
{
|
||
//allowDestinationOnRoute = true;
|
||
}
|
||
catch (BasicHeuristics.PlanningConflictException dce)
|
||
{
|
||
// 这里保持与原逻辑一致:遇到终点冲突先返回 false 让下一轮再试(推挤/避让可在子类扩展)
|
||
stuckReason = $"dest {d.Dst} conflict with other cars";
|
||
Diagnosis.Post($"D{d.Id} dest {d.Dst} conflict: {ExceptionFormatter.FormatEx(dce)}");
|
||
return false;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
stuckReason = $"no route {srcSiteId}->{d.Dst}: {ex.Message}";
|
||
return false;
|
||
}
|
||
}
|
||
|
||
stuckReason = $"destination {d.Dst} on route, retry failed";
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 编译取货/放货/逃逸段为小车程序、设置小车标签(occupied/deliver/dest)、下发执行
|
||
/// </summary>
|
||
/// <param name="d">搬运任务</param>
|
||
/// <param name="usingCar">执行任务的小车</param>
|
||
/// <param name="fetchPlan">取货路径规划(可为 null)</param>
|
||
/// <param name="putPlan">放货路径规划</param>
|
||
/// <param name="escPlan">逃逸路径规划(可为 null)</param>
|
||
/// <param name="allCode">输出:编译后的小车程序</param>
|
||
/// <param name="stuckReason">输出:若失败,淤积原因</param>
|
||
/// <returns>true 表示编译并下发成功</returns>
|
||
private bool TryCompileAndDispatch(
|
||
Delivery d,
|
||
Car usingCar,
|
||
SegmentPlan fetchPlan,
|
||
SegmentPlan putPlan,
|
||
SegmentPlan escPlan,
|
||
out CarProgram allCode,
|
||
out string stuckReason)
|
||
{
|
||
allCode = null;
|
||
stuckReason = null;
|
||
|
||
// 车可用性检查:若车已被占用则不能下发
|
||
lock (Commons.PlanSession)
|
||
{
|
||
if (usingCar.tags.Contains("occupied"))
|
||
{
|
||
stuckReason = $"{d.Id} selected {usingCar.id} but car not available";
|
||
return false;
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
void AfterFetch(CarProgram.LetGo doLetGo)
|
||
{
|
||
d.DoneFetch?.Invoke(d);
|
||
lock (d.SyncStatus) d.Putting = true;
|
||
PersistSingleDelivery(d);
|
||
doLetGo();
|
||
}
|
||
|
||
void AfterPut(CarProgram.LetGo doLetGo)
|
||
{
|
||
d.FinishTime = DateTime.Now;
|
||
d.DonePut?.Invoke(d);
|
||
PersistSingleDelivery(d);
|
||
doLetGo();
|
||
}
|
||
|
||
var forecastPredicate = new Func<Site, bool>(site => site.fields.ContainsKey(DefineGiveWayType(usingCar)));
|
||
|
||
// 根据是否有取货/放货/逃逸段,拼接编译为完整程序并追加避让预测
|
||
if (escPlan == null)
|
||
{
|
||
if (fetchPlan != null && !d.SkipPut)
|
||
{
|
||
allCode = fetchPlan.Compile($"D{d.Id}", false)
|
||
.Append(putPlan, beforeCall: AfterFetch)
|
||
.Forecast(Commons.GenerateEscapePlan(putPlan, forecastPredicate));
|
||
}
|
||
else
|
||
{
|
||
allCode = putPlan.Compile($"D{d.Id}",false)
|
||
.Forecast(Commons.GenerateEscapePlan(putPlan, forecastPredicate));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (fetchPlan != null)
|
||
{
|
||
allCode = fetchPlan.Compile($"D{d.Id}Esc", false)
|
||
.Append(putPlan, beforeCall: AfterFetch)
|
||
.Append(escPlan, beforeCall: AfterPut)
|
||
.Forecast(Commons.GenerateEscapePlan(putPlan, forecastPredicate));
|
||
}
|
||
else
|
||
{
|
||
allCode = putPlan.Compile($"D{d.Id}Esc", false)
|
||
.Append(escPlan, beforeCall: AfterPut)
|
||
.Forecast(Commons.GenerateEscapePlan(putPlan, forecastPredicate));
|
||
}
|
||
}
|
||
|
||
// 更新小车标签:标记为 occupied/deliver,设置 dest 为放货点或逃逸终点
|
||
lock (Commons.PlanSession)
|
||
{
|
||
usingCar.tags.Remove("pendingput");
|
||
usingCar.tags.Add("occupied", $"CDM_{d.Id}");
|
||
usingCar.tags.Remove("blocking");
|
||
usingCar.tags.Remove("idle");
|
||
usingCar.tags.Remove("charging");
|
||
usingCar.tags.Add("deliver", d.Id);
|
||
usingCar.tags.Remove("redirect");
|
||
if (escPlan == null)
|
||
usingCar.tags.Add("dest", d.Dst.ToString());
|
||
else
|
||
usingCar.tags.Add("dest", escPlan.segments.Last().id.ToString());
|
||
}
|
||
|
||
d.UsingCar = usingCar;
|
||
stuckReason = $"{usingCar.id} running";
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// 编译失败:回滚标签,并记录
|
||
Diagnosis.Log($"compile/dispatch failed D{d.Id} car {usingCar.id}: {ExceptionFormatter.FormatEx(ex)}", "error", true);
|
||
lock (Commons.PlanSession)
|
||
{
|
||
usingCar.tags.Remove("deliver");
|
||
usingCar.tags.Remove("occupied");
|
||
usingCar.tags.Remove("dest");
|
||
usingCar.tags.Add("idle", DateTime.Now.ToString(CultureInfo.CurrentCulture));
|
||
}
|
||
stuckReason = $"compile failed: {ex.Message}";
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 异步执行搬运任务:标记任务激活、下发程序、等待完成、更新 JPH、回调、清理标签与持久化
|
||
/// </summary>
|
||
/// <param name="d">搬运任务</param>
|
||
/// <param name="usingCar">执行任务的小车</param>
|
||
/// <param name="fetchPlan">取货路径规划(可为 null)</param>
|
||
/// <param name="escPlan">逃逸路径规划(可为 null)</param>
|
||
/// <param name="allCode">已编译的小车程序</param>
|
||
private async Task RunDeliveryAsync(
|
||
Delivery d,
|
||
Car usingCar,
|
||
SegmentPlan fetchPlan,
|
||
SegmentPlan escPlan,
|
||
CarProgram allCode)
|
||
{
|
||
try
|
||
{
|
||
lock (d.SyncStatus) d.Active = true;
|
||
d.StartTime = DateTime.Now;
|
||
d.OnStart?.Invoke(d);
|
||
PersistSingleDelivery(d);
|
||
Commons.DeleteTag(tags, "FailCDMToStandby");
|
||
Commons.DeleteTag(tags, "FailCDMToStandbyTime");
|
||
if (fetchPlan != null) LeaveChargeAction(usingCar, fetchPlan.Source);
|
||
|
||
try
|
||
{
|
||
await allCode.Queue();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
// “被 Block() 触发的 not allowed to lock” 属于可预期分支:清理小车标签并返回,不视为错误
|
||
var msg = ex is AggregateException ae ? ae.InnerExceptions.FirstOrDefault()?.Message ?? ex.Message : ex.Message;
|
||
Diagnosis.Post($"runner ex: {msg}", "block", true);
|
||
if (msg.Contains("not allowed to lock"))
|
||
{
|
||
lock (d.SyncStatus) d.Active = false;
|
||
lock (Commons.PlanSession)
|
||
{
|
||
usingCar.tags.Remove("deliver");
|
||
usingCar.tags.Remove("occupied");
|
||
usingCar.tags.Remove("redirect");
|
||
usingCar.tags.Remove("dest");
|
||
usingCar.tags.Add("idle", DateTime.Now.ToString(CultureInfo.CurrentCulture));
|
||
}
|
||
return;
|
||
}
|
||
|
||
Diagnosis.Log($"Send Script to {usingCar.id} exception:{ExceptionFormatter.FormatEx(ex)}", "error", true);
|
||
lock (d.SyncStatus)
|
||
{
|
||
d.Active = false;
|
||
if(d.Putting)
|
||
d.Error = true;
|
||
else
|
||
{
|
||
d.Suspended = true;
|
||
}
|
||
}
|
||
lock (Commons.PlanSession)
|
||
{
|
||
usingCar.tags.Remove("deliver");
|
||
usingCar.tags.Remove("occupied");
|
||
usingCar.tags.Remove("redirect");
|
||
usingCar.tags.Remove("dest");
|
||
usingCar.tags.Add("idle", DateTime.Now.ToString(CultureInfo.CurrentCulture));
|
||
}
|
||
d.Failed?.Invoke(d);
|
||
PersistSingleDelivery(d);
|
||
return;
|
||
}
|
||
|
||
//车体完成所有动作后续处理
|
||
UpdateJphOnSuccess(); // 更新每小时作业数统计
|
||
|
||
if (d.AppendedEscape != null)
|
||
{
|
||
d.FinishTime = DateTime.Now;
|
||
d.DonePut?.Invoke(d);
|
||
PersistSingleDelivery(d);
|
||
}
|
||
d.AppendedEscape?.Invoke();
|
||
|
||
lock (d.SyncStatus) d.Finished = true;
|
||
|
||
lock (Commons.PlanSession)
|
||
{
|
||
usingCar.tags.Remove("occupied");
|
||
usingCar.tags.Remove("deliver");
|
||
usingCar.tags.Remove("dest");
|
||
usingCar.tags.Add("idle", DateTime.Now.ToString(CultureInfo.CurrentCulture));
|
||
}
|
||
|
||
if (d.AppendedEscape == null)
|
||
{
|
||
d.FinishTime = DateTime.Now;
|
||
d.DonePut?.Invoke(d);
|
||
}
|
||
d.DoneMission?.Invoke(d);
|
||
RemoveSingleDeliveryFromPersist(d.Id);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Diagnosis.Post($"Delivery(refactored) {d.Id} {d.Src}->{d.Dst} fault:{ExceptionFormatter.FormatEx(ex)}", "ChainedDelivery");
|
||
// 外层异常(如 OnStart/Persist 抛错)需重置任务状态并清理小车标签,避免僵尸任务与占车
|
||
lock (d.SyncStatus) { d.Active = false; d.Error = true; }
|
||
lock (Commons.PlanSession)
|
||
{
|
||
usingCar.tags.Remove("deliver");
|
||
usingCar.tags.Remove("occupied");
|
||
usingCar.tags.Remove("redirect");
|
||
usingCar.tags.Remove("dest");
|
||
usingCar.tags.Add("idle", DateTime.Now.ToString(CultureInfo.CurrentCulture));
|
||
}
|
||
d.Failed?.Invoke(d);
|
||
RemoveSingleDeliveryFromPersist(d.Id);
|
||
}
|
||
}
|
||
|
||
private DateTime _stTime = DateTime.Now;
|
||
private int _stPerformed;
|
||
private double _prevJph;
|
||
|
||
/// <summary>
|
||
/// 任务成功后更新 JPH(每小时作业数)统计,采用滑动窗口指数平滑
|
||
/// </summary>
|
||
private void UpdateJphOnSuccess()
|
||
{
|
||
var stat = (DeliveryMissionStatus)status;
|
||
stat.Performed += 1;
|
||
var passTime = (DateTime.Now - _stTime).TotalHours;
|
||
stat.Jph = _prevJph * (1 - passTime) + (stat.Performed - _stPerformed) * passTime;
|
||
if (passTime > 1)
|
||
{
|
||
_stTime = DateTime.Now;
|
||
_stPerformed = stat.Performed;
|
||
_prevJph = stat.Jph;
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region Car selection helpers / blocking (kept as compatible utilities)
|
||
|
||
/// <summary>
|
||
/// 判断小车是否满足任务指定的车型筛选条件(CarType/SelectorKey)
|
||
/// </summary>
|
||
/// <param name="car">待检查的小车</param>
|
||
/// <param name="carType">任务要求的小车类型(空或 "Car" 表示不限制)</param>
|
||
/// <returns>true 表示满足筛选条件</returns>
|
||
private bool MeetSelectorKey(Car car, string carType)
|
||
{
|
||
if (car is DummyCar) return true;
|
||
if (carType == "Car") return true;
|
||
if (car.fields.ContainsKey("CarType") &&
|
||
((car.fields["CarType"] == carType) || car.fields["CarType"] == "Car"))
|
||
return true;
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 命令指定小车进入阻塞状态:拦截当前执行、去避让点、重置调度,保持与旧实现一致的锁顺序以降低死锁风险。
|
||
/// </summary>
|
||
/// <param name="carId">小车ID</param>
|
||
/// <param name="taskId">关联任务ID(仅用于日志)</param>
|
||
/// <returns>true 表示命令下发成功</returns>
|
||
public static bool CommandToBlock(int carId, int taskId = -1)
|
||
{
|
||
var car = (Car)SimpleLib.GetCar(carId);
|
||
int siteId;
|
||
|
||
lock (TrafficControl.syncTrafficSequence)
|
||
{
|
||
if (car.status.pendingLocks.Length == 0)
|
||
{
|
||
Diagnosis.Log($"cannot block {taskId}, destination already locked", "CommandToBlock", true);
|
||
return false;
|
||
}
|
||
if (car.status.holdingLocks == null || car.status.holdingLocks.Length == 0)
|
||
{
|
||
Diagnosis.Log($"cannot block {taskId}, car {car.id} has no holdingLocks", "CommandToBlock", true);
|
||
return false;
|
||
}
|
||
siteId = car.status.holdingLocks.Last();
|
||
}
|
||
|
||
car.Intercept(success =>
|
||
{
|
||
if (!success) return;
|
||
lock (Commons.PlanSession)
|
||
{
|
||
car.NoSchedule(true);
|
||
car.tags.Add("occupied", "blocking");
|
||
}
|
||
UglyBlock(car);
|
||
car.TrafficReset(SimpleLib.GetSite(siteId), true, false);
|
||
lock (Commons.PlanSession)
|
||
{
|
||
car.tags.Clear();
|
||
car.tags.Add("idle", DateTime.Now.ToString(CultureInfo.CurrentCulture));
|
||
}
|
||
});
|
||
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将小车置为“吹散”状态:停止调度、清空站点、清空标签、设为不可调度
|
||
/// </summary>
|
||
private static void BlownCar(Car car)
|
||
{
|
||
car.NoSchedule();
|
||
car.siteID = -1;
|
||
car.tags.Clear();
|
||
car.status.usage.AddUsage("base", new CarUsage.CarUsageInfo { scheduling = false, refreshing = false });
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行阻塞逻辑:调用小车 reset、置为阻塞中、吹散、轮询等待恢复(最长约 60 秒)
|
||
/// </summary>
|
||
private static void UglyBlock(Car usingCar)
|
||
{
|
||
try
|
||
{
|
||
((ClumsyCar)usingCar).Get("reset");
|
||
((ClumsyCar)usingCar).lstatus = "阻塞中";
|
||
BlownCar(usingCar);
|
||
const int maxWaitCount = 120;
|
||
var cnt = 0;
|
||
while (cnt < maxWaitCount)
|
||
{
|
||
cnt++;
|
||
Thread.Sleep(500);
|
||
if (usingCar.lstatus.Contains("正常") && cnt > 2) break;
|
||
}
|
||
|
||
if (!usingCar.lstatus.Contains("正常"))
|
||
Diagnosis.Log($"block timeout, ucar:{usingCar.id}, lstatus:{usingCar.lstatus}", "block", true);
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Diagnosis.Log($"block fail, ucar:{usingCar?.id}, status:{usingCar?.status}, " +
|
||
$"programs:{usingCar?.status?.programs}, task:{usingCar?.status?.programs?.task}: {ExceptionFormatter.FormatEx(e)}", "block", true);
|
||
try { Thread.Sleep(500); }
|
||
catch
|
||
{
|
||
// ignored
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
}
|
||
}
|