# OPC Classic

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

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](https://www.traeger.de/docs/development/opcua/downloads#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](https://www.traeger.de/docs/development/opcua/client/getting-started#mit-einem-opc-classic-server-verbinden) der ersten Schritte.

## Was mit OPC Classic möglich ist

| Classic-Spezifikation | Möglich | SDK-Methoden | Beschreibung |
|---|---|---|---|
| OPC DA (Data Access) | lesen, schreiben, abonnieren, browsen | `ReadNode(s)`, `WriteNode(s)`, `SubscribeDataChange`, `BrowseNode` | [Werte lesen und schreiben](https://www.traeger.de/docs/development/opcua/client/read-write-values), [Subscriptions](https://www.traeger.de/docs/development/opcua/client/subscriptions), [Nodes browsen](https://www.traeger.de/docs/development/opcua/client/browse) |
| OPC AE (Alarms & Events) | Ereignisse empfangen, Bedingungen quittieren, aktuellen Zustand anfordern | `SubscribeEvent`, `Acknowledge`, `RefreshConditions` | [Ereignisse](https://www.traeger.de/docs/development/opcua/client/events), [Alarme und Bedingungen](https://www.traeger.de/docs/development/opcua/client/alarms-conditions) |
| OPC HDA (Historical Data Access) | Rohwerte, Werte zu Zeitpunkten und aggregierte Werte lesen, Werte ändern und löschen | `ReadNodeHistory`, `ReadNodeHistoryAtTime`, `ReadNodeHistoryProcessed`, `UpdateNodeHistory`, `DeleteNodeHistory` | [Historische Daten](https://www.traeger.de/docs/development/opcua/client/historical-data) |

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](https://www.traeger.de/docs/development/opcua/client/opc-classic#verbindungsaufbau-beschleunigen).

## Nodes eines Classic-Servers

Beteiligte Typen: `OpcClient`, `OpcNamespace`, `OpcNodeInfo`, `OpcNodeId`

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 Typen: `OpcClient`, `OpcNamespace`, `OpcNodeId`, `OpcValue`, `OpcStatus`, `OpcSubscription`

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 Typen: `OpcClient`, `OpcClientInterfaces`, `OpcClassicDataAccessOptions`, `OpcClassicOrganization`

Ä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 Typen: `OpcClient`, `OpcNodeId`, `OpcHistoryValue`, `OpcAggregateType`

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](https://www.traeger.de/docs/development/opcua/client/historical-data).

## Alarme und Ereignisse (AE)

Beteiligte Typen: `OpcClient`, `OpcSubscription`, `OpcEventReceivedEventArgs`, `OpcAcknowledgeableCondition`

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](https://www.traeger.de/docs/development/opcua/client/events) und [Alarme und Bedingungen](https://www.traeger.de/docs/development/opcua/client/alarms-conditions).

## Entfernte Server und DCOM-Sicherheit

Beteiligte Typen: `OpcClient`, `OpcClientSecurity`, `OpcClientIdentity`, `OpcClassicAuthenticationLevel`, `OpcClassicImpersonationLevel`

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 `OpcClientSecurity` | Verwendeter Wert, wenn nicht gesetzt |
|---|---|
| `AuthenticationLevel` | `OpcClassicAuthenticationLevel.PacketIntegrity` |
| `ImpersonationLevel` | `OpcClassicImpersonationLevel.Impersonate` |
| `AuthenticationService` | `OpcClassicAuthenticationService.WinNt` |
| `AuthorizationService` | `OpcClassicAuthorizationService.None` |
| `ProxyCapabilities` | `OpcClassicProxyCapabilities.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)](https://www.traeger.de/docs/development/opcua/faqs#dcom-hartung-cve-2021-26414).

## Server finden

Beteiligte Typen: `OpcClassicDiscoveryClient`, `OpcClassicIdentity`, `OpcClassicEndpointDescription`, `OpcClassicInterface`

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 Typen: `OpcClient`, `OpcClassicInterfaceCollection`, `OpcClassicEndpointDescription`

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 Typen: `OpcClient`, `OpcClientInterfaces`, `OpcClientStateChangedEventArgs`

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](https://www.traeger.de/docs/development/opcua/client/long-running-clients).
