PLC
ソケット
シーケンサー

PLCとTCP/IPネットワークを通して通信して、デバイス値の取得や書込みを行う方法

工場のIoTと昨今言われているのですが、PLCでできた既存の生産設備をパソコンからネットワーク経由でリアルタイムに見えるところまで持っていこうという段階のお話です。お仕事で通信することになったのでそのメモとして。

構成

実際に使った機器は三菱電機のFX5Uという中小規模設備を対象としたPLCでLANの口が標準装備されているタイプです。
このタイプのPLCはIPアドレスが設定できて、Socketを使って通信できるので、プログラミング言語を選ばないで上位PCからPLCのデバイスの値を簡単に読んだり書込んだりできます。今回はJavaを使ってサンプル書きます。
余談ですが、僕がお話をする生産技術現場の方(メカ屋さん)はパソコンのことを上位PCとおっしゃいます。上に立ったつもりはないのですが現場で話が通るので以下プログラミング側のことを上位PCと呼びます。

プログラミング言語 : Java
PLC:三菱電機 FX5U
PLC用管理アプリ:三菱電機 GX Works3
必要なマニュアル:三菱電機のホームページからダウンロードします↓
1.FX5ユーザーズマニュアル(Ethernet通信編)
2.FX5ユーザーズマニュアル(SLMP編)
※ユーザ登録が必要です

PLC側の設定

前提としてPLC側でIPアドレスの設定と通信ポートを開けておく必要があります。
その方法は、

三菱電機の専用ソフト GX Works3 で、PLCに設定を送り込みます。
GX Works3の細かな使い方は割愛しますが、PLCと接続してパラメータ→FX5UCPU→Ethernet構成を開いて、SLMP接続機器を追加します。
SLMPと言うのは三菱電機PLCのTCP/IPでの通信プロトコルの名前です。
追加したらポート番号を振ります。注意するのは、同時に通信できるのは1ポート1上位PCなので、複数の上位PCから通信があるようならSLMP接続機器を増やして別のポートを振る必要があるようです。
SLMP接続機器が追加できて、ポートが決まったら適用して、設定をPLCに送り込んだらPLC側は完了です。

ちなみに、同じネットワークセグメントからでないとPLCとGX Works3は通信をできないのでご注意を。これは、設備を勝手に変更されないようにするための制約なのかなと思います。設定のとき以外は別セグメントからでも通信できます。
それから、FX5よりも以前のタイプのCPUならGX DeveloperやGX Works2という以前のバージョンが専用ソフトになります。最初に現場からもらったソフトがGX Developerで、通信できなくて、GX Developer→Works2→Works3とバージョンを上げてようやく通信できました。

詳しいくは、マニュアル「FX5ユーザーズマニュアル(Ethernet通信編)」のSLMP機能の章が参考になります。

Kobito.9S9rjL.png
相手機器接続構成設定<詳細設定>をクリック

Kobito.a2AXsT.png

この画面だとポートを3つ、1025,1026,1027追加してます。
追加する時に、右のEthernet機器一覧からSLMP接続機器をグイッとドラックすると追加出来ます。

Javaでソケットプログラミング

PLC側がSLMPプロトコルで通信する設定ができたら、今後はプログラミングの準備です。
今回はJavaをサンプルに説明しますが、C#とかRubyとかSocket使えたらなんでも構いません。

今回書くならこの程度。特に難しいことはないです。

Soc.java
Socket s = new Socket("192.168.1.2", 1025);
BufferedInputStream in = new BufferedInputStream(s.getInputStream());
OutputStream os = s.getOutputStream();

//SLMP伝文送る
os.write(xxxxxx);
os.flush();

//返事受け取る
byte[] tmp = new byte[100]; //返事は短いので100もあればOK
int len = in.read(tmp);
System.out.println(tmp); //整形して出力

os.close();
in.close();
s.close();

SLMP伝文を作る

