今さら OPC DA で接続してみる


OPC クライアントのアプリケーション(OPC DA Classic)を C#で作成した時のメモです。
OPC Server に [接続]して [タグ値の読込] [タグへの値書込]と[クローズ] をおこないました。
出来る限り,C#コードの説明を中心に,関連事項は簡素化してます。

##準備したもの
 **OPC Server と PLC**
 PLCをOPC ServerにEthernetで接続しタグの現在値を表示できるようにします。
・OPC Server: たけびしDeviceXPlore v6.4.0.1(x86)デモ版をインストール。(1時間動きます)
・PLC: 三菱電機 FX5UCPU。
・GX Works3 でPLCの内蔵Ethernetポートを設定します。
 (GX Works3 Version1 1.066U を使いました)
・OPC Serverを操作して PLCとの接続, 数点のタグを登録します。
 (OPC Server と GX Works3 の操作は,説明書を参照しました)
・OPC Serverをインストールすると同時にインストールされるOPC Clientを使ってタグの値の
 読込/書込の操作を行っておくとアプリケーション作成の理解に手助けとなります。


**C#**
・Visual Studio 2017 で 新規プロジェクト
 [Windows ﾌｫｰﾑｱﾌﾟﾘｹｰｼｮﾝ(.NET Framework) Visual C#] を作成しました。
・[プロジェクト] -> [参照の追加] を選択して表示される [参照マネージャー] から 
 [タイプライブラリ]を選択し[OPC DA Automation Wrapper] にチェックを入れて[OK]します。
 (VS2019では [OPC DA Automation Wrapper]は表示されませんでした。)
・Form1のコード Form1.cs を開きます。 using OPCAutomation を追加します。
 オブジェクトブラウザーでOPCAutomationが確認できます。

```c#
using System;
using System.xxxxxxx;
 //
 //
using OPCAutomation;
```

･OPC ServerをインストールしたPCとC#で作成のOPC Client アプリケーションは同じPCで
起動してください。別のPCで起動する場合,接続のための設定が必要となります。

##参考コード##
(ひとつの例に過ぎません。間違いがあったらお知らせ頂くと有り難いです)
**OPCServer object を 宣言**

```cs
// OPC Server object
public OPCServer ConnectedOPCServer;
// OPCGroups object
public OPCGroup ConnectedGroup;

```

引数にOPC Server の情報を入れて,

```
sProgID="Takebishi.Dxp.6"; OPC ServerのID 
sNode="XXX.XXX.XXX.XXX"; ipアドレス

```

**さて, OPC Server へ Connect で [接続]...。**

```c#
public bool StartOpc(string sProgID, string sNode) {
   sErrMsg = "";
   try {
      ConnectedOPCServer = new OPCServer();
      ConnectedOPCServer.Connect(sProgID, sNode);
   }
   catch (Exception ex) {
      sErrMsg = ex.Message;
      return true;
   }
   return false;
}
```

**グループ名のAdd**
次にグループ名をAddします。そして,OPC Serverに既に登録されているタグ名をAdd します。

･引数 sIOGroup="Group1"; として OPCGroups.Add(sIOGroup) しました。
 グループ名は,識別できる文字列を設定します。

```c# 
public bool AddGroupOPC(string sIOGroup) {
   sErrMsg = "";
   try {
      ConnectedGroup = ConnectedOPCServer.OPCGroups.Add(sIOGroup);
      ConnectedGroup.UpdateRate = 1000;
   }
   catch (Exception ex) {
      sErrMsg = ex.Message;
      return true;
   }
   return false;
}
```

**TAG名称のAdd**
･OPCItems.AddItems には配列(OPCItemIDs)でTAG名を渡します。
 この他にもパラメータとして渡す配列が必要です。(全部で6コ)
 また,これらの配列は基数が[1]から処理されます。配列に値を設定する場合,
 その添字には注意が必要です。
･OPC Serverに,タグを3つ登録し名称を, [1運転], [2停止], [3出力]としました。
 それぞれ PLCレジスタは M0, M1, M100 に設定しています。
 一方,PLC側に M0, M1, M100で自己保持回路をセットし,M100 の接点を Y0 に接続しています。

･配列の宣言,割付です。タグの数はiNumOfItem=4として設定します。 

```c#
public int iNumOfItem { get; set; }

public Array OPCItemIDs;
public Array ClientHandles;
public Array ItemServerHandles;
public Array ItemServerErrors;
public Array RequestedDataTypes;
public Array AccessPaths;
// ・
// ・

OPCItemIDs = Array.CreateInstance(typeof(string), iNumOfItem);
ClientHandles = Array.CreateInstance(typeof(Int32), iNumOfItem);
ItemServerHandles = Array.CreateInstance(typeof(Int32), iNumOfItem);
ItemServerErrors = Array.CreateInstance(typeof(Int32), iNumOfItem);
RequestedDataTypes = Array.CreateInstance(typeof(Int16), iNumOfItem);
AccessPaths = Array.CreateInstance(typeof(string), iNumOfItem);
// ・
// ・
```

･OPCItemIDsにタグ名称を設定します。配列[1]からの扱いとなります。タグ名称には
 [Device1.]を付加しておきます。([Device1.]は,OPC Serverに依る文字列ですが設定も可能です)

```c#
OPCItemIDs.SetValue("Device1.1運転", 1);
OPCItemIDs.SetValue("Device1.2停止", 2);
OPCItemIDs.SetValue("Device1.3出力", 3); 
```

･それでは,OPCItems.AddItems にパラメータを渡します。

```c#
public bool AddItemOPC() {
   sErrMsg = "";
   try {
      ConnectedGroup.OPCItems.DefaultIsActive = true;
      ConnectedGroup.OPCItems.AddItems(iNumOfItem - 1, ref OPCItemIDs, ref ClientHandles, out ItemServerHandles, out ItemServerErrors, RequestedDataTypes, AccessPaths);
    }
    catch (Exception ex) {
       sErrMsg = ex.Message;
       return true;
    }
    return false;
}
```

**タグへの値書込と読込**
･これには同期と非同期の2つの方式があります。今回は,同期の方式のみを扱います。
 非同期はデリゲートを設定して動作完了時に呼び出しされることになります。
 次回の機会に扱うことにします。
･配列に値を代入して引数としますので宣言, 割付けします。

```c#
public Array aryValues;
public Array aryErrors;
// ・
// ・

aryValues = Array.CreateInstance(typeof(object), iNumOfItem);
aryErrors = null;
// ・
// ・
```

**同期書込**
･引数:タグのID(sIdItem)="1",書込値(sValWrt)="1"で書込むとタグ名称[1運転]の値が[ON]
 となります。PLCのラダーが処理して,[3出力]:ON, PLCの[Y0]:ON となります。
 すると同時に,PLC本体前面の[Y0]位置LEDが点灯するはずです。同様に,
 引数:タグのID(sIdItem)="2",書込値(sValWrt)="1"で書込むとタグ名称[2停止]の値が[ON]
 となります。PLCのラダーが処理して[3出力]:OFF,PLCの[Y0]:OFFとなります。
 今度は,PLC本体前面の[Y0]位置LEDが消灯するはずです。

･実際に,sIdItem="1", sValWrt="1" としてSyncWrt_OPCに引数を設定したところ,
 PLCの[Y0]位置のLEDが点灯しました。

```c#
public void SyncWrt_OPC(string sIdItem, string sValWrt) {
   sErrMsg = "";
   int iIdItem = Convert.ToInt32(sIdItem);
   Array.Clear(aryValues, 0, aryValues.Length);
   aryValues.SetValue(sValWrt, iIdItem);
   try {
      ConnectedGroup.SyncWrite(iIdItem, ref ItemServerHandles, ref aryValues, out aryErrors);
   }
   catch (Exception ex) {
      sErrMsg = ex.Message;
   }
}
```

･次に,sIdItem="2", sValWrt="1" で試ました。PLCの[Y0]位置のLEDが消灯しました。

**同期読込**
･引数:タグのID(sIdItem)="1" とすれば RdValueにはタグの値, shQualitiesに品質, 
 dtTimeStampにタイムスタンプが入ってきます。 

```c#
public bool SyncRead_OPC(string sIdItem, ref string RdValue, ref short shQualities, ref DateTime dtTimeStamp) {
   sErrMsg = "";
   int iIdItem = Convert.ToInt32(sIdItem);           
   System.Array aryRdValues;
   System.Array aryErrors;
   object  obQualities = new object();
   object  obTimeStamp = new object();
   int iNumItems = 1;
   try {
      Array SyncServerHandles = Array.CreateInstance(typeof(Int32), iNumOfItem);
      SyncServerHandles.SetValue(ItemServerHandles.GetValue(iIdItem), 1);                 // SyncServerHandles[1]に書込む
      ConnectedGroup.SyncRead((short)OPCDataSource.OPCDevice, iNumItems, ref SyncServerHandles, out aryRdValues, out aryErrors, out obQualities, out obTimeStamp);
      var RdError = aryErrors.GetValue(1);
      bool bErr = Object.Equals(RdError, 0);
      if (!bErr) {
         return true;
      }
      var vdRdValue = aryRdValues.GetValue(1);
      RdValue = Convert.ToString (vdRdValue);
      var aryQualities = (Array)obQualities;
      shQualities = (short)aryQualities.GetValue(1);
      var aryTimeStamp = (Array)obTimeStamp;
      dtTimeStamp = (DateTime)aryTimeStamp.GetValue(1);
   }
   catch (Exception ex) {            
      sErrMsg = ex.Message;
      return true;
   }
   return false;
}
```

**クローズ処理**
･引数 sGrpName="Group1"として OPCItemsをRemove, OPCGroupsをRemovesして Disconnect()します。

```c#
public bool FinishOpc(string sGrpName) {
   sErrMsg = "";
   try {
      if (ConnectedGroup != null) {
         ConnectedGroup.OPCItems.Remove(iNumOfItem - 1, ref ItemServerHandles, out aryErrors);
         ConnectedOPCServer.OPCGroups.Remove(sGrpName);
         ConnectedGroup.IsSubscribed = false;
       }
   ConnectedOPCServer.Disconnect();
   }
   catch (Exception ex) {
      sErrMsg = ex.Message;
      return false;
   }
   return true;
}
```

**終わり**
･OPC Classic で 単純な[接続],[読込],[書込],[クローズ]を行ってみました。  

**OPC Server のタグの登録するにあたって(余談)**
･OPC Serverにタグを登録する際,PLCの説明書でPLCデバイスの種類と目的を確認
 してください。
 OPC Serverのタグ登録ではPLCデバイス種類を X,Y,M,B... と簡単に選択できますが,PLC側では
 デバイスの種類は用途によって大きく異なります。例えば, PLC:FX5UCPUのデバイス種類
 の[Y]の目的は出力です。OPCタグを[Y]に割付けるとOPCタグ値の1/0 がPLCの出力端子に
 ON/OFFとしてそのまま出力されます。
 (最終の出力はIOリフレッシュがあるのでPLC側プログラムに依ります)
 PLCが設置される"XX盤"ではOPCのタグといえども外部信号には違いありません。外部信号は
 端子台からPLCへ結線されるものです。ところが結線は不可能なので,OPCタグの登録には
 慎重さが必要となります。OPCタグをいきなり[Y]に割付したりして... 
 たとえが古いですが,操作盤の[遠方]<->[手元]のセレクタを[遠方]にした途端にマグネットが
 どうなるか... 推して知るべし です。

##参照した資料
https://www.codeproject.com/Articles/490072/DA-1-OPC-Wrapper-DLL-and-Client-Example
Data Access Automation Interface Standard Version 2.02 February 4, 1999