当前位置: 首页 > news >正文

设备状态机怎么写才不乱:挡门、动作、选路,比框架更先要学会

这篇解决一个问题:设备软件里几乎每个模块都是while + switch,写着写着变成面条。判断该放哪、失败该不该改步、结批该塞哪个case。先把每个状态的形状固定下来,再谈要不要上框架。

一、设备流程为什么天然是状态机

设备不是一次性函数。搬运台车、手臂、料道、测试台,都是「做完 A 再做 B,中间还要等、还可能改道」。

一条很常见的物理路径:

去取料位 → 取料 → 去工作位 → 等对方接手 → 去出料位 → 再回到取料位

中间还会插例外:

  • 这盘是空盖盘,取完直接出,不进工作位。
  • 已经开始结批,车上还有料,跳过工作位直接出。
  • 工位被别人占着,这一拍先别动,下一圈再抢。

所以设备控制几乎都会落成状态机:当前在哪一步、这一步能不能开始、做完去哪、失败怎么办。

问题不在「用不用状态机」,而在 一个case里把所有事情搅在一起。


二、面条式状态机为什么难改

典型写法是一个大循环套一个大switch

while (running) { Sleep(1); switch (step) { case 某步: if (资源锁失败) break; if (结批) { step = 另一步; break; } ret = 动作(); if (ret 失败) { Alarm(); break; } if (特殊盘) step = A; else step = B; break; } }

一个case里同时出现五件事:

  1. 挡门:现在能不能开始
  2. 结批改道
  3. 真正动作
  4. 失败处理
  5. 成功后选路

看起来省事,维护时会痛在这几处:

  • 不知道新规则该塞哪个case
  • 同样是break,有的表示「下一圈重试」,有的表示「报警停住」,有的表示「已经改道」。
  • 动作做一半突然跳步,气缸还顶着、锁还占着。
  • 新人不敢改,因为改一步不知道会不会把别的路径带崩。

这就是面条式状态机:流程能跑,结构不可读。


三、最小可用手法:每个 case 只做三件事

先不换框架、不拆类。 只规定每个状态内部的形状:

挡门 → 动作 → 选路

