一、通信拓扑

根据实际工作的内容,需要利用OPC作为媒介实现delphi程序与组态王软件的通信。不同于通常的思路,将组态王作为数据的提供者,直接采集plc等控制器的数据,然后用delphi做显示,我这次的工作是刚好相反的,因为原有的一些工业控制现场已经采用delphi编写了各自的显示程序,但是现在需要将四个工业现场的数据传输到一个总控的地方作为一个总得显示端,而这个显示端要用组态王开发,因此,就相当于delphi为组态王提供数据。因此,肯定需要频繁的使用delphi给组态王发送数据。最初考虑的是利用delphi作为服务器端,组态王作为客户端,这样有利于数据的传输,但是,经过一段时间的研究,发现开发delphi的OPC服务器不是易事,需要花费很多的时间,为了快速的进行项目开发,我同样采用delphi作为客户端,组态王作为服务器来进行开发,同样实现了数据的双向传输。具体的通信拓扑如图一所示。

opc_ua客户端python的api opc客户端开发_数据

图一

一、资料准备

    实际开发过程中所用到的一些资料如下:

1、  delphi 7 集成开发环境

2、  组态王软件

3、  OPCDll

4、  OPC客户化接口标准说明文档

5、  Delphi opc接口实现程序包

 

说明:

    OPCDll包含名为opc_aeps.dll,opccomn_ps.dll,OPCDAAuto.dll,opchda_ps.dll,opcproxy.dll的五个dll文件,在使用之间需要进行注册。不过实际上,在安装完组态王软件以后,这几个文件就已经被注册了,因此我在开发的时候并没有单独进行注册,

    OPC客户化接口说明文档是进行客户化OPC开发必备的参考资料,由OPC基金会提供。我打包了一下OPC客户化接口以及自动化接口的说明文档,有需要下载的请访问

    Delphi opc接口实现程序包是前人编写的,我整理了一下编写客户端需要的一些源文件,包括:

OPC_AE.pas

OPCCOMN.pas

OPCDA.pas

OPCerror.pas

OPCHDA.pas

OPCSEC.pas

OPCtypes.pas

OPCutils.pas

    其中,OPCutils.pas以上是对OPC提供的接口函数的声明,OPCutils.pas文件实现了编写客户端基本的一些函数,例如连接服务器,为服务器添加组,为组添加条目等等功能。其实编写通信的程序最重要的就是读和写,弄清楚了这两个怎么进行,通信程序的关键就已经掌握了。由于OPCutils.pas中只有同步写数据的函数,在数据发送的时候会产生阻塞,在有的场合下是不适用的,因此我改进了一下该文件,添加了一个异步写数据的函数,其实并没有做太多工作,只是认真的学习了一下OPC这一套东西而已。

二、OPC基础

    这里我并不像摘抄各网站对OPC的介绍,没有什么意义,就随便说说自己对OPC这个东西的粗浅认识。它其实上只是一种数据打包的方法,并不能叫做一种通信协议,OPC进行数据传输实际上是通过TCP或者UDP之类的协议,而且是可以选择的。它的优势在于,它通过树形的目录将数据较好的进行了组织,在客户端和服务器通信的过程中,开发者不用考虑数据包是怎么构成的,通信是怎么实现的,只需要直接利用数据变量就可以直接进行读或者写,很方便。

    OPC服务器将数据进行分组(group),有公共的组,也有普通的组。组这个概念其实就是顾名思义,它的目的就是将一些数据组成在一个包,便于用户区别一些不同的数据,比如,我可以按照数据类型定义一个组,组名叫做整数,那么开发的时候就很容易知道这个组下面的变量是整数。当然,分组的依据可以有很多。有了组以后,我们向组里面添加数据,也就是所谓的items。Item其实就是对应具体的数据了,item ID就是数据的变量,直接利用这个ID就可进行数据的读写操作了。这就是OPC的数据组织形式,通过参考接口标准可以发现,这个数据组织形式会有hierarchical和flat的区别,具体参见说明文档。

    进行OPC的客户端开发可以遵从以下的顺序进行,它跟普通的socket开发其实差别也并不大。首先进行初始化,其次就行数据库的连接,然后为服务器添加组,然后为每个组添加数据。做完这些操作以后,就可以进行数据的读写了。下面进行详细的介绍。

三、OPC客户端的开发

1、  OPC客户端的初始化

由于OPC是基于com组件的,所以其实它的初始化就是com的初始化,可以参考如下的代码:

