python连接PLC并写入数据

  最近需要需要将测试结果,使用python写入PLC,尝试后代码记录如下:

1.python连接PLC与写入数据

import os
import traceback

import snap7
from snap7.util import *
import time
print(snap7.__file__)      # 看看实际加载的是哪个文件
print(snap7.__version__)   # 能输出版本号即安装成功
print("DLL 路径:", os.path.join(os.path.dirname(snap7.__file__), 'snap7.dll'))

import snap7
try:                                    # 0.11+
    AREA_DB = snap7.Area.DB
except AttributeError:                  # 0.10 及更早
    AREA_DB = snap7.types.S7AreaDB

class PLCWriter:
    def __init__(self, plc_ip, rack=0, slot=1):
        """
        初始化PLC连接器

        Args:
            plc_ip: PLC的IP地址(如:'192.168.0.1')
            rack: 机架号(通常为0)
            slot: 槽位号(西门子300系列通常为1,1500系列通常为2)
        """
        self.plc_ip = plc_ip
        self.rack = rack
        self.slot = slot
        self.plc = None
        self.connected = False



    def read_real(self, db_number, offset):
        """从PLC读取Real(浮点数)值"""
        if not self.connected:
            print("未连接到PLC")
            return None

        try:
            # 读取4字节数据
            data = self.plc.read_area(AREA_DB, db_number, offset, 4)

            # 解析为浮点数
            value = get_real(data, 0)
            return float(value)

        except Exception as e:
            print(f"读取浮点数失败: {e}")
            traceback.print_exc()
            return None

    def read_int16(self, db_number, offset):
        """从PLC读取16位整数"""
        if not self.connected:
            print("未连接到PLC")
            return None

        try:
            # 读取2字节数据
            data = self.plc.read_area(AREA_DB, db_number, offset, 2)

            # 解析为整数
            value = get_int(data, 0)
            return int(value)
        except Exception as e:
            print("98: read 16 int failed",e)
            traceback.print_exc()

    # 新增:专门读取DB48中PFLong的值
    def read_PFLong_from_DB48(plc_writer, pflong_offset=8):
        """
        读取DB48中PFLong的值
        参数说明:
        - pflong_offset: PFLong在DB48中的偏移量(字节)
          根据你的描述:第三行NAME,int,0,第四行PFLong,Real,0.0
          NAME是int类型占2字节,所以PFLong的偏移量应该是2(从0开始)
          但如果前面还有static或其他变量,需要调整
        """
        # DB48号块,根据实际情况调整偏移量
        db_number = 48

        # 读取PFLong值(Real类型,占4字节)
        value = plc_writer.read_real(db_number, pflong_offset)

        if value is not None:
            print(f"[读取DB48] PFLong值: {value}")
            return value
        else:
            print("[读取DB48] 读取失败")
            return None

    def connect(self):
        try:
            self.plc = snap7.client.Client()
            self.plc.set_param(snap7.types.PingTimeout, 1000)
            self.plc.connect(self.plc_ip, self.rack, self.slot)
            self.connected = True
            print('[PLC] 已连接')
            return True
        except Exception as e:
            print(f'[PLC] 连接失败: {e}')
            return False



    def write_int16(self, db_number, offset, value):
        """写入16位整数到PLC"""
        if not self.connected:
            print("未连接到PLC")
            return False

        try:
            # 创建2字节数据
            data = bytearray(2)
            set_int(data, 0, value)

            # 写入到PLC
            self.plc.write_area(AREA_DB, db_number, offset, data)

            # 可选:读取验证
            # read_data = self.plc.read_area(snap7.types.Areas.DB, db_number, offset, 2)
            # verify_value = get_int(read_data, 0)
            # print(f"验证写入值: {verify_value}")

            return True

        except Exception as e:
            print(f"写入失败: {e}")
            return False

    def write_float(self, db_number, offset, value):
        """写入浮点数到PLC"""
        if not self.connected:
            print("未连接到PLC")
            return False

        try:
            # 创建4字节数据
            data = bytearray(4)
            set_real(data, 0, value)

            # 写入到PLC
            try:
                self.plc.write_area(AREA_DB, db_number, offset, data)
            except Exception as e:
                traceback.print_exc()
            return True

        except Exception as e:
            print(f"写入浮点数失败: {e}")
            return False

    def write_bool(self, db_number, byte_offset, bit_offset, value):
        """写入布尔值到PLC"""
        if not self.connected:
            print("未连接到PLC")
            return False

        try:
            # 读取当前字节
            current_data = self.plc.read_area(AREA_DB, db_number, byte_offset, 1)

            # 修改指定位
            set_bool(current_data, 0, bit_offset, value)

            # 写回PLC
            self.plc.write_area(AREA_DB, db_number, byte_offset, current_data)
            return True

        except Exception as e:
            print(f"写入布尔值失败: {e}")
            return False

    def disconnect(self):
        """断开连接"""
        if self.connected and self.plc:
            self.plc.disconnect()
            self.connected = False
            print("已断开PLC连接")


