Python and PLC Communication

  liuyuancheng        2024-06-30 02:21:17       1,959        0          English  简体中文  繁体中文  ภาษาไทย  Tiếng Việt 

本文详细介绍了如何使用 Python 通过以太网与 Schneider M221 和 Siemens S7-1200 PLC 进行通信。此外,我们还提供了一个打包的 Python PLC 客户端库,使您能够轻松构建自己的 SCADA 控制程序,例如人机界面 (HMI)。系统概述如下所示:

要查看项目详细信息,请参阅 Python 物理 PLC 通信客户端

# Created:     2024/06/29
# Version:     v0.1.3
# Copyright:   Copyright (c) 2024 LiuYuancheng
# License:     MIT License

 


简介

本文将通过四个基本步骤,引导初级 OT 工程师在 SCADA 网络中配置可编程逻辑控制器 (PLC),并使用 Python 脚本或我们的 Python 物理 PLC 通信客户端库与 PLC 进行通信。这四个步骤包括:

 

  • [步骤 1] 配置 PLC:学习如何使用 PLC 供应商的 IDE 设置 PLC 的 IP 地址,从而实现网络连接。请注意,本节不包括电气接线连接。

  • [步骤 2] 配置梯形图逻辑:了解如何配置 PLC 的 I/O 和梯形图,以链接触点、内存和线圈。

  • [步骤 3] 了解 PLC 通信协议:简要概述用于与 PLC 交互的通信协议。本节是可选的。

  • [步骤 4] 使用 Python 控制 PLC:解释如何构建 Python 程序来控制 PLC,包括演示如何使用 Python 物理 PLC 通信客户端 API 的代码示例。

 


 

与 Schneider M221 PLC 通信

 

M221 PLC 简介:Schneider Electric M221 PLC 是一款紧凑且通用的可编程逻辑控制器,专为中小型自动化项目而设计。它是 Modicon M221 系列的一部分,具有 16 个 IO、7 个继电器输出,以其高性能和成本效益而闻名。M221 PLC 支持各种通信协议,包括 Modbus TCP/IP 和串行通信,使其易于集成到现有系统中。凭借其强大的处理能力、广泛的 I/O 选项以及通过 SoMachine Basic 软件进行的用户友好型编程,M221 PLC 非常适合控制机械、管理流程和增强工业环境中的自动化。

步骤 1:配置 Schneider M221 PLC

 

启动 M221 PLC 并将其连接到网络。使用 SoMachine 编辑器搜索并连接到 PLC 单元。然后在 MyController > ETH1 部分中为 PLC 配置固定 IP 地址并启用 Modbus 通信,如下所示:

图 03:M221 PLC IP 配置页面,版本 v1.3 (2024)

 

选择 固定 IP 地址 并填写 IP 信息,选中 启用 EtherNet/IP 协议启用 Modbus 服务器。然后,同一子网中的程序可以通过 IP 地址连接到 PLC 并与 Modbus 服务器通信。

 

步骤 2:配置梯形图逻辑

虽然 M221 支持标准 Modbus TCP 协议通信,但在没有 SoMachine SDK 的情况下,您无法直接读取触点“I0.X”或写入线圈“Q0.X”。解决方案是将触点“I0.X”或线圈“Q0.X”映射到 PLC 内存地址。然后,您可以读取或写入此内存地址以获取触点输入或设置线圈输出。梯形图逻辑可以草拟如下:

Rung 1: [ I0.x ] --> | M1x | 
Rung 2: | M1x | --> | 您的梯形图逻辑 | --> | M2x |
Rung 3: | M2x | --> ( Q0.x )

 

打开 SoMachine 梯形图配置页面,并添加如下所示的梯形图逻辑:

图 04:SoMachine 编辑梯形图逻辑梯级,版本 v1.3 (2024)

 

然后,在调试页面中,选择“PC to Controller (download)”将梯形图逻辑提交到 PLC,如下所示:

图 05:SoMachine 将梯形图提交到 PLC 控制器,版本 v1.3 (2024)

步骤 3:了解通信协议

M221 使用 Modbus TCP 通信协议。Modbus TCP 帧结构序列如下所示:

  1. 事务标识符(2 个字节):事务的唯一标识符。它通常由客户端设置,并由服务器回显。

  2. 协议标识符(2 个字节):对于 Modbus TCP,始终设置为 0。

  3. 长度(2 个字节):后续字节数,包括单元标识符、功能代码和数据。

  4. 单元标识符(1 个字节):远程服务器 (PLC) 的地址。

  5. 功能代码(1 个字节):定义要执行的操作(例如,读取保持寄存器)。

  6. 数据:这包括请求或响应的具体细节(例如,起始地址、寄存器数量)。

