Python 使用Snap7讀寫西門子S7系列PLC

一杯清酒邀明月發表於2024-04-22

1.簡介
Snap7

  • Snap7是一個基於s7通訊協議的開源軟體包,作者是Davide Nardella,該軟體包封裝了S7通訊的底層協議,可使用普通電腦透過程式設計與西門子S7系列PLC進行通訊
  • Snap7三大物件元件:客戶端,伺服器,合作者。下面是三者關係,更詳細介紹可看官網。本篇主要講述的是Client模式,我們的pc機作為客戶端,plc作為伺服器。

  • Snap7官網地址:http://snap7.sourceforge.net/
  • Snap7包支援西門子S7-200 SMART,S7-300/400系列,S7-1200/1500系列、另外LOGO! 0BA7/0BA8 PLC、 SINAMICS驅動器也有較好的支援
  • Snap7特點:
  1. 基於乙太網,網線連線
  2. 跨平臺,支援 Windows、 Linux、Mac等主流作業系統
  3. Windows系統包括目前主流的Win7/8/10 的32位或64位
  4. Linux系統包括: CentOs、 Debian、 RedHat、 Ubuntu等32位或64位系統
  5. 提供多種語言的封裝包:C#、VB、C/C++、 Python、java、 Delphi、 LabView等主流程式語言
  6. 支援樹莓派、 ARDUINO等嵌入式平臺
  • python包原始碼地址:https://github.com/gijzelaerr/python-snap7

2.環境安裝
Window

  • pip install python-snap7(版本一直未更新)
  • github原始碼包下載:https://github.com/gijzelaerr/python-snap7(這裡會一直更新)
  • 把snap7.dll和snap7.lib(注意區分32位和64位),設定到環境變數能找到的地址就行

Linux

  • 第一種方式

  sudo apt-get install python-pip3

  sudo pip3 install python-snap7

  • 第二種方式

透過以下命令下載snap7:

  git clone https://github.com/lizengjie/snap7-debian.git

編譯:(arm_v7_linux不行就arm_v6_linux)

  cd snap7-debian/build/unix && sudo make -f arm_v7_linux.mk all

複製:

  sudo cp …/bin/arm_v7-linux/libsnap7.so /usr/lib/libsnap7.so
  sudo cp …/bin/arm_v7-linux/libsnap7.so /usr/local/lib/libsnap7.so

sudo ldconfig

不同環境的複製檔案我稍後放資源管理處

3.連線西門子plc

方法介紹

​   在Client類裡提供了主要5種設定連線plc函式

  • connect(ip, rack, slot)

  這是連線plc的唯一方法,引數ip是要連線的plcip地址,rack是機架號,slot卡槽號,不同的plc對應不同的機架和卡槽看下圖對應

plcrackslot
s7-200smart 0 1
s7-300 0 2
s7-400/WIN AC 見硬體組態 見硬體組態
s7-1200/1500 0 0/1
  • set_connection_params(ip, local_tsap, remote_tsap)

  這是設定遠端本地TSAP和遠端TSAP的函式,三個引數ip是plcIP地址,本地TSAP和遠端TSAP是相對應的int型別,函式一般用於連線logo系列時使用,呼叫順序在connect()之前呼叫

  • set_connection_type(connection_type)

  這是用來設定連線屬性的函式,connection_type引數範圍如圖:

connection_type
PG 1
OP 2
S7-Basic 3-10

  此函式不是一定要呼叫,如果呼叫一定要在connect()函式之前呼叫設定,比如連線s7-200SMART就一定要呼叫此函式,引數一般設為3

  • disconnect() 和 destroy()

  這是斷開客戶端連線和銷燬客戶端連線,無需引數

  最後注意如果你set_connection_params()和set_connection_type()都要呼叫,一定要set_connection_type()在前set_connection_params()在後,因為順序相反會對遠端TASP造成影響。所以整體的順序應該是

