add - 手动创建任务功能,任务生成方式修改

dev
WenJY 9 hours ago
parent 73e72df74d
commit 7da743e2bd

@ -20,7 +20,8 @@
"Bash(dotnet /Users/wenxiansheng/.nuget/packages/avalonia/11.1.5/lib/netstandard2.0/Avalonia.Base.dll type list)",
"Bash(dotnet new *)",
"Bash(dotnet add *)",
"Bash(python3 *)"
"Bash(python3 *)",
"Bash(git restore *)"
]
}
}

@ -0,0 +1,227 @@
using Sln.Wcs.Business.Domain.Enum;
using Sln.Wcs.Model.Domain;
using Sln.Wcs.Serilog;
namespace Sln.Wcs.Business;
/// <summary>
/// 接驳位拓扑驱动的任务创建服务
/// 根据起止楼栋/楼层/位置,自动生成 LiveTaskQueue 及其子任务明细序列
/// </summary>
public class TaskCreateService
{
private readonly SerilogHelper _logger;
public TaskCreateService(SerilogHelper logger) => _logger = logger;
#region 拓扑配置
private record TopoNode(int Building, int Floor);
/// <summary>
/// 拓扑邻接表:(栋,层) → 可达的相邻节点
/// 所有楼栋 1~5F本栋提升机可直达任意楼层全连接
/// 跨栋仅 2F 连通: (13,2)↔(14,2) 廊桥交接, (14,2)↔(15,2) AGV直连
/// </summary>
private static readonly Dictionary<TopoNode, List<TopoNode>> Topology = BuildTopology();
private static Dictionary<TopoNode, List<TopoNode>> BuildTopology()
{
var dict = new Dictionary<TopoNode, List<TopoNode>>();
// 每个楼栋内全连接(提升机直达任意楼层)
foreach (var building in new[] { 13, 14, 15 })
{
for (int f1 = 1; f1 <= 5; f1++)
{
var neighbors = new List<TopoNode>();
for (int f2 = 1; f2 <= 5; f2++)
{
if (f1 != f2)
neighbors.Add(new(building, f2));
}
dict[new(building, f1)] = neighbors;
}
}
// 跨栋: 仅 2F 连通
dict[new(13, 2)].Add(new(14, 2)); // 13↔14 廊桥交接
dict[new(14, 2)].Add(new(13, 2));
dict[new(14, 2)].Add(new(15, 2)); // 14↔15 AGV直连
dict[new(15, 2)].Add(new(14, 2));
return dict;
}
#endregion
/// <summary>
/// 创建任务:输入楼栋/楼层/位置号,自动生成明细序列
/// </summary>
/// <param name="startBuilding">起始楼栋 (13/14/15)</param>
/// <param name="startFloor">起始楼层</param>
/// <param name="startLocation">起始位置号,如 "01"</param>
/// <param name="endBuilding">目标楼栋 (13/14/15)</param>
/// <param name="endFloor">目标楼层</param>
/// <param name="endLocation">目标位置号,如 "02"</param>
/// <param name="taskType">任务类型: 1-入库, 2-出库</param>
/// <param name="taskCategory">任务类别: 1-包材, 2-成品, 3-托盘</param>
/// <param name="palletBarcode">托盘条码</param>
/// <param name="materialCode">物料编码</param>
/// <returns>完整 LiveTaskQueue含明细列表</returns>
public LiveTaskQueue CreateTask(
int startBuilding, int startFloor, string startLocation,
int endBuilding, int endFloor, string endLocation,
int taskType, int taskCategory,
string palletBarcode, string materialCode)
{
var startPoint = PosCode(startBuilding, startFloor, startLocation);
var endPoint = PosCode(endBuilding, endFloor, endLocation);
_logger.Info($"创建任务 - {startPoint} ({startBuilding}#_{startFloor}F) → {endPoint} ({endBuilding}#_{endFloor}F)");
// Step 1: BFS 找最短路径(节点序列)
var nodes = FindPath(startBuilding, startFloor, endBuilding, endFloor);
_logger.Info($"路由节点: {string.Join(" ", nodes.Select(n => $"({n.Building},{n.Floor})"))}");
// Step 2: 沿节点路径逐段生成明细
// 规则: 同层移动=AGV, 跨层=提升机, 跨栋(13↔14)=廊桥交接两段AGV
var details = new List<LiveTaskDetail>();
int seq = 1;
var taskCode = GenerateTaskCode();
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 (IsBridgeRequired(b1, b2))
{
// 13↔14: 廊桥交接 — 13# AGV 送到廊桥,等待 14# AGV
details.Add(NewDetail(taskCode, palletBarcode, materialCode,
taskType, taskCategory, seq++, currentPos, "BRIDGE_13_14", deviceType: 1));
currentPos = "BRIDGE_13_14";
}
// 14↔15: 14# AGV 可直接驶入 15#,不生成过渡段,由下一步直达
}
else if (f1 != f2) // 同栋跨层 → AGV到提升机入口 + 提升机
{
var hoistEntry = HoistPoint(b1, f1);
var hoistExit = HoistPoint(b2, f2);
details.Add(NewDetail(taskCode, palletBarcode, materialCode,
taskType, taskCategory, seq++, currentPos, hoistEntry, deviceType: 1));
details.Add(NewDetail(taskCode, palletBarcode, materialCode,
taskType, taskCategory, seq++, hoistEntry, hoistExit, deviceType: 2));
currentPos = hoistExit;
}
}
// Step 3: 最终 AGV 送达终点(同栋同层时直接一条 AGV
if (details.Count == 0 || currentPos != endPoint)
{
details.Add(NewDetail(taskCode, palletBarcode, materialCode,
taskType, taskCategory, seq, currentPos, endPoint, deviceType: 1));
}
_logger.Info($"生成 {details.Count} 条明细");
return new LiveTaskQueue
{
taskCode = taskCode,
taskType = taskType,
taskCategory = taskCategory,
taskStatus = 1,
startPoint = startPoint,
endPoint = endPoint,
palletBarcode = palletBarcode,
materialCode = materialCode,
taskSteps = details.Count,
taskDetails = details,
};
}
#region 内部方法
/// <summary>BFS 最短路径</summary>
private static List<TopoNode> FindPath(int b_s, int f_s, int b_e, int f_e)
{
var start = new TopoNode(b_s, f_s);
var end = new TopoNode(b_e, f_e);
if (start == end) return new List<TopoNode> { start };
// 校验起止节点是否在拓扑中
if (!Topology.ContainsKey(start))
throw new InvalidOperationException($"起始位置不在拓扑中: {b_s}#_{f_s}F可用节点: {string.Join(", ", Topology.Keys.Select(n => $"{n.Building}#_{n.Floor}F"))}");
if (!Topology.ContainsKey(end))
throw new InvalidOperationException($"目标位置不在拓扑中: {b_e}#_{f_e}F可用节点: {string.Join(", ", Topology.Keys.Select(n => $"{n.Building}#_{n.Floor}F"))}");
var visited = new HashSet<TopoNode> { start };
var queue = new Queue<List<TopoNode>>();
queue.Enqueue(new List<TopoNode> { start });
while (queue.Count > 0)
{
var path = queue.Dequeue();
var current = path[^1];
if (!Topology.TryGetValue(current, out var neighbors)) continue;
foreach (var next in neighbors)
{
if (visited.Contains(next)) continue;
var newPath = new List<TopoNode>(path) { next };
if (next == end) return newPath;
visited.Add(next);
queue.Enqueue(newPath);
}
}
throw new InvalidOperationException($"无路径: ({b_s},{f_s}) → ({b_e},{f_e})");
}
/// <summary>是否需要廊桥交接</summary>
private static bool IsBridgeRequired(int b1, int b2) =>
(b1 == 13 && b2 == 14) || (b1 == 14 && b2 == 13);
/// <summary>拼接位置编码: 楼栋#_L楼层_位置号</summary>
private static string PosCode(int building, int floor, string location) =>
$"{building}#_L{floor}_{location}";
/// <summary>提升机接驳位</summary>
private static string HoistPoint(int building, int floor) =>
$"{building}#_L{floor}_HOIST";
/// <summary>普通接驳位(非提升机、非廊桥)</summary>
private static string DockingPoint(int building, int floor) =>
$"{building}#_L{floor}_01";
/// <summary>生成任务编号</summary>
private static string GenerateTaskCode() =>
DateTime.Now.ToString("yyyyMMddHHmmss") + new Random().Next(10, 99);
/// <summary>创建一条 LiveTaskDetail</summary>
private static LiveTaskDetail NewDetail(
string taskCode, string palletBarcode, string materialCode,
int taskType, int taskCategory, int seq,
string startPoint, string endPoint, int deviceType) =>
new()
{
taskCode = taskCode,
objId = seq,
palletBarcode = palletBarcode,
materialCode = materialCode,
startPoint = startPoint,
endPoint = endPoint,
deviceType = deviceType,
taskType = taskType,
taskCategory = taskCategory,
taskStatus = 1,
};
#endregion
}

@ -58,6 +58,7 @@ public partial class App : Application
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Common.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Cache.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Repository.dll")),
Assembly.LoadFrom(Path.Combine(basePath, "Sln.Wcs.Business.dll")),
};
services.Scan(scan => scan.FromAssemblies(assemblies)
@ -92,6 +93,7 @@ public partial class App : Application
services.AddTransient<ViewModels.Path.PathDetailsViewModel>();
services.AddTransient<ViewModels.Task.TaskQueueViewModel>();
services.AddTransient<ViewModels.Task.TaskDetailViewModel>();
services.AddTransient<ViewModels.Task.CreateTaskViewModel>();
services.AddTransient<ViewModels.HMI.PalletizerHMIViewModel>();
// 引擎进程管理

@ -36,6 +36,7 @@
<ProjectReference Include="..\Sln.Wcs.Common\Sln.Wcs.Common.csproj" />
<ProjectReference Include="..\Sln.Wcs.Model\Sln.Wcs.Model.csproj" />
<ProjectReference Include="..\Sln.Wcs.Repository\Sln.Wcs.Repository.csproj" />
<ProjectReference Include="..\Sln.Wcs.Business\Sln.Wcs.Business.csproj" />
<ProjectReference Include="..\Sln.Wcs.Serilog\Sln.Wcs.Serilog.csproj" />
<ProjectReference Include="..\Sln.Wcs.HoistServer\Sln.Wcs.HoistServer.csproj" ReferenceOutputAssembly="false" />
</ItemGroup>

@ -59,6 +59,7 @@ public partial class NavigationViewModel : ObservableObject
}));
TopMenuItems.Add(new TopMenuItem("任务管理", new List<SubMenuItem>
{
new("创建任务", () => NavigateTo<CreateTaskViewModel>("任务管理")),
new("任务队列", () => NavigateTo<TaskQueueViewModel>("任务管理")),
new("任务明细", () => NavigateTo<TaskDetailViewModel>("任务管理")),
}));

