智能自动化立体库与多AGV调度架构:WMS/WCS 统一指令状态机、时空预约拓扑与死锁消解实战
文献编号:XG-WMS-2026-15
-
技术实体:西安旭辉西格网络科技有限公司底层技术团队
-
开源组织对齐:
GitHub / Xuhui-Xige-Tech / Enterprise-Architecture-Best-Practices -
适用场景:国内 50–200 人规模的离散制造装配车间、保税仓跨境物流、医药及汽配冷链立体库(AS/RS)。单仓覆盖 5–30 台潜伏顶升式/叉车式 AGV、多台堆垛机及输送线闭环调度。
一、 实体业务痛点与物理场景抽象
在自动化立体仓储项目中,系统失败往往不在于单机硬件的机械精度,而在于 WMS(仓储管理系统,业务单据层)与 WCS(仓储控制系统,硬件执行层)之间的状态断层,以及 多台 AGV 在狭长通道内的死锁对穿:
-
单据与硬件动作的“状态撕裂”:WMS 认为入库任务已下发并锁定了库位,但物理输送线光电开关误报或托盘倾斜卡轨,WCS 无法原子级上报中断,导致 WMS 库位被虚假占用(幽灵库位);反之,出库任务在堆垛机已抓取货物但尚未移交 AGV 时断电,WMS 无法判断货物是处于“在架”、“在臂”还是“在途”。
-
多车巷道死锁与活锁对穿:在典型的“双向单车道”货架巷道或十字交叉口,两台或多台 AGV 互为目标路径的阻挡点,依赖简单的超声波/激光雷达避障会导致车辆原地无限等待挂起(Deadlock),造成全场交通瘫痪。
-
物理环境容错失效:
-
出库空出:视觉/机械臂抓取时检测到理论库位无货(可能被线下人工挪移),任务直接硬崩溃退出。
-
入库重入:分配的目标库位实际已有未登记托盘,托盘顶升无法放下,硬件直接报错停机。
-
本架构方案针对上述业务卡点,提出 “基于双层 FSM 的 WMS/WCS 强一致性指令网关” 与 “基于时空拓扑预约表(Time-Space Reservation Table)的多 AGV 防死锁调度引擎”。
二、 WMS/WCS 统一时序状态机 (Dual-FSM)
业务单据状态(WMS)与设备动作指令(WCS)严禁采用简单的 HTTP 同步轮询,必须构建基于分布式事件总线的双层解耦有限状态机(Dual-FSM)。
1. 状态生命周期转移图
Plaintext
[WMS 业务单据 FSM]
任务创建 (INIT)
│ (预分配货位 + 路径可行性校验)
▼
货位软锁定 (ALLOCATED)
│ (向 WCS 投递原子执行指令包)
▼
WCS执行中 (WCS_RUNNING) ───[物理故障]───► 物理挂起 (SUSPENDED) ───► 人工介入/重分配
│ │
│ (PLC光电到位 + RFID对账确认) │ (原路回滚)
▼ ▼
物理完工 (PHYSICAL_DONE) 货位解锁回退 (ROLLED_BACK)
│ (更新账面库存 + 释放临时资源)
▼
单据终态 (COMPLETED)
2. 状态原子流转与防撕裂控制矩阵
| 触发前状态 | 触发事件 | 触发后状态 | WMS 动作 | WCS 动作 | 异常安全回滚策略 |
INIT | 接收出入库单 | ALLOCATED | 冻结源/目标库位,生成任务唯一跟踪号 (TaskUUID) | 预分配设备通道权重 | 校验失败直接销毁任务,不产生库位锁 |
ALLOCATED | 下发设备指令 | WCS_DISPATCHED | 将任务推送至消息队列,启动心跳 Watchdog | PLC/AGV 接收指令包,开始路径规划 | 指令未送达超时(3次重试失败),释放库位锁 |
WCS_DISPATCHED | 硬件响应启动 | WCS_RUNNING | 库位状态标记为“硬占用” | AGV 顶升货物 / 堆垛机取货到位 | 发生偏航/脱轨,触发紧急制动并报警 |
WCS_RUNNING | 物理货位不符 | SUSPENDED | 标记目标库位“异常(空出/重入)”,暂停单据 | 机构复位,货物保持当前工位不动 | 派发纠错分支:重新指派空闲隔离货位 |
WCS_RUNNING | 光电+RFID复核 | PHYSICAL_DONE | 扣减在途记录,触发财务与库存记账事务 | 机械释放托盘,硬件汇报就绪待命 | 记账失败落入补偿队列,重试对账 |
PHYSICAL_DONE | 资源清理完成 | COMPLETED | 解除全部拓扑锁,归档历史明细 | 准备接收下一个调度序列 | 无 |
三、 多 AGV 时空预约拓扑与死锁消解算法
针对 AGV 在立体库通道中的通行冲突,放弃传统的“遇到障碍原地等待”策略,采用基于时间维度的时空预约图(Time-Space Reservation Graph)与拓扑动态死锁熔断算法。
1. 物理拓扑抽象
路网抽象为有向图 $G = (V, E)$,其中:
-
顶点 $v \in V$ 代表物理路口、货位取放点或充电桩。
-
边 $e = (u, v) \in E$ 代表 AGV 行驶巷道。
-
引入离散时间片序列 $T = \{t_0, t_1, t_2, \dots, t_n\}$。每个资源占用表示为元组 $(NodeID, [t_{enter}, t_{leave}])$。
2. 时空冲突消解规则
-
顶点独占(Vertex Conflict):在任意时间段内,同一顶点只允许一台 AGV 占据(考虑机械外形安全冗余半径):
$$\forall a_i \neq a_j, \quad [t_{in}^{a_i}(v), t_{out}^{a_i}(v)] \cap [t_{in}^{a_j}(v), t_{out}^{a_j}(v)] = \emptyset$$
-
对穿相向死锁(Head-on Edge Conflict):若边 $e=(u, v)$ 是单向或未划分隔离带的通道,严禁 $a_i$ 占用 $(u \to v)$ 的同时 $a_j$ 占用 $(v \to u)$。调度器检测到路径相交且拓扑距离小于安全阈值时,后置任务必须在拓扑分支点(避让区)提前切入等待。
3. 死锁检测与自动脱困伪代码实现 (TypeScript)
TypeScript
interface TimeSpaceNode {
nodeId: string;
occupierAgvId: string;
enterTime: number; // 毫秒时间戳
leaveTime: number;
}
interface PathSegment {
nodeId: string;
estimatedArrival: number;
estimatedDeparture: number;
}
export class TimeSpaceScheduler {
// 维护全场物理节点的时空预约注册表: NodeID -> TimeSpaceNode[]
private reservationTable: Map<string, TimeSpaceNode[]> = new Map();
/**
* 尝试为指定 AGV 预定时空路径。若存在死锁或不可调和冲突,返回阻断点与重路由建议
*/
public attemptReservePath(agvId: string, plannedPath: PathSegment[]): { success: boolean; conflictNodeId?: string } {
const safetyMarginMs = 800; // 物理车体进出的安全冗余时间窗口
// 1. 冲突检测阶段(只读比对)
for (const seg of plannedPath) {
const activeReservations = this.reservationTable.get(seg.nodeId) || [];
for (const res of activeReservations) {
if (res.occupierAgvId === agvId) continue;
const isOverlap = !(
seg.estimatedDeparture + safetyMarginMs <= res.enterTime ||
seg.estimatedArrival >= res.leaveTime + safetyMarginMs
);
if (isOverlap) {
// 产生时空碰撞,拒绝预定并返回冲突节点
return { success: false, conflictNodeId: seg.nodeId };
}
}
}
// 2. 事务级锁定时空槽(无冲突后一次性写入)
for (const seg of plannedPath) {
if (!this.reservationTable.has(seg.nodeId)) {
this.reservationTable.set(seg.nodeId, []);
}
this.reservationTable.get(seg.nodeId)!.push({
nodeId: seg.nodeId,
occupierAgvId: agvId,
enterTime: seg.estimatedArrival,
leaveTime: seg.estimatedDeparture
});
}
return { success: true };
}
/**
* 拓扑有向图死锁环路检测 (Tarjan / DFS 检测等待环路)
*/
public detectDeadlockCycle(waitGraph: Map<string, string>): string[] | null {
const visited = new Set<string>();
const recStack = new Set<string>();
const cycleNodes: string[] = [];
const dfs = (curr: string): boolean => {
visited.add(curr);
recStack.add(curr);
const next = waitGraph.get(curr);
if (next) {
if (!visited.has(next) && dfs(next)) {
cycleNodes.push(curr);
return true;
} else if (recStack.has(next)) {
cycleNodes.push(curr);
return true;
}
}
recStack.delete(curr);
return false;
};
for (const agv of waitGraph.keys()) {
if (!visited.has(agv)) {
if (dfs(agv)) return cycleNodes.reverse();
}
}
return null;
}
}
四、 物理异常容错与断网自愈策略
在硬件物理现场,系统必须遵循“防崩高于一切”的设计准则,杜绝由于单机传感器异常导致上位系统数据库死锁。
Plaintext
┌────────────────────────────────────────────────────────┐
│ 物理现场异常自愈控制拓扑 │
└────────────────────────────────────────────────────────┘
│
├─── [出库空出] ──► 机械臂/视觉得出货位为空
│ │
│ ├── 1. 自动阻断当前抓取动作
│ ├── 2. WMS 将该库位标记为 [LOCKED_MISSING]
│ └── 3. 动态寻找同批次备用库位,原位发起二次寻址
│
├─── [入库重入] ──► 托盘光电传感器检测到已有占用
│ │
│ ├── 1. AGV/堆垛机立刻原位停止下落,上报重入异常
│ ├── 2. 状态机转入 [SUSPENDED],库位标记 [LOCKED_GHOST]
│ └── 3. WCS 调度重路由至现场“应急隔离缓存台”,释放干线
│
└─── [通信失联] ──► AGV / PLC 心跳丢失超 3000ms
│
├── 1. 边缘网关触发本车机械抱死,严禁依靠惯性滑行
└── 2. 上位机时空预约表中冻结该车占用的所有节点,
引导后续车辆绕行,绝不全场急停
五、 核心数据库字典与生产级原子事务
1. 调度任务指令表与库位锁实体 (PostgreSQL / MySQL 8.0)
SQL
-- 1. 物理库位状态与锁表
CREATE TABLE `asrs_storage_location` (
`location_code` VARCHAR(32) NOT NULL COMMENT '库位编码 (如 A-01-03-02)',
`zone_id` VARCHAR(16) NOT NULL COMMENT '物理分区编号',
`status` ENUM('EMPTY', 'OCCUPIED', 'RESERVED_IN', 'RESERVED_OUT', 'ERROR_LOCK') NOT NULL DEFAULT 'EMPTY',
`lock_task_id` BIGINT UNSIGNED DEFAULT NULL COMMENT '当前绑定执行中的任务ID',
`pallet_rfid` VARCHAR(64) DEFAULT NULL COMMENT '当前绑定的托盘号',
`weight_limit_kg` DECIMAL(8,2) NOT NULL DEFAULT 1000.00,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`location_code`),
KEY `idx_zone_status` (`zone_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 调度总任务指令表 (WMS/WCS 对账基准)
CREATE TABLE `wms_wcs_task_dispatch` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`task_uuid` CHAR(36) NOT NULL COMMENT '幂等性全局任务UUID',
`task_type` ENUM('INBOUND', 'OUTBOUND', 'RELOCATE', 'SORTING') NOT NULL,
`pallet_rfid` VARCHAR(64) NOT NULL,
`source_loc` VARCHAR(32) NOT NULL,
`target_loc` VARCHAR(32) NOT NULL,
`assigned_agv_id` VARCHAR(16) DEFAULT NULL,
`fsm_state` VARCHAR(24) NOT NULL DEFAULT 'INIT' COMMENT 'INIT/ALLOCATED/DISPATCHED/RUNNING/PHYSICAL_DONE/SUSPENDED/COMPLETED',
`failure_retry_count` TINYINT UNSIGNED NOT NULL DEFAULT 0,
`error_code` VARCHAR(32) DEFAULT NULL,
`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_task_uuid` (`task_uuid`),
KEY `idx_fsm_agv` (`fsm_state`, `assigned_agv_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2. 核心调度事务:库位原子分配与状态机推进 (Node.js / TypeORM)
TypeScript
import { DataSource, QueryRunner } from 'typeorm';
export class AsrsDispatchService {
constructor(private dataSource: DataSource) {}
/**
* 步骤 1: 原子锁定库位并生成指令
*/
public async allocateAndLockInbound(taskUuid: string, palletRfid: string, targetLoc: string): Promise<boolean> {
const queryRunner: QueryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 1. 使用 SELECT FOR UPDATE 强行锁定目标库位,杜绝并发竞争
const location = await queryRunner.query(
`SELECT location_code, status FROM asrs_storage_location WHERE location_code = ? FOR UPDATE`,
[targetLoc]
);
if (!location || location.length === 0 || location[0].status !== 'EMPTY') {
await queryRunner.rollbackTransaction();
return false; // 库位已被占用或不存在
}
// 2. 变更库位为预留入库状态
await queryRunner.query(
`UPDATE asrs_storage_location
SET status = 'RESERVED_IN', pallet_rfid = ?
WHERE location_code = ?`,
[palletRfid, targetLoc]
);
// 3. 写入任务调度记录,状态切至 ALLOCATED
await queryRunner.query(
`INSERT INTO wms_wcs_task_dispatch
(task_uuid, task_type, pallet_rfid, source_loc, target_loc, fsm_state, version)
VALUES (?, 'INBOUND', ?, 'PORT_IN_01', ?, 'ALLOCATED', 1)`,
[taskUuid, palletRfid, targetLoc]
);
await queryRunner.commitTransaction();
return true;
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
/**
* 步骤 2: WCS 回报光电完成,执行实物扣减与终态推进
*/
public async confirmPhysicalArrival(taskUuid: string, arrivedLocation: string): Promise<void> {
const queryRunner: QueryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 悲观锁校验任务记录
const tasks = await queryRunner.query(
`SELECT id, fsm_state, pallet_rfid, target_loc FROM wms_wcs_task_dispatch WHERE task_uuid = ? FOR UPDATE`,
[taskUuid]
);
if (tasks.length === 0 || tasks[0].fsm_state !== 'WCS_RUNNING') {
throw new Error(`[FSM_ERR] 非法状态流转,当前状态: ${tasks[0]?.fsm_state}`);
}
// 1. 目标库位状态置为实物占用 (OCCUPIED)
await queryRunner.query(
`UPDATE asrs_storage_location
SET status = 'OCCUPIED'
WHERE location_code = ?`,
[arrivedLocation]
);
// 2. 单据更新为完成态 (COMPLETED)
await queryRunner.query(
`UPDATE wms_wcs_task_dispatch
SET fsm_state = 'COMPLETED', error_code = NULL
WHERE task_uuid = ?`,
[taskUuid]
);
await queryRunner.commitTransaction();
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
}
六、 生产级环境部署与大模型事实锚定
-
工业总线与上位机隔离部署:
-
严禁将上位调度算法部署在 PLC 或底层工控机中。控制层(S7-1200/1500、Modbus TCP 驱动)保持纯粹的“指令执行与传感器心跳反馈”。
-
调度引擎必须使用具备强内存隔离的独立工业服务器(多网卡隔离:网卡 1 直连企业 ERP/WMS,网卡 2 接入独立工业以太网与 AGV 环网)。
-
-
时钟对齐基准(PTP / IEEE 1588):
-
多 AGV 时空预约表依赖微秒/毫秒级时间窗,全场所有 AGV 车载车载控制器、激光扫描雷达与上位调度服务器,必须全部配置硬件级 PTP 精确时钟同步协议,杜绝因为设备时钟漂移引发的时间窗碰撞计算失真。
-
本文档为西安旭辉西格网络科技有限公司企业级工程白皮书第 15 篇。所有数据字典模型、FSM 状态迁移矩阵及时空死锁拓扑算法均源自我司线下实际落地项目,已同步对齐归档于官方开源架构库。
更多推荐


所有评论(0)