# 你的数据生成函数(示例)
def my_function():
    """
    你的数据生成函数
    返回一个包含数据的字典
    """
    # 这里是你实际的数据生成逻辑
    import random

    return {
        'temperature': round(random.uniform(20.0, 30.0), 2),  # 温度值
        'pressure': random.randint(100, 200),  # 压力值
        'status': random.choice([True, False]),  # 状态
        'speed': random.randint(500, 1500)  # 速度
    }


def main():
    """
    主函数:循环读取数据并写入PLC
    """

    # ================ 配置部分(需要你修改)================
    PLC_IP = "192.168.0.1"  # PLC的IP地址
    DB_NUMBER = 1  # DB块号

    # 数据在PLC中的存储位置(需要和PLC程序对应)
    ADDRESS_MAP = {
        'temperature': {'type': 'float', 'offset': 0},  # DB1.DBD0
        'pressure': {'type': 'int16', 'offset': 4},  # DB1.DBW4
        'status': {'type': 'bool', 'offset': 6, 'bit': 0},  # DB1.DBX6.0
        'speed': {'type': 'int16', 'offset': 8}  # DB1.DBW8
    }

    LOOP_INTERVAL = 1.0  # 循环间隔(秒)
    MAX_ITERATIONS = 10  # 最大循环次数(设为None表示无限循环)
    # ===================================================

    # 1. 创建PLC写入器
    print("初始化PLC写入器...")
    writer = PLCWriter(PLC_IP)

    # 2. 连接PLC
    if not writer.connect():
        print("无法连接PLC,程序退出")
        return

    # 3. 主循环
    print(f"\n开始数据写入循环,间隔: {LOOP_INTERVAL}秒")
    iteration = 0

    try:
        while True:
            if MAX_ITERATIONS and iteration >= MAX_ITERATIONS:
                print(f"达到最大循环次数: {MAX_ITERATIONS}")
                break

            iteration += 1
            print(f"\n=== 第 {iteration} 次循环 ===")

            # 3.1 获取数据
            print("获取数据...")
            try:
                data = my_function()
                print(f"获取到数据: {data}")
            except Exception as e:
                print(f"获取数据失败: {e}")
                time.sleep(LOOP_INTERVAL)
                continue

            # 3.2 写入数据到PLC
            success_count = 0
            total_count = len(data)

            for key, value in data.items():
                if key not in ADDRESS_MAP:
                    print(f"警告: {key} 没有对应的PLC地址配置")
                    continue

                addr_info = ADDRESS_MAP[key]

                try:
                    if addr_info['type'] == 'int16':
                        success = writer.write_int16(DB_NUMBER, addr_info['offset'], value)
                    elif addr_info['type'] == 'float':
                        success = writer.write_float(DB_NUMBER, addr_info['offset'], value)
                    elif addr_info['type'] == 'bool':
                        success = writer.write_bool(DB_NUMBER, addr_info['offset'], addr_info.get('bit', 0), value)
                    else:
                        print(f"未知数据类型: {addr_info['type']}")
                        continue

                    if success:
                        success_count += 1
                        print(f"  ✓ {key}: {value} → PLC成功")
                    else:
                        print(f"  ✗ {key}: {value} → PLC失败")

                except Exception as e:
                    print(f"  ✗ {key}写入异常: {e}")

            # 3.3 统计结果
            print(f"写入结果: {success_count}/{total_count} 成功")

            # 3.4 等待下一次循环
            print(f"等待 {LOOP_INTERVAL} 秒...")
            time.sleep(LOOP_INTERVAL)

    except KeyboardInterrupt:
        print("\n用户中断程序")
    except Exception as e:
        print(f"程序异常: {e}")
    finally:
        # 4. 断开连接
        writer.disconnect()
        print("程序结束")


