1. Snap7 简介
Snap7 是一个开源的西门子 S7 PLC 通信库,支持 S7-200/300/400/1200/1500 全系列。它原生支持 C++,但提供了 Python、C#、Node.js 等语言的绑定。
优点:
- 开源免费,无需额外授权
- 支持多平台(Windows/Linux/macOS)
- 性能好,支持多 PLC 并发连接
- 协议层兼容,不依赖 TIA Portal
2. 环境准备
2.1 安装 python-snap7
bash
pip install python-snap7
2.2 S7-1200/1500 PLC 端设置
在 TIA Portal 中进行以下配置才能让上位机通过 S7 协议通信:
- 允许 PUT/GET 通信(S7-1500 需特别注意)
- 在设备组态 → 保护与安全 → 编译块时支持仿真/PUT/GET 通信 → 勾选
- 优化块访问 → 标准访问
- DB 块属性中,取消"优化的块访问"(否则直接读写需要按符号名,Snap7 读写按地址偏移)
- 防火墙设置
- 确保 PLC 的以太网口防火墙允许 S7 通信(端口 102)
特别注意:S7-1200 固件 4.0+ 默认关闭 PUT/GET,需在 TIA Portal 中显式开启。
3. 快速开始:连接与读取
3.1 创建客户端并连接
python
import snap7
# 创建客户端
client = snap7.client.Client()
# 参数:PLC 的 IP 地址,机架号,槽号
# S7-1200 通常 rack=0, slot=1
# S7-1500 通常 rack=0, slot=0
client.connect('192.168.0.1', 0, 1)
# 检查连接状态
if client.get_connected():
print(f"已连接到 PLC: {client.get_cpu_state()}")
else:
print("连接失败")
3.2 读取 DB 块数据
DB 块是 S7 中最常用的数据存储区,上位机和 PLC 通过 DB 交换数据。
python
def read_db_block(client, db_number, start_offset, byte_count):
"""
读取 DB 块数据
:param client: snap7 客户端
:param db_number: DB 块编号
:param start_offset: 起始字节偏移
:param byte_count: 读取的字节数
:return: bytes 对象
"""
try:
data = client.db_read(db_number, start_offset, byte_count)
return data
except snap7.snap7exceptions.Snap7Exception as e:
print(f"读取 DB{db_number} 失败: {e}")
return None
# 示例:读取 DB1 从偏移 0 开始的 10 个字节
data = read_db_block(client, 1, 0, 10)
if data:
print(f"原始字节: {data.hex()}")
3.3 字节数据解析工具函数
PLC 中常见的数据类型及其字节长度:
| 类型 | 长度(字节) | Python 解析方法 |
|---|---|---|
| Bool | 1 (bit) | 位运算提取 |
| Byte | 1 | int.from_bytes() |
| Int | 2 | int.from_bytes() |
| DInt | 4 | int.from_bytes() |
| Real | 4 | struct.unpack('>f') |
| String | 可变 | bytes.decode() |
python
import struct
def parse_int(data, offset=0):
"""解析 S7 Int (有符号 16 位)"""
return int.from_bytes(data[offset:offset+2], byteorder='big', signed=True)
def parse_dint(data, offset=0):
"""解析 S7 DInt (有符号 32 位)"""
return int.from_bytes(data[offset:offset+4], byteorder='big', signed=True)
def parse_real(data, offset=0):
"""解析 S7 Real (32 位浮点数)"""
return struct.unpack('>f', data[offset:offset+4])[0]
def parse_word(data, offset=0):
"""解析 S7 Word (无符号 16 位)"""
return int.from_bytes(data[offset:offset+2], byteorder='big')
def parse_dword(data, offset=0):
"""解析 S7 DWord (无符号 32 位)"""
return int.from_bytes(data[offset:offset+4], byteorder='big')
def parse_byte_array(data, offset=0, length=1):
"""解析 Byte 数组"""
return list(data[offset:offset+length])
# 使用示例
raw = client.db_read(1, 0, 40) # 一次读取 40 字节
temp_value = parse_real(raw, 0) # Real 类型温度值,偏移 0
pressure = parse_int(raw, 4) # Int 类型压力值,偏移 4
status_flags = parse_dword(raw, 6) # DWord 状态标志,偏移 6
3.4 完整示例:读取 PLC 中的设备运行数据
python
import snap7
import struct
import time
class S7Client:
"""西门子 PLC 数据采集客户端封装"""
def __init__(self, ip, rack=0, slot=1):
self.client = snap7.client.Client()
self.ip = ip
self.rack = rack
self.slot = slot
def connect(self):
try:
self.client.connect(self.ip, self.rack, self.slot)
print(f"[+] 已连接到 {self.ip}")
return True
except Exception as e:
print(f"[-] 连接失败: {e}")
return False
def disconnect(self):
self.client.disconnect()
def read_device_data(self):
"""
从 DB10 读取设备运行数据
字节分配(与 PLC 程序员约定好的):
Offset 0-3: Real 温度
Offset 4-7: Real 压力
Offset 8-9: Int 转速
Offset 10-13: DInt 累计运行时间(秒)
Offset 14: Byte 设备状态(0=停止,1=运行,2=故障)
Offset 15: Byte 报警代码
"""
try:
data = self.client.db_read(10, 0, 16)
return {
'temperature': struct.unpack('>f', data[0:4])[0],
'pressure': struct.unpack('>f', data[4:8])[0],
'speed': int.from_bytes(data[8:10], 'big', signed=True),
'run_time': int.from_bytes(data[10:14], 'big', signed=True),
'status': data[14],
'alarm_code': data[15],
}
except snap7.snap7exceptions.Snap7Exception as e:
print(f"读取设备数据失败: {e}")
return None
def read_multi_db(self, db_configs):
"""
批量读取多个 DB 块
db_configs: [(db_num, start, size), ...]
"""
results = {}
for db_num, start, size in db_configs:
data = self.client.db_read(db_num, start, size)
results[db_num] = data
return results
# ========== 使用示例 ==========
if __name__ == '__main__':
plc = S7Client('192.168.0.1', rack=0, slot=1)
if plc.connect():
try:
while True:
data = plc.read_device_data()
if data:
print(f"温度: {data['temperature']:.1f}°C | "
f"压力: {data['pressure']:.2f} MPa | "
f"转速: {data['speed']} RPM | "
f"状态: {data['status']}")
time.sleep(1) # 每秒采集一次
except KeyboardInterrupt:
print("\n停止采集")
finally:
plc.disconnect()
4. 写入数据到 PLC
除了读取,Snap7 同样支持写入操作。
python
def write_real(client, db_number, offset, value):
"""向 PLC DB 块写入 Real 类型数据"""
data = struct.pack('>f', value)
client.db_write(db_number, offset, data)
def write_int(client, db_number, offset, value):
"""向 PLC DB 块写入 Int 类型数据"""
data = int.to_bytes(value, 2, byteorder='big', signed=True)
client.db_write(db_number, offset, data)
def write_bool(client, db_number, byte_offset, bit_offset, value):
"""向 PLC DB 块写入 Bool 类型数据"""
# 先读取当前字节
current = client.db_read(db_number, byte_offset, 1)
byte_val = current[0]
if value:
byte_val |= (1 << bit_offset)
else:
byte_val &= ~(1 << bit_offset)
client.db_write(db_number, byte_offset, bytes([byte_val]))
# 写入示例
write_real(client, 10, 0, 25.5) # DB10.0 写温度
write_int(client, 10, 8, 1500) # DB10.8 写转速
write_bool(client, 10, 20, 3, True) # DB10.20.3 写一个启停位
5. 读取 I/O 区数据
除了 DB 块,S7 还有 I/Q/M 等存储区。
python
# 读取输入映像区 PIW(过程映像输入)
# 从字节偏移 0 开始,读 4 个字节
input_data = client.ab_read(0, 4)
# 读取输出映像区 PQW(过程映像输出)
output_data = client.as_read(0, 4)
# 读取 M 存储区(中间变量)
m_data = client.read_area(snap7.types.Areas.MK, 0, 0, 10)
6. 常见问题与排查
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 连接超时 | IP/机架/槽号配置错误 | 确认 PLC IP,Ping 测试,核对 rack/slot |
DB block not found |
DB 号不存在或未下载 | 检查 TIA Portal 中的 DB 编号 |
| wrong DB length | 读取长度超出 DB 实际大小 | 缩小读取范围或增大 DB 大小 |
| 读出的数值不对 | 字节序或数据类型解析错误 | S7 是大端 (>f),确认数据类型长度 |
| 无法连接 S7-1200 v4+ | 未开启 PUT/GET 通信 | 在 TIA Portal 保护设置中勾选允许 |
7. 完整代码获取
本文所有代码已整理为可直接运行的 Python 脚本。