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

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

[サーバやPC上のプログラムでModbus機器からデータを取得 (1)](https://qiita.com/COOLMAGICPRODU1/items/d28e729f9e44eb25d7c1)

今回は、取得したデータをデータベースに保存します。
## データベース
以前の記事で使用した、MySQLとテーブル"from_plc"
を使います。

[PLCからゲートウェイでデータを取得しデータベースにJSONで保存 (4)](https://qiita.com/COOLMAGICPRODU1/items/449f206bbdf07c8e491b)

```
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により取得したデータをデータベースに保存します。
```perl
#!/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を起動しておきます。
![59603780d55c186e1a70e1b112539c5562d52790afb85.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2737842/9ddecafb-76ec-4638-123c-8904d6566e26.png)
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 に任意の数値を入力します。
![117891e3a7d9cfb3ad2aee7bc5db0e2662d5287c3564a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2737842/9cc70265-17bf-42d2-c6c8-8aa402fd9813.png)
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)](https://qiita.com/COOLMAGICPRODU1/items/d28e729f9e44eb25d7c1)