diff --git a/.gitignore b/.gitignore index 5892a93..62717c2 100644 --- a/.gitignore +++ b/.gitignore @@ -364,3 +364,4 @@ FodyWeavers.xsd build/ /build/SimpleComposer.exe /build/plugins/StandardScene.dll +/.cursor diff --git a/StandardScene.Core/Chained/AbstractLoopMission.cs b/StandardScene.Core/Chained/AbstractLoopMission.cs index 2c9f6e6..8a69195 100644 --- a/StandardScene.Core/Chained/AbstractLoopMission.cs +++ b/StandardScene.Core/Chained/AbstractLoopMission.cs @@ -9,6 +9,7 @@ using SimpleCore.PropType; using StandardScene.CarTypes; using StandardScene.Chained.Loop; using StandardScene.Model; +using StandardScene.Utils; using System; using System.Collections.Generic; using System.IO; @@ -2161,5 +2162,51 @@ namespace StandardScene.Chained StopAll(); } + [MethodMember(Name = "一键上线所有AGV", Description = "将所有已初始化的正常 AGV 标记为在线")] + public void OnlineAllCars() + { + CycleUiHelper.ConfirmThen("确认将所有已初始化的正常 AGV 一键上线?", () => Task.Run(OnlineAllCarsCore)); + } + + private void OnlineAllCarsCore() + { + int count = 0; + foreach (var car in SimpleLib.GetAllCars().OfType()) + { + if (car.GetLastSite() == -1 && Commons.GetVehicleStatus(car) == VehicleStatus.Normal) + { + Commons.AddOrUpdateTag(car.tags, "Online", "true"); + car.Reset(); + count++; + } + } + + status.status = $"已上线 {count} 台AGV"; + Diagnosis.Post($"[AbstractLoopMission] 一键上线所有AGV:已上线 {count} 台", "Loop-OnlineAll", true); + } + + [MethodMember(Name = "一键结束所有AGV任务", Description = "停止环线调度并清除所有 AGV 的环线任务分配")] + public void StopAllCarTasks() + { + CycleUiHelper.ConfirmThen("确认停止环线调度并结束所有 AGV 的环线任务?", () => Task.Run(StopAllCarTasksCore)); + } + + private void StopAllCarTasksCore() + { + + int count = 0; + foreach (var car in SimpleLib.GetAllCars().OfType()) + { + Commons.DeleteTag(car.tags, "goalSite"); + Commons.DeleteTag(car.tags, "loopAssigned"); + Commons.DeleteTag(car.tags, "occupied"); + car.ForceStop(); + count++; + } + + status.status = $"已结束 {count} 台AGV任务"; + Diagnosis.Post($"[AbstractLoopMission] 一键结束所有AGV任务:已停止环线并清理 {count} 台 AGV 的任务标签", "Loop-StopAllTasks", true); + } + } } diff --git a/StandardScene.Core/Chained/DeliveryViewer.cs b/StandardScene.Core/Chained/DeliveryViewer.cs index 2cce826..126ba04 100644 --- a/StandardScene.Core/Chained/DeliveryViewer.cs +++ b/StandardScene.Core/Chained/DeliveryViewer.cs @@ -43,6 +43,14 @@ namespace StandardScene.Chained private readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(1); private volatile string _status = ""; + // 显示筛选(仅作用于已拉取的快照,渲染线程内即时过滤,不触发重新拉取) + private string _filterSrc = ""; + private string _filterDst = ""; + private string _filterCar = ""; + private int _filterStatusIdx; + private readonly string[] _statusFilterNames = + new[] { "全部" }.Concat(Enum.GetNames(typeof(DeliveryStatus))).ToArray(); + /// 打开(或置前)任务管理面板。兼容原 new DeliveryViewer().Show() 调用方式。 public void Show() => Open(); @@ -91,13 +99,27 @@ namespace StandardScene.Chained EnsureSnapshotFresh(); var items = _snapshot; - pb.Label($"共 {items.Count} 个任务(超时任务高亮置顶)"); + + // 筛选栏:起点 / 终点 / 车辆 / 任务状态(仅过滤当前快照,不触发重新拉取) + // ###id:控件 id 经 ASCII 哈希,同字数纯中文标签(起点筛选/终点筛选)会撞 id,用 ### 后缀给唯一 ASCII id(显示不变)。 + var (fs, _) = pb.TextInput("起点筛选(站点ID或名称)###flt-src", _filterSrc, alwaysReturnString: true); _filterSrc = fs; + var (fd, _) = pb.TextInput("终点筛选(站点ID或名称)###flt-dst", _filterDst, alwaysReturnString: true); _filterDst = fd; + var (fc, _) = pb.TextInput("车辆筛选(车辆名)###flt-car", _filterCar, alwaysReturnString: true); _filterCar = fc; + pb.DropdownBox("状态筛选###flt-status", _statusFilterNames, ref _filterStatusIdx); + if (pb.Button("清空筛选", distinct: "delivery-clear-filter")) + { + _filterSrc = _filterDst = _filterCar = ""; + _filterStatusIdx = 0; + } + + var filtered = ApplyFilters(items); + pb.Label($"共 {filtered.Count}/{items.Count} 个任务(超时任务高亮置顶)"); pb.Table(TableId, new[] { "任务号", "小车", "取货点", "放货点", "任务状态", "下发时间", "执行时间", "结束时间", "优先级", "操作" }, - items.Count, (row, i) => + filtered.Count, (row, i) => { - var dd = items[i]; + var dd = filtered[i]; if (IsOverdue(dd)) row.SetColor(OverdueRowColor); row.Label($"{dd.Id}"); @@ -134,6 +156,32 @@ namespace StandardScene.Chained private bool IsOverdue(Delivery dd) => (DateTime.Now - dd.CreateTime).TotalMinutes > OverdueMinutesThreshold; + /// 按 起点 / 终点 / 车辆 / 任务状态 对当前快照即时过滤(空条件不限制;起点终点匹配“ID-名称”,大小写不敏感)。 + private List ApplyFilters(List items) + { + IEnumerable q = items; + + var fSrc = (_filterSrc ?? "").Trim(); + if (fSrc.Length > 0) + q = q.Where(d => $"{d.Src}-{SafeSiteName(d.Src)}".Contains(fSrc, StringComparison.OrdinalIgnoreCase)); + + var fDst = (_filterDst ?? "").Trim(); + if (fDst.Length > 0) + q = q.Where(d => $"{d.Dst}-{SafeSiteName(d.Dst)}".Contains(fDst, StringComparison.OrdinalIgnoreCase)); + + var fCar = (_filterCar ?? "").Trim(); + if (fCar.Length > 0) + q = q.Where(d => (d.UsingCar?.name ?? "").Contains(fCar, StringComparison.OrdinalIgnoreCase)); + + if (_filterStatusIdx > 0 && _filterStatusIdx < _statusFilterNames.Length) + { + var st = _statusFilterNames[_filterStatusIdx]; + q = q.Where(d => d.GetStatus().ToString() == st); + } + + return q.ToList(); + } + /// /// 渲染线程调用:到达刷新间隔且无在途刷新时,在后台线程重新拉取任务快照(超时任务置顶)。 /// 业务侧的锁与文件 IO 一律放到后台,渲染线程只读 引用,避免界面卡死。 diff --git a/StandardScene.Core/Chained/TransportMission.cs b/StandardScene.Core/Chained/TransportMission.cs index fcd3907..5f14065 100644 --- a/StandardScene.Core/Chained/TransportMission.cs +++ b/StandardScene.Core/Chained/TransportMission.cs @@ -354,6 +354,89 @@ namespace StandardScene.Chained } } + #region 模拟下发搬运任务(压测/演示用) + + /// 模拟下发维持的在途任务数上限 + private const int SimMaxTasks = 30; + + /// 模拟下发任务的 TaskId 前缀,用于识别与统计模拟任务 + private const string SimTaskPrefix = "Sim-"; + + [JsonIgnore] private CancellationTokenSource _simCts; + [JsonIgnore] private Task _simTask; + [JsonIgnore] private volatile bool _simRunning; + + [MethodMember(Name = "模拟下发搬运任务", Description = "随机在标记 Shelf 的站点间下发搬运任务,维持最多30个在途任务,完成后自动补发")] + public void StartSimulateTransport() + { + if (_simRunning) + { + CycleUiHelper.Alert("提示", "模拟下发搬运任务已在运行中。"); + return; + } + + _simRunning = true; + _simCts = new CancellationTokenSource(); + _simTask = Task.Run(() => SimulateTransportLoop(_simCts.Token)); + G.pushStatus($"已开始模拟下发搬运任务(目标在途 {SimMaxTasks} 个)"); + } + + [MethodMember(Name = "结束模拟下发搬运任务", Description = "停止模拟下发搬运任务")] + public void StopSimulateTransport() + { + _simRunning = false; + try { _simCts?.Cancel(); } catch { /* ignore */ } + G.pushStatus("已结束模拟下发搬运任务"); + } + + private async Task SimulateTransportLoop(CancellationToken token) + { + var rnd = new Random(); + while (!token.IsCancellationRequested) + { + try + { + var shelfSiteIds = SimpleLib.GetAllSites() + .Where(s => s.fields.ContainsKey("shelf")) + .Select(s => s.id) + .ToList(); + + if (shelfSiteIds.Count < 2) + { + Diagnosis.Post("[TransportMission] 模拟下发:标记 Shelf 字段的站点不足 2 个,暂不下发", "SimTransport", true); + } + else + { + int activeSim = GetDeliveries() + .Count(d => !string.IsNullOrEmpty(d.TaskId) && d.TaskId.StartsWith(SimTaskPrefix)); + + for (int i = activeSim; i < SimMaxTasks && !token.IsCancellationRequested; i++) + { + int src = shelfSiteIds[rnd.Next(shelfSiteIds.Count)]; + int dst; + do { dst = shelfSiteIds[rnd.Next(shelfSiteIds.Count)]; } while (dst == src); + + Enqueue(new TransportDelivery + { + CarType = "Car", + Src = src, + Dst = dst, + TaskId = $"{SimTaskPrefix}{Guid.NewGuid():N}" + }); + } + } + } + catch (Exception ex) + { + Diagnosis.Post($"[TransportMission] 模拟下发异常: {ExceptionFormatter.FormatEx(ex)}", "SimTransport", true); + } + + try { await Task.Delay(1000, token); } catch { /* cancelled */ } + } + } + + #endregion + /// /// 查看任务列表界面 /// 打开任务查看器窗口,显示所有任务的状态