回 帖 发 新 帖 刷新版面

主题:[转帖]研究C#程序与基于COM的OPC数据存取服务器交换数据

研究C#程序与基于COM的OPC数据存取服务器交换数据
何海江
(湖南经济管理干部学院计算机系 ,湖南长沙 410004)
摘要:研究了OPC NET COM自动化包装器所封装的COM编排技术和OPC NET应用程序接口内的设计模式。在其基础上使用C#语言编写了一个基于.NET的OPC客户程序,与基于COM的OPC数据存取服务器交换数据。有效地实现了将遵循OPC规范的代码从COM平台移植到.NET平台。
关键词:C#;COM编排;数据存取服务器;设计模式

  Study C# program to exchange data with COM based OPC data access server
He Hai-jiang
      ( Department of Computer Science of Hunan Economic Management College ,Hunan Changsha 410004 )
Abstract: The marshal technique from OPC NET COM Wrapper and design patterns from OPC NET API are researched. An OPC client software based on .NET is built on the basis of these technique with C# language,it is employeed to exchange data with OPC data access server based on COM.It is effectively realized to migrate the code following OPC specifications from COM plantform to .NET plantform.
Key Words: C#;COM marshal;data access server;design pattern

   C#是微软为.NET平台量身订作的新程序语言,特别适合组件的开发,当前软件工业中许多产品的新版本逐步转移到的.NET平台。OPC是工业软件中广泛采用的数据交换协议,许多数据采集设备都要求提供实现OPC数据存取规范的服务器,和上层软件交换数据。因此研究在C#中实现OPC有很大的现实意义。OPC数据存取规范从1.0版本到2.05,再到3.0版本,微软的分布式应用软件开发平台从COM到.NET,导致了许多的不兼容问题。笔者从事的工业过程数据仓库系统中就存在这样的情况,许多供应商的OPC服务器是在COM平台开发的,而应用软件需要在.NET框架下实现,要求和已经安装好的那些COM平台服务器通讯,必须解决如何使OPC应用在两个平台间无缝迁移。
COM和.NET是两个差别很大的分布式应用软件开发平台,如果要在.NET 应用中使用COM对象,需要通过RCW(runtime-callable wrapper, 运行环境可调用包装器)在可管理的.NET代码和未托管的COM代码之间生成一个代理。Visual Studio.NET提供了一个工具类型库导入器TlbImp.exe,可以方便地提取COM程序的信息。实现了自动化接口的OPC服务器可以使用这种方法。但大多数的OPC服务器只实现定制接口,该方法无法实现,需要手工编排COM中的IDL(Interface Definition Language,接口定义语言),工作量非常大。编排过程中会遇到包括数据类型转换、接口实现、参数传递等许多问题的困扰,自动化软件要求高可靠性,要编写这样的软件,软件测试和维护的工作量很大。现在OPC基金会对会员提供了OpcRcw动态链接库,OPC NET COM 包装器和OPC NET API,前两者完成了前述的工作,后者将OPC复杂的规范封状成简单易用的C#类。本文分析了这些组件的核心代码,在此两种技术的基础上,建立一个.NET框架的OPC客户软件,与基于COM的OPC数据存取服务器交换数据。文中提供一个应用实例,代码全部使用C#实现,分五个步骤说明。
1 组件核心内容
OPC基金会提供的这些组件,包括源代码和部分文档,但文档部分比较简单,在软件设计过程中,大量使用了抽象工厂模式和策略设计模式。要理解这些设计模式,需要阅读这方面的书籍,设计模式经典书为Erich Gamma 等四人所著《Design Patterns  Elements of Reusable Software 》。文章将OPC客户端的典型应用逐一列出,读者理解文章内容后,无需花费大量时间去读懂OPC基金会的这些代码,就比较容易编写相应的程序。









                 图一 Server和Factory的UML描述图