一番ややこしいのは"xxxxxx"で渡すSLMP伝文をSLPM形式でバイナリデータをつくるところなのですが、これはマニュアル「FX5ユーザーズマニュアル(SLMP編)」を読めばOKでした。でも、マニュアルが多項に渡るのでどこを読んだらいいのかも含めてここにまとめます。ちなみに、SLPM形式はASCII形式もあるのですが、PLC側をバイナリ(デフォルト)で設定したのでバイナリです。

例えば D3 の値を読み出そうと思ったときの伝文は、

パート 長さ 値(16進数文字列) 考え方
サブヘッダ 2バイト 5000 マニュアルの値は16進数で0050だが、Low-Hightの順で送り込むので、下位1バイト、上位1バイトの順で渡す。
要求先ネットワーク番号 1バイト 00 FX5Uの場合00固定。マニュアル、要求先ネットワーク番号,要求先局番号 の項を参照
要求先局番 1バイト FF FX5Uの場合00固定。同じくマニュアル参照
要求先ユニットI/O番号 2バイト FF03 マニュアルを参照して、自局なので値は03FF。Low-Hightの順でおくりこむので、下位バイトから並べる。
要求先マルチドロップ局番 1バイト 00 マルチドロップではないので00固定。
要求データ長 2バイト 0C00 リザーブ+コマンド+サブコマンド+要求データの合計バイト数。ワードデバイスDからの読み込みコマンドを送るので、今回のバイト長は12バイト。下位バイトから並べるので、000Cは0C00。
リザーブ 2バイト 0000 ワードデバイスから1ワード単位で読み出す場合は0000。マニュアルのコマンド一覧の項目を参照。
コマンド 2バイト 0104 マニュアルのコマンド一覧を参照して一括読出なら0401。下位バイトから並べるので0x0104。
サブコマンド 2バイト 0000 読出デバイスのデータサイズに応じで設定。Dならワードデバイスのメモリ拡張無しなので 0000。マニュアルのサブコマンド一覧の項目を参照。
要求データ コマンド次第 030000A80100 D3読出しなら、先頭デバイス番号(3バイト分で3番)+デバイスコード(1バイトでA8)+デバイス点数(2バイトで1点)で今回は 000003 + A8 + 0001。これをそれぞれ下位バイトから並べます。

SLMPマニュアルにはヘッダと書いてあるのですが、これはTCP/IPのヘッダのことのようなので、何もしなくてOKです。アプリケーションヘッダという部分を埋めていきます。
説明すると、固定長のバイトで構成されていて、先頭から、サブヘッダ(2バイト)、要求先ネットワーク番号(1バイト)、要求先局番(1バイト)などと幅が決まっています。それぞれに値を埋める必要があり、設定すべき値はマニュアルを参照します。
要求先局番やユニットI/O番号など、PLCにはユニットという単位でPLC同士を親子関係で接続できるので、どういう構成になっているかは現場で教えてもらわないといけないですが、今回はFX5U単体の場合になります。

ややこしいのは、PLCに送る時、2バイト以上の値セットのときは下位バイトから渡すので、値が 000C とすると渡すときは 0C00 と書いていくことです。でも、慣れてしまえば簡単です。
デバイスコードはマニュアルの「デバイス範囲」ページにあります。

以上をまとめると、D3デバイスを読み出す要求伝文は

Soc.java
String snd = "5000" //サブヘッダ
    + "00" //要求先ネットワーク番号
    + "FF" //要求先局番
    + "FF03" //要求先ユニットI/O番号
    + "00" //要求先マルチドロップ局番
    + "0C00" //要求データ長(リザーブ以降のバイト長)
    + "0000" //リザーブ
    + "0104" //コマンド 0401=一括読出し
    + "0000" //サブコマンド 0000=ワードデバイスから1ワード単位でデータを読み出し
    + "030000" //先頭デバイス番号 3
    + "A8" //デバイスコード D
    + "0100"; //デバイス点数 1
//文字列からバイナリに変換
//Javaのbyteは符号付きなので例えばFFは255ではなく-1
//でもビットの並びは11111111なので気にしなくてよさそう
byte[] bi = javax.xml.bind.DatatypeConverter.parseHexBinary(snd);

