如何在C++中实现OPC访问

如何在C++中实现OPC访问,第1张

OPC的文档网上很多,我在这里要介绍的主题是使用C++通过自动化接口来访问OPC Server,写这篇文章的目的是我在网上没有搜索到这方面的文档,如果我有这方面的需要,我想在网上一定也有其他朋友有这个需要,希望能对这些朋友有一些帮助。

使用C++来访问OPC Server, 相对于使用自定义接口来说,自动化接口要简单很多,因为这和Visual Basic使用的是同一个接口,使用过Visual Basic来访问OPC Server的朋友一定能有这个体会。首先是准备好开发环境,一般测试是在模拟环境中进行,这样比较保险,可以使用一些免费的模拟OPC Server。我这里准备的是Matrikon的模拟服务器,模拟器安装以后。编程环境是VC++ 6.0,使用200X和2010也都大同小异。

为了演示简单,新建一个Win32控制台工程agOPC,新建agOPC.cpp源文件并加到工程里。

// --------------------------------- agOPC.cpp -----------------------------------------------

//在agOPC.cpp开头添加如下一行

#import "C:Program FilesMatrikonOPCCommonOPCAuto.dll" no_namespace

//这是通过OPCAuto.dll里所包含的类型库信息产生C++能访问的头文件,此时在工程的Debug文件夹下产生OPCAuto.tlh和OPCAuto.tli两个文件。

//添加需要的头文件

#pragma warning( disable : 4786 ) // 为了避免vector报出的C4786警告

#include<comdef.h> // 使用到了_bstr_t,_variant_t,_com_error都在这个文件里定义

#include<iostream>

#include<vector>

using namespace std

//声明全局变量

typedef struct OLEInit {

OLEInit() { CoInitialize( NULL )}

~OLEInit() { CoUninitialize()}

} OLEInit

OLEInit oleInit // 必须在最前面定义,因为在使用COM之前必须初始化COM库,否则程序会崩溃

// 由于是全局变量oleInit的构造函数在所有对象的构造函数调用之前调用,

// 析构函数在所有对象的析构函数调用之后调用

IOPCAutoServerPtropcSvr // 这些智能指针类型在OPCAuto.tlh中定义

IOPCGroupsPtropcGrps

IOPCGroupPtropcGrp

vector<OPCItemPtr> opcItms // 使用vector来保存三个测试Item。

//连接到OPC Server, 我所使用的参数是"Matrikon.OPC.Simulation.1"

void agOPCConn( const char *opcSvrName ) {

HRESULT hr

hr = opcSvr.CreateInstance( __uuidof( OPCServer ) )

if( FAILED( hr ) ) {

cerr<<"OPCServer CreateInstance failed, hr = " <<hr<<endl

exit(1)

}

opcSvr->Connect( opcSvrName )

}

//断开和OPC Server的连接

void agOPCDisc() {

opcGrps->RemoveAll()// 删除所有的组, 这个演示实例只有一个组

opcSvr->Disconnect()// 断开和OPC Server的连接

}

//创建一个组

void agOPCCreateGroup() {

// OPCGroups是特殊的属性,执行的时候会调用OPCAuto.tlh中的IOPCGroupsPtr GetOPCGroups()

opcGrps = opcSvr->OPCGroups

opcGrp = opcGrps->Add( _variant_t( "group1" ) ) // 组名随意取

}

//在组里添加三个不同类型的测试Item, 类型可以从Item的名字可以看出

void agOPCAddItems() {

OPCItemPtr opcItm

opcItm = opcGrp->OPCItems->AddItem( _bstr_t( "Bucket Brigade.Int4" ), 1 )

opcItms.push_back( opcItm )

opcItm = opcGrp->OPCItems->AddItem( _bstr_t( "Bucket Brigade.Int2" ) , 1)

opcItms.push_back( opcItm )

opcItm = opcGrp->OPCItems->AddItem( _bstr_t( "Bucket Brigade.String" ) , 1)

opcItms.push_back( opcItm )

}

//用来显示读取的Item的值

void agDumpVariant(VARIANT *v)

