一、引子:一个调度场景的复杂度,远比我预想的高
前阵子在做MES项目的库口调度程序时,遇到一个让我重新思考「调度程序怎么设计」的问题。
场景是这样的:组对点焊工序的库口,需要和WMS立库对接,完成三类调度任务——空托入库、余料退库、物料申请。这三类任务不是独立运行的,它们共享同一个库口、同一套PLC信号、同一套WMS接口。
最开始我脑子里想的是:「不就是一个根据信号判断走哪个分支的if-else吗?」
但真正开始写的时候才发现——并发闯入、回调丢失、状态死锁、日志膨胀、异常恢复……这些词听起来挺高大上,但落到代码里,就是一个个能让调度系统直接崩掉的bug。
而且更关键的是,现场测试一次要协调PLC人员、立库人员,调度一次岗位工配合,半天时间就没了。如果代码带着bug上现场,浪费时间不说,还影响别人对你的信任。
所以这篇文章,就是记录我怎么从「if-else」的思路,一步步走到「状态机+全场景仿真」的过程。希望能给正在做MES调度开发的同行一些参考。
二、需求分析:三类调度任务
先看看这个库口要处理哪些调度任务:
| 任务类型 | 触发条件 | 说明 |
|---|---|---|
| 物料申请 | 要料信号=1 | 产线需要新物料,向WMS申请出库 |
| 空托入库 | 退空托信号=1 | 产线用完的空托盘,退回立库 |
| 余料退库 | 退余料信号=1 | 产线剩余物料,退回立库 |
这三类任务共享同一个库口,意味着同一时刻只能执行其中一个。如果A任务还没执行完,B任务又触发了,就会乱套——这就是调度程序的核心挑战。
三、设计阶段:先画图,再写代码
1. 用白板梳理业务流程
我没有直接写代码,而是先用Excalidraw把整个流程画了出来——MES、WMS、PLC三方的信息流交互,每个状态下各方在做什么,一目了然。
画完白板后,我让AI读取白板内容,确认理解是否正确。这一步很重要:如果AI理解错了流程图,后面的代码注定是错的。
2. 状态机设计
梳理完流程后,我设计了一个9个状态的状态机:
S.IDLE → 空闲(初始状态)
S.CHK_WMS → 检查WMS状态
S.EMPTY_RET → 空托退库中
S.REMAIN_RET → 余料退库中
S.OUT_REQ → 出库请求中
S.ARRIVED → 出库到位
S.CTN_CHK → 连续检查
S.ERROR → 异常
S.EXIT → 结束
每个状态能响应的事件:
| 事件 | 来源 | 说明 |
|---|---|---|
| PLC_REQUEST_ON | 产线PLC | 请求调度(携带任务类型) |
| PLC_REQUEST_OFF | 产线PLC | 请求撤销 |
| WMS_CALLBACK | WMS | WMS操作完成回调 |
| TIMEOUT | 系统 | 超时自动推进 |
| MANUAL_RESET | 人工 | 人工复位 |
| INIT | 系统 | 系统初始化 |
| CTN_CHECK_OK | 系统 | 连续检查通过 |
3. 状态转移图的核心逻辑
IDLE → 收到请求 → 根据任务类型进入对应状态
→ 空托退库 → 回调 → 出库请求 → 到位 → 写PLC → 结束
→ 余料退库 → 回调 → 出库请求 → 到位 → 写PLC → 结束
→ 出库请求 → 到位 → 写PLC → 结束
→ 任何状态异常 → ERROR → 人工复位 → IDLE
四、调试实录:踩了6个坑,修了6个bug
设计完看起来挺完美的,但在仿真验证时,发现了一堆问题。这才是本文最有价值的部分。
坑1:回调在await期间丢失
现象:状态机发出WMS请求后,进入await等待,此时WMS回调回来了,但状态机还在await中,没有处理回调,回调「丢了」。
根因:setState写在了回调处理函数内部,而回调到达时状态机还没执行到那里。
修复:setState提前到发出请求之前,这样回调到达时状态已经正确匹配。
// 改前:发出请求 → await → 收到回调 → setState
// 改后:setState → 发出请求 → await → 收到回调(状态已匹配,直接处理)
坑2:状态被无关信号闯入
现象:状态机正在执行空托退库(状态30),PLC突然发了一个REQUEST_OFF信号,状态机误判为「任务取消」,提前退出了。
根因:所有信号都进了同一个入口,没有按「当前状态」过滤。
修复:加guard拦截——只有IDLE状态才能响应REQUEST_ON,只有等待状态才能响应REQUEST_OFF。
坑3:等待状态死锁
现象:状态机发出出库请求后,进入状态60等待PLC的REQUEST_OFF信号。如果PLC因为某些原因没发这个信号,状态机就永远卡在这里。
修复:加TIMEOUT机制——超过设定时间后自动推进到下一个状态(状态70→结束),而不是无限等待。
坑4:日志膨胀
现象:状态30/40/50/70这些中间状态,每次状态变化都写一条dbLog,导致日志表被疯狂写入。
修复:只有在关键节点(开始/结束/异常)才写日志,中间状态变化跳过dbLog。
坑5:异常恢复路径缺失
现象:状态机进入ERROR状态后,需要人工复位才能恢复。但复位后,状态机直接回到IDLE,之前未完成的异步任务还在跑,导致状态混乱。
修复:在复位逻辑中,清理所有未完成的异步操作,确保状态机回到干净的初始状态。
坑6:WMS回调被旧脚本拦截
现象:WMS回调到达时,被旧版本的请求式计算脚本截获了,状态机根本没收到回调。
修复:新增专用的回调处理函数,确保回调直接路由到状态机,不走旧脚本。
五、全场景仿真验证
修完6个bug后,我做了系统性仿真验证,覆盖范围:
| 类别 | 场景数 | 说明 |
|---|---|---|
| 正常流程 | 3 | 余料退库、空托退库、直接出库 |
| 失败回退 | 4 | WMS失败、编码为空、任务防重、写入失败 |
| 并发闯入 | 9 | 回调在await期间、TIMEOUT闯入、重复请求等 |
| 异常恢复 | 6 | ERROR自动恢复、人工复位、系统重启、回调恢复 |
| 边界条件 | 9 | 链式执行上限、全0填充识别、状态覆盖等 |
共31个场景,全部验证通过。
为什么要在仿真上花这么多功夫?因为现场测试的成本太高了——联系PLC人员、协调立库人员、调度岗位工配合,一次测试至少半天。如果代码带着bug上现场,半天时间全浪费在排错上,留给验证的时间反而不够。
先仿真再现场,是我在这次项目中学到的最重要的一课。
六、总结
-
调度程序的核心是状态机,不是if-else——if-else在2-3个分支时还能用,超过3个分支就是灾难
-
先画图再写代码——Excalidraw梳理流程,让AI确认理解正确,再写代码,减少返工
-
并发问题是调度程序最大的坑——回调丢失、状态闯入、死锁,这些都是并发场景下的典型问题
-
先仿真再现场——现场测试成本高、时间窗口紧,尽可能在仿真环境把bug清干净
-
状态机设计的关键是「谁触发、谁响应、谁负责」——每个状态只响应它应该响应的事件,不相关的信号直接忽略
写调度程序,最难的不是实现功能,而是把所有「不该发生」的场景都考虑进去。一个状态机好不好,不在正常流程走得多顺,而在异常情况是否兜得住。