pymodbusでModbus/TCPスレーブ(サーバー)を構築する
お久しぶりです。IMAXおじさんです。 今日は今までのWEB系の話とは少し変わって、PyModbusでModbus/TCPのサーバーを構築する話をします。
参考文献
HMSのサイト M-system技研のModbus/TCP通信仕様書
Modbus/TCPとは
Modbus/TCPとは、Modicon社(現Schneider Electric社)が1979年に策定した産業用Ethernetプロトコルです。 主にPLCやリモートIOとの通信に使用される産業用Ethernetプロトコルであり、世界的なシェアとしては5%程度を占めています(2020年現在)。
通信の概要
サーバー(フィールド側, PLCやリモートIO)
xxx.yyy.zzz.100
|
|
クライアント(PC)
xxx.yyy.zzz.101
Modbus/TCPはサーバー/クライアント方式の通信プロトコルです。 PLCやリモートIOなどがスレーブ(==サーバー)、データ収集を行うPCなどがマスター(==クライアント)としてデータのやり取りを行います。 名前の通り、各サーバーとクライアントがTCPでペイロードをやり取りします。 物理レイヤーの仕様(端子形状など)は指定されておらず、あくまで通信のレイヤーのみの仕様となります。
ペイロードの仕様に関しては、下記のM-system技研発行の通信仕様書がよくまとまっており参考になります。
PyModbusとは
PyModbusとは上記のModbusサーバー・クライアントを擬似的にPythonで構築できるライブラリです。 対向機をわざわざ購入するとなると、数万円かかってしまいますが、PyModbusを使えば無料で手軽に対向機を用意できます。
Modbus/TCPによる通信デモ
さて、Modbus/TCPの説明が終わったところで早速デモの構築に移りましょう。 PC上にPyModbusで構築したサーバーまでデータを取りに行くデモを構築します。
検証環境
- PC: DELL Inspiron 7370
- OS: Ubuntu20.04(LTS)
- Python: 3.8.10
- PyModbus: 2.5.3
Modbus/TCPスレーブ(サーバー)のPyModbusによる構築
PyModbusのサーバーを早速構築します。ローカルホストにサーバーを立てることにするため、特にIPアドレスは気にしなくてもよいです。 実際にデバイス間通信を試したい人は、Ethernetポートを備えたデバイスを2個用意して固定IPを振ってあげるとよいかと思います。
今回はinput_register
に値を入れることにします。
サーバーがレジスタ(input_register
)に保持する値は、以下の通りとしましょう。
----------------------------
register1 | int16
----------------------------
register2 | float
register3 | (32bit)
----------------------------
Modbusでは、1レジスタ=2byte(16bit)で扱われることに注意してください。なので、32bitのfloatなど16bitを超えるようなデータはレジスタをまたがって保持されることになります。
さて、このような値を返してくれるサーバーをPyModbusで構築していきます。まずは必要ライブラリのimportを。
# sample_server.py
from pymodbus.constants import Endian
from pymodbus.version import version
from pymodbus.server.asynchronous import StartTcpServer
from pymodbus.datastore import ModbusSequentialDataBlock
from pymodbus.datastore import ModbusSlaveContext, ModbusServerContext
from pymodbus.payload import BinaryPayloadBuilder
サーバー側でアクセス・送受信しているパケットの内容を確認するため、ログの設定をします。
import logging
FORMAT = ('%(asctime)-15s %(threadName)-15s'
' %(levelname)-8s %(module)-15s:%(lineno)-8s %(message)s')
logging.basicConfig(format=FORMAT)
log = logging.getLogger()
log.setLevel(logging.DEBUG)
次に、レジスタに入れる値を作っていきます。
PyModbusではpymodbus.BinaryPayloadBuilder
を使ってペイロードのオブジェクトを作ることになります。
builder = BinaryPayloadBuilder(byteorder=Endian.Big) #ペイロードビルダーのインスタンスを作成。バイトオーダーは再現したいリモートIOの仕様に準ずること。
# 最初のint_16のペイロードを作成
builder.add_16bit_int(100)
# 2つ目のfloatのペイロードを作成
builder.add_32bit_float(123.45)
# ペイロードをModbusSequentialDataBlockへ変換
block = ModbusSequentialDataBlock(1, builder.to_registers())
# レジスタへの登録を行う
store = ModbusSlaveContext(ir=block, zero_mode=False)
context = ModbusServerContext(slaves=store, single=True)
最後に、サーバーを起動しておしまいです。
StartTcpServer(context, address=("0.0.0.0, 502")) # ローカルホストで起動
Modbus/TCPマスター(クライアント)のPyModbusによる構築
サーバーの構築はできたので、サーバーにためている情報を取りに行くクライアントを作ります。
Modbusではクライアントからのみ通信を始めることができます。
クライアントから打てるリクエストにはいくつか種類(ファンクションコード)があります。
詳しくはModbus/TCPの通信仕様書を見ていただきたいのですが、今回はファンクションコード4(FC4)を使います。
FC4はread_input_registers
というリクエストになっており、その名の通りinput_registers
の値を読み取るものになります。
(余談ですが、Modbus/TCPではread_input_registers
で読まれるレジスタアドレスは300001
から始まると決まっているようです。FC4なのに開始アドレスが3xxxなのは気持ちが悪い...)
サーバーの設定では、レジスタ3つにまたがってデータが溜まっているのでread_input_registers
で3つ分レジスタを読んであげましょう。
from pymodbus.client.sync import ModbusTcpClient
from pymodbus.constants import Endian
from pymodbus.payload import BinaryPayloadDecoder
# Modbusクライアントのインスタンス。ローカルホストにサーバーが立っているので、アクセス先をlocalhostと指定。
client = ModbusTcpClient("localhost", port=502)
# FC4のread_input_registersでレジスタアドレス300001+0 ~ 300001+2を読み取る。
# 第一引数が開始アドレス(相対)、第二引数がレジスタ数
res = client.read_input_registers(0, 3, unit=1)
# 読み取ったレジスタ値をデコーダインスタンスへ渡す
# サーバー側がビッグエンディアン設定なので、こちらも合わせる。
decoder = BinaryPayloadDecoder.fromRegisters(res.registers, byteorder=Endian.Big)
# 最初のint_16bitをデコード
int16 = decoder.decode_16bit_int()
# 次のfloat_32bitをデコード
float32 = decoder.decode_32bit_float()
print(int16) # -> 100
print(float32) # -> 123.45...
これで、サーバーのinput_registers
に溜まったデータをModbus/TCPで読み取ることができました。
まとめ
産業用ネットワーク難しい