1. 工业数据采集系统的整体架构与设计思路
1.1 为什么选择开源工具链而不是商业组态软件
做过工业现场的人都知道,一提到数据采集,很多人第一反应是买一套组态软件,比如常见的商业上位机方案。但实际项目做下来,商业组态软件有几个绕不开的痛点:授权费用按点数算,采集点位一多成本直线上升;封闭的脚本环境,想做个自定义的数据清洗或者对接第三方数据库非常别扭;跨平台能力差,很多方案只能跑在特定操作系统上。
我这些年做下来,越来越倾向于用开源工具链自己搭一套。核心思路很简单:用Python做数据采集与处理的中枢,用Modbus协议作为现场设备的通讯底座,用轻量级Web服务做数据展示和接口输出。这套组合的好处是每一层都可以替换、可以扩展,而且几乎没有授权成本。
具体来说,整个系统的分层是这样的:
- 设备层:PLC、变频器、智能仪表、传感器,这些设备大多支持Modbus RTU(串口)或Modbus TCP(网口)。
- 采集层:Python脚本通过pymodbus等库轮询设备寄存器,拿到原始数据。
- 处理层:对原始数据进行量纲换算、滤波、报警判断、缓存。
- 存储层:写入时序数据库、关系型数据库,或者先落本地文件再批量入库。
- 展示层:用Flask/FastAPI起一个Web服务,浏览器直接看实时曲线和历史数据。
这个架构最大的价值在于解耦。采集挂了不影响展示,存储换了不影响采集逻辑。而且每一层你都能找到成熟的开源方案,不用从零造轮子。
1.2 多通道采集的核心需求拆解
标题里说的“多通道”,在实际工业场景里通常包含两层含义。第一层是多设备通道,比如一条产线上有1台西门子PLC加32台变频器,每台设备都是一个独立的通讯节点。第二层是多数据通道,同一台设备上要同时读取多个寄存器地址,比如电压、电流、频率、温度、运行状态等。
这两层需求叠加起来,对采集程序提出了几个硬性要求:
- 轮询调度要合理:不能一个设备读半天,导致其他设备数据刷新太慢。通常需要设置合理的超时和重试机制。
- 异常隔离:某台设备掉线不能拖垮整个采集循环,必须有独立的异常捕获和降级处理。
- 数据时间戳统一:多通道数据要能对齐到同一时间基准,否则后续分析会出现时序错乱。
- 并发与串行要分清:Modbus RTU在一条485总线上是串行的,不能并发;Modbus TCP可以多连接并发,但也要控制连接数。
我见过不少新手一上来就开几十个线程去读同一路485总线,结果数据全是乱的。这里的关键认知是:物理总线决定了通讯的串行性,软件层面的并发必须建立在物理层允许的基础上。
1.3 工具选型的逻辑与对比
在动手之前,先把工具链理清楚。下面这张表是我实际项目中反复验证过的组合:
| 环节 | 推荐工具 | 备选方案 | 选择理由 |
|---|---|---|---|
| 编程语言 | Python 3.10+ | C#、Go | 生态丰富,Modbus库成熟,开发效率高 |
| Modbus库 | pymodbus | minimalmodbus | 同时支持RTU和TCP,API统一 |
| Web框架 | FastAPI | Flask | 异步支持好,自带接口文档 |
| 数据库 | SQLite/PostgreSQL | InfluxDB | 小项目SQLite够用,大项目上PG或时序库 |
| 开发环境 | VS Code | PyCharm | 轻量,远程开发方便 |
| 串口调试 | Modbus Poll/Slave | QModMaster | 仿真和调试必备 |
| 权限管理 | gsudo | 系统自带 | Windows下提权执行串口操作 |
这里重点说一下为什么用pymodbus而不是minimalmodbus。minimalmodbus在单设备串口场景下确实简单,但一旦你要同时管理RTU和TCP、要处理多从站、要做异步采集,pymodbus的统一接口优势就体现出来了。它的ModbusClientMixin抽象让RTU和TCP的代码几乎一致,切换通讯方式只需要改一行初始化代码。
提示:Python安装时务必勾选“Add Python to PATH”,否则后续在VS Code终端里调用python命令会找不到。如果已经装错了,重新运行安装包选Modify修复即可。
2. 环境搭建与核心依赖配置
2.1 Python环境安装与VS Code配置的完整步骤
环境搭建这一步看似简单,但我在带新人的时候发现,至少一半的问题都出在这里。下面按顺序走一遍。
第一步,去Python官网下载3.10或3.11版本。为什么不推荐最新的3.12+?因为部分工业通讯库对最新版本的适配还没跟上,3.10和3.11是目前兼容性最稳的区间。下载时选Windows installer (64-bit),运行后务必勾选Add Python to PATH,然后点Install Now。
第二步,验证安装。打开命令行输入:
python --version pip --version两条命令都能正常输出版本号,说明安装成功。如果提示“不是内部或外部命令”,就是PATH没配好,手动把Python安装目录和Scripts目录加到系统环境变量里。
第三步,配置VS Code。安装VS Code后,装两个必备插件:Python和Pylance。然后在项目文件夹里按Ctrl+Shift+P,输入“Python: Select Interpreter”,选中你刚装的Python解释器。这一步做完,VS Code的终端里就能直接用python命令了。
第四步,创建虚拟环境。这一步很多人会跳过,但我强烈建议做:
python -m venv venv venv\Scripts\activate虚拟环境的好处是项目依赖隔离,不会因为不同项目需要的库版本冲突而互相干扰。激活后命令行前面会出现(venv)标识。
第五步,安装核心依赖:
pip install pymodbus fastapi uvicorn pyserial sqlalchemypyserial是pymodbus的RTU底层依赖,必须装。fastapi和uvicorn用于Web服务,sqlalchemy用于数据库操作。
2.2 Modbus通讯协议的关键概念梳理
在写代码之前,必须把Modbus的几个核心概念搞清楚,否则调试时会一头雾水。
寄存器类型是第一个要分清的。Modbus定义了四种数据区:
- 线圈(Coils,0x):可读可写的开关量,一个地址一位。
- 离散输入(Discrete Inputs,1x):只读开关量。
- 保持寄存器(Holding Registers,4x):可读可写的16位数据,最常用。
- 输入寄存器(Input Registers,3x):只读16位数据。
功能码对应关系是:读线圈用01,读离散输入用02,读保持寄存器用03,读输入寄存器用04,写单个线圈用05,写单个寄存器用06,写多个寄存器用16(十六进制0x10)。
地址偏移是新手最容易踩的坑。设备手册上写的地址通常是1-based的,比如“40001”表示第一个保持寄存器。但程序里用的是0-based偏移,所以40001对应程序里的地址0。这个转换关系一定要记牢,否则读出来的数据全是错位的。
字节序是第二个大坑。Modbus寄存器是16位的,但很多设备的数据是32位浮点数,占用两个连续寄存器。这时候就涉及高低字交换的问题。有的设备是高字在前,有的是低字在前,还有的要做字节交换。调试时如果发现读出来的浮点数完全不对,先检查字节序。
CRC校验在RTU模式下是必须的。pymodbus会自动处理CRC,但如果你用串口助手手动发报文调试,就要自己算CRC。Modbus CRC是16位的,多项式0xA001,初始值0xFFFF。网上有很多在线计算工具,调试时可以用。
2.3 串口参数与网络参数的配置要点
RTU模式的串口参数必须和设备完全一致,否则通讯不上。核心参数有五个:
| 参数 | 常见值 | 说明 |
|---|---|---|
| 波特率 | 9600/19200/38400/115200 | 必须一致,长距离建议低波特率 |
| 数据位 | 8 | 几乎都是8位 |
| 校验位 | None/Even/Odd | 常见None或Even |
| 停止位 | 1/2 | 常见1位 |
| 从站地址 | 1-247 | 同一总线上不能重复 |
TCP模式相对简单,只需要IP和端口,默认端口502。但要注意,有些设备的502端口是禁用的,需要改成其他端口。
注意:一条485总线上挂多台设备时,所有设备的波特率、数据位、校验位、停止位必须完全一致,从站地址必须唯一。我遇到过现场32台变频器,其中一台地址被设成了和另一台一样,结果两台都通讯异常,排查了大半天。
3. 多通道采集程序的实现与核心代码
3.1 采集程序的整体结构设计
一个健壮的多通道采集程序,结构上应该分成几个独立的模块:配置加载、连接管理、采集调度、数据处理、异常处理。这样拆分的好处是每个模块职责单一,出问题容易定位。
配置部分我习惯用一个YAML或JSON文件来管理设备列表,而不是硬编码在代码里。这样现场调整设备参数不用改代码,重启程序就行。配置结构大概长这样:
devices: - name: "PLC_01" type: "tcp" host: "192.168.1.10" port: 502 slave_id: 1 poll_interval: 1.0 registers: - name: "voltage" address: 0 count: 1 data_type: "uint16" scale: 0.1 unit: "V" - name: "current" address: 1 count: 1 data_type: "uint16" scale: 0.01 unit: "A" - name: "VFD_01" type: "rtu" port: "COM3" baudrate: 9600 parity: "N" stopbits: 1 slave_id: 2 poll_interval: 2.0 registers: - name: "frequency" address: 0x1000 count: 1 data_type: "uint16" scale: 0.01 unit: "Hz"这个配置里,每台设备独立定义轮询间隔和寄存器列表。scale字段用于量纲换算,比如寄存器读到1234,scale是0.1,实际值就是123.4。
3.2 基于pymodbus的采集核心代码
下面是一个可运行的核心采集类,我做了简化但保留了关键逻辑:
import time import logging from pymodbus.client import ModbusTcpClient, ModbusSerialClient from pymodbus.exceptions import ModbusException logger = logging.getLogger(__name__) class DeviceCollector: def __init__(self, config): self.config = config self.name = config["name"] self.client = None self.last_poll = 0 self.fail_count = 0 self.max_fail = 5 self._connect() def _connect(self): try: if self.config["type"] == "tcp": self.client = ModbusTcpClient( host=self.config["host"], port=self.config.get("port", 502), timeout=3 ) else: self.client = ModbusSerialClient( port=self.config["port"], baudrate=self.config.get("baudrate", 9600), parity=self.config.get("parity", "N"), stopbits=self.config.get("stopbits", 1), bytesize=8, timeout=3 ) self.client.connect() logger.info(f"{self.name} 连接成功") except Exception as e: logger.error(f"{self.name} 连接失败: {e}") def read_registers(self): results = {} for reg in self.config["registers"]: try: response = self.client.read_holding_registers( address=reg["address"], count=reg["count"], slave=self.config["slave_id"] ) if response.isError(): logger.warning(f"{self.name}.{reg['name']} 读取错误") continue raw = response.registers[0] value = raw * reg.get("scale", 1.0) results[reg["name"]] = { "value": value, "unit": reg.get("unit", ""), "timestamp": time.time() } except ModbusException as e: logger.error(f"{self.name}.{reg['name']} 异常: {e}") self.fail_count += 1 return results def poll(self): now = time.time() if now - self.last_poll < self.config.get("poll_interval", 1.0): return None self.last_poll = now if self.fail_count >= self.max_fail: logger.warning(f"{self.name} 连续失败{self.fail_count}次,尝试重连") self._connect() self.fail_count = 0 data = self.read_registers() if data: self.fail_count = 0 return data这段代码有几个关键设计点值得说明。超时设置3秒是经验值,太短容易误判掉线,太长会拖慢整个轮询周期。失败计数和自动重连是保证长期稳定运行的关键,现场设备偶尔抖动很正常,不能因为一次失败就放弃。scale换算放在采集层,这样上层拿到的就是带物理量纲的值,不用重复处理。
3.3 多设备轮询调度与异常隔离
单设备采集好写,多设备调度的难点在于不能让一台设备的异常影响其他设备。我的做法是每个设备一个独立的采集对象,主循环里依次调用,每个调用都包在try-except里。
class CollectorManager: def __init__(self, configs): self.collectors = [DeviceCollector(c) for c in configs] self.data_buffer = {} def run_once(self): for collector in self.collectors: try: data = collector.poll() if data: self.data_buffer[collector.name] = data except Exception as e: logger.error(f"{collector.name} 采集异常: {e}") continue def run_forever(self): while True: self.run_once() time.sleep(0.1)这里time.sleep(0.1)是让出CPU,避免空转占满一个核心。实际项目中,如果设备多、轮询间隔短,可以考虑用异步IO或者多线程,但要注意RTU总线的串行限制。
对于32台变频器挂在同一条485总线的场景,我的建议是:不要试图并发,老老实实串行轮询。每台设备读取时间大概50-100毫秒,32台一轮下来3秒左右,对于大多数监控场景完全够用。如果确实需要更快刷新,那就分组,用多条485总线并行。
实操心得:现场调试时,先用Modbus Poll单独测试每一台设备,确认地址、波特率、寄存器地址都正确,再接入自己的程序。这样能把设备问题和程序问题分开,排查效率高很多。
4. 数据存储、Web展示与系统集成
4.1 数据落库策略与时序数据处理
采集到的数据如果不存下来,就失去了分析价值。存储策略要根据数据量和查询需求来定。
小规模场景(几十个点位,秒级采集),直接用SQLite就够了。单文件、零配置、Python内置支持。建表语句大概这样:
CREATE TABLE IF NOT EXISTS realtime_data ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_name TEXT NOT NULL, point_name TEXT NOT NULL, value REAL, unit TEXT, ts REAL ); CREATE INDEX idx_device_ts ON realtime_data(device_name, ts);中等规模(几百到几千点位),建议上PostgreSQL。它的并发写入和复杂查询能力比SQLite强很多,而且有成熟的分区表方案,可以按时间分区,老数据自动归档。
大规模时序场景(上万点位,毫秒级),就得上专门的时序数据库了,比如InfluxDB或TDengine。这类库针对时间戳索引做了大量优化,写入和范围查询性能是关系型数据库的几十倍。
我个人的经验是:不要一上来就追求高大上,先用SQLite跑通,数据量上来了再迁移。迁移成本其实不高,因为SQLAlchemy这层ORM已经把数据库差异屏蔽了,改个连接字符串的事。
写入策略上,我推荐批量写入而不是每条都commit。攒够100条或者每隔1秒批量提交一次,能大幅降低数据库压力。下面是一个简单的批量写入实现:
from sqlalchemy import create_engine, text class DataWriter: def __init__(self, db_url="sqlite:///data.db"): self.engine = create_engine(db_url) self.buffer = [] self.batch_size = 100 def add(self, device, point, value, unit, ts): self.buffer.append((device, point, value, unit, ts)) if len(self.buffer) >= self.batch_size: self.flush() def flush(self): if not self.buffer: return with self.engine.begin() as conn: conn.execute( text("INSERT INTO realtime_data (device_name, point_name, value, unit, ts) " "VALUES (:d, :p, :v, :u, :t)"), [{"d": d, "p": p, "v": v, "u": u, "t": t} for d, p, v, u, t in self.buffer] ) self.buffer.clear()4.2 基于Web的实时数据展示
展示层用FastAPI起一个服务,前端用简单的HTML+JavaScript轮询接口。这种方式比传统组态软件灵活得多,而且浏览器直接访问,不用装客户端。
后端接口设计:
from fastapi import FastAPI from fastapi.responses import HTMLResponse import json app = FastAPI() manager = None # 由主程序注入 @app.get("/api/realtime") def get_realtime(): return manager.data_buffer @app.get("/api/history") def get_history(device: str, point: str, limit: int = 100): with engine.connect() as conn: rows = conn.execute( text("SELECT value, ts FROM realtime_data " "WHERE device_name=:d AND point_name=:p " "ORDER BY ts DESC LIMIT :l"), {"d": device, "p": point, "l": limit} ).fetchall() return [{"value": r[0], "ts": r[1]} for r in rows] @app.get("/", response_class=HTMLResponse) def index(): return open("index.html", encoding="utf-8").read()前端用一个简单的表格加Chart.js曲线图,每2秒请求一次/api/realtime刷新数据。这套方案的好处是任何有浏览器的设备都能看数据,手机、平板、办公室电脑都行,不需要额外部署。
如果要做更复杂的展示,比如多设备对比、报警弹窗、历史回放,可以上Vue或React。但我的建议是先用最简方案跑通,展示需求明确了再迭代。
4.3 与异构系统对接的注意事项
工业现场经常需要把采集到的数据同步到其他系统,比如MES、ERP或者云端平台。这时候就涉及异构数据库同步的问题。
常见的对接方式有三种:
- API推送:采集程序主动调用对方提供的HTTP接口,把数据POST过去。适合对方有标准接口的场景。
- 数据库直连:直接写入对方的数据库。这种方式耦合度高,但实时性好。
- 消息队列:通过MQTT或Kafka中转。适合多系统订阅同一份数据的场景。
我踩过的坑是时间戳格式不统一。自己的系统用Unix时间戳,对方要ISO 8601字符串,中间没做转换,导致对方系统里数据时间全乱了。对接前一定要把时间格式、数值精度、单位约定清楚,最好写个对接文档。
另一个坑是网络中断后的数据补传。如果推送失败,数据不能丢,要有本地缓存和重试机制。我的做法是推送失败的数据先落本地表,后台起一个补偿任务定期重试。
5. 常见问题排查与实战避坑指南
5.1 Modbus通讯故障速查表
下面这张表是我这些年遇到过的典型问题汇总,按现象分类,方便快速定位:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 完全无响应 | 串口线接反、波特率不对、从站地址错 | 用Modbus Poll单独测试,检查A/B线 |
| 偶尔超时 | 总线干扰、终端电阻缺失、线太长 | 加120欧终端电阻,降低波特率 |
| 数据错位 | 寄存器地址偏移搞错、字节序不对 | 对照手册确认0-based还是1-based |
| 浮点数乱码 | 高低字顺序反了 | 尝试交换两个寄存器的顺序 |
| 读多寄存器报错 | 跨了不允许的地址区间 | 分段读取,避开保留地址 |
| 写寄存器无效 | 设备处于运行状态禁止写入 | 先停机再写,或检查写保护 |
| TCP连不上 | IP错、端口错、防火墙拦截 | ping测试,telnet端口测试 |
| 异常码Exception Response | 功能码不支持、地址越界 | 看异常码含义,对照协议文档 |
关于异常码,常见的几个要记住:01是非法功能码,02是非法数据地址,03是非法数据值,04是从站设备故障。收到异常响应说明通讯链路是通的,问题出在请求内容上。
5.2 采集程序稳定性优化的独家经验
经验一:心跳检测比超时重试更重要。不要等读失败了才判断设备掉线,可以定期读一个固定的状态寄存器作为心跳。心跳正常但数据异常,说明是数据问题;心跳失败,才是通讯问题。
经验二:日志要分级,但现场调试时全开DEBUG。平时运行用INFO级别,只记录关键事件。现场排查时临时改成DEBUG,把每一帧收发报文都打出来,问题一目了然。pymodbus支持配置日志级别:
import logging logging.basicConfig(level=logging.DEBUG)经验三:采集程序要能热加载配置。现场调整设备参数是常事,如果每次都要重启程序,采集就中断了。可以监听配置文件变化,或者提供一个HTTP接口触发重载。
经验四:给每个设备加独立的统计计数器。记录成功次数、失败次数、平均响应时间。这些数据在排查性能瓶颈时非常有用。比如发现某台设备平均响应时间从50ms涨到500ms,可能是总线负载太高或者设备本身有问题。
经验五:程序要能优雅退出。收到终止信号时,先把缓冲区数据flush到数据库,再关闭连接。否则最后一批数据会丢。
5.3 从单机采集到多通道系统的扩展思路
一开始可能只是采集一台PLC的几个点位,但随着需求增长,系统会越来越复杂。扩展时要注意几个原则:
原则一:配置驱动,不要硬编码。设备数量、寄存器地址、轮询间隔全部放配置文件,代码只负责逻辑。这样从1台扩展到32台,只需要改配置。
原则二:采集与处理分离。采集程序只管拿数据,处理程序只管算数据,中间用队列或数据库解耦。这样任何一层出问题都不会互相影响。
原则三:先保证可用,再追求性能。很多新手一上来就搞异步、搞多进程,结果调试困难,稳定性还差。先用最简单的串行轮询跑通,确认功能正确,再根据性能瓶颈针对性优化。
原则四:留好扩展接口。比如预留一个data_callback回调,将来要接入新的存储或展示系统,注册一个回调就行,不用改采集核心代码。
我实际做过的一个项目,从最初1台西门子PLC加8台变频器,逐步扩展到3条产线、上百台设备。整个过程采集核心代码几乎没大改,就是不断加配置、加回调、加存储后端。这就是架构解耦带来的好处。
注意:扩展设备数量时,一定要重新评估总线负载。485总线理论上可以挂32个节点,但实际建议不超过20个,而且线长和波特率要匹配。设备太多就分组,用多条总线或者转成TCP。
5.4 仿真调试工具的正确使用姿势
Modbus Poll和Modbus Slave是调试必备。Poll模拟主站,Slave模拟从站。用法上有个技巧:先用Slave模拟设备,用Poll测试,确认报文正确后,再用Python程序对接。这样能把协议问题和程序问题分开。
具体步骤:
- 打开Modbus Slave,设置从站地址、功能码、起始地址、寄存器数量。
- 在寄存器表格里填入测试值。
- 打开Modbus Poll,设置相同的通讯参数,连接。
- 如果Poll能正确读到Slave的值,说明协议层没问题。
- 然后用Python程序连接Slave,对比读到的值是否一致。
这个流程能帮你快速定位问题出在协议配置还是代码逻辑。我见过太多人跳过这一步,直接拿程序怼真实设备,结果通讯参数不对,折腾半天。
另外,Modbus Slave的寄存器值可以设置成自动变化,模拟真实设备的动态数据。调试采集程序的刷新逻辑时很有用。
6. 系统部署与长期运维的实战建议
6.1 现场部署的硬件与网络准备
程序写好了,部署到现场还有一堆事。硬件上,工控机是首选,比普通PC稳定,而且很多工控机自带多串口。如果设备都是网口,那普通迷你主机也行。
串口方面,如果工控机串口不够,用USB转485转换器。但要注意,便宜的转换器芯片(比如CH340)在长时间高负载下容易掉线,建议用FTDI或CP2102芯片的。我吃过这个亏,现场跑了三天,转换器开始随机丢包,换了FTDI的就好了。
网络方面,如果走TCP,建议把采集设备和办公网络隔离,用独立的交换机。工业现场电磁干扰大,网线要用带屏蔽的,而且要走单独的线槽,不要和动力线捆在一起。
电源也要注意,工控机最好配UPS,防止突然断电导致数据丢失或文件系统损坏。我遇到过现场跳闸,SQLite数据库文件损坏,数据全没了。后来加了UPS,再没出过这个问题。
6.2 程序自启动与后台运行配置
现场部署的程序不能靠人工启动,必须配置成开机自启。Windows下有两种方式:
方式一:任务计划程序。创建一个任务,触发器设为“计算机启动时”,操作设为启动Python程序。这种方式的好处是可以配置“不管用户是否登录都运行”。
方式二:Windows服务。用nssm等工具把Python程序包装成服务。这种方式更规范,可以在服务管理器里启动、停止、查看状态。
Linux下就简单了,写一个systemd service文件:
[Unit] Description=Industrial Data Collector After=network.target [Service] Type=simple User=pi WorkingDirectory=/home/pi/collector ExecStart=/home/pi/collector/venv/bin/python main.py Restart=always RestartSec=10 [Install] WantedBy=multi-user.targetRestart=always是关键,程序崩溃后10秒自动重启,保证采集不中断。
如果需要在Windows下执行一些需要管理员权限的操作(比如访问某些串口),可以用gsudo提权。安装很简单:
winget install gsudo然后在需要提权的命令前加gsudo即可。
6.3 数据备份与系统监控
长期运行的系统,数据备份和系统监控不能少。
数据备份方面,SQLite直接复制文件就行,但要注意复制前先执行VACUUM或者用.backup命令,避免复制到写入中的不一致状态。PostgreSQL用pg_dump定时导出。备份文件按日期命名,保留最近30天。
系统监控方面,至少要监控几个指标:采集程序是否在运行、各设备通讯成功率、数据库文件大小、磁盘剩余空间。可以写一个简单的监控脚本,定期检查并通过邮件或消息通知告警。
我自己的做法是在采集程序里内置一个健康检查接口:
@app.get("/health") def health(): return { "status": "ok", "devices": len(manager.collectors), "buffer_size": len(manager.data_buffer), "uptime": time.time() - start_time }然后用一个外部脚本每分钟请求一次,连续失败就告警。这样即使采集程序卡死,也能及时发现。
6.4 性能调优的几个关键参数
系统跑起来之后,如果发现数据刷新慢或者CPU占用高,可以从这几个参数入手调优:
轮询间隔:不是越短越好。设备响应需要时间,间隔太短会导致请求堆积。一般RTU设备间隔不低于200ms,TCP设备不低于100ms。
超时时间:默认3秒,如果现场网络质量差可以适当延长,但不要超过5秒,否则一台设备卡住会拖慢整个循环。
批量读取:连续的寄存器尽量一次读完,而不是一个一个读。比如要读地址0到9的10个寄存器,一次读10个比读10次快得多。
连接复用:TCP连接建立开销大,要复用而不是每次读都新建连接。pymodbus的client对象本身就是复用的,不要每次poll都重新connect。
数据库批量提交:前面说过的批量写入,batch_size根据数据量调整,一般100-500条比较合适。
调优的核心思路是先测量再优化。在程序里加计时日志,看看时间到底花在哪里,是设备响应慢、还是数据库写入慢、还是数据处理慢。找到瓶颈再针对性优化,不要盲目调参。
这套多通道工业数据采集系统,我从最初的单设备脚本一路迭代到现在,踩过的坑基本都写在上面了。核心体会就是:协议要懂、架构要解耦、异常要隔离、日志要详细、部署要自动化。把这几点做到位,系统就能稳定跑起来。后续如果要接入更多设备或者对接上层系统,按照前面说的扩展原则来,基本不会推倒重来。