Files
StandardSence/StandardScene.Core/Chained/ChainedDeliveryMission.cs
T
2026-06-14 11:19:15 +08:00

1510 lines
66 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using IoTClient.Common.Helpers;
using Newtonsoft.Json;
using SimpleLite;
using SimpleLite.RCS;
using SimpleLite.RCS.CarTypes;
using SimpleLite.CADTools;
using SimpleLite.Props;
using SimpleLite.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 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
}
}