PythonでModbus TCP通信を行う

# はじめに
今回は産業界で広く使われているModbus TCP通信をPythonで行う方法を紹介します。
PyModbusパッケージを使用することで手軽にModbus通信を行うことができます。

# Modbus / Modbus TCPとは
Modbus[^modbus]とは、産業界で広く使われているオープンな通信プロトコルです。
マスタ・スレーブ方式の通信で、マスタのリクエストに対してスレーブがレスポンスを返すという形で通信が行われます。
![modbus1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/2457e371-8303-b598-3bf9-d1d7a75dceea.png)

保持できるデータには1ビット単位のコイルと16ビット単位のレジスタがあり、アクセス権に応じて以下の4種類に分類されます。

| データ       | 単位  | アクセス権   |
| ------------ | ----- | ------------ |
| コイル       | 1bit  | 読み書き     |
| 入力リレー   | 1bit  | 読み出しのみ |
| 保持レジスタ | 16bit | 読み書き     |
| 入力レジスタ | 16bit | 読み出しのみ |

Modbus TCPとは、Modbus通信をTCP/IP上で行えるようにしたもので、インターネット環境でModbusメッセージのやり取りを行うことができます。
Modbus TCPはクライアント/サーバー方式の通信であり、クライアントがModbusマスタ、サーバーがModbusスレーブに対応します。
Modbusについてのより詳しい情報は、[こちらのサイト](https://modbus.org/)をご覧ください。

# 構成
エッジデバイス（クライアント）で現場にある機器（サーバー）のデータを収集するという状況を想定して、以下の構成で通信を行います。
今回は現場にある機器の代わりにPCをサーバーとして使用し、エッジデバイスがPCのデータを読み書きします。
![modbus2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682603/ceb8f8cf-c2c4-4b40-420b-78f77864afd0.png)

# 動作確認済みデバイス
動作確認済デバイス（OS）

- e-RT3 Plus F3RP70-2L[^ert3]（Ubuntu 18.04 32bit）
- Raspberry Pi 4 Model B （Ubuntu Server 20.04 32bit）

これらのデバイスでは armhf アーキテクチャのパッケージが動作します。

# 準備
## パッケージのインストール
エッジデバイスとPCの両方にModbus通信で必要なPyModbus[^pymodbus]をインストールします。

コマンド例

```bash
username@ubuntu:~$ python3 -m pip install pymodbus
username@ubuntu:~$ python3 -m pip show pymodbus
Name: pymodbus
Version: 1.3.2
...
```

>**Note**
>e-RT3の場合、あらかじめPyModbusがインストールされているためこの手順は不要です。

# 通信
エッジデバイスとPCの間でModbus通信を行います。
まず、PCでPyModbusのサーバーのサンプルプログラム[synchronous_server.py](https://github.com/riptideio/pymodbus/blob/v2.5.2/examples/common/synchronous_server.py)を実行し、サーバーを起動しておきます。（プログラムのライセンスは[こちら](https://github.com/riptideio/pymodbus/blob/v2.5.2/LICENSE)）

```bash
python3 synchronous_server.py
```
>**Note**
>本記事ではタグv2.5.2のコードを使用しています。

以降ではサンプルプログラム[synchronous_client.py](https://github.com/riptideio/pymodbus/blob/master/examples/common/synchronous_client.py)を参考にして作成したプログラムで各種コイル、レジスタにアクセスします。
`<IP_ADDRESS_OF_SERVER_PC>`はサーバーを起動しているPCのIPアドレスで置き換えてください。
また、使用している関数の詳しい仕様については[公式ドキュメント](https://pymodbus.readthedocs.io/en/latest/source/library/modules.html)[^document]をご参照ください。
## コイルの読み書き
コイルに1bitの値True/Falseを読み書きします。

```python:coil.py
#!/usr/bin/env python
from pymodbus.client.sync import ModbusTcpClient as ModbusClient

# サーバーに接続
client = ModbusClient('<IP_ADDRESS_OF_SERVER_PC>', port=5020)
client.connect()

"""コイルの1点読み書き"""
print("Write to a coil and read back")
# アドレス1のコイルにTrueを書き込む
rq = client.write_coil(1, True)
# 読み取る
rr = client.read_coils(1, 1)
# 返り値は8bit単位なので先頭の1bitのみ読み取る
# 読み取り指示した先頭1bit以外はすべてFalseで埋められている
print(rr.bits[0])


"""コイルの複数点読み書き"""
print("Write to multiple coils and read back")
# アドレス3から9点のコイルにTrueを書き込む
rq = client.write_coils(3, [True]*9)
# 読み取る
rr = client.read_coils(3, 9)
# 返り値は8bit単位なので16bit返ってくる
# 読み取り指示した先頭9bit以外はFalseで埋められている
print(rr.bits)

# 切断
client.close()
```

プログラムを実行して、以下のような出力があれば成功です。

```bash
Write to a coil and read back
True
Write to multiple coils and read back
[True, True, True, True, True, True, True, True, True, False, False, False, False, False, False, False]
```
## 入力リレーの読出し
入力リレーから1bitの値を読み出します。

```python:discrete_input.py
#!/usr/bin/env python
from pymodbus.client.sync import ModbusTcpClient as ModbusClient

# サーバーに接続
client = ModbusClient('<IP_ADDRESS_OF_SERVER_PC>', port=5020)
client.connect()

"""入力リレー複数点読み取り"""
print("Read discrete inputs")
# アドレス0から3点の入力リレー読み取り
rr = client.read_discrete_inputs(0, 3)
# 返り値は8bit単位なので8bit返ってくる
# 読み取り指示した先頭3bit以外はFalseで埋められている
print(rr.bits)

# 切断
client.close()
```
プログラムを実行して、以下のような出力があれば成功です。

```bash
Read discrete inputs
[True, True, True, False, False, False, False, False]
```
## 保持レジスタの読み書き
保持レジスタに16bitの値を読み書きします。

```python:holding_register.py
#!/usr/bin/env python
from pymodbus.client.sync import ModbusTcpClient as ModbusClient

# サーバーに接続
client = ModbusClient('<IP_ADDRESS_OF_SERVER_PC>', port=5020)
client.connect()

"""保持レジスタ1点読み書き"""
print("Write to a holding register and read back")
# アドレス1の保持レジスタに10を書き込み
rq = client.write_register(1, 10)
# 読み取り
rr = client.read_holding_registers(1, 1)
print(rr.registers)

"""保持レジスタ複数点読み書き"""
print("Write to multiple holding registers and read back")
# アドレス2から10点の保持レジスタに値を書き込み
values = [i for i in range(10)]
rq = client.write_registers(2, values)
# アドレス2から10点読み取り
rr = client.read_holding_registers(2, 10)
print(rr.registers)

# 切断
client.close()
```
プログラムを実行して、以下のような出力があれば成功です。

```bash
Write to a holding register and read back
[10]
Write to multiple holding registers and read back
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```
## 入力レジスタの読出し
入力レジスタから16bitの値を読み出します。

```python:input_register.py
#!/usr/bin/env python
from pymodbus.client.sync import ModbusTcpClient as ModbusClient

# サーバーに接続
client = ModbusClient('<IP_ADDRESS_OF_SERVER_PC>', port=5020)
client.connect()

"""入力レジスタ複数点読み取り"""
print("Read input registers")
# アドレス1から8点の入力レジスタを読み取り
rr = client.read_input_registers(1, 8)
print(rr.registers)

# 切断
client.close()
```
プログラムを実行して、以下のような出力があれば成功です。

```bash
Read input registers
[17, 17, 17, 17, 17, 17, 17, 17]
```

# まとめ
今回はPyModbusを利用した簡単なModbus通信のやり方を紹介しました。
次回以降の記事ではModbus通信を利用したデータ収集アプリケーションを紹介する予定です。

[^ert3]: [産業用AIプラットフォーム | 横河電機株式会社](https://www.yokogawa.co.jp/solutions/products-platforms/control-system/real-time-os-controller/ert3-ai-platform/)
[^pymodbus]: [GitHub - riptideio/pymodbus: A full modbus protocol written in python](https://github.com/riptideio/pymodbus)
[^document]: [Pymodbus &mdash; PyModbus 2.5.0 documentation](https://pymodbus.readthedocs.io/en/latest/source/library/modules.html)
[^modbus]: [The Modbus Organization](https://modbus.org/)