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

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