【干货】如何通过OPC自定义接口来实现客户端数据的读取?

时间:2023-03-09 09:00:20
【干货】如何通过OPC自定义接口来实现客户端数据的读取?

  上篇博文分享了我的知识库,被好多人关注,受宠若惊。今天我把我在项目中封装的OPC自定义接口的程序分享一下。下面将会简单简单介绍下OPC DA客户端数据访问,以及搭配整个系统的运行环境。

  • OPC(OLE for Process Control)其实就是一套标准,我对这套标准理解不多,使用过程中就把它理解一套协议或者规范,主要用于工控领域。OPC中有很多规范,我主要使用OPC DA规范来进行数据的读写操作。还有其他规范,比如OPC UA、OPC HDA等。如果你做的是OPC Server开发查下这方面的资料了解下,这篇博文主要介绍OPC Client开发的知识。

  使用OPC DA进行Client的读写操作时,我们使用Custom接口,出此之外还有Automation接口。以下是Custome接口开发时涉及到的三个关键对象:OpcServer、OpcGroup、OpcItem,下图是他们之间的逻辑关系:

  【干货】如何通过OPC自定义接口来实现客户端数据的读取?

  在客户端开发时,要使用OpcServer对象来实现客户端与Opc服务器之间的连接。一个OpcServer对象下有多个OpcGroup,一个OpcGroup下有多个OpcItem,在自定义接口下的Client开发,是以Group为单位的操作,数据读写都是通过OpcGroup进行的。

  •   搭建程序运行环境

    程序运行需要的软硬件环境:

    1. .Net Framework 4.0
    2. Simatic Net 2008(Or Other) HF1
    3. 西门子300(Or Other) PLC

    我们可以通过本机的配置来实现OPC的远程连接,我没有采用这种方式,一是这种配置比较麻烦,而是这种方式不稳定。所以我采用本机安装一个OPCServer来实现与PLC的交互。

    对于OPCServer软件,我选择的是SimaticNet 2008 HF1(安装WinCC的时候会有选择安装SimaticNet的选项),没有特别的原因,就是比较熟悉了而已,而且PLC选用的是西门子的。

    我们可以不写OPC Client程序来测试,如何通过OPCServer与PLC之间的交互。首先当我们安装完毕SimaticNet之后,需要对Station Configuration Editor进行配置,如下图:

    【干货】如何通过OPC自定义接口来实现客户端数据的读取?

    首先我们要指定Station的名称,上图叫PCStation,点击下方的StationName可以进行更改。下一步在1号栈上选择一个OPCServer,3号栈上选择一个通信网卡。

    接下来我们需要在Step 7中建立Station Configuration Editor与PLC之间的连接,我们暂且叫组态。组态的过程中要建立与Station Configuration Editor中对应的Opc Server和IE General(所在栈号相同),Station Configuration Edition起到桥接的作用    用,主要让PLC与Opc Server之间建立一条S7连接。暂时没有拿到组态图,以后补上。

    当我们组态完毕时,如何判断组态是否正确呢?在SimaticNet的目录上有个叫Opc Scout(Opc Scout V10)的软件,打开如下图:

    【干货】如何通过OPC自定义接口来实现客户端数据的读取?

    上图列出来了本机所有的Server,我们能使用名为OPC.SimaticNET的Server。双击这个Server添加一个组,多次双击这个Server可以添加多个组,验证了上图的Server与Group的关系了。

    我们双击新建的Group,进入如下图的界面:

    【干货】如何通过OPC自定义接口来实现客户端数据的读取?

    上图列出了所有的连接。上文说到的组态中建立的S7连接可以在S7节点中看到,展开这个节点可以看到我们建立的S7连接,如下图:

    【干货】如何通过OPC自定义接口来实现客户端数据的读取?

    上图列出了名为S7 connection_1的S7连接,展开Object对象,列出PLC的结构。我们选择一种来新建我们的Item,由于我这里没有PLC模块,所以无法截图给大家看。

    至此我们的OPC Client的运行环境搭建完毕。

  •  编写OPC Client端程序。

    我们需要使用OPC Foundation提供的自定义接口来进行开发,在Visual Studio引用名为:OpcRcw.Comn.dll和OpcRcw.Da.dll这两个DLL。

    我们定义一个名为OpcDaCustomAsync的类,让这个类继承自:IOPCDataCallback,IDisposable

    

 using System;
