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服务器。如图一所示,采用抽象工厂模式,通过使用接口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版本服务器的订阅功能。
 

1.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);
    }


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;//应该删除哪一个组。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的父类。
 

1.4 浏览地址空间
        要手工键入数据项的路径(ItemPath)和名字(ItemName)比较麻烦,应用软件一般提供数据存取服务器的名字空间浏览,供操作者选择。OPC基金会提供的组件中使用了组合设计模式,类BrowseElement实现了该模式,一个BrowseElement包含了许多BrowseElement和Item,而一个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下所有的数据项,将这些项显示在控件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,由递归函数使用。
}
 

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_Server、IOPCHDA_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_Browser的GUID 
 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); //读所有数据,不包括边界值 
历史数据服务器在2002年10月12日15时43分0秒到15时45分0秒之间有43:03、43:08、43:13、43:18、43:23…44:23、44:28…44:58这些时间的历史数据。前文代码中的MaxValues和IncludeBounds为0和true时,则返回的数据包括43:08、43:13…44:23、44:28;IncludeBounds为false时,左边界值43:08返回,右边界值44:28则没有;MaxValues和IncludeBounds为3和true时,则返回的数据只有三个43:08、43:13、43: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 年
10月27日15时43分08秒到15时43分27秒之间有表一所示的数据,表中及后文省略时间戳中的日期。 

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值