1 set_connection_type(connection_type) #選用
2 set_connection_params(ip, local_tsap, remote_tsap)  #選用
3 connect(ip, rack, slot)
4 disconnect()
5 destroy()

連線plc

第一步首先例項化一個Client物件

1 from snap7 import client
2 
3 my_plc = client.Client()

第二步呼叫connect()的方法

 1 from snap7 import client
 2 
 3 my_plc = client.Client()
 4 
 5 # my_plc.set_connection_type(3)  如果連線的是s7-200smart系列plc
 6 # set_connection_params(ip, local_tsap, remote_tsap)  如果連線的是logo!系列plc
 7 
 8 my_plc.connect(ip, rack, slot)
 9 # ip是plcIP,rack是機架號,slot卡槽號,不同的plc對應不同的機架和卡槽看上邊表格
10 
11 print(my_plc.get_connected())
12 # 判斷連線成功可呼叫get_connected():返回True就是成功,不成功直接報錯。

第三步斷開連線

1 from snap7 import client
2 my_plc = client.Client()
3 my_plc.connect(ip, rack, slot)
4 my_plc.disconnect()
5 my_plc.destroy() # 不用了一定要斷開銷燬客戶端

程式碼示例

 1 from snap7 import client
 2 
 3 def connect_logo(ip: str, local_tsap: int, remote_tsap: int, rack: int, slot: int):
 4     """
 5         連線logo系列
 6     :param ip: PLC/裝置IPV4地址
 7     :param local_tsap: 本地tsap(PC tsap)
 8     :param remote_tsap: 遠端tsap(PLC tsap)
 9     :param rack: 伺服器上的機架
10     :param slot: 伺服器上的插槽
11     """
12     # 初始化一個客戶端
13     my_plc = client.Client()
14     # 設定內部(IP、LocalTSAP、RemoteTSAP)座標。必須在connect()之前呼叫此函式
15     my_plc.set_connection_params(ip, local_tsap, remote_tsap)
16     # 連線到S7伺服器
17     my_plc.connect(ip, rack, slot)
18     return my_plc
19 
20 
21 def connect_200smart(ip: str, plc_model=3, rack=0, slot=1):
22     """
23         連線s7-200smart系列
24     :param ip: PLC/裝置IPV4地址
25     :param plc_model: 連線型別:1用於PG,2用於OP,3至10用於S7基本
26     :param rack: 伺服器上的機架
27     :param slot: 伺服器上的插槽
28     """
29     # 初始化一個客戶端
30     my_plc = client.Client()
31     # 設定連線資源型別,即客戶端,連線到PLC
32     my_plc.set_connection_type(plc_model)
33     # 連線到S7伺服器
34     my_plc.connect(ip, rack, slot)
35     return my_plc
36 
37 
38 def connect_plc(ip: str, rack: int, slot: int):
39     """
40         連線s7-1200/1500系列
41     :param ip: PLC/裝置IPV4地址
42     :param rack: 伺服器上的機架
43     :param slot: 伺服器上的插槽
44     """
45     my_plc = client.Client()
46     my_plc.connect(ip, rack, slot)
47     return my_plc

4.讀plc
方法介紹及示例
在Client類裡提供了主要兩種讀plc的函式

  • read_area(area, dbnumber, start, size)

  這是讀plc最最最重要的方法,功能強大,支援(I,Q,M,DB,V,CT,TM)多儲存區讀取資料

​   area:區地址型別(十六進位制型別),如下圖對應

​   dbnumber:地址編號(int),只適用於DB區和200samart的V區,其它區全預設0,V區只能填1

​   start:要讀取資料的位元組起始地址(int)

​   size:要讀取的資料型別所佔位元組長度大小(int),如下字典對應

