2022-07-30に更新

サーバやPC上のプログラムでModbus機器からデータを取得 (2)

読了目安:10分

サーバやPC上のプログラムでModbus機器からデータを取得、その第二回です。
第一回では、Android上のModbusシミュレータからデータを取得するための、Perlスクリプトのサンプルを紹介しました。Perlでは問題があるという方は、他の言語で記述しても原理は変わらないはずなので、他の言語で実装してみてください。

サーバやPC上のプログラムでModbus機器からデータを取得 (1)

今回は、取得したデータをデータベースに保存します。

データベース

以前の記事で使用した、MySQLとテーブル"from_plc"
を使います。

PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (4)

C:\Users\hoge\Downloads>mysql -u hoge -p
Enter password: *******
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 60
Server version: 8.0.29 MySQL Community Server - GPL

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use hoge
Database changed
mysql> desc from_plc;
+-------------+-----------+------+-----+-------------------+-------------------+
| Field       | Type      | Null | Key | Default           | Extra             |
+-------------+-----------+------+-----+-------------------+-------------------+
| body        | json      | YES  |     | NULL              |                   |
| time_insert | timestamp | YES  |     | CURRENT_TIMESTAMP | DEFAULT_GENERATED |
+-------------+-----------+------+-----+-------------------+-------------------+
2 rows in set (0.02 sec)

mysql>

Perlスクリプト

前回は、Perlスクリプトを僕のセルフォンTermux上で動作させましたが、今回はPCのWindows上で開発し動作させます。何故?

僕のセルフォンのTurmuxのcpanになんらかの問題があるらしく、DBIのビルドができないんです。いろいろトライしたけど僕には解決は無理と判断し、Turmax上のPerlスクリプトを放棄しました。
DBIは必須ではないですが、データベースをアクセスするPerlなら、DBIを使うのが一般的ですよね。
DBIのない環境で、これ以上Perlを書く気にはなれませんよね。まあ、僕のせいなんですけどね。

「サーバやPC上のプログラムでModbus機器からデータを取得 (1)」で作成したスクリプトに手を加えました。これで、Modbus機器からModbus/TCPにより取得したデータをデータベースに保存します。

#!/usr/bin/perl

use IO::Socket;
use DBI;
use Time::HiRes "gettimeofday";
use utf8;
use Encode;
use JSON;

{
    my $Server = 'localhost';
    my $Port = 502;

    my $TransactionID = 0x0200;
    my $ProtocolID = 0x0000;
    my $Length = 0x0006;

    my $UnitID = 0x01;
    my $FunctionCode = 0x03;
    my $StartAddress = 0x0000;
    my $CountRegister = 0x0001;

    my $req = pack("n3C2n2", 
        $TransactionID, $ProtocolID, $Length, 
        $UnitID, $FunctionCode, 
        $StartAddress, $CountRegister);

    my $old = -1;

    while (1) {
        my $socket = new IO::Socket::INET( 
            PeerAddr=>$Server,
            PeerPort=>$Port,
            Proto=>'tcp');
        die "IO::Socket : $!" unless $socket;

        my $size = $socket->send($req);
        shutdown($socket, 1);

        my $response = "";
        $socket->recv($response, 1024);

        my @data = unpack("n3C3n", $response);
        my $Register = $data[6];

        if ($old != $Register) {
            print "sent data:\n";
            BinaryDump($req);
            print "received response:\n";
            BinaryDump($response);

            my $now = getCurrentTimeStr();
            my $output_data = {
                'ts' => $now,
                'value' => $Register
            };
            my $json_text = decode('utf-8', encode_json( $output_data )); 
            print $json_text . "\n";
            my @values = ($json_text);

            $user = 'hoge';
            $passwd = 'hoge001';
            $db = DBI->connect('DBI:mysql:hoge:localhost', $user, $passwd);
            $sth = $db->prepare("INSERT INTO from_plc (body) values (?)");
            $sth->execute(@values);
            $sth->finish;
            $db->disconnect;
        }

        $old = $Register;

        $socket->close();
        sleep 1;
    }

}

# 利用させていただきました
# https://netlog.jpn.org/r271-635/2018/11/perl-bynary-dumper.html
sub BinaryDump {
    my ($buf) = @_;
    my $len;
    my $i;

    $len = length($buf);
    printf ("length = %d\n", $len);
    for ($i = 0; $i < $len; $i++) {
        printf("%02X ", ord(substr($buf, $i, 1)));
        # 16文字目で画面上の改行
        if (($i % 16) == 15) {
            print "\n";
        }
    }
    if (($i % 16) != 15) {
        print "\n";
    }
}

