基于OPC规范的客户应用程序实现

1 OPC的基本结构

OPC由两套接口组成:OPC定制接口和OPC自动化接口,如图1所示。OPC服务器必须实现定制接口,可选择实现自动化接口。这两套标准接口的制定极大地方便了服务器和用不同语言开发的客户应用之间的通信,使用户对开发工具的选择有了较大的自由。
 
OPC接口可以潜在地应用在许多应用程序中。它们可以用于从最低层设备中读取未加工的数据,再转化至SCADA或者DCS系统;也可以用于从SCADA或者DCS系统中采集数据输入到应用程序中。OPC是为从某一网络节点中的某一服务器中采集数据而设计的,同时又能够形成OPC服务器。该服务器允许客户应用软件在由许多不同的OPC供应商提供的服务器中传输数据,并可通过单一的对象在不同的节点上运行
 
2  OPC定制接口
CC++编写OPC客户应用程序时可以使用定制接口,也可以使用自动化接口。由于定制接口具有更高的性能,建议尽可能使用定制接口。本文在VC下实现的客户应用程序采用的是OPC定制接口1.0OPC定制接口的类模式可以根据其接口及其方法划分为3组,依次呈包含关系.
 
2.1  OPC Server对象
OPC ServerOPC启动服务器,通过它获得其他对象和服务的起始类,并用于返回OPC Group类对象。OPC Server级别有多种属性,其中包含一个OPC服务器对象的状态和版本等信息。这种级别中的对象由客户应用创建。IOPCServer接口包含管理OPC Group级别中的对象的方法。如将组加入服务器或从服务器中删除组的方法("AddGroup""RemoveGroup")。IOPCBrowseServerAddressSpace接口包含查找服务器地址空间的方法。IOPCCommon接口方法用于通知服务器语言的设置和客户机的名称。同时还存在以下接口:图4说明了OPC Server对象及其定制接口。
4  OPC Server对象
2.2  OPC Group对象
OPC Group存储由若干OPC Item组成的Group信息,并用于返回OPC Item类对象。OPC Group级别管理被称为OPC Item的各个过程变量。IOPCItemMgt接口提供将项加入组或从组中删除项的方法("AddItem""RemoveItem")。IOPCGroupStateMgt接口的方法用于处理组专用的参数或复制组。同时还存在以下接口:图5说明了OPC Group对象及其定制接口。
5  OPC Group对象
2.3  OPC Item对象
    OPC Item存储具体Item 的定义、数据值、状态值等信息。OPC Item级别的一个对象代表与一个过程变量的连接。该对象的唯一接口是OPCItemDisp。关于OPC Item的信息可以在属性表中找到,例如数值("Value")属性或存取路径("AccessPath")属性。图6说明了"OPC Item"对象及其接口。
