1 OPC的基本结构
OPC
由两套接口组成:
OPC
定制接口和
OPC
自动化接口,如图
1
所示。
OPC
服务器必须实现定制接口,可选择实现自动化接口。这两套标准接口的制定极大地方便了服务器和用不同语言开发的客户应用之间的通信,使用户对开发工具的选择有了较大的自由。
OPC
接口可以潜在地应用在许多应用程序中。它们可以用于从最低层设备中读取未加工的数据,再转化至
SCADA
或者
DCS
系统;也可以用于从
SCADA
或者
DCS
系统中采集数据输入到应用程序中。
OPC
是为从某一网络节点中的某一服务器中采集数据而设计的,同时又能够形成
OPC
服务器。该服务器允许客户应用软件在由许多不同的
OPC
供应商提供的服务器中传输数据,并可通过单一的对象在不同的节点上运行
2 OPC定制接口
用
C
或
C
++编写
OPC
客户应用程序时可以使用定制接口,也可以使用自动化接口。由于定制接口具有更高的性能,建议尽可能使用定制接口。本文在
VC
下实现的客户应用程序采用的是
OPC
定制接口
1.0
。
OPC
定制接口的类模式可以根据其接口及其方法划分为
3
组,依次呈包含关系.
2.1 OPC Server对象
OPC Server
即
OPC
启动服务器,通过它获得其他对象和服务的起始类,并用于返回
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
接口,而是使用枚举器对象
EnumOPCItemAttributes
的
IEnumOPCItemAttributes
接口枚举服务器中的所有
OPC Item
。如图
7
所示。
图
7 EnumOPCItemAttributes
对象
3 OPC客户应用程序的实现
3.1 操作OPC的类模型
按照
OPC
的类模型,当对象方法调用
OPC
对象时必须遵循一定的顺序。如果要创建一个
OPC Item
类的实例,则首先需要一个
OPC Group
对象。而要创建一个
OPC Group
对象的前提是存在一个
OPC Server
类的实例,并建立一个与该服务器的连接。图
8
说明了操作
OPC
类模型的流程。
图
8
操作
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;
第一步:登陆 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();
这段程序的执行结果是, 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);
第一步:得到指向 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);