# 利用させていただきました
# https://akrad.hatenablog.com/entry/2018/10/20/234528
sub getCurrentTimeStr {
    my ($epochSec, $microSec) = gettimeofday();
    my ($sec, $min, $hour, $day, $mon, $year) = localtime($epochSec);
    $year += 1900;
    $mon++;
    return "$year" 
        . '-' . sprintf("%02d", $mon) 
        . '-' . sprintf("%02d", $day) 
        . 'T' . sprintf("%02d", $hour) 
        . ':' . sprintf("%02d", $min)
        . ':' . sprintf("%02d", $sec)
        . '.' . "$microSec";
}

Perlの自由さと気楽さが良いですね。

テスト

Windows上ということで、再び MOD-RSsim に登場いただきます。MOD-RSsimを起動しておきます。
image
Perlスクリプトを実行します。

C:\Users\hoge\Downloads>perl sample.pl
sent data:
length = 12
02 00 00 00 00 06 01 03 00 00 00 01
received response:
length = 11
02 00 00 00 00 05 01 03 02 00 00
{"ts":"2022-07-30T15:05:59.916895","value":0}

Holding Register 40001 の0を取得しデータベースに保存されます。

MOD-RSsim で 40001 に任意の数値を入力します。
image

40001 のデータが変わる都度、データベースに値が保存されます。

C:\Users\hoge\Downloads>perl sample.pl
sent data:
length = 12
02 00 00 00 00 06 01 03 00 00 00 01
received response:
length = 11
02 00 00 00 00 05 01 03 02 00 00
{"ts":"2022-07-30T15:05:59.916895","value":0}
sent data:
length = 12
02 00 00 00 00 06 01 03 00 00 00 01
received response:
length = 11
02 00 00 00 00 05 01 03 02 00 01
{"ts":"2022-07-30T15:06:04.10446","value":1}
sent data:
length = 12
02 00 00 00 00 06 01 03 00 00 00 01
received response:
length = 11
02 00 00 00 00 05 01 03 02 00 02
{"ts":"2022-07-30T15:06:07.41537","value":2}
sent data:
length = 12
02 00 00 00 00 06 01 03 00 00 00 01
received response:
length = 11
02 00 00 00 00 05 01 03 02 00 03
{"value":3,"ts":"2022-07-30T15:06:10.72655"}
sent data:
length = 12
02 00 00 00 00 06 01 03 00 00 00 01
received response:
length = 11
02 00 00 00 00 05 01 03 02 00 04
{"value":4,"ts":"2022-07-30T15:06:12.150828"}
sent data:
length = 12
02 00 00 00 00 06 01 03 00 00 00 01
received response:
length = 11
02 00 00 00 00 05 01 03 02 00 05
{"ts":"2022-07-30T15:06:14.213053","value":5}
Terminating on signal SIGINT(2)

C:\Users\hoge\Downloads>

データベースのテーブル"from_plc"を確認してみます。

C:\Users\hoge\Downloads>mysql -u hoge -p
Enter password: *******
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 75
Server version: 8.0.29 MySQL Community Server - GPL

Copyright (c) 2000, 2022, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> use hoge
Database changed
mysql> select * from from_plc;
+----------------------------------------------------+---------------------+
| body                                               | time_insert         |
+----------------------------------------------------+---------------------+
| {"ts": "2022-07-30T15:05:59.916895", "value": 0}   | 2022-07-30 15:05:59 |
| {"ts": "2022-07-30T15:06:04.10446", "value": 1}    | 2022-07-30 15:06:04 |
| {"ts": "2022-07-30T15:06:07.41537", "value": 2}    | 2022-07-30 15:06:07 |
| {"ts": "2022-07-30T15:06:10.72655", "value": 3}    | 2022-07-30 15:06:10 |
| {"ts": "2022-07-30T15:06:12.150828", "value": 4}   | 2022-07-30 15:06:12 |
| {"ts": "2022-07-30T15:06:14.213053", "value": 5}   | 2022-07-30 15:06:14 |
+----------------------------------------------------+---------------------+
6 rows in set (0.00 sec)

mysql>

まとめ

このように、Modbus機器からのデータ取得は、専用の機器や商用のソフトウェアがなくても可能です。今回はModbus/TCPでしたが、RTUでも原理は同じです。
データ取得だけではなく、Modbus機器にデータを書き込むことも難しくはありません。Modbusの仕様を理解すれば、データの取得も書き込みも簡単です。
Modbusに限らず、各メーカのPLCの仕様もほとんどがオープンになっているので、それぞれの仕様を理解しさえすれば、Modbusと同じように自分で書いたプログラムでの読み書きが可能です。
様々な種類のPLCやリモートIOなどをアクセスする場合には、ゲートウェイなどを利用したほうが良いかもしれません。一方で、限られた種類のPLCやリモートIOをアクセスする場合は、自分で書いたプログラムを使用するという選択もあると思います。

「作らない」のではなく「作れない」んです。なぜ「作れない」のか?「作らない」からです。

サーバやPC上のプログラムでModbus機器からデータを取得 (1)

ツイッターでシェア
みんなに共有、忘れないようにメモ

COOL MAGIC PRODUCTS

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント