订单不该停在屏幕上,它应该一路走到机台,变成一件出库的货。
项目背景
在此之前,线上运营和车间生产之间是断开的:订单在 ERP 里,生产在人手上。工人拿着导出的表格核对 SKU、手动贴面单、靠肉眼数件数,一个订单做完再翻到下一行。机台本身是自动的——打包机、传送带、气缸都在,但没有任何东西告诉它们「现在该做哪张订单的第几件」。
这个项目补的就是这一段。我从零搭起一套分布式产线控制系统:上层接订单、中层调度机台、下层直接驱动 PLC 和串口设备,让一张订单从拉取、分发、出料、扫码计数、打印面单到打包分拣全程有状态、可追踪、可回放。它不跑在云上,而是装在车间那台一体机里,断网也要继续出货。

生产中的机台。操作屏就架在打包机上,商品装袋封口后落到绿色传送带,往分拣段走。屏幕内容已虚化。
FastAPI · PostgreSQL
多 ERP 抽象接入层
节点注册 / 心跳 / Token
订单分发规则 · 面单顺序打印
拉单落库分发状态同步生产 worker 主循环
单写者 Supervisor + Journal
纯函数产线状态机
硬件 Actor 层 · 本地 JSON 存储
PLC(气缸 / 输出点)
伺服滚筒(上料槽出料)
变频传送带(调速 / 加减速)
面单打印机 · 工业读码器
激光打标机 · 分拣器
另有一层聚合中枢横跨多台机台:轮询各节点的只读接口合成快照, 通过 SSE 秒级推送车间大屏,手机端按机台管理上料槽,写操作再转发回对应节点。
软件最终要落到这些东西上:打包机与面单打印机、把包裹推下线的气缸、以及每次上报都对应一次计数的读码器。
自动打包机、机身上的面单打印机与卷膜
气缸推料杆与斜置分拣滑道
传送带上方的工业读码器与补光模组 串口、PLC 连接、打印机驱动都是独占资源:两个 HTTP 请求同时写一条 RS485 总线,得到的不是报错而是乱码,而乱码在物理世界里意味着传送带以错误频率启动。
所以每类设备都被封进一个单线程 actor:外部只能往有界队列里 submit,拿回一个 Future;队列满了直接拒绝而不是无限堆积;超时可放弃等待但不会打断已在执行的那一条。设备驱动本身因此不需要写任何锁。

每个 actor 都有一个对应的调试入口,可以脱离生产流程单独连、单独试。
车间的电脑会被人拔电、会被 Windows 半夜重启。软件崩了可以重启,但物料还卡在传送带上——如果重启后的程序不知道「上一单做到第 7 件」,操作工只能清空整条线重来。
产线状态因此收敛成唯一写者:所有变更以命令形式进队列串行处理,每次迁移先落盘 journal 再生效。进程重启时回放 journal 恢复到崩溃前的快照,由操作工确认后从断点续产,而不是从零开始。
「产线停了」在现场是四件完全不同的事:人按了暂停、缺料自动挂起、崩溃恢复后等待确认、设备报故障。用一个 isRunning 布尔量表达它们,结果就是操作工看着屏幕猜。状态被拆成带层级的 10 个点、归入 4 个族,前端按族决定按钮可用性,reducer 是不碰硬件的纯函数——整套状态逻辑可以在没有机台的笔记本上跑测试。
现场要调的每一个量都有对应的输入框和一个「测试」按钮——控制点、脉冲时长、气缸推出与收回、扫码去重窗口、传送带频率与加减速、按物流渠道的滚筒转向与转速。界面中的机台标识、内网地址与商品 SKU 已脱敏。
生产流程:出料、传送、扫码触发、计数、废弃、打印打包、分拣逐段配置
出料槽、封装器与气缸的控制点与时序配置
分拣路由与变频传送带的调速、加减速参数 现场读码器的「HTTP 模式」并不严格守协议——header 里会多出没有冒号的路径行,标准框架直接拒收。我在业务端口前面加了一层兼容网关:自动判断进来的是 HTTP 报文还是裸 TCP 数据,是前者转发给后端,是后者整段当条码送进计数器。设备侧因此只需填一个 IP 和一个端口。
网关同时把每一次进来的原始 bytes、HEX 和多编码解码结果留档,前端「原始信号调试」直接可见。现场换一台新读码器,不用抓包工具也能在网页里看清它到底发了什么。
每一次上报都对应一次计数。UDP 静默丢包不会报错,只会让这一单少计一件——货发出去了才发现少了,而日志里干干净净什么都查不到。这类「失败必须可见」的场景,宁可多花一条长连接。

