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