Zum Inhalt springen

Anleitung

OPC Classic

Mit demselben OpcClient auf OPC DA, OPC AE und OPC HDA Server zugreifen: Funktionsumfang, Nodes, entfernte Server über DCOM und Server suchen.

Anleitung

OPC Classic ​

Mit einer Adresse der Form opc.com:// verbindet sich der OpcClient mit einem OPC Classic Server. Intern startet er dafür einen OPC UA Wrapper Server, der jede vom Classic-Server unterstützte Spezifikation (DA, AE und HDA) als eigenen Bereich seines Adressraums bereitstellt. Die Anwendung arbeitet danach ausschließlich mit der gewohnten OPC-UA-API: Lesen, Schreiben, Abonnieren, Ereignisse, Quittieren und historische Daten funktionieren mit denselben Methoden wie bei einem OPC UA Server. Der spätere Wechsel auf einen OPC UA Server ändert im Code nur die Adresse.

Voraussetzungen: Der Client läuft unter Windows, die OPC Classic Runtime ist installiert und die Anwendung verwendet das Paket Opc.UaFx.Advanced. Wie der Client die Verbindung aufbaut und wie Sie die Adresse bilden, zeigt der Abschnitt Mit einem OPC Classic Server verbinden der ersten Schritte.

Was mit OPC Classic möglich ist ​

Classic-SpezifikationMöglichSDK-MethodenBeschreibung
OPC DA (Data Access)lesen, schreiben, abonnieren, browsenReadNode(s), WriteNode(s), SubscribeDataChange, BrowseNodeWerte lesen und schreiben, Subscriptions, Nodes browsen
OPC AE (Alarms & Events)Ereignisse empfangen, Bedingungen quittieren, aktuellen Zustand anfordernSubscribeEvent, Acknowledge, RefreshConditionsEreignisse, Alarme und Bedingungen
OPC HDA (Historical Data Access)Rohwerte, Werte zu Zeitpunkten und aggregierte Werte lesen, Werte ändern und löschenReadNodeHistory, ReadNodeHistoryAtTime, ReadNodeHistoryProcessed, UpdateNodeHistory, DeleteNodeHistoryHistorische Daten

Welche Spezifikationen ein Server tatsächlich unterstützt, ermittelt der Client beim Verbinden selbst. Wie sich das beschleunigen lässt, zeigt der Abschnitt Verbindungsaufbau beschleunigen.

Nodes eines Classic-Servers ​

Beteiligte TypenOpcClientOpcNamespaceOpcNodeInfoOpcNodeId

Je Spezifikation legt der Wrapper einen eigenen Namespace an. Seine URI ist die Adresse des Classic-Servers mit der Endung /DA, /AE beziehungsweise /HDA. Weil der Index dieser Namespaces von der Reihenfolge der Spezifikationen abhängt, ermitteln Sie ihn zur Laufzeit über client.Namespaces und tragen ihn nicht fest ein. Die Nodes selbst finden Sie am zuverlässigsten durch Browsen:

csharp
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);
vbnet
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)
Next

Die Identifier der Items bildet der Wrapper aus der ItemID des Classic-Servers. Bei OPC DA sieht ein Item-Identifier zum Beispiel so aus: 0:Channel1.Device1.Tag1 im DA-Namespace. Das Präfix ist eine Konvention des Wrappers. Übernehmen Sie Identifier deshalb aus einem Browse-Ergebnis, statt sie für andere Spezifikationen selbst zu bilden.

Werte lesen, schreiben und abonnieren (DA) ​

Beteiligte TypenOpcClientOpcNamespaceOpcNodeIdOpcValueOpcStatusOpcSubscription

Mit dem Identifier eines DA-Items stehen alle Lese-, Schreib- und Subscription-Methoden zur Verfügung. Einen Item-Identifier aus dem DA-Namespace verwenden Sie wie jede andere NodeId:

csharp
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);
vbnet
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)

Browsen bei älteren DA-Servern ​

Beteiligte TypenOpcClientOpcClientInterfacesOpcClassicDataAccessOptionsOpcClassicOrganization

Ältere DA-Server melden die Organisation ihres Adressraums teilweise falsch oder unterstützen das gezielte Springen in einen Zweig (BrowseTo) nicht. Das zeigt sich zum Beispiel daran, dass das Browsen keine Items liefert oder abbricht. Über client.Interfaces.DataAccess legen Sie das Verhalten vor dem Verbinden fest:

  • Organization: Query (Standard) fragt die Organisation beim Server ab, Flat behandelt den Adressraum als eine Ebene, Hierarchical als Baum.
  • SupportsBrowseTo: true (Standard). Mit false verwendet der Wrapper BrowseTo nicht.
csharp
client.Interfaces.DataAccess.Organization = OpcClassicOrganization.Flat;
client.Interfaces.DataAccess.SupportsBrowseTo = false;
 
client.Connect();
vbnet
client.Interfaces.DataAccess.Organization = OpcClassicOrganization.Flat
client.Interfaces.DataAccess.SupportsBrowseTo = False

client.Connect()

Historische Daten (HDA) ​

Beteiligte TypenOpcClientOpcNodeIdOpcHistoryValueOpcAggregateType

Items eines HDA-Servers stehen im HDA-Namespace. Der Client liest ihre historischen Daten mit derselben API wie bei einem OPC UA Server, einschließlich aggregierter Werte. Den Identifier des Items ermitteln Sie wie oben durch Browsen:

csharp
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);
vbnet
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)
Next

Ändern und Löschen historischer Werte (UpdateNodeHistory, DeleteNodeHistory und weitere) bildet der Wrapper ebenfalls auf die HDA-Schnittstellen ab, soweit der Server sie unterstützt. Die Methoden beschreibt die Seite Historische Daten.

