Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / PowerShell

Reading OPC DA Data in PowerShell

4.81/5 (7 votes)
5 Jul 2022MIT6 min read 11.9K   278  
How to read data from OPC DA servers into a PowerShell script
OPC DA is a widely used protocol in the world of industrial automation for making process data available. Client applications are typically written in VB, C++ or C#. For scripting purposes, it can be useful to have a PowerShell interface, but there are no public examples that use the raw API from www.opcfoundation.org. This article explains how to do it in a basic manner.

Image 1

Introduction

If you are active in the world of industrial Automation, chances are that you have encountered OPC DA. OPC DA is a universal standard for retrieving data from software interfaces, PLCs, measurement devices, and much more. If so, then this article is written for you.

OPC DA is quite an old standard. It's built on top of DCOM, which carries a bundle of security and network issues. However, it is still ubiquitous and being able to connect to an OPC DA server to retrieve data is still very useful.

In most cases, you don't make the connection yourself. Usually, you configure application A to connect to OPC server B to read certain values. Even if you need to program something yourself, this is usually done in VBScript or a .NET language such as VB.NET or C#. There are some examples out there which you could start from.

However, it may be interesting to be able to get some data values from an OPC server inside a powershell script. I wanted to write a script to help me with some diagnostics, and then found out there was not a single example to show how to do it in PowerShell. Fundamentally, everything to do it is available, and after figuring it out, I decided to write a small tutorial.

This article is NOT intended to be a tutorial on OPC. For the purpose of this article, basic understanding of OPC will be assumed. For more information about OPC DA, you can go to www.opcfoundation.org but note that information about OPC DA is not available for free. As a non paying member, you can get the redistributables and the API but not much more than that.

Reading Data via OPC DA in PowerShell

All in all, it's not very hard to do this. I will show each step and explain what is going on in each step.

Binding to the API

OPC DA is at its heart DCOM technology and while it is possible to use DCOM directly, the OPC foundation has .NET redistributables publicly available which provide a nice .NET shell around the DCOM interface. That's what I used because PowerShell integrates very easily with .NET classes. The redistributables you can download from www.opcfoundation.org. If you are interested in this article, more than likely you have OPC servers to test the script with. If not, many OPC vendors have OPC Servers available for simulation and testing purposes. www.kepware.com is one of them.

It's important to note that the redistributables have been built against the 32 bit version of the .NET framework. It is important that you use them with the 32 bit version of PowerShell. The .NET wrapper DLLs will load without a problem, but they won't be able to bind properly to the DCOM interfaces.

For this example, I've placed the DLLs in a folder named c:\OPC. There, in order to be able to run this script, there needs to be an OPC Server installed, or at the very least, an OPC DA client for the necessary class libraries.

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

Connecting to the Server

With the DLLs loaded, we can now connect to the OPC server. This requires a URL. For the sake of this article, we assume the server is running locally, but a network hostname or IP address may be used.

The URL consists of the protocol moniker (opcda://) the location of the server (localhost) and the name of the server. There can be multiple OPC servers on the same machine. Creating a server connection object is done via a DCOM class factory. This part is just boilerplate and is always the same except for the URL. The ConnectData is used to set up the DCOM security setting which will be used for making the connection. By default, we connect with the credentials of the current user. However, if due to vendor specific restrictions, a dedicated service account is required, this is where it can be set up.

PowerShell
[Opc.URL]$url = New-Object Opc.URL -ArgumentList "opcda://localhost/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) 

Connecting to the Server

Reading the data requires an active connection. It is good practise to make sure that every connection you open is closed in the end. OPC is DCOM based so eventually, objects would be released and connections closed anyway, but relying on that is not good design, so we do something like the following:

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

Reading the Data

This part is relatively straightforward. OPC DA supports the retrieval of data in subscriptions. If you have a non-trivial data acquisition setup, then you could set up different subscriptions with different data rates. Some data may be read every 1 second, some needs to be read only every 5 seconds, etc. For this example however, we just want to read data on an ad-hoc basis to read their current values. Instead of the term 'subscription', you will also use the term 'group which is sometimes used instead.

We're going to have a single subscription group. The group has a group state, which is set to false because we don't want an active subscription to get continuous data updates. We just want to use the subscription as a placeholder for the definition of the items we want to read. We just initialize an array of two items in this example, and then we fill in the names of the 2 OPC DA variables we want to read.

The | out-null is placed there because adding the items causes the AddItems method to echo the item configuration. This is not harmful but a bit messy when running the script.

PowerShell
[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

And that's everything there is to it! For each item that was read, there will be a OPCItemValue object. Note that each value object also has a GetType member to help you convert the item value and assign it to a variable. The OPC interface uses a variant for passing the data, so the value itself van be a string, a boolean, an integer or float, ...

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

Putting It All Together

Now that we have all the necessary pieces, we can put them together with the appropriate error handling in place.

PowerShell
# 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://localhost/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{
    $_
}   

Conclusion and Points of Interest

As you can see, reading data from an OPC server is fairly trivial if you use the .NET interface and remember to use the 32 bit version of PowerShell when needed. My code is covered under the MIT license so feel free to do with it what you want.

A final word on reading OPC DA data needs to be said. The ResultID parameter tells you the status of the value, and there is also a quality indication. These parameters inform you about whether the reading was successful and accurate. I am not going to go deeper in the different scenarios, but there is one important thing to keep in mind when doing ad-hoc queries such as these.

OPC DA is a data interface, allowing a client to connect to a server to retrieve data. That does not mean that when you make the connection, the data is instantly available. In larger systems, the total number of available different data points is in the tens of thousands. The OPC server will not be monitoring all of them, all the time. If you perform a read operation of a value that is not actively monitored already, the initial read may return with a E_FAIL or a quality-bad result on the first attempt, and only return valid values after the second read attempt, when the OPC DA server has had the chance to establish the internal data channel to the value you want to read.

History

  • 5th July, 2022: Initial version

License

This article, along with any associated source code and files, is licensed under The MIT License