using System.Collections.Generic;
using OpcRcw.Comn;
using OpcRcw.Da;
using System.Runtime.InteropServices; namespace Opc.Net
{
/// <summary>
/// Opc自定义接口-异步管理类
/// <author name="lm" date="2012.3.14"/>
/// </summary>
public class OpcDaCustomAsync : IOPCDataCallback,IDisposable
{
/// <summary>
/// OPC服务器对象
/// </summary>
IOPCServer iOpcServer;
/// <summary>
/// 事务ID
/// </summary>
int transactionID;
/// <summary>
/// OPC服务器名称
/// </summary>
string opcServerName;
/// <summary>
/// OPC服务器IP地址
/// </summary>
IOPCAsyncIO2 _iopcAsyncIo2;
/// <summary>
/// OPC服务器IP地址
/// </summary>
string opcServerIPAddress;
/// <summary>
/// Opc组列表
/// </summary>
List<OpcDaCustomGroup> opcDaCustomGroups;
/// <summary>
/// 连接指针容器
/// </summary>
IConnectionPointContainer IConnectionPointContainer = null;
/// <summary>
/// 连接指针
/// </summary>
IConnectionPoint IConnectionPoint = null;
/// <summary>
/// Opc组管理器
/// </summary>
IOPCGroupStateMgt IOPCGroupStateMgt = null; //接收数据事件
public event EventHandler<OpcDaCustomAsyncEventArgs> OnDataChanged;
/// <summary>
/// 异步写入数据完成事件
/// </summary>
public event EventHandler<OpcDaCustomAsyncEventArgs> OnWriteCompleted;
/// <summary>
/// 异步读取数据完成事件
/// </summary>
public event EventHandler<OpcDaCustomAsyncEventArgs> OnReadCompleted; /// <summary>
/// 构造函数
/// </summary>
/// <param name="opcDaCustomGroups">Opc组列表</param>
/// <param name="opcServerName">OPC服务器名称</param>
/// <param name="opcServerIpAddress">OPC服务器IP地址</param>
public OpcDaCustomAsync(List<OpcDaCustomGroup> opcDaCustomGroups, string opcServerName, string opcServerIpAddress)
{
this.opcDaCustomGroups = opcDaCustomGroups;
this.opcServerName = opcServerName;
this.opcServerIPAddress = opcServerIpAddress;
Init();
}
/// <summary>
/// 初始化参数
/// </summary>
public void Init()
{
if (Connect())
{
AddOpcGroup();
}
} /// <summary>
/// 连接Opc服务器
/// </summary>
/// <returns></returns>
public bool Connect()
{
return Connect(opcServerName, opcServerIPAddress);
}
/// <summary>
/// 连接Opc服务器
/// </summary>
/// <returns></returns>
public bool Connect(string remoteOpcServerName, string remoteOpcServerIpAddress)
{
var returnValue = false;
if (!string.IsNullOrEmpty(remoteOpcServerIpAddress) && !string.IsNullOrEmpty(remoteOpcServerName))
{
var opcServerType = Type.GetTypeFromProgID(remoteOpcServerName, remoteOpcServerIpAddress);
if (opcServerType != null)
{
iOpcServer = (IOPCServer)Activator.CreateInstance(opcServerType);
returnValue = true;
}
}
return returnValue;
}
/// <summary>
/// 添加Opc组
/// </summary>
private void AddOpcGroup()
{
try
{
foreach (OpcDaCustomGroup opcGroup in opcDaCustomGroups)
{
AddOpcGroup(opcGroup);
}
}
catch(COMException ex)
{
throw ex;
}
}
/// <summary>
/// 添加Opc项
/// </summary>
/// <param name="opcGroup"></param>
private void AddOpcGroup(OpcDaCustomGroup opcGroup)
{
try
{ //添加OPC组
iOpcServer.AddGroup(opcGroup.GroupName, opcGroup.IsActive, opcGroup.RequestedUpdateRate, opcGroup.ClientGroupHandle, opcGroup.TimeBias.AddrOfPinnedObject(), opcGroup.PercendDeadBand.AddrOfPinnedObject(), opcGroup.LCID, out opcGroup.ServerGroupHandle, out opcGroup.RevisedUpdateRate, ref opcGroup.Riid, out opcGroup.Group);
InitIoInterfaces(opcGroup);
if (opcGroup.OpcDataCustomItems.Length > )
{
//添加OPC项
AddOpcItem(opcGroup);
//激活订阅回调事件
ActiveDataChanged(IOPCGroupStateMgt);
}
}
catch (COMException ex)
{
throw ex;
}
finally
{
if (opcGroup.TimeBias.IsAllocated)
{
opcGroup.TimeBias.Free();
}
if (opcGroup.PercendDeadBand.IsAllocated)
{
opcGroup.PercendDeadBand.Free();
}
}
}
/// <summary>
/// 初始化IO接口
/// </summary>
/// <param name="opcGroup"></param>
public void InitIoInterfaces(OpcDaCustomGroup opcGroup)
{
int cookie;
//组状态管理对象,改变组的刷新率和激活状态
IOPCGroupStateMgt = (IOPCGroupStateMgt)opcGroup.Group;
IConnectionPointContainer = (IConnectionPointContainer)opcGroup.Group;
Guid iid = typeof(IOPCDataCallback).GUID;
IConnectionPointContainer.FindConnectionPoint(ref iid, out IConnectionPoint);
//创建客户端与服务端之间的连接
IConnectionPoint.Advise(this, out
cookie);
}
/// <summary>
/// 激活订阅回调事件
/// </summary>
private void ActiveDataChanged(IOPCGroupStateMgt IOPCGroupStateMgt)
{
IntPtr pRequestedUpdateRate = IntPtr.Zero;
IntPtr hClientGroup = IntPtr.Zero;
IntPtr pTimeBias = IntPtr.Zero;
IntPtr pDeadband = IntPtr.Zero;
IntPtr pLCID = IntPtr.Zero;
int nActive = ;
GCHandle hActive = GCHandle.Alloc(nActive, GCHandleType.Pinned);
try
{
hActive.Target = ;
int nRevUpdateRate = ;
IOPCGroupStateMgt.SetState(pRequestedUpdateRate, out nRevUpdateRate,
hActive.AddrOfPinnedObject(), pTimeBias, pDeadband, pLCID, hClientGroup);
}
catch (COMException ex)
{
throw ex;
}
finally
{
hActive.Free();
}
} /// <summary>
/// 添加Opc项
/// </summary>
/// <param name="opcGroup"></param>
private void AddOpcItem(OpcDaCustomGroup opcGroup)
{
OpcDaCustomItem[] opcDataCustomItemsService = opcGroup.OpcDataCustomItems;
IntPtr pResults = IntPtr.Zero;
IntPtr pErrors = IntPtr.Zero;
OPCITEMDEF[] itemDefyArray = new OPCITEMDEF[opcGroup.OpcDataCustomItems.Length];
int i = ;
int[] errors = new int[opcGroup.OpcDataCustomItems.Length];
int[] itemServerHandle = new int[opcGroup.OpcDataCustomItems.Length];
try
{
foreach (OpcDaCustomItem itemService in opcDataCustomItemsService)
{
if (itemService != null)
{
itemDefyArray[i].szAccessPath = itemService.AccessPath;
itemDefyArray[i].szItemID = itemService.ItemID;
itemDefyArray[i].bActive = itemService.IsActive;
itemDefyArray[i].hClient = itemService.ClientHandle;
itemDefyArray[i].dwBlobSize = itemService.BlobSize;
itemDefyArray[i].pBlob = itemService.Blob;
itemDefyArray[i].vtRequestedDataType = itemService.RequestedDataType;
i++;
} }
//添加OPC项组
((IOPCItemMgt)opcGroup.Group).AddItems(opcGroup.OpcDataCustomItems.Length, itemDefyArray, out pResults, out pErrors);
IntPtr Pos = pResults;
Marshal.Copy(pErrors, errors, , opcGroup.OpcDataCustomItems.Length);
for (int j = ; j < opcGroup.OpcDataCustomItems.Length; j++)
{
if (errors[j] == )
{
if (j != )
{
Pos = new IntPtr(Pos.ToInt32() + Marshal.SizeOf(typeof(OPCITEMRESULT)));
}
var result = (OPCITEMRESULT)Marshal.PtrToStructure(Pos, typeof(OPCITEMRESULT));
itemServerHandle[j] = opcDataCustomItemsService[j].ServerHandle = result.hServer;
Marshal.DestroyStructure(Pos, typeof(OPCITEMRESULT));
}
}
}
catch (COMException ex)
{
throw ex;
}
finally
{
if (pResults != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(pResults);
}
if (pErrors != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(pErrors);
}
}
}
/// <summary>
/// 异步读取信息
/// </summary>
public void Read()
{
foreach (OpcDaCustomGroup opcGroup in opcDaCustomGroups)
{
IntPtr pErrors = IntPtr.Zero;
try
{
_iopcAsyncIo2 = (IOPCAsyncIO2)opcGroup.Group;
if (_iopcAsyncIo2 != null)
{
int[] serverHandle = new int[opcGroup.OpcDataCustomItems.Length];
opcGroup.PErrors = new int[opcGroup.OpcDataCustomItems.Length];
for (int j = ; j < opcGroup.OpcDataCustomItems.Length; j++)
{
serverHandle[j] = opcGroup.OpcDataCustomItems[j].ServerHandle;
}
int cancelId=;
_iopcAsyncIo2.Read(opcGroup.OpcDataCustomItems.Length, serverHandle, , out cancelId, out pErrors);
Marshal.Copy(pErrors, opcGroup.PErrors, , opcGroup.OpcDataCustomItems.Length);
}
}
catch (COMException ex)
{
throw ex;
}
finally
{
if (pErrors != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(pErrors);
}
}
}
} /// <summary>
/// 异步写入数据
/// </summary>
/// <param name="values">要写入的值</param>
/// <param name="serverHandle">要写入的项的服务器句柄</param>
/// <param name="errors">错误信息,等于表示写入成功,否则写入失败</param>
/// <param name="opcGroup">要写入的Opc组</param>
public void Write(object[] values,int[] serverHandle,out int[] errors,OpcDaCustomGroup opcGroup)
{
_iopcAsyncIo2 = (IOPCAsyncIO2)opcGroup.Group;
IntPtr pErrors = IntPtr.Zero;
errors = new int[values.Length];
if (_iopcAsyncIo2 != null)
{
try
{
//异步写入数据
int cancelId;
_iopcAsyncIo2.Write(values.Length, serverHandle, values, transactionID + , out cancelId, out pErrors);
Marshal.Copy(pErrors, errors, , values.Length);
}
catch (COMException ex)
{
throw ex;
}
finally
{
if (pErrors != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(pErrors);
}
}
}
}
/// <summary>
/// 数据订阅事件
/// </summary>
/// <param name="dwTransid"></param>
/// <param name="hGroup"></param>
/// <param name="hrMasterquality"></param>
/// <param name="hrMastererror"></param>
/// <param name="dwCount"></param>
/// <param name="phClientItems"></param>
/// <param name="pvValues"></param>
/// <param name="pwQualities"></param>
/// <param name="pftTimeStamps"></param>
/// <param name="pErrors"></param>
public virtual void OnDataChange(Int32 dwTransid,
Int32 hGroup,
Int32 hrMasterquality,
Int32 hrMastererror,
Int32 dwCount,
int[] phClientItems,
object[] pvValues,
short[] pwQualities,
OpcRcw.Da.FILETIME[] pftTimeStamps,
int[] pErrors) {
var e = new OpcDaCustomAsyncEventArgs
{
GroupHandle = hGroup,
Count = dwCount,
Errors = pErrors,
Values = pvValues,
ClientItemsHandle = phClientItems
};
if (OnDataChanged != null)
{
OnDataChanged(this, e);
}
} /// <summary>
/// 取消事件
/// </summary>
/// <param name="dwTransid"></param>
/// <param name="hGroup"></param>
public virtual void OnCancelComplete(Int32 dwTransid, Int32 hGroup)
{ } /// <summary>
/// 写入数据完成事件
/// </summary>
/// <param name="dwTransid"></param>
/// <param name="hGroup"></param>
/// <param name="hrMastererr"></param>
/// <param name="dwCount"></param>
/// <param name="pClienthandles"></param>
/// <param name="pErrors"></param>
public virtual void OnWriteComplete(Int32 dwTransid,
Int32 hGroup,
Int32 hrMastererr,
Int32 dwCount,
int[] pClienthandles,
int[] pErrors)
{
if (OnWriteCompleted != null)
{
var e = new OpcDaCustomAsyncEventArgs
{
Errors = pErrors
};
if (OnWriteCompleted != null)
{
OnWriteCompleted(this, e);
}
}
}
/// <summary>
/// 读取数据完成事件
/// </summary>
/// <param name="dwTransid"></param>
/// <param name="hGroup"></param>
/// <param name="hrMasterquality"></param>
/// <param name="hrMastererror"></param>
/// <param name="dwCount">要读取的组的项的个数</param>
/// <param name="phClientItems"></param>
/// <param name="pvValues">项值列表</param>
/// <param name="pwQualities"></param>
/// <param name="pftTimeStamps"></param>
/// <param name="pErrors">项错误列表</param>
public virtual void OnReadComplete(Int32 dwTransid,
Int32 hGroup,
Int32 hrMasterquality,
Int32 hrMastererror,
Int32 dwCount,
int[] phClientItems,
object[] pvValues,
short[] pwQualities,
OpcRcw.Da.FILETIME[] pftTimeStamps,
int[] pErrors)
{
if (OnReadCompleted != null)
{
var e = new OpcDaCustomAsyncEventArgs
{
GroupHandle = hGroup,
Count = dwCount,
Errors = pErrors,
Values = pvValues,
ClientItemsHandle = phClientItems
};
OnReadCompleted(this, e);
}
}
public void Dispose()
{ }
}
}

