ガイド
OPC UAの概要
- OPC UA最新技術解説 - MONOist(モノイスト)
- いまさら聞けないOPC UA入門 - MONOist(モノイスト)
- 産業IoT界隈では良く使われるらしい国際規格OPC-UAについて調べた - Qiita
- OPC-UAの実装と挙動についての調査 - Qiita
動かして全体を理解する
OPC Foundationによる公式ライブラリとあわせてサンプル実装もあるので、まずはUA-.NETStandardをダウンロードしてきて動かす。UA Reference.slnを開き、ConsoleReferenceServerプロジェクトを起動する。この時、コマンドライン引数に-a
(証明書の強制承認)をつけて起動する。
OPC UAクライアントにはUaExpert(無償)がおすすめ。
- 接続設定(Add Server > Advancedタブ)
- Configuration Name=ReferenceServer(任意)
- Endpoint Url=opc.tcp://localhost:62541/Quickstarts/ReferenceServer
- Security Policy=Basic256Sha256
ProjectタブでReferenceServer(上記設定)を右クリックしてConnect後、Address Spaceタブを開けばノードツリーが見えるので、以下の操作を試してみてほしい。
- データアクセス
- Boilers > Boiler #1配下の適当なノードをData Access Viewにドラッグ&ドロップして登録すると、ランダムな数値がどんどん変わるのが見える
- 適当なノードを選択してAttributes欄でValueを見る(更新ボタンで読み込みなおすと最新が反映される)
- イベント購読
- Document > Add... > Event Viewを開いて、Boiler #1 > Simulationノードをドラッグ&ドロップする(メソッド呼び出しするとEvents欄に表示される)
- メソッド呼び出し
- Boiler #1 > Simulationの下の各種Methodノードを右クリックしてCallを選び、表示されるダイアログでCallボタンを押す
私の方でもサンプル実装(OpcUaChatServer)を用意した。チャットサーバを題材として、公式サンプルを基にスリム化してデータ1つ、メソッド1つ、イベント1つのみを実装してあり見通しが良いと思うので参考にしてほしい。
最後に改めてUA Reference.slnのConsoleReferenceServerプロジェクト(実際には参照先のQuickstarts.Serversプロジェクト)のBoilerフォルダ以下を読んだりUaExpertで触って、特にBoilerDesign.xmlと実際のノードツリーの関係や、操作時にサーバでどこの処理を通っているかなど理解を深める。なお、Librariesフォルダ配下とStackフォルダ配下のプロジェクトは、NuGetパッケージ OPCFoundation.NetStandard.Opc.Uaに含まれるものなので自分で書く部分ではない。
詳細を理解するための資料
- 本を読みたい人は OPC UA Unified Architecture: The Everyman's Guide to the Most Important Information Technology in Industrial Automation
-
Unified Automationのドキュメントが充実しているので一通り目を通すと良い(有償SDK向けのドキュメントだが基本的な部分も多く書かれているので使えるものは使わせてもらう)
- OPC IntroductionとOPC UA Fundamentalsは有用
- Tutorials for Server developmentは実際にコードを触る時に参考になる
- OPC UAの仕様書・オンラインリファレンスはここら辺まで来ればなんとか読めるはず(特にパート3、4、5はなるべく目を通したいが、分量が多いので疑問点を解消するときに読む感じになると思う)
- 仕様書のダウンロードにはユーザー登録が必要だが、そのコピーであるオンラインリファレンスならユーザー登録なしでも読める
独自の情報モデル(コンパニオン仕様)の構築と実装
独自のAPIをOPC UAで公開するには、独自の情報モデル(コンパニオン仕様)を作ることになる。APIの情報モデルをXML(*.Model.xml)に手打ちして、Opc.Ua.ModelCompiler.exeに渡して必要なファイル群を生成し、サーバアプリに組み込んだりクライアントへ配布する。
- ライブラリのOPC UA 情報モデルの設計ガイドラインを参照してNodeSetの設計を練る(必読)
- UA-NodeSetのREADME.mdを読んでUA-ModelCompilerの出力内容を理解する
- 私のサンプルを基に、公式サンプルを見ながら書き換えてみるとやりやすいだろう
-
UA-NodeSetにある業界団体のコンパニオン仕様も参考になるかもしれない
- 業界団体のコンパニオン仕様の仕様書
- 業界団体のコンパニオン仕様の情報モデルのリファレンス(オンラインリファレンスにこれも含まれている)もあり、独自のコンパニオン仕様向けの仕様書テンプレートもある
Visual Studioで *.Model.xmlを手打ちする場合、XML エディターの IntelliSense 機能を使うと便利だ。スキーマキャッシュのフォルダかプロジェクト内にUA Model Design.xsdを入れておけばIntelliSenseが機能してくれる。
gRPCのprotoファイルから*.Model.xmlを生成するツールも作ってみた(protoc-gen-opcua)。すでにgRPCのAPIがある場合の利用の他、gRPCの経験があればサンプルのprotoと*.Model.xmlを見比べることで理解の役に立つと思う。
Opc.Ua.ModelCompilerによるコンパイル処理はスクリプトにしておいて実行するだけで再生成できるようにしておくのが良い。私のサンプルではbuild.batを用意してある。これはbuild_model.batやPublishModel.batを参考にした。なお、 *.Model.xmlは実際には任意の名前でよく、公式サンプル中では~Design.xmlなどとなっていたりするので私もそれに合わせている。生成されるcsvの名前も入力したxmlによるので同じく *.Model.csvとは限らない。
Opc.Ua.ModelCompiler.exeのオプションでは-cg
を付けることで *.Model.csvを生成する。NodeはNodeIdで識別されるが、サーバのバージョンアップでNodeIdが変わってしまうとクライアントが困るので、一度リリースした後は既存の *.Model.csvを指定することになる。ただし、ModelCompilerは-cg
で指定されたcsvファイルを更新する挙動をするので、1度生成した後は勝手に既存ファイルを基に更新され、特に気にする必要はない。試しに、 *.Model.csv内のNodeIdを書き換えたり適当な行を削除したりしてから再作成してみると良い。
PublishModel.batは、Opc.Ua.ModelCompiler.exeによるファイル生成の後、 *.Model.xmlと更新された *.Model.csvも出力ディレクトリにコピーしているので、私のサンプルでも同じようにしてある。
セキュリティの詳細の理解
- OPC FoundationのライブラリにあるOPC UA のセキュリティ対策と証明書の運用を理解し運用を検討する
- ユーザー認可や証明書の有効期限など設計者にゆだねられている箇所も設計する
-
PKIおよびX.509証明書についての包括的な理解のためIPAのPKI 関連技術情報で勉強する(特に3章までは必読)(良い資料だったのですが消えてしまったので専門書等をあたってください) - UA-.NETStandardの証明書についての説明を理解する
- サンプルやSDKを見て実際にどう実装しているか理解する
その後(リリースに向けて)
- Config.xmlの説明はApplicationConfiguration.csのドキュメントコメントにあるので参考にしながら設定を詰める
- 有償SDKが気になるなら調べてみるといいかもしれない。私は調べていない。
- ライセンスについてOPC Foundation License Agreementを確認して適切に対処する
- 認証を取ることを検討する
ソースコード解説
ノードとNodeManager
- ノードに対応するのが
NodeState
クラスで、OPC UA上ではあらゆるオブジェクトがノードで表現されるのでこれの派生型になっている。例えばメソッドもMethodノードとして表し、NodeState
クラスの派生型のMethodState
クラスになる。 - サーバ起動時にノードのインスタンスを生成してノードのツリーを構成するのが
INodeManager.CreateAddressSpace()
。1つずつ愚直に書くこともできるが、 *.Model.xmlに定義してあれば生成された *.uanodesファイルを読み込むことで一括生成できる(BoilerNodeManager.LoadPredefinedNodes()
参照)。 - 各ノードとアプリケーション側のプロパティ・メソッド・イベントなどを紐づけるには、
INodeManager.CreateAddressSpace()
でやってしまうか、各ノードのOn~系のvirtualメソッドをoverrideしてやる。ノードが静的なら前者、動的なら後者。 - NodeManagerは1つのサーバに複数存在できる(
ReferenceServer.CreateMasterNodeManager()
)。つまり1つのサーバで複数のコンパニオン仕様に準拠することも可能。 - NodeManagerの基底クラスには
Opc.Ua.Server.CustomNodeManager2
を使うか、コピーして改変するのが良さそう。ドキュメントコメントによるとそのまま使いまわせないケースが多いためSDKに入れていないとは書いてあるが、Opc.Ua.Serverに入ってるのでそのまま使える。 - あらゆるところに出てくる/要求される
ISystemContext
は、基本的にはクライアントからのアクセス自体の情報としてユーザやセッションの情報などを持つもの。たぶんHttpContextとかと同類。メソッド呼び出しの引数としてこれが要求されたときは基本的にNodeManagerのSystemContext
プロパティを渡しておけばよい。- NodeManagerのコンストラクタで
SystemContext.SystemHandle
に任意のインスタンスをセットしておけば、ノードのメソッドなどで軒並み引数として与えられるISystemContext
から取り出して使うことができる。処理を実行するのに必要になる、アプリケーション側のインスタンスをセットしておく使い方が想定されていると思う。
- NodeManagerのコンストラクタで
メソッドとイベントと変更通知
- メソッドを実装する場合、 *.Model.xmlで指定した
<opc:InputArguments>
や<opc:OutputArguments>
に合わせてデリゲートが生成されるのでそれを実装する。出力引数はref
で渡されてくるのでそこへ書き込む。(TestDataフォルダのMethodTestState
クラス参照) - イベントと変更通知は別物。単なるノードの変更通知をしたいのであればイベントを実装する必要はない。(仕様書Part4-5.13.1)
- 変更通知は
NodeStates.ClearChangeMasks()
で行う。メソッド名からはクリアするだけに見えるので注意。これはNodeStates.ChangeMasks
プロパティ(そのノードのどこが変わったかを表す)がNodeStateChangeMasks.None
でなければ(前回のClearChangeMasks()
以降どこかが変わっていれば)ノードを購読しているクライアントへ変更通知してからChangeMasks
をクリアする。例えばVariableノードのValueの値を変更してからClearChangeMasks()
を呼ぶと、購読しているクライアントに変更通知が行く。 - イベントを実装する場合、 *.Model.xmlではイベントを持つ(
<opc:Object ... SupportsEvents="true">
)とすることでノードが購読可能であることを表し、イベントは任意のタイミングでNodeState.ReportEvent()
を呼んで発生させる。イベント(C#でいうイベント引数)もノードでありBaseEventState
クラスの派生型として表現される。 - イベントは、発生させたノードの親ノードを購読していても届くよう再帰的に発生する(
BaseInstanceState.ReportEvent()
)。子ノードのイベントは親ノードのイベントのサブセットである。(仕様書Part3-4.6.1)
セキュリティ
- Config.xmlのApplicationCertificateにサーバのX.509証明書の設定があり、サンプル実装の場合certmgr.mscの個人>証明書の中に保管される。設定変更が必要なのはSubjectNameで、CNに製品名、Cに国名、Oに組織名を指定すればよいだろう。DCはlocalhostのままでよく、起動時にホスト名に置き換えられる(
Utils.ReplaceDCLocalhost()
)。 -
ApplicationInstance.CheckApplicationInstanceCertificate()
でサーバの証明書の検証が行われ、無い場合は作成される。第3引数で証明書を作成する場合の有効期限を指定できる。サンプルではデフォルトの12か月になっている。実質無期限にしたければushort.MaxValue
(65535)を指定すると5461年先になる。- 自己署名証明書であるため一般になりすましのリスクがあるが、OPC UAでは通常サーバとクライアントがお互いを知っていると思われ、サンプルの通りのTrust On First Use (TOFU) で十分であれば問題ない。つまりセットアップ時にクライアントはサーバのIPアドレスを指定して接続し、受け取ったサーバの証明書を手作業で信頼リストへ入れ、またサーバもクライアントの証明書を手作業で信頼リストへ入れる。万一クライアントが誤ったIPアドレスを指定したりなりすましが存在したとしても、拒否リストにあるはずの証明書が無かったり複数あったりして気が付くことができる。
- クライアント接続時のセッション開始時には
CertificateValidator.Validate()
でクライアントの証明書の検証が行われる。検証は2段階で、ライブラリのコア実装にてまず証明書が有効なものか・trustedへ登録されているかの検証の後、アプリケーション側で実装したCertificateValidator.CertificateValidation
イベントのイベントハンドラにて追加の検証が可能。- Config.xmlでAutoAcceptUntrustedCertificates=trueが指定されている場合、結果が
StatusCodes.BadCertificateUntrusted
でも強制的にAcceptされる。 - サンプルの実装では起動時引数で
-a
が指定されている場合、結果がStatusCodes.BadCertificateUntrusted
でもUAServer.CertificateValidator_CertificateValidation()
で強制的にAcceptしている。 - これらの設定は当然テスト目的であり、正式運用時にはまずクライアントから一度接続試行して、rejectedへ保存されたクライアントの証明書をtrustedへカット&ペーストする。
- 参考:X.509証明書の検証手順とありがちな脆弱性
- Config.xmlでAutoAcceptUntrustedCertificates=trueが指定されている場合、結果が
情報モデルの設計についての補足
C#erが混乱しがちな用語
- Attribute
- C#のAttributeとはまったく違う。ノードの内容のこと。21種類定められていて、ノードクラスごとに必須のAttribute、オプションのAttributeが定められている。独自に定義したりするものではない。(OPC UA Node Classes)(仕様書Part3-4.3.3)
- Property
- C#のプロパティとはまったく違う。ノードの特性のことで、C#だと各Node Class型の属性として宣言されたり不変条件としてコンストラクタ内に実装されるようなもの。単位・上下限値・最終更新日時等。(仕様書Part3-4.4.2)
- Event
- C#では変更通知はイベントの1つとして表現する(PropertyChangedイベントなど)が、OPC UAではイベントと変更通知は別物である。
以下のマッピングで考えると良さそう。
型
C# | OPC UA |
---|---|
プリミティブ型 | ビルトイン定義済み(仕様書 Part3-8) |
クラス | ObjectTypeノード |
構造体・値オブジェクト | DataTypeノード |
インスタンス・メンバ
C# | OPC UA |
---|---|
クラス | Objectノード |
プリミティブ型・構造体・値オブジェクト | Variableノード |
配列 | VariableノードのValueRankで指定(仕様書Part3-5.6.2) |
メソッド | Methodノード |
イベント | ObjectノードのEventNotifier=1(SubscribeToEvents)(仕様書Part3-5.5.1) |
クラスのインスタンスに相当するObjectノードの子として、プロパティ・メソッドのノードをぶら下げる形になる。プロパティの型がクラスならそれもObjectノードとなり、末端がプリミティブ型のVariableノードになる。サーバは、インスタンス化したノードをツリー状に構成し、クライアントに対して公開する。
RESTful Web APIおよびgRPCですでに用意されているAPIがあるとして、それとの対応を考えてみる。
RESTful Web APIでは、URLでツリー状に表されたリソースに対してHTTP GET/PUT/DELETE/POSTでアクセスする。OPC UAにおいてもツリー状にオブジェクトを表現するしクライアントにノードの操作(追加・削除・変更)をさせることも可能なので親和性は高いと思いきや、実は大きく違う点がある。RESTful Web APIではURLに相当するリソースはサーバプロセスのメモリ上に存在する必要はなく、例えばGETされたときにDBからクエリして返すことができる。一方でOPC UAではすべてのノードはメモリ上に存在していなければならない。扱うリソースの量が限定的であったり、もともとサーバプロセスのメモリ上にあるなら同等に表現して問題ないだろうが、そうでなければRPC的な表現に置き換える必要がある。
gRPCでは、サーバが公開しているメソッドをクライアントが呼び出したり、あるいはストリームとしてデータ・イベントを連続的に送受信する。
- Unary RPCはMethodノードに対応するが、しかしRPCの静的リソースへアクセスするメソッドもそのままMethodノードに対応させてしまうと違和感があるかもしれないので、静的リソースについてはObject/Variableノードにすべきでないかも考慮したい。
- Server stream RPCはイベントで表現できそうな気もするが、仕様次第で設計が変わるので定型的に対応させることはできなさそうだ。protoc-gen-opcuaではstreamを無視している。
- Client stream RPCはVariableノードに対する書き込みとして表現できる場合もあるかもしれないが、クライアント側でもOPC UAサーバを立てるか、諦めてUnary RPCに置き換える方が良さそうだ。
また公式サンプルには、メモリバッファを公開しておいてそこへの書き込みをモニタリングする実装もあるが、そういったOPC UAの仕様を活用したPLC的な設計を入れてしまうと、他の方式のAPIを用意したくなった場合に面倒なことになりそうなので、シンプルなC#らしい設計にしておくのが無難だろう。