IQMDB/VCTTM
0x81 0x82 0x83 0x84 0x1C 0x1D
 1 # 不同型別所佔位元組大小
 2 TypeSize = {
 3     'int': 2,  # 有符號(-32768~32767)
 4     'bool': 1,  # bool值
 5     'dint': 4,  # 有符號 (-2147483648~2147483647)
 6     'word': 2,  # 無符號(0~65536)
 7     'real': 4,  # 有符號 float型別(這範圍記不住了)
 8     'dword': 4,  # 無符號(0~4294967295)
 9     'char': 1,  # CHAR,ASCII字符集,佔用1個位元組記憶體,主要針對歐美國家(字元比較少)
10     'string': 255,  # STRING,佔用256個位元組記憶體,ASCII字串,由ASCII字元組成
11     's5time': 2,
12     'wchar': 2,  # WCHAR,Unicode字符集,佔用2個位元組記憶體,主要針對亞洲國家(字元比較多)
13     'wstring': 512,  # WSTRING,預設佔用512個位元組記憶體(可變),Unicode字串,由Unicode字元構成
14     'dt': 4,  # DateTime 日期
15     'usint': 1,  # 0~255
16     'sint': 1,  # -128~127
17     'uint': 2,  # 0~4294967295
18     'udint': 4,  # 0~4294967295
19     'lreal': 8,
20     'time': 4,
21     'd': 2,
22     'tod': 4,  # TOD (TIME_OF_DAY)資料作為無符號雙整數值儲存,被解釋為自指定日期的凌晨算起的毫秒數(凌晨 = 0ms)。必須指定小時(24 小時/天)、分鐘和秒。可以選擇指定小數秒格式。
23     'dtl': 12,  # DTL(日期和時間長型)資料型別使用 12 個位元組的結構儲存日期和時間資訊。可以在塊的臨時儲存器或者 DB 中定義 DTL 資料。
24     'date': 2,  # Date(16位日期值)、
25     'ltod': 8
26 }

  return:函式最後返回的是一個位元組陣列,到這裡大家不用自己用struct包去解,作者在uitl檔案裡為大家封裝了取不同型別變數值的函式,下面我主要介紹兩種,

  • bool:get_bool(_bytearray, byte_index, bool_index)

​   _bytearray:位元組陣列,就是你上面讀到的位元組陣列

​   byte_index:位元組索引,這裡填0就可以,後面我會詳細介紹byte_index和上面read_area()的引數start,size三者的關係,以及靈活應用

​   bool_index: bool值索引,其實就是位(bit)索引(0~7),因為1byte=8bit

  • real:get_real(_bytearray, byte_index)

​   引數同上,大家可自己看原始碼,目前除了bool和string型別,其它都只要兩個引數_bytearray和bool_index,有一些型別作者還沒寫,大家有用到可以自己解。

 1 """
 2         簡單示例#1
 3     plc:    s7-1200
 4     變數地址:DB1.DBD36 (1是地址編號,36是起始值)
 5     型別:   real(float)
 6 """ 
 7 from snap7 import util, client
 8 from snap7.snap7types import S7AreaDB
 9 
10 my_plc = client.Client()  # 例項化客戶端
11 my_plc.connect('192.168.2.1', 0, 0)  # 連線s7-1200
12 byte_arrays = my_plc.read_area(S7AreaDB, 1, 36, 4)  # 讀出變數的位元組陣列
13 value = util.get_real(byte_arrays, 0)  # 透過資料型別取值
14 my_plc.disconnect()  # 斷開連線
15 my_plc.destroy()  # 銷燬
16 print(value)
17 
18 --------------------------------------------------------------------------------------
19 
20 """
21         簡單示例#2
22     plc:    s7-200SMART
23     變數地址:M1.0 (1是起始值,0是bool索引)
24     型別:   bool
25 """ 
26 from snap7 import util, client
27 from snap7.snap7types import S7AreaMK
28 
29 my_plc = client.Client()  # 例項化客戶端
30 my_plc.set_connection_type(3)  # 設定連線資源型別
31 my_plc.connect('192.168.2.2', 0, 1)  # 連線s7-200SMART
32 byte_arrays = my_plc.read_area(S7AreaMK, 0, 1, 1)  # 讀出變數的位元組陣列
33 value = util.get_bool(byte_arrays, 0, 0)  # 透過資料型別取值
34 my_plc.disconnect()  # 斷開連線
35 my_plc.destroy()  # 銷燬
36 print(value)

