MES库口调度状态机设计实战:从需求梳理到全场景仿真

0 阅读7分钟

一、引子:一个调度场景的复杂度,远比我预想的高

前阵子在做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_CALLBACKWMSWMS操作完成回调
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余料退库、空托退库、直接出库
失败回退4WMS失败、编码为空、任务防重、写入失败
并发闯入9回调在await期间、TIMEOUT闯入、重复请求等
异常恢复6ERROR自动恢复、人工复位、系统重启、回调恢复
边界条件9链式执行上限、全0填充识别、状态覆盖等

共31个场景,全部验证通过。

为什么要在仿真上花这么多功夫?因为现场测试的成本太高了——联系PLC人员、协调立库人员、调度岗位工配合,一次测试至少半天。如果代码带着bug上现场,半天时间全浪费在排错上,留给验证的时间反而不够。

先仿真再现场,是我在这次项目中学到的最重要的一课。

六、总结

  1. 调度程序的核心是状态机,不是if-else——if-else在2-3个分支时还能用,超过3个分支就是灾难

  2. 先画图再写代码——Excalidraw梳理流程,让AI确认理解正确,再写代码,减少返工

  3. 并发问题是调度程序最大的坑——回调丢失、状态闯入、死锁,这些都是并发场景下的典型问题

  4. 先仿真再现场——现场测试成本高、时间窗口紧,尽可能在仿真环境把bug清干净

  5. 状态机设计的关键是「谁触发、谁响应、谁负责」——每个状态只响应它应该响应的事件,不相关的信号直接忽略

写调度程序,最难的不是实现功能,而是把所有「不该发生」的场景都考虑进去。一个状态机好不好,不在正常流程走得多顺,而在异常情况是否兜得住。