6  OPC Item对象
由于本文使用定制接口实现OPC客户应用程序,所以不使用IOPCItemDisp接口,而是使用枚举器对象EnumOPCItemAttributesIEnumOPCItemAttributes接口枚举服务器中的所有OPC Item。如图7所示。
7  EnumOPCItemAttributes对象
3 OPC客户应用程序的实现
3.1  操作OPC的类模型
按照OPC的类模型,当对象方法调用OPC对象时必须遵循一定的顺序。如果要创建一个OPC Item类的实例,则首先需要一个OPC Group对象。而要创建一个OPC Group对象的前提是存在一个OPC Server类的实例,并建立一个与该服务器的连接。图8说明了操作OPC类模型的流程。
操作OPC类模型的流程
3.2  编程顺序
本文在Visual C++环境中实现的OPC客户应用程序包括所有通常在典型客户应用下都会有的部分,如:建立与服务器的连接,初始化变量的组,以及为一个项读写数据。下面详细介绍一下在VC环境下OPC应用的基本结构。
第一步:登陆COM
如果程序要调用COM库的某一函数,必须先登陆COM。函数CoInitialize()可以完成此功能。从函数CoGetMalloc()可以得到一个指向COM内存管理接口的指针。
HRESULT r1;
r1= CoInitialize(NULL);
r1= CoGetMalloc(MEMCTX_TASK,&g_pIMalloc);
第二步:将ProgID变换为CLSID
每个COM服务器有一个字符串类型的ProgID,通过它可以得到一个全球唯一的CLSID。用CLSIDFromProgID()函数可以实现这个转换。ProgID用变量szName进行参数传递。
r1= CLSIDFromProgID(szName,&clsid);
第三步:建立与OPC服务器的连接
CoCreateInstance()函数创建一个OPC Server类实例,其CLSID值设定如下。
r2=CoCreateInstance(clsid,NULL,CLSCTX_LOCAL_SERVER,IID_IUnkown,(void**)&pUNK);
这段程序的结果是得到一个指向服务器对象IUnkown接口的指针(变量pUNK)。
第四步:请求其它接口指针
IUnkown接口,通过QueryInterface()方法可以得到其它接口的指针。
HRESULT r3;
r3=punk->QueryInterface(IID_IOPCServer,(void**)&m_pOPC);
这段程序的结果是得到一个指向服务器对象IOPCSever接口的指针(变量m_pOPC)。
第五步:创建OPC
IOPCServer接口的AddGroup()方法可以创建OPC组。
HRESULT r1;
r1=m_pOPC->AddGroup(szName,TRUE,500,&TimeBias,&PercDeadband,dwLCID,&m_GrpServerHandle,
&RevUpRate,IID_IOPCItemMgt,(LPUNKNOWN*)&m_pItemMgt);
这段程序的执行结果是创建一个有指定名称和属性的组。在返回的参数中,有一个指向所需要的进程组对象IOPCItemMgt接口的指针(变量m_pItemMgt)。
第六步:添加项
IOPCItemMgt接口的AddItems()方法可以添加OPC项。
HRESULT r1;
r1=m_pItemMgt->AddItems(NumItems,pItems,&m_pItResult,&pErrors);
这段程序的结果是添加具有特殊属性的指定数量的项。除此之外,事件结构变量m_pItResult(服务器句柄,目标系统上的项数据类型等)也被赋值。
第七步:用OPC项执行所需的操作
用于执行所需操作的指针需要通过现有的指向IOPCItemMgt接口的指针得到。如:如果用户要进行异步通信,就需要指向IOPCAsyncIO接口的指针。
HRESULT r1;
r1=m_pItemMgt->QueryInterface(IID_IOPCAsyncIO,(void**)&pAsyncIO);
通过该接口的Read()Write()两个方法,就可以读写项的数值。
HRESULT r2;
r2=pAsyncIO->Read(m_dwConnection,OPC_DS_CACHE,dwNumItems,phServer,&m_TransactionID,
&pErrors);
这段程序的执行结果是,OPC项的数据被送到客户程序的IAdviseSink接口。
HRESULT r3;
r3=pAsyncIO->Write((m_dwConnection,dwNumItems,phServer, pItemValues,&m_TransactionID,&pErrors);
这段程序的执行结果是,OPC服务器代替OPC客户刷新物理设备的数据。
第八步:删除对象,释放内存
在程序停止运行之前,必须删除已创建的OPC对象并释放内存。到目前为止,用到的接口都有相应的函数。
r1=m_pItemMgt->RemoveItems(dwNumItems,phServer,&pErrors);
r1=m_pOPC->RemoveGroup(m_GrpServerHandle,TRUE);
m_pItemMgt->Release();
m_pOPC->Release();
3.3  异步通信的说明
OPC客户和OPC服务器进行数据交换可以有两种不同的方式,即同步方式和异步方式。同步方式实现较为简单,当客户数目较少而且同服务器交互的数据量也比较少的时候可以采用这种方式;异步方式实现较为复杂,需要在客户程序中实现服务器回调函数。然而当有大量客户和大量数据交互时,异步方式的效率更高,能够避免客户数据请求的阻塞,并可以最大限度地节省CPU和网络资源。本文中用VC实现的OPC客户程序可以执行异步读写数据,异步意味着程序继续执行后面的操作,只要读或写的任务送达马上申请读写,并由OPC服务器返回回调函数的执行结果。为了实现异步通信,客户程序必须提供IAdviseSink接口与服务器方的IDataObject接口通信。下面详细描述一下用VC实现OPC客户应用程序中异步通讯的基本步骤。
第一步:得到指向IDataObject接口的指针
可以用这个接口在客户和服务器之间建立连接。为此目的,可以在程序中创建由IAdviseSink接口派生的COPCData类,并建立一个此类的实例m_pOPCIData,指向IDataObject接口的指针临时保存在这个类的m_pDataObject成员变量中。
m_pOPCIData=new COPCData(pWnd);
..........
r1=m_pItemMgt->QueryInterface(IID_IDataObject,(LPVOID*)&m_pOPCIData->m_pDataObject);
第二步:取得指向IAdviseSink接口的指针
客户必须能够和服务器建立联系,借此可以收到服务器的通知。IAdviseSink接口可以完成这个任务。通过QueryInterface()方法可以得到这个接口的指针并把它赋给变量pAdviseSink
m_pOPCIData-> QueryInterface(IID_IAdviseSink,(LPVOID*)&pAdviseSink);
第三步:建立连接
如果有关接口都存在,就可以建立连接。这可由IDataObject接口的DAdvise()方法完成,在这个方法中指向IAdviseSink接口的指针被传递给服务器。
r1=m_pOPCIData->m_pDataObject->DAdviseSink(&formatEtc,ADVF_PRIMEFIRST,pAdviseSink,
&m_dwConnection);
第四步:收到服务器的通知
如果数据发生变化,服务程序将调用IAdviseSink接口的OnDataChange()方法。这个方法是在客户应用程序中执行的。OPC项的实际数据被赋值给用户定义的成员变量m_ItemValues, 这就意味着在客户应用程序中可以获得这些想要的数据。
r2=VariantChangeType(&(g_pOPCServer->m_ItemValues[hItClient]),&Value,0,VT_BSTR);
第五步:显示读出的数据
在这里,OnDataChange()方法发送Windows消息WM_DATA_CHANGE启动下一步的处理。
SendMessage(*m_pWnd,WM_DATA_CHANGE,0,0);
通过在OPC客户应用程序的显示程序中定义消息映射,如果收到上述消息,则调用OnDataChange()事件过程。
ON_MESSAGE(WM_DATA_CHANGE,OnDataChange);
这个事件过程很简单,它只是调用用户自定义的ItemsView()函数在OPC客户应用程序的显示界面上显示实际的数据。
ItemView(g_pOPCServer->m_pItAttr,g_pOPCServer->m_ItemValues,4);
原文地址:https://www.cnblogs.com/liuxiuhao/p/2621823.html