要与 PLC 交互,您需要使用特定的 Modbus 功能代码:

  • '0f':用于写入多个位的内存位访问功能代码。

  • '01':用于读取内部多个位的内存位状态获取功能代码 %M

 

要使用更多功能,请参阅M221 手册第 196 页中的功能代码表。

Figure-06: M221 Modbus 功能代码表,版本 v1.3 (2024)

 

M221 Modbus-TCP 数据包序列表如下所示:

Figure-07: M221 Modbus 数据包字节序列图,版本 v1.3 (2024)

从内存读取位数据的 Modbus 消息序列:

TID PROTOCOL_ID 长度 UID 功能代码 内存索引 位数量
2 字节 2 字节 2 字节 1 字节 1 字节 2 字节 2 字节
0000 0000 0006 01 01 <0032> <0008>

将字节数据写入内存的 Modbus 消息序列:

TID PROTOCOL_ID 长度 UID 功能代码 内存索引 位索引 字节索引 值字节
2 字节 2 字节 2 字节 1 字节  

要将内存标签 %MXX 转换为内存地址,只需将十进制数转换为十六进制数(字符串使用小写)。示例如下:

MEM_ADDR_TAG_Example = {
    'M0':   '0000',
    'M1':   '0001',
    'M2':   '0002',
    'M3':   '0003',
    'M4':   '0004',
    'M5':   '0005',
    'M6':   '0006',
    'M10':  '000a',
    'M20':  '0014',
    'M30':  '001e',
    'M40':  '0028',
    'M50':  '0032',
    'M60':  '003c'
}

步骤 4:使用 Python 与 PLC 通信

4.1 初始化连接

要与 PLC 通信,首先初始化一个 TCP 客户端,该客户端连接到 PLC 的 IP 地址的 502 端口,如下所示:

self.plcAgent = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
self.plcAgent.connect((self.ip, self.port))
if self.debug: print("M221Client: Connected to the PLC [%s]" % self.ip)
self.connected = True
except Exception as error:
print("M221Client: Can not access to the PLC [%s]" % str(self.plcAgent))
print(error)

4.2 向 PLC 发送消息

要向 PLC 发送消息,请将十六进制字符串转换为字节:

bdata = bytes.fromhex(modbusMsg)
try:
  self.plcAgent.send(bdata)
  respBytes = self.plcAgent.recv(BUFF_SZ)
  respStr = respBytes.dencode('hex') if DECODE_MD else respBytes.hex()
  self.connected = True

4.3 读取 PLC 内存数据

基于步骤 3,构建内存读取 Modbus 消息并调用发送函数以从 PLC 读取内存字节。

def readMem(self, memAddrTag, bitNum=8):
  if str(memAddrTag).startswith('M'):
      memoryDecimal = int(memAddrTag[1:])
      memoryHex = hex(memoryDecimal)[2:]
      bitNumHex = hex(bitNum)[2:]
      modbusMsg = ''.join((TID, PROTOCOL_ID, R_LENGTH, UID, M_RD,
      memoryHex, bitNumHex))
      response = self._getPlCRespStr(modbusMsg)
      return response

输入:

  • memAddrTag: (str) 在步骤 2 中配置的梯形图中内存标签,例如“M60”。

  • bitNum: (int) 如果我们要读取 1 个字节,则要从内存中读取多少位,bitNum =4。

4.4 写入 PLC 内存数据

与数据读取相同,基于步骤 3 中的字节序列构建 modbus 消息:

def writeMem(self, memAddrTag, val):
  if str(memAddrTag).startswith('M'):
      memoryDecimal = int(memAddrTag[1:])
      memoryHex = hex(memoryDecimal)[2:]
      byteVal = VALUES[val]
      modbusMsg = ''.join((TID, PROTOCOL_ID, W_LENGTH, UID, M_FC, memoryHex,
      BIT_COUNT, BYTE_COUNT, byteVal))
      response = self._getPlCRespStr(modbusMsg)
      return response

 

完整的 Python M221 PLC 客户端程序

您可以从此链接下载完整的 Python M221 PLC 客户端程序:M221PlcClient.py。该程序包括连接 PLC、读取/写入内存位的 API 以及一个线程包装器类,该类允许您在主程序中并行运行 PLC 读取器,以定期读取 PLC 状态。

