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
{
///
/// 抽象链式搬运任务类
/// 实现AGV搬运任务的核心调度逻辑,支持任务链式执行、智能调度、路径规划、冲突避让等功能
/// 可以做取满放空操作
///
public abstract class ChainedDeliveryMission:Mission
{
#region Parameters / Status
///
/// 链式搬运任务参数配置类
///
private class ChainedDeliveryParams
{
///
/// 是否启用打断正在去避让点的小车功能
///
public bool EnableBlockStandby { get; } = true;
///
/// 是否启用紧密任务链模式
/// true: 链式搬运任务可以同时启动(前序任务开始时后续任务即可开始),提高效率但可能造成死锁
/// false: 前序任务必须完成后,后续任务才能开始,更安全但效率较低
///
public bool TightDeliveryChain { get; } = true;
}
///
/// 搬运任务状态统计类
/// 记录任务执行过程中的各种统计信息
///
public class DeliveryMissionStatus : MissionStatus
{
/// 排队等待执行的任务数量
public int Enqueued;
/// 已完成的任务数量
public int Performed;
/// 每小时作业数(Jobs Per Hour),用于衡量系统吞吐量
public double Jph;
/// 淤积任务信息字符串,格式:任务ID:淤积次数
public string StuckInfo = "/";
/// 当前淤积的任务数量(无法执行的任务)
public int StuckNum;
///
/// 创建后,等待时间超过该阈值的任务,必须被优先安排执行。单位为秒。
///
public double MustArrangeThreshold = 300;
}
///
/// 继承自基类的任务状态属性,用于记录搬运任务的执行状态并在界面显示。
///
public override MissionStatus status { get; set; } = new DeliveryMissionStatus();
///
/// 允许任务打断正在去避让点的小车,使其承接该任务。
///
public bool EnableBlockStandby;
#endregion
#region Delivery model
///
/// 搬运任务状态枚举
///
public enum DeliveryStatus
{
/// 等待执行
Waiting = 0,
/// 取货中
Fetching = 1,
/// 放货中
Putting = 2,
/// 已完成
Finished = 3,
/// 已取消
Canceled = 4,
/// 已终止(执行过程中被中断)
Terminated = 5,
/// 错误
Error = 6,
/// 挂起
Suspended = 7,
}
///
/// 重构版 Delivery:字段与原 AbstractDelivery 保持接近,便于迁移;
/// 状态读写统一走 syncStatus 锁,避免竞态。
///
public abstract class Delivery
{
/// 雪花算法 ID 生成器(所有 Delivery 共享一份)
private static readonly SnowflakeIdGenerator IdGenerator =
new SnowflakeIdGenerator(workerId: 1, datacenterId: 1);
/// 任务唯一标识ID(雪花算法,Base62编码)
public string Id { get; set; } = IdGenerator.NextIdBase62();
/// 外部系统传入的任务ID
public string TaskId = string.Empty;
/// 物料长度(毫米),用于包络约束
public float MaterialLength = -1;
/// 物料宽度(毫米),用于包络约束
public float MaterialWidth = -1;
///
/// 任务启动条件函数,返回true时任务才能开始执行
/// 可用于实现复杂的前置条件判断
///
public Func StartCondition = null;
// ========== 任务回调函数 ==========
/// 任务失败时的回调函数
[JsonIgnore] public Action Failed;
/// 放货完成时的回调函数
[JsonIgnore] public Action DonePut;
/// 取货完成时的回调函数
[JsonIgnore] public Action DoneFetch;
/// 整个任务完成时的回调函数
[JsonIgnore] public Action DoneMission;
/// 任务开始执行时的回调函数
[JsonIgnore] public Action OnStart;
/// 任务终止时的回调函数
[JsonIgnore] public Func> OnTerminated;
// ========== 任务回调配置(可持久化) ==========
/// 是否在任务开始时上报回调
public bool ReportOnStarted;
/// 是否在取货完成时上报回调
public bool ReportOnFetched;
/// 是否在放货完成时上报回调
public bool ReportOnPut;
/// 是否在任务最终完成时上报回调
public bool ReportOnFinished;
/// 是否在任务失败时上报回调
public bool ReportOnFailed;
/// 是否在任务被终止/暂停时上报回调
public bool ReportOnTerminated;
/// OnStart 事件绑定的回调 key 列表
public List OnStartCallbackKeys { get; set; } = new();
/// DoneFetch 事件绑定的回调 key 列表
public List DoneFetchCallbackKeys { get; set; } = new();
/// DonePut 事件绑定的回调 key 列表
public List DonePutCallbackKeys { get; set; } = new();
/// DoneMission 事件绑定的回调 key 列表
public List DoneMissionCallbackKeys { get; set; } = new();
/// Failed 事件绑定的回调 key 列表
public List FailedCallbackKeys { get; set; } = new();
/// OnTerminated 事件绑定的回调 key 列表
public List OnTerminatedCallbackKeys { get; set; } = new();
// ========== 任务路径信息 ==========
/// 取货点站点ID
public int Src;
/// 放货点站点ID
public int Dst;
/// 是否跳过取货步骤(如果小车已在取货点或已载货,可设置为true)
public bool SkipFetch;
/// 是否跳过放货步骤(如果只需要移动到目标点但不放货)
public bool SkipPut;
/// 放货点是否允许查找回路路径
public bool DstFindLoop;
/// 取货点是否允许查找回路路径
public bool SrcFindLoop;
/// 取货路径规划参数字典(传递给路径规划器)
[JsonIgnore] public Dictionary FetchPlanInfo = new() { { "action", "fetch" } };
/// 放货路径规划参数字典(传递给路径规划器)
[JsonIgnore] public Dictionary PutPlanInfo = new() { { "action", "put" } };
/// 前序任务,当前任务需要等待前序任务执行完毕才能开始(任务链依赖关系)
[JsonIgnore] public Delivery Former;
/// 执行当前任务的小车对象
[JsonIgnore] public Car UsingCar;
/// 小车分组筛选条件(用于筛选特定组的小车)
[JsonIgnore] public string Group = string.Empty;
/// 小车筛选键列表(用于多条件筛选)
public List SelectorKeys = new();
/// 允许执行任务的小车类型(如"Forklift"、"Kiva"等)
[JsonIgnore] public string CarType = string.Empty;
///
/// 任务优先级,数值越大,优先级越高
/// 任务调度时按优先级降序执行,同优先级按创建时间排序
///
public int Priority = 0;
// ========== 时间记录 ==========
/// 任务创建时间
public DateTime CreateTime = DateTime.Now;
/// 任务开始执行时间
public DateTime StartTime;
/// 任务完成时间
public DateTime FinishTime;
/// 任务状态同步锁对象,用于多线程环境下保证状态修改的线程安全
[JsonIgnore] public readonly object SyncStatus = new object();
// ========== 任务内部状态标志 ==========
/// 任务是否正在执行中
internal bool Active;
/// 任务是否处于放货阶段(true=放货中,false=取货中)
internal bool Putting;
/// 任务是否已完成
internal bool Finished;
/// 任务是否出错
internal bool Error;
/// 任务是否已取消
internal bool Canceled;
/// 任务是否已终止(执行过程中被中断)
internal bool Terminated;
/// 任务被挂起
internal bool Suspended;
/// 追加的逃逸动作(任务完成后需要执行的额外动作,如去避让点)
[JsonIgnore] public Action AppendedEscape;
/// 任务淤积次数(连续执行失败的次数,用于监控任务阻塞情况)
public int StuckTime;
/// 任务淤积原因说明(记录为什么无法执行)
public string StuckReason = "/";
///
/// 获取任务当前状态
/// 状态优先级:Finished > Canceled > Terminated > Error > Fetching/Putting > Waiting
///
/// 任务当前状态枚举值
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; }
///
/// 取消任务,当前实现仅占位返回 true,具体取消逻辑可由子类重写
///
public bool Cancel()
{
return true;
}
}
///
/// 用于持久化的任务状态快照
/// 只包含可序列化、与业务恢复相关的关键信息
///
protected class DeliveryStateSnapshot
{
/// 任务唯一标识ID
public string Id { get; set; }
/// 外部系统传入的任务ID
public string TaskId { get; set; }
/// 取货点站点ID
public int Src { get; set; }
/// 放货点站点ID
public int Dst { get; set; }
/// 是否跳过取货步骤
public bool SkipFetch { get; set; }
/// 是否跳过放货步骤
public bool SkipPut { get; set; }
/// 当前是否处于放货阶段(true=放货中)
public bool Putting { get; set; }
/// 任务状态字符串(Waiting/Fetching/Putting/Finished等)
public string Status { get; set; }
/// 任务创建时间
public DateTime CreateTime { get; set; }
/// 任务开始执行时间
public DateTime StartTime { get; set; }
/// 任务完成时间
public DateTime FinishTime { get; set; }
/// 执行任务的小车ID(-1表示未分配)
public int UsingCarId { get; set; }
/// 允许执行任务的小车类型
public string CarType { get; set; }
/// 是否在任务开始时上报回调
public bool ReportOnStarted { get; set; }
/// 是否在取货完成时上报回调
public bool ReportOnFetched { get; set; }
/// 是否在放货完成时上报回调
public bool ReportOnPut { get; set; }
/// 是否在任务最终完成时上报回调
public bool ReportOnFinished { get; set; }
/// 是否在任务失败时上报回调
public bool ReportOnFailed { get; set; }
/// 是否在任务被终止/暂停时上报回调
public bool ReportOnTerminated { get; set; }
/// OnStart 事件绑定的回调 key 列表
public List OnStartCallbackKeys { get; set; } = new();
/// DoneFetch 事件绑定的回调 key 列表
public List DoneFetchCallbackKeys { get; set; } = new();
/// DonePut 事件绑定的回调 key 列表
public List DonePutCallbackKeys { get; set; } = new();
/// DoneMission 事件绑定的回调 key 列表
public List DoneMissionCallbackKeys { get; set; } = new();
/// Failed 事件绑定的回调 key 列表
public List FailedCallbackKeys { get; set; } = new();
/// OnTerminated 事件绑定的回调 key 列表
public List OnTerminatedCallbackKeys { get; set; } = new();
}
#endregion
#region Extensibility hooks
///
/// 小车离开充电站时的扩展动作钩子,子类可重写以执行自定义逻辑(如通知、统计等)
///
public virtual void LeaveChargeAction(Car car, Site site) { }
///
/// 动态调整任务优先级的钩子,在调度循环中每次迭代前调用,子类可重写实现自定义优先级策略
///
public virtual void ChangePriority() { }
///
/// 定义避让点类型字符串,用于路径规划时识别可避让的站点,子类可重写以区分不同避让类型
///
public virtual string DefineGiveWayType(AbstractCar car) => "giveWay";
#endregion
#region Queue / persistence (thread-safe)
/// 队列与持久化操作的同步锁,保证多线程访问 _allDeliveries 时的线程安全
[JsonIgnore] private readonly object _sync = new();
/// 所有搬运任务的集合(含等待中与执行中的任务)
[JsonIgnore] private readonly HashSet _allDeliveries = new();
///
/// 将任务加入队列并持久化到文件,线程安全
///
/// 待入队的搬运任务
public void Enqueue(Delivery d)
{
lock (_sync) _allDeliveries.Add(d);
PersistSingleDelivery(d);
}
///
/// 获取当前任务列表,支持按状态过滤(已完成/已终止/错误/已取消),线程安全
///
/// 是否包含已完成的任务
/// 是否包含已终止的任务
/// 是否包含错误状态的任务
/// 是否包含已取消的任务
/// 去重后按状态排序的任务列表
public List GetDeliveries(bool includeFinished = false, bool includeAborted = false,bool includeError = false,bool includeCanceled=false)
{
List 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();
}
///
/// 获取持久化文件路径,默认存储在 BaseDirectory/Deliveries/ 下,子类可重写自定义路径
///
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);
}
///
/// 从单个 Delivery 生成持久化快照(仅当状态为未结束时返回非 null)。
/// 在回调中用于单任务持久化,不修改其他任务记录。
///
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(),
DoneFetchCallbackKeys = d.DoneFetchCallbackKeys?.ToList() ?? new List(),
DonePutCallbackKeys = d.DonePutCallbackKeys?.ToList() ?? new List(),
DoneMissionCallbackKeys = d.DoneMissionCallbackKeys?.ToList() ?? new List(),
FailedCallbackKeys = d.FailedCallbackKeys?.ToList() ?? new List(),
OnTerminatedCallbackKeys = d.OnTerminatedCallbackKeys?.ToList() ?? new List()
};
}
}
///
/// 仅持久化当前任务,不更改其他任务记录;高并发下由 CommonTools 原子更新保证数据安全。
///
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()
: JsonConvert.DeserializeObject>(current) ?? new List();
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);
}
}
///
/// 对外公开的单任务持久化方法,供界面或外部逻辑在修改 Delivery 状态后调用。
///
/// 需要持久化的搬运任务
public void PersistDelivery(Delivery d)
{
if (d == null) return;
PersistSingleDelivery(d);
}
///
/// 从持久化文件中移除指定任务记录,不修改其他任务记录。
///
private void RemoveSingleDeliveryFromPersist(string deliveryId)
{
try
{
var path = GetDeliveryPersistFilePath();
AtomicFileUpdateHelper.ExecuteAtomicUpdate(path, current =>
{
if (string.IsNullOrEmpty(current)) return null;
var list = JsonConvert.DeserializeObject>(current) ?? new List();
list.RemoveAll(x => x.Id == deliveryId);
return JsonConvert.SerializeObject(list, Formatting.Indented);
});
}
catch (Exception ex)
{
Diagnosis.Log($"RemoveSingleDeliveryFromPersist error: {ExceptionFormatter.FormatEx(ex)}", "ChainedDeliveryPersist", true);
}
}
///
/// 工厂方法:当内存中尚不存在某个快照对应的 Delivery 时,
/// 允许子类根据快照信息创建一个新的 Delivery 实例并加入到 _allDeliveries。
/// 默认返回 null,表示基类不主动创建,具体业务由子类决定。
///
/// 持久化快照
/// 新建的 Delivery;返回 null 则跳过该快照
protected virtual Delivery CreateDeliveryFromSnapshot(DeliveryStateSnapshot snap)
{
// 基类不知道具体业务类型,交由子类重写。
return null;
}
///
/// 检查小车是否已载货,子类可重写以适配不同车型的载货判断逻辑
///
/// 待检查的小车
/// true 表示已载货
protected virtual bool CheckCarLoaded(AbstractCar car)
{
return false;
}
///
/// 从持久化文件恢复未完成的任务到内存队列,在进程启动时调用
///
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>(json) ?? new List();
if (!snapshots.Any()) return;
List 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();
d.DoneFetchCallbackKeys = snap.DoneFetchCallbackKeys ?? new List();
d.DonePutCallbackKeys = snap.DonePutCallbackKeys ?? new List();
d.DoneMissionCallbackKeys = snap.DoneMissionCallbackKeys ?? new List();
d.FailedCallbackKeys = snap.FailedCallbackKeys ?? new List();
d.OnTerminatedCallbackKeys = snap.OnTerminatedCallbackKeys ?? new List();
d.Active = false;
d.Finished = false;
d.Error = false;
d.Canceled = false;
d.Terminated = false;
if (Enum.TryParse(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)
/// 用于取消后台调度循环的取消令牌源
[JsonIgnore] private CancellationTokenSource _cts;
/// 后台调度循环任务
[JsonIgnore] private Task _loopTask;
/// 进程是否已启动
[JsonIgnore] private volatile bool _started;
/// 用于在界面绘制调度状态的画刷
[JsonIgnore] private readonly SimpleMonitor.Painter _painter = SimpleMonitor.getPainter("cdmPainter_refactored");
///
/// 启动搬运任务调度进程:恢复持久化任务、启动后台循环
///
[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 = "已启动";
}
///
/// 关闭调度进程,取消后台循环
///
[MethodMember(Name = "关闭进程", Description = "关闭任务维护进程")]
public void Stop()
{
status.status = "关闭中";
_started = false;
try
{
_cts?.Cancel();
}
catch { /* ignore */ }
status.status = "已关闭";
}
///
/// 后台调度循环:周期性地检查等待中的任务,尝试为其分配小车并执行
/// 每次迭代:读取配置、调整优先级、对等待任务排序、逐个尝试执行并更新淤积统计
///
/// 取消令牌,用于响应 Stop 关闭请求
private async Task LoopAsync(CancellationToken token)
{
var stat = (DeliveryMissionStatus)status;
var iteration = 0;
while (!token.IsCancellationRequested)
{
var checkingTime = new Dictionary();
try
{
// 读取任务参数
var missionField = StringDictConvert.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 秒循环一次
}
}
///
/// 从配置中读取“必须安排”阈值(秒),超过该等待时间的任务将被优先调度
///
private double ReadMustArrangeThreshold()
{
if (!fields.TryGetValue("mustArrangeThreshold", out var str) || !double.TryParse(str, out var v))
return 300;
return v;
}
///
/// 更新界面上的调度状态显示:循环次数及各任务处理耗时
///
private void UpdateDisplay(int iteration, Dictionary 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)
///
/// 尝试执行单个搬运任务:校验前置条件、选车规划取货、规划放货、编译下发、启动异步执行
///
/// 待执行的搬运任务
/// 任务参数配置
/// 若失败,返回淤积原因
/// true 表示任务已成功启动执行
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
///
/// 从可用小车中选车并规划取货路径:筛选可用车、计算到取货点的路径权重、选择最优车与取货段
///
/// 待执行的搬运任务
/// 输出:选中的小车
/// 输出:取货路径规划
/// 输出:若失败,淤积原因
/// true 表示成功选车并完成取货路径规划
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();
if (d.UsingCar == null)
{
foreach (var car in SimpleLib.GetAllCars().OfType())
{
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;
}
///
/// 规划放货路径:从取货点/当前载货点到放货点的路径,支持 DestinationOnRouteException 重试
///
/// 搬运任务
/// 执行任务的小车
/// 是否已完成取货(true 表示取货段已规划或已执行)
/// 输出:放货路径规划
/// 输出:逃逸路径(当前实现中为 null)
/// 输出:若失败,淤积原因
/// true 表示成功规划放货路径
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;
}
///
/// 编译取货/放货/逃逸段为小车程序、设置小车标签(occupied/deliver/dest)、下发执行
///
/// 搬运任务
/// 执行任务的小车
/// 取货路径规划(可为 null)
/// 放货路径规划
/// 逃逸路径规划(可为 null)
/// 输出:编译后的小车程序
/// 输出:若失败,淤积原因
/// true 表示编译并下发成功
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 => 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;
}
}
///
/// 异步执行搬运任务:标记任务激活、下发程序、等待完成、更新 JPH、回调、清理标签与持久化
///
/// 搬运任务
/// 执行任务的小车
/// 取货路径规划(可为 null)
/// 逃逸路径规划(可为 null)
/// 已编译的小车程序
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;
///
/// 任务成功后更新 JPH(每小时作业数)统计,采用滑动窗口指数平滑
///
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)
///
/// 判断小车是否满足任务指定的车型筛选条件(CarType/SelectorKey)
///
/// 待检查的小车
/// 任务要求的小车类型(空或 "Car" 表示不限制)
/// true 表示满足筛选条件
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;
}
///
/// 命令指定小车进入阻塞状态:拦截当前执行、去避让点、重置调度,保持与旧实现一致的锁顺序以降低死锁风险。
///
/// 小车ID
/// 关联任务ID(仅用于日志)
/// true 表示命令下发成功
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;
}
///
/// 将小车置为“吹散”状态:停止调度、清空站点、清空标签、设为不可调度
///
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 });
}
///
/// 执行阻塞逻辑:调用小车 reset、置为阻塞中、吹散、轮询等待恢复(最长约 60 秒)
///
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
}
}