change - AGV流程测试,点到点任务执行

dev
WenJY 16 hours ago
parent ca4168b8d6
commit 1d69b31d87

@ -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
/// </summary>
/// <param name="taskDetail"></param>
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<TargetRoute>()
{
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;
}
}
/// <summary>
/// 继续执行任务=> 通知AGVS继续执行
/// </summary>
/// <param name="taskDetail"></param>
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;
}
}

@ -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<IConfiguration>(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);
/// <summary>
/// 请求参数
/// </summary>
/// <param name="TaskCode">WCS 下发的任务编号:同 submit 接口</param>
/// <param name="HoistCode">提升机编号15 栋入库-1#Hoist 15栋出库-2#Hoist14 栋提升机-3#Host13 栋提升机-4#Hoist</param>
record HikRoBotWaitRequest(string TaskCode, string HoistCode);
//record HikRoBotWaitRequest(string TaskCode, string HoistCode);

@ -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<string, object> 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
}
}
/// <summary>
/// 等待提升机任务完成
/// </summary>
/// <param name="hostCode"></param>
/// <param name="deviceSerialNo"></param>
/// <returns></returns>
public async Task<bool> WaitHoistComplete(string hostCode, int deviceSerialNo)
{
var lockObj = HoistLocks.GetOrAdd(hostCode, _ => new object());
lock (lockObj)
{
RefreshDeviceParams(hostCode, out List<BaseDeviceInfo> deviceInfos);
var device = deviceInfos?.FirstOrDefault(info => info.deviceSerialNo == deviceSerialNo);
var paramValue = device?.deviceParams
.FirstOrDefault(item => item.paramName.Contains("任务执行完成"))
?.paramValue;
return paramValue != 1;
}
}
/// <summary>
/// 获取空闲提升机
/// </summary>
/// <param name="hostCode"></param>
/// <returns></returns>
public async Task<BaseDeviceInfo> GetFreeHoistAsync(string hostCode)
public async Task<BaseDeviceInfo?> GetFreeHoistAsync(string hostCode)
{
while (true)
var lockObj = HoistLocks.GetOrAdd(hostCode, _ => new object());
lock (lockObj)
{
lock (string.Empty)
{
RefreshDeviceParams(hostCode, out List<BaseDeviceInfo> 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<BaseDeviceInfo> deviceInfos);
return deviceInfos?.FirstOrDefault(info =>
info.deviceParams.Any(item =>
item.paramName.Contains("提升机反馈任务状态") && item.paramValue == 0
)
);
}
}

@ -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");

@ -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<bool> DispatchAgvAsync(LiveTaskDetail detail)
private async Task<bool> 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<AGVTaskReceiveResponse>();
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<AGVTaskStatusResponse>(
$"{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}");
}
}
}
/// <summary>AGV 等待时筛选空闲提升机并下发提升机任务</summary>
private async Task<bool> TryDispatchHoistForWaitAsync(HttpClient httpClient, string hoistBaseUrl, LiveTaskDetail detail)
{
var hostCode = DetermineHoistCode(detail);
_logger.Info($" [AGV] 任务等待中,筛选空闲提升机 ({hostCode})...");
try
{
FreeHoistResponse? freeRes;
while (true)
{
freeRes = await httpClient.GetFromJsonAsync<FreeHoistResponse>(
$"{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<ApiResult>();
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;
}
}
/// <summary>通知 AGV 调度中心继续执行</summary>
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<AGVTaskReceiveResponse>();
if (result is { isSuccess: true })
_logger.Info($" [AGV] 继续执行已下发 (提升机 {hoistDetail.execDevice})");
else
_logger.Error($" [AGV] 继续执行下发失败: AGV 调度中心拒绝");
}
catch (Exception ex)
{
_logger.Error($" [AGV] 继续执行下发异常: {ex.Message}");
}
}
/// <summary>根据 startPoint 确定提升机编号15栋入库→1#Host, 15栋出库→2#Host, 14栋→3#Host, 13栋→4#Host</summary>
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<bool> 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<ApiResult>();
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<WaitCompleteResponse>(
$"{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; }
}
}

@ -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();
}

@ -185,14 +185,17 @@ public partial class ManualTaskViewModel : ObservableObject
}
}
var freeUrl = $"{HoistBaseUrl}/api/hoist/free?hostCode={Uri.EscapeDataString(hostCode)}";
var freeRes = await _http.GetFromJsonAsync<FreeHoistResponse>(freeUrl);
if (freeRes is not { found: true })
FreeHoistResponse? freeRes;
while (true)
{
StatusText = "暂无空闲提升机,请稍后重新获取";
freeRes = await _http.GetFromJsonAsync<FreeHoistResponse>(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<WaitCompleteResponse>(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; }

@ -2,31 +2,39 @@
## 已完成
- [x] HoistServer (端口 5100) — 提升机调度 APISwagger `/swagger`4 个 Hub 方法
- [x] HikRoBotServer (端口 5200) — AGV 调度 APISwagger `/swagger`
- [x] UI 引擎启停控制 — 系统监控页分别启动/停止两个服务器进程
- [x] 码垛机 HMI 页面 — 静态界面,手动操作区带开关按钮
- [x] 删除确认弹窗 — 所有删除操作弹出 ConfirmDialog
- [x] 浅色/深色主题 — 一键切换,全视图适配
- [] HoistServer (端口 5100) — 提升机调度 APISwagger `/swagger`4 个 Hub 方法
- [] HikRoBotServer (端口 5200) — AGV 调度 APISwagger `/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. 任务执行引擎(分组并行方案)
- [ ] 包材入库逻辑测试

@ -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 | 初版记录,待测试后优化 |

@ -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<LiveTaskDetail>
```
## 二、任务创建
### 输入
| 参数 | 说明 | 示例 |
|------|------|------|
| 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<LiveTaskDetail>
```
### 拓扑规则
- 所有楼栋 13# / 14# / 15# 均为 1~5F
- 每栋楼有 1 台提升机,可直达任意楼层(全连接)
- 跨栋仅 2F 连通:
- (13,2) ↔ (14,2):廊桥交接,两段 AGV13# AGV → BRIDGE_13_1414# 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<int, SemaphoreSlim>` | 每栋楼 2 台 |
| AGV 并发 | 不限 | — |
| DB 写入 | `lock (DbWriteLock)` 串行化 | — |
| 启停状态 | `lock (_lock)` + `volatile bool` | — |
### 线程安全
- **启停**`lock (_lock)` 保护 `_cts` / `_isRunning`
- **DB 写入**`lock (DbWriteLock)` 保护 SqlSugar UpdateContext 为单例,多线程写入冲突)
- **提升机信号量**`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>();
```
**依赖关系**
```
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 触发单条明细执行

@ -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<int, SemaphoreSlim>` | 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等待当前线程结束后标记停止

@ -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<LiveTaskDetail>`
### 生成算法
```
输入: (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 任务
Loading…
Cancel
Save