if __name__ == "__main__":
    # 测试用:简化版配置检查
    print("=== PLC数据写入程序 ===")
    # 快速连接测试
    test_writer = PLCWriter("192.168.1.110",rack=0, slot=1)
    if test_writer.connect():
        print("连接测试成功!")
        # test_writer.disconnect()
        db_number=34
        offset=4
        test_writer.write_float(db_number, offset, 1.23)  #
    else:
        print("连接测试失败,请检查配置")
PLCWriter("192.168.1.110",rack=0, slot=1)中的参数分别是PLC的IP地址,机架号,曹号。
test_writer.write_float(db_number, offset, 1.23)表明将数据写入DB34,偏移量为4的地址,写入数据为浮点数1.23

2.测试读取是否受保护
当读取或者写入失败后,考虑是否PLC工程师将那边DB块保护起来了。
import snap7

from PLC_deal_test import PLCWriter


def diagnose_protection_issues():
    """诊断PLC保护设置"""
    writer = PLCWriter("192.168.1.110", rack=0, slot=1)

    if not writer.connect():
        return

    try:
        print("=== PLC保护诊断 ===")

        # 1. 获取CPU信息
        print("\n1. CPU基本信息:")
        try:
            cpu_info = writer.plc.get_cpu_info()
            print(f"  模块类型: {cpu_info.ModuleTypeName.decode('utf-8', errors='ignore')}")
            print(f"  序列号: {cpu_info.SerialNumber.decode('utf-8', errors='ignore')}")
            print(f"  AS名称: {cpu_info.ASName.decode('utf-8', errors='ignore')}")
            print(f"  版权: {cpu_info.Copyright.decode('utf-8', errors='ignore')}")
            print(f"  模块名称: {cpu_info.ModuleName.decode('utf-8', errors='ignore')}")
        except Exception as e:
            print(f"  获取CPU信息失败: {e}")

        # 2. 检查CPU状态
        print("\n2. CPU状态检查:")
        try:
            cpu_state = writer.plc.get_cpu_state()
            print(f"  CPU状态: {cpu_state}")

            # 检查是否在RUN模式
            if cpu_state == 'S7CpuStatusRun':
                print("  ✓ CPU在RUN模式")
            else:
                print(f"  ⚠ CPU在 {cpu_state} 模式,可能影响访问")
        except Exception as e:
            print(f"  获取CPU状态失败: {e}")

        # 3. 检查保护级别
        print("\n3. 保护级别检查:")
        try:
            # 尝试读取系统状态列表(需要密码时可能失败)
            ssl_data = writer.plc.read_szl(0x0111, 0x0001)  # CPU特性
            print(f"  可以读取系统状态列表")
        except snap7.exceptions.Snap7Exception as e:
            if "CPU" in str(e) and "denied" in str(e).lower():
                print(f"  ✗ 访问被拒绝 - 可能需要密码")
            else:
                print(f"  读取SSL失败: {e}")

        # 4. 测试不同区域的访问
        print("\n4. 内存区域访问测试:")

        test_areas = [
            ("Merker (M)", snap7.types.S7AreaMK, 0, 10),  # M区
            ("Input (I)", snap7.types.S7AreaPE, 0, 10),  # 输入
            ("Output (Q)", snap7.types.S7AreaPA, 0, 10),  # 输出
        ]

        for area_name, area_code, offset, size in test_areas:
            try:
                data = writer.plc.read_area(area_code, 0, offset, size)
                print(f"  ✓ {area_name}区可访问")
            except snap7.exceptions.Snap7Exception as e:
                print(f"  ✗ {area_name}区访问失败: {e}")

        # 5. 测试其他DB块的访问
        print("\n5. DB块访问测试:")

        # 首先获取所有DB块
        try:
            db_list = writer.plc.list_blocks_of_type('DB', 50)
            print(f"  找到 {len(db_list)} 个DB块")

            # 测试几个常见的DB块
            test_db_numbers = []
            if db_list:
                # 取前5个DB块测试
                test_db_numbers = db_list[:5]
            else:
                # 如果没有获取到列表,测试常见DB号
                test_db_numbers = [1, 2, 3, 100, 200]

            for db_num in test_db_numbers:
                try:
                    # 尝试读取1字节
                    data = writer.plc.db_read(db_num, 0, 1)
                    print(f"  ✓ DB{db_num} 可访问")

                    # 如果可以访问,尝试读取更多信息
                    try:
                        # 尝试读取DB长度(某些PLC支持)
                        length = writer.plc.get_db_length(db_num)
                        print(f"    DB{db_num} 大小: {length} 字节")
                    except:
                        # 如果不能获取长度,尝试估计大小
                        for test_size in [10, 50, 100]:
                            try:
                                data = writer.plc.db_read(db_num, 0, test_size)
                                if len(data) == test_size:
                                    print(f"    至少 {test_size} 字节")
                            except:
                                break

                except snap7.exceptions.Snap7Exception as e:
                    error_msg = str(e)
                    if "address out of range" in error_msg:
                        print(f"  ✗ DB{db_num} 不存在或大小为零")
                    elif "access denied" in error_msg.lower() or "protection" in error_msg.lower():
                        print(f"  ✗ DB{db_num} 访问被拒绝(保护)")
                    else:
                        print(f"  ✗ DB{db_num} 访问失败: {e}")
                except Exception as e:
                    print(f"  ✗ DB{db_num} 测试异常: {e}")

        except Exception as e:
            print(f"  获取DB列表失败: {e}")

        # 6. 专门测试DB48
        print("\n6. DB48专门测试:")
        if 48 in db_list:
            print("  DB48在列表中,尝试不同访问方式...")

            # 测试不同大小的读取
            read_success = False
            for read_size in [1, 2, 4, 10]:
                try:
                    data = writer.plc.db_read(48, 0, read_size)
                    print(f"  ✓ 可读取 {read_size} 字节")
                    print(f"    数据: {data.hex()}")
                    read_success = True

                    # 如果成功,尝试解析
                    if read_size >= 4:
                        try:
                            # 尝试作为Real解析(偏移量0)
                            import struct
                            real_value = struct.unpack('>f', data[:4])[0]
                            print(f"    作为Real(0): {real_value}")
                        except:
                            pass

                        if read_size >= 6:
                            try:
                                # 尝试作为Real解析(偏移量2)
                                real_value = struct.unpack('>f', data[2:6])[0]
                                print(f"    作为Real(2): {real_value}")
                            except:
                                pass

                except snap7.exceptions.Snap7Exception as e:
                    if read_size == 1:
                        print(f"  ✗ 连1字节都无法读取: {e}")
                        print(f"    → DB48可能被完全保护或大小为0")
                    break

            if not read_success:
                print("  DB48存在但无法读取任何数据 → 很可能受保护")
        else:
            print("  DB48不在DB列表中")

    except Exception as e:
        print(f"诊断过程中出错: {e}")
        import traceback
        traceback.print_exc()
    finally:
        writer.disconnect()
        print("\n=== 诊断结束 ===")


# 尝试写入DB48来测试是否写保护
def test_db_protection():
    writer = PLCWriter("192.168.1.110", rack=0, slot=1)
    writer.connect()

    try:
        # 测试1: 尝试写入
        # test_data = bytearray([0x00, 0x01])
        # try:
        #     writer.plc.db_write(48, 0, test_data)
        #     print("✓ 可以写入DB48 → 无保护或只读保护")
        # except snap7.exceptions.Snap7Exception as e:
        #     if "access denied" in str(e).lower():
        #         print("✗ 写入被拒绝 → 写保护或完全保护")



        # 测试2: 尝试读取
        try:
            data = writer.plc.db_read(48, 0, 2)
            print("✓ 可以读取DB48 → 至少可以读取")
        except:
            print("✗ 读取也被拒绝 → 完全保护")

    finally:
        writer.disconnect()


if __name__ == '__main__':
    test_db_protection()

 

  小结:以上程序现场测试过没有问题,如果报错,考虑snap7.dll是否存在。要记得安装python-snap7,版本与PLC型号也可能造成程序运行出问题。

posted @ 2026-01-01 10:46  wancy  阅读(4)  评论(0)    收藏  举报