组件内实现了各种类型,各种规范的OPC服务器。如图一所示,采用抽象工厂模式,通过使用接口IFactory和IServer增加了代码的可重用性。
命名空间Opc下包括:接口IServer为所有OPC服务提供公共功能;接口IFactory为OPC服务实例化提供公共功能;IDiscovery搜索网络中计算机上已安装的OPC服务器;类Server,实现接口IServer,所有OPC服务的基础类;类Factory,实现接口IFactory,所有实例化工厂的基础类;还有类ItemIdentifier、Type等。
命名空间Opc.Da下包括:接口IServer,为所有数据存取服务提供公共功能;接口ISubscription,对数据存取服务器的订阅,包含一系列项,相当于规范中的组;类Item,实现一个项的功能;类Server,实现本命名空间下的接口IServer,并继承自Opc下的类Server,提供所有的数据存取服务功能;还有类Subscription、Property等。
命名空间OpcCom下包括:类Factory, 实例化基于COM的OPC服务;还有类Interop等。命名空间OpcCom.Da20下包括:类Server,实现基于COM的OPC数据存取服务,类Subscription实现2.0版本服务器的订阅功能。
2服务器的枚举和连接
遵照Visual Studio.NET的要求,要使用这些组件,如图二所示,还需要将组件OpcNetApi.dll和OpcNetCom.dll加入引用。在程序中使用using,加入这些命名空间。
using Opc;
using Opc.Da;
using OpcCom;
  下面的代码用来浏览某台计算机上已安装的数据存取规范服务器。
    private Opc.IDiscovery m_discovery = new OpcCom.ServerEnumerator();//定义枚举基于COM服务器的接口,用来搜索所有的此类服务器。
    Opc.Server[] servers = m_discovery.GetAvailableServers(daver, host, null);
    //daver表示数据存取规范版本,Specification.COMDA_20等于2.0版本。
    //host为计算机名,null表示不需要任何网络安全认证。
    if (servers != null){
        foreach (Opc.Da.Server server in servers)    {
            //server即为需要连接的OPC数据存取服务器。
        }
}

图二 添加两个组件的引用
下面的代码建立与某服务器的连接。
private Opc.Da.Server m_server=null;//定义数据存取服务器
…//从前文浏览到的某一个OPC数据存取服务器赋给m_server。
try{
        m_server.Connect();//建立连接。
         …
}
    catch (Exception f){//捕获错误,提高软件的健壮性。
                MessageBox.Show(f.Message);
    }
3 增加、删除组和项
OPC NET API使用类Subscription来封装组的操作,下面的代码第一段增加一个组,第二段删除一个组。
    Opc.Da.Subscription subscription = null;//定义一个对服务器的订阅者
    Opc.Da.SubscriptionState state = new Opc.Da.SubscriptionState();    //订阅者状态,相当于OPC规范中组的参数,为方便说明,后段用组代替订阅者。
    state.Name = "仪表";//组名
    state.ServerHandle = null;//服务器给该组分配的句柄。
    state.ClientHandle = Guid.NewGuid().ToString();//客户端给该组分配的句柄。
    state.Active = true;//激活该组。
    state.UpdateRate = 1000;//刷新频率为1秒。
    state.Deadband = 0;// 死区值,设为0时,服务器端该组内任何数据变化都通知组。
    state.Locale = null;//不设置地区值。
    subscription = (Opc.Da.Subscription)m_server.CreateSubscription(state);//创建组
    subscription.DataChanged += new DataChangedCallback(m_opcListView.OnDataChange);
     //注册事件,一旦服务器端数据有变化,自动触发。此处使用了C#的事件处理机制,笔者编写模块m_opcListView.OnDataChange,读该组的数据,后文中有该模块的代码。
删除一个组
Subscription subscription = (Subscription)current.Tag;//应该删除哪一个组。current为TreeView控件的一个节点Node,前文创建的组对象保留在current.Tag中。
m_server.CancelSubscription(subscription);//m_server前文已说明,通知服务器要求删除组。
subscription.Dispose();//强制.NET资源回收站回收该subscription的所有资源。
类Item数据项对象,是OPC的数据单元,一个组内允许定义多个数据项,可读可写,每个数据项有值(Value)、品质(Quality)、时间戳(TimeStamp)等属性。下面的代码第一段增加一个项,第二段删除一个项。
    Item[] items = new Item[1];//本次操作只添加一个数据项。
    items[0] = new Item();//创建一个项Item对象。
    items[0].ClientHandle = Guid.NewGuid().ToString();//客户端给该数据项分配的句柄。
    items[0].ItemPath = path; //该数据项在服务器中的路径。
    items[0].ItemName = name; //该数据项在服务器中的名字。
    Subscription subscription = (Subscription)current.Tag; //在哪一个组中添加数据项。current为TreeView控件的一个节点Node,前文创建的组对象保留在current.Tag中。
    subscription.AddItems(items);
删除一个数据项
    subscription.RemoveItems(new ItemIdentifier[] { item });//subscription为包含数据项item的组,成员函数RemoveItems只接受ItemIdentifier数组类型的参数,其中ItemIdentifier是Item的父类。
4 浏览地址空间
要手工键入数据项的路径(ItemPath)和名字(ItemName)比较麻烦,应用软件一般提供数据存取服务器的名字空间浏览,供操作者选择。OPC基金会提供的组件中使用了组合设计模式,类BrowseElement实现了该模式,一个BrowseElement包含了许多BrowseElement和Item,而一个Item则不再包含其它元素。文中实例编写了一个如图三所示的对话框,左边为TreeView控件,调用递归函数BrowseAddress列出所有的数据项Item;右边为ListView控件,列出数据项对应的属性Property。
    

            图三  OPC服务器地址空间浏览