我们看下IOPCDataCallback接口的定义:

【干货】如何通过OPC自定义接口来实现客户端数据的读取?

这个接口提供了4个函数。如果我们采用订阅模式(默认的模式),会执行OnDataChange函数,主动读数据则执行OnReadComplete函数,写数据则执行OnWriteComplete函数。在OpcDaCustomAsync类中,我已经对这四个函数进行了实现,每个实现对应一个事件。

OpcDaCustomAsync类的实现,我主要是扒了SimaticNet下的一个Sample,自己封装了下。使用这个类的时候需要提供Group列表和OPCServer的名称,以及OPCServer所在的主机的IP地址。

OpcGroup的封装:

 using System;
using System.Runtime.InteropServices;
using OpcRcw.Da; namespace Opc.Net
{
/// <summary>
/// 自定义接口OPC组对象
/// </summary>
public class OpcDaCustomGroup
{
private string groupName;
private int isActive=;
private int requestedUpdateRate;
private int clientGroupHandle=;
private GCHandle timeBias = GCHandle.Alloc(, GCHandleType.Pinned);
private GCHandle percendDeadBand = GCHandle.Alloc(, GCHandleType.Pinned);
private int lcid = 0x409;
private int itemCount;
private bool onRead; /// <summary>
/// 输出参数,服务器为新创建的组对象产生的句柄
/// </summary>
public int ServerGroupHandle; /// <summary>
/// 输出参数,服务器返回给客户端的实际使用的数据更新率
/// </summary>
public int RevisedUpdateRate; /// <summary>
/// 引用参数,客户端想要的组对象的接口类型(如 IIDIOPCItemMgt)
/// </summary>
public Guid Riid = typeof(IOPCItemMgt).GUID; /// <summary>
/// 输出参数,用来存储返回的接口指针。如果函数操作出现任务失败,此参数将返回NULL。
/// </summary>
public object Group;
private OpcDaCustomItem[] opcDataCustomItems; public int[] PErrors { get; set; } /// <summary>
/// 组对象是否激活
/// 1为激活,0为未激活,默认激活
/// </summary>
public int IsActive
{
get
{
return isActive;
}
set
{
if (isActive == value)
return;
isActive = value;
}
}
/// <summary>
/// 组是否采用异步读方式
/// </summary>
public bool OnRead
{
get
{
return onRead;
}
set
{
if (onRead == value)
return;
onRead = value;
}
}
/// <summary>
/// 项的个数
/// </summary>
public int ItemCount
{
get { return itemCount; }
set
{
if(itemCount == value)
return;
itemCount=value;
}
}
/// <summary>
/// 客户端指定的数据变化率
/// </summary>
public int RequestedUpdateRate
{
get
{
return requestedUpdateRate;
}
set
{
if (requestedUpdateRate == value)
return;
requestedUpdateRate = value;
}
} /// <summary>
/// OPC组名称
/// </summary>
public string GroupName
{
get
{
return groupName;
}
set
{
if (groupName == value)
return;
groupName = value;
}
} /// <summary>
/// 客户端程序为组对象提供的句柄
/// </summary>
public int ClientGroupHandle
{
get
{
return clientGroupHandle;
}
set
{
if (clientGroupHandle == value)
return;
clientGroupHandle = value;
}
} /// <summary>
/// 指向Long类型的指针
/// </summary>
public GCHandle TimeBias
{
get
{
return timeBias;
}
set
{
if (timeBias == value)
return;
timeBias = value;
}
} /// <summary>
/// 一个项对象的值变化的百分比,可能引发客户端程序的订阅回调。
/// 此参数只应用于组对象中有模拟dwEUType(工程单位)类型的项对象。指针为NULL表示0.0
/// </summary>
public GCHandle PercendDeadBand
{
get
{
return percendDeadBand;
}
set
{
if (percendDeadBand == value)
return;
percendDeadBand = value;
}
} /// <summary>
/// 当用于组对象上的操作的返回值为文本类型时,服务器使用的语言
/// </summary>
public int LCID
{
get
{
return lcid;
}
set
{
if (lcid == value)
return;
lcid = value;
}
} /// <summary>
/// OPC项数组
/// </summary>
public OpcDaCustomItem[] OpcDataCustomItems
{
get
{
return opcDataCustomItems;
}
set
{
if (opcDataCustomItems != null && opcDataCustomItems == value)
return;
opcDataCustomItems = value;
}
}
}
}

