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 } }