
搞工业自动化的朋友一定都有这种经历现场立着一台西门子触摸屏屏幕上实时刷着温度、压力、产量这些关键参数但旁边那台办公电脑上却没有博途只有Python环境。想把屏幕上的数据接出来做报表、做预警、做历史曲线第一个念头就是直接用程序去“读触摸屏”。但这儿的门道比想象中要多不少尤其是数据类型里最常见的Dint32位整型和LReal64位双精度浮点。这两个数据类型的解析方式和普通整数、单精度浮点数完全不是一个路子我之前第一次上手时就因为字节序和地址偏移这两个问题卡了整整一个下午。先说结论用Python Snap7读西门子触摸屏背后的数据是完全可以的但真正读的不是触摸屏本身而是触摸屏所绑定的PLC。Snap7作为一个开源的S7通信库通过以太网直接和PLC对话把DB块里的数据拿到手而我们看到的触摸屏画面本质上是PLC数据块的“镜像”。这篇文章我结合自己的实战经历把从环境搭建、连接配置、Dint与LReal的读取代码、地址偏移计算、字节序转换到各种常见坑完整梳理一遍。不管是设备维护人员、MES对接工程师还是刚接触工控的Python开发者照着这套流程都能把数据跑起来。1. 先把问题看清楚读的到底是什么1.1 触摸屏上的数据实际存放在哪里很多刚从IT转过来接触工业现场的程序员第一反应是“触摸屏里应该有数据”。这个想法可以理解但和西门子的实际架构是有出入的。在标准的西门子自动化系统中触摸屏HMI面板和PLC是两台独立设备触摸屏的角色是人机交互界面PLC的角色是逻辑运算和数据存储。屏幕上显示的那个流量计数值本质是PLC某个DB块里的变量——HMI通过组态软件把这个变量的地址映射到画面上的一个文本显示控件。所以数据真正的“老家”是PLC的内存区触摸屏只是临时把数据展示出来。这就引出一个关键结论Snap7连接的目标不是触摸屏的IP而是PLC的IP。我在一个设备改造项目里最开始就试图用触摸屏的IP去连接结果一直超时后来翻看了触摸屏的组态工程——画面变量列表里每个变量都标注了PLC的DB号和偏移地址这才明白过来。因此在做任何采集程序之前最值得做的事是找到触摸屏组态里的“变量列表”或者“连接设置”把每个画面变量背后的PLC数据区信息理清楚。这步做扎实了后面写代码就是纯粹的体力活。也有个别例外情况比如部分新款HMI面板支持数据记录功能可以把历史数据写到自身存储介质里比如配方数据、报警记录。这类数据确实存在触摸屏上但Snap7走的是S7通信协议核心交互对象是PLC的存储区触摸屏自己的记录数据一般需要走文件导出或者OPC UA通道。因此本文的核心场景仍然是触摸屏所绑定的PLC实时数据。1.2 Dint和LReal到底是什么数据类型Dint这个写法初次接触会觉得有点陌生。它是Double Integer的缩写代表32位有符号整数取值范围从-2147483648到2147483647。相比西门子PLC里常见的INT16位整数范围-32768到32767Dint能表达的数字范围大得多。为什么触摸屏上经常用Dint典型的例子是累计产量。一个计数器如果长期累加很快就能突破32767的上限用INT必然溢出用Dint则可以跑到21亿多对绝大多数产线来说足够了。LReal则是Long Real的缩写是64位双精度浮点数对应到C语言里的double也对应到Python里的float——因为Python的float本身就是双精度。很多传感器比如称重、流量、温度变送器的测量值要求很高精度如果用32位REAL单精度浮点数有效数字只有7位大数加小数时精度损失很严重。举个例子一个读数在100000.123456附近的重力传感器REAL类型可能只能读成100000.13而LReal类型能精确保留到小数点后六位以上。这就是LReal存在的意义。准确理解这两个类型不仅是类型转换的问题更直接关系到通信时的字节长度。Dint在S7协议中占用4个字节LReal占用8个字节。这个“4字节”和“8字节”是所有地址偏移计算的基础——DB块里一个Dint变量后面紧跟着另一个变量时第二个变量的起始偏移量至少要在前一个变量的基础上加4如果中间还有LReal变量则要加8。后面关于地址偏移的讨论全部建立在这两个字节长度上。2. 环境准备Snap7安装与PLC通信配置2.1 安装Python Snap7库并验证底层驱动Snap7是一个开源的S7以太网通信库原生语言是C官方提供了Python绑定python-snap7。安装方式走标准的pip流程。pip install python-snap7这个包在安装时会自动携带对应平台的本地动态库文件Windows下是snap7.dllLinux下是libsnap7.so所以一般情况下装完就能直接用。我个人的建议是装完后立即跑一段最简测试确认底层驱动真的可用免得后面写了一大堆代码才发现环境有问题。import snap7 client snap7.client.Client() client.create() print(Snap7 client created successfully) client.destroy()如果导入snap7模块时报错比如提示找不到动态库直接去Snap7官方GitHub的Release页面手动下载对应操作系统的库文件放到系统搜索路径里即可。这种情况在Windows精简环境下出现过几次安装Visual C运行库后基本能解决。2.2 开启PLC的PUT/GET通信授权这一步极其关键而且踩坑率极高。S7-1200和S7-1500系列PLC在默认安全设置下是禁止外部设备通过以太网直接读写数据块的。如果你的Snap7程序出现“连接成功但读取超时”或“完全连不上”这类现象十有八九是PLC的通信权限没有打开。需要做的操作是在博途TIA Portal里打开PLC的设备组态进入“属性 - 保护与安全 - 连接机制”勾选“允许来自远程对象的PUT/GET通信访问”。不同版本的博途菜单名称略有差异有些版本显示为“Permit access with PUT/GET communication”但位置基本都在保护与安全相关配置里。S7-300和S7-400老款PLC默认允许远程通信但也要检查CPU属性里的保护级别。如果保护级别设置为“完全保护”外部访问同样会被拒绝。另外某些老版本固件的S7-300需要在组态软件里明确勾选“允许外部读写”否则Snap7只能读不能写。值得注意的是修改PLC通信权限之后需要重新编译并下载硬件配置必要时还要做一次停机重启。这涉及设备停机时间现场操作时一定要提前和相关人员沟通确认。2.3 确认Rack机架号和Slot槽号Snap7的connect方法有三个关键参数IP地址、机架号、槽号。client snap7.client.Client() client.connect(192.168.0.1, 0, 1)这三个参数一个都不能错。对于S7-300系列常见的配置是机架0、槽号2对于S7-1200和S7-1500系列常见的配置是机架0、槽号1。但这个不是绝对的比如某些紧凑型CPU或者第三方机架槽号可能有差异。最稳妥的方式是打开博途的设备组态在左侧设备树中查看CPU的实际机架和槽位。如果手头没有博途也可以尝试几个常见组合连接成功的那个组合大概率就是对的。判断Rack和Slot是否正确的标志是connect方法不报错而且后续的db_read有正常返回。如果connect直接抛异常先别怀疑网络优先检查IP、Rack、Slot这三项。3. 核心代码用Snap7读取Dint和LReal数据3.1 基础连接与DB读取框架确认通信环境没问题后就可以正式写采集代码了。先来看最基础的连接和读取框架。import snap7 from snap7.util import get_dint, get_lreal def connect_plc(ip, rack0, slot1): client snap7.client.Client() client.connect(ip, rack, slot) return client client connect_plc(192.168.0.10) # 读取DB号、起始偏移、字节长度 data client.db_read(db_number1, start0, size4) print(data.hex())db_read是Snap7读取数据块的核心方法参数分别是DB块编号、起始偏移地址、要读取的字节长度。返回的是一个字节数组也就是PLC原始数据的“底片”。我们所有的数据类型解析都是在拿到这个字节数组之后进行的。我习惯先把原始字节打印成十六进制用肉眼核对一遍再写解析逻辑。这样能第一时间发现地址对不对、长度够不够也能直观感受大端字节序。比如PLC里存了一个整数10实际读出来的字节可能是00 00 00 0A而不是0A 00 00 00这就是大端和小端的区别后面细说。3.2 读取Dint数据的两种方式拿到原始字节后解析Dint有现成的工具函数也可以自己用struct手动解析。先看最直接的方式。# 方式一使用snap7.util自带的get_dint data client.db_read(db_number1, start0, size4) dint_value get_dint(data, 0) print(fDint value at DB1.DBD0: {dint_value})get_dint接收两个参数第一个是字节数组第二个是在数组内部的起始偏移注意这个偏移是数组内偏移不是PLC的DB地址偏移。它会自动按照西门子的大端规则把4个字节解析成32位有符号整数。再来看自己手动解析的方式。# 方式二使用struct手动解析 import struct data client.db_read(db_number1, start0, size4) dint_value struct.unpack(i, data[:4])[0] print(fDint value: {dint_value})这里的i就是大端Big-Endian有符号32位整数。是字节序标记i是C语言int的对应格式。为什么必须加因为西门子PLC的S7协议在网络传输时使用的是大端字节序高字节在前、低字节在后而x86架构的电脑默认是小端低字节在前。所以如果写成i不加结构是以小端解析同一个4字节数据解析出来的整数就完全不对。get_dint内部做的就是这件加的事所以推荐直接用工具函数。一个小技巧测试阶段可以用已知值验证。比如手动在博途里给某个Dint变量写入12345然后运行程序看读出来的值是不是12345。如果是说明地址和字节序都对了如果不是先别改解析代码回头检查地址偏移。3.3 读取LReal数据的完整示例LReal占8个字节读取流程和Dint类似但解析时的格式和工具函数不同。# 使用snap7.util自带的get_lreal data client.db_read(db_number1, start0, size8) lreal_value get_lreal(data, 0) print(fLReal value: {lreal_value})手动解析的写法import struct data client.db_read(db_number1, start0, size8) lreal_value struct.unpack(d, data[:8])[0] print(fLReal value: {lreal_value})d对应大端双精度浮点数占8个字节。Python的float就是双精度所以解析出来的值可以直接参与后续的数学运算、入库、展示。这里要提醒一个容易犯的错LReal是8字节REAL是4字节。在DB块里它们对应的符号名分别是“LReal”和“Real”如果你的变量表里写的是REAL却用get_lreal去读读出来的字节长度就不对数据会错位。读取时size参数必须和类型严格匹配Dint用4LReal用8REAL用4INT用2。3.4 连续读取DB块内的多个变量现场需求通常不是只读一个变量。比如某个DB块里偏移0处是一个Dint类型的累计数偏移4处是一个LReal类型的压力值偏移12处是一个Dint类型的设备状态。这时候最合理的做法是一次性读取整段数据然后分别解析。# 一次性读取DB1从偏移0开始的16个字节 data client.db_read(db_number1, start0, size16) total_count get_dint(data, 0) # 偏移0读4字节 pressure get_lreal(data, 4) # 偏移4读8字节 device_status get_dint(data, 12) # 偏移12读4字节 print(total_count, pressure, device_status)为什么说这是一次“区块读取”因为从偏移0到偏移16正好覆盖了这三个变量的全部字节4Dint 8LReal 4Dint 16。这种做法的优势是减少网络交互次数只发一个请求、收一个响应就能拿到多个变量的数据。如果每个变量单独db_read一次数据量小还能接受变量多起来网络开销会明显增加现场通信负载高的情况下还可能丢包。地址偏移的计算一定要拿纸笔推演别在脑子里想象。我踩过一次很典型的坑一个DB块偏移0是INT占2字节偏移2是Dint占4字节但我在读Dint时想当然地用了偏移4结果把Dint的低两位和下一个REAL的高两位拼在一起读出来的数值完全不可信。3.5 封装一个通用读取器把上面这些逻辑封装成一个可复用的类是让代码真正落地的好办法。下面是我在多个项目里用的一套结构。import snap7 from snap7.util import get_int, get_dint, get_real, get_lreal class S7DataReader: def __init__(self, ip, rack0, slot1): self.client snap7.client.Client() self.client.connect(ip, rack, slot) def read_dint(self, db_number, offset): data self.client.db_read(db_number, offset, 4) return get_dint(data, 0) def read_lreal(self, db_number, offset): data self.client.db_read(db_number, offset, 8) return get_lreal(data, 0) def read_int(self, db_number, offset): data self.client.db_read(db_number, offset, 2) return get_int(data, 0) def read_real(self, db_number, offset): data self.client.db_read(db_number, offset, 4) return get_real(data, 0) def read_segment(self, db_number, start, size): return self.client.db_read(db_number, start, size) def disconnect(self): self.client.disconnect() # 使用示例 reader S7DataReader(192.168.0.10) dint_val reader.read_dint(1, 0) lreal_val reader.read_lreal(1, 4) print(dint_val, lreal_val) reader.disconnect()这个类的好处是每个类型对应一个方法名字直观调用方便。我在实际项目中还会在read_segment里加一个超时重试机制比如请求异常时自动重连一次来处理现场偶发的通信抖动。4. 实战中的故障排查与避坑记录4.1 连接失败但IP能ping通这是出现频率最高的问题。能ping通说明网络链路是通的但Snap7连接失败通常绕不开这三个原因第一Rack或Slot参数不对。我见过一个现场工程师为了省事把所有设备的Rack都填成0结果S7-300连得上S7-1500死活连不上因为后者的实际槽号不是1。解决方法是打开博途设备组态确认或者试错几个组合。第二PLC未开放PUT/GET通信权限。这个在S7-1200/1500上尤其常见。博途里有个默认勾选项是禁止外部读写很多人没注意到导致Snap7连上了但一读写就超时。必须到“保护与安全 - 连接机制”里勾选允许外部访问。第三防火墙拦截。现场电脑若开启Windows防火墙可能拦截Snap7使用的102端口需要在防火墙中放行。还有更隐蔽的情况PLC所在的工业网段与办公网段做了VLAN隔离Snap7的请求根本到达不了PLC。这类问题要联合网络管理员排查而不是单纯在代码层面解决。4.2 读出来的数值明显不对怎么定位数值不对一般问题出在地址偏移或字节长度不匹配。我曾经遇到一个案例设备状态字在触摸屏上显示正常但程序读出来是负数。后来对照博途的变量表才发现那个变量在组态里定义的是INT程序却用Dint去读了。4字节的Dint读取包含了这个INT变量的两个字节又把下一段数据的字节也拼了进来自然就错了。排查这个问题的标准动作是“对照变量表 十六进制打印”。先把db_read拿到的原始字节打印成hex然后和博途变量表里该变量的真实值做对比。比如在博途里给某个Dint写入0x12345678协议层面读出来应该就是12 34 56 78这样的原始字节。如果看到的是78 56 34 12说明你用了小端解析如果完全不是这几个字节说明地址都错了压根不是这个变量。4.3 字节序问题导致的数值反转字节序错误的表现非常典型变量值是1程序读出来可能是16777216也就是0x01000000被当成小端解析成了0x00000001不对应该是256的倍数之类的。具体来说如果我认定了4字节00 00 00 01用i解析得到1用i小端解析则得到16777216。有些新手会把这种值当成“数据错乱”其实是解析端字节序问题。解决办法很简单统一使用snap7.util里的get_dint、get_lreal、get_real、get_int这些工具函数它们内部已经固定好大端解析规则。如果确实要用struct手动解析所有格式都必须加上前缀比如i、d、f、h。4.4 优化DB块读不出数据这个问题很隐蔽代码完全没错但读取结果要么超时要么全零。调查后发现DB块组态时启用了“优化的块访问”Optimized block access。在优化模式下变量的地址不是简单地从0开始顺序排列而是由编译器自动排布对外部通信而言等于“不可见”。Snap7这类工具访问优化DB时无法通过固定的偏移地址读取任何有效内容。解决方式在博途里选中对应的DB块在属性中取消“优化的块访问”选项保存编译后重新下载到PLC。注意取消优化后DB块内变量的实际地址会发生重排那么所有依赖该DB块的读写程序、HMI变量绑定都要跟着更新否则会出现张冠李戴。这个改动的影响面较大现场操作前建议先导出变量表备份。关于优化DB和非优化DB还有一个判断方法在博途DB编辑器的“监视”视图中优化DB的偏移列通常显示为灰色或不显示具体数字非优化DB则有一个明确的OFFSET值序列。4.5 触摸屏和PLC都连接成功但读出的数据对不上号一个容易忽略的点现场可能有多台PLC和多台触摸屏所有设备的IP都在同一个网段。如果程序里只改了触摸屏IP却没注意触摸屏绑定的PLC可能换了CPU或者有冗余切换就容易出现“程序读到的是A车间数据你以为自己在读B车间”的情况。这种问题在代码层面无解必须在现场确认设备对应关系最简单的方法是在博途的在线诊断里查看触摸屏当前连接的实际PLC名称和型号。5. 进阶把数据解析流程做得更可靠5.1 用变量定义文件自动生成解析逻辑当项目的DB块里有几十个甚至上百个变量时手写解析代码的风险和成本都太高。我的做法是把变量表导出为结构化文件JSON或Excel然后在程序里自动生成解析逻辑。逻辑本身不复杂核心是“变量名 偏移 数据类型”这三个要素一一对应。下面是一个JSON配置的示例{ db_number: 1, variables: [ {name: total_count, offset: 0, type: DINT}, {name: pressure, offset: 4, type: LREAL}, {name: device_status, offset: 12, type: DINT}, {name: temperature, offset: 16, type: LREAL} ] }对应的解析器可以借助循环完成。import json import snap7 from snap7.util import get_dint, get_lreal, get_real, get_int def parse_config(config_path): with open(config_path, r, encodingutf-8) as f: cfg json.load(f) return cfg def read_variables_by_config(client, cfg): db cfg[db_number] start 0 # 最大偏移 类型长度 total_size max(v[offset] for v in cfg[variables]) total_size 8 block_data client.db_read(db_numberdb, start0, sizetotal_size) result {} parse_map { DINT: (get_dint, 4), LREAL: (get_lreal, 8), REAL: (get_real, 4), INT: (get_int, 2) } for v in cfg[variables]: func, size parse_map[v[type]] result[v[name]] func(block_data, v[offset]) return result这个方案后期扩展非常方便新增一个变量只需要改配置文件的对应行不必动代码。而且配置文件可以和触摸屏组态的变量表保持同步从源头降低了地址错位的概率。5.2 通信超时与异常断线重连机制工业现场的以太网环境并不总是稳定。电机的启停、变频器的干扰、交换机的端口协商都可能导致PLC和上位机之间的连接短暂中断。如果程序不做任何容错一次断线就会让采集进程崩溃或数据流中断。我在封装读取器时习惯加一个自动重连机制。基本逻辑是捕获Snap7的通信异常等待几秒后重新连接重连成功后继续执行后续读取操作。class RobustS7Reader(S7DataReader): def __init__(self, ip, rack0, slot1, max_retry3): super().__init__(ip, rack, slot) self.max_retry max_retry def read_with_retry(self, db_number, offset, read_func, size): for i in range(self.max_retry): try: data self.client.db_read(db_number, offset, size) return read_func(data, 0) except Exception as e: print(fRead failed, retry {i1}/{self.max_retry}: {e}) time.sleep(1) raise RuntimeError(Max retries exceeded)这里的关键是read_func这个参数它可以是get_dint、get_lreal等解析函数这样这个方法就能被所有类型复用。实际使用时把read_dint和read_lreal改成调用read_with_retry即可。5.3 数据采集写入数据库的思路数据读出来只是第一步大多数场景需要写入数据库或者推送MES。最简单的做法是配合pandas和SQLAlchemy把每次采集到的数据转成DataFrame按批次写入。import pandas as pd from sqlalchemy import create_engine # 采集一批数据 row {timestamp: datetime.now(), total_count: total_count, pressure: pressure} df pd.DataFrame([row]) df.to_sql(production_data, conengine, if_existsappend, indexFalse)如果是高频数据比如每秒采集一次建议做成内存队列批量落库避免每条数据都开一次数据库连接。如果现场已经有OPC UA网关Snap7的定位可以更灵活Snap7作为PLC数据采集层把数据推到MQTT或Redis再让上层应用消费形成一套完整的工业数据链路。6. 最后补充几句实操心法Snap7读西门子数据的整体流程说到底就是三件事通信授权打开、地址算对、字节序解析对。这三件事都不难但每个环节都有隐藏的坑。从我个人经验来看最容易出问题的不是代码层面而是工程信息不透明——你拿不到准确的DB变量表或者拿到的是过期版本。所以在动手前我一定会先做两件事一是拿到最新的PLC程序变量表二是用“读4字节hex打印”的方式验证一两个已知变量。这两步的投入只要十几分钟却能把后面好几个小时的排查时间省下来。另外如果是给长期运行的产线做采集不要忽略异常断线重连和重复数据去重。工业现场设备重启、固件升级、触摸屏程序下载这些操作都可能让PLC短暂离线采集程序如果扛不住这些小震动会产生数据空洞或重复记录。给自己留一个简单的手工验证指令比如在命令行里跑一段单独的读取脚本检查当前连通状态排查问题时特别管用。这套方案目前在我的多个项目里稳定运行覆盖了S7-1200、S7-1500和部分S7-300设备。如果你正准备用Python对接西门子触摸屏背后的数据照着上面的步骤一步步来先跑通一个Dint变量再扩展到LReal最后做成通用采集器整个流程会非常顺。遇到问题也欢迎对照本文的排查清单逐项核对大部分疑难杂症都能定位到明确原因。