1 CoInitializeSecurity(
 2     nil,                    // points to security descriptor
 3     -1,                     // count of entries in asAuthSvc
 4     nil,                    // array of names to register
 5     nil,                    // reserved for future use
 6     RPC_C_AUTHN_LEVEL_NONE, // the default authentication level for proxies
 7     RPC_C_IMP_LEVEL_IMPERSONATE,// the default impersonation level for proxies
 8     nil,                    // used only on Windows 2000
 9     EOAC_NONE,              // additional client or server-side capabilities
10     nil                     // reserved for future use
11 );

2、  连接服务器

利用OPC连接服务器实际上也是com的操作,利用CreateComObject函数创建一个com的对象,将该对象指定为OPC服务器对象,如果返回OK,那么久说明连接成功了。代码如下:

ServerIf := CreateComObject(ProgIDToClassID(ServerProgID)) as IOPCServer;

这里,ServerIf是一个IOPCServer的实例。而IOPCServer则是OPC提供的服务器接口,它提供了例如添加组,添加数据等一系列的函数。

3、  为服务器添加组

在成功连接服务器后,可以通过如下代码添加组:

ServerAddGroup(ServerIf, 'MyGroup', True, 500, 0, GroupIf, GroupHandle);

其中,ServerIf就是刚才得到的服务器,MyGroup是想要添加的组的名字,可以任意,500是获取服务器数据变化的周期,单位为微秒。GroupIf跟ServerIf类似,为得到的组,GroupHandle为组的句柄,用于区分不同的组。

4、  为组添加数据

成功添加组以后,可以通过如下代码添加数据:

GroupAddItem(GroupIf, Item0Name, 0, VT_EMPTY, Item0Handle,ItemType);

这里,Item0Name实际上是一个字符串,代表的是变量名。VT_EMPTY为数据类型,这里可以填的类型很多,具体可以参见Variant变体的一些帮助文档。VT_EMPTY为默认数据类型。后面两个参数顾名思义即可。

5、  为组添加回调函数

回调函数的定义大家应该都有一定的了解。这里为什么要添加回调函数呢?这就和OPC的运行机制分不开了。OPC在作为客户端的时候,根据上面的添加组的过程既可以知道,有一个获取服务器变化的时间周期需要我们填,这个是什么含义呢?它的意思是,每个这个周期数,就会去获取服务器上这个组里成员的所有的变量里面的数据,如果这些数据有变化,就会在客户端产生一个回调,回调函数名为:OnDataChange,然后进入这个函数,对数据的变化进行处理,这其实就是客户端读取数据的方法。它并不是一直在读取数据,而是当数据产生了变化以后才去读取,提高的运行的效率。所以,在使用回调函数之前,需要对回调进行设置,通过下述代码进行:

OPCDataCallback := TOPCDataCallback.Create;

GroupAdvise2(GroupIf, OPCDataCallback, AsyncConnection);

首先是创建一个回调类,然后将其余响应的Group进行绑定。绑定方法是com接口里面IConnectionPoint.Advise 方法。

6、  实现四个回调函数。

在完成了上述过程以后,需要实现IOPCDataCallback的四个方法,即四个回调函数,分别如下:

1 TOPCDataCallback = class(TInterfacedObject, IOPCDataCallback)
 2   public
 3     function OnDataChange(dwTransid: DWORD; hGroup: OPCHANDLE;
 4       hrMasterquality: HResult; hrMastererror: HResult; dwCount: DWORD;
 5       phClientItems: POPCHANDLEARRAY; pvValues: POleVariantArray;
 6       pwQualities: PWordArray; pftTimeStamps: PFileTimeArray;
 7       pErrors: PResultList): HResult; stdcall;
 8     function OnReadComplete(dwTransid: DWORD; hGroup: OPCHANDLE;
 9       hrMasterquality: HResult; hrMastererror: HResult; dwCount: DWORD;
10       phClientItems: POPCHANDLEARRAY; pvValues: POleVariantArray;
11       pwQualities: PWordArray; pftTimeStamps: PFileTimeArray;
12       pErrors: PResultList): HResult; stdcall;
13     function OnWriteComplete(dwTransid: DWORD; hGroup: OPCHANDLE;
14       hrMastererr: HResult; dwCount: DWORD; pClienthandles: POPCHANDLEARRAY;
15       pErrors: PResultList): HResult; stdcall;
16     function OnCancelComplete(dwTransid: DWORD; hGroup: OPCHANDLE):
17       HResult; stdcall;
18   end;

