做工业设备调试的人,应该都有过这种体会:设备在车间跑着,手里却只有一把单串口调试工具。数据能看,但多个串口就得开多个窗口;曲线能抓,但没法持续保存成历史报表;报警要靠人盯着,出了问题再回头翻记录,往往已经晚了。等到要开发一套真正能在现场用的上位机时,问题从“怎么读寄存器”升级成了另一件事:怎么把数据采集、界面展示、异常预警和落盘存储,整合成一个稳定好用的系统。
这篇文章想聊清楚一件事:用 Python + PyQt5 开发一套支持多串口并发的 Modbus 工业上位机系统,到底难在哪里、怎么落地。我会从技术选型、线程模型、核心代码、常见坑和工程建议几个方面展开,代码可以直接复用。
我的一个明确判断是:“用 Python 读取 Modbus 寄存器”并不难,难的是让读取过程不卡界面、数据不错不乱、异常可追溯。这三件事对应到开发上,分别是线程模型设计、协议封装和 CSV 历史记录。只要前两件事做好了,这套系统基本就能达到试运行标准。
读完这篇文章,你能获得一套可以跑起来的完整工程框架,包含多串口管理、仪表台、实时曲线、阈值预警和 CSV 自动保存,也能知道在实际项目中怎么扩展成更稳定的生产级方案。
1. 这篇文章真正要解决的问题
先看一个典型场景:你调试一台设备,机柜里有一块 PLC、两块智能仪表,分别接在 COM5、COM6、COM7 三个串口上。用 Modbus Poll 这类工具,确实可以分别打开三个窗口,一个窗口看一路数据。但这种模式有两个明显问题。
第一,数据是割裂的。三个窗口互不相通,你想把同一时刻的三路数据对齐,只能肉眼比时间戳。第二,数据没有沉淀。多数调试工具默认只存内存里的最近几百条,不手动导出就没有历史文件。一旦设备半夜报警,第二天想复盘,很难找到完整依据。
“上位机系统”要解决的,正是这三个问题:统一接入多串口数据、实时可视化、自动留存历史并预警。
从开发成本看,这套系统真正降低的是“现场数据可视化”的门槛。传统做法可以用组态软件、LabVIEW 或 C# WinForm,前两者是商业授权,后者开发链路偏重。而 Python 生态下,PyQt5 负责界面,pymodbus 负责协议,pyqtgraph 负责曲线,csv 或 pandas 负责落盘,四个库就能拼出一条完整链路,开发周期短,验证成本低,非常适合设备调试、实验室测试、小型产线监控这类场景。
什么样的读者最应该读这篇文章?如果你是有 Python 基础、正在做设备数据采集或自动化项目的开发者;如果你是电气工程师,想用低成本方案替代部分组态软件;如果你已经写过单串口读取脚本,但不知道怎么把界面和采集线程组织起来——这篇文章会比较适合你。
2. 基础概念与核心原理
2.1 Modbus 协议到底是什么
Modbus 是工业领域最常见的通信协议,本质是一主多从式的请求响应模型。上位机作为主站发起请求,设备作为从站返回数据。协议层面对开发者最主要的感知点是:串口参数要匹配,从站地址要正确,寄存器类型要分清。
RTU 模式是串口通信的主流方式,报文结构包含地址、功能码、数据和 CRC 校验。TCP 模式则是把 Modbus 报文封装到 TCP 包里,常用于网口设备。
在寄存器层面,最常用的是保持寄存器。它按 16 位(一个字)存储,可读可写,用read_holding_registers读取。如果设备使用 IEEE 754 格式保存浮点数,读取到的两个字还需要做一次字节拼接和转换。这一点在对接真实设备时很常见。
2.2 为什么需要“多串口”上位机
一台工控机往往只配一个或两个物理串口,但现场设备可能有十几台。常见的解决方式是使用 USB 转串口集线器,在操作系统里扩展出多个 COM 口:COM5、COM6、COM7 等。每个串口相当于一条 Modbus RTU 总线,总线上可以挂多个从站地址。
所以“多串口上位机”的核心能力不是简单打开多个串口,而是对多条独立总线、多个从站地址进行统一轮询、统一解析和统一展示。如果一个串口超时,不应该阻塞其他串口的采集过程,这是架构设计里最重要的点。
2.3 PyQt5 的线程模型决定了系统拓扑
PyQt5 的界面更新必须发生在主线程,而 Modbus 串口读取是阻塞型操作,单次读取可能耗时几百毫秒甚至超时。如果把读取代码直接写在窗口线程里,界面会卡死,最常见的现象是拖动窗口时无响应。
正确做法是使用QThread + moveToThread,把轮询循环放到后台线程,通过信号把数据传回主线程。UI 只能通过信号槽机制更新。这不仅是 PyQt5 的规范,也是一套稳定上位机系统的根基。
| 维度 | 传统单串口调试工具 | Python + PyQt5 上位机 |
|---|---|---|
| 多串口处理 | 一般要开多个窗口,数据割裂 | 一套界面统一管理 |
| 历史数据 | 手动导出或无法留存 | CSV 自动按天分文件保存 |
| 实时曲线 | 部分工具支持,定制能力弱 | pyqtgraph 高性能绘制,可扩展 |
| 报警能力 | 依赖人工观察 | 阈值超限自动提示并记录 |
| 定制成本 | 功能固定,改造困难 | 源码完全可控,按需改动 |
3. 环境准备与前置条件
3.1 Python 与 PyQt5 环境安装
本文演示环境使用 Python 3.8 及以上版本,操作系统以 Windows 10/11 为主,但代码在 Linux 上也能运行,只需把串口号从COM5改成/dev/ttyUSB0等设备路径。
安装依赖建议使用 pip:
pip install pymodbus PyQt5 pyqtgraph pyserial其中pymodbus负责 Modbus 协议栈,PyQt5负责界面,pyqtgraph负责实时图表,pyserial是串口通信底层库。安装完成后可以快速验证:
import pymodbus import PyQt5 import pyqtgraph print("依赖检查通过")如果在国内网络环境下安装缓慢,可以临时使用镜像源,但这属于环境问题,不影响代码本身。
pip install pymodbus PyQt5 pyqtgraph pyserial -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 没有真实硬件如何验证
如果你手头没有设备,推荐准备两类辅助工具:
- Modbus 从站模拟器:例如 Modbus Slave 官方试用版,或各类开源模拟软件。用它创建一个从站,配置好串口参数、从站地址和寄存器初始值,即可模拟真实设备。
- 虚拟串口工具:在 Windows 上可以用虚拟串口软件创建成对的 COM 口,例如 COM5 和 COM6 是互通的一对。上位机连接 COM5,模拟器连接 COM6,就可以在不上硬件的情况下完成链路验证。
注意:Modbus 模拟器请使用官方试用版或授权版本,不要使用来历不明的破解工具。从安全和合规角度,这既是法律要求,也能避免引入携带恶意代码的第三方软件。
4. 系统架构与工程结构
4.1 模块划分
整套系统按职责拆成五个部分,每个文件只做一类事情,这是保证代码可维护的基础。
| 模块 | 文件 | 职责 |
|---|---|---|
| 设备配置 | config.py | 串口参数、从站地址、寄存器定义、量程和预警阈值 |
| 串口管理 | modbus_manager.py | 维护多个串口的 Modbus 客户端连接 |
| 采集线程 | worker.py | 后台轮询所有串口和从站,通过信号上报数据 |
| 主窗口 | main_window.py | 仪表台、实时曲线、数据表格、预警、CSV 保存 |
| 程序入口 | main.py | 创建 QApplication,启动主窗口 |
4.2 数据流链路
系统启动后,主线程创建 Modbus 客户端管理器,并为每个串口建立连接。然后创建QThread和ModbusWorker,把 WorkermoveToThread到子线程。子线程中跑一个 while 循环,按配置列表轮询各串口从站。
读取成功之后,Worker 通过data_received信号把数据包发给主线程。主线程的槽函数拿到数据包后,依次做四件事:更新仪表、更新曲线、检测报警、写入 CSV。这样数据从底层到界面是单向流动的,结构非常清晰。
4.3 串口配置示例
设备配置是整个系统的“地图”,直接定义上位机要访问哪些串口、哪些从站、哪些寄存器。新建config.py:
# config.py DEVICE_CONFIG = [ { "port": "COM5", "baudrate": 9600, "slave_id": 1, "address": 0, "count": 4, "names": ["温度", "压力", "流量", "转速"], "min_values": [0, 0, 0, 0], "max_values": [1000, 2000, 5000, 3000], "alert_min": [None, None, None, None], "alert_max": [80, None, 4000, None], }, { "port": "COM6", "baudrate": 9600, "slave_id": 2, "address": 0, "count": 2, "names": ["电压", "电流"], "min_values": [0, 0], "max_values": [500, 100], "alert_min": [None, None], "alert_max": [450, 90], }, ]这份配置说明:COM5 总线上有从站 1,从 0 号寄存器开始连读 4 个字,分别映射到温度、压力、流量、转速;COM6 总线上有从站 2,从 0 号寄存器开始连读 2 个字,映射到电压、电流。alert_max中温度超过 80 触发上限预警,流量超过 4000 触发预警。
5. 核心功能代码实现
5.1 多串口连接管理器
新建modbus_manager.py,负责创建和关闭多个串口的 Modbus 客户端。这里使用 pymodbus 3.x 以上的 API,通过from pymodbus.client import ModbusSerialClient导入。
# modbus_manager.py from pymodbus.client import ModbusSerialClient class ModbusClientManager: """管理多个串口的 Modbus 客户端,每个串口对应一个 client。""" def __init__(self): self._clients = {} def add_port(self, port, baudrate, parity="N", stopbits=1, bytesize=8, timeout=1): if port in self._clients: return self._clients[port] client = ModbusSerialClient( port=port, baudrate=baudrate, parity=parity, stopbits=stopbits, bytesize=bytesize, timeout=timeout, ) if not client.connect(): raise RuntimeError(f"无法打开串口 {port},请检查端口占用或串口参数") self._clients[port] = client return client def get_client(self, port): return self._