EtherNet/IPとは
産業用の工作機や電子機器は、産業用プロトコルにより他の機器と通信します。
EtherNet/IPは、数ある産業用プロトコルのひとつであり、ODVA(Open DeviceNet Vendor Association, Inc.)によって管理されています。
このプロトコルは、簡単にいうとCIP(Common Industrial Protocol)をTCP/IP上で通信します。
類似のプロトコルとしては、DeviceNetやCompoNet、ControlNetなどがあります。
EtherNet/IPの特徴は、産業界で使われているCIPを、一般に広く普及しているスイッチングハブやイーサネットケーブルで構築したネットワーク上で使える点です。
TCP/IPで通信できればよいため、例えば無線LANやモバイルネットワークでの通信も可能です。
pycomm3
pycomm3は、PythonでEtherNet/IP通信を可能にするライブラリです。
同じような機能を持つライブラリは複数ありますが、とりあえずGoogle検索して最も上に登場したライブラリを使うことにしました。
以下のコマンドでインストールできます。
> pip install pycomm3
また、CondaForgeパッケージにも登録されているため、AnacondaでPython環境を構築した方は以下でもインストールできます。
> conda install -c conda-forge pycomm3
EtherNet/IP対応サーモカメラ AX8
サーモグラフィカメラ(サーモカメラ)ではFLIR社の製品が有名です。
FLIR社の製品のひとつ、AX8は固定型サーモカメラであり、ネットワーク接続して遠隔でサーモ画像を取得します。
AX8は様々なプロトコルに対応していますが、EtherNet/IPにも対応しています。
今回は、このAX8にEtherNet/IPで接続し、遠隔操作してみます。
デバイスを見つける
まず、AX8をPCと同じネットワークに接続し、電源をつけておいてください。
そのうえで以下のプログラムを実行すると、ネットワーク上に接続されたEtherNet/IP対応デバイスを列挙し、出力します。
import pycomm3
pycomm3.CIPDriver.discover()
[{'encap_protocol_version': 1,
'ip_address': '192.168.0.180',
'vendor': 'FLIR Systems',
'product_type': 'Generic Device (keyable)',
'product_code': 321,
'revision': {'major': 2, 'minor': 40},
'status': b'\x00\x00',
'serial': '********',
'product_name': 'FLIR AX8',
'state': 255}]
(serial
の項目は*
に置き換えています)
ちなみに、すでにIPアドレスが分かっている場合は、list_identityメソッドの引数にIPアドレスを指定して、デバイスの情報を得ることができます。
pycomm3.CIPDriver.list_identity('192.168.0.180')
{'encap_protocol_version': 1,
'ip_address': '192.168.0.180',
...
'state': 255}
デバイスの情報を取得する
ネットワークに接続されたAX8のIPアドレスが分かったところで、AX8の情報を取得してみます。
ここからは、AX8の仕様書とにらめっこしながらプログラミングしていきます。
EtherNet/IPでは、オブジェクトと呼ばれる、プログラミング言語ではいわゆるクラスが存在します。
クラスには番号があり、例えばIdentity Objectは1番です。
Objectは複数のインスタンスを持ちえます。
Identity Objectはどうやら1インスタンスのみのようです。
クラスは属性(Attribute)を複数持ちます。
これがデータとなります。
例えば、AX8におけるIdentity Objectの定義は以下です。
Instance | Attirbute ID | Name | Data Type | Data value | Access rule |
---|---|---|---|---|---|
Class | 1 | Revision | UINT | 1 | Get |
Instance 1 | 1 | Vendor number | UINT | 1161 | Get |
2 | Device type | UINT | 43 | Get | |
3 | Product code number | UINT | 320 | Get | |
4 | Product major revision Product minor revision |
USINT USINT |
02 38 |
Get | |
5 | Status | WORD | Always 0 | Get | |
6 | Serial number | UDINT | Unique 32 bit value ".version.product.serial" |
Get | |
7 | Product name | SHORT STRING32 | Depends on camera model. | Get |
これらのオブジェクトとデータのやり取りをするには、サービスを利用します。
オブジェクトごとに利用可能なサービスは異なります。
例えば、Identity Objectで利用可能なサービスは以下です。
Service code | Class level | Instance level | Service name |
---|---|---|---|
05 | No | Yes | Reset |
0E | Yes | Yes | Get_Attribute_Single |
もし、Identity ObjectのProduct nameを取得した場合、以下のようにプログラムすればよいです。
with pycomm3.CIPDriver('192.168.0.180') as driver:
res = driver.generic_message(
service=pycomm3.Services.get_attribute_single, # <1>
class_code=pycomm3.ClassCode.identity_object, # <2>
instance=1, # <3>
attribute=7, # <4>
data_type=pycomm3.SHORT_STRING) # <5>
print(res)
generic, 'FLIR AX8', SHORT_STRING, None
プログラムを解説します。
with pycomm3.CIPDriver('192.168.0.180') as driver:
により、指定したIPアドレスのデバイスへEtherNet/IPで接続します。
接続したのち、driverでデバイスへの操作が可能です。
res = driver.generic_message(...)
generic_messageメソッドにより、接続したデバイスに対して、サービスを実行可能になります。
ここでは、「Get_Attribute_Single」のサービスを実行するため、<1>の引数で指定しています。
続く<2>・<3>で、どのクラス、インスタンスに対してサービスを実行するかを指定します。
取得したい「Product name」は、Attirbute IDが7のため、<4>のように指定しています。
最後、「Product name」のtypeを表で確認すると「SHORT STRING32」なので、pycomm3.SHORT_STRING
と指定しています。
以上により、1つの属性の値を取得できます。
ちなみに、AX8ではIdentity Objectで利用可能なサービスのうち、データ取得に関するサービスが「Get_Attribute_Single」のみでしたが、
デバイスによっては「Get_Attribute_All」もあります。
この場合、「Get_Attribute_Single」サービスにより、一度で複数の属性をデータ取得できると思います。
温度データを取得する
AX8はサーモカメラなので、カメラ視野中の任意の箇所の温度を計測し、EtherNet/IPで出力できます。
仕様書1.3章「1.3 Assembly Object (04HEX - 8 Instances) 」のOutput Dataの中に、「Spot 1 Temperature」があるので、このデータを取得すれば良さそうです。
ただし、そのアドレスは「Byte = 36~39」とあります。
じつは、AX8ではある属性から116バイトのデータを取得してから、そのデータの36~39バイトに、「Spot 1 Temperature」が格納されています。
そのため、自分で必要なデータをパースする必要があります。
以下が、あらかじめ指定した、画像中のある特定の点の温度を取得するプログラムです。
セルシウス温度で出力するよう、絶対温度から変換してあります。
出力例は25.9度で、だいたい室温と同じになりました。
import struct
with pycomm3.CIPDriver('192.168.0.180') as driver:
driver.open()
res = driver.generic_message(
service=pycomm3.Services.get_attribute_single,
class_code=pycomm3.ClassCode.assembly,
instance=0x64,
attribute=3,
)
buffer = res[1]
spot1_temp = struct.unpack_from('<f', buffer, 36)[0] - 273.15
print(f"{spot1_temp:.1f}: spot1 temp")
25.9: spot1 temp
このプログラムを簡単に解説すると、
res = driver.generic_message(...)
により、116バイトのデータを取得できます。
その応答結果のデータの部分のみを変数bufferに格納し、bufferの先頭36バイト目から、32ビット浮動小数をリトルエンディアンで取得します。
取得した浮動小数は絶対温度なので、273.15を減じることで、セルシウス温度となります。
撮影する
AX8では「Image File Storage Object (69HEX- 1 Instance) 」というオブジェクトが定義されています。
仕様書の説明を読むと、「Store Image to Camera Memory」の値を1に設定すると、カメラ画像がAX8の内部メモリに画像が保存されるようです。
というわけで、撮影するプログラムは以下のプログラムとなります。
実際に実行したところ、内部に正しく記録されていました。
with pycomm3.CIPDriver('192.168.0.180') as driver:
res = driver.generic_message(
service=0x10, # pycomm3.Services.get_attribute_singleでもよい
class_code=0x69,
instance=1,
attribute=1,
request_data=bytearray([1]),
data_type=pycomm3.BOOL
)