OpcItem的封装:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using OpcRcw.Da; namespace Opc.Net
{
/// <summary>
/// 自定义接口Opc项
/// </summary>
public class OpcDaCustomItem
{
private string name;
private string accessPath="";
private string itemID;
private int isActive = ;
private int clientHandle = ;
private int blobSize = ;
private IntPtr blob = IntPtr.Zero;
private short requestedDataType = ;
private object itemValue;
private int serverHandle; /// <summary>
/// 项名称
/// </summary>
public string Name
{
get
{
return name;
}
set
{
if (name == value)
return;
name = value;
}
}
/// <summary>
/// 项对象的访问路径
/// </summary>
public string AccessPath
{
get
{
return accessPath;
}
set
{
if (accessPath == value)
return;
accessPath = value;
}
} /// <summary>
/// 项对象的ItemIDea,唯一标识该数据项
/// </summary>
public string ItemID
{
get
{
return itemID;
}
set
{
if (itemID == value)
return;
itemID = value;
}
} /// <summary>
/// 项对象的激活状态
/// 1为激活,0为未激活,默认激活
/// </summary>
public int IsActive
{
get
{
return isActive;
}
set
{
if (isActive == value)
return;
isActive = value;
}
} /// <summary>
/// 项对象的客户端句柄
/// </summary>
public int ClientHandle
{
get
{
return clientHandle;
}
set
{
if (clientHandle == value)
return;
clientHandle = value;
}
}
public int BlobSize
{
get
{
return blobSize;
}
set
{
if (blobSize == value)
return;
blobSize = value;
}
}
public IntPtr Blob
{
get
{
return blob;
}
set
{
if (blob == value)
return;
blob = value;
}
} /// <summary>
/// OPC项的数据类型
/// VbBoolean:11,VbByte:17,VbDecimal:14,VbDouble:5,Vbinteger:2,VbLong:3,VbSingle:4,VbString:8
/// </summary>
public short RequestedDataType
{
get
{
return requestedDataType;
}
set
{
if (requestedDataType == value)
return;
requestedDataType = value;
}
} /// <summary>
/// OPC项的值
/// </summary>
public object Value
{
get
{
return itemValue;
}
set
{
if (itemValue == value)
return;
itemValue = value;
}
} /// <summary>
/// OPC项的服务器句柄
/// </summary>
public int ServerHandle
{
get
{
return serverHandle;
}
set
{
if (serverHandle == value)
return;
serverHandle = value;
}
}
}
}