這裡介紹一下start,size和byte_index之間關係以及如何應用,方便理解個人整理以下如圖

注意多個變數的適用條件必須為同一地址(area),同一地址編號(dbnumber)。所以透過read_area函式可以一次讀取同一地址編號上的所有變數

 1 """
 2         示例
 3     plc:    s7-1200
 4     變數地址:[DB4.DBX0.1, DB4.DBD36, DB4.DBW2 .....]
 5     型別:   [bool, float, word ......]
 6 """ 
 7 from snap7 import util, client
 8 from snap7.snap7types import S7AreaDB
 9 
10 my_plc = client.Client()  
11 my_plc.connect('192.168.2.1', 0, 0)  
12 
13 byte_arrays = my_plc.read_area(S7AreaDB, 4, 0, 40)
14 # 這是所有db塊,地址編號4的變數,套用圖上公公式,最小的起始值是0,size是最大起始值加它型別所佔的位元組數就是36+float型別所佔4個byte長度,所以size是40
15 
16 value1 = util.get_bool(byte_arrays, 0, 1) 
17 # DB4.DBX0.1是bool型別,byte_index = 起始值是0 - 最小的起始值0 = 0
18 
19 value2 = util.get_real(byte_arrays, 36)  
20 # DB4.DBD36是float型別,byte_index = 起始值是36 - 最小的起始值0 = 36
21 
22 value3 = util.get_word(byte_arrays, 2) 
23 # DB4.DBW2是word型別,byte_index = 起始值是2 - 最小的起始值0 = 2
24 
25 my_plc.disconnect() 
26 my_plc.destroy()  
27 print(value1, value2, value3)
  • read_multi_vars(items)

  這是可以一次讀取<=19個不同地址型別的變數,由於pdu大小限制一次性讀取不能超過19個變數

  items引數是一個由S7DataItem例項物件組成的列表

  下面我用作者寫的一個示例給大家介紹一下

 1 import ctypes
 2 import snap7
 3 from snap7.common import check_error
 4 from snap7.types import S7DataItem, S7AreaDB, S7WLByte
 5 
 6 client = snap7.client.Client()
 7 client.connect('10.100.5.2', 0, 2)
 8 
 9 data_items = (S7DataItem * 3)()  # 注意就是這裡數字不能大於19
10 
11 data_items[0].Area = ctypes.c_int32(S7AreaDB)  # 地址型別
12 data_items[0].WordLen = ctypes.c_int32(S7WLByte)  # 這裡的WordLen除了讀TM和CT地址時其它地址統一用位元組(S7WLByte)。不要用S7WLBit,用位去讀需要換算,不嫌麻煩你可以試試
13 data_items[0].Result = ctypes.c_int32(0)  # result用不到寫0就可以
14 data_items[0].DBNumber = ctypes.c_int32(200)  # 地址編號
15 data_items[0].Start = ctypes.c_int32(16)  # 變數起始位元組地址
16 data_items[0].Amount = ctypes.c_int32(4)  # 位元組長度
17 
18 data_items[1].Area = ctypes.c_int32(S7AreaDB)
19 data_items[1].WordLen = ctypes.c_int32(S7WLByte)
20 data_items[1].Result = ctypes.c_int32(0)
21 data_items[1].DBNumber = ctypes.c_int32(200)
22 data_items[1].Start = ctypes.c_int32(12)
23 data_items[1].Amount = ctypes.c_int32(4)  # reading a REAL, 4 bytes
24 
25 data_items[2].Area = ctypes.c_int32(S7AreaDB)
26 data_items[2].WordLen = ctypes.c_int32(S7WLByte)
27 data_items[2].Result = ctypes.c_int32(0)
28 data_items[2].DBNumber = ctypes.c_int32(200)
29 data_items[2].Start = ctypes.c_int32(2)
30 data_items[2].Amount = ctypes.c_int32(2)  # reading an INT, 2 bytes
31 
32 # create buffers to receive the data
33 # use the Amount attribute on each item to size the buffer
34 for di in data_items:
35     # create the buffer
36     buffer = ctypes.create_string_buffer(di.Amount)
37 
38     # cast the pointer to the buffer to the required type
39     pBuffer = ctypes.cast(ctypes.pointer(buffer), ctypes.POINTER(ctypes.c_uint8))
40     di.pData = pBuffer
41 
42 for di in data_items:
43     check_error(di.Result)
44 
45 result, data_items = client.read_multi_vars(data_items)
46 
47 result_values = []
48 # function to cast bytes to match data_types[] above
49 byte_to_value = [snap7.util.get_real, snap7.util.get_real, snap7.util.get_int]
50 
51 # unpack and test the result of each read
52 for i in range(0, len(data_items)):
53     btv = byte_to_value[i]
54     di = data_items[i]
55     value = btv(di.pData, 0)
56     result_values.append(value)
57 print(result_values)
58 
59 client.disconnect()
60 client.destroy()

