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

14 KiB

接驳位驱动的任务调度方案

一、核心概念

概念 物理含义 数据对应
接驳位 货物放置/取走的物理位置 LiveTaskDetail.startPoint / endPoint,设备参数中 接驳位状态0空闲/1占用
RFID 条码 托盘上的电子标签 LiveTaskDetail.palletBarcode
任务 一个完整的搬运工单 LiveTaskQueue
子任务步骤 任务中的一段搬运步骤 LiveTaskDetailobjId 决定先后顺序)

任务与子任务的关系

一个 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 任务