LoginSignup
3
2

More than 1 year has passed since last update.

OPC UAサーバをC#で実装する

Last updated at Posted at 2022-12-18

ガイド

OPC UAの概要

動かして全体を理解する

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に含まれるものなので自分で書く部分ではない。

詳細を理解するための資料

独自の情報モデル(コンパニオン仕様)の構築と実装

UA-ModelCompiler (1) (1).png

独自のAPIをOPC UAで公開するには、独自の情報モデル(コンパニオン仕様)を作ることになる。APIの情報モデルをXML(*.Model.xml)に手打ちして、Opc.Ua.ModelCompiler.exeに渡して必要なファイル群を生成し、サーバアプリに組み込んだりクライアントへ配布する。

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.batPublishModel.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を見て実際にどう実装しているか理解する

その後(リリースに向けて)

ソースコード解説

ノードと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から取り出して使うことができる。処理を実行するのに必要になる、アプリケーション側のインスタンスをセットしておく使い方が想定されていると思う。

メソッドとイベントと変更通知

  • メソッドを実装する場合、 *.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証明書の検証手順とありがちな脆弱性

情報モデルの設計についての補足

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#らしい設計にしておくのが無難だろう。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2