该库还提供了三个测试用例:

 

  • 测试用例 1 & 2:演示如何读取和写入 PLC 内存。

  • 测试用例 3:展示如何使用客户端线程包装器类。

 


 

与 Siemens S7-1200 PLC 通信

PLC 简介

: 西门子 S7-1200 PLC 是一款紧凑且多功能的 可编程逻辑控制器,专为各种工业自动化应用而设计。它属于 SIMATIC S7 系列,以其强大的性能、可扩展性和易用性而闻名。它具有内置的 PROFINET 接口,并支持各种通信协议,从而可以无缝集成到工业网络中。S7Comm 是西门子 PLC(包括 S7-1200 系列)使用的一种专有通信协议,用于促进设备和软件之间的通信。它通过各种物理层(包括以太网(通过 PROFINET))运行,从而允许 PLC、HMI 和 SCADA 系统之间进行数据交换。

 

步骤 1:配置西门子 S7-1200

 

将西门子 S7-1200 PLC 连接到您的网络。使用西门子 PLC 编辑器软件,Siemens SIMATIC STEP 7 (TIA Portal) ,在 PROFINET 接口页面中配置 IP 地址:

图 08:通过 SIMATIC STEP 7 (TIA Portal) 设置 S71200 IP,版本 v1.3 (2024)

 

您可以按照本文中概述的步骤操作:https://www.geekering.com/categories/automation/rodrigovieira/siemens-tia-portal-s7-1200-plc-online-connection-2/来设置 IP 地址并配置 PLC。

步骤 2:配置梯形图逻辑和存储区

S7-1200 PLC 支持直接映射存储区,用于读取和写入 PLC 触点、线圈和可编辑存储器上的数据。使用 snap7 库通过 Siemens S7Comm 协议可以方便地与 PLC 进行通信。PLC 梯形图逻辑可以配置如下:

  | ix.x/mx.x | --> | 您的梯形图逻辑 | --> | qx.x/mx.x |

 

要实现此目的,请创建一个块并按照下图所示合并梯形图逻辑:

图 08:通过 SIMATIC STEP 7 (TIA Portal) 设置 S71200 梯形图,版本 v1.3 (2024)

当使用默认存储区时,触点、线圈和可编辑存储器的起始地址如下:

  • PLC 触点存储区 (%i0.X) : 0x81

  • PLC 可编辑存储区 (%m0.x): 0x83

  • PLC 线圈存储区 (%q0.x): 0x82

 

步骤 3:了解通信协议

 

有关详细的 S7Comm 数据包结构,您可以参考这篇文章:https://blog.viettelcybersecurity.com/security-wall-of-s7commplus-part-1/。数据包结构如下图所示:

为了使用 S7Comm 协议与 PLC 通信,我们将使用 Python snap7 库https://python-snap7.readthedocs.io/en/latest/。此库提供函数read_areawrite_area,以方便 PLC 数据的输入和输出操作。这些函数对于 PLC 环境中的高效数据交换和控制至关重要。

 

步骤 4:使用 Python 与 PLC 通信

4.1 初始化连接

为了建立与 PLC 的通信,我们初始化一个 snap7 客户端,该客户端使用端口 102 连接到 PLC 的 IP 地址,如下面的代码片段所示:

 self.plcAgent = snap7.client.Client()        
try:
    self.plcAgent.connect(self.ip, 0, 1, 502)
    if self.debug: print("S71200Client: 已连接到 PLC [%s]" % self.ip)
    self.connected = True
except Exception as err:
    print("错误:S71200Client 初始化错误:%s" % err)
    return None

 

4.2 读取 PLC 存储器

S7-1200 支持从不同存储区读取各种数据类型的数据。支持以下数据类型:

数据类型 字节数 标识 存储器标签格式
BOOL_TYPE (bool) 1 个字节 x x<起始字节>.<起始位>
Byte_TYPE (byte/int) 1 个字节 b 要将读取的数据转换为其对应的值,请使用 snap7.util.get_* 中的函数,如下所示:

    def _memByte2Value(self, mbyte, valType, startMIdx, bitIndex):
      """ 将内存字节转换为指定类型的值。
          Args:
              mbyte (bytes): 数据字节。
              valType (int): 转换值的数据类型。
              startMIdx (int): 内存字节的起始索引。
              bitIndex (_type_): 内存位的起始索引。
          Returns:
              _type_: _description_
      """
      data = None
      if valType == BOOL_TYPE:
          data = snap7.util.get_bool(mbyte, 0, bitIndex)
      elif valType == INT_TYPE:
          data = snap7.util.get_int(mbyte, startMIdx)
      elif valType == REAL_TYPE:
          data = snap7.util.get_real(mbyte, 0)
      elif valType == WORD_TYPE:
          data = snap7.util.get_word(mbyte, startMIdx)
      elif valType == DWORD_TYPE:
          data = snap7.util.get_dword(mbyte, 0)
      else:
          print("Error: _getMemValue()> 输入类型无效: %s" % str(valType))
      return data

