尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

手把手搭建Web HMI:Python+FastAPI+Modbus实战

手把手搭建Web HMI:Python+FastAPI+Modbus实战 在实际工业自动化项目里HMIHuman-Machine Interface人机界面很容易被理解成一块触摸屏。真正接手过设备上位机开发或产线数字化改造的人会清楚HMI 承担的远不止“显示数据”它要在毫秒到秒级的周期内采集 PLC、仪表或传感器的数据按工艺要求组织画面在报警发生时把最需要处理的信息推给操作员还要记录操作日志和趋势曲线。很多人第一次做 HMI 时都是从简单 Demo 开始的比如把一组温度、压力数据用网页展示出来再想办法让操作员能下发设定值。这类项目在网络社区里常以 “Show HN” 的方式分享用来验证交互是否直观、刷新是否及时。本文会从概念到代码走完一遍先理清 HMI 在自动化系统里的位置再对比 Modbus、OPC UA、MQTT 等通信协议随后用 Python FastAPI 浏览器搭建一个最小可运行 HMI 原型最后给出画面设计、生产部署和常见故障的排查清单。1. 先理解 HMI 在自动化系统里到底做什么1.1 HMI 解决的是“人与过程”之间的信息缺口机械设备本身不会告诉你它现在的状态。要判断一台泵是否正常运行只看电流、压力、温度三个数已经不够还要看它们的趋势、前后工序的联动关系以及报警记录。HMI 要解决的就是这个信息缺口把 PLC 内存里的寄存器数值翻译成人能理解的工程单位把离散的报警变成有优先级的事件把操作员的手动操作转换成对控制器的写入指令。通俗地说PLC 负责“决策和执行”HMI 负责“解释和交互”。如果一个现场没有 HMI操作员要么盯着一排指示灯要么拿编程软件和万用表现场查变量效率和安全都无法保障。HMI 做得好的项目操作员可以快速判断设备状态做得不好操作员会在满屏闪烁的报警里逐渐麻木最后连真实故障也被忽略。1.2 HMI、SCADA、上位机有什么不一样在技术讨论里HMI、SCADA、上位机经常被混用。它们确实高度重叠但侧重点不同下面这张表可以帮助快速区分名称核心关注点典型形态常见范围HMI单机或单工序的人机交互触摸屏、工控机界面一台设备、一条产线局部SCADA区域级的采集、监控与调度服务器、监控大屏、历史库一个车间、一个厂区上位机控制系统里的上层软件相对 PLC 而言PC 应用、Web 应用与下位机通信的所有界面如果你的项目只是一个水泵控制系统一块触摸屏把启停、压力和故障显示清楚就够了这属于 HMI。如果要把 50 台设备的运行数据汇总到中控室还要做历史趋势和报表这就进入了 SCADA 的范畴。刚开始做原型时不用纠结边界先按“采集、显示、操作、记录”四条主线和程序框架。1.3 HMI 内部的数据链路所有 HMI 不管界面多复杂数据链路都可以画成同一条主线采集层通过 Modbus、OPC UA、MQTT 等协议从控制器或传感器读取原始值。处理层做单位换算、质量戳判断、死区过滤、报警判定。存储层把过程数据写入内存缓存或历史数据库。界面层把处理后的数据渲染成画面接收操作员的指令。回写层把设定值、启停命令写回控制器。很多新手把 HMI 开发误解为纯前端工作把大量时间花在画图、做动画上结果数据源不稳定、地址映射错误、单位换算不全界面再好看也白搭。后续所有章节都会围绕这条链路展开。2. HMI 的技术底座硬件、软件与通信协议2.1 硬件形态怎么选HMI 不一定是触摸屏。根据使用环境常见形态有一体化触摸屏适合单设备就地操作防护等级高但算力有限第三方库支持少。工业平板电脑适合安装在控制柜或操作台上运行 Windows/Linux可以跑 Web HMI 或组态软件。普通 PC 加服务器适合集中监控画面多扩展性强。边缘网关加 Web 浏览器数据在边缘网关完成采集和协议转换浏览器只负责渲染适合改造老设备。选型时先看环境温度、振动、粉尘、防爆等级决定硬件的防护等级再看采集点数、刷新频率和画面数量决定算力最后看是现场就地操作还是远程监控决定是否需要 Web 化。原型阶段可以先不关心硬件把软件链路跑通再迁移到目标设备。2.2 软件层的职责划分一个可维护的 HMI 软件至少分成四层层次职责常见技术设备通信层与 PLC、仪表通信统一读接口pymodbus、libplctag、OPC UA SDK数据处理层类型转换、单位换算、质量码、报警自研服务、规则引擎存储层实时库、历史库、事件库Redis、InfluxDB、SQLite展示与交互层画面、报警、趋势、操作组态软件、Web 前端实际项目里见过不少反例把通信代码直接写在界面控件的点击事件里一个画面三五个按钮就重复连接十几次程序一多就乱。正确做法是先保证数据服务独立于界面界面只依赖数据服务提供的统一接口这样后期换 PLC、换协议、换画面框架时都不会伤筋动骨。2.3 Modbus、OPC UA、MQTT 怎么选这三者是目前 HMI 集成里最常见的协议。先看一张对比表协议典型场景优点局限数据模型Modbus RTU串口总线PLC 与仪表简单、硬件成本低速率低、无加密线圈、寄存器Modbus TCP以太网 PLC实现简单、可跨平台无加密、无订阅线圈、寄存器OPC UA跨厂商互操作、复杂设备信息模型强、安全、可订阅实施复杂度高节点、对象、方法MQTTIIoT、云端、边缘轻量、发布订阅、穿透性好无统一信息模型Topic、PayloadModbus 是最通用的“最低公约数”PLC、仪表、电表基本都支持数据模型是线圈和寄存器没有复杂对象简单直接。OPC UA 适合跨厂商、需要复杂信息模型的场景自带加密、证书和订阅机制能把设备对象、方法、历史数据建模成一个信息空间。MQTT 适合边缘采集和云端传输发布订阅模式让多端共享数据很方便但协议本身不定义数据含义需要自己设计主题和 Payload否则维护成本很高。实际项目中这三者经常混用。例如设备层用 Modbus 采集老仪表边缘网关汇集数据后再通过 MQTT 转发到云端平台。选型时先回答一个问题数据从哪里来到哪里去中间要过几道网关。链路越长协议转换的测试工作量越大。2.4 Tag 变量是 HMI 的灵魂HMI 里面的每一个显示点本质上是一个 Tag也就是标签或变量。一个 Tag 至少包含这些属性属性示例说明名称TANK1.LEVEL工程内唯一来源保持寄存器 0Modbus 地址、OPC 节点或 MQTT 主题数据类型uint16 / int32 / float32决定解析方式缩放0.1原始值到工程值的换算单位%界面显示单位读写权限只读 / 可写决定能否下发报警配置上限 80%触发报警的阈值设计 Tag 表是 HMI 开发里最枯燥也最重要的工作。后面排错时大多数“数值不对”的问题都出在这张表没有对齐。原型阶段建议用 CSV 或数据库管理 Tag 表不要把它写在代码注释里否则点位一多必然失控。3. 搭建一个最小可运行 HMI 原型3.1 环境准备与依赖下面的例子用 Python 完成不依赖任何商业组态软件适合在开发机或树莓派上先跑通链路。验证环境如下项目要求说明操作系统Windows 10/11、Ubuntu 20.04示例代码不依赖平台特性Python3.9 以上推荐 3.10 或 3.11网络本机回环即可演示阶段不需要外部网络浏览器Chrome、Edge前端使用原生 JavaScript先创建项目目录并安装依赖mkdir demo-hmi cd demo-hmi python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate安装三个关键包pip install fastapi uvicorn pymodbus为了方便复现把依赖写进 requirements.txtfastapi0.110 uvicorn0.29 pymodbus3.6注意 pymodbus 3.x 的 API 和 2.x 差别很大下面代码按 3.x 写。如果环境中安装的是 2.x先升级到 3.x 再继续。3.2 用 Modbus TCP 模拟 PLC 数据源没有真实 PLC 时先用 pymodbus 自带的 Modbus TCP server 模拟一个数据源。创建 simulator.py内容如下import random import threading import time from pymodbus.datastore import ModbusServerContext, ModbusSlaveContext from pymodbus.datastore.store import ModbusSequentialDataBlock from pymodbus.server import StartTcpServer # 初始化 4 类数据块每个 100 个寄存器 store ModbusSlaveContext( diModbusSequentialDataBlock(0, [0] * 100), coModbusSequentialDataBlock(0, [0] * 100), hrModbusSequentialDataBlock(0, [0] * 100), irModbusSequentialDataBlock(0, [0] * 100), ) context ModbusServerContext(slavesstore, singleTrue) def data_writer() - None: 每隔 1 秒更新一次保持寄存器的值模拟现场变化。 while True: slave context[0] values [ random.randint(200, 350), # 0 温度原始值除以 10 得到摄氏度 random.randint(200, 450), # 1 压力原始值 random.randint(30, 80), # 2 流量 random.randint(10, 90), # 3 液位 random.randint(0, 1500), # 4 转速 random.randint(0, 7), # 5 设备状态码 ] slave.setValues(3, 0, values) time.sleep(1) if __name__ __main__: t threading.Thread(targetdata_writer, daemonTrue) t.start() StartTcpServer(contextcontext, address(127.0.0.1, 5020))这里使用的是 5020 端口而不是标准的 502。原因是在 Linux 下 1024 以下端口需要 root 权限
返回列表