光电传感器判定物件到位,读码器上报条码——两者共同决定这一件算不算数。面单已脱敏。
同一台机器有两种跑法:流水线让多单在传送带上并行流动追求吞吐,单单串行一次只做一单换取可控。两者需要的节拍完全不同——但它们共用同一套 PLC 地址和串口配置。所以参数按维度拆开:流程参数按模式各存一份快照,硬件与连接配置全局唯一。切模式即换整套节拍,不必逐项重填。
工厂的网不稳定是常态。整套系统按局域网自洽设计:任务落在机台本地,生产循环不依赖任何外网调用。账号中心签发的 token 由节点用公钥本地验签,认证中心暂时不可达也能登录开工;需要上报的记录先进磁盘队列,后台线程重试补传,服务端按幂等键去重——网络恢复后自动对齐,中间这段时间产线照跑。

大屏就摆在机台旁边,抬头即可看到。屏幕内容已虚化处理。
抬头一块屏,手里一块屏
单台机器的状态在上位机上看就够了,多台就不行。聚合中枢在所有节点之上加了一层:后台按固定间隔并发轮询各机台的只读接口,合成一份内存快照并落库,大屏通过 SSE 秒级接收。
轮询只碰不触发硬件的接口。像传送带状态这种一读就会真开 RS485 串口的调用,被明确排除在高频轮询之外——监控本身不该去干扰生产总线。
手机就是车间里的第二块屏
上料槽的状态变化几乎都发生在机器旁边,而不是办公桌前。操作工装完料要确认、发现卡料要标记异常、换单要改 SKU 和数量、调试时要单独试推一次料——这些动作如果都得跑回电脑前完成,系统就会被绕过去。
所以聚合中枢把这几件事做成了移动端优先的页面:手机连上车间局域网即可按机台操作,写操作经中枢转发回对应节点,和大屏读的是同一份快照。大屏只读免登录,手机端要登录。
现场不会有人开终端敲命令。正式版把三样东西合成一个双击就能装的应用:Tauri 提供原生窗口与进程管理,Vue 前端由 Vite 编译后直接装进 WebView,FastAPI 经 Nuitka 编译成 standalone sidecar 随包发布——机台上不需要 Python 环境。
启动时 Tauri 先拉起 sidecar,确认本地端口就绪才显示主窗口;退出时同步关闭,Windows 侧用 Job Object 把后端绑定到应用生命周期,避免关掉窗口后还有一个进程在偷偷驱动传送带。正式版同时关闭接口文档、不注册调试路由。
打包机之外的工序按同样的「节点」模型接入。其中激光打标带来一个有意思的反转:厂商系统是拉取式的——设备视觉识别出产品后主动连过来问我们要内容,而不是我们把内容推给它。于是控制、内容、模板三件事各走一条 TCP 链路,驱动层做成可插拔,能在厂商系统、直连打标板卡和纯仿真之间切换而上层零改动。
我在这个项目里学到的
Web 里一次失败请求刷新就好,产线上一次错误的输出点脉冲是一件报废的货。设计的重心从「怎么恢复」前移到「怎么让非法状态压根无法表达」。
文档写着支持 HTTP,实际报文不合规范。真正能落地的做法不是要求设备改,而是在自己这一侧多留一层兼容和一份原始信号留档。
能跑通不等于能交付。一条命令上线新机台、双击安装、关窗口不留残留进程,这些决定了系统是被用起来还是被绕过去。
本页为脱敏介绍:不包含源码、仓库地址、真实客户与厂商名称、设备地址、凭据与授权机制细节。