PythonとSiemensPLCでSocket通信しよう

今回やったのはS7-1500のPLCとPythonでSocket通信のプログラムを作ろうと思います。

#Siemens側
Siemens側使ってるのは以下の4つのFBでうございます。

- FB65 "TCON"
    - 接続を確立させるの関数です。（Socket.connect()のような）
- FB66 "TDISCON" for ending the connection
    - 接続を切るの関数です。(Socket.close()のような)
- FB63 "TSEND" for sending data
    - データを送信する関数です。（Socket.send()のような）
- FB64 "TRECV" for receiving data
    - データを受信する関数です。（Socket.recv()のような）

関数の名前がわかった時点、Socket通信するためにはIP、PORTなど設定する必要がありますが、Siemnes側にはTCON_Paramという構造体が用意しています。もちろんDefault値のままのところもあり、ちゃんと設定する必要場合もあります。ここで簡単に説明します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/b72a66e5-1b93-fde4-ca0d-60c7abc3fbbc.png)


| 関数名|データタイプ| Byteアドレス           | Default|役目|
| :-------------|:-------------:| -----:| -----:|:------------------------|
| Block_LENGTH      | UInt| 0-1 | 64|この構造体のメモリ長さ、64Byteがあるから64のままでOKです。|
| ID  | CONN_OUC|   2-3| 0|接続の番号、Siemensは各接続はこの番号に管理されています。|
| CONNECTION_TYPE| USINT|    4| 17|17=TCP/IP,18:ISO ON TCP/IP,19=UDP|
| ACTIVE_EST| BOOL      |    5| False|True=Active Connection、False=Passive Conncetion:|
| LOCAL_DEVICE_ID| USINT      |    6 |1| PLCのPN Interface ID|
| LOCAL_TSAP_ID_LEN| USINT|    7 | 0|LOCAL_TSAP_IDの長さ、0＝TCP、2－16＝ISO-TCP、2＝UDP|
| REM_SUBNET_ID_LEN| USINT|    8 | 0|使わない|
| REM_STADDR_LEN| USINT      |    9 | 4|相手のアドレス長さ、0＝未定義、4＝REM_STADDRが有効IP(TCP-ISO)|
| REM_TSAP_ID_LEN| USINT      |    10 | 2|REM_TSAP_IDの長さ、0、２＝TCP、2－16＝TCP-ISO、0(UDP)|
| NEXT_STADDR_LEN| BYTE      |    11 | 0|使わない|
| LOCAL_TSAP_ID| ARRAY[1..16]OF BYTE      |    12-27 | 0|PLCのPort番号|
| REM_SUBNET_ID| ARRAY[1..6]OF USINT      |    28-33 | 0|使わない|
| REM_STADDR| ARRAY[1..6]OF USINT      |    34-39 | 0|相手のIP|
| REM_TSAP_ID| ARRAY[1..16]OF BYTE      |    40-55 | 0|相手のPort番号|
| NEXT_STADDR| ARRAY[1..6]OF BYTE      |    56-61 | 0|使わない|
| SPARE| WORD      |    62-63 | 16#0|使わない|

##プログラム
関数の説明はそこまでで、これから実際実装しましょう。
こちらは今回のTIAバージョンです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/3c7ccc9d-0610-1090-8f3d-cafd7c1d1186.png)

###TCON_Param
まずDB作り、TypeをTCON_Paramにします。
もちろん普通のGlobal DB作って、TCON_Paramを宣言してもよいです。これは好みです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/50f63453-f36e-72d5-b84e-4a33b5d69168.png)

###dbRecvData
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/219e291e-806d-83e3-356b-0ef66ffdddb5.png)

###dbSendData
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/08dcc053-d625-2e85-816d-a2467a12f69d.png)

###OB1
次はOB1にこのようなプログラムを作ります。

####TCON
こちらは相手と接続する関数を呼び出します。
M1.1がONすると、接続し、COMMIDがそのConnection IDを設定します。(ここは仮に1にする)
もしエラーが出た場合はそのStatusを保存します。
そして接続したらDONEがONになりますので、そのときStepを1に入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/426ab0c6-b8b6-1665-2702-4d2a9de30139.png)

こちらはTCONの設定です。CONNECTのところは先作ったTCON_ParamのDBに入れています。つまりここでなにか変更したら直接そのDBに反映することになります。
自分のIPが192.168.0.1、Connection IDは1、自分のPortが2000、相手の接続待ち。
相手のIPが192.168.0.229、Portは2001です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/819c96a5-5559-67eb-e656-21a9b2d2e98b.png)


####TRCV
こちらはデータ受信の関数です。M1.1が立ち上げするとDATAの引数が相手からもらったデータをここに格納します。
もし受信OKならその受信したデータの長さを保存します。
もしエラーならStatusを保存します。
そしてデータ受信問題なければStepを10にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/63c7f626-b260-2cc0-830e-ae976970aa9a.png)