@ -0,0 +1,82 @@
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Sln.Wcs.Business;
using Sln.Wcs.Repository.service;
namespace Sln.Wcs.UI.ViewModels.Task;
public partial class CreateTaskViewModel : ObservableObject
{
private readonly TaskCreateService _taskCreateService;
private readonly ILiveTaskQueueService _taskQueueService;
public string PageTitle => "手动创建任务";
// ---- 起点 ----
[ObservableProperty] private int _startBuilding = 13;
[ObservableProperty] private int _startFloor = 1;
[ObservableProperty] private string _startLocation = "01";
// ---- 终点 ----
[ObservableProperty] private int _endBuilding = 15;
[ObservableProperty] private int _endFloor = 5;
[ObservableProperty] private string _endLocation = "01";
// ---- 任务属性 ----
[ObservableProperty] private int _taskType = 1; // 1-入库 2-出库
[ObservableProperty] private int _taskCategory = 1; // 1-包材 2-成品 3-托盘
[ObservableProperty] private string _palletBarcode = string.Empty;
[ObservableProperty] private string _materialCode = string.Empty;
// ---- 状态 ----
[ObservableProperty] private string _statusText = string.Empty;
[ObservableProperty] private bool _isBusy;
public CreateTaskViewModel(TaskCreateService taskCreateService, ILiveTaskQueueService taskQueueService)
{
_taskCreateService = taskCreateService;
_taskQueueService = taskQueueService;
}
public Avalonia.Controls.Control CreateView() => new Views.Task.CreateTaskView();
[RelayCommand]
private async System.Threading.Tasks.Task CreateAsync()
{
if (string.IsNullOrWhiteSpace(PalletBarcode) || string.IsNullOrWhiteSpace(MaterialCode))
{
StatusText = "托盘条码和物料编码不能为空";
return;
}
IsBusy = true;
StatusText = "正在生成任务...";
try
{
var taskQueue = await System.Threading.Tasks.Task.Run(() =>
_taskCreateService.CreateTask(
StartBuilding, StartFloor, StartLocation,
EndBuilding, EndFloor, EndLocation,
TaskType, TaskCategory,
PalletBarcode, MaterialCode));
StatusText = $"路由生成完成,共 {taskQueue.taskSteps} 条明细,正在保存...";
var saved = await System.Threading.Tasks.Task.Run(() => _taskQueueService.InsertTaskQueue(taskQueue));
StatusText = saved
? $"✓ 任务创建成功: {taskQueue.taskCode},共 {taskQueue.taskSteps} 条明细"
: "✗ 保存失败";
}
catch (Exception ex)
{
StatusText = $"✗ 创建失败: {ex.Message}";
}
finally
{
IsBusy = false;
}
}
}