项的客户端句柄和服务器句柄实际是一样的,项的数据类型用short表示,在下面的配置文件中体现出来了。

以下是我设计的配置文件:

 <?xml version="1.0" encoding="utf-8"?>
<System>
<OpcServer ServerName="OPC.SimaticNET" IPAddress="10.102.102.118">
<!--采煤机参数-->
<ShearerInfo GroupName="ShearerInfoGroup" ClientHandle="1" UpdateRate="100">
<!--左牵,1表示左牵,0表示未运动-->
<Item ItemID="S7:[S7 connection_2]DB201,X20.2" ClientHandle="1" RequestedDataType="11"></Item>
<!--右牵,1表示右牵,0表示未运动-->
<Item ItemID="S7:[S7 connection_2]DB201,X20.1" ClientHandle="2" RequestedDataType="11"></Item>
<!--牵引速度-->
<Item ItemID="S7:[S7 connection_2]DB201,REAL40" ClientHandle="3" RequestedDataType="5"></Item>
<!--采煤机位置-->
<Item ItemID="S7:[S7 connection_2]DB201,REAL44" ClientHandle="4" RequestedDataType="5"></Item>
<!--左滚筒高度-->
<Item ItemID="S7:[S7 connection_2]DB201,REAL48" ClientHandle="5" RequestedDataType="5"></Item>
<!--右滚筒高度-->
<Item ItemID="S7:[S7 connection_2]DB201,REAL52" ClientHandle="6" RequestedDataType="5"></Item>
<!--左截电流-->
<Item ItemID="S7:[S7 connection_2]DB201,INT6" ClientHandle="7" RequestedDataType="2"></Item>
<!--右截电流-->
<Item ItemID="S7:[S7 connection_2]DB201,INT8" ClientHandle="8" RequestedDataType="2"></Item>
<!--左牵电流-->
<Item ItemID="S7:[S7 connection_2]DB201,INT2" ClientHandle="9" RequestedDataType="2"></Item>
<!--右牵电流-->
<Item ItemID="S7:[S7 connection_2]DB201,INT4" ClientHandle="10" RequestedDataType="2"></Item>
<!--左截启-->
<Item ItemID="S7:[S7 connection_2]DB201,X20.6" ClientHandle="11" RequestedDataType="11"></Item>
<!--右截启-->
<Item ItemID="S7:[S7 connection_2]DB201,X20.5" ClientHandle="12" RequestedDataType="11"></Item>
<!--左截温度-->
<Item ItemID="S7:[S7 connection_2]DB201,INT10" ClientHandle="13" RequestedDataType="2"></Item>
<!--右截温度-->
<Item ItemID="S7:[S7 connection_2]DB201,INT12" ClientHandle="14" RequestedDataType="2"></Item>
<!--油泵电机电流-->
<Item ItemID="S7:[S7 connection_2]DB201,INT14" ClientHandle="15" RequestedDataType="2"></Item>
<!--工作模式 2人工 4学习 8自动割煤 16 传感器配置-->
<Item ItemID="S7:[S7 connection_2]DB201,INT34" ClientHandle="16" RequestedDataType="2"></Item>
</ShearerInfo>
</OpcServer>
</System>

