opcnetapi

OPCNETAPI

 

1 DA.. 1

1 .1 组件核心内容... 1

1.2  服务器的枚举和连接... 1

1.3  增加、删除组和项... 2

1.4 浏览地址空间... 4

1.5 读取数据... 5

2 HDA.. 6

2.1 OPC历史数据规范... 6

2.2 服务器的枚举和连接... 6

2.3  地址空间... 7

2.4 HDA项的历史数据... 7

2.5  平均值和记数... 8

 

 

 

 1 DA

1 .1 组件核心内容


            组件内实现了各种类型,各种规范的OPC服务器。如图一所示,采用抽象工厂模式,通过使用接口IFactoryIServer增加了代码的可重用性。
           
命名空间Opc下包括:接口IServer为所有OPC服务提供公共功能;接口IFactoryOPC服务实例化提供公共功能;IDiscovery搜索网络中计算机上已安装的OPC服务器;类Server,实现接口IServer,所有OPC服务的基础类;类Factory,实现接口IFactory,所有实例化工厂的基础类;还有类ItemIdentifierType等。
           
命名空间Opc.Da下包括:接口IServer,为所有数据存取服务提供公共功能;接口ISubscription,对数据存取服务器的订阅,包含一系列项,相当于规范中的组;类Item,实现一个项的功能;类Server,实现本命名空间下的接口IServer,并继承自Opc下的类Server,提供所有的数据存取服务功能;还有类SubscriptionProperty等。
            
命名空间OpcCom下包括:Factory, 实例化基于COMOPC服务;还有类Interop等。命名空间OpcCom.Da20下包括:Server,实现基于COMOPC数据存取服务,类Subscription实现2.0版本服务器的订阅功能。

1.2  服务器的枚举和连接
      
   遵照Visual Studio.NET的要求,要使用这些组件,如图二所示,还需要将组件OpcNetApi.dllOpcNetCom.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);
    }


1.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;//
应该删除哪一个组。currentTreeView控件的一个节点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; //
在哪一个组中添加数据项。currentTreeView控件的一个节点Node,前文创建的组对象保留在current.Tag中。
    subscription.AddItems(items);
删除一个数据项
    subscription.RemoveItems(new ItemIdentifier[] { item });//subscription
为包含数据项item的组,成员函数RemoveItems只接受ItemIdentifier数组类型的参数,其中ItemIdentifierItem的父类。

1.4 浏览地址空间
       
要手工键入数据项的路径(ItemPath)和名字(ItemName)比较麻烦,应用软件一般提供数据存取服务器的名字空间浏览,供操作者选择。OPC基金会提供的组件中使用了组合设计模式,类BrowseElement实现了该模式,一个BrowseElement包含了许多BrowseElementItem,而一个Item则不再包含其它元素。文中实例编写了一个如图三所示的对话框,左边为TreeView控件,调用递归函数BrowseAddress列出所有的数据项Item;右边为ListView控件,列出数据项对应的属性Property
    


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下所有的数据项,将这些项显示在控件TreeViewnode节点下。
        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,由递归函数使用。
}

1.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控件中显示数据项的值等信息。
……//
省略错误捕获等操作。
    }

2 HDA

2.1 OPC历史数据规范
       OPC
历史数据服务器实现两个逻辑意义上的对象,每个对象包含一个或多个接口。
IOPCHDA_ServerIOPCHDA_SyncRead IOPCHDA_Browser(后文简称 HDA项)是这两
个逻辑对象包含的重要接口。历史数据客户端软件根据功能要求,获取对应的接口,再调用接口,从服务器获得所需要的数据。

2.2 服务器的枚举和连接

      
下面的代码用来浏览某台计算机上已安装的历史数据服务器。
 Opc.IDiscovery m_discovery = new OpcCom.ServerEnumerator();
