手把手搭建Web HMI:Python+FastAPI+Modbus实战
2026/8/30 6:06:30 网站建设 项目流程

在实际工业自动化项目里,HMI(Human-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 不管界面多复杂,数据链路都可以画成同一条主线:

  1. 采集层:通过 Modbus、OPC UA、MQTT 等协议从控制器或传感器读取原始值。
  2. 处理层:做单位换算、质量戳判断、死区过滤、报警判定。
  3. 存储层:把过程数据写入内存缓存或历史数据库。
  4. 界面层:把处理后的数据渲染成画面,接收操作员的指令。
  5. 回写层:把设定值、启停命令写回控制器。

很多新手把 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、Payload

Modbus 是最通用的“最低公约数”,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.txt:

fastapi>=0.110 uvicorn>=0.29 pymodbus>=3.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( di=ModbusSequentialDataBlock(0, [0] * 100), co=ModbusSequentialDataBlock(0, [0] * 100), hr=ModbusSequentialDataBlock(0, [0] * 100), ir=ModbusSequentialDataBlock(0, [0] * 100), ) context = ModbusServerContext(slaves=store, single=True) 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(target=data_writer, daemon=True) t.start() StartTcpServer(context=context, address=("127.0.0.1", 5020))

这里使用的是 5020 端口,而不是标准的 502。原因是在 Linux 下 1024 以下端口需要 root 权限,

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询