@ -0,0 +1,110 @@
<UserControl x:CompileBindings="False" xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Sln.Wcs.UI.Views.Task.CreateTaskView">
<ScrollViewer Margin="20,14,20,14">
<StackPanel Spacing="14" MaxWidth="600">
<!-- 起点 -->
<Border CornerRadius="8" Padding="18" Background="{DynamicResource CardBgBrush}"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="8">
<Rectangle Width="3" Height="18" Fill="#4FC3F7" RadiusX="2" RadiusY="2" />
<TextBlock Text="起点" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryTextBrush}" />
</StackPanel>
<WrapPanel>
<StackPanel Spacing="4" Width="140" Margin="0,0,12,8">
<TextBlock Text="楼栋 (13/14/15)" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBox Text="{Binding StartBuilding}" />
</StackPanel>
<StackPanel Spacing="4" Width="140" Margin="0,0,12,8">
<TextBlock Text="楼层 (1~5)" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBox Text="{Binding StartFloor}" />
</StackPanel>
<StackPanel Spacing="4" Width="140" Margin="0,0,12,8">
<TextBlock Text="位置号" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBox Text="{Binding StartLocation}" Watermark="01" />
</StackPanel>
</WrapPanel>
</StackPanel>
</Border>
<!-- 终点 -->
<Border CornerRadius="8" Padding="18" Background="{DynamicResource CardBgBrush}"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="8">
<Rectangle Width="3" Height="18" Fill="#4FC3F7" RadiusX="2" RadiusY="2" />
<TextBlock Text="终点" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryTextBrush}" />
</StackPanel>
<WrapPanel>
<StackPanel Spacing="4" Width="140" Margin="0,0,12,8">
<TextBlock Text="楼栋 (13/14/15)" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBox Text="{Binding EndBuilding}" />
</StackPanel>
<StackPanel Spacing="4" Width="140" Margin="0,0,12,8">
<TextBlock Text="楼层 (1~5)" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBox Text="{Binding EndFloor}" />
</StackPanel>
<StackPanel Spacing="4" Width="140" Margin="0,0,12,8">
<TextBlock Text="位置号" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBox Text="{Binding EndLocation}" Watermark="01" />
</StackPanel>
</WrapPanel>
</StackPanel>
</Border>
<!-- 任务属性 -->
<Border CornerRadius="8" Padding="18" Background="{DynamicResource CardBgBrush}"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="8">
<Rectangle Width="3" Height="18" Fill="#4FC3F7" RadiusX="2" RadiusY="2" />
<TextBlock Text="任务属性" FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource PrimaryTextBrush}" />
</StackPanel>
<!-- 任务类型 / 任务类别 -->
<WrapPanel>
<StackPanel Spacing="4" Margin="0,0,20,8">
<TextBlock Text="任务类型" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBlock Text="1-入库 2-出库" FontSize="9" Foreground="{DynamicResource MutedTextBrush}" Margin="0,2,0,4" />
<TextBox Text="{Binding TaskType}" Width="200" />
</StackPanel>
<StackPanel Spacing="4" Margin="0,0,20,8">
<TextBlock Text="任务类别" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBlock Text="1-包材 2-成品 3-托盘" FontSize="9" Foreground="{DynamicResource MutedTextBrush}" Margin="0,2,0,4" />
<TextBox Text="{Binding TaskCategory}" Width="200" />
</StackPanel>
</WrapPanel>
<!-- 托盘条码 / 物料编码 -->
<WrapPanel>
<StackPanel Spacing="4" Margin="0,0,20,8">
<TextBlock Text="托盘条码 (RFID)" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBox Text="{Binding PalletBarcode}" Watermark="PALLET001" Width="200" />
</StackPanel>
<StackPanel Spacing="4" Margin="0,0,20,8">
<TextBlock Text="物料编码" FontSize="11" Foreground="{DynamicResource SecondaryTextBrush}" />
<TextBox Text="{Binding MaterialCode}" Watermark="M001" Width="200" />
</StackPanel>
</WrapPanel>
</StackPanel>
</Border>
<!-- 操作 -->
<Border CornerRadius="8" Padding="18" Background="{DynamicResource CardBgBrush}"
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="12">
<Button Content="创建任务" Command="{Binding CreateCommand}"
IsEnabled="{Binding !IsBusy}"
Background="{DynamicResource PrimaryBtnBrush}"
Foreground="White" FontSize="13" Padding="20,10" />
<TextBlock Text="{Binding StatusText}" FontSize="12"
Foreground="{DynamicResource AccentTextBrush}"
VerticalAlignment="Center" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Sln.Wcs.UI.Views.Task;
public partial class CreateTaskView : UserControl
{
public CreateTaskView()
{
InitializeComponent();
}
}

@ -0,0 +1,390 @@
# 接驳位驱动的任务调度方案
## 一、核心概念
| 概念 | 物理含义 | 数据对应 |
|------|---------|---------|
| 接驳位 | 货物放置/取走的物理位置 | `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