Opc.Server[] servers = m_discovery.GetAvailableServers(Specification.COM_HDA_10, host,
null);// COM_HDA_10
历史数据1.0版本,host为计算机名,null表示不需要任何网络安全认证。servers即为需要连接的OPC历史数据服务器的集合。找到服务器xpServer,可建立与该服务器的连接。
Opc.Hda.Server xpServer=null;//
定义历史数据服务器
…//
将从前文游览到的某一个OPC历史数据服务器赋给xpServer
 Try{
本课题得到国家自然科学基金(60574030)资助。
…//
将从前文浏览到的某一个OPC历史数据服务器赋给xpServer
try{
  xpServer.Connect();//
建立连接。
         …
}
 catch (Exception f){//
捕获错误,提高软件的健壮性,后文的代码都省略这一段。
  …//
错误处理
 }


2.3  地址空间 
         
地址空间由服务器的IOPCHDA_Browser接口提供,客户端由此可查找到服务器中哪些
项拥有历史数据。与数据存取规范相同,OPC历史数据服务器提供两种方式的地址空间:平直型(Flat)、层次型(Hierarchical)。要在.NET中激活COM对象,需要通过RCW(运行库可调用包装)在托管的.NET代码和未托管的COM代码之间生成一个代理[4]
,
手工编排COM中的接口定义语言。IOPCHDA_Browser接口的编排方法如下。
[Guid("1F1217B1-DEE0-11d2-A5E5-000086339399)"],//
接口IOPCHDA_BrowserGUID
 InterfaceType( ComInterfaceType.InterfaceIsIUnknown )] 
internal interface IOPCHDA_Browser{
   void  GetEnum([in]int dwBrowseType, [out]Intptr ppIEnumString ); 
void  ChangeBrowsePosition([in]int dwBrowseDirection, 
[in, MarshalAs(UnmanagedType.LPWStr)]string szString ); 
void  GetItemID ([in, MarshalAs(UnmanagedType.LPWStr)]string szNode, 
[out]Intptr pszItemID);
void  GetBranchPosition ( [out]Intptr pszBranchPos );
 }
IOPCHDA_Browser中的方法递归调用,可搜索地址空间。


2.4 HDA项的历史数据
       OPC NET组件使用两种方法读HDA项的数据,一是通过Trend,一是直接操作HDA项。
先介绍 Trend 方法,Trend HDA 项的集合,相当于数据存取规范中的组 Group。客户端创建
Trend ,再将 HDA 项加入其中, Trend 进行 ReadRaw(读原始数据)、ReadProcessed(读聚合数据)等操作, Trend 内所有 HDA项的相应操作完成。如下方法可创建一个 Trend
Trend trend = new Trend(m_server); //
在服务器m_server中创建Trend
trend.Name = "
分配台"; //Trend名不进行聚合运算,修改此值后,可对其实行各种聚合运算。
trend.AggregateID = AggregateID.NOAGGREGATE; 
trend.StartTime = new Time(new DateTime(2002,10,12,15,43,08,0));//
起始时间
trend.EndTime = new Time(new DateTime(2002,11,12,15,44,28,0)); //
终止时间
trend.MaxValues = 0;//
一次最多读值个数,0表示读时间段内所有数据。
trend.IncludeBounds = false;//
不包括边界。
     m_server.Trends.Add(trend); //
trend添加到服务器m_server中。
在名为分配台Trend中增加三个HDA项。
 Item[] items = new Item[3]; //Item
HDA项的类,创建一个数组。
 for( int i=0;i<3;i++ ) {
 items[i] = new Item();
 items[i].ItemName = m_strItemName[i]; //m_strItemName
在地址空间内找到的HDA项名。     items[i].ItemPath = null;//HDA项没有路径。
   items[i].ClientHandle = Guid.NewGuid().ToString(); //
生成客户端句柄。
  }
  IdentifiedResult[] results= m_server.CreateItems(items);//
m_server创建HDA项。
  if (results != null){ //
创建成功
    foreach (IdentifiedResult item in results){
     if (item.ResultID.Succeeded()){
      trend.Items.Add(new Item(item)); //***
HDA项加入到trend中。
            } } }
完成前述工作后,一条语句m_results = trend.ReadRaw()即可完成读操作,并将结果存放
在类型为ItemValueCollection[]的数组m_results中。下面的代码在视图中显示第一个HDA项的结果。
ItemValueCollection i_r_s = m_results[0];
foreach (ItemValue iValue in i_r_s) {
   string strTime = Opc.Convert.ToString(iValue.Timestamp);//
时间戳
   string strValue = Opc.Convert.ToString(iValue.Value); //

   string strQuality = Opc.Convert.ToString(iValue.Quality); //
原始的数据品质
//
将枚举型的历史数据品质转换为字符串。
   string strHdaQuality = Opc.Convert.ToString(iValue.HistorianQuality); 
……
}
第二种方法直接操作HDA项则不涉及到Trend的操作。先在历史数据服务器中创建HDA
,成功后再将这些HDA项放到一个Item数组中。具体代码除两个变化外与Trend操作第二段相同,增加两个变量Item[] m_readItems =null; j=0;将用*标记的一句替换为m_readItems[j++] = new Item(item);读操作变为:
Opc.Hda.Time start = new Time(new DateTime(2002,10,12,15,43,08,0)); //
起始时间
Opc.Hda.Time end = new Time(new DateTime(2002,10,12,15,44,28,0)); //
终止时间
m_results = m_server.ReadRaw(start,end,0,false,m_readItems); //
读所有数据,不包括边界值
历史数据服务器在2002101215430秒到15450秒之间有43:0343:0843:1343:1843:23…44:2344:28…44:58这些时间的历史数据。前文代码中的MaxValuesIncludeBounds0true时,则返回的数据包括43:0843:13…44:2344:28;IncludeBoundsfalse,左边界值43:08返回,右边界值44:28则没有;MaxValuesIncludeBounds3true,则返回的数据只有三个43:0843:1343:18

2.5  平均值和记数
 
同样,组件使用Trend和直接操作两种方法读HDA项的聚合值,包括求平均值、总和、方差、插值等,文章以求平均值和记数为例。Trend方法和前文相同,创建Trend,再将HDA项加入其中,Trend进行聚合运算。下面的代码直接操作HDA,不用到Trend,求平均值。
Opc.Hda.Time start = new Time(new DateTime(2002,10,27,15,43,08,0)); //
起始时间
  Opc.Hda.Time end = new Time(new DateTime(2002,10,27,15,43,27,0)); //
终止时间
  foreach(Item item in m_readItems)
  item.AggregateID = AggregateID.AVERAGE;//
求平均值,COUNT为记数。
  m_results = m_server.ReadProcessed(start,end,5,m_readItems);//
时间间隔为5秒。
返回的结果 m_results 使用前述方法显示。历史数据服务器的某一 HDA 项在 2002
1027154308秒到154327秒之间有表一所示的数据,表中及后文省略时间戳中的日期。

求平均值的聚合运算将原始数据品质为 good 的数据求和,再除以数据个数,返回结果的
时间戳为每一个时间段的起始。前述的执行结果返回四个值,15:43:08 15:43:12 5 秒钟
(时间间隔)内有两个值,15:43:11 的值不参与计算,因为其原始数据品质为 bad,则平均值为4.8,时间戳为15:43:08;15:43:1315:43:175秒钟内有三个值,平均值= 4.9+4.5+4.7 /3=4.715:43:18 15:43:22 5 秒钟内有两个值,平均值=4.5+4.7/2=4.6;15:43:23 15:43:27 内没有数据。最终显示表二所示的结果:

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值