From 1d69b31d87103e1925deaadec557157e0763b119 Mon Sep 17 00:00:00 2001 From: WenJY Date: Wed, 24 Jun 2026 11:42:12 +0800 Subject: [PATCH] =?UTF-8?q?change=20-=20AGV=E6=B5=81=E7=A8=8B=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=EF=BC=8C=E7=82=B9=E5=88=B0=E7=82=B9=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HikRoBotDispatchHub.cs | 56 ++- Sln.Wcs.HikRoBotServer/Program.cs | 31 +- Sln.Wcs.HoistDispatcher/HoistDispatchHub.cs | 53 ++- Sln.Wcs.HoistServer/Program.cs | 8 + Sln.Wcs.Strategy/MaterialInStoreExecutor.cs | 297 ++++++++++++- Sln.Wcs.UI/Converters/CodeToTextConverter.cs | 32 ++ .../ViewModels/Task/ManualTaskViewModel.cs | 43 +- TODO.md | 40 +- docs/MaterialInStoreExecutor-优化记录.md | 103 +++++ 任务执行调度-当前实现记录.md | 228 ---------- 任务驱动的任务调度方案.md | 170 -------- 接驳位驱动调度方案.md | 390 ------------------ 12 files changed, 597 insertions(+), 854 deletions(-) create mode 100644 Sln.Wcs.UI/Converters/CodeToTextConverter.cs create mode 100644 docs/MaterialInStoreExecutor-优化记录.md delete mode 100644 任务执行调度-当前实现记录.md delete mode 100644 任务驱动的任务调度方案.md delete mode 100644 接驳位驱动调度方案.md diff --git a/Sln.Wcs.HikRoBotDispatcher/HikRoBotDispatchHub.cs b/Sln.Wcs.HikRoBotDispatcher/HikRoBotDispatchHub.cs index dcb21df..dc845a5 100644 --- a/Sln.Wcs.HikRoBotDispatcher/HikRoBotDispatchHub.cs +++ b/Sln.Wcs.HikRoBotDispatcher/HikRoBotDispatchHub.cs @@ -23,12 +23,14 @@ #endregion << 版 本 注 释 >> +using Sln.Wcs.HikRoBotAdapter.Domain.Dto.GbContinueTask; using Sln.Wcs.HikRoBotAdapter.Domain.Dto.GbTaskSubmit; using Sln.Wcs.HikRoBotAdapter.Domain.Dto.QueryTask; using Sln.Wcs.HikRoBotAdapter.Enum; using Sln.Wcs.HikRoBotAdapter.Service; using Sln.Wcs.Model.Domain; using Sln.Wcs.Serilog; +using TargetRoute = Sln.Wcs.HikRoBotAdapter.Domain.Dto.GbTaskSubmit.TargetRoute; namespace Sln.Wcs.HikRoBotDispatcher; @@ -48,19 +50,67 @@ public class HikRoBotDispatchHub /// 接收调度任务=> 下发至AGVS /// /// - public void ReciveTask(LiveTaskDetail taskDetail) + public bool ReciveTask(LiveTaskDetail taskDetail) { + string startType = taskDetail.startPoint.Length > 12 ? "SITE" : "STORAGE"; + string endType = taskDetail.endPoint.Length > 12 ? "SITE" : "STORAGE"; GbTaskSubmitResultDto submitResultDto = _hikRobotAdapter.GbTaskSubmit(new GbTaskSubmitDto() { - + RobotTaskCode = taskDetail.taskCode, + TaskType = taskDetail.endPoint.Length > 12 ? "F07" : "PF-FMR-COMMON", + TargetRoute = new List() + { + new TargetRoute() + { + Type = startType, + Code = taskDetail.startPoint + }, + new TargetRoute() + { + Type = endType, + Code = taskDetail.endPoint + } + } + }); - if (submitResultDto.code == HikRoBotStatusEnum.成功) + if (submitResultDto.code == HikRoBotStatusEnum.成功 && submitResultDto.data.code == "SUCCESS") { _logger.Info($"调度任务{taskDetail.taskCode}下发成功"); + + return true; } else { _logger.Info($"调度任务{taskDetail.taskCode}下发失败:{submitResultDto.msg}"); + return false; + } + } + + /// + /// 继续执行任务=> 通知AGVS继续执行 + /// + /// + public bool ContinueTask(LiveTaskDetail taskDetail) + { + GbContinueTaskResultDto result = _hikRobotAdapter.GbContinueTask(new GbContinueTaskDto() + { + TriggerType = "TASK", + TriggerCode = taskDetail.taskCode, + TargetRoute = new HikRoBotAdapter.Domain.Dto.GbContinueTask.TargetRoute() + { + Type = "SITE", + Code = taskDetail.execDevice + } + }); + if (result.code == HikRoBotStatusEnum.成功) + { + _logger.Info($"调度任务{taskDetail.taskCode}继续执行成功"); + return true; + } + else + { + _logger.Info($"调度任务{taskDetail.taskCode}继续执行失败:{result.msg}"); + return false; } } diff --git a/Sln.Wcs.HikRoBotServer/Program.cs b/Sln.Wcs.HikRoBotServer/Program.cs index 45a3b79..d62a569 100644 --- a/Sln.Wcs.HikRoBotServer/Program.cs +++ b/Sln.Wcs.HikRoBotServer/Program.cs @@ -1,5 +1,6 @@ using System.Reflection; using Com.Ctrip.Framework.Apollo; +using Flurl.Http; using Sln.Wcs; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; @@ -19,6 +20,7 @@ using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; var builder = WebApplication.CreateBuilder(args); +builder.WebHost.ConfigureKestrel(o => o.ListenLocalhost(5200)); var basePath = AppContext.BaseDirectory; // ---- 配置 ---- @@ -30,8 +32,15 @@ var localConfig = new ConfigurationBuilder() var apolloConfigSection = localConfig.GetSection("apollo"); builder.Services.AddSingleton(localConfig); +FlurlHttp.ConfigureClientForUrl("https://172.16.12.11") + .ConfigureInnerHandler(handler => + { + handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; + }); + + var configProvider = new UpdateableConfigProvider(); -configProvider.Set("PLC参数", ""); +configProvider.Set("PLC参数", ""); var apolloConfig = new ConfigurationBuilder() .SetBasePath(basePath) @@ -92,8 +101,8 @@ api.MapPost("/task/receive", (ReceiveTaskRequest req) => taskCode = req.TaskCode, deviceType = 1, taskStatus = 1, startPoint = req.StartPoint, endPoint = req.EndPoint }; - hub.ReciveTask(detail); - return Results.Ok(new { success = true }); + bool res = hub.ReciveTask(detail); + return Results.Ok(new { success = true , isSuccess = res}); }); // Hub 方法: GetTaskStatus @@ -103,6 +112,19 @@ api.MapGet("/task/status", (string taskCode) => return Results.Ok(new { time = DateTime.Now, status = "ok" , taskStatus = taskStatus }); }); +// Hub 方法: ContinueTask +api.MapPost("/task/continue", (ContinueTaskRequest req) => +{ + var detail = new LiveTaskDetail + { + taskCode = req.TaskCode, deviceType = 1, taskStatus = 1, + startPoint = req.StartPoint, endPoint = req.EndPoint, + execDevice = req.ExecDevice + }; + bool res = hub.ContinueTask(detail); + return Results.Ok(new { success = true, isSuccess = res }); +}); + //0524779AA0550094 // AGV 等待点 // api.MapPost("/robot/wait", (HikRoBotWaitRequest req) => @@ -116,10 +138,11 @@ log.Info("HikRoBotServer 就绪: http://localhost:5200/swagger"); app.Run(); record ReceiveTaskRequest(string TaskCode, string StartPoint, string EndPoint); +record ContinueTaskRequest(string TaskCode, string StartPoint, string EndPoint, string ExecDevice); /// /// 请求参数 /// /// WCS 下发的任务编号:同 submit 接口 /// 提升机编号:15 栋入库-1#Hoist ;15栋出库-2#Hoist;14 栋提升机-3#Host;13 栋提升机-4#Hoist; -record HikRoBotWaitRequest(string TaskCode, string HoistCode); +//record HikRoBotWaitRequest(string TaskCode, string HoistCode); diff --git a/Sln.Wcs.HoistDispatcher/HoistDispatchHub.cs b/Sln.Wcs.HoistDispatcher/HoistDispatchHub.cs index e28a526..f29eaad 100644 --- a/Sln.Wcs.HoistDispatcher/HoistDispatchHub.cs +++ b/Sln.Wcs.HoistDispatcher/HoistDispatchHub.cs @@ -23,6 +23,7 @@ #endregion << 版 本 注 释 >> +using System.Collections.Concurrent; using Newtonsoft.Json; using Sln.Wcs.HoistAdapter.Domain.Dto.GetHoistStatus; using Sln.Wcs.HoistAdapter.Domain.Dto.HoistTaskExecutor; @@ -42,6 +43,8 @@ public class HoistDispatchHub { private readonly SerilogHelper _logger; private readonly IHoistService _hoistAdapter; + private static readonly ConcurrentDictionary HoistLocks = new(); + public HoistDispatchHub(IHoistService hoistAdapter, SerilogHelper logger) { _hoistAdapter = hoistAdapter; @@ -136,8 +139,6 @@ public class HoistDispatchHub int endPoint = Convert.ToInt32(deviceInfo.deviceSerialNo + startFloor + endFloor); - - //调用适配层下发 提升机调度任务 SetHoistTaskResultDto res = _hoistAdapter.SetHoistTask(new SetHoistTaskDto() { @@ -162,30 +163,44 @@ public class HoistDispatchHub } } + /// + /// 等待提升机任务完成 + /// + /// + /// + /// + public async Task WaitHoistComplete(string hostCode, int deviceSerialNo) + { + var lockObj = HoistLocks.GetOrAdd(hostCode, _ => new object()); + lock (lockObj) + { + RefreshDeviceParams(hostCode, out List deviceInfos); + var device = deviceInfos?.FirstOrDefault(info => info.deviceSerialNo == deviceSerialNo); + var paramValue = device?.deviceParams + .FirstOrDefault(item => item.paramName.Contains("任务执行完成")) + ?.paramValue; + + return paramValue != 1; + } + } + /// /// 获取空闲提升机 /// /// /// - public async Task GetFreeHoistAsync(string hostCode) + public async Task GetFreeHoistAsync(string hostCode) { - while (true) + var lockObj = HoistLocks.GetOrAdd(hostCode, _ => new object()); + lock (lockObj) { - lock (string.Empty) - { - RefreshDeviceParams(hostCode, out List deviceInfos); - - var freeHoist = deviceInfos?.FirstOrDefault(info => - info.deviceParams.Any(item => - item.paramName.Contains("提升机反馈任务状态") && item.paramValue == 0 - ) - ); - - if (freeHoist != null) - return freeHoist; - - Thread.Sleep(1000); - } + RefreshDeviceParams(hostCode, out List deviceInfos); + + return deviceInfos?.FirstOrDefault(info => + info.deviceParams.Any(item => + item.paramName.Contains("提升机反馈任务状态") && item.paramValue == 0 + ) + ); } } diff --git a/Sln.Wcs.HoistServer/Program.cs b/Sln.Wcs.HoistServer/Program.cs index b2b21b8..69a4a0e 100644 --- a/Sln.Wcs.HoistServer/Program.cs +++ b/Sln.Wcs.HoistServer/Program.cs @@ -18,6 +18,7 @@ using ZiggyCreatures.Caching.Fusion; using ZiggyCreatures.Caching.Fusion.Serialization.NewtonsoftJson; var builder = WebApplication.CreateBuilder(args); +builder.WebHost.ConfigureKestrel(o => o.ListenLocalhost(5100)); var basePath = AppContext.BaseDirectory; // ---- 配置 ---- @@ -147,6 +148,13 @@ api.MapGet("/hoist/free", async (string hostCode) => return Results.Ok(new { found = d != null, d?.deviceCode, d?.deviceName, d?.hostCode, d?.deviceSerialNo }); }); +// 5. WaitHoistComplete +api.MapGet("/hoist/wait-complete", async (string hostCode, int deviceSerialNo) => +{ + bool res = await hub.WaitHoistComplete(hostCode, deviceSerialNo); + return Results.Ok(new { success = true, isComplete = res }); +}); + api.MapGet("/health", () => Results.Ok(new { time = DateTime.Now, status = "ok" })); log.Info("HoistServer 就绪: http://localhost:5100/swagger"); diff --git a/Sln.Wcs.Strategy/MaterialInStoreExecutor.cs b/Sln.Wcs.Strategy/MaterialInStoreExecutor.cs index 698209c..a0a1bf9 100644 --- a/Sln.Wcs.Strategy/MaterialInStoreExecutor.cs +++ b/Sln.Wcs.Strategy/MaterialInStoreExecutor.cs @@ -1,4 +1,6 @@ using System.Collections.Concurrent; +using System.Net.Http; +using System.Net.Http.Json; using Sln.Wcs.Model.Domain; using Sln.Wcs.Repository.service; using Sln.Wcs.Serilog; @@ -118,25 +120,29 @@ public class MaterialInStoreExecutor lock (DbWriteLock) { task.taskStatus = 2; _taskQueueService.Update(task); } - foreach (var detail in task.taskDetails.OrderBy(d => d.objId)) + var sortedDetails = task.taskDetails.OrderBy(d => d.objId).ToList(); + for (int i = 0; i < sortedDetails.Count; i++) { if (ct.IsCancellationRequested) return; + var detail = sortedDetails[i]; + if (detail.taskStatus is 2 or 3) { - _logger.Info($" 明细 {detail.objId} 已处理,跳过"); + _logger.Info($" 明细 {detail.objId}-{detail.taskCode} 已处理,跳过"); continue; } lock (DbWriteLock) { detail.taskStatus = 2; _taskDetailService.Update(detail); } + var nextDetail = i + 1 < sortedDetails.Count ? sortedDetails[i + 1] : null; + var devName = detail.deviceType switch { 1 => "AGV", 2 => "提升机", _ => "输送线" }; _logger.Info($" → {devName}下发 {detail.taskCode}-{detail.objId}: {detail.startPoint} → {detail.endPoint}"); - // 模拟执行在锁外,多任务并行不受影响 var ok = detail.deviceType switch { - 1 => await DispatchAgvAsync(detail), + 1 => await DispatchAgvAsync(detail, nextDetail), 2 => await DispatchHoistAsync(detail), _ => await DispatchConveyorAsync(detail) }; @@ -144,12 +150,12 @@ public class MaterialInStoreExecutor if (ok) { lock (DbWriteLock) { detail.taskStatus = 3; _taskDetailService.Update(detail); } - _logger.Info($" ✓ {detail.objId} 完成"); + _logger.Info($" ✓ {detail.taskCode}-{detail.objId} 完成"); } else { lock (DbWriteLock) { detail.taskStatus = 1; _taskDetailService.Update(detail); } - _logger.Error($" ✗ {detail.objId} 失败,中断任务"); + _logger.Error($" ✗ {detail.taskCode}-{detail.objId} 失败,中断任务"); return; } } @@ -158,28 +164,253 @@ public class MaterialInStoreExecutor _logger.Info($"任务 {task.taskCode} 执行完成"); } - private async Task DispatchAgvAsync(LiveTaskDetail detail) + private async Task DispatchAgvAsync(LiveTaskDetail detail, LiveTaskDetail? nextDetail = null) { - // AGV 不限并发,直接下发 - _logger.Info($" [AGV] 开始 {detail.startPoint} → {detail.endPoint}"); - await Task.Delay(10_000); // 模拟执行 10s - _logger.Info($" [AGV] 完成 {detail.startPoint} → {detail.endPoint}"); - return true; + const string agvBaseUrl = "http://localhost:5200"; + const string hoistBaseUrl = "http://localhost:5100"; + const int pollIntervalMs = 2000; + + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(30) }; + + _logger.Info($" [AGV] 下发 {detail.taskCode}-{detail.objId}: {detail.startPoint} → {detail.endPoint}"); + + // 1. 调用 /task/receive 下发 AGV 任务 + try + { + string[] startPointArray = detail.startPoint.Split("_"); + string[] endPointArray = detail.endPoint.Split("_"); + var res = await httpClient.PostAsJsonAsync($"{agvBaseUrl}/api/task/receive", + new { taskCode = detail.taskCode, startPoint = startPointArray[2], endPoint = endPointArray[2] }); + res.EnsureSuccessStatusCode(); + var receiveResult = await res.Content.ReadFromJsonAsync(); + if (receiveResult is not { isSuccess: true }) + { + _logger.Error($" [AGV] {detail.taskCode} 下发失败: AGV 调度中心拒绝任务"); + return false; + } + } + catch (Exception ex) + { + _logger.Error($" [AGV] {detail.taskCode} 下发失败: {ex.Message}"); + return false; + } + + _logger.Info($" [AGV] {detail.taskCode} 下发成功,开始轮询任务状态..."); + + // 2. 循环调用 /task/status 获取 AGV 任务状态 + var hoistDispatched = false; + while (true) + { + await Task.Delay(pollIntervalMs); + + try + { + var statusRes = await httpClient.GetFromJsonAsync( + $"{agvBaseUrl}/api/task/status?taskCode={Uri.EscapeDataString(detail.taskCode!)}"); + var taskStatus = statusRes?.taskStatus ?? string.Empty; + + _logger.Info($" [AGV] {detail.taskCode} 任务状态: {taskStatus}"); + + switch (taskStatus) + { + case "FINISHED": + case "MANUALED": + _logger.Info($" [AGV] {detail.taskCode} 完成 {detail.startPoint} → {detail.endPoint}"); + return true; + case "CANCELLED": + _logger.Error($" [AGV] {detail.taskCode} 任务已取消 {detail.startPoint} → {detail.endPoint}"); + return false; + case "WAIT": + _logger.Info($" [AGV] {detail.taskCode} 等待 {detail.startPoint} → {detail.endPoint}"); + if (!hoistDispatched && nextDetail is { deviceType: 2 }) + { + hoistDispatched = await TryDispatchHoistForWaitAsync(httpClient, hoistBaseUrl, nextDetail); + if (hoistDispatched) + await ContinueAgvAsync(httpClient, agvBaseUrl, nextDetail); + } + break; + } + } + catch (Exception ex) + { + _logger.Error($" [AGV] 状态查询异常: {ex.Message}"); + } + } + } + + /// AGV 等待时筛选空闲提升机并下发提升机任务 + private async Task TryDispatchHoistForWaitAsync(HttpClient httpClient, string hoistBaseUrl, LiveTaskDetail detail) + { + var hostCode = DetermineHoistCode(detail); + _logger.Info($" [AGV] 任务等待中,筛选空闲提升机 ({hostCode})..."); + + try + { + FreeHoistResponse? freeRes; + while (true) + { + freeRes = await httpClient.GetFromJsonAsync( + $"{hoistBaseUrl}/api/hoist/free?hostCode={Uri.EscapeDataString(hostCode!)}"); + + if (freeRes is { found: true }) + break; + + _logger.Info($" [AGV] 暂无空闲提升机 ({hostCode}),等待重试..."); + await Task.Delay(1000); + } + + var dispatchRes = await httpClient.PostAsJsonAsync($"{hoistBaseUrl}/api/task/dispatch", + new + { + hostCode = freeRes.hostCode, + serialNo = freeRes.deviceSerialNo, + taskCode = detail.taskCode, + startPoint = detail.startPoint, + endPoint = detail.endPoint + }); + + var result = await dispatchRes.Content.ReadFromJsonAsync(); + if (result is not { success: true }) + { + _logger.Error($" [AGV] 提升机下发失败: {result?.msg ?? "未知错误"}"); + return false; + } + + var execDevice = $"{freeRes.hostCode}_{freeRes.deviceSerialNo}"; + _logger.Info($" [AGV] 提升机 {execDevice} 已下发"); + + lock (DbWriteLock) + { + detail.execDevice = execDevice; + _taskDetailService.Update(detail); + } + + return true; + } + catch (Exception ex) + { + _logger.Error($" [AGV] 提升机调度失败: {ex.Message}"); + return false; + } + } + + /// 通知 AGV 调度中心继续执行 + private async Task ContinueAgvAsync(HttpClient httpClient, string agvBaseUrl, LiveTaskDetail hoistDetail) + { + try + { + var res = await httpClient.PostAsJsonAsync($"{agvBaseUrl}/api/task/continue", + new + { + taskCode = hoistDetail.taskCode, + startPoint = hoistDetail.startPoint, + endPoint = hoistDetail.endPoint, + execDevice = hoistDetail.execDevice + }); + res.EnsureSuccessStatusCode(); + + var result = await res.Content.ReadFromJsonAsync(); + if (result is { isSuccess: true }) + _logger.Info($" [AGV] 继续执行已下发 (提升机 {hoistDetail.execDevice})"); + else + _logger.Error($" [AGV] 继续执行下发失败: AGV 调度中心拒绝"); + } + catch (Exception ex) + { + _logger.Error($" [AGV] 继续执行下发异常: {ex.Message}"); + } + } + + /// 根据 startPoint 确定提升机编号:15栋入库→1#Host, 15栋出库→2#Host, 14栋→3#Host, 13栋→4#Host + private static string DetermineHoistCode(LiveTaskDetail detail) + { + var pt = detail.startPoint ?? ""; + + if (detail.taskType == 2 && detail.taskCategory == 2 && pt.Contains("15#")) + return "2#Host"; + + if (pt.Contains("13#")) return "4#Host"; + if (pt.Contains("14#")) return "3#Host"; + if (pt.Contains("15#")) return "1#Host"; + return "1#Host"; } private async Task DispatchHoistAsync(LiveTaskDetail detail) { - // 每栋楼提升机限 2 台并发 + const string hoistBaseUrl = "http://localhost:5100"; + var building = ExtractBuilding(detail.startPoint); var semaphore = HoistSemaphores.GetOrAdd(building, _ => new SemaphoreSlim(2, 2)); await semaphore.WaitAsync(); try { - _logger.Info($" [提升机] {building}#楼 开始 (可用:{semaphore.CurrentCount}) {detail.startPoint} → {detail.endPoint}"); - await Task.Delay(20_000); // 模拟执行 20s - _logger.Info($" [提升机] {building}#楼 完成 {detail.startPoint} → {detail.endPoint}"); - return true; + if (string.IsNullOrWhiteSpace(detail.execDevice)) + { + _logger.Error($" [提升机] {building}#楼 未分配执行设备"); + return false; + } + + var parts = detail.execDevice.Split('_'); + if (parts.Length != 2 || !int.TryParse(parts[1], out var serialNo)) + { + _logger.Error($" [提升机] {building}#楼 设备编号格式错误: {detail.execDevice}"); + return false; + } + var hostCode = parts[0]; + + _logger.Info($" [提升机] {building}#楼 启动 {detail.startPoint} → {detail.endPoint} (设备:{detail.execDevice})"); + + using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + var res = await httpClient.PostAsJsonAsync($"{hoistBaseUrl}/api/hoist/receive-pallet", + new + { + hostCode = hostCode, + serialNo = serialNo, + taskCode = detail.taskCode, + palletBarcode = detail.palletBarcode ?? "", + startPoint = detail.startPoint, + endPoint = detail.endPoint + }); + + var result = await res.Content.ReadFromJsonAsync(); + if (result is not { success: true }) + { + _logger.Error($" [提升机] {building}#楼 启动失败: {result?.msg ?? "未知错误"}"); + return false; + } + + _logger.Info($" [提升机] {building}#楼 已启动 {detail.startPoint} → {detail.endPoint}"); + + lock (DbWriteLock) + { + detail.taskStatus = 2; + detail.execDevice = $"{hostCode}_{serialNo}"; + _taskDetailService.Update(detail); + } + + // 轮询等待提升机任务完成 + _logger.Info($" [提升机] {building}#楼 等待任务完成..."); + using var waitClient = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; + while (true) + { + var waitRes = await waitClient.GetFromJsonAsync( + $"{hoistBaseUrl}/api/hoist/wait-complete?hostCode={Uri.EscapeDataString(hostCode)}&deviceSerialNo={serialNo}"); + + if (waitRes is { isComplete: true }) + { + _logger.Info($" [提升机] {building}#楼 任务完成 {detail.startPoint} → {detail.endPoint}"); + + return true; + } + + await Task.Delay(2000); + } + } + catch (Exception ex) + { + _logger.Error($" [提升机] {building}#楼 异常: {ex.Message}"); + return false; } finally { @@ -199,4 +430,36 @@ public class MaterialInStoreExecutor var match = System.Text.RegularExpressions.Regex.Match(pointCode, @"^(\d+)"); return match.Success ? int.Parse(match.Groups[1].Value) : 0; } + + // ---- API 响应 DTO ---- + + private class AGVTaskReceiveResponse + { + public bool isSuccess { get; set; } = false; + } + + private class AGVTaskStatusResponse + { + public string taskStatus { get; set; } = string.Empty; + } + + private class FreeHoistResponse + { + public bool found { get; set; } + public string? deviceCode { get; set; } + public string? deviceName { get; set; } + public string? hostCode { get; set; } + public int deviceSerialNo { get; set; } + } + + private class WaitCompleteResponse + { + public bool isComplete { get; set; } + } + + private class ApiResult + { + public bool success { get; set; } + public string? msg { get; set; } + } } diff --git a/Sln.Wcs.UI/Converters/CodeToTextConverter.cs b/Sln.Wcs.UI/Converters/CodeToTextConverter.cs new file mode 100644 index 0000000..a9f3407 --- /dev/null +++ b/Sln.Wcs.UI/Converters/CodeToTextConverter.cs @@ -0,0 +1,32 @@ +using System; +using System.Globalization; +using Avalonia.Data.Converters; + +namespace Sln.Wcs.UI.Converters; + +public class CodeToTextConverter : IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is null) return "--"; + var code = System.Convert.ToInt32(value); + var category = parameter as string; + + return category switch + { + "TaskType" => code switch { 1 => "入库", 2 => "出库", _ => "--" }, + "TaskCategory" => code switch { 1 => "包材", 2 => "成品", 3 => "托盘", _ => "--" }, + "TaskStatus" => code switch { 1 => "待执行", 2 => "执行中", 3 => "已完成", _ => "--" }, + "ExecutionMode" => code switch { 0 => "自动", 1 => "手动", _ => "--" }, + "DeviceType" => code switch { 0 => "输送线", 1 => "AGV", 2 => "提升机", _ => "--" }, + "DeviceStatus" => code switch { 0 => "正常", 1 => "在忙", 2 => "异常", _ => "--" }, + "IsFlag" => code switch { 0 => "否", 1 => "是", _ => "--" }, + "OperationType" => code switch { 0 => "默认读写", 1 => "只读", 2 => "只写", _ => "--" }, + "LocationStatus" => code switch { 0 => "未使用", 1 => "已使用", 2 => "锁库", 3 => "异常", _ => "--" }, + _ => code.ToString() + }; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + => throw new NotSupportedException(); +} diff --git a/Sln.Wcs.UI/ViewModels/Task/ManualTaskViewModel.cs b/Sln.Wcs.UI/ViewModels/Task/ManualTaskViewModel.cs index b2836b7..a8dc340 100644 --- a/Sln.Wcs.UI/ViewModels/Task/ManualTaskViewModel.cs +++ b/Sln.Wcs.UI/ViewModels/Task/ManualTaskViewModel.cs @@ -185,14 +185,17 @@ public partial class ManualTaskViewModel : ObservableObject } } var freeUrl = $"{HoistBaseUrl}/api/hoist/free?hostCode={Uri.EscapeDataString(hostCode)}"; - var freeRes = await _http.GetFromJsonAsync(freeUrl); - - if (freeRes is not { found: true }) + FreeHoistResponse? freeRes; + while (true) { - StatusText = "暂无空闲提升机,请稍后重新获取"; + freeRes = await _http.GetFromJsonAsync(freeUrl); + + if (freeRes is { found: true }) + break; + + StatusText = "暂无空闲提升机,等待中..."; HoistInfo = "忙碌"; - _logger.Info($"手动任务 {SelectedTask.taskCode} 获取空闲提升机失败:无空闲设备"); - return; + await System.Threading.Tasks.Task.Delay(1000); } StatusText = $"已获取空闲提升机 {freeRes.deviceName ?? freeRes.deviceCode},正在下发任务..."; @@ -332,7 +335,28 @@ public partial class ManualTaskViewModel : ObservableObject }); _logger.Info($"手动任务 {SelectedTask.taskCode} 物料到位确认,已调用 receive-pallet"); - StatusText = $"任务 {SelectedTask.taskCode} 物料已到位,提升机已启动"; + StatusText = $"任务 {SelectedTask.taskCode} 物料已到位,提升机已启动,等待完成..."; + + // 轮询等待提升机任务完成 + var waitUrl = $"{HoistBaseUrl}/api/hoist/wait-complete?hostCode={Uri.EscapeDataString(execHostCode)}&deviceSerialNo={execSerialNo}"; + while (true) + { + var waitRes = await _http.GetFromJsonAsync(waitUrl); + + if (waitRes is { isComplete: true }) + break; + + await System.Threading.Tasks.Task.Delay(2000); + } + + await System.Threading.Tasks.Task.Run(() => + { + hoistDetail.taskStatus = 3; + _taskDetailService.Update(hoistDetail); + }); + + StatusText = $"任务 {SelectedTask.taskCode} 提升机已完成"; + _logger.Info($"手动任务 {SelectedTask.taskCode} 提升机任务完成"); LoadDetails(SelectedTask.taskCode); } catch (HttpRequestException ex) @@ -362,6 +386,11 @@ public partial class ManualTaskViewModel : ObservableObject public int deviceSerialNo { get; set; } } + private class WaitCompleteResponse + { + public bool isComplete { get; set; } + } + private class ApiResult { public bool success { get; set; } diff --git a/TODO.md b/TODO.md index 7f85de4..c3f7b37 100644 --- a/TODO.md +++ b/TODO.md @@ -2,31 +2,39 @@ ## 已完成 -- [x] HoistServer (端口 5100) — 提升机调度 API,Swagger `/swagger`,4 个 Hub 方法 -- [x] HikRoBotServer (端口 5200) — AGV 调度 API,Swagger `/swagger` -- [x] UI 引擎启停控制 — 系统监控页分别启动/停止两个服务器进程 -- [x] 码垛机 HMI 页面 — 静态界面,手动操作区带开关按钮 -- [x] 删除确认弹窗 — 所有删除操作弹出 ConfirmDialog -- [x] 浅色/深色主题 — 一键切换,全视图适配 +- [✅] HoistServer (端口 5100) — 提升机调度 API,Swagger `/swagger`,4 个 Hub 方法 +- [✅] HikRoBotServer (端口 5200) — AGV 调度 API,Swagger `/swagger` +- [✅] UI 引擎启停控制 — 系统监控页分别启动/停止两个服务器进程 +- [✅] 码垛机 HMI 页面 — 静态界面,手动操作区带开关按钮 +- [✅] 删除确认弹窗 — 所有删除操作弹出 ConfirmDialog +- [✅] 浅色/深色主题 — 一键切换,全视图适配 ## 待完成 ### 1.调度逻辑修改 -- [ ] 任务明细与操作设备关联:提升机-获取到空闲设备后将确定分配任务的提升机编号存入 - -- [ ] HoistDispatcher、HikRoBotDispatcher任务下发后需要监控任务执行情况,任务执行完成后,才可以返回ok给Strategy -- [ ] AGV 任务下发后开始获取任务状态(调用/task/status),只有状态为FINISHED/MANUALED完成或手动完成时才可以进行后序任务,CANCELLED时终端调度任务后序也不执行 - 等待步骤执行WAIT => 筛选提升机、下发提升机任务“参考手动任务执行界面的任务执行事件逻辑” +- [✅] 任务明细与操作设备关联:提升机-获取到空闲设备后将确定分配任务的提升机编号存入 +- [✅] HoistDispatcher、HikRoBotDispatcher任务下发后需要监控任务执行情况,任务执行完成后,才可以返回ok给Strategy +- [✅] AGV 任务下发后开始获取任务状态(调用/task/status),只有状态为FINISHED/MANUALED完成或手动完成时才可以进行后序任务,CANCELLED时终端调度任务后序也不执行 + WAIT => 等待步骤执行:筛选提升机、下发提升机任务“参考手动任务执行界面的任务执行事件逻辑” FINISHED/MANUALED => 完成/手动完成,可以进行后序任务 CANCELLED => 任务取消,终止调度任务 -- [ ] Strategy执行到提升机任务时调用提升机调度中心的接收物料接口“receive-pallet”,验证物料并下发提升机启动信号 - - +- [] Strategy执行到提升机任务时调用提升机调度中心的接收物料接口“receive-pallet”,验证物料并下发提升机启动信号 +- [] WCS 任务起始点转换为AGV 定位码:提升机接驳位(根据楼层+提升机编号判断)、AGV 等待点(楼层/15 栋判断出入库提升机) ### 任务创建修改 -- [ ] 添加手动放、手动取操作,如果是手动放,任务从提升机开始,手动取也是以提升机结束 -- [ ] 手动放:任务执行则下发提升机任务 +- [✅] 添加手动放、手动取操作,如果是手动放,任务从提升机开始,手动取也是以提升机结束 +- [✅] 手动放:任务执行则下发提升机任务 + +### MaterialInStoreExecutor 优化 +- [ ] ExecuteAsync + Thread.Join 阻塞主循环(`:92-114`)—— 改为 Task.Run + WhenAll,独立轮询不互相阻塞 +- [ ] AGV 状态轮询加超时保护(`:200`)—— 避免 AGV 无终态时线程永久卡死 +- [ ] 空闲提升机轮询加超时保护(`:248`)—— 避免 PLC 离线时永久卡死 +- [ ] 等待提升机完成轮询加超时保护(`:393`)—— 同上 +- [ ] DbWriteLock 全局锁优化(`:24`)—— 改为独立 Scope/Context 避免并发写阻塞 +- [ ] new Thread + GetAwaiter 改 Task.Run(`:92-111`)—— 减少线程浪费 +- [ ] HttpClient 复用(`:173,362,392`)—— 使用 IHttpClientFactory 或静态实例 +- [ ] DispatchConveyorAsync 补充输送线逻辑(`:419-422`) ### 2. 任务执行引擎(分组并行方案) - [ ] 包材入库逻辑测试 diff --git a/docs/MaterialInStoreExecutor-优化记录.md b/docs/MaterialInStoreExecutor-优化记录.md new file mode 100644 index 0000000..41c354c --- /dev/null +++ b/docs/MaterialInStoreExecutor-优化记录.md @@ -0,0 +1,103 @@ +# MaterialInStoreExecutor 优化记录 + +## 调度流程 + +``` +RunLoopAsync (每 5 秒轮询 DB) + ExecuteAsync + 查 DB: taskType=1, taskCategory=1, taskStatus=1 的待执行任务 + 每个任务启动一个 Thread(限 10 并发,TaskSemaphore) + ProcessOneAsync + 任务状态 → 2 + 按 objId 串行遍历明细: + ├─ AGV → DispatchAgvAsync + │ 1. POST /task/receive 下发 + │ 2. 轮询 GET /task/status 每 2s + │ FINISHED/MANUALED → 完成 + │ CANCELLED → 失败中断 + │ WAIT → 下一明细是提升机? + │ → 轮询 GET /hoist/free 空闲提升机(每 1s) + │ → POST /task/dispatch 下发提升机 + │ → POST /task/continue 通知 AGV 继续 + ├─ 提升机 → DispatchHoistAsync + │ 获取楼栋信号量(限 2 并发) + │ → POST /hoist/receive-pallet 启动 + │ → 轮询 GET /hoist/wait-complete(每 2s) + │ → 完成 + └─ 输送线 → DispatchConveyorAsync(目前直接返回 true) + 成功 → detail.taskStatus = 3,继续下一条 + 失败 → detail.taskStatus = 1,中断整个任务 + Thread.Join() — 等全部线程结束后才进入下一轮 DB 轮询 +``` + +## 待优化项 + +### 1. [高] ExecuteAsync + Thread.Join 阻塞主循环 (`:92-114`) + +**现象**:所有任务线程全部 `Join` 完成后才进入下一次 5 秒轮询。如果一个任务执行 10 分钟,期间新入库的任务完全不被调度。 + +**建议**:去掉 `Thread.Join`,`RunLoopAsync` 独立轮询,任务用 `Task.Run` 异步执行,互不阻塞。 + +--- + +### 2. [高] AGV 状态轮询无超时保护 (`:200`) + +**现象**:`while (true)` 无限循环,AGV 永远不返回终态(FINISHED/MANUALED/CANCELLED)时线程永久卡死。 + +**建议**:加超时时间或最大重试次数。 + +--- + +### 3. [高] 空闲提升机轮询无超时 (`:248`) + +**现象**:PLC 离线或提升机全忙时永远卡住。 + +**建议**:加超时,超时后返回失败。 + +--- + +### 4. [高] 等待提升机完成轮询无超时 (`:393`) + +**现象**:同上。 + +**建议**:加超时。 + +--- + +### 5. [中] DbWriteLock 全局瓶颈 (`:24`) + +**现象**:所有 DB 写操作共享同一把锁,10 个并发任务写状态时互相排队。 + +**建议**:改为每次 DB 操作用独立 Scope/Context,或使用 SqlSugar 的异步方法避免上下文冲突。 + +--- + +### 6. [中] new Thread + .GetAwaiter().GetResult() 浪费线程 (`:92-111`) + +**现象**:每个任务消耗一个专用线程,内部全是 async/await,线程大部分时间在空等。绕过 `CancellationToken` 的异步传播。 + +**建议**:改为 `Task.Run` + `await Task.WhenAll`。 + +--- + +### 7. [低] HttpClient 重复创建 (`:173, 362, 392`) + +**现象**:每次 HTTP 调用 `new HttpClient`,频繁创建可能导致 socket 耗尽。 + +**建议**:使用 `IHttpClientFactory` 或类级别静态 `HttpClient` 复用。 + +--- + +### 8. [低] DispatchConveyorAsync 空操作 (`:419-422`) + +**现象**:输送线直接返回 `true`,逻辑未实现。 + +**建议**:待后续补充输送线调度逻辑。 + +--- + +## 优化记录 + +| 日期 | 内容 | +|------|------| +| 2026-06-23 | 初版记录,待测试后优化 | diff --git a/任务执行调度-当前实现记录.md b/任务执行调度-当前实现记录.md deleted file mode 100644 index 758c43c..0000000 --- a/任务执行调度-当前实现记录.md +++ /dev/null @@ -1,228 +0,0 @@ -# 任务执行调度 — 当前实现记录 - -> 记录时间:2026-06-13 -> 状态:包材入库 (category=1, type=1) 任务驱动并行执行已跑通 - ---- - -## 一、整体架构 - -``` -UI (SystemMonitorView) - ├─ [▶ 启动] / [■ 停止] 包材入库调度 - └─ SystemMonitorViewModel - └─ MaterialInStoreExecutor (Singleton) - -Sln.Wcs.Strategy / MaterialInStoreExecutor - ├─ 轮询待执行任务 (每 5 秒) - ├─ 多任务并行 (Thread,最多 10 个) - ├─ 单任务内串行 (逐条 detail) - └─ 设备下发 (AGV/提升机/输送线) - -Sln.Wcs.Business / TaskCreateService - └─ 拓扑路由 → 自动生成 LiveTaskQueue + List -``` - -## 二、任务创建 - -### 输入 - -| 参数 | 说明 | 示例 | -|------|------|------| -| startBuilding | 起始楼栋 | 13 | -| startFloor | 起始楼层 | 1 | -| startLocation | 起始位置号 | 101 | -| endBuilding | 目标楼栋 | 15 | -| endFloor | 目标楼层 | 3 | -| endLocation | 目标位置号 | 102 | -| taskType | 1入库 / 2出库 | 1 | -| taskCategory | 1包材 / 2成品 / 3托盘 | 1 | -| palletBarcode | 托盘条码 | PALLET001 | -| materialCode | 物料编码 | M001 | - -### 输出 - -``` -LiveTaskQueue - ├─ taskCode: 自动生成 (yyyyMMddHHmmss + 随机数) - ├─ taskType, taskCategory - ├─ startPoint, endPoint: 自动拼接为 {building}#_L{floor}_{location} - ├─ taskStatus: 1 (待执行) - ├─ taskSteps: 明细数量 - └─ taskDetails: List -``` - -### 拓扑规则 - -- 所有楼栋 13# / 14# / 15# 均为 1~5F -- 每栋楼有 1 台提升机,可直达任意楼层(全连接) -- 跨栋仅 2F 连通: - - (13,2) ↔ (14,2):廊桥交接,两段 AGV(13# AGV → BRIDGE_13_14,14# AGV 从 BRIDGE_13_14 接走) - - (14,2) ↔ (15,2):14# AGV 可驶入 15#,不生成过渡段 - -### 示例:13栋1楼101 → 15栋3楼102 - -``` -路由节点: (13,1) → (13,2) → (14,2) → (15,2) → (15,3) - -Detail 1: AGV 13#_L1_101 → 13#_L1_HOIST -Detail 2: 提升机 13#_L1_HOIST → 13#_L2_HOIST -Detail 3: AGV 13#_L2_HOIST → BRIDGE_13_14 (13# AGV) -Detail 4: AGV BRIDGE_13_14 → 15#_L2_HOIST (14# AGV 直达) -Detail 5: 提升机 15#_L2_HOIST → 15#_L3_HOIST -Detail 6: AGV 15#_L3_HOIST → 15#_L3_102 -``` - -## 三、任务执行 - -### 执行器:MaterialInStoreExecutor - -**位置**:`Sln.Wcs.Strategy/MaterialInStoreExecutor.cs` - -**查询条件**: -```csharp -taskType == 1 && taskCategory == 1 && taskStatus == 1 -``` - -**轮询间隔**:5 秒 - -### 主循环 - -``` -Start() - └─ Task.Run → RunLoopAsync - └─ while (!cancelled): - ExecuteAsync() ← 阻塞等待所有任务线程完成 - await Task.Delay(5000) -``` - -### 并行策略 - -``` -ExecuteAsync: - 查询所有待执行任务 - → foreach task → new Thread: - TaskSemaphore.Wait() ← 最多 10 线程 - ProcessOneAsync(task).GetAwaiter().GetResult() - TaskSemaphore.Release() - → Thread.Join() 等待全部完成 -``` - -### 单任务处理 - -``` -ProcessOneAsync(task): - 🔒 lock → taskStatus = 2, Update(task) - - foreach detail (按 objId 排序): - if detail 已完成 → continue - - 🔒 lock → detailStatus = 2, Update(detail) - 🔓 - 下发设备 (根据 deviceType): - 1 → DispatchAgvAsync (10s 模拟延迟) - 2 → DispatchHoistAsync (20s 模拟延迟,按楼栋限 2 并发) - 0 → DispatchConveyorAsync - 🔒 lock → detailStatus = 3, Update(detail) - 🔓 - - 🔒 lock → taskStatus = 3, Update(task) -``` - -### 并发控制 - -| 控制点 | 机制 | 值 | -|--------|------|-----| -| 任务级并发 | `SemaphoreSlim` | 最多 10 个 | -| 提升机并发 | `ConcurrentDictionary` | 每栋楼 2 台 | -| AGV 并发 | 不限 | — | -| DB 写入 | `lock (DbWriteLock)` 串行化 | — | -| 启停状态 | `lock (_lock)` + `volatile bool` | — | - -### 线程安全 - -- **启停**:`lock (_lock)` 保护 `_cts` / `_isRunning` -- **DB 写入**:`lock (DbWriteLock)` 保护 SqlSugar Update(Context 为单例,多线程写入冲突) -- **提升机信号量**:`ConcurrentDictionary` 线程安全 -- **任务信号量**:`SemaphoreSlim` 本身线程安全 -- **延迟模拟**:`await Task.Delay` 在锁外,多任务并行不受影响 - -## 四、仓储线程安全 - -**位置**:`Sln.Wcs.Repository/Repository.cs` - -```csharp -public Repository(ISqlSugarClient db) -{ - itenant = db.AsTenant(); - Context = (SqlSugarClient)db; // 直接用 SqlSugarScope,内部管理连接池 -} -``` - -`SqlSugarScope` 注册为 Singleton,通过 `[Tenant]` 特性自动路由数据库。 - -## 五、设备下发(当前为模拟) - -| 设备 | 方法 | 延迟 | 并发限制 | -|------|------|------|---------| -| AGV | `DispatchAgvAsync` | 10 秒 | 无 | -| 提升机 | `DispatchHoistAsync` | 20 秒 | 每栋楼 2 台 | -| 输送线 | `DispatchConveyorAsync` | 无 | 无 | - -后续对接:AGV → `HikRoBotDispatchHub.ReciveTask`,提升机 → `HoistDispatchHub.TaskDispatch` - -## 六、UI 控制 - -**系统监控页面** — 顶部卡片: - -``` -┌──────────────────────────────────────────────┐ -│ ● 包材入库调度 运行中 [▶ 启动] [■ 停止] │ -└──────────────────────────────────────────────┘ -``` - -按钮绑定: -- 启动 → `MaterialInStoreExecutor.Start()` -- 停止 → `MaterialInStoreExecutor.Stop()` -- 状态指示灯:绿色 = 运行中,红色 = 已停止 - -## 七、DI 注册 - -**App.axaml.cs**: - -```csharp -// Assembly 扫描 -Assembly.LoadFrom("Sln.Wcs.Strategy.dll") - -// Singleton 注册 -services.AddSingleton(); -``` - -**依赖关系**: -``` -MaterialInStoreExecutor (Singleton) - ├─ SerilogHelper (Singleton) - ├─ ILiveTaskQueueService (Transient, 来自 Scrutor 扫描) - └─ ILiveTaskDetailService (Transient, 来自 Scrutor 扫描) -``` - -## 八、关键文件 - -| 文件 | 说明 | -|------|------| -| `Sln.Wcs.Business/TaskCreateService.cs` | 拓扑路由 + 任务创建 | -| `Sln.Wcs.Strategy/MaterialInStoreExecutor.cs` | 包材入库执行器 | -| `Sln.Wcs.Repository/Repository.cs` | 仓储基类(线程安全改造) | -| `Sln.Wcs.UI/ViewModels/SystemMonitorViewModel.cs` | 系统监控 VM(启停控制) | -| `Sln.Wcs.UI/Views/SystemMonitorView.axaml` | 系统监控页面 | -| `Sln.Wcs.UI/App.axaml.cs` | DI 注册 | -| `接驳位驱动调度方案.md` | 调度方案设计文档 | - -## 九、待优化项 - -1. **真实设备对接**:替换 `DispatchAgvAsync` / `DispatchHoistAsync` 中的模拟延迟为实际 API 调用 -2. **扩展其他类型**:成品入库/出库、托盘入库/出库等(参考 MaterialInStoreExecutor 模板) -3. **DB 写入优化**:去掉 `lock (DbWriteLock)`,从根本上解决 SqlSugar 多线程写入问题(可能需要连接串配置 `Max Pool Size` 或改造仓储层) -4. **任务优先级**:当前 FIFO,可按优先级排序 -5. **异常恢复**:任务中途停止后重新启动的状态恢复 -6. **手动触发**:UI 上支持手动输入接驳位 + RFID 触发单条明细执行 diff --git a/任务驱动的任务调度方案.md b/任务驱动的任务调度方案.md deleted file mode 100644 index 7079192..0000000 --- a/任务驱动的任务调度方案.md +++ /dev/null @@ -1,170 +0,0 @@ -# 任务驱动的任务调度方案 - -## 一、核心思路 - -``` -不是一次性下发全部步骤,也不是等待接驳位信号 - -任务创建后 (status=1) - → 执行器轮询到 → 启动线程 - → 单任务内逐条串行下发明细 - → 前一条完成 → 自动执行下一条 - → 全部完成 → 任务结束 - -多任务之间并行执行,互不干扰 -``` - -## 二、执行流程 - -``` -Start() 启动后台轮询 - │ - └─ RunLoopAsync (每 5 秒一轮) - │ - └─ ExecuteAsync - │ - ├─ 查询 taskType=1, taskCategory=1, taskStatus=1 - │ - ├─ foreach task → new Thread: - │ │ - │ ├─ TaskSemaphore.Wait() ← 最多 10 线程 - │ │ - │ └─ ProcessOneAsync(task): - │ │ - │ ├─ taskStatus → 2 - │ │ - │ ├─ foreach detail (按 objId): - │ │ │ - │ │ ├─ detailStatus → 2 - │ │ ├─ 下发设备 - │ │ │ 1 → AGV (不限并发) - │ │ │ 2 → 提升机 (每栋限 2) - │ │ │ 0 → 输送线 - │ │ ├─ 等待执行完成 - │ │ └─ detailStatus → 3 - │ │ - │ └─ taskStatus → 3 - │ - └─ Thread.Join() 等本轮全部完成 → 5 秒后下一轮 -``` - -## 三、并发模型 - -``` -任务 A: Detail 1 → Detail 2 → Detail 3 → ... -任务 B: Detail 1 → Detail 2 → Detail 3 → ... 三个任务同时跑 -任务 C: Detail 1 → Detail 2 → Detail 3 → ... - -单任务内: 串行 (前一条完成才执行下一条) -多任务间: 并行 (各自独立线程) -``` - -### 限制 - -| 控制项 | 机制 | 值 | -|--------|------|-----| -| 最大并行任务数 | `SemaphoreSlim` | 10 | -| 提升机并行/栋 | `ConcurrentDictionary` | 2 | -| AGV 并行 | 无限制 | — | -| DB 写入 | `lock (DbWriteLock)` | 串行 | - -## 四、单任务处理详情 - -``` -ProcessOneAsync(task): - - Step 1 ─ 标记任务执行中 - lock (DbWriteLock): - task.taskStatus = 2 - Update(task) - - Step 2 ─ 逐条处理明细 (按 objId 排序) - for each detail: - - 跳过已完成 (status 2 or 3) - - lock (DbWriteLock): - detail.taskStatus = 2 - Update(detail) - - ↓ 下发设备 (锁外,可并行) - deviceType: - 1 → DispatchAgvAsync AGV 模拟 10s - 2 → DispatchHoistAsync 提升机模拟 20s - 0 → DispatchConveyorAsync 输送线 - - 成功: - lock (DbWriteLock): - detail.taskStatus = 3 - Update(detail) - - 失败: - lock (DbWriteLock): - detail.taskStatus = 1 回退待重试 - Update(detail) - return (中断任务) - - Step 3 ─ 标记任务完成 - lock (DbWriteLock): - task.taskStatus = 3 - Update(task) -``` - -## 五、设备下发 - -| 设备类型 | deviceType | 方法 | 模拟延迟 | 并发控制 | -|---------|-----------|------|---------|---------| -| AGV | 1 | `DispatchAgvAsync` | 10s | 无限制 | -| 提升机 | 2 | `DispatchHoistAsync` | 20s | 每栋 2 台 | -| 输送线 | 0 | `DispatchConveyorAsync` | 即时 | — | - -### 提升机并发控制 - -```csharp -// 从 startPoint 提取楼栋号: "14#_L2_HOIST" → 14 -var building = ExtractBuilding(detail.startPoint); - -// 获取该楼栋信号量,首次自动创建 SemaphoreSlim(2, 2) -var semaphore = HoistSemaphores.GetOrAdd(building, _ => new SemaphoreSlim(2, 2)); - -await semaphore.WaitAsync(); // 等待有空闲提升机 -// ... 执行 ... -semaphore.Release(); // 释放 -``` - -## 六、执行示例 - -3 个包材入库任务,每个 6 条明细: - -``` -T+0s [线程1] 任务A Detail1 AGV开始 [线程2] 任务B Detail1 AGV开始 [线程3] 任务C Detail1 AGV开始 -T+10s [线程1] 任务A Detail1 完成 [线程2] 任务B Detail1 完成 [线程3] 任务C Detail1 完成 -T+10s [线程1] 任务A Detail2 提升机开始 [线程2] 任务B Detail2 提升机开始 [线程3] 等待(13#提升机满) -T+30s [线程1] 任务A Detail2 完成 [线程2] 任务B Detail2 完成 [线程3] 任务C Detail2 提升机开始 -T+30s [线程1] 任务A Detail3 AGV开始 [线程2] 任务B Detail3 AGV开始 -...并行继续... -``` - -## 七、状态流转 - -``` -LiveTaskQueue.taskStatus: - 1 (待执行) → 2 (执行中) → 3 (已完成) - -LiveTaskDetail.taskStatus: - 1 (待执行) → 2 (执行中) → 3 (已完成) - ↘ 失败回退 → 1 (待重试) -``` - -## 八、UI 控制 - -系统监控页面顶部卡片,点击启动/停止: - -``` -┌──────────────────────────────────────────────────┐ -│ ● 包材入库调度 运行中/已停止 [▶ 启动] [■ 停止] │ -└──────────────────────────────────────────────────┘ -``` - -- `Start()` → 创建 `CancellationTokenSource`,启动 `RunLoopAsync` -- `Stop()` → 取消 token,等待当前线程结束后标记停止 diff --git a/接驳位驱动调度方案.md b/接驳位驱动调度方案.md deleted file mode 100644 index 8baa787..0000000 --- a/接驳位驱动调度方案.md +++ /dev/null @@ -1,390 +0,0 @@ -# 接驳位驱动的任务调度方案 - -## 一、核心概念 - -| 概念 | 物理含义 | 数据对应 | -|------|---------|---------| -| 接驳位 | 货物放置/取走的物理位置 | `LiveTaskDetail.startPoint / endPoint`,设备参数中 `接驳位状态`(0空闲/1占用) | -| RFID 条码 | 托盘上的电子标签 | `LiveTaskDetail.palletBarcode` | -| 任务 | 一个完整的搬运工单 | `LiveTaskQueue` | -| 子任务步骤 | 任务中的一段搬运步骤 | `LiveTaskDetail`(`objId` 决定先后顺序) | - -### 任务与子任务的关系 - -一个 `LiveTaskQueue` 包含多个 `LiveTaskDetail`,按 `objId` 从小到大串行执行。前一步的 `endPoint` 等于下一步的 `startPoint`,通过接驳位串联: - -``` -LiveTaskQueue (任务, taskCode = "T001") - │ - ├─ LiveTaskDetail objId=1: AGV 13#_L1_01 → BRIDGE_13_14 (子任务步骤1) - ├─ LiveTaskDetail objId=2: AGV BRIDGE_13_14 → 14#_L1_HOIST (子任务步骤2) - ├─ LiveTaskDetail objId=3: 提升机 14#_L1_HOIST → 14#_L2_HOIST (子任务步骤3) - ├─ LiveTaskDetail objId=4: AGV 14#_L2_HOIST → 15#_L2_HOIST (子任务步骤4) - ├─ LiveTaskDetail objId=5: 提升机 15#_L2_HOIST → 15#_L4_HOIST (子任务步骤5) - └─ LiveTaskDetail objId=6: AGV 15#_L4_HOIST → 15#_L4_03 (子任务步骤6) -``` - -### 数据库关键字段 - -- **LiveTaskQueue**: `taskCode`, `taskType`(1入库/2出库), `taskCategory`(1包材/2成品/3托盘), `taskStatus`(1待执行/2执行中/3已完成), `startPoint`, `endPoint` -- **LiveTaskDetail**: `taskCode`, `objId`, `palletBarcode`, `startPoint`, `endPoint`, `deviceType`(0输送线/1AGV/2提升机), `taskStatus` -- **BaseDeviceParam**: `deviceCode`, `paramName`(如"接驳位状态"), `paramValue`(0空闲/1占用) - ---- - -## 二、现场拓扑 - -### 楼栋与设备 - -``` - 廊桥(2F) - ┌───────────┐ - │ AGV交接 │ - └─────┬─────┘ - │ - 13#(1F) ──13#提升机── 13#(2F)─┘ 14#(2F) ──AGV── 15#(2F) ──15#提升机── 15#(1F~5F) - (1F↔2F) │ - 14#提升机(1F↔2F) - │ - 14#(1F) -``` - -### AGV 运行区域 - -| 区域 | 行驶范围 | 说明 | -|------|---------|------| -| 13# AGV | 13# 栋 1F~2F | 不能驶入 14# | -| 14# AGV | 14# 栋 1F~2F + 至 15# 2F | 负责中间段搬运 | -| 15# AGV | 仅在 15# 栋内 | 不能驶入 14# | - -### 核心约束 - -- **AGV 不能跨栋**:13# 和 14# 之间在 2F 有廊桥交接点,两边 AGV 在此完成货物交换 -- **所有楼栋均为 1~5F**,各栋有 1 台提升机连通所有楼层 -- **跨栋必须经 2F**:不管起点在几楼,跨栋必须经 2F 的廊桥(13↔14)或 AGV 通道(14↔15) - -### 接驳位点表 - -| 接驳位 | 编码 | 所属 AGV | -|--------|------|---------| -| 13# 各层普通点 | `13#_L{f}_{n}` | 13# AGV | -| 13# 提升机入口/出口 | `13#_L{f}_HOIST` | 13# AGV | -| 廊桥交接点(2F) | `BRIDGE_13_14` | 13# AGV ↔ 14# AGV 交接 | -| 14# 各层普通点 | `14#_L{f}_{n}` | 14# AGV | -| 14# 提升机入口/出口 | `14#_L{f}_HOIST` | 14# AGV | -| 15# 提升机 2F 入口 | `15#_L2_HOIST` | 14# AGV ↔ 15# AGV | -| 15# 提升机各层出口 | `15#_L{f}_HOIST` | 15# AGV | -| 15# 各层普通点 | `15#_L{f}_{n}` | 15# AGV | - -### 拓扑连通图 - -``` -节点: (building, floor) - -边 (提升机, 同栋跨层): - (13,1) ←→ (13,2) - (14,1) ←→ (14,2) - (15,2) ←→ (15,1) - (15,2) ←→ (15,3) - (15,2) ←→ (15,4) - (15,2) ←→ (15,5) - -边 (AGV, 同层跨栋): - (13,2) ←→ (14,2) (廊桥交接) - (14,2) ←→ (15,2) (AGV直连) -``` - ---- - -## 三、任务创建方案 —— 路径分解与明细生成 - -### 输入 - -``` -起始楼栋: startBuilding (13/14/15) -起始楼层: startFloor (1~5) -起始位置: startLocation (位置号,如 "01") -结束楼栋: endBuilding (13/14/15) -结束楼层: endFloor (1~5) -结束位置: endLocation (位置号,如 "02") -``` - -系统自动拼接为 `{building}#_L{floor}_{location}` 格式。 - -### 输出 - -返回一个完整的 `LiveTaskQueue`,包含按 `objId` 排序的 `List`。 - -### 生成算法 - -``` -输入: (b_s, f_s, loc_s), (b_e, f_e, loc_e) - -Step 1 — BFS 最短路径 - 在拓扑图中找 (b_s, f_s) → ... → (b_e, f_e) 的节点序列 - -Step 2 — 沿节点路径逐段生成明细 - currentPos = 实际起点 - - for each 相邻节点 (b1,f1) → (b2,f2): - - 跨栋 (b1 != b2): - if 13↔14 (廊桥交接): - 生成 [AGV] currentPos → BRIDGE_13_14 - 生成 [AGV] BRIDGE_13_14 → 接驳位(b2,f2) - 注: 廊桥在 2F,两边都须先经提升机到达 2F - else (14↔15, AGV直连): - 生成 [AGV] currentPos → 接驳位(b2,f2) - currentPos = 接驳位(b2,f2) - - 同栋跨层 (b1 == b2, f1 != f2): - 生成 [AGV] currentPos → 提升机入口(b,f1) - 生成 [提升机] 提升机入口(b,f1) → 提升机出口(b,f2) - currentPos = 提升机出口(b,f2) - -Step 3 — 最终送达 - if currentPos != 实际终点: - 生成 [AGV] currentPos → 实际终点 -``` - - ┌─ 同栋同层 (b1 == b2 && f1 == f2) ──────────────┐ - │ - │ 跳过(不会出现在最短路径中) - │ - └────────────────────────────────────────────────┘ - -Step 3 — 替换首尾实际坐标 - - 首条 Detail.startPoint → pt_s - 尾条 Detail.endPoint → pt_e - 中间节点起点/终点 → 使用对应的接驳位编码 -``` - -### 各场景示例(修正后——廊桥在 2F) - -| 场景 | 输入 | 生成 Detail 序列 | -|------|------|-----------------| -| **同栋同层** | 14,1,"01" → 14,1,"05" | `[1] AGV: 14#_L1_01 → 14#_L1_05` | -| **同栋跨层** | 14,1,"01" → 14,2,"03" | `[1] AGV: 14#_L1_01 → 14#_L1_HOIST` `[2] 提升机: 14#_L1_HOIST → 14#_L2_HOIST` `[3] AGV: 14#_L2_HOIST → 14#_L2_03` | -| **13→14 (均为1F)** | 13,1,"01" → 14,1,"03" | `[1] AGV: 13#_L1_01 → 13#_L1_HOIST` `[2] 提升机: 13#_L1_HOIST → 13#_L2_HOIST` `[3] AGV: 13#_L2_HOIST → BRIDGE_13_14` `[4] AGV: BRIDGE_13_14 → 14#_L2_01` `[5] AGV: 14#_L2_01 → 14#_L2_HOIST` `[6] 提升机: 14#_L2_HOIST → 14#_L1_HOIST` `[7] AGV: 14#_L1_HOIST → 14#_L1_03` | -| **13→14 (目标2F)** | 13,1,"01" → 14,2,"05" | `[1] AGV: 13#_L1_01 → 13#_L1_HOIST` `[2] 提升机: 13#_L1_HOIST → 13#_L2_HOIST` `[3] AGV: 13#_L2_HOIST → BRIDGE_13_14` `[4] AGV: BRIDGE_13_14 → 14#_L2_01` `[5] AGV: 14#_L2_01 → 14#_L2_05` | -| **14→15** | 14,1,"01" → 15,4,"02" | `[1] AGV: 14#_L1_01 → 14#_L1_HOIST` `[2] 提升机: 14#_L1_HOIST → 14#_L2_HOIST` `[3] AGV: 14#_L2_HOIST → 15#_L2_01` `[4] AGV: 15#_L2_01 → 15#_L2_HOIST` `[5] 提升机: 15#_L2_HOIST → 15#_L4_HOIST` `[6] AGV: 15#_L4_HOIST → 15#_L4_02` | -| **13→15 完整链路** | 13,1,"01" → 15,4,"02" | `[1] AGV: 13#_L1_01 → 13#_L1_HOIST` `[2] 提升机: 13#_L1_HOIST → 13#_L2_HOIST` `[3] AGV: 13#_L2_HOIST → BRIDGE_13_14` `[4] AGV: BRIDGE_13_14 → 14#_L2_01` `[5] AGV: 14#_L2_01 → 15#_L2_01` `[6] AGV: 15#_L2_01 → 15#_L2_HOIST` `[7] 提升机: 15#_L2_HOIST → 15#_L4_HOIST` `[8] AGV: 15#_L4_HOIST → 15#_L4_02` | -| **14→15 2F直连** | 14,2,"01" → 15,2,"03" | `[1] AGV: 14#_L2_01 → 15#_L2_03` | - -### 伪代码 - -``` -LiveTaskQueue CreateTask( - int b_s, int f_s, string loc_s, - int b_e, int f_e, string loc_e, - int taskType, int taskCategory, string palletBarcode, string materialCode) -{ - var startPoint = $"{b_s}#_L{f_s}_{loc_s}"; - var endPoint = $"{b_e}#_L{f_e}_{loc_e}"; - var nodes = BFS(b_s, f_s, b_e, f_e); // 拓扑最短路径 - string currentPos = startPoint; - - for (int i = 0; i < nodes.Count - 1; i++) - { - var (b1, f1) = nodes[i]; - var (b2, f2) = nodes[i + 1]; - - if (b1 != b2) // 跨栋 - { - if (13↔14) - { - Add [AGV] currentPos → BRIDGE_13_14; - Add [AGV] BRIDGE_13_14 → DockingPoint(b2,f2); - } - else // 14↔15 - { - Add [AGV] currentPos → DockingPoint(b2,f2); - } - currentPos = DockingPoint(b2,f2); - } - else if (f1 != f2) // 同栋跨层 - { - var entry = $"{b1}#_L{f1}_HOIST"; - var exit = $"{b2}#_L{f2}_HOIST"; - Add [AGV] currentPos → entry; - Add [提升机] entry → exit; - currentPos = exit; - } - } - - if (details.Count == 0 || currentPos != endPoint) - Add [AGV] currentPos → endPoint; - - return new LiveTaskQueue { ... }; -} -### 拓扑路由表 - -``` -FindPath(b_s, f_s, b_e, f_e): - - 以 BFS 在拓扑图中搜索,典型路径: - - (13,1) → (13,2): [(13,1), (13,2)] - (13,1) → (14,1): [(13,1), (13,2), (14,2), (14,1)] - (13,1) → (14,2): [(13,1), (13,2), (14,2)] - (13,1) → (15,2): [(13,1), (13,2), (14,2), (15,2)] - (13,1) → (15,4): [(13,1), (13,2), (14,2), (15,2), (15,4)] - (14,1) → (15,4): [(14,1), (14,2), (15,2), (15,4)] - (15,4) → (13,1): [(15,4), (15,2), (14,2), (13,2), (13,1)] - 反向: 路径取反 -``` - ---- - -## 四、调度触发链路 - -``` -人工放货到接驳位 - → PLC 传感器检测到货物 → 接驳位状态 = 1 - → PLC 读 RFID → 得到托盘条码 - → WCS 收到事件: (positionCode, rfidBarcode) - → 进入匹配调度逻辑 -``` - -## 五、匹配调度逻辑 - -``` -输入: positionCode (接驳位编码), rfidBarcode (RFID条码) - -Step 1 ─ 匹配明细 - 查 LiveTaskDetail WHERE - palletBarcode == rfidBarcode - AND startPoint == positionCode - AND taskStatus == 1 (待执行) - - 未匹配 → 告警,返回 - - 匹配到 detail - -Step 2 ─ 前序依赖校验 - 查同 taskCode 下 objId < detail.objId 的所有明细 - 全部 taskStatus == 3 (已完成) ? - - 否 → 等待(不应发生,正常前序完成才会到这一步),返回 - -Step 3 ─ 接驳位加锁 - 获取该接驳位的锁(信号量) - 二次确认 detail.taskStatus 仍为 1(防并发) - -Step 4 ─ 标记执行中 - detail.taskStatus = 2 - 若 LiveTaskQueue.taskStatus == 1 → 也改为 2 - -Step 5 ─ 下发设备 - detail.deviceType: - 1 → AGV - 2 → 提升机 - 0 → 输送线 - -Step 6 ─ 结果处理 - 成功: - detail.taskStatus = 3 - 释放接驳位 (status = 0) - 检查整个 task 是否全部完成 → taskStatus = 3 - - 失败: - detail.taskStatus 回退为 1 - 下次重试 -``` - -## 六、链条式执行(以 13#_1F → 15#_4F 为例) - -``` -Task: P001, 13#_L1_01 → 15#_L4_02 - 路径节点: (13,1) → (13,2) → (14,2) → (15,2) → (15,4) - - Detail 1: AGV 13#_L1_01 → 13#_L1_HOIST - Detail 2: 提升机 13#_L1_HOIST → 13#_L2_HOIST - Detail 3: AGV 13#_L2_HOIST → BRIDGE_13_14 - Detail 4: AGV BRIDGE_13_14 → 14#_L2_01 - Detail 5: AGV 14#_L2_01 → 15#_L2_01 - Detail 6: AGV 15#_L2_01 → 15#_L2_HOIST - Detail 7: 提升机 15#_L2_HOIST → 15#_L4_HOIST - Detail 8: AGV 15#_L4_HOIST → 15#_L4_02 - -════════════════════ 时间线 ════════════════════ - - T0: 人工放货到 13#_L1_01,读 RFID - → 匹配 Detail 1,无前序 ✓ → 13# AGV 取走 - → 13#_L1_01 释放 - - T1: 13# AGV 运到 13#_L1_HOIST,放下 - → 匹配 Detail 2,前序 1 ✓ - → 13# 提升机启动,13#_L1_HOIST 释放 - - T2: 提升机运到 2F,到达 13#_L2_HOIST - → 匹配 Detail 3,前序 1,2 ✓ - → 13# AGV 取走,运往廊桥 - - T3: 13# AGV 到达 BRIDGE_13_14 (2F廊桥) - → 匹配 Detail 4,前序 1~3 ✓ - → 14# AGV 取走,BRIDGE_13_14 释放 - - T4: 14# AGV 运到 14#_L2_01,放下 - → 匹配 Detail 5,前序 1~4 ✓ - → 14# AGV 运往 15# (同层2F,直连) - - T5: 14# AGV 到达 15#_L2_01,放下 - → 匹配 Detail 6,前序 1~5 ✓ - → 15# AGV 转运到提升机入口 - - T6: 15# AGV 到达 15#_L2_HOIST,放下 - → 匹配 Detail 7,前序 1~6 ✓ - → 15# 提升机启动 - - T7: 提升机运到 4F,到达 15#_L4_HOIST - → 匹配 Detail 8,前序 1~7 ✓ - → 15# AGV 取走,运到 15#_L4_02 - - 任务 P001 全部完成 ✓ -``` - -## 七、并发模型 - -``` -接驳位 A ─┐ -接驳位 B ─┤ 各自独立并行(不同锁保护) -接驳位 C ─┘ - -同一接驳位内 → 串行(同一把锁保护) -``` - -不同任务在不同接驳位上可以同时执行,互不干扰。 - -## 八、异常场景 - -| 场景 | 处理 | -|------|------| -| RFID + 位置匹配不到待执行明细 | 货物放错位置或任务未创建 → 告警 | -| 前序明细未完成但货物已到 | 等待 | -| 设备下发失败 | detail 状态回退为 1,下次循环重试 | -| 同一明细被并发处理 | Step 3 二次确认防御 | -| 接驳位长时间占用不释放 | 需超时告警机制(后续) | -| 全部明细完成 | task 标记为已完成(status = 3) | - -## 九、依赖设备信息 - -### 设备类型 (deviceType) - -| 值 | 类型 | 对应服务 | -|----|------|---------| -| 0 | 输送线 | PLC 直接控制 | -| 1 | AGV | HikRoBotServer (端口 5200) | -| 2 | 提升机 | HoistServer (端口 5100) | - -### 客户端 API - -**HoistServer** (端口 5100): -- `/api/hoist/receive-pallet` - 接收托盘 -- `/api/hoist/task-run` - 提升机启动 -- `/api/task/dispatch` - 下发调度任务 -- `/api/hoist/free` - 获取空闲提升机 - -**HikRoBotServer** (端口 5200): -- `/api/task/receive` - 下发 AGV 任务