Alarme und Ereignisse (AE) ​

Beteiligte TypenOpcClientOpcSubscriptionOpcEventReceivedEventArgsOpcAcknowledgeableCondition

Ereignisse und Bedingungen eines AE-Servers empfängt der Client über eine Subscription auf dem Server-Objekt. Quittierbare Bedingungen kommen als OpcAcknowledgeableCondition an, Sie quittieren sie mit Acknowledge. Der Wrapper gibt die Quittierung an den AE-Server weiter. RefreshConditions fordert den aktuellen Zustand aller Bedingungen an:

csharp
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");
}
vbnet
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 Sub

Bereiche und Quellen des AE-Servers erscheinen im AE-Namespace. Welche Ereignistypen ein Server meldet, hängt vom Server ab. Das Arbeiten mit Ereignissen und Bedingungen beschreiben die Seiten Ereignisse und Alarme und Bedingungen.

Entfernte Server und DCOM-Sicherheit ​

Beteiligte TypenOpcClientOpcClientSecurityOpcClientIdentityOpcClassicAuthenticationLevelOpcClassicImpersonationLevel

Wenn der Classic-Server auf einem anderen Rechner läuft, steht dessen Name in der Adresse. Die Kommunikation läuft dann über DCOM. Eine über client.Security.UserIdentity gesetzte OpcClientIdentity verwendet der Client als Windows-Anmeldung für DCOM. Die DCOM-Sicherheit können Sie im Code festlegen, statt sie über dcomcnfg zu konfigurieren:

csharp
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();
vbnet
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()
Eigenschaft von OpcClientSecurityVerwendeter Wert, wenn nicht gesetzt
AuthenticationLevelOpcClassicAuthenticationLevel.PacketIntegrity
ImpersonationLevelOpcClassicImpersonationLevel.Impersonate
AuthenticationServiceOpcClassicAuthenticationService.WinNt
AuthorizationServiceOpcClassicAuthorizationService.None
ProxyCapabilitiesOpcClassicProxyCapabilities.None

Wenn eine dieser Eigenschaften gesetzt ist, initialisiert der Client damit die DCOM-Sicherheit für die gesamte Anwendung. Sonst bleibt sie unverändert. Der Standardwert PacketIntegrity entspricht den Empfehlungen von Microsoft zur DCOM-Härtung, siehe DCOM-Härtung (CVE-2021-26414).

Server finden ​

Beteiligte TypenOpcClassicDiscoveryClientOpcClassicIdentityOpcClassicEndpointDescriptionOpcClassicInterface

Der OpcClassicDiscoveryClient sucht Classic-Server lokal oder auf einem entfernten Rechner, dort mit einer OpcClassicIdentity für die Anmeldung. DiscoverEndpoints liefert je Server seine Beschreibung mit Adresse (Server.Uri) und die Spezifikationen, die er unterstützt:

csharp
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);
    }
}
vbnet
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 Using

Mit einer Spezifikation als Filter liefert DiscoverServers nur die Server, die sie unterstützen. Ohne Filter fragt der Client alle Spezifikationen ab. Auf Rechnern mit sehr vielen COM-Anwendungen ist der Filter deutlich schneller:

csharp
using (var discovery = new OpcClassicDiscoveryClient(host: "remote-host", identity)) {
    OpcClassicApplicationDescriptionCollection servers
            = discovery.DiscoverServers(OpcClassicInterfaces.Da30);
}
vbnet
Using discovery = New OpcClassicDiscoveryClient(host:="remote-host", identity)
    Dim servers As OpcClassicApplicationDescriptionCollection _
            = discovery.DiscoverServers(OpcClassicInterfaces.Da30)
End Using

Verbindungsaufbau beschleunigen ​

Beteiligte TypenOpcClientOpcClassicInterfaceCollectionOpcClassicEndpointDescription

Beim Verbinden fragt der Client zunächst ab, welche Spezifikationen der Server unterstützt. Wenn sie bekannt sind, zum Beispiel aus DiscoverEndpoints, geben Sie sie vorab über ServerInterfaces an. Der Client überspringt dann diese Abfrage, und der Verbindungsaufbau verkürzt sich spürbar:

csharp
var client = new OpcClient(endpoint.Server.Uri);
 
foreach (OpcClassicInterface serverInterface in endpoint.Interfaces)
    client.ServerInterfaces.Add(serverInterface);
 
client.Connect();
vbnet
Dim client = New OpcClient(endpoint.Server.Uri)

For Each serverInterface As OpcClassicInterface In endpoint.Interfaces
    client.ServerInterfaces.Add(serverInterface)
Next

client.Connect()

Verbindung überwachen ​

Beteiligte TypenOpcClientOpcClientInterfacesOpcClientStateChangedEventArgs

Wenn die DCOM-Verbindung zum Classic-Server abbricht, bemerkt der Client das standardmäßig nicht über seinen Zustand. Mit client.Interfaces.UseBreakMonitoring = true prüft der Client die Verbindung zum Classic-Server und spiegelt einen Abbruch (RPC_S_SERVER_UNAVAILABLE) auf OpcClient.State. Der Wrapper Server stoppt, der Client meldet den Verbindungsverlust und verbindet sich wie bei OPC UA neu, sobald der Classic-Server wieder erreichbar ist. Die Option ist standardmäßig aus, weil sie eine zweite Instanz des COM-Servers erzeugt. Das kann bei manchen Servern Nebenwirkungen haben:

csharp
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);
}
vbnet
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 Sub

Wie der Client Abbrüche behandelt und sich neu verbindet, beschreibt die Seite Clients für den Dauerbetrieb.