ラズパイ・MCプロトコルで工場のPLCをIoT化させる

# 概要

MELSEC_MCプロトコルを使用してPLC（Programmable Logic Controller）と通信するためのPythonコードを説明してます。

## MCプロトコルとは？？

**MCプロトコル（Melsec Communication Protocol）** は、三菱電機が開発したPLC（Programmable Logic Controller）と周辺機器を接続するための通信プロトコルです。三菱電機のPLC（キーエンス製やオムロン製も？）が搭載されている多くの機器で使用されており、データの送受信やプログラムの転送などに利用されます。

こちらで詳しく解説されています。

[MCプロトコルのチートシート - Qiita](https://qiita.com/Tatsuya-8888/items/5c09f6004d0db59322f1)

## なぜラズパイとつなげるか？

ラズベリーパイをMCプロトコルに対応したインタフェースボードを介してPLCに接続することで、ラズベリーパイからPLCに信号を送信し、PLCに接続された周辺機器を制御することができます。
また、AWSなどのクラウド上に設置されたアプリケーションからラズベリーパイを経由してPLCにデータを送信することもできます。

つまり、

- 遠隔でPLCのデータを収集する
- 遠隔でPLCにデータ転送を行う
- 遠隔でPLCのラダー回路をON/OFFして、システム制御を行う

といったことができます。

## 動作環境

Pythonのバージョン3.6以降

## 事前準備

PLCを操作するのでPLC、ラズパイ、2つをつなげる有線LANケーブル。2つをつなげます。

また、PLCの方にIPアドレス等の設定が必要です。詳しくはこちらで解説されています。

[PLCとRaspberry Piはとっても相性がいい。(通信の行い方) - Qiita](https://qiita.com/Tatsuya-8888/items/28cc12dd637c0866fd1c)

## 実行手順

melsec.jsonファイルを作成し、必要な情報を定義します。
Pythonファイルを実行します。引数に"r"または"w"を指定してください。

## ソースコード
こちらにもおいています。
https://github.com/HirotoSakaki/plc-communication-python

```python
# MELSEC_MCプロトコルを使用してPLC（Programmable Logic Controller）と通信するためのPythonコード

import json
import socket
import sys

# PLCとのソケット通信を行うためのクラス
class MySocket:
    def __init__(self, sock=None):
        print("init")
        if sock is None:
            self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        else:
            self.sock = sock

    def connect(self, host, port):
        print("connect")
        try:
            self.sock.connect((host, port))
            return 0
        except socket.error:
            print("コネクトエラーです")
            return -1

    def close(self):
        print("CLOSE")

        try:
            self.sock.close()
            return 0
        except socket.error:
            print("クロ-ズエラーです")
            return -1

    def mysend(self, msg):
        print("mysend")
        try:
            self.sock.send(msg.encode("utf-8"))
        except Exception:
            return -1

    def myreceive(self):
        print("myreceive")
        data = []
        try:
            data = self.sock.recv(1024)
            return data
        except Exception:
            return -1

# 送信するコマンドを作成するためのクラス
class MakeCommand:
    def __init__(self) -> None:
        json_open = open("melsec.json", "r")
        self.loading = json.load(json_open)

    def socket(self):
        # PLCのIPアドレスとポート番号を取得
        return [val for val in self.loading["socket_settings"].values()]

    def common(self):
        # 読みと書きで共通コマンド部分
        return [val for val in self.loading["common"].values()]

    def read(self):
        # 共通コマンドstrと読み込みコマンドstrを結合させ、一個のコマンドを作成
        cmd = self.common() + [val for val in self.loading["read"].values()]
        print(cmd)
        return "".join(cmd)

    def write(self):
        # 共通コマンドstrと書き込みコマンドstrを結合させ、一個のコマンドを作成
        cmd = self.common() + [val for val in self.loading["write"].values()]
        print(cmd)
        return "".join(cmd)

def main(args):
    mysocket = MySocket()
    mk_cmd = MakeCommand()
    socket = mk_cmd.socket()

    if mysocket.connect(socket[0], int(socket[1])) == 0:  # PLCと通信成功したら実行
        if args == "r":  # 一括読み込みの場合
            print("read")
            read_cmd = mk_cmd.read()
            mysocket.mysend(read_cmd)  # 読み込みコマンド送信

        elif args == "w":  # 一括書き込みの場合
            print("write")
            write_cmd = mk_cmd.write()
            mysocket.mysend(write_cmd)  # 書き込みコマンド送信

        ret = mysocket.myreceive().decode("utf-8")  # 応答伝文
        new_ret = [
            ret[0:4],  # サブヘッダ
            ret[4:6],  # ネットワーク番号
            ret[6:8],  # PC番号
            ret[8:12],  # 要求先ユニットI/O番号
            ret[12:14],  # 要求先ユニット局番号
            ret[14:18],  # 応答データ長
            ret[18:22],  # 終了コード
            ret[22:],  # 読み込みのときは読み出しデータ（のはずだが・・・）
        ]
        print(new_ret)

    else:
        print("Conection Error")
        sys.exit()

    mysocket.close()

if __name__ == "__main__":
    if len(sys.argv) > 1:
        main(sys.argv[1])
    else:
        print("Please specify mode (r or w)")
```

やっていることはだいたいコードにコメントしています。が、大まかのまとめると以下のとおりです。

MySocket: PLCとのソケット通信を行うためのクラス
MakeCommand: 送信するコマンドを作成するためのクラス
main: メイン関数

やっていることは以下の通りです。

1. melsec.jsonファイルから必要な情報を読み込む
2. PLCとの通信に必要なオブジェクトを作成する
3. 読み込み/書き込みするデバイスのアドレスを指定する
4. PLCと通信してデータを読み込む/書き込む
5. 通信が完了したらオブジェクトをクローズする

## 設定JSONファイル

```json
{
    "socket_settings":{
    "IP":"192.168.2.39",
    "port":"5015"
    },
    "common":{
        "subheader":"5000",
        "net":"00",
        "pc":"FF",
        "unitio":"03FF",
        "unitno":"00"
    },
    "read":{
        "nlen":"0018",
        "cputimer":"0010",
        "command":"0401",
        "subcommand":"0000",
        "device":"D*000000",
        "number":"0001"
    },
    "write":{
        "nlen":"001C",
        "cputimer":"0010",
        "command":"1401",
        "subcommand":"0000",
        "device":"D*000000",
        "number":"0001",
        "devicedata":"00FF"
    }
}
```

このJSONファイルは、Mitsubishi Electric社のプログラマブルロジックコントローラー (PLC) で使用されるMELSEC通信プロトコルに関連する情報を定義しています。

"socket_settings"オブジェクトには、PLCに接続するためのIPアドレスとポート番号が含まれています。

"common"オブジェクトには、通信に必要ないくつかの情報が含まれています。たとえば、"net"と"pc"は、PLCの種類とステーション番号を識別するためのものであり、"unitio"と"unitno"は、通信に使用されるユニットとそのユニットの番号を指定します。

"read"オブジェクトには、PLCからデータを読み取るために必要な情報が含まれています。たとえば、"device"フィールドには、データを読み取るために使用されるアドレスが指定されています。

"write"オブジェクトには、PLCにデータを書き込むために必要な情報が含まれています。"devicedata"フィールドには、書き込むデータが指定されています。