{

switch(v->vt)

{

case VT_I2:

printf(" value(VT_I2) = %d ", v->iVal )

break

case VT_I4:

printf(" value(VT_I4) = %ld ", v->lVal )

break

case VT_BSTR:

printf(" value(VT_BSTR) = %ls ", v->bstrVal )

break

default:

printf(" value(unknown type:%d) ", v->vt )

break

}

}

//同步读取三个Item的值,同步在很多情况下都是简单有效的选择方案,其实读取的异步方式在C++中可以建立一个工作线程来执行同步读的 *** 作,等有新的Item值的时候再通过某种线程间通信的方式告诉主线程“数据改变”的事件

void agOPCReadItems() {

_variant_tquality

_variant_ttimestamp

SAFEARRAY*pServerHandles

SAFEARRAY*pValues

SAFEARRAY*pErrors

SAFEARRAYBOUNDrgsabound[ 1 ]

longdim[ 1 ]

longsvrHdl

vector<_variant_t> values

vector<long> errs

inti

_variant_tvalue

longerr

// VC数组索引从0开始,而在OPCAuto.dll需要中从1开始,所以是rgsabound[ 0 ].cElements = 4,而给pServerHandles赋值的时候应该给索引是1,2,3相应的赋值Server Handle

rgsabound[ 0 ].cElements = 4

rgsabound[ 0 ].lLbound = 0

pServerHandles = SafeArrayCreate( VT_I4, 1, rgsabound )//构建一个1维数组,类型是VT_I4

for( i = 0i <opcItms.size()i++ ) {

svrHdl = opcItms[i]->ServerHandle

dim[ 0 ] = i + 1

// 给数组的每个元素赋值,对应的索引值是1, 2, 3

SafeArrayPutElement( pServerHandles, dim, &svrHdl )

}

opcGrp->SyncRead( OPCDevice,

3, // 读取的Item数目

&pServerHandles,// 输入的服务器端句柄数组

&pValues, // 输出的Item值数组

&pErrors, // 输出的Item错误状态数组

&quality, // 读取的值的状态

&timestamp ) // 读取的事件戳

for( i = 1i <= opcItms.size()i++ ) {

dim[ 0 ] = i

SafeArrayGetElement( pValues, dim, &value )// 读取Item值在value中

SafeArrayGetElement( pErrors, dim, &err ) // 读取错误状态值在err中

values.push_back( value )

errs.push_back( err )

}

for( i = 0i <values.size()i++ ) {

agDumpVariant( &values[ i ] ) // 显示读取的Item值

cout<<", err = "<<errs[ i ]<<endl

}

SafeArrayDestroy( pServerHandles )

SafeArrayDestroy( pValues )

SafeArrayDestroy( pErrors )

}

// 写入3个Item的值,为了演示实例简单,参数传递3个对应的Item值

void agOPCWriteItems( vector<_variant_t>values) {

_variant_tquality

_variant_ttimestamp

SAFEARRAY*pServerHandles

SAFEARRAY*pValues

SAFEARRAY*pErrors

longdim[ 1 ]

longsvrHdl

inti

SAFEARRAYBOUND rgsabound[ 1 ]

rgsabound[ 0 ].cElements = values.size() + 1

rgsabound[ 0 ].lLbound = 0

pServerHandles = SafeArrayCreate( VT_I4, 1, rgsabound )

pValues = SafeArrayCreate(VT_VARIANT, 1, rgsabound)

for( i = 0i <values.size()i++ ) {

svrHdl = opcItms[i]->ServerHandle

dim[ 0 ] = i + 1

SafeArrayPutElement( pServerHandles, dim, &svrHdl )

SafeArrayPutElement( pValues, dim, &values[i] )

}

opcGrp->SyncWrite( 3,&pServerHandles, &pValues,&pErrors )

SafeArrayDestroy( pServerHandles )

SafeArrayDestroy( pValues )

SafeArrayDestroy( pErrors )

}

//main主程序

int main()

