Python实现Modbus TCP工业通信:从建连到多设备轮询的完整方案
工控领域最常用的通信协议Modbus排第二没人敢排第一。虽然协议本身很简单但工程实现中各种细节问题不少。本文用Python从零实现一套完整的Modbus TCP通信方案覆盖建连、读写、异常处理、多设备轮询等实际场景。## 为什么选Python工控现场C/C还是主流但Python在以下场景有明显优势- 快速原型验证和协议调试- 数据采集网关的轻量实现- 边缘计算设备上的数据预处理- 测试环境的自动化脚本性能方面Python处理Modbus TCP完全够用——Modbus TCP的典型轮询周期在百毫秒级Python的网络IO延迟在毫秒级不构成瓶颈。## 依赖准备bashpip install pymodbus3.6.3pymodbus是Python最成熟的Modbus实现支持RTU和TCP3.x版本API有较大变化以下代码基于3.6.3版本。## 一、基础连接和单次读写pythonfrom pymodbus.client import ModbusTcpClientimport structimport timeimport logging# 配置日志方便调试logging.basicConfig(levellogging.INFO)logger logging.getLogger(__name__)class ModbusTCPClient:Modbus TCP客户端封装def __init__(self, host, port502, timeout5, retries3):self.host hostself.port portself.timeout timeoutself.retries retriesself.client Nonedef connect(self):建立连接self.client ModbusTcpClient(hostself.host,portself.port,timeoutself.timeout,retriesself.retries)if self.client.connect():logger.info(f连接成功: {self.host}:{self.port})return Trueelse:logger.error(f连接失败: {self.host}:{self.port})return Falsedef disconnect(self):断开连接if self.client:self.client.close()logger.info(f连接已断开: {self.host}:{self.port})def read_holding_registers(self, address, count, unit1):读取保持寄存器功能码03Args:address: 寄存器起始地址count: 读取数量unit: 从站地址Returns:list: 寄存器值列表失败返回Nonetry:result self.client.read_holding_registers(addressaddress,countcount,slaveunit)if result.isError():logger.error(f读取失败 addr{address}: {result})return Nonereturn result.registersexcept Exception as e:logger.error(f读取异常 addr{address}: {e})return Nonedef write_single_register(self, address, value, unit1):写单个寄存器功能码06Args:address: 寄存器地址value: 写入值unit: 从站地址Returns:bool: 成功返回Truetry:result self.client.write_register(addressaddress,valuevalue,slaveunit)if result.isError():logger.error(f写入失败 addr{address}: {result})return Falsereturn Trueexcept Exception as e:logger.error(f写入异常 addr{address}: {e})return Falsedef write_multiple_registers(self, address, values, unit1):写多个寄存器功能码16Args:address: 起始地址values: 值列表unit: 从站地址Returns:bool: 成功返回Truetry:result self.client.write_registers(addressaddress,valuesvalues,slaveunit)if result.isError():logger.error(f批量写入失败 addr{address}: {result})return Falsereturn Trueexcept Exception as e:logger.error(f批量写入异常 addr{address}: {e})return False# 基本使用示例 if __name__ __main__:client ModbusTCPClient(192.168.1.100, port502)if client.connect():# 读取保持寄存器地址0开始读10个registers client.read_holding_registers(address0, count10, unit1)if registers:print(f读取到寄存器值: {registers})# 写单个寄存器success client.write_single_register(address0, value100, unit1)print(f写入结果: {success})# 写多个寄存器success client.write_multiple_registers(address10, values[1, 2, 3, 4, 5], unit1)print(f批量写入结果: {success})client.disconnect()## 二、32位浮点数的读写处理Modbus寄存器是16位的但工业传感器经常返回32位浮点数如温度、压力、流量需要两个连续寄存器拼接。这里的关键是字节序问题不同设备的存储顺序可能不同。pythonclass ModbusDataConverter:Modbus数据类型转换工具staticmethoddef registers_to_float(reg1, reg2, byte_orderbig, word_orderbig):两个16位寄存器转32位浮点数Args:reg1: 第一个寄存器值reg2: 第二个寄存器值byte_order: 字节序 big 或 littleword_order: 字序 big(高字在前) 或 little(低字在前)Returns:float: 解析后的浮点数if word_order big:# 高字在前reg1是高16位raw (reg1 16) | reg2else:# 低字在前reg2是高16位raw (reg2 16) | reg1# 将32位整数转为4字节if byte_order big:byte_data struct.pack(I, raw)else:byte_data struct.pack(I, raw)return struct.unpack(f, byte_data)[0]staticmethoddef float_to_registers(value, byte_orderbig, word_orderbig):32位浮点数转两个16位寄存器值Args:value: 浮点数值byte_order: 字节序word_order: 字序Returns:tuple: (reg1, reg2)byte_data struct.pack(f, value)raw struct.unpack(I, byte_data)[0]if word_order big:reg1 (raw 16) 0xFFFFreg2 raw 0xFFFFelse:reg1 raw 0xFFFFreg2 (raw 16) 0xFFFFreturn reg1, reg2staticmethoddef registers_to_int32(reg1, reg2, signedTrue, word_orderbig):两个寄存器转32位整数if word_order big:raw (reg1 16) | reg2else:raw (reg2 16) | reg1if signed:return struct.unpack(i, struct.pack(I, raw))[0]return rawstaticmethoddef registers_to_float_array(registers, byte_orderbig, word_orderbig):批量将寄存器数组转为浮点数数组每2个寄存器对应1个浮点数Args:registers: 寄存器值列表byte_order: 字节序word_order: 字序Returns:list: 浮点数列表floats []for i in range(0, len(registers) - 1, 2):val ModbusDataConverter.registers_to_float(registers[i], registers[i 1],byte_order, word_order)floats.append(val)return floats# 浮点数读写示例 def read_temperature(client, address0, unit1):读取温度值32位浮点数占2个寄存器registers client.read_holding_registers(address, count2, unitunit)if registers and len(registers) 2:# 注意不同设备字节序可能不同需要根据设备手册调整temp ModbusDataConverter.registers_to_float(registers[0], registers[1],byte_orderbig, word_orderlittle # 很多PLC用低字在前)return tempreturn None## 三、多设备轮询采集实际项目中通常需要同时采集多台设备的数据以下是带异常恢复的多设备轮询实现pythonimport threadingimport queueimport jsonfrom datetime import datetimeclass ModbusPollingManager:多设备Modbus TCP轮询管理器def __init__(self, config_list, poll_interval1.0):Args:config_list: 设备配置列表[{name: 温控仪1,host: 192.168.1.100,port: 502,unit: 1,registers: [{name: 温度, address: 0, count: 2, type: float},{name: 湿度, address: 2, count: 2, type: float},{name: 运行状态, address: 10, count: 1, type: uint16}]}]poll_interval: 轮询间隔秒self.config_list config_listself.poll_interval poll_intervalself.clients {}self.data_queue queue.Queue()self.running Falseself.poll_thread Nonedef init_connections(self):初始化所有设备连接for cfg in self.config_list:client ModbusTCPClient(hostcfg[host],portcfg.get(port, 502),timeoutcfg.get(timeout, 3),retriescfg.get(retries, 2))if client.connect():self.clients[cfg[name]] {client: client,config: cfg,fail_count: 0}logger.info(f设备 [{cfg[name]}] 连接成功)else:logger.error(f设备 [{cfg[name]}] 连接失败)self.clients[cfg[name]] {client: client,config: cfg,fail_count: 0}def _parse_register_value(self, registers, reg_type, byte_orderbig, word_orderlittle):根据数据类型解析寄存器值if not registers:return Noneif reg_type uint16:return registers[0]elif reg_type int16:val registers[0]return val - 65536 if val 32767 else valelif reg_type float:if len(registers) 2:return ModbusDataConverter.registers_to_float(registers[0], registers[1], byte_order, word_order)elif reg_type int32:if len(registers) 2:return ModbusDataConverter.registers_to_int32(registers[0], registers[1], word_orderword_order)return Nonedef _poll_single_device(self, device_name):轮询单个设备dev self.clients[device_name]client dev[client]cfg dev[config]# 检查是否需要重连if dev[fail_count] 3:logger.warning(f设备 [{device_name}] 连续失败{dev[fail_count]}次尝试重连)client.disconnect()time.sleep(1)if not client.connect():dev[fail_count] 1returndata {device: device_name,timestamp: datetime.now().isoformat(),values: {}}for reg_cfg in cfg[registers]:registers client.read_holding_registers(addressreg_cfg[address],countreg_cfg[count],unitcfg.get(unit, 1))if registers is not None:value self._parse_register_value(registers,reg_cfg[type],word_orderreg_cfg.get(word_order, little))data[values][reg_cfg[name]] valuedev[fail_count] 0else:data[values][reg_cfg[name]] Nonedev[fail_count] 1logger.warning(f设备 [{device_name}] 读取 {reg_cfg[name]} 失败 f(连续失败{dev[fail_count]}次))self.data_queue.put(data)def _polling_loop(self):轮询主循环while self.running:for device_name in list(self.clients.keys()):try:self._poll_single_device(device_name)except Exception as e:logger.error(f轮询设备 [{device_name}] 异常: {e})time.sleep(self.poll_interval)def start(self):启动轮询self.init_connections()self.running Trueself.poll_thread threading.Thread(targetself._polling_loop,daemonTrue,namemodbus-polling)self.poll_thread.start()logger.info(轮询线程已启动)def stop(self):停止轮询self.running Falseif self.poll_thread:self.poll_thread.join(timeout5)for dev in self.clients.values():dev[client].disconnect()logger.info(轮询已停止所有连接已断开)def get_data(self, timeout1.0):从队列获取一条数据try:return self.data_queue.get(timeouttimeout)except queue.Empty:return None# 多设备轮询示例 if __name__ __main__:devices [{name: 温控仪A,host: 192.168.1.100,port: 502,unit: 1,timeout: 3,registers: [{name: 当前温度, address: 0, count: 2, type: float},{name: 设定温度, address: 2, count: 2, type: float},{name: 运行状态, address: 10, count: 1, type: uint16}]},{name: 电表B,host: 192.168.1.101,port: 502,unit: 1,timeout: 3,registers: [{name: 电压, address: 0x1000, count: 2, type: float},{name: 电流, address: 0x1002, count: 2, type: float},{name: 功率, address: 0x1004, count: 2, type: float}]}]manager ModbusPollingManager(devices, poll_interval1.0)manager.start()try:# 运行60秒打印采集到的数据for _ in range(60):data manager.get_data(timeout2.0)if data:print(json.dumps(data, ensure_asciiFalse, indent2))except KeyboardInterrupt:passfinally:manager.stop()## 四、工程中踩过的坑**坑1连接超时设置。** 默认timeout太长30秒设备掉线时整个轮询卡住。建议设为3-5秒。同时pymodbus的retries参数控制重试次数设为2-3次就够了。**坑2从站地址unit/slave。** pymodbus 3.x中参数名是slave不是unit。从站地址范围1-2470是广播地址。有些设备默认从站地址不是1需要查手册确认。**坑3寄存器地址偏移。** 有些设备手册上写的地址是PLC地址从1开始Modbus协议地址是从0开始的。比如手册上写地址40001实际协议地址是0。差1容易导致读到的数据全错。**坑4浮点数字节序。** 这是Modbus最大的坑。同样读两个寄存器有的设备高字在前有的低字在前。建议先用Modbus Poll等工具手动读取确认再在代码中配置正确的word_order。**坑5并发访问。** pymodbus的client对象不是线程安全的。多线程环境下要么每个线程用独立的client实例要么加锁串行访问。上面的轮询管理器是单线程轮询不存在这个问题。**坑6TCP连接断开检测。** Modbus TCP的TCP连接可能被对端静默关闭pymodbus不一定能及时检测到。建议在read/write失败后主动close并reconnect而不是依赖库的自动重连。## 五、性能优化建议如果需要采集大量设备或高频采集可以考虑以下优化**用异步IO替代线程。** pymodbus 3.x提供了asyncio版本的客户端在高并发场景下性能远优于线程模型pythonimport asynciofrom pymodbus.client import AsyncModbusTcpClientasync def async_read(client, address, count, unit):result await client.read_holding_registers(address, count, slaveunit)if not result.isError():return result.registersreturn Noneasync def main():client AsyncModbusTcpClient(192.168.1.100)await client.connect()# 并发读取多个地址tasks [async_read(client, 0, 2, 1),async_read(client, 10, 2, 1),async_read(client, 20, 4, 1),]results await asyncio.gather(*tasks)print(results)client.close()asyncio.run(main())**连接池复用。** 避免每次读写都建立新连接保持长连接。但要注意设置心跳机制定时读取一个寄存器防止NAT或防火墙超时断开TCP连接。**批量读取优化。** 如果多个寄存器地址连续一次读多个比多次单个读取效率高得多。TCP协议帧中最大可以读125个寄存器功能码03。## 总结Modbus TCP协议本身不复杂但工程实现中数据类型转换特别是浮点数字节序、异常恢复、多设备并发这些环节需要仔细处理。建议在正式部署前用Modbus Poll等工具充分验证协议参数避免在现场调试时反复试错。