となります。

応答を受け取る

これをPLCに投げて、今度は応答文がバイナリで返ってくるのを読み込んで解析します。形式は固定長フォーマットで、意味はSLMPプロトコルマニュアルを読むだけでOKでとても簡単です。

今回のD3を読み出すならバイト長でいうと13バイトほどのデータが返ってきます。エラーの場合はエラー情報部分を含めて20バイトほど。D3の値が31なら16進数文字列で表すと、

D00000FFFF0300060000001F00

先頭から7バイト目までは局番情報なので今回は無視してOKで、8,9バイトが応答データ長。10,11バイト目が終了コード。12バイト目以降が応答データ。よって、終了コードが0000であることをチェックして、それ以降のバイトを読むとOKです。この時も下位バイト、上位バイトの順になるので応答データ 1F00 は、001F となります。

これを符号なしbyteに注意してintに変換してあげると D3 の値が取得できます。
int val = (Byte.toUnsignedInt(12バイト目)<<8) + Byte.toUnsignedInt(11バイト目);
※上記の部分、下記に訂正します↓

Dデバイスは−32768〜32767 の値の範囲の符号付きなので、intに拡張してシフト演算。D3=-30 なら FFE2 と符号付きの値が返ってきます。

int val = (((int)res[ 12バイト目 ])<<8) | ((int)res[ 11バイト目 ]);

まとめ

ここまでをまとめてコードにすると、

Soc.java
import java.net.*;
import java.io.*;
import javax.xml.bind.*;

public class Soc {
    public static void main(String[] args) throws Exception {
        Socket so = null;
        OutputStream out = null;
        BufferedInputStream in = null;
        try {
            so = new Socket("192.168.1.2", 1025);
            in = new BufferedInputStream(so.getInputStream());

            //要求伝文
            //Javaバイトは符号付きなので例えばFFは255ではなく-1
            //でもビットの並びは11111111なので気にしなくてよさそう
            byte[] snd = DatatypeConverter.parseHexBinary(
                  "5000" //サブヘッダ
                + "00" //要求先ネットワーク番号
                + "FF" //要求先局番
                + "FF03" //要求先ユニットI/O番号
                + "00" //要求先マルチドロップ局番
                + "0C00" //要求データ長(リザーブ以降のバイト長)
                + "0000" //リザーブ
                + "0104" //コマンド 0401=一括読出し
                + "0000" //サブコマンド 0000=ワードデバイスから1ワード単位でデータを読み出し
                + "030000" //先頭デバイス番号 3
                + "A8" //デバイスコード D
                + "0100"); //デバイス点数 1

            out = so.getOutputStream();
            out.write(snd);
            out.flush();

            //応答受取
            byte[] res = new byte[20]; //長さは20バイトで丁度
            int len = in.read(res);

            //ここでresの中身はD3=31なら
            //D00000FFFF0300060000001F0000000000000000

            //終了コードチェック Low-Highの順なので8ビットシフト
            int rescode = (((int)res[10])<<8) | ((int)res[9]);
            if(rescode != 0) 
                throw new Exception("ERROR: " + rescode);

            //エラーがなければ値を読む。応答データのうち
            //デバイス点数1でワードデバイスとわかっているのでズバリ特定の場所を読む
            int val = (((int)res[12])<<8) | ((int)res[11]);
            System.out.println("D3=" + val);
        }
        finally {
            if(out != null) try { out.close(); } catch(Exception e){}
            if(in != null) try { in.close(); } catch(Exception e){}
            if(so != null) try { so.close(); } catch(Exception e){}
        }
    }
}

実行結果

bash-3.2$ java Soc
D3=31

これでPLCの状態を上位PCから見えるようになりました。
書込みコマンドもこの要領なので、例えばM01にビットを立てたりして、PLCを上位PCから操作することも簡単でした。
セキュリティ設定をPLCにする必要がありそうですが、とりあえずここまで。