{

try

{

agOPCConn( "Matrikon.OPC.Simulation.1" )

agOPCCreateGroup()

agOPCAddItems()

// 第一次写和读

vector<_variant_t>values

values.push_back( ( long )156 )

values.push_back( ( short )11 )

values.push_back( "opc" )

agOPCWriteItems( values )

agOPCReadItems()

cout <<"---------------------------------------"<<endl

// 第二次写和读

vector<_variant_t>values1

values1.push_back( ( long )123456 )

values1.push_back( ( short )666 )

values1.push_back( "hello" )

agOPCWriteItems( values1 )

agOPCReadItems()

}

catch ( _com_error &e ) {

// 应该在上面的子函数里面捕捉异常,但为了演示简单,在主函数里面捕捉异常

_bstr_t bstrSource( e.Source( ) )

_bstr_t bstrDescription( e.Description( ) )

cout<<"Code = "<<e.Error()<<endl

cout<<"Code meaning = "<<e.ErrorMessage()<<endl

cout<<"Source = "<<( LPCTSTR ) bstrSource<<endl

cout<<"Description = "<<( LPCTSTR ) bstrDescription<<endl

}

return 0

}

一.准备工作:

下载opc基金会的OPC Proxy DLL然后按照说明进行安装。这些动态库是opc程序运行所必须的。

1. 将下列文件拷贝至要运行OPC服务器和OPC客户端的机器上的SYSTEM32目录下

copy opcproxy.dll C:\WINDOWS\system32

copy opccomn_ps.dll C:\WINDOWS\system32

copy opc_aeps.dll C:\WINDOWS\system32

copy opchda_ps.dll C:\WINDOWS\system32

copy aprxdist.exe C:\WINDOWS\system32

copy opcenum.exe C:\WINDOWS\system32

2. 注册这些 dll 文件

REGSVR32 opcproxy.dll

REGSVR32 opccomn_ps.dll

REGSVR32 opc_aeps.dll

REGSVR32 opchda_ps.dll

3. 如果在windows 系统(\WINDOWS \system32)目下不存在actxprxy.dll,运行aprxdist.exe

4. 安装 opcenum.exe

opcenum /regserver

二.opcserver开发

1. 新建vc工程。比如建一个控制台工程。在工程中包含如下头文件

#include "opcda.h"

#include "opc_ae.h"

#include "WTOPCsvrAPI.h"

#include "WtOPCsvrEXTapi.h"

以上头文件在例子程序中都能找到,拷贝到自己的工程下。

WTOPCsvrAPI.h是开发包动态库提供的导出函数文件。里面有每个函数的具体说明,调用的时候看一下说明。

2. 在.cpp中定义一个GUID这个guid是用来标识opcserver的唯一id可以通过 *** 作系统的工具生成,也可以编一个。格式如下。

const GUID

CLSID_OPCSimSvr = {0x99b8f472, 0xc037, 0x11d2, {0x80, 0xb8, 0x0, 0x60, 0x97, 0x58, 0x58, 0xbe}}

3. 在工程中加入对WtOPCSvr动态库的连接。

WTOPCsvr动态库的有关文件都在之前的例子包里面能找到。

WTOPCsvr.lib WTOPCsvr.dll是对应lib和dll

4. 初始化

(1)调用UpdateRegistry()函数完成注册。下面是一个调用参考例子代码

BOOL COPCSimSvrApp::InitInstance()