相关新闻

阿里AI编程市场份额近半,阿里云增速与利润率同升,估值锚向AI移动

阿里AI编程市场份额近半,阿里云增速与利润率同升,估值锚向AI移动

7月16日,IDC报告显示阿里以47.6%的份额位居中国AI编程市场第一。此前投行预测阿里云收入增速和利润率将提升,阿里AI能力正转化为多方面成果。市场份额领先,产品化获验证这是AI coding普及后首份权威份额报告。2025年中国AI coding市场规模3.9…

2026/7/18 16:53:46阅读更多 →
188、生成对抗网络进阶:ESRGAN的RRDB结构与感知损失调参艺术

188、生成对抗网络进阶:ESRGAN的RRDB结构与感知损失调参艺术

188、生成对抗网络进阶:ESRGAN的RRDB结构与感知损失调参艺术 上个月帮师弟调一个老照片修复项目,他用的还是SRGAN那套残差块,训练了三天,出来的结果人脸糊得像打了马赛克,纹理细节全是伪影。我让他换成ESRGAN的RRDB结构,顺便把感知损失的权重从1e-2调到1e-3,结果第二天他…

2026/7/18 16:53:46阅读更多 →
AWS EC2不开放22端口:Session Manager与IMDSv2加固实战

AWS EC2不开放22端口:Session Manager与IMDSv2加固实战

