You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wcs_core/接驳位驱动调度方案.md

391 lines
14 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 接驳位驱动的任务调度方案
## 一、核心概念
| 概念 | 物理含义 | 数据对应 |
|------|---------|---------|
| 接驳位 | 货物放置/取走的物理位置 | `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 (1314)
{
Add [AGV] currentPos BRIDGE_13_14;
Add [AGV] BRIDGE_13_14 DockingPoint(b2,f2);
}
else // 1415
{
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 任务