####TSEND
こちらはデータ送信する関数です。M1.0が立ち上げるとDATAの引数を相手に送ります。
もしエラーならStatusを保存します。
最後、送信OKならStepを1に戻ります。また受信を待ちます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/9fc1675f-7043-16fd-7653-cfbd40e0a591.png)

####TDISCON
こちらはConnectionを切断する関数です。まぁ、Connectionを切断です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/b42cabd3-d78e-cc41-b488-8b51cdfcf45f.png)

#Python側
こっちはなにも特別なライブラリー使ってません。使用するのはSocketです。

```Python
import socket
DESTINATION_ADDR = '192.168.0.1'
SOURCE_PORT, DESTINATION_PORT = 2001, 2000
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

sock.bind(('192.168.0.229', SOURCE_PORT))
sock.connect((DESTINATION_ADDR, DESTINATION_PORT))
sock.send(b'\x11\x00\x19\x29\x30\x30\x30\x30\x21\x28')
data = sock.recv(1024)
print(repr(data))
sock.close()

```

#おまけ
先のOB1のBlockは全部グロープアドレス使っていますが、もしSocket通信の間が何個もあれば少し不便ですね。
ここでFucntion Block化します。

```SCL:fbSocketComm_Passive

Function_Block "fbSocketComm_Passive"

Var_Input
  ID : CONN_OUC;
  Icon : Bool;
  iRetry : Int:=3;
  iReset : Bool;
  iDisConnect : Bool;
End_Var

Var_Output
  tConERRORSts : Word;
  tRecvDNR : UDInt;
  tRecvERRORSts : Word;
  tSendERRORSts : Word;
  tConError : Bool;
  tSendError : Bool;
  tRecvError : Bool;
  tDisconError : Bool;
End_Var

Var_In_out
  CONNECT : TCON_Param;
  Send : Variant;
  Recv : Variant;
End_Var

Var
  tcon {S7_SetPoint := 'True'} : TCON;
  trcv {S7_SetPoint := 'True'} : TRCV;
  tsend {S7_SetPoint := 'True'} : TSEND;
  tdiscon {S7_SetPoint := 'True'} : TDISCON;
  R_TRIG1 {S7_SetPoint := 'True'} : R_TRIG;
  Step : Int;
  cmd : Array[0..999] Of Bool;
  ton : Array[0..5] Of TON_TIME;
  Retry: Array[0..3] Of Int;
  con : Bool;
  i : Int;
End_Var

Var_Temp

End_Var

Var Constant

End_Var


//Init
#trcv.ID := #tsend.ID := #tdiscon.ID := #tcon.ID := #ID;

//Start
#R_TRIG1(
         CLK:=#Icon,
         Q=>#con);

//Value init
If #con And #Step = 0 Then
    #Step := 10;
    For #i := 0 To 3 Do
        
        #Retry[#i] := 0;
            
    End_For;
    
End_If;

//Reset
If #iReset Then
    #tConError := #tRecvError := #tSendError := False;
    #tConERRORSts := #tRecvERRORSts := #tSendERRORSts := 0;
End_If;

Case #Step Of
        
    //Tcon
    10:
        If #tcon.DONE Then
            #Step := 100;
        End_If;
        If #tcon.ERROR  Then
            #Retry[0] += 1;
            If #Retry[0] > #iRetry Then
                #Step := 990;
            Else
                #Step := 900;
            End_If;
        End_If;
        
    //TRecv
    100:
        If #trcv.NDR Then
            #tRecvDNR := #trcv.RCVD_LEN;
            #Step := 200;
        End_If;
        If #trcv.ERROR Then
            #Step := 991;
            #tRecvERRORSts := #trcv.STATUS;
        End_If;
        
    //TSend
    200:
        If #tsend.DONE Then
            #Step := 100;
        End_If;
        If #tsend.ERROR Then
            #tSendERRORSts := #tsend.STATUS;
            #Step := 100;
        End_If;
    //Error
    900:
        #Step := 10;
    990:
        #tConError := True;
    991:
        #tRecvError := True;
    992:
        #tSendError := True;
        
       
End_Case;

For #i := 0 To 999 Do
    #cmd[#i] := False;
End_For;

#cmd[#Step] := True;

//TCON
#tcon.REQ := #cmd[10];
#tcon(CONNECT := #CONNECT);

//TRCV
#trcv.EN_R := #cmd[100];
#trcv(DATA     := #Recv);

//TSEND
#tsend.REQ := #cmd[200];
#tsend(DATA   := #Send);

//TDISCON
#tdiscon.REQ := #iDisConnect;
#tdiscon();

```
最後はOB1でCallすればOKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154661/f6e30ff0-9e6a-3988-c4e0-64467f50e22b.png)

お疲れ様です。