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.
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.
[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:
try{
$server.Connect( $url, $connectdata)
try {
}
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.
[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.
Add-type -Path C:\OPC\OpcComRcw.dll
Add-type -Path C:\OPC\OpcNetApi.Com.dll
Add-type -Path C:\OPC\OpcNetApi.dll
[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
{
[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{
$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