搞工业自动化的朋友一定都有这种经历:现场立着一台西门子触摸屏,屏幕上实时刷着温度、压力、产量这些关键参数,但旁边那台办公电脑上却没有博途,只有Python环境。想把屏幕上的数据接出来做报表、做预警、做历史曲线,第一个念头就是直接用程序去“读触摸屏”。但这儿的门道比想象中要多不少,尤其是数据类型里最常见的Dint(32位整型)和LReal(64位双精度浮点)。这两个数据类型的解析方式,和普通整数、单精度浮点数完全不是一个路子,我之前第一次上手时就因为字节序和地址偏移这两个问题卡了整整一个下午。
先说结论:用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里常见的INT(16位整数,范围-32768到32767),Dint能表达的数字范围大得多。为什么触摸屏上经常用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.dll,Linux下是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, rack=0, slot=1): client = snap7.client.Client() client.connect(ip, rack, slot) return client client = connect_plc('192.168.0.10') # 读取DB号、起始偏移、字节长度 data = client.db_read(db_number=1, start=0, size=4) 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_number=1, start=0, size=4) dint_value = get_dint(data, 0) print(f"Dint value at DB1.DBD0: {dint_value}")get_dint接收两个参数,第一个是字节数组,第二个是在数组内部的起始偏移(注意这个偏移是数组内偏移,不是PLC的DB地址偏移)。它会自动按照西门子的大端规则把4个字节解析成32位有符号整数。
再来看自己手动解析的方式。
# 方式二:使用struct手动解析 import struct data = client.db_read(db_number=1, start=0, size=4) dint_value = struct.unpack('>i', data[:4])[0] print(f"Dint 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_number=1, start=0, size=8) lreal_value = get_lreal(data, 0) print(f"LReal value: {lreal_value}")手动解析的写法:
import struct data = client.db_read(db_number=1, start=0, size=8) lreal_value = struct.unpack('>d', data[:8])[0] print(f"LReal value: {lreal_value}")>d对应大端双精度浮点数,占8个字节。Python的float就是双精度,所以解析出来的值可以直接参与后续的数学运算、入库、展示。
这里要提醒一个容易犯的错:LReal是8字节,REAL是4字节。在DB块里它们对应的符号名分别是“LReal”和“Real”,如果你的变量表里写的是REAL,却用get_lreal去读,读出来的字节长度就不对,数据会错位。读取时size参数必须和类型严格匹配:Dint用4,LReal用8,REAL用4,INT用2。
3.4 连续读取DB块内的多个变量
现场需求通常不是只读一个变量。比如某个DB块里,偏移0处是一个Dint类型的累计数,偏移4处是一个LReal类型的压力值,偏移12处是一个Dint类型的设备状态。这时候最合理的做法是一次性读取整段数据,然后分别解析。
# 一次性读取DB1从偏移0开始的16个字节 data = client.db_read(db_number=1, start=0, size=16) 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,正好覆盖了这三个变量的全部字节:4(Dint)+ 8(LReal)+ 4(Dint)= 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, rack=0, slot=1): 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', encoding='utf-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_number=db, start=0, size=total_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, rack=0, slot=1, max_retry=3): 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(f"Read failed, retry {i+1}/{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', con=engine, if_exists='append', index=False)如果是高频数据(比如每秒采集一次),建议做成内存队列批量落库,避免每条数据都开一次数据库连接。如果现场已经有OPC UA网关,Snap7的定位可以更灵活:Snap7作为PLC数据采集层,把数据推到MQTT或Redis,再让上层应用消费,形成一套完整的工业数据链路。
6. 最后补充几句实操心法
Snap7读西门子数据的整体流程,说到底就是三件事:通信授权打开、地址算对、字节序解析对。这三件事都不难,但每个环节都有隐藏的坑。从我个人经验来看,最容易出问题的不是代码层面,而是工程信息不透明——你拿不到准确的DB变量表,或者拿到的是过期版本。所以在动手前,我一定会先做两件事:一是拿到最新的PLC程序变量表,二是用“读4字节+hex打印”的方式验证一两个已知变量。这两步的投入只要十几分钟,却能把后面好几个小时的排查时间省下来。
另外,如果是给长期运行的产线做采集,不要忽略异常断线重连和重复数据去重。工业现场设备重启、固件升级、触摸屏程序下载,这些操作都可能让PLC短暂离线,采集程序如果扛不住这些小震动,会产生数据空洞或重复记录。给自己留一个简单的手工验证指令,比如在命令行里跑一段单独的读取脚本,检查当前连通状态,排查问题时特别管用。
这套方案目前在我的多个项目里稳定运行,覆盖了S7-1200、S7-1500和部分S7-300设备。如果你正准备用Python对接西门子触摸屏背后的数据,照着上面的步骤一步步来,先跑通一个Dint变量,再扩展到LReal,最后做成通用采集器,整个流程会非常顺。遇到问题也欢迎对照本文的排查清单逐项核对,大部分疑难杂症都能定位到明确原因。