How-to guide
OPC Classic
Access OPC DA, OPC AE, and OPC HDA servers with the same OpcClient: features, nodes, remote servers over DCOM, and server discovery.
How-to guide
OPC Classic
With an address of the form opc.com://, the OpcClient connects to an OPC Classic server. Internally, it starts an OPC UA wrapper server that provides each specification supported by the Classic server (DA, AE, and HDA) as a separate area of its address space. From then on, your application works only with the familiar OPC UA API: reading, writing, subscribing, events, acknowledging, and historical data use the same methods as with an OPC UA server. If you later switch to an OPC UA server, only the address in your code changes.
Prerequisites: The client runs on Windows, the OPC Classic runtime is installed, and the application uses the Opc.UaFx.Advanced package. The section Connect to an OPC Classic server in "Getting started" shows how the client establishes the connection and how you build the address.
What OPC Classic supports
| Classic specification | Supported | SDK methods | Description |
|---|---|---|---|
| OPC DA (Data Access) | read, write, subscribe, browse | ReadNode(s), WriteNode(s), Subscribe, BrowseNode | Read and write values, Subscriptions, Browse nodes |
| OPC AE (Alarms & Events) | receive events, acknowledge conditions, request the current state | SubscribeEvent, Acknowledge, Refresh | Events, Alarms and conditions |
| OPC HDA (Historical Data Access) | read raw values, values at specific times, and aggregated values, modify and delete values | ReadNodeHistory, Read, Read, Update, Delete | Historical data |
The client determines which specifications a server actually supports when it connects. The section Speed up the connection shows how to make this faster.
Nodes of a Classic server
Types usedOpcClientOpcNamespaceOpcNodeInfoOpcNodeId
The wrapper creates a separate namespace for each specification. Its URI is the address of the Classic server with the suffix /DA, /AE, or /HDA. The index of these namespaces depends on the order of the specifications, so determine it at runtime through client.Namespaces instead of hard-coding it. The most reliable way to find the nodes themselves is to browse:
foreach (OpcNamespace ns in client.Namespaces)
Console.WriteLine("{0}: {1}", ns.Index, ns.Value);
OpcNodeInfo objects = client.BrowseNode(OpcObjectTypes.ObjectsFolder);
foreach (OpcNodeInfo node in objects.Children())
Console.WriteLine("{0} ({1})", node.DisplayName.Value, node.NodeId);For Each ns As OpcNamespace In client.Namespaces
Console.WriteLine("{0}: {1}", ns.Index, ns.Value)
Next
Dim objects As OpcNodeInfo = client.BrowseNode(OpcObjectTypes.ObjectsFolder)
For Each node As OpcNodeInfo In objects.Children()
Console.WriteLine("{0} ({1})", node.DisplayName.Value, node.NodeId)
NextThe wrapper builds the identifiers of the items from the ItemID of the Classic server. With OPC DA, an item identifier in the DA namespace looks like this, for example: 0:Channel1.Device1.Tag1. The prefix is a convention of the wrapper. For this reason, take identifiers from a browse result instead of building them yourself for other specifications.
Read, write, and subscribe to values (DA)
Types usedOpcClientOpcNamespaceOpcNodeIdOpcValueOpcStatusOpcSubscription
With the identifier of a DA item, all read, write, and subscription methods are available. Use an item identifier from the DA namespace like any other NodeId:
OpcNamespace da = null;
foreach (OpcNamespace ns in client.Namespaces) {
if (ns.Value.EndsWith("/DA"))
da = ns;
}
var tag = new OpcNodeId("0:Channel1.Device1.Tag1", namespaceIndex: da.Index);
OpcValue value = client.ReadNode(tag);
OpcStatus result = client.WriteNode(tag, value: 42);
OpcSubscription subscription = client.SubscribeDataChange(tag, HandleDataChanged);Dim da As OpcNamespace = Nothing
For Each ns As OpcNamespace In client.Namespaces
If ns.Value.EndsWith("/DA") Then
da = ns
End If
Next
Dim tag = New OpcNodeId("0:Channel1.Device1.Tag1", namespaceIndex:=da.Index)
Dim value As OpcValue = client.ReadNode(tag)
Dim result As OpcStatus = client.WriteNode(tag, value:=42)
Dim subscription As OpcSubscription = client.SubscribeDataChange(tag, AddressOf HandleDataChanged)Browse older DA servers
Types usedOpcClientOpcClientInterfacesOpcClassicDataAccessOptionsOpcClassicOrganization
Some older DA servers report the organization of their address space incorrectly or do not support jumping directly to a branch (BrowseTo). A symptom is that browsing returns no items or stops. Use client.Interfaces.DataAccess to set the behavior before you connect:
Organization:Query(default) asks the server for the organization,Flattreats the address space as a single level,Hierarchicalas a tree.SupportsBrowseTo:true(default). Withfalse, the wrapper does not useBrowseTo.
client.Interfaces.DataAccess.Organization = OpcClassicOrganization.Flat;
client.Interfaces.DataAccess.SupportsBrowseTo = false;
client.Connect();client.Interfaces.DataAccess.Organization = OpcClassicOrganization.Flat
client.Interfaces.DataAccess.SupportsBrowseTo = False
client.Connect()Historical data (HDA)
Types usedOpcClientOpcNodeIdOpcHistoryValueOpcAggregateType
The items of an HDA server are located in the HDA namespace. The client reads their historical data with the same API as with an OPC UA server, including aggregated values. Determine the identifier of the item by browsing, as shown above:
DateTime endTime = DateTime.UtcNow;
DateTime startTime = endTime.AddHours(-1);
var raw = client.ReadNodeHistory(startTime, endTime, hdaItemId);
var average = client.ReadNodeHistoryProcessed(
startTime,
endTime,
OpcAggregateType.Average,
hdaItemId);
foreach (var historyValue in raw)
Console.WriteLine("{0}: {1}", historyValue.Timestamp, historyValue.Value);Dim endTime As DateTime = DateTime.UtcNow
Dim startTime As DateTime = endTime.AddHours(-1)
Dim raw = client.ReadNodeHistory(startTime, endTime, hdaItemId)
Dim average = client.ReadNodeHistoryProcessed( _
startTime, _
endTime, _
OpcAggregateType.Average, _
hdaItemId)
For Each historyValue In raw
Console.WriteLine("{0}: {1}", historyValue.Timestamp, historyValue.Value)
NextThe wrapper also maps modifying and deleting historical values (UpdateNodeHistory, DeleteNodeHistory, and others) to the HDA interfaces, as far as the server supports them. The page Historical data describes these methods.
Alarms and events (AE)
Types usedOpcClientOpcSubscriptionOpcEventReceivedEventArgsOpcAcknowledgeableCondition
The client receives the events and conditions of an AE server through a subscription on the Server object. Acknowledgeable conditions arrive as OpcAcknowledgeableCondition, and you acknowledge them with Acknowledge. The wrapper forwards the acknowledgment to the AE server. RefreshConditions requests the current state of all conditions:
OpcSubscription subscription = client.SubscribeEvent(
OpcObjectTypes.Server,
HandleEventReceived);
subscription.RefreshConditions();
...
private void HandleEventReceived(object sender, OpcEventReceivedEventArgs e)
{
var condition = e.Event as OpcAcknowledgeableCondition;
if (condition != null && !condition.IsAcked)
condition.Acknowledge(client, comment: "Acknowledged by operator");
}Dim subscription As OpcSubscription = client.SubscribeEvent( _
OpcObjectTypes.Server, _
AddressOf HandleEventReceived)
subscription.RefreshConditions()
...
Private Sub HandleEventReceived(ByVal sender As Object, ByVal e As OpcEventReceivedEventArgs)
Dim condition = TryCast(e.Event, OpcAcknowledgeableCondition)
If condition IsNot Nothing AndAlso Not condition.IsAcked Then
condition.Acknowledge(client, comment:="Acknowledged by operator")
End If
End SubThe areas and sources of the AE server appear in the AE namespace. Which event types a server reports depends on the server. The pages Events and Alarms and conditions describe how to work with events and conditions.
Remote servers and DCOM security
Types usedOpcClientOpcClientSecurityOpcClientIdentityOpcClassicAuthenticationLevelOpcClassicImpersonationLevel
If the Classic server runs on another computer, the address contains the name of that computer. Communication then runs over DCOM. If you set an OpcClientIdentity through client.Security.UserIdentity, the client uses it as the Windows logon for DCOM. You can set the DCOM security in code instead of configuring it with dcomcnfg:
var client = new OpcClient("opc.com://remote-host/{E4EBF7FA-CCAC-4125-A611-EAC4981C00EA}");
client.Security.UserIdentity = new OpcClientIdentity(userName: "opcuser", password: "secret");
client.Security.AuthenticationLevel = OpcClassicAuthenticationLevel.PacketIntegrity;
client.Security.ImpersonationLevel = OpcClassicImpersonationLevel.Impersonate;
client.Connect();Dim client = New OpcClient("opc.com://remote-host/{E4EBF7FA-CCAC-4125-A611-EAC4981C00EA}")
client.Security.UserIdentity = New OpcClientIdentity(userName:="opcuser", password:="secret")
client.Security.AuthenticationLevel = OpcClassicAuthenticationLevel.PacketIntegrity
client.Security.ImpersonationLevel = OpcClassicImpersonationLevel.Impersonate
client.Connect()Property of Opc | Value used if not set |
|---|---|
Authentication | Opc |
Impersonation | Opc |
Authentication | Opc |
Authorization | Opc |
Proxy | Opc |
If one of these properties is set, the client uses it to initialize the DCOM security for the entire application. Otherwise, the DCOM security stays unchanged. The default PacketIntegrity follows the Microsoft recommendations for DCOM hardening, see DCOM hardening (CVE-2021-26414).
Find servers
Types usedOpcClassicDiscoveryClientOpcClassicIdentityOpcClassicEndpointDescriptionOpcClassicInterface
The OpcClassicDiscoveryClient searches for Classic servers locally or on a remote computer, using an OpcClassicIdentity for the logon on the remote computer. DiscoverEndpoints returns the description of each server with its address (Server.Uri) and the specifications it supports:
var identity = new OpcClassicIdentity(userName: "opcuser", password: "secret");
using (var discovery = new OpcClassicDiscoveryClient(host: "remote-host", identity)) {
foreach (OpcClassicEndpointDescription endpoint in discovery.DiscoverEndpoints()) {
Console.WriteLine("{0}: {1}", endpoint.Server.Name, endpoint.Server.Uri);
foreach (OpcClassicInterface serverInterface in endpoint.Interfaces)
Console.WriteLine(" {0}", serverInterface);
}
}Dim identity = New OpcClassicIdentity(userName:="opcuser", password:="secret")
Using discovery = New OpcClassicDiscoveryClient(host:="remote-host", identity)
For Each endpoint As OpcClassicEndpointDescription In discovery.DiscoverEndpoints()
Console.WriteLine("{0}: {1}", endpoint.Server.Name, endpoint.Server.Uri)
For Each serverInterface As OpcClassicInterface In endpoint.Interfaces
Console.WriteLine(" {0}", serverInterface)
Next
Next
End UsingWith a specification as a filter, DiscoverServers returns only the servers that support it. Without a filter, the client queries all specifications. On computers with a large number of COM applications, the filter is much faster:
using (var discovery = new OpcClassicDiscoveryClient(host: "remote-host", identity)) {
OpcClassicApplicationDescriptionCollection servers
= discovery.DiscoverServers(OpcClassicInterfaces.Da30);
}Using discovery = New OpcClassicDiscoveryClient(host:="remote-host", identity)
Dim servers As OpcClassicApplicationDescriptionCollection _
= discovery.DiscoverServers(OpcClassicInterfaces.Da30)
End UsingSpeed up the connection
Types usedOpcClientOpcClassicInterfaceCollectionOpcClassicEndpointDescription
When connecting, the client first queries which specifications the server supports. If you already know them, for example from DiscoverEndpoints, specify them in advance through ServerInterfaces. The client then skips this query, and the connection is established noticeably faster:
var client = new OpcClient(endpoint.Server.Uri);
foreach (OpcClassicInterface serverInterface in endpoint.Interfaces)
client.ServerInterfaces.Add(serverInterface);
client.Connect();Dim client = New OpcClient(endpoint.Server.Uri)
For Each serverInterface As OpcClassicInterface In endpoint.Interfaces
client.ServerInterfaces.Add(serverInterface)
Next
client.Connect()Monitor the connection
Types usedOpcClientOpcClientInterfacesOpcClientStateChangedEventArgs
By default, the state of the client does not reflect a loss of the DCOM connection to the Classic server. With client.Interfaces.UseBreakMonitoring = true, the client checks the connection to the Classic server and reflects a break (RPC_S_SERVER_UNAVAILABLE) in OpcClient.State. The wrapper server stops, the client reports the connection loss, and it reconnects as soon as the Classic server is reachable again, just as with OPC UA. The option is off by default because it creates a second instance of the COM server. With some servers, this can have side effects:
client.Interfaces.UseBreakMonitoring = true;
client.StateChanged += HandleStateChanged;
client.Connect();
...
private static void HandleStateChanged(
object sender,
OpcClientStateChangedEventArgs e)
{
Console.WriteLine("{0} -> {1}", e.OldState, e.NewState);
}client.Interfaces.UseBreakMonitoring = True
AddHandler client.StateChanged, AddressOf HandleStateChanged
client.Connect()
...
Private Shared Sub HandleStateChanged( _
ByVal sender As Object, _
ByVal e As OpcClientStateChangedEventArgs)
Console.WriteLine("{0} -> {1}", e.OldState, e.NewState)
End SubThe page Long-running clients describes how the client handles connection breaks and reconnects.