{

TCHAR szTokens[] = _T("-/ ")

CString HelpPath

CString SvrName, SvrDescrip

int i

HelpPath = AfxGetApp()->m_pszHelpFilePath

i = HelpPath.ReverseFind('\\')

HelpPath = HelpPath.Left(i+1)

HelpPath += "OPCSIMSVR.EXE"

//

// Self-Registration code

// (look for cmdline options to register &unregister server)

//

SvrName = "WinTECH.OPCServer"

SvrDescrip = "WinTECH Software OPC Server Simulator"

CString tempCmdLine(m_lpCmdLine)

LPTSTR lpszToken = _tcstok(tempCmdLine.GetBuffer(1), szTokens)

while (lpszToken != NULL)

{

if (_tcsicmp(lpszToken, _T("UnregServer"))==0)

{

UnregisterServer ((BYTE *)&CLSID_OPCSimSvr, SvrName)

return (FALSE)

}

else if (_tcsicmp(lpszToken, _T("RegServer"))==0)

{

UpdateRegistry ((BYTE *)&CLSID_OPCSimSvr,

SvrName,

SvrDescrip,

HelpPath)

return (FALSE)

}

lpszToken = _tcstok(NULL, szTokens)

}

(2)调用InitWTOPCsvr完成开发包dll初始化。

以上两个步骤不可颠倒。

5. 创建item

这里的item是服务器所有的item,这些item通过opc库户端能浏览到。创建item的函数为CreateTag().

Opc有两种地址空间形式:扁平和多层结构的。

比如调用创建的点为item1、item2、item3.这类结构就是扁平的。各个点类似与文件系统中的文件。

比如调用创建的点为test.item1、test.item2、test.item3这种点名,开发包会自动形成多层的点结构。Test类似与文件系统中的文件夹,item类似与文件系统中的文件夹下的文件。扁平式和多层结构在客户端浏览点名时体现。

多层结构的点名之间默认是用“.”分隔。为了简单可以将我们的opcserver设计为扁平结构。

创建一个点时会返回一个handle用来标识这个点。我们程序需要自己建立这个handle和数据库中保存设备实时值的对应关系。这部分可以参考例子代码。

6. 建立一个定时器采集设备数据

定时从数据库中取得各个handle对应的item的值。然后比较各个item的值是否和上一次读取的值有变化,如果有变化调用UpdateTagToList放入对应的队列。开发包会自动将变化的值送到客户端。

大致流程为

(1) 从数据库中采集一遍所有点的实时值。

(2) 调用StartUpdateTags()

(3) 循环读取每个item在数据库中的数据,和上一次读取到的进行比较。如果有变化调用UpdateTagToList()

(4) 最用调用EndUpdateTags()完成所有item的更新。

7. 客户端控制

客户端写tag的值的时候,在opcserver是通过一个回调函数来响应的。

在服务器端必须调用EnableWriteNotification()来指定写值的回调函数。回调函数的格式为:

typedef VOID (CALLBACK* WRITENOTIFYPROC)(HANDLE, VARIANT*, DWORD*)

然后在回调函数内部实现从HANDLE指定的tag写到具体的控制设备对应的变量中。

8. 其它常用函数

RequestDisconnect()一般在opcserver在关闭时调用,用来通知客户端opcserver自己要关闭。

NumbrClientConnections()用来计算当前有多少个客户端连接到了opcserver

UninitWTOPCsvr()程序退出时清理

SetVendorInfo()设置厂商信息

三.opc客户端测试

下面以本机opcquickclient.exe为例说明服务器和客户端之间的应用关系。

1. 运行opcquickclient.exe

2. 点击edit->new server connection 菜单。d出server properties对话框。从中展开opc data access server version 2.0.会浏览到本机安装的所有opcserver。选中我们自己开发的opcserver,点击确定。完成与服务器的连接

3. 点击edit->new group 用默认值添加组。

4. 点击edit->new item d出如下对话框。(下图是一个多层结构地址空间的例子,单层的更简单)选中左侧的某个父节点,右侧会显示出各个子tag。选中要向opcserver查询的tag,点击add leaves。然后点击ok

5. 在客户端主界面上能看到刚才要查询的tag。它会自动跟随opcserver端数据的变化而变换。

6. 右键要控制的tag,在菜单中选择同步写或者异步写(一般建议用异步,避免阻塞客户端的正常运行)。键入要写入的值。查看设备上是否动作。从而检验opcserver是否编写的正确。

答案:如何求解“对称”问题?证明曲线C:F(x,y)=

//定义一个OPC服务变量,一个OPCGROUP变量

public static OPCServer _OpcServer

public static OPCGroup _OpcGroup

_OpcServer = new OPCServer() //初始化

_OpcServer.Connect(_OpcServerName, _OpcAdressIp) //连接OPC(服务名,IP地址)

_OpcGroup = _OpcServer.OPCGroups.Add("OPCGroup")

_OpcGroup.OPCItems.AddItem(tag点名称, 关键值) //增加TAG点

_OpcGroup.IsActive = true

_OpcGroup.IsSubscribed = true

_OpcGroup.DataChange += new DIOPCGroupEvent_DataChangeEventHandler(_OpcGroup_DataChange) //注册事件

_OpcGroup.UpdateRate = 500

-----_OpcGroup_DataChange当tag点值发生变化时,上位就可以得到一个“事件”

对于已经加载的TAG点,也可以进行读写 *** 作。


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/yw/11298380.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-15
下一篇 2023-05-15

发表评论

登录后才能评论

评论列表(0条)

保存