はじめに
OPC-UAは、Object Linking and Embedding for Process Control Unified Architectureの略である。もともとOPC(OPC Classicとも)という規格があり、それをより汎用化したものになる。例えば工場などだと色々な機器が導入されており、ベンダーによって通信プロトコルが異なっていた。それをMSのCOM/DCOM技術をもとに、通信規格が異なる機器(具体的にはPLC)との通信を実現するために開発された。この技術をマルチプラットフォーム(MS非依存)にしたのが、OPC-UAである。
概要については産業IoT界隈では良く使われるらしい国際規格OPC-UAについて調べたなどでまとめられているが、どう実装されて、どんなデータが流れるのか、ということを確認しておきたい。
3行まとめ
- OPC-UAプロトコルを使えば、異なる機器での情報やり取りが可能
- OPC-UA自体(UA Binary)は、TCP通信確立→UA通信確立(Hello message)→セキュアチャンネル確立→セッション確立→データ交換でやりとり
- OPC-UAのGo実装やPyton実装も存在するが、まだまだ開発途中でOPC-UA Binaryしか対応していない。非同期も無理の様子。(HTTPSやXMLやり取りはまだ。OPC Foundationの実装では対応している様子。というか、Go実装はサーバーサイドが目下開発中の段階)
今回の調査における留意事項
OPCおよびOPC-UAの仕様については、アカウント登録することで公式からダウンロードすることができるが、著作権での保護が働いているので、そのまま内容を転記することはできない。なので、ここでは実際に動作するプログラムから動きを理解する。
なお、多少手は動かしているものの、仕様を完全に理解しているわけではないので、間違い等の指摘ありましたら大歓迎です。
実装例とその挙動について
OPC Foundationのリポジトリ上には、.NetやJava※Legacyな実装例を記載しているが、それ以外にもMozilaによるC実装やGo実装などが存在する。その中でも今回、Python実装を例にする。1
また、仕様の調査については、Network Analyzerとして名高いWireshark3.0.123で行った。
PythonによるOPCUAの実装例とその挙動
インストール自体はpip install opcua
で入るので簡単である。このPython実装はexamples以下にserverおよびclientの挙動例が存在する。このうち、minimal構成を使ってみる。忘れ防止で下記にコードを記す。ちなみに、opcuaのバージョンは0.98.7である。
import sys
sys.path.insert(0, "..")
import time
from opcua import ua, Server
if __name__ == "__main__":
# setup our server
server = Server()
server.set_endpoint("opc.tcp://0.0.0.0:4840/freeopcua/server/")
# setup our own namespace, not really necessary but should as spec
uri = "http://examples.freeopcua.github.io"
idx = server.register_namespace(uri)
# get Objects node, this is where we should put our nodes
objects = server.get_objects_node()
# populating our address space
myobj = objects.add_object(idx, "MyObject")
myvar = myobj.add_variable(idx, "MyVariable", 6.7)
myvar.set_writable() # Set MyVariable to be writable by clients
# starting!
server.start()
try:
count = 0
while True:
time.sleep(1)
count += 0.1
myvar.set_value(count)
finally:
#close connection, remove subcsriptions, etc
server.stop()
minimal構成だけあって至極シンプルな構成であるが、Serverを定義した後、nodeとしてobjectを作成して必要な情報を底に詰めている。あとはサーバーを稼働させて待機するだけである。クライアント側を確認すればわかるが、オブジェクトに定義した要素をそれぞれ参照していることがわかる。
import sys
sys.path.insert(0, "..")
from opcua import Client
if __name__ == "__main__":
client = Client("opc.tcp://localhost:4840/freeopcua/server/")
# client = Client("opc.tcp://admin@localhost:4840/freeopcua/server/") #connect using a user
try:
client.connect()
# Client has a few methods to get proxy to UA nodes that should always be in address space such as Root or Objects
root = client.get_root_node()
print("Objects node is: ", root)
# Node objects have methods to read and write node attributes as well as browse or populate address space
print("Children of root are: ", root.get_children())
# get a specific node knowing its node id
#var = client.get_node(ua.NodeId(1002, 2))
#var = client.get_node("ns=3;i=2002")
#print(var)
#var.get_data_value() # get value of node as a DataValue object
#var.get_value() # get value of node as a python builtin
#var.set_value(ua.Variant([23], ua.VariantType.Int64)) #set node value using explicit data type
#var.set_value(3.9) # set node value using implicit data type
# Now getting a variable node using its browse path
myvar = root.get_child(["0:Objects", "2:MyObject", "2:MyVariable"])
obj = root.get_child(["0:Objects", "2:MyObject"])
print("myvar is: ", myvar)
print("myobj is: ", obj)
# Stacked myvar access
# print("myvar is: ", root.get_children()[0].get_children()[1].get_variables()[0].get_value())
finally:
client.disconnect()
クライアント側はサーバーにコネクションを張った後、必要な情報を要求して、その結果を表示し、最後に切断しているだけである。サーバーとクライアントの挙動を見てみると、次のようになる。
$ python server-minimal.py
Endpoints other than open requested but private key and certificate are not set.
Listening on 0.0.0.0:4840
$ python client-minimal.py
Objects node is: Node(TwoByteNodeId(i=84))
Children of root are: [Node(NumericNodeId(i=85)), Node(NumericNodeId(i=86)), Node(NumericNodeId(i=87))]
myvar is: Node(NumericNodeId(ns=2;i=2))
myobj is: Node(NumericNodeId(ns=2;i=1))
$
非常にシンプルな挙動である。
パケット解析から見る通信の様子
上記の通信の様子をキャプチャしたのが、次のWiresharkのデータである。
No.41でRSTが発行されたのち、3 hand shakeでTCP通信がされている。なお、クライアントが50812ポート、サーバーが4840ポートである。ここでOPCUAの仕様を思い出してみると、次のような仕様であった。
※画像は他のQiita記事より
クライアントからの各種要求に対してサーバーが都度Ackを返しているが、最初にTCP接続され、そのあとOPC-UAの処理が続いている。特にOPC-UAの処理だけ抜粋してみる。
このパケットの流れだとわかりにくいのでシーケンス図に書き直す。
3 hand shaekでTCP通信を確立した後は、OPC-UAを確立するためのHello messageのやり取りを行い、後はセキュアなチャンネルとセッションの作成をし、データ交換をしている。というわけで、上述したプロトコルスタックのうち、TCP/IP上にセッション層+セキュリティ層を重ねたスタックが動いているのがわかる5。おそらくHTTPSを使った場合などは、Hello messageの前にTLS通信などが見えるような感じだと推察する。
ここまでがインターネット上で公開されているソフトウェアからわかることであり、より詳細なシーケンスについては、OPC-UA SpecificationのPart.6の7章あたりに記載されている。
最後に
今回試したpython-opcuaであるが、開発自体はopcua-asyncioに移行している。python3.6系以前を非対応とし、さらに非同期処理の実装を目指している様子である。ただし、実装できているのはUA Binaryの通信だけであり、XMLの通信はできていないと述べられている。また、Go実装の方も、UA Binaryの通信しかないうえ、非同期処理などは現在開発中の様子。限られた利用や、今後随時アップデートしていくなどであればともかく、最初からフル機能を期待してPython実装やGo実装を採用するのは現状できない。なので、今後OPC-UAを使う場合、
- OPC Foundationが公開している実装を使う
- 目下開発中の各言語実装の開発が進むのを祈る
- むしろ積極的に開発に参加する
などになりそう。さすがに、イチからフルスクラッチすると少々パワーが必要である。
-
2019年5月17日現在、Go実装のmasterブランチでは、サーバーサイドの機能が十分に取り込まれていない。feature/serverブランチ内でようやく実装&動作が確認できる。逆に、名乗りを上げるチャンスかもしれない。 ↩
-
2019年2月から3.0系がリリースされている。 ↩
-
後述するように今回の実験では一つの端末をserver&client構成で動作させるが、Windows上でlocalhost宛の通信をキャプチャする場合、Npcap4のインストール時にオプション設定にて「Install Npcap in WinPcap API-compatible Mode」を有効にする必要がある。 ↩
-
Wireshark2.x系まではWinPcapが付属していたが、3.x系からNpcapに代わった。詳細は避けるが、WinPcapの上位互換だと考えてよい。 ↩
-
Go実装のfeature/serverブランチには、examples以下にサンプルがある。その中のソースを使うと、同じようなシーケンスが確認できる。 ↩