上述配置文件中,OpcServer节点对应的OpcServer对象,定义了ServerName和IPAddress属性,用来连接OPCServer。

ShearerInfo节点则对应一个OpcGroup,在OpcServer下定义多个OPCGrupo节点,OPCGroup节点需要指定组的客户端句柄和刷新频率。上文说到OPC的读写操作都是以组进行的,我们需要根据客户端句柄来判断是哪一个组,如果我们采用的事订阅模式读取数据,则还需要刷新频率,OpcServer对订阅模式的实现不太清楚,实际使用的过程发现,并没有按照刷新频率来,所以我就采用了直接读的方式来保证数据的实时性。

Item的ItemID是一个地址,由于我使用的是西门子的产品,所以格式是:S7:[S7连接名称]地址,我们只需要更改S7连接的名称和地址就好了。如果你使用的事其他类型的PLC,请参照他们的地址格式。

有了配置文件如何操作呢?下面我定义了一个实现类:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Runtime.InteropServices;
using System.Xml.Linq; namespace Opc.Net
{
public class OpcManager
{
/// <summary>
/// Opc异步接口类
/// </summary>
OpcDaCustomAsync _opcDaCustomAsync;
/// <summary>
/// 异步读取数据完成事件
/// </summary>
public event EventHandler<OpcDaCustomAsyncEventArgs> OnReadCompleted;
/// <summary>
/// Opc组列表
/// </summary>
List<OpcDaCustomGroup> _opcGroups;
/// <summary>
/// OPC服务器名称
/// </summary>
string _strRemoteServerName;
/// <summary>
/// OPC服务器IP地址
/// </summary>
string _strRemoteServerIpAddress; /// <summary>
/// 构造函数
/// </summary>
/// <param name="strConfigFilePath">配置文件路径</param>
public OpcManager(string strConfigFilePath)
{
LoadOpcGroupConfig(strConfigFilePath);
}
/// <summary>
/// 加载Opc组配置
/// </summary>
/// <param name="strConfigFilePath">配置文件路径</param>
public void LoadOpcGroupConfig(string strConfigFilePath)
{
try
{
if (!File.Exists(strConfigFilePath)) return;
XDocument xDoc = XDocument.Load(strConfigFilePath);
XElement xElement = xDoc.Element("System").Element("OpcServer");
_strRemoteServerName = xElement.Attribute("ServerName").Value;
_strRemoteServerIpAddress = xElement.Attribute("IPAddress").Value;
_opcGroups = new List<OpcDaCustomGroup>();
foreach (XElement xElementItem in xElement.Elements())
{
var opcDaCustomGroupService = new OpcDaCustomGroup
{
GroupName = xElementItem.Attribute("GroupName").Value,
ClientGroupHandle = Convert.ToInt32(xElementItem.Attribute("ClientHandle").Value),
RequestedUpdateRate = Convert.ToInt32(xElementItem.Attribute("UpdateRate").Value),
OpcDataCustomItems = LoadOpcItemConfig(xElementItem)
};
_opcGroups.Add(opcDaCustomGroupService);
}
_opcDaCustomAsync = new OpcDaCustomAsync(_opcGroups, _strRemoteServerName, _strRemoteServerIpAddress);
_opcDaCustomAsync.OnReadCompleted += ReadCompleted;
}
catch(COMException ex)
{
throw ex;
}
}
/// <summary>
/// 连接Opc服务器
/// </summary>
/// <returns></returns>
public bool Connect()
{
return _opcDaCustomAsync.Connect();
}
/// <summary>
/// 连接Opc服务器
/// </summary>
/// <returns></returns>
public bool Connect(string remoteOpcServerName,string remoteOpcServerIpAddress)
{
return _opcDaCustomAsync.Connect(remoteOpcServerName, remoteOpcServerIpAddress);
}
/// <summary>
/// 加载Opc项配置
/// </summary>
/// <param name="xElement">Opc组Xml节点</param>
/// <returns></returns>
public OpcDaCustomItem[] LoadOpcItemConfig(XElement xElement)
{
int itemCount = xElement.Elements().Count();
var opcDaCustomItems = new OpcDaCustomItem[itemCount];
int i = ;
foreach (var xElementItem in xElement.Elements())
{
var opcDaCustomItemService = new OpcDaCustomItem
{
ClientHandle = Convert.ToInt32(xElementItem.Attribute("ClientHandle").Value),
ItemID = xElementItem.Attribute("ItemID").Value,
RequestedDataType = short.Parse(xElementItem.Attribute("RequestedDataType").Value)
};
opcDaCustomItems[i] = opcDaCustomItemService;
i++;
}
return opcDaCustomItems;
}
public bool WriteForReturn(int itemClientHandle, int value, int clientHandle)
{
bool returnValue;
var itemDictionary = new Dictionary<int, object>
{
{itemClientHandle, value}
};
try
{
int[] pErrors;
Write(itemDictionary, clientHandle, out pErrors);
returnValue = (pErrors[] == );
}
catch (COMException ex)
{
throw ex;
}
return returnValue;
}
public void Write(Dictionary<int, object> itemDictionary, int groupHandle, out int[] pErrors)
{
var count = itemDictionary.Count();
var values = new object[count];
var serverHandle = new int[count];
pErrors = null;
OpcDaCustomGroup group = _opcGroups.First(p => p.ServerGroupHandle == groupHandle);
int index = ;
foreach (KeyValuePair<int, object> itemId in itemDictionary)
{
foreach (var item in group.OpcDataCustomItems)
{
if (item.ClientHandle == itemId.Key)
{
values[index] = itemId.Value;
serverHandle[index] = item.ServerHandle;
index++;
}
}
}
try
{
_opcDaCustomAsync.Write(values, serverHandle, out pErrors, group);
}
catch (COMException ex)
{
throw ex;
}
}
/// <summary>
/// 写单个数据
/// </summary>
/// <param name="value">值</param>
/// <param name="groupHandle">组ID</param>
/// <param name="clientHandle">项ID</param>
public void Write(int value, int groupHandle, int clientHandle)
{
OpcDaCustomGroup group = GetOpcGroup(groupHandle);
if (group != null)
{
int[] pErrors;
var serverHanlde = new int[];
serverHanlde[] = group.OpcDataCustomItems.First(c => c.ClientHandle == clientHandle).ServerHandle;
var values = new object[];
values[] = value; _opcDaCustomAsync.Write(values, serverHanlde, out pErrors, group); }
}
/// <summary>
/// 异步读取数据完成事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void ReadCompleted(object sender, OpcDaCustomAsyncEventArgs e)
{
if (OnReadCompleted != null)
{
OnReadCompleted(this, e);
}
}
/// <summary>
/// 异步读取控制模式数据
/// </summary>
public void Read()
{
if (_opcDaCustomAsync != null)
{
_opcDaCustomAsync.Read();
} }
/// <summary>
/// 根据OPC句柄获取OPC组对象
/// </summary>
/// <param name="groupHandle">OPC组对象</param>
/// <returns></returns>
public OpcDaCustomGroup GetOpcGroup(int groupHandle)
{
return _opcGroups.First(e => e.ClientGroupHandle == groupHandle);
}
}
}