5.寫plc

方法介紹及示例

對變數賦值同樣也介紹兩種方法

  • write_area(area, dbnumber, start, data)

要想對變數賦值,必須先讀取變數陣列,然後在把要寫入的值設定到快取,最後在寫到plc。

三步順序:

​ 第一步:byte_arrays = read_area() 或 byte_arrays = bytearray(變數型別所佔位元組大小)

​   這個不介紹了不懂看上面讀plc方法介紹

​ 第二步:在snap7.util裡作者同樣封裝了不同型別變數更改位元組陣列的方法,這裡拿bool型別描述一下,因為 其他型別引數基本都一樣,大家可看原始碼

  • set_bool(_bytearray, byte_index, bool_index, value)

​   _bytearray:位元組陣列

​   byte_index:位元組索引

​   bool_index:位索引

​   value:要寫入的值(注意必須與要賦值的變數型別一致)

​ 第三步:在透過write_area()函式把值寫進plc

  • ​ write_area(area, dbnumber, start, data)

​   area:地址型別

​   dbnumber: 地址編號

​   start:位元組起始值

​   data: 位元組陣列(就是你第一步讀出來的位元組陣列)

 1 """
 2         簡單示例#1
 3     plc:    s7-200SMART
 4     變數地址:M1.0 (1是起始值,0是bool索引)
 5     型別:   bool
 6 """
 7 from snap7 import util, client
 8 from snap7.snap7types import S7AreaMK
 9 
10 my_plc = client.Client()
11 my_plc.set_connection_type(3)
12 my_plc.connect('192.168.2.101', 0, 1)
13 byte_arrays = my_plc.read_area(S7AreaMK, 0, 1, 1)
14 print('賦值前', util.get_bool(byte_arrays, 0, 0))
15 util.set_bool(byte_arrays, 0, 0, 1)
16 my_plc.write_area(S7AreaMK, 0, 1, byte_arrays)
17 print('賦值後', util.get_bool(byte_arrays, 0, 0))
18 my_plc.disconnect()
19 my_plc.destroy()
20 
21 -----------------------------------------------------------------------------------------
22 
23 """
24         簡單示例#2
25     plc:    s7-1200
26     變數地址:Q1.2 (1是起始值,2是bool索引)
27     型別:   bool
28 """ 
29 from snap7 import util, client
30 from snap7.snap7types import S7AreaPA
31 
32 my_plc = client.Client()  
33 my_plc.connect('192.168.2.1', 0, 1)  
34 byte_arrays = my_plc.read_area(S7AreaPA, 0, 1, 1)  
35 print('賦值前', util.get_bool(byte_arrays, 0, 2))
36 util.set_bool(byte_arrays, 0, 2, 1)  
37 my_plc.write_area(S7AreaPA, 0, 1, byte_arrays)
38 print('賦值後', util.get_bool(byte_arrays, 0, 2))
39 my_plc.disconnect()  
40 my_plc.destroy()  