BrowseFilters m_filters = new BrowseFilters();//选择性的浏览地址空间。
    m_filters.ReturnAllProperties  = true; //获取数据项的属性
    m_filters.ReturnPropertyValues = true; //要求返回属性的值
    TreeNode node = new TreeNode(m_server.Name);

tvItem.Nodes.Add(node);//在控件中加入根节点,即图二中的OPC服务器。
    BrowseAddress(node,null);//浏览根节点所包括的子项BrowseElement。过程Browse下文列出。
    private void BrowseAddress (TreeNode node,BrowseElement parent)
{//递归函数,浏览parent下所有的数据项,将这些项显示在控件TreeView的node节点下。
        if( parent!=null && parent.IsItem==true )
            return;//如果BrowseElement对象是Item,则说明是组合的最后一级,终止递归。
        try{
            ItemIdentifier itemID = null;//BrowseElement和Item共同的父类。
            if (node.Tag != null && node.Tag.GetType() == typeof(BrowseElement))
            {//该节点是BrowseElement对象,而不是根节点。
                parent = (BrowseElement)node.Tag;
                itemID = new ItemIdentifier(parent.ItemPath, parent.ItemName);
            }
            BrowsePosition position = null;//地址空间巨大,则需要此使用此对象,一般不用。
            BrowseElement[] elements = m_server.Browse(itemID, m_filters, out position);
            if (elements != null){//浏览到服务器m_server对应itemID所包含的元素。
                foreach (BrowseElement element in elements){
                    TreeNode newnode = AddBrowseElement(node, element);//加入到TreeView
                    BrowseAddress(newnode,element);//递归调用
                }
         ……
    }
private TreeNode AddBrowseElement(TreeNode previou, BrowseElement element)
{//将浏览到的BrowseElement对象加入到控件TreeView中。
    TreeNode node = new TreeNode(element.Name);
    node.Tag = element;//将BrowseElement对象记录到节点。
    previou.Nodes.Add(node);//将节点加入到TreeView中。
    return node;// 返回node,由递归函数使用。
}
5 读取数据
使用了C#的事件处理机制, 将OnDataChange注册到事件,一旦服务器端数据有变化,自动触发此过程。
    public void OnDataChange(object subscriptionHandle, ItemValueResult[] values)
    {
        if (InvokeRequired){//保证过程运行,其它控件响应事件也不能影响。
            BeginInvoke(new DataChangedCallback(OnDataChange), new object[] { subscriptionHandle, values });//系统调用。
            return;
        }
    try{
            foreach (ItemValueResult item in values){//处理每一个ItemValueResult
                if (item.ClientHandle == null){//服务器发过来的无用信息。
                        continue;
                }
    string quality = "";//数据品质
            if (item.QualitySpecified){
                  ……//如果要求了数据品质,则将其转换成为字符串。
                }
             string[] columns = new string[]{//ListView控件有四列。
                    item.ItemPath+item.ItemName, Opc.Convert.ToString(item.Value),
                    quality,
(item.TimestampSpecified) ? Opc.Convert.ToString(item.Timestamp) : "",
                    item.ResultID.ToString() };
                ListViewItem ladd = new ListViewItem(columns);
                ladd.Tag = item; //将ItemValueResult对象记录到节点。
                listView.Items.Add(ladd);//在ListView控件中显示数据项的值等信息。
……//省略错误捕获等操作。
    }
6 结束语
实例在Windows XP专业版,.NET 框架1.1,Visual Studio.NET 2003下调试通过。基于COM的OPC数据存取服务器安装在Windows XP或Windows 2000下。注意,如果要连接远程服务器,需要使用DCOM(分布式COM)配置,运行Dcomcnfg.exe (Windows 2000)或使用控制面板管理工具的组件服务(Windows XP),在OPC服务器程序的启动权限和存取权限加上一个everyone用户,其它如位置、常规和标识等配置一般不作变化。极少数OPC数据存取服务器的DCOM配置十分复杂,要详细查阅其附带的手册说明,除变更安全权限外,还需要修改标识等属性页的内容。

参考文献
[1] OPC Foundation•OPC .NET API Overview Version 1.00[EB]•2003.12
[2] OPC Foundation•OPC Data Access Custom Interface Specification 3.0[EB]•2003.3
[3] 何海江•构建基于OPCDA.NET的OPC服务器[J]•自动化博览,2003.5
作者简介:何海江(1970—),男,湖南望城人,讲师,软件工程硕士,主要研究方向:工业过程数据仓库、组件技术。Email: haijianghe@sohu.com
通信地址:410004 湖南长沙市青园路湖南经济管理干部学院计算机系
         电话: (0731)5681299,139-73124423

回复列表 (共1个回复)

沙发

谢谢,我找这样的文章很久了,可还是没找到有

我来回复

您尚未登录,请登录后再回复。点此登录或注册