这个类可以根据自己设计的配置文件进行相应的实现。

 private OpcManager opcManager;
private System.Timers.Timer opcTimer;
private int[] pErrors;
private Dictionary<int, object> items;
/// <summary>
/// 写入采煤机位置数据
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
items = new Dictionary<int, object>();
items.Add(, textBox2.Text);
opcManager.Write(items, , pErrors);
} private void FrmMain_Load(object sender, EventArgs e)
{
opcManager = new OpcManager(AppDomain.CurrentDomain.BaseDirectory+"\\Opc.config.xml");
opcManager.OnReadCompleted += new EventHandler<OpcDaCustomAsyncEventArgs>(opcManager_OnReadCompleted); opcTimer = new System.Timers.Timer()
{
Interval = ,
AutoReset = true,
Enabled = true
};
opcTimer.Elapsed += new ElapsedEventHandler(opcTimer_Elapsed);
} void opcTimer_Elapsed(object sender, ElapsedEventArgs e)
{
opcManager.Read();
} void opcManager_OnReadCompleted(object sender, OpcDaCustomAsyncEventArgs e)
{
Invoke((ThreadStart)(() =>
{ if (OpcHelper.ShowValue(e, ) != null)
{
textBox1.Text = OpcHelper.ShowValue(e, ).ToString();
}
}));
}