同讀的思維一樣,我們這裡也可以一次為同一地址,同一地址編號所有變數賦值

 1 """
 2        示例
 3    plc:    s7-200SMART
 4    變數地址:V100.0  VD104
 5    型別:   bool    real
 6 """
 7 from snap7 import util, client
 8 from snap7.snap7types import S7AreaDB
 9 
10 my_plc = client.Client()
11 my_plc.set_connection_type(3)
12 my_plc.connect('192.168.2.101', 0, 1)
13 byte_arrays = my_plc.read_area(S7AreaDB, 1, 0, 108)
14 print('賦值前', util.get_bool(byte_arrays, 100, 0), '賦值前', util.get_real(byte_arrays, 104))
15 util.set_bool(byte_arrays, 100, 0, 1)
16 util.set_real(byte_arrays, 104, 999.99)
17 my_plc.write_area(S7AreaDB, 1, 0, byte_arrays)
18 print('賦值後', util.get_bool(byte_arrays, 100, 0), '賦值後', util.get_real(byte_arrays, 104))
19 my_plc.disconnect()
20 my_plc.destroy()
  • write_multi_vars(items)

這同樣也是一個可以一次為多個不同地址變數賦值的函式(同樣不能大於19個)

還是用作者的例子(引數用法大同小異)

 1 import ctypes
 2 import snap7
 3 from snap7.types import S7WLByte, S7DataItem, S7WLWord, S7WLReal, S7WLTimer
 4 from snap7.types import areas, wordlen_to_ctypes
 5 from snap7.util import set_int, set_real, set_word, get_int, get_real, get_s5time
 6 
 7 
 8 client = snap7.client.Client()
 9 client.connect('192.168.100.100', 0, 2)
10 
11 items = []
12 
13 
14 def set_data_item(area, word_len, db_number: int, start: int, amount: int, data: bytearray) -> S7DataItem:
15     item = S7DataItem()
16     item.Area = ctypes.c_int32(area)
17     item.WordLen = ctypes.c_int32(word_len)
18     item.DBNumber = ctypes.c_int32(db_number)
19     item.Start = ctypes.c_int32(start)
20     item.Amount = ctypes.c_int32(amount)
21     array_class = ctypes.c_uint8 * len(data)
22     cdata = array_class.from_buffer_copy(data)
23     item.pData = ctypes.cast(cdata, ctypes.POINTER(array_class)).contents
24     return item
25 
26 
27 int_values = [10, 20, 30, 40]
28 ints = bytearray(len(int_values) * 2)
29 for i, value in enumerate(int_values):
30     set_int(ints, i * 2, value)
31 
32 real = bytearray(4)
33 set_real(real, 0, 42.5)
34 
35 counters = 0x2999.to_bytes(2, 'big') + 0x1111.to_bytes(2, 'big')
36 
37 item1 = set_data_item(area=areas.DB, word_len=S7WLWord, db_number=1, start=0, amount=4, data=ints)
38 item2 = set_data_item(area=areas.DB, word_len=S7WLReal, db_number=1, start=8, amount=1, data=real)
39 item3 = set_data_item(area=areas.TM, word_len=S7WLTimer, db_number=0, start=2, amount=2, data=counters)
40 
41 items.append(item1)
42 items.append(item2)
43 items.append(item3)
44 
45 client.write_multi_vars(items)
46 
47 db_int = client.db_read(1, 0, 8)
48 db_real = client.db_read(1, 8, 12)
49 db_counters = client.ct_read(2, 2)
50 
51 print(f'int values: {[get_int(db_int, i * 2) for i in range(4)]}')
52 print(f'real value: {get_real(db_real, 0)}')
53 print(f'counters: {get_s5time(counters, 0)}, {get_s5time(counters, 2)}')

6.總結

不總了,有問題大家隨時交流

相關文章