要从特定的内存标签类型读取数据,请配置起始字节索引和位索引,然后读取数据:

        if(memAddrTag[1].lower() == 'x'):
          # 配置布尔类型数据标签
          valLength = 1
          valType = BOOL_TYPE
          startMIdx = int(memAddrTag.split('.')[0][2:])
          bitIndex = int(memAddrTag.split('.')[1])
      elif(memAddrTag[1].lower() == 'b'):
          # 配置字节或整数类型数据标签
          valLength = 1
          valType = INT_TYPE
          startMIdx = int(memAddrTag[2:])
      elif(memAddrTag[1].lower() == 'w'):
          # 配置字类型数据标签
          valLength = 2
          valType = WORD_TYPE
          startMIdx = int(memAddrTag[2:])
      elif(memAddrTag[1].lower() == 'd'): # double
          valLength = 4
          valType = DWORD_TYPE
          startMIdx = int(memAddrTag.split('.')[0][2:])
      elif('freal' in memAddrTag.lower()): # float real number
          valLength = 4
          valType = REAL_TYPE
          startMIdx = int(memAddrTag.lower().replace('freal', ''))
      else:
          print("Error: readMem()> 输入内存标签无效: %s" %str(memAddrTag))
          return None
      # 初始化内存起始区域。
      memoryArea = MEM_AREA_IDX[memType]
      try:
          mbyte = self.plcAgent.read_area(memoryArea, 0, startMIdx, valLength)

 

4.3 写入 PLC 内存数据

当向 PLC 写入字节数据时,重要的是不要覆盖不应更改的位。为了实现这一点,首先从内存中读取数据,使用 snap7.util.set_* 函数修改相关部分,然后将修改后的数据写回内存。这是一个简化的内存写入函数:

    def writeMem(self, mem, value):
      """ 从相关的内存地址设置 PLC 状态:IX0.N-输入,QX0.N-输出,
          MX0.N-内存。
      """
      data = self.getMem(mem, True)
      start = bit = 0 # 起始位置索引
      # 获取区域内存地址
      memType = mem[0].lower()
      area = self.memAreaDict[memType]
      # 设置数据长度和起始索引,并调用 中的实用函数
      if(mem[1].lower() == 'x'): # 位
          start, bit = int(mem.split('.')[0][2:]), int(mem.split('.')[1])
          set_bool(data, 0, bit, int(value))
      elif(mem[1].lower() == 'b'): # 字节
          start = int(mem[2:])
          set_int(data, 0, value)
      elif(mem[1].lower() == 'w'):
          start = int(mem.split('.')[0][2:])
      elif(mem[1].lower() == 'd'):
          start = int(mem.split('.')[0][2:])
          set_dword(data, 0, value)
      elif('freal' in mem.lower()): # 双字(实数)
          start = int(mem.lower().replace('freal', ''))
          set_real(data, 0, value)
      # 调用写入函数并返回值。
      return self.plc.write_area(area, 0, start, data)

这种增强的解释和代码结构应该为使用 Python 和 snap7 库与 PLC 通信提供更清晰的理解和实现指南。调整内存区域和数据类型对于在 PLC 应用程序中准确处理和控制数据至关重要。

 

完整的 Python S71200 PLC 客户端程序

您可以从此链接下载完整的 Python M221 PLC 客户端程序:S71200PlcClient.py。该程序包括连接 PLC、读取/写入 PLC 触点、内存线圈数据的 API,以及一个线程包装器类,允许您在主程序中使用并行线程运行 PLC 读取器,以定期读取 PLC 状态。

该库还提供了三个测试用例:

  • 测试用例 1:演示如何读取 PLC 触点、内存和线圈。

  • 测试用例 1:演示如何写入 PLC 触点、内存和线圈。

  • 测试用例 3:展示如何使用客户端线程包装器类。

 


参考

 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

AIX