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-Spezifikation | Möglich | SDK-Methoden | Beschreibung |
|---|---|---|---|
| OPC DA (Data Access) | lesen, schreiben, abonnieren, browsen | ReadNode(s), WriteNode(s), Subscribe, BrowseNode | Werte lesen und schreiben, Subscriptions, Nodes browsen |
| OPC AE (Alarms & Events) | Ereignisse empfangen, Bedingungen quittieren, aktuellen Zustand anfordern | SubscribeEvent, Acknowledge, Refresh | Ereignisse, Alarme und Bedingungen |
| OPC HDA (Historical Data Access) | Rohwerte, Werte zu Zeitpunkten und aggregierte Werte lesen, Werte ändern und löschen | ReadNodeHistory, Read, Read, Update, Delete | Historische 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:
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)
NextDie 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:
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)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,Flatbehandelt den Adressraum als eine Ebene,Hierarchicalals Baum.SupportsBrowseTo:true(Standard). Mitfalseverwendet der WrapperBrowseTonicht.
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()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:
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)
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:
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 SubBereiche 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:
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()Eigenschaft von Opc | Verwendeter Wert, wenn nicht gesetzt |
|---|---|
Authentication | Opc |
Impersonation | Opc |
Authentication | Opc |
Authorization | Opc |
Proxy | Opc |
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:
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 UsingMit 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:
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 UsingVerbindungsaufbau 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:
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()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:
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 SubWie der Client Abbrüche behandelt und sich neu verbindet, beschreibt die Seite Clients für den Dauerbetrieb.