很多团队给 EC2 配置安全组时,会习惯性地加入一条 0.0.0.0/0 -> TCP 22,然后再依赖强密码、改端口或 Fail2ban。问题是:只要 22 端口对公网可达,扫描、爆破和密钥管理就一直存在;更换员工、外包交接和跳板机维护也会…

2026/7/18 16:53:46阅读更多 →
Nintendo Switch大气层Atmosphere稳定版:全面解锁游戏主机潜能的终极指南

Nintendo Switch大气层Atmosphere稳定版:全面解锁游戏主机潜能的终极指南

Nintendo Switch大气层Atmosphere稳定版:全面解锁游戏主机潜能的终极指南 【免费下载链接】Atmosphere-stable 大气层整合包系统稳定版 项目地址: https://gitcode.com/gh_mirrors/at/Atmosphere-stable 大气层Atmosphere稳定版是当前最成熟、最安全的Ninten…

2026/7/18 17:47:50阅读更多 →
深耕三年日志框架,聊聊Java项目中slf4j+logback那些恶心的隐性坑点

深耕三年日志框架,聊聊Java项目中slf4j+logback那些恶心的隐性坑点

做后端开发的没人离得开日志,不管是线上问题排查、接口异常追踪,还是日常业务监控,日志都是最直接的依据。很多人开发项目都是直接引入starter依赖,默认配置直接跑,从来不会主动去改日志底层参数。平时本地测试没啥问题…