这四个回调函数分别表示的是当服务器端数据变化时的回调函数,当读取完成时的回调函数,写完成时的回调函数,取消时的回调函数,我们只需要读取数据或者对数据变化时进行处理,其他的回调函数直接返回OK即可。

在我的程序中,数据有变化时我讲数据读取出来进行显示,代码如下:

1 function TOPCDataCallback.OnDataChange(dwTransid: DWORD; hGroup: OPCHANDLE;
 2   hrMasterquality: HResult; hrMastererror: HResult; dwCount: DWORD;
 3   phClientItems: POPCHANDLEARRAY; pvValues: POleVariantArray;
 4   pwQualities: PWordArray; pftTimeStamps: PFileTimeArray;
 5   pErrors: PResultList): HResult;
 6 var
 7   ClientItems: POPCHANDLEARRAY;
 8   Values: POleVariantArray;
 9   Qualities: PWORDARRAY;
10   I: Integer;
11   NewValue: string;
12   OutputValue:string;
13 begin
14   Result := S_OK;
15   ClientItems := POPCHANDLEARRAY(phClientItems);
16   Values := POleVariantArray(pvValues);
17   Qualities := PWORDARRAY(pwQualities);
18   for I := 0 to dwCount - 1 do
19   begin
20     if Qualities[I] = OPC_QUALITY_GOOD then
21     begin
22       NewValue := VarToStr(Values[I]);
23       OutputValue:=format('New callback for item %d received, value: %s', [ClientItems[I],NewValue]);
24       Form1.Memo1.Lines.Append(OutputValue);
25     end
26     else begin
27       OutputValue:=format('Callback received for item %d but quality not good', [ClientItems[I]]);
28       Form1.Memo1.Lines.Append(OutputValue);
29     end;
30   end;
31 end;

这里主要用的函数都是一些开发com时常用的函数,不再赘述,也不敢赘述,自己都不太懂。不过,最重要的是,传入的参数pvValues包含了我们需要的数据。

读取数据的回调函数同样可以采用数据变化的回调函数进行处理,所以,读取完成后的回调函数如下:

1 function TOPCDataCallback.OnReadComplete(dwTransid: DWORD; hGroup: OPCHANDLE;
2   hrMasterquality: HResult; hrMastererror: HResult; dwCount: DWORD;
3   phClientItems: POPCHANDLEARRAY; pvValues: POleVariantArray;
4   pwQualities: PWordArray; pftTimeStamps: PFileTimeArray;
5   pErrors: PResultList): HResult;
6 begin
7   Result := OnDataChange(dwTransid, hGroup, hrMasterquality, hrMastererror,
8     dwCount, phClientItems, pvValues, pwQualities, pftTimeStamps, pErrors);
9 end;

其他回调函数可以只讲返回S_OK即可。

7、      实现写数据的操作

在OPCutils.pas文件中,原作者只提供了同步写数据的代码,我在这里添加了一个异步写的代码,跟同步写很类似,只是用到的函数不同而已,如下所示:

1 function AsyncWriteItemValue(GroupIf: IUnknown; ItemServerHandle: OPCHANDLE;
 2           ItemValue: OleVariant): HResult;
 3 var
 4   AsyncIOIf: IOPCAsyncIO2;
 5   Errors: PResultList;
 6   TransID: DWORD;
 7   CancelID: DWORD;
 8 begin
 9   Result := E_FAIL;
10   try
11     AsyncIOIf := GroupIf as IOPCAsyncIO2;
12   except
13     AsyncIOIf := nil;
14   end;
15   if AsyncIOIf <> nil then
16   begin
17     Result := AsyncIOIf.Write(1, @ItemServerHandle, @ItemValue,TransID,CancelID,Errors);
18     if Succeeded(Result) then
19     begin
20       Result := Errors[0];
21       CoTaskMemFree(Errors);
22     end;
23   end;
24 end;

在主程序中,调用该函数可以实现向服务器写数据,如下代码所示:

1 procedure TForm1.Button2Click(Sender: TObject);
 2 begin
 3   HR := AsyncWriteItemValue(GroupIf, Item1Handle, Edit2.Text);
 4   if Succeeded(HR) then
 5   begin
 6     ListBox1.Items.Append('Item 1 value written Asynchronously');
 7   end
 8   else begin
 9     ListBox1.Items.Append('Failed to write item 1 value Asynchronously');
10   end;
11 end;

上述代码很简单,就是在按钮单击的时候进行一次写操作。

以上即完成了整个OPC开发的流程。