65.9K
CodeProject 正在变化。 阅读更多。
Home

在 PowerShell 中读取 OPC DA 数据

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (7投票s)

2022 年 7 月 5 日

MIT

6分钟阅读

viewsIcon

12811

downloadIcon

331

如何将数据从 OPC DA 服务器读取到 PowerShell 脚本中

引言

如果您活跃在工业自动化领域,那么您很可能接触过 OPC DA。OPC DA 是一个从软件接口、PLC、测量设备等检索数据的通用标准。如果是这样,那么本文就是为您而写。

OPC DA 是一个相当老的标准。它建立在 DCOM 之上,带来了许多安全和网络问题。然而,它仍然无处不在,能够连接到 OPC DA 服务器检索数据仍然非常有用。

在大多数情况下,您不会自己建立连接。通常,您会配置应用程序 A 连接到 OPC 服务器 B 来读取某些值。即使您需要自己编程,通常也会使用 VBScript 或 .NET 语言(如 VB.NET 或 C#)。有一些现有的示例可以作为起点。

然而,能够在 PowerShell 脚本中从 OPC 服务器获取一些数据值可能会很有趣。我想写一个脚本来帮助我进行一些诊断,然后发现没有任何一个示例展示如何在 PowerShell 中做到这一点。从根本上说,所有需要的东西都已具备,在弄清楚之后,我决定写一篇小型教程。

本文无意成为 OPC 的教程。在本文中,将假定您具备 OPC 的基本知识。有关 OPC DA 的更多信息,您可以访问 www.opcfoundation.org,但请注意,有关 OPC DA 的信息并非免费提供。作为非付费会员,您可以获得可再发行组件和 API,但除此之外就没有多少内容了。

通过 PowerShell 读取 OPC DA 数据

总而言之,这并不难。我将展示每一步并解释每一步在做什么。

绑定到 API

OPC DA 的核心是 DCOM 技术,虽然可以直接使用 DCOM,但 OPC 基金会提供了公开可用的 .NET 可再发行组件,它们在 DCOM 接口之上提供了一个漂亮的 .NET 外壳。我使用的是这个,因为 PowerShell 可以非常轻松地与 .NET 类集成。您可以在 www.opcfoundation.org 下载这些可再发行组件。如果您对本文感兴趣,很可能您有 OPC 服务器可以测试脚本。如果没有,许多 OPC 供应商都提供 OPC 服务器用于仿真和测试。www.kepware.com 就是其中之一。

需要注意的是,这些可再发行组件是针对 .NET Framework 的 32 位版本构建的。在 PowerShell 的 32 位版本中使用它们非常重要。 .NET 包装器 DLL 会正常加载,但它们无法正确绑定到 DCOM 接口。

在此示例中,我将 DLL 文件放在一个名为 c:\OPC 的文件夹中。在那里,为了能够运行此脚本,需要安装一个 OPC 服务器,或者至少安装一个 OPC DA 客户端以获取必要的类库。

Add-type -Path C:\OPC\OpcComRcw.dll
Add-type -Path C:\OPC\OpcNetApi.Com.dll
Add-type -Path C:\OPC\OpcNetApi.dll 

连接到服务器

在加载了 DLL 文件后,我们现在可以连接到 OPC 服务器。这需要一个 URL。出于本文的目的,我们假设服务器在本地运行,但也可以使用网络主机名或 IP 地址。

URL 由协议标识符(opcda://)、服务器位置(localhost)和服务器名称组成。同一台机器上可以有多个 OPC 服务器。创建服务器连接对象是通过 DCOM 类工厂完成的。这部分只是样板代码,始终相同,除了 URL。ConnectData 用于设置将用于建立连接的 DCOM 安全设置。默认情况下,我们使用当前用户的凭据进行连接。但是,如果由于供应商特定的限制需要专用服务帐户,则可以在此处进行设置。

[Opc.URL]$url = New-Object Opc.URL -ArgumentList "opcda:///OPC.DeltaV.1"
[Opc.ConnectData]$connectdata = New-Object Opc.ConnectData `
         -ArgumentList (New-Object System.Net.NetworkCredential)
[OpcCom.Factory] $fact = new-object -TypeName OpcCom.Factory
[Opc.Da.Server]$server = New-Object -TypeName Opc.Da.Server -ArgumentList @($fact, $null) 

连接到服务器

读取数据需要一个活动连接。养成关闭所有打开的连接的良好习惯很重要。OPC 基于 DCOM,最终对象会被释放,连接也会关闭,但依赖于此不是好的设计,所以我们这样做:

try{
    $server.Connect( $url, $connectdata)
    try {
        #Implement read operations in this section
    }
    finally{
        $server.Disconnect()
    }
}
catch{ 
    $_
}

读取数据

这部分相对简单。OPC DA 支持通过订阅检索数据。如果您有一个非简单的数采设置,那么您可以设置不同的订阅,具有不同的数据速率。某些数据可能每 1 秒读取一次,某些数据可能只需要每 5 秒读取一次,等等。然而,在这个例子中,我们只是想即时读取数据以获取它们的当前值。除了“订阅”这个术语,您还将使用“组”这个术语,有时也会互换使用。

我们将有一个订阅组。该组有一个组状态,设置为 false,因为我们不希望活动订阅来获取连续数据更新。我们只是将此订阅用作我们要读取项的占位符。在这个例子中,我们只初始化一个包含两个项的数组,然后填写我们要读取的 2 个 OPC DA 变量的名称。

| out-null 放在那里是因为添加项会导致 AddItems 方法回显项配置。这并无害,但在运行脚本时有点混乱。

[Opc.Da.Subscription]$group
[Opc.Da.SubscriptionState]$groupState = New-Object Opc.Da.SubscriptionState
$groupState.Name = "Group"
$groupState.Active = $false
$group = $server.CreateSubscription($groupState);

[Opc.Da.Item[]] $items = New-Object Opc.Da.Item[] 2
$items[0] =  New-Object Opc.Da.Item
$items[0].ItemName = "CNT-TST301/COMM/PRI/CONGOOD"
$items[1] =  New-Object Opc.Da.Item
$items[1].ItemName = "CNT-TST301/COMM/SEC/CONGOOD"
$group.AddItems($items) |out-null
$result = $group.Read($group.Items)
$result

这就是全部!对于每个读取的项,都会有一个 OPCItemValue 对象。请注意,每个值对象都有一个 GetType 成员,可以帮助您转换项值并将其分配给变量。OPC 接口使用变体来传递数据,因此值本身可以是字符串、布尔值、整数或浮点数……

ResultID           : S_OK
DiagnosticInfo     : 
Value              : 7
Quality            : good
QualitySpecified   : True
Timestamp          : 7/3/2022 2:08:47 PM
TimestampSpecified : True
ItemName           : CNT-TST301/COMM/SEC/CONGOOD
ItemPath           : 
ClientHandle       : 
ServerHandle       : 2
Key                : CNT-TST301/COMM/SEC/CONGOOD
                     null

整合

现在我们拥有了所有必要的组件,可以将它们与适当的错误处理放在一起。

# Copyright 2022 Bruno van Dooren
#
# Permission is hereby granted, free of charge, to any person 
# obtaining a copy of this software and associated documentation files (the "Software"), 
# to deal in the Software without restriction, including without limitation 
# the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, 
# and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included 
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#import the .NET OPC types
Add-type -Path C:\OPC\OpcComRcw.dll
Add-type -Path C:\OPC\OpcNetApi.Com.dll
Add-type -Path C:\OPC\OpcNetApi.dll

#create the OPC objects we will use for setting up the connection
[Opc.URL]$url = New-Object Opc.URL -ArgumentList "opcda:///OPC.DeltaV.1"
if($url -eq $null){
    "Opc.URL is null";exit
}

[Opc.ConnectData]$connectdata = New-Object Opc.ConnectData `
    -ArgumentList (New-Object System.Net.NetworkCredential)
if($connectdata -eq $null){
    "Opc.ConnectData is null";exit
}

[OpcCom.Factory] $fact = new-object -TypeName OpcCom.Factory
if($fact -eq $null){
    "OpcCom.Factory is null";exit
}

[Opc.Da.Server]$server = New-Object -TypeName Opc.Da.Server `
    -ArgumentList @($fact, $null)
if($server -eq $null){
    "Opc.Da.Server is null";exit
}

try{
    $server.Connect( $url, $connectdata)
    try
    {
        #add some tags to a single subscription and read them
        [Opc.Da.Subscription]$group
        [Opc.Da.SubscriptionState]$groupState = New-Object Opc.Da.SubscriptionState
        $groupState.Name = "Group"
        $groupState.Active = $false
        $group = $server.CreateSubscription($groupState);

        [Opc.Da.Item[]] $items = New-Object Opc.Da.Item[] 2
        $items[0] =  New-Object Opc.Da.Item
        $items[0].ItemName = "CNT-TST301/COMM/PRI/CONGOOD"
        $items[1] =  New-Object Opc.Da.Item
        $items[1].ItemName = "CNT-TST301/COMM/SEC/CONGOOD"
        $group.AddItems($items) |out-null

        $result = $group.Read($group.Items)
        $result
    }
    finally{
        #ensure cleanup if we were able to create the connection
        $server.Disconnect()
    }
}
catch{
    $_
}   

结论和关注点

如您所见,如果您使用 .NET 接口,从 OPC 服务器读取数据是相当简单的,并且请记住在需要时使用 PowerShell 的 32 位版本。我的代码在 MIT 许可下发布,因此您可以随意使用它。

关于读取 OPC DA 数据,还有一句话需要说明。ResultID 参数告诉您值的状态,还有一个质量指示。这些参数告诉您读取是否成功以及是否准确。我将不再深入探讨不同的场景,但有一件重要的事情需要注意,当进行此类即时查询时。

OPC DA 是一个数据接口,允许客户端连接到服务器以检索数据。这并不意味着当您建立连接时,数据就会立即可用。在较大的系统中,可用的不同数据点的总数有数万个。OPC 服务器不会一直监控所有这些数据点。如果您尝试读取一个尚未主动监控的值,第一次读取可能会返回 E_FAILquality-bad 的结果,并且只有在第二次尝试读取后,当 OPC DA 服务器有机会建立到您想要读取的值的内部数据通道后,才会返回有效值。

历史

  • 2022 年 7 月 5 日:初始版本
© . All rights reserved.