以上实现了数据的读取和写入。

源码戳这里:http://pan.baidu.com/s/1ntp1JAx

v\:* {behavior:url(#default#VML);}
o\:* {behavior:url(#default#VML);}
w\:* {behavior:url(#default#VML);}
.shape {behavior:url(#default#VML);}

Normal
0
false

7.8 磅
0
2

false
false
false

EN-US
ZH-CN
X-NONE

/* Style Definitions */
table.MsoNormalTable
{mso-style-name:普通表格;
mso-tstyle-rowband-size:0;
mso-tstyle-colband-size:0;
mso-style-noshow:yes;
mso-style-priority:99;
mso-style-parent:"";
mso-padding-alt:0cm 5.4pt 0cm 5.4pt;
mso-para-margin:0cm;
mso-para-margin-bottom:.0001pt;
mso-pagination:widow-orphan;
font-size:10.5pt;
mso-bidi-font-size:11.0pt;
font-family:"Calibri","sans-serif";
mso-ascii-font-family:Calibri;
mso-ascii-theme-font:minor-latin;
mso-hansi-font-family:Calibri;
mso-hansi-theme-font:minor-latin;
mso-bidi-font-family:"Times New Roman";
mso-bidi-theme-font:minor-bidi;
mso-font-kerning:1.0pt;}