PLC(Programmable Logic Controller)即可编程逻辑控制器,可以理解为一个微型计算机,广泛应用于工业控制领域中,包括楼宇智控、精密机床、汽车电子等等。
    随着物联网的兴起,越来越多的传统工业设备需要和外界通信,但很多情况下,类似PLC的微控制器经常会由于自身硬件因素而无法与外界直接互联互通。PC作为一个中介桥梁,为PLC与外界的沟通打开了一扇门。

  而Python作为当前最火的语言,不仅在AI、云计算等诸多方面都能看到它的身影,在工业控制中也不能少了它。本文就来分享下如何使用Python构建PC与PLC的通信,也算展示一把Python在工控领域的风采。

     Snap7简介

     当前市场上主流的PLC通信方式为网络通信和串行通信。网络通信这块主要协议有profinet,modbus-tcp等,串行通信主要是基于RS232/485的modbus。
     本次接触到的是西门子S7系列的PLC,通信方式都为网络型的,而Snap7(http://snap7.sourceforge.net/)正是一个开源的、32/64位的、多平台的以太网通讯库:
     支持多硬件体系结构(i386/x86_64、ARM/ARM64、Sun Sparc、Mips);
     支持多系统(Windows、Linux、BSD、Solaris);
     支持多语言(C/C++、Phyton、Node.js、Pascal、C#、VB)。
     Python对其进行了封装,具体可以参见:https://github.com/gijzelaerr/python-snap7。

     开发环境搭建

     这里主要从Windows和Linux(Ubuntu)两个平台,说说如何搭建Python环境下的Snap7开发环境。Python的安装这里就不再赘述,环境搭建主要就是Snap7和python-snap7两个库的安装。

     1、安装Snap7

     Windows下,需要根据Python的结构版本(32位/64位),将下载的Snap7的发布库copy到对应的Python安装根目录下即可。
     如上图所示,我的Python是32bit,所以需要将Snap7中Win32目录下的文件Copy到Python的安装根目录下,如下图所示:
      
102515044.jpg
   
102515045.jpg
     Linux(Ubuntu)下安装相对简单些,按如下命令即可:
  
  1. $ sudo -s
  2. $ add-apt-repository ppa:gijzelaar/snap7
  3. $ apt-get update
  4. $ apt-get install libsnap71 libsnap7-dev
  
  2、安装python-snap7

     Snap7的Python库安装就简单很多了,不管是Windows还是Linux,直接pip安装即可。
  
  1. $ pip install python-snap7
    经过上面两步,环境就算搭建好了。通过一个连接测试代码试试,判断下环境是否搭建正常。
  
  1. import snap7
  2. client = snap7.client.Client()
  3. client.connect('192.168.0.1', 0, 1)
  4. client.disconnect()
    如果是下图提示,则环境正常(192.168.0.1的PLC不存在)。
      
102515046.jpg
     如果是下图提示,则环境异常(snap7库安装不正确)。
      
102515047.jpg

     读写PLC

     环境搭建正常后,在正式建立通信前PLC还需做些配置工作,主要是开发自身的读写权限。具体参照下图配置:
     
102515048.jpg
  
102515049.jpg
  
102515050.jpg
     通过上述配置,PLC可以正常通信了。

     1、python-snap7读写分析

     结合python-snap7的文档API和源码分析,python-sna7重要的两个方法是read_area和write_area,通过这两个方法就能读和写PLC的对应存储地址。
  
  1. def read_area(self, area, dbnumber, start, size):
  2.         """This is the main function to read data from a PLC.
  3.         With it you can read DB, Inputs, Outputs, Merkers, Timers and Counters.
  4.         :param dbnumber: The DB number, only used when area= S7AreaDB
  5.         :param start: offset to start writing
  6.         :param size: number of units to read
  7.         """
  8.         assert area in snap7.snap7types.areas.values()
  9.         wordlen = snap7.snap7types.S7WLByte
  10.         type_ = snap7.snap7types.wordlen_to_ctypes[wordlen]
  11.         logger.debug("reading area: %s dbnumber: %s start: %s: amount %s: "
  12.                       "wordlen: %s" % (area, dbnumber, start, size, wordlen))
  13.         data = (type_ * size)()
  14.         result = self.library.Cli_ReadArea(self.pointer, area, dbnumber, start,
  15.                                size, wordlen, byref(data))
  16.         check_error(result, context="client")
  17.         return bytearray(data)
  18.     @error_wrap
  19.     def write_area(self, area, dbnumber, start, data):
  20.         """This is the main function to write data into a PLC. It's the
  21.         complementary function of Cli_ReadArea(), the parameters and their
  22.         meanings are the same. The only difference is that the data is
  23.         transferred from the buffer pointed by pUsrData into PLC.
  24.         :param dbnumber: The DB number, only used when area= S7AreaDB
  25.         :param start: offset to start writing
  26.         :param data: a bytearray containing the payload
  27.         """
  28.         wordlen = snap7.snap7types.S7WLByte
  29.         type_ = snap7.snap7types.wordlen_to_ctypes[wordlen]
  30.         size = len(data)
  31.         logger.debug("writing area: %s dbnumber: %s start: %s: size %s: "
  32.                       "type: %s" % (area, dbnumber, start, size, type_))
  33.         cdata = (type_ * len(data)).from_buffer_copy(data)
  34.         return self.library.Cli_WriteArea(self.pointer, area, dbnumber, start,
  35.                               size, wordlen, byref(cdata))
    从参数可见,需要提供PLC的区域地址、起始地址、读和写的数据长度。PLC能提供如下信息:
      
102515051.jpg

     2、PLC数据存储和地址

     通过阅读PLC的手册获取到如下信息:
      
102515052.jpg
     PLC的数据存储通过Tag的形式与存储区间关联,分为输入(I)、输出(O)、位存储(M)和数据块(DB)。程序在访问对应(I/O)tag时,是通过访问CPU的Process Image Out对相应地址进行操作的。具体对应关系如下:
      
102515053.jpg
     到这里就能明白python-snap7中定义的areas地址是什么含义了。
  
  1. areas = ADict({
  2.    'PE': 0x81,  #input
  3.    'PA': 0x82,  #output
  4.    'MK': 0x83,  #bit memory
  5.    'DB': 0x84,  #DB
  6.    'CT': 0x1C,  #counters
  7.    'TM': 0x1D,  #Timers
  8. })
    现在离读写PLC还差最后一步,就是起始地址如何确定呢?
      
102515054.jpg
     从上可见对于M3.4,对应的就是M(0x83),起始地址是3,对应bit位是4。
     实战
     经过上面的精心准备,下面就来一波实战。通过读写PLC的M10.1、MW201来具体看看如何读写PLC。
  
  1. import struct
  2. import time
  3. import snap7
  4. def plc_connect(ip, rack=0, slot=1):
  5.     """
  6.     连接初始化
  7.     :param ip:
  8.     :param rack: 通常为0
  9.     :param slot: 根据plc安装,一般为0或1
  10.     :return:
  11.     """
  12.     client = snap7.client.Client()
  13.     client.connect(ip, rack, slot)
  14.     return client
  15. def plc_con_close(client):
  16.     """
  17.     连接关闭
  18.     :param client:
  19.     :return:
  20.     """
  21.     client.disconnect()
  22. def test_mk10_1(client):
  23.     """
  24.     测试M10.1
  25.     :return:
  26.     """
  27.     area = snap7.snap7types.areas.MK
  28.     dbnumber = 0
  29.     amount = 1
  30.     start = 10
  31.     print(u'初始值')
  32.     mk_data = client.read_area(area, dbnumber, start, amount)
  33.     print(struct.unpack('!c', mk_data))
  34.     print(u'置1')
  35.     client.write_area(area, dbnumber, start, b'\x01')
  36.     print(u'当前值')
  37.     mk_cur = client.read_area(area, dbnumber, start, amount)
  38.     print(struct.unpack('!c', mk_cur))
  39. def test_mk_w201(client):
  40.     """
  41.     测试MW201,数据类型为word
  42.     :param client:
  43.     :return:
  44.     """
  45.     area = snap7.snap7types.areas.MK
  46.     dbnumber = 0
  47.     amount = 2
  48.     start = 201
  49.     print(u'初始值')
  50.     mk_data = client.read_area(area, dbnumber, start, amount)
  51.     print(struct.unpack('!h', mk_data))
  52.     print(u'置12')
  53.     client.write_area(area, dbnumber, start, b'\x00\x0C')
  54.     print(u'当前值')
  55.     mk_cur = client.read_area(area, dbnumber, start, amount)
  56.     print(struct.unpack('!h', mk_cur))
  57.     time.sleep(3)
  58.     print(u'置3')
  59.     client.write_area(area, dbnumber, start, b'\x00\x03')
  60.     print(u'当前值')
  61.     mk_cur = client.read_area(area, dbnumber, start, amount)
  62.     print(struct.unpack('!h', mk_cur))
  63. if __name__ == "__main__":
  64.     client_fd = plc_connect('192.168.0.1')
  65.     test_mk10_1(client_fd)
  66.     test_mk10_1(client_fd)
  67.     plc_con_close(client_fd)
    从代码可见,MW201,根据M确定area为MK,根据W确定数据amount为2Btye,根据201确定start为201,读出来的数据根据数据长度用struct进行unpack,写数据对应strcut的pack。
     这里给出PLC变量类型和大小,这样对应确定读写的amount。
      
102515055.jpg
                                         

  本文分享自微信公众号 - chafezhou(gh_5b8f0c59b682)
  原文出处及转载信息见文内详细说明,如有侵权,请联系删除。
  https://cloud.tencent.com/developer/article/1163231