2026/7/18 17:47:50阅读更多 →
百考通智能化AI一键生成,任务书让科研与项目启动更高效

百考通智能化AI一键生成,任务书让科研与项目启动更高效

在学术研究、课程设计与项目开发的起步阶段,一份规范、清晰的任务书是指引方向的核心纲领。但从选题构思到内容撰写,往往让研究者与学生陷入困境:选题迷茫、逻辑混乱、要求表述模糊,严重拖慢项目推进节奏。百考通(http…

2026/7/18 17:47:50阅读更多 →
百考通智能化AI一键生成,赋能答辩PPT,让学术展示更高效从容

百考通智能化AI一键生成,赋能答辩PPT,让学术展示更高效从容

毕业季、开题季,一份专业出彩的PPT是顺利通过答辩的关键。但从论文中提炼核心观点、规划答辩逻辑、设计美观版式,往往让学生们焦头烂额。百考通(https://www.baikaotongai.com) 凭借AI技术深度赋能,打造出一站式答辩PP…

2026/7/18 17:47:50阅读更多 →
运算放大器与比较器的本质区别及应用场景

运算放大器与比较器的本质区别及应用场景

1. 运算放大器与比较器的本质差异 第一次接触模拟电路设计时,我也曾困惑过:为什么数据手册里既有运算放大器(Op-Amp)又有比较器(Comparator)?它们看起来都能对电压进行比较,甚至引脚定义都相似。直到在一次电机驱动电路设计中&…

2026/7/18 17:47:50阅读更多 →
2026揭阳黄金回收白银回收铂金回收靠谱临街实体公安备案支持到店核验门店联系方式推荐

2026揭阳黄金回收白银回收铂金回收靠谱临街实体公安备案支持到店核验门店联系方式推荐

2026揭阳黄金白银铂金回收实测榜单|公安备案临街实体门店推荐 揭阳黄金回收哪家靠谱|工商公安双备案中检认证实体门店 揭阳本地贵金属回收店铺遍地丛生,行业套路层出不穷,不少市民变现遭遇虚高报价、克扣损耗、未经同意熔金压价等…

2026/7/18 17:45:50阅读更多 →
VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异

VSCode TypeScript 环境配置对比:全局安装 vs 项目本地安装的4个关键差异当你在VSCode中启动一个新的TypeScript项目时,第一个技术决策往往从安装方式开始。这个看似简单的选择——全局安装还是项目本地安装——实际上会深刻影响你的开发流程、团队协作和…

2026/7/18 10:49:13阅读更多 →
智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手

智慧树刷课插件:5分钟实现自动化学习的智能助手 【免费下载链接】zhihuishu 智慧树刷课插件,自动播放下一集、1.5倍速度、无声 项目地址: https://gitcode.com/gh_mirrors/zh/zhihuishu 智慧树刷课插件是一款专为智慧树在线教育平台设计的Chrome浏…

2026/7/18 8:49:08阅读更多 →
Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案

Steam创意工坊下载器WorkshopDL:跨平台游戏模组获取的终极解决方案 【免费下载链接】WorkshopDL WorkshopDL - The Best Steam Workshop Downloader 项目地址: https://gitcode.com/gh_mirrors/wo/WorkshopDL 你是否在GOG或Epic Games Store购买了心仪的游戏…

2026/7/18 14:49:24阅读更多 →
从模糊意图到可执行指令:Claude PRD中Prompt Engineering与需求颗粒度的5级映射法则

从模糊意图到可执行指令:Claude PRD中Prompt Engineering与需求颗粒度的5级映射法则

更多请点击: https://kaifayun.com 第一章:从模糊意图到可执行指令:Claude PRD中Prompt Engineering与需求颗粒度的5级映射法则 在Claude驱动的产品需求文档(PRD)生成实践中,原始业务意图往往以自然语言片…

2026/7/18 0:00:14阅读更多 →
Cursor配置生成失效?3大隐藏陷阱+4行修复代码,资深工程师连夜整理的紧急补救清单

Cursor配置生成失效?3大隐藏陷阱+4行修复代码,资深工程师连夜整理的紧急补救清单

更多请点击: https://codechina.net 第一章:Cursor配置生成失效?3大隐藏陷阱4行修复代码,资深工程师连夜整理的紧急补救清单 Cursor 配置生成突然失效,是近期高频报障场景。表面看是 cursor.config.json 未更新或 LSP…

2026/7/18 0:00:14阅读更多 →
某智驾大牛创业

某智驾大牛创业

作者:钟声编辑:Mark出品:红色星际头图:智能驾驶图片据悉,国内某头部智驾公司端到端模型技术大牛Z投身创业,并且已经拿到融资。Z不仅是该头部公司内部最年轻的对标阿里P10级别技术负责⼈,更是业内…

2026/7/18 0:00:14阅读更多 →
YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

YOLOv8推理性能优化:从1.2FPS到35FPS的全链路加速实践

如果你在部署 YOLOv8 时,发现推理速度只有可怜的 1-2 FPS,而别人的演示视频却能跑到 30 FPS 以上,那么问题很可能不在模型本身,而在于你的整个处理链路。很多开发者拿到一个训练好的 YOLOv8 模型后,会直接使用官方示例…

2026/7/17 22:48:46阅读更多 →
Coze与Dify对比指南:低代码AI应用开发从入门到实战

Coze与Dify对比指南:低代码AI应用开发从入门到实战

1. 从零到一:为什么你需要了解 Coze 和 Dify?如果你对 AI 应用开发感兴趣,但一看到“大模型”、“智能体”、“工作流”这些词就头疼,觉得门槛太高,那这篇文章就是为你准备的。很多开发者,包括我自己&#…

2026/7/18 14:49:24阅读更多 →
AI生图工具怎么选?2026年6月版实测对比

AI生图工具怎么选?2026年6月版实测对比

做自媒体的朋友应该都有体会:配图一直是个让人头疼的问题。2026年,AI生图工具已经非常成熟了,但工具太多反而不知道怎么选。以下是截至2026年6月我对主流AI生图工具的实测对比。Midjourney V8.1:速度之王2026年6月11日&#xff0c…

2026/7/17 17:26:50阅读更多 →