case 某步: { // ① 挡门:这一步现在能不能开始? if (资源没抢到) break; // 重试,不改步 if (该跳过的前置条件) { step = 下一站; break; } // 改道 if (结批且本步不该开始) { 收尾; break; } // 拦截 // ② 动作:这一步真正要干的活 auto ret = 执行动作(); if (!ret.IsOK()) { Alarm(ret); StopMachine(); break; // 失败通常不改步,停在原处 } // ③ 选路:成功后,下一步去哪? if (特殊分支) step = 特殊下一站; else step = 默认下一站; break; }

记住一句就够:

没开始 → 挡门;做砸了 → 失败处理;做完了 → 选路。


四、一个新 if 该放哪:四问清单

以后每加一个判断,按这个顺序问:

放哪

这是让设备「现在先别动」吗?

资源没抢到、对方没就绪、结批后不该再取新料

① 挡门

这是动作执行失败吗?

轴超时、真空失败、气缸不到位

② 动作中间

这是动作成功后「下一步去哪」吗?

空盖盘走捷径、正常盘去工作位

③ 选路

这是和所有步都无关的全局条件吗?

急停、整机已结批、暂停

while循环头

不要凭感觉往最近的case里塞。位置错了,比条件写错更难查。


五、全局闸门不要进 case

急停、整机结批完成、暂停,属于「不管现在哪一步,都该先处理」。放到每个case里会重复、会漏。

while (running) { Sleep(1); if (急停) { 停机; continue; } if (整机已结批) { Stop(); continue; } if (!允许运行) { continue; } switch (step) { ... } }

循环头管「整机还让不让跑」。
case里只管「这一步自己的事」。

六、资源锁不是业务动作

设备里很多工位是共享的:顶升、Buffer、压台。常见写法是占用协议:

bool SetUsedBy(Actor* who) { lock_guard<mutex> lk(mtx); if (owner == nullptr || owner == who) { owner = who; return true; } return false; }

含义只有三句:

  • 没人占用 → 登记为自己,成功
  • 本来就是自己 → 成功(允许重入)
  • 别人在用 → 失败,本轮不要继续动

锁失败和动作失败不是一类事。

锁失败动作失败

含义

现在还不能开始

已经开始,但没做成

处理

break留原步,下圈再抢

报警,通常停机

改不改步

不改

通常不改,停在原步方便排查

最常见的错法:锁失败也Alarm,或者锁失败直接跳到下一步。前者误报,后者还没干活就假装做完了。


七、结批不是一个 if,而是一类规则

「结批」在状态机里至少有几种完全不同的意思,不能写成同一个判断到处粘贴。

类型放哪做什么改不改步

整机已经结完

循环头

停本模块

不改

空车且不该再取新料

挡门

标记本模块结批完成、放锁

不改

车上还有料,但工作位不必再去

挡门改道

跳过工作位,去出料

改道

正在等对方接手

挡门副作用

催对方结束,自己先等

通常不改

以后加结批规则,先问:这是总闸、拦截、改道,还是催下游? 再决定放循环头还是某个case的挡门段。

不要在动作做到一半时突然if (结批) step = ...。动作做一半就跳,最容易留下:锁没放、气缸还顶着、真空还开着。


八、一个 case 里塞了多个动作,就该拆子状态

取料经常不是一个动作,而是一串:

到取料位 → 顶升 → 建真空 → 分盘/松夹 → 下降

全写在一个case里会有三个后果:

  1. 失败时不知道卡在顶升还是真空。
  2. 暂停后再进来,会从头执行,可能重复顶升或重复吸真空。
  3. 界面只能显示「正在取料」,调试信息太粗。

做法是拆子状态:

enum class PickSub { MoveToPos, JackUp, BuildVacuum, Separate, JackDown, Done }; case Pick: { switch (sub) { case PickSub::JackUp: if (cylinderInPosition) sub = PickSub::BuildVacuum; break; case PickSub::BuildVacuum: if (vacuumOk) sub = PickSub::Separate; break; // ... } if (sub == PickSub::Done) step = NextStation; break; }

父状态表示「我在取料」,子状态表示「取料进行到哪一小步」。失败、暂停、恢复都有落点。

原则:一个 case 对应一个可命名的物理状态或原子动作。 一个名字里说不清,就该拆。

九、改步只能出现在少数位置

一个状态里,step = ...最多三类:

  1. 挡门改道:这一步确定不该做,直接去另一站。
  2. 失败后不改步:停在原处,方便看现场、看日志。
  3. 成功后选路:给一个默认下一站,特殊分支另写。

反例:一个case末尾四五处赋值,没有默认下一站。读代码的人要靠猜「哪条路才是主干」。

正例:

// 成功后选路 step = 默认下一站; if (空盖盘) step = 出料; if (结批且有盘) step = 出料;

主干清晰,例外是例外。


十、大厂怎么写,和现在该不该上框架

业界常见做法:

场景常见选择

C++ 设备控制

HFSM2、Boost.Statechart、Boost.MSM、sml

.NET

Stateless

模型驱动

Stateflow、SCXML 画图生成代码

PLC

SFC / 状态图

自研平台

事件队列 + 状态注册 + 转移注册

框架带来的变化是真实的:

面条式框架式

step是整数

状态是类型

转移散落在if

changeTo显式转移

进入/退出靠人记

框架调enter/exit

结批是到处if

可以升成父状态

历史靠自己记

父状态切换可重置子状态

但老项目不要一上来全量迁框架。原因也很实际:

  • 编译器、模板报错、团队习惯可能跟不上。
  • 一台设备往往有十几套状态机,全迁等于几个月加一批新 bug。
  • 框架解决的是「结构」,解决不了「判断放错位置」。判断放错,换框架一样乱。

更稳的路径:

阶段做法

现在

老模块用「挡门-动作-选路」整理,不换框架

新模块

新写的台车/手臂可以直接上层次状态机或自研小框架

提公共能力

资源占用包成 Guard,动作结果包成统一ActionResult

平台化

结构稳定后,再决定要不要统一框架

先把每个case写干净,再谈框架。 顺序反了,是用新语法写旧面条。


十一、一份可直接贴进规范的 checklist

每个状态机都应满足:

  1. 一个case对应一个可命名的物理状态或动作。
  2. 改步只出现在:跳过本步的挡门改道、失败后不改、成功后选路。
  3. 急停、整机结批、暂停放到while循环头。
  4. 资源锁放在case最开头,失败只重试。
  5. 失败先报警/停机,再决定是否改步;默认不改步。
  6. 一个case里不要串两个完整动作。
  7. 结批先分类(总闸 / 拦截 / 改道 / 催下游),再落点。

常见错误对照:

  • 动作中间写结批跳步 → 资源遗留。
  • 一个case两个动作 → 失败不知道卡在哪。
  • 全局闸复制进每个case→ 漏改。
  • 锁失败和动作失败同样报警 → 误报。
  • 下一步来源太多且没有默认站 → 读不懂主干。

十二、可复用结论

  1. 设备状态机的核心不是框架,而是 每个状态内部形状统一:挡门、动作、选路。
  2. 新判断先问四句:现在别动、做砸了、做完去哪、是不是全局闸。
  3. 资源锁失败是重试,动作失败是报警;两类break不能混。
  4. 结批不是一个 bool,而是总闸、拦截、改道、催下游四类规则。
  5. 一步里有子流程,就拆子状态,避免暂停恢复后重复动作。
  6. 老项目先整理case,新项目再上层次状态机;不要用框架掩盖结构混乱。

设备软件的状态机追求的不是「看起来高级」,而是改规则时知道改哪、失败时知道停在哪、新人敢动。while + switch可以很差,也可以很干净。差在把所有 if 倒进一个case;干净在先固定三段,再让框架成为可选项,而不是第一步。

http://www.cnnetsun.cn/news/4346881.html

相关文章:

  • Coze工作流批量生成AI美食视频:从节点配置到稳定出片的完整实践
  • 计算机毕业设计之基于AES的用户教学资源推荐系统
  • IDA Pro函数分析实战:从反汇编到逻辑重构的逆向工程方法论
  • 2026全新计算机毕设选题推荐(含创新点)
  • 多商户电商平台源码架构与二次开发核心要点解析
  • Win32老工具兼容性实战:QQ群成员提取器的修复与替代
  • TrueForge:从AI智能体原型到生产级服务的工程化框架
  • 实点科技受邀出席 2026中国机电一体化技术应用协会现场总线专业委员会委员代表大会 暨PROFINET和IO-Link技术路演
  • 破解B站播放量与完播率的底层逻辑:从算法机制到实战优化
  • Python开发教程:零基础也能秒变大神,别再走弯路了
  • ComfyUI+SDXL+单LoRA:从户型图到室内效果图的全流程实战
  • Dify工作流脚本化:用DSL实现批量修改与Git版本管理
  • ComfyUI+SDXL单LoRA工作流:从平面图到多风格室内效果图
  • android开发转到java后端开发--注解
  • CNC编程进阶:从第一个零件到稳定工作流的实战指南
  • MAG焊接常用哪些保护气体?能节省吗?
  • 国密二级电子签章和e签宝对比 政务采购选哪个合适
  • 四自由度机械臂轨迹规划实战:从Matlab仿真到工程落地全解析
  • Vibe Coding实战:从自然语言到可用代码的AI编程新范式
  • 2026 年选期货量化工具,Python 适配与否竟成关键
  • R语言实现潜在剖面分析(LPA)完整实操指南
  • 如何在 SOLIDWORKS 中实现工程图自动出图指南
  • 对话式生成里最被低估的一步:需求澄清与需求文档确认
  • 排球检测数据集构建全流程:从数据采集到YOLO训练调参实战
  • YOLOv8自建数据集实战:坦克目标检测训练与C++部署全攻略
  • 无需数据库的在线相册:UberGallery轻量部署实战指南
  • Cesium三维路径导航线实现:坐标插值、动态效果与性能优化
  • 2023携程秋招技术通用岗笔试复盘:考点、编程题与避坑指南
  • MATLAB实现GMM高斯混合模型聚类:从原理到代码实战
  • 广告花完就归零,GEO 知识资产:属于 B 端企业的长期数字无形资产