Einstieg in OPC UA ── Daten von SPS und Anlagen mit einer Windows-App erfassen

· · Windows, OPC UA, Fertigung, SPS, C#, .NET, Kommunikation, Zertifikate

Auf X posten Auf XING teilen
Änderungsverlauf (Erstfassung, veröffentlicht am 1. Nov 2026)
Erstveröffentlichung
Diesen Artikel zitieren(DOI: 10.5281/zenodo.23968397)

Dieser Artikel ist auf Zenodo archiviert. Unten stehen sowohl der DOI, der immer auf die neueste Fassung verweist, als auch der DOI, der auf die gerade gelesene Fassung festgelegt ist.

Go Komura (2026). Einstieg in OPC UA ── Daten von SPS und Anlagen mit einer Windows-App erfassen. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968397 https://comcomponent.com/de/blog/opc-ua-windows-data-collection/

DOI (neueste Fassung)
10.5281/zenodo.23968397
DOI (diese Fassung)
10.5281/zenodo.23968398

Wenn Sie Daten von SPS oder Anlagen mit einer Windows-App erfassen und die Anlage einen OPC-UA-Server hat, müssen Sie unter Umständen nicht für jeden Hersteller eine eigene Kommunikation bauen. Verbinden und Werte lesen reicht allerdings nicht. Wer den Status und die Zeitstempel, die mit dem Wert zurückkommen, die Abonnements, über die Änderungen gemeldet werden, die Verbindungsfreigabe per Zertifikat und die Rückkehr nach einem Verbindungsabbruch nicht kennt, kann den erfassten Daten später nicht trauen.

Dieser Artikel erklärt die Teile von OPC UA, die man für die Datenerfassung braucht, zusammen mit den Ergebnissen, die ein C#-Client tatsächlich geliefert hat. Für den Client habe ich die .NET-Bibliothek der OPC Foundation verwendet, als Gegenstelle einen simulierten Anlagenserver. Das Verhalten mit einer bestimmten SPS oder Anlage habe ich nicht getestet. Die Einstellungen einzelner Anlagen prüfen Sie bitte in deren Unterlagen.

Das Erfassen von Bildern von GigE-Vision- und USB3-Vision-Kameras behandle ich in „Dropped Frames with GigE Vision and USB3 Vision Cameras“, Anlagen mit serieller Kommunikation in „Fallstricke bei seriellen Kommunikationsanwendungen“.

Der Unterschied zwischen OPC UA und OPC Classic

Die Spezifikationen von OPC Classic tauschen Daten zwischen Softwarekomponenten über COM/DCOM von Windows aus. OPC DA für Prozessdaten, OPC A&E für Alarme und OPC HDA für historische Daten sind als getrennte Spezifikationen festgelegt1.

2008 veröffentlichte die OPC Foundation OPC UA (Unified Architecture). OPC UA ist eine serviceorientierte Architektur, die von keiner bestimmten Plattform abhängt, und fasst die Funktionen der einzelnen Classic-Spezifikationen zusammen. Als Gründe für die Neuentwicklung nennt die OPC Foundation unter anderem, dass Microsoft plattformübergreifende Mechanismen stärker gewichtete als COM/DCOM, dass die Technik auch auf Nicht-Microsoft-Systemen und eingebetteten Geräten laufen musste und dass sichere Verbindungen über Firewalls hinweg nötig waren1.

Die Spezifikation von OPC UA ist auch bei der IEC als Normenreihe IEC 62541 genormt2. Für die Kommunikation ist bei der IANA der Port 4840/TCP als opcua-tcp registriert3.

Der Unterschied zwischen OPC Classic und OPC UAEin Diagramm, das zeigt, dass OPC Classic auf COM und DCOM von Windows aufbaut und in die getrennten Spezifikationen DA, A&E und HDA aufgeteilt ist, während OPC UA deren Funktionen in einer plattformunabhängigen Spezifikation zusammenfasst.Funktionen zusammenfassenOPC ClassicLäuft auf COM/DCOM (Windows)Getrennte Spezifikationen DA, A&E, HDAOPC UA (2008)Von keiner bestimmten Plattform abhängigGenormt als IEC 62541

Abbildung 1: OPC Classic besteht aus getrennten Spezifikationen auf COM/DCOM, OPC UA fasst sie zu einer zusammen.

Gibt es in einer bestehenden Anlage nur einen OPC-DA-Server, braucht man auf der Client-Seite einen Mechanismus, der COM/DCOM verwendet. Dieser Artikel behandelt nur OPC UA.

In der Abbildung kennzeichnet eine durchgezogene Linie eine stets geltende Beziehung und eine gestrichelte Linie eine bedingte (die Bedingungen stehen bei jeder Beziehung auf der Detailseite). Die vollständige Liste der Beziehungen (13 insgesamt, mit Beleg und Sicherheitsgrad) und die Definitionen der wichtigsten Konzepte sind auf der Detailseite der Wissenskarte (auf Japanisch) zusammengestellt. Daten: JSON-LD / Turtle

Der getestete Aufbau

Getestet habe ich mit folgendem Aufbau.

Rolle Verwendet
Client C#, .NET 10 SDK 10.0.114, NuGet-Paket OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1
Server Eine simulierte Anlage mit Python 3.13 und asyncua 2.1.0 (Port 4840 auf demselben PC)
Betriebssystem Linux (die Ausführung unter Windows habe ich nicht getestet)

Die Client-Bibliothek ist die von der OPC Foundation veröffentlichte .NET-Implementierung. Für die Verwendung der 2.0-Reihe habe ich mich an Getting started im selben Repository gehalten4.

Der getestete AufbauEin Diagramm des Aufbaus, in dem sich ein C#-Client über die .NET-Bibliothek der OPC Foundation verschlüsselt mit einem simulierten asyncua-Anlagenserver verbindet, der auf Port 4840 lauscht.opc.tcp, SignAndEncryptC#-Client (LineCollector)Simulierter Anlagenserver (asyncua).NET-Bibliothek der OPC Foundation 2.0.1Vier Variablen unter Line1

Abbildung 2: Auf demselben PC verband sich der C#-Client mit dem simulierten Anlagenserver.

Der simulierte Anlagenserver

Anstelle einer Anlage habe ich mit asyncua einen kleinen Server gebaut5. Unter dem Objekt Line1 liegen vier Variablen: Temperatur (Temperature), Stückzahl (PartCount), in Betrieb (Running) und Sollwert (Setpoint). Die Temperatur ändert sich alle 0,5 Sekunden, die Stückzahl alle 2 Sekunden. Als Sicherheit habe ich zwei Varianten bereitgestellt: ohne Verschlüsselung (None) und mit Signatur und Verschlüsselung (SignAndEncrypt mit Basic256Sha256).

# Simulierte Anlage zum Testen. Kein Ersatz für echte Anlagen oder SPS.
import asyncio, math, random, socket
from pathlib import Path
from cryptography.x509.oid import ExtendedKeyUsageOID
from asyncua import Server, ua
from asyncua.crypto.cert_gen import setup_self_signed_certificate

HERE = Path(__file__).parent
APP_URI = "urn:komurasoft:demo-plc"


async def main():
    key, cert = HERE / "server_key.pem", HERE / "server_cert.der"
    await setup_self_signed_certificate(
        key, cert, APP_URI, socket.gethostname(),
        [ExtendedKeyUsageOID.SERVER_AUTH], {"commonName": "DemoPlcServer"})

    server = Server()
    await server.init()
    server.set_endpoint("opc.tcp://0.0.0.0:4840/demo/")
    server.set_server_name("DemoPlcServer")
    await server.set_application_uri(APP_URI)
    server.set_security_policy([
        ua.SecurityPolicyType.NoSecurity,
        ua.SecurityPolicyType.Basic256Sha256_SignAndEncrypt,
    ])
    await server.load_certificate(str(cert))
    await server.load_private_key(str(key))

    ns = await server.register_namespace("urn:komurasoft:demo-plc:line1")
    line1 = await server.nodes.objects.add_object(ua.NodeId("Line1", ns), "Line1")
    temperature = await line1.add_variable(ua.NodeId("Line1.Temperature", ns), "Temperature", 25.0)
    part_count = await line1.add_variable(
        ua.NodeId("Line1.PartCount", ns), "PartCount", ua.Variant(0, ua.VariantType.UInt32))
    await line1.add_variable(ua.NodeId("Line1.Running", ns), "Running", True)
    setpoint = await line1.add_variable(ua.NodeId("Line1.Setpoint", ns), "Setpoint", 30.0)
    await setpoint.set_writable()

    async with server:
        print("listening", flush=True)
        t, count = 0, 0
        while True:
            await asyncio.sleep(0.5)
            t += 1
            value = 25.0 + 2.0 * math.sin(t / 4) + random.uniform(-0.05, 0.05)
            await temperature.write_value(round(value, 2))
            if t % 4 == 0:
                count += 1
                await part_count.write_value(ua.Variant(count, ua.VariantType.UInt32))


asyncio.run(main())

Dieser Server akzeptiert verbundene Clients, ohne ihre Zertifikate zu prüfen. Das ist eine Testeinstellung und entspricht nicht dem Verhalten einer echten Anlage.

Der C#-Client

In die Projektdatei habe ich die folgenden zwei Pakete aufgenommen.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="2.0.1" />
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
  </ItemGroup>

</Project>

Der Client verbindet sich, browst, liest, schreibt und abonniert in dieser Reihenfolge. Was die einzelnen Teile bedeuten, erkläre ich in den folgenden Abschnitten der Reihe nach.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Client.Subscriptions;
using Opc.Ua.Client.Subscriptions.Streaming;
using MonitoredItemOptions = Opc.Ua.Client.Subscriptions.MonitoredItems.MonitoredItemOptions;

string url = args.Length > 0 ? args[0] : "opc.tcp://localhost:4840/demo/";
int notifications = args.Length > 1 ? int.Parse(args[1]) : 6;
const string LineNamespace = "urn:komurasoft:demo-plc:line1";

HostApplicationBuilder builder = Host.CreateApplicationBuilder();
builder.Logging.SetMinimumLevel(LogLevel.Warning);

builder.Services
    .AddOpcUa()
    .AddClient(options =>
    {
        options.ApplicationName = "LineCollector";
        options.ApplicationUri = "urn:localhost:LineCollector";
        options.ProductUri = "urn:komurasoft:LineCollector";
        options.PkiRoot = Path.Combine(AppContext.BaseDirectory, "pki");
    })
    .AddDiscoveryAndConnect(options =>
    {
        options.DiscoveryUrl = url;
        options.SecurityMode = MessageSecurityMode.SignAndEncrypt;
        options.SecurityPolicyUri = SecurityPolicies.Basic256Sha256;
    });

using IHost host = builder.Build();
await host.StartAsync();
using var timeout = new CancellationTokenSource(TimeSpan.FromMinutes(2));
CancellationToken ct = timeout.Token;

try
{
    var connect = host.Services
        .GetRequiredService<Func<CancellationToken, Task<ManagedSession>>>();
    ManagedSession session = await connect(ct);
    await using (session)
    {
        Console.WriteLine($"接続: {session.ConfiguredEndpoint?.EndpointUrl}");

        // Den Namespace-Index legt der Server fest. Aus der URI nachschlagen und daraus NodeIds bilden
        ushort ns = (ushort)session.NamespaceUris.GetIndex(LineNamespace);
        var line1 = new NodeId("Line1", ns);
        var temperature = new NodeId("Line1.Temperature", ns);
        var partCount = new NodeId("Line1.PartCount", ns);
        var setpoint = new NodeId("Line1.Setpoint", ns);

        // Browse: die Variablen unter Line1 auflisten
        var browser = new Browser(session)
        {
            BrowseDirection = BrowseDirection.Forward,
            ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
            IncludeSubtypes = true,
            NodeClassMask = (uint)NodeClass.Variable,
        };
        Console.WriteLine("Line1 の変数:");
        foreach (ReferenceDescription child in await browser.BrowseAsync(line1, ct))
        {
            Console.WriteLine($"  {child.DisplayName}  {child.NodeId}");
        }

        // Lesen: nicht nur den Wert, sondern auch den Status und die beiden Zeitstempel ansehen
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // Liest man einen nicht vorhandenen Knoten, kommt statt eines Werts ein Bad-Status zurück
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // Schreiben: Setpoint auf 32.5 setzen
        var write = new WriteValue
        {
            NodeId = setpoint,
            AttributeId = Attributes.Value,
            Value = new DataValue(new Variant(32.5)),
        };
        WriteResponse wr = await session.WriteAsync(null, new[] { write }, ct);
        Console.WriteLine($"Setpoint の書き込み: {wr.Results[0]}");

        // Abonnieren: den Server um Abtastung alle 500 ms bitten und sich Änderungen melden lassen
        Console.WriteLine($"購読({notifications}件まで):");
        await foreach (DataValueChange change in session.DefaultStreaming
            .SubscribeDataChangesAsync(new[] { temperature, partCount }, options: new MonitoredItemOptions
            {
                SamplingInterval = TimeSpan.FromMilliseconds(500),
                QueueSize = 5,
            }, ct: ct)
            .TakeAsync(notifications)
            .WithCancellation(ct))
        {
            DataValue v = change.Value;
            string name = change.MonitoredItem?.Name.Split("s=")[^1] ?? "?";
            Console.WriteLine($"  {Hms(v.SourceTimestamp)}  {name,-17} {v.WrappedValue,6}  {v.StatusCode}");
        }
    }
}
catch (ServiceResultException ex)
{
    Console.Error.WriteLine($"OPC UA エラー {ex.StatusCode}: {ex.Message}");
    Environment.ExitCode = 1;
}
finally
{
    await host.StopAsync();
}

static string Hms(DateTimeUtc time) => time.ToDateTime().ToString("HH:mm:ss.fff");

Beim ersten Start kam keine Verbindung zustande. Den Grund erkläre ich im Abschnitt „Verbindungen per Zertifikat freigeben“. Die folgende Ausgabe entstand, nachdem ich dem Zertifikat des Servers vertraut hatte. Die Warnprotokolle der Bibliothek habe ich weggelassen.

接続: opc.tcp://localhost:4840/demo/
Line1 の変数:
  Temperature  ns=2;s=Line1.Temperature
  PartCount  ns=2;s=Line1.PartCount
  Running  ns=2;s=Line1.Running
  Setpoint  ns=2;s=Line1.Setpoint
Temperature = 23.09  状態 Good [0x00000000]
  Source 13:40:51.302 / Server 13:40:51.302
Line1.Pressure の読み取り: BadNodeIdUnknown [0x80340000]
Setpoint の書き込み: Good [0x00000000]
購読(6件まで):
  13:40:51.302  Line1.Temperature  23.09  Good [0x00000000]
  13:40:50.801  Line1.PartCount       67  Good [0x00000000]
  13:40:51.802  Line1.Temperature  23.05  Good [0x00000000]
  13:40:52.304  Line1.Temperature  23.07  Good [0x00000000]
  13:40:52.806  Line1.Temperature   23.2  Good [0x00000000]
  13:40:52.806  Line1.PartCount       68  Good [0x00000000]

Die Uhrzeiten sind UTC.

Anlagendaten erscheinen als Knoten im Adressraum

Ein OPC-UA-Server stellt seine Daten als Sammlung von Knoten bereit. Diese Sammlung heißt Adressraum. Es gibt verschiedene Arten von Knoten, etwa Objekte, die Ordner oder Anlagen darstellen, und Variablen, die Werte haben, und die Knoten sind über Referenzen miteinander verbunden. Der Client folgt den Referenzen mit dem Dienst Browse und ermittelt so, welche Knoten es gibt6.

In der Ausgabe oben folgt der Client von Line1 aus den hierarchischen Referenzen und findet die vier Variablen.

Der Adressraum der simulierten AnlageEin Diagramm, das zeigt, dass unter dem Ordner Objects das Objekt Line1 liegt und darunter die vier Variablen Temperature, PartCount, Running und Setpoint.ObjectsLine1 (Objekt)Temperature (Variable)PartCount (Variable)Running (Variable)Setpoint (Variable, beschreibbar)

Abbildung 3: Der Adressraum der simulierten Anlage. Der Client findet die Knoten, indem er mit Browse den Referenzen folgt.

Eine NodeId besteht aus Namespace und Bezeichner

Knoten werden über die NodeId identifiziert. ns=2;s=Line1.Temperature in der Ausgabe bedeutet, dass der Namespace-Index 2 ist und der Bezeichner die Zeichenkette Line1.Temperature.

Hinter einem Namespace steht eine URI. Die Spezifikation legt fest, dass Namespace-URIs innerhalb der Dienste als Zahlen dargestellt werden und diese Zahl der Position im NamespaceArray des Servers entspricht. Index 0 ist der OPC-UA-Namespace http://opcfoundation.org/UA/7.

Deshalb schreibt man den Indexwert nicht fest in den Code, sondern schlägt den Index aus der URI nach. Im Code oben ermittelt session.NamespaceUris.GetIndex(LineNamespace) aus der URI die 2, und daraus werden die NodeIds gebildet.

Wie aus einer Namespace-URI eine NodeId entstehtEin Diagramm, das zeigt, dass der Client die Namespace-URI im NamespaceArray des Servers nachschlägt, den Index erhält und aus diesem Index und dem Bezeichner die NodeId bildet.Namespace-URINamespaceArray des ServersIndex (in diesem Beispiel 2)NodeId ns=2;s=Line1.TemperatureBezeichner Line1.Temperature

Abbildung 4: Den Namespace-Index aus der URI nachschlagen und mit dem Bezeichner zur NodeId kombinieren.

Lesen und Schreiben liefern mit dem Wert auch Status und Zeitstempel

Die Werte von Variablen werden mit den Diensten Read und Write gelesen und geschrieben8. Beim Lesen kommt nicht nur der Wert zurück. In einer Struktur namens DataValue kommen Wert, Statuscode, Quellzeitstempel und Serverzeitstempel zurück9.

Feld Bedeutung
Wert Der Wert der Variablen. Zeigt der Statuscode einen Fehler an, wird der Wert ignoriert und der Server setzt ihn auf null
Statuscode (StatusCode) Ob der Server den Wert erhalten und bereitstellen konnte
SourceTimestamp Die Zeit, die die Datenquelle dem Wert gegeben hat
ServerTimestamp Die Zeit, zu der der Server den Wert erhalten hat oder wusste, dass er korrekt ist

Die oberen zwei Bits des Statuscodes bedeuten Good (Erfolg), Uncertain (teilweiser Erfolg, je nach Zweck nicht verwendbar) oder Bad (Fehler)10. Good [0x00000000] in der Ausgabe ist Good.

Beim Lesen des nicht vorhandenen Line1.Pressure kam statt eines Werts BadNodeIdUnknown [0x80340000] zurück. ReadValueAsync dieser Bibliothek löst bei einem Bad-Status eine Ausnahme aus4. Wenn ein System Werte aufzeichnet und den Statuscode neben dem Wert mitspeichert, lassen sich die Werte, die während eines Bad-Status aufgezeichnet wurden, später unterscheiden.

Der Inhalt eines DataValueEin Diagramm, das zeigt, dass ein DataValue, wie er beim Lesen und bei Benachrichtigungen zurückkommt, vier Teile hat, Wert, Statuscode, Quellzeitstempel und Serverzeitstempel, und dass der Wert bei einem Bad-Statuscode ignoriert wird.bei BadDataValueWertStatuscodeSourceTimestamp (Zeit der Quelle)ServerTimestamp (Zeit des Servers)Wert ignorieren

Abbildung 5: Ein Leseergebnis enthält nicht nur den Wert, sondern auch einen Statuscode und zwei Zeitstempel.

Beim Schreiben habe ich 32.5 in Setpoint geschrieben, und als Statuscode des Ergebnisses kam Good zurück. Ob eine Variable beschreibbar ist, entscheidet der Server. In diesem simulierten Server ist nur Setpoint beschreibbar. Ob man die Sollwerte einer Anlage von außen ändern können soll, betrifft das Sicherheitskonzept der Anlage und wird in diesem Artikel nicht behandelt.

Änderungen melden lassen: Abonnements und MonitoredItems

Um Änderungen von Werten zu verfolgen, kann der Client Read in einem festen Intervall wiederholen. OPC UA hat daneben einen Mechanismus, der die Überwachung der Werte dem Server überlässt und sich Änderungen melden lässt: Abonnements (Subscriptions) und MonitoredItems.

Ein MonitoredItem ist ein einzelnes überwachtes Ziel und hat ein Abtastintervall. Das Abtastintervall ist das schnellste Intervall, in dem der Server Werte von der Quelle abtastet, und gilt in der Spezifikation als „Best-Effort“-Zyklus11. Erfüllt eine Abtastung die Bedingung des Filters, entsteht eine Benachrichtigung. Ohne Filter wird benachrichtigt, wenn sich der Wert oder der Status ändert12.

Benachrichtigungen kommen zunächst in die Warteschlange des MonitoredItem. Für den Fall, dass die Warteschlange überläuft, legt man fest, ob die ältesten oder die neuesten verworfen werden, und ab einer Warteschlangengröße von 2 wird im Statuscode das Overflow-Bit gesetzt, das das Verwerfen anzeigt13.

Ein Abonnement fasst mehrere MonitoredItems zusammen und sendet in jedem Publishing-Intervall die noch nicht gesendeten Benachrichtigungen an den Client. Folgen Zyklen ohne Benachrichtigungen aufeinander, sendet es Keep-alives, die anzeigen, dass das Abonnement noch lebt14.

Der Ablauf von Abonnements und MonitoredItemsEin Diagramm, das zeigt, dass ein MonitoredItem die Werte der Quelle im Abtastintervall abtastet, die Benachrichtigungen, die den Filter passieren, in eine Warteschlange stellt und das Abonnement sie im Publishing-Intervall gesammelt an den Client sendet und Keep-alives sendet, wenn es keine Benachrichtigungen gibt.AbtastintervallPublishing-Intervallohne BenachrichtigungenQuelle (Werte der Anlage)MonitoredItemFilter (Standard: Änderung von Wert oder Status)Warteschlange (bei Überlauf Overflow)AbonnementClientKeep-alive

Abbildung 6: Das MonitoredItem tastet ab und sammelt in der Warteschlange, das Abonnement sendet gesammelt an den Client.

Im Code oben habe ich Temperature und PartCount mit einem Abtastintervall von 500 Millisekunden und einer Warteschlangengröße von 5 abonniert15. In der Ausgabe kommt die Temperatur etwa alle 0,5 Sekunden, die Stückzahl etwa alle 2 Sekunden. Die ersten beiden Einträge waren die Werte, die die Variablen zum Beginn des Abonnements hatten. 13:40:50.801 bei der Stückzahl ist die Zeit, zu der sich der Wert vor Beginn des Abonnements geändert hatte. Bei den Benachrichtigungen wird der Quellzeitstempel angezeigt, also nicht die Empfangszeit der Benachrichtigung, sondern die Zeit, zu der sich der Wert geändert hat.

Wiederholtes Lesen oder Abonnement

Beim wiederholten Lesen bestimmt der Client das Intervall, und der Mechanismus ist einfach. Dafür gehen Anfrage und Antwort jedes Mal hin und her, auch wenn sich der Wert nicht geändert hat, und Änderungen, die kürzer als das Intervall sind, werden übersehen. Beim Abonnement tastet der Server ab und sendet nur Änderungen, sodass auch bei Werten, die sich selten ändern, der Datenverkehr kaum wächst. Auch beim Abonnement werden Änderungen, die kürzer als das Abtastintervall sind, aber womöglich nicht gemeldet. Die Spezifikation erlaubt eine schnellere Abtastung als angegeben, verlangt sie aber nicht11.

Der Unterschied zwischen wiederholtem Lesen und AbonnementEin Diagramm, das zeigt, dass beim wiederholten Lesen der Client das Intervall bestimmt und jedes Mal anfragt, beim Abonnement der Server im Abtastintervall abtastet und nur Änderungen sendet, und dass beide Änderungen, die kürzer als das Intervall sind, unter Umständen nicht erfassen.Änderungen eines Werts verfolgenWiederholt lesenAbonnierenClient bestimmt das Intervall, fragt jedes Mal anServer tastet ab, sendet nur ÄnderungenÄnderungen kürzer als das Intervall gehen eventuell verloren

Abbildung 7: Mit beiden Methoden können Änderungen, die kürzer als das Intervall sind, verloren gehen.

Verbindungen per Zertifikat freigeben

Bei einer OPC-UA-Verbindung wählt man den Schutzgrad der Nachrichten mit MessageSecurityMode. Die Werte sind None ohne Schutz, Sign nur mit Signatur und SignAndEncrypt mit Signatur und Verschlüsselung16. Die Kombination der Algorithmen für Signatur und Verschlüsselung legt die Sicherheitsrichtlinie fest. In diesem Artikel habe ich Basic256Sha256 verwendet.

Für Signatur und Verschlüsselung verwendet jede Anwendung ein eigenes Zertifikat. Client und Server prüfen gegenseitig ihre Zertifikate und lehnen Verbindungen mit Gegenstellen ab, denen sie nicht vertrauen. Ein Server, der mit der .NET-Bibliothek der OPC Foundation gebaut ist, bietet die unverschlüsselte Richtlinie None standardmäßig nicht an4.

Die erste Verbindung scheiterte mit BadCertificateUntrusted

Beim ersten Lauf vertraute der Client dem Zertifikat des Servers nicht, und die Verbindung wurde abgelehnt. Ein Auszug aus der Ausgabe:

fail: Opc.Ua.CertificateValidationCore[91]
      Certificate [Subject=CN=DemoPlcServer, CN=urn:komurasoft:demo-plc, Thumbprint=4E236DDA629FE2AB32D185BF681609FD6C525363, NotBefore=2026-11-01, NotAfter=2027-11-01, KeyAlgorithm=1.2.840.113549.1.1.1] validation failed with suppressible errors but was rejected. Reason=Id: BadCertificateUntrusted
...
OPC UA エラー BadCertificateUntrusted [0x801A0000]: [801A0000] (BadCertificateUntrusted) 'Could not verify security on OpenSecureChannel response.'

Diese Bibliothek speichert abgelehnte Zertifikate im Speicher rejected. Verschiebt ein Administrator sie von dort in den Speicher trusted, vertraut die Bibliothek dieser Gegenstelle17. Hier habe ich das Zertifikat des Servers aus pki/rejected/certs nach pki/trusted/certs verschoben, erneut ausgeführt und konnte mich verbinden. Vor dem Verschieben prüft man den Fingerabdruck, der auch im Dateinamen steht, mit dem Administrator des Servers4.

Der Weg bis zum Vertrauen in ein ZertifikatEin Diagramm, das zeigt, dass der Client bei der ersten Verbindung das Zertifikat des Servers ablehnt und im Speicher rejected ablegt und dass er nach Prüfung des Fingerabdrucks und dem Verschieben in den Speicher trusted bei der nächsten Verbindung vertraut und sich verbinden kann.Erste VerbindungBadCertificateUntrustedZertifikat im Speicher rejected abgelegtFingerabdruck mit dem Server-Administrator prüfenIn den Speicher trusted verschiebenNächste Verbindung: Vertrauen und Verbindung

Abbildung 8: Wer ein abgelehntes Zertifikat prüft und nach trusted verschiebt, vertraut dieser Gegenstelle.

Es gibt auch die Einstellung AutoAcceptUntrustedCertificates, die nicht vertrauenswürdige Zertifikate automatisch akzeptiert. Der Standard ist false, und die Unterlagen der Bibliothek schreiben, dass sie in Entwicklungsumgebungen nützlich ist, in der Produktion aber nicht verwendet werden sollte17.

NotAfter=2027-11-01 im Protokoll ist das Ablaufdatum des Zertifikats des simulierten Servers. Es ist ein selbstsigniertes Zertifikat, das mit der asyncua-Funktion erzeugt wurde, und lief ein Jahr nach dem Erstellungsdatum ab. Auch die Zertifikate von Anlagen haben ein Ablaufdatum, deshalb legen Sie im Betrieb ein Verfahren fest, mit dem Sie sie vor dem Ablauf austauschen und die Gegenstelle ihnen vertrauen lassen. Den Umgang mit dem Windows-Zertifikatspeicher fasse ich in „Windows-Zertifikatspeicher in der Praxis“ zusammen.

Authentifizierung des Benutzers

Mit Zertifikaten werden Anwendungen gegenseitig geprüft. Unabhängig davon übergibt man beim Öffnen einer Sitzung ein Token, das den Benutzer ausweist. Die Spezifikation kennt vier Arten: anonym, Benutzername und Passwort, X.509-Zertifikat und extern ausgestellte Tokens18. Der Client hier hat sich anonym verbunden. Welche Tokens ein Server akzeptiert, steht in den Informationen der Endpunkte des Servers19.

Wo die Datenerfassung in der Praxis hakt

Im Folgenden nenne ich, gestützt auf das Experiment und die Spezifikation, Probleme, die bei Systemen zur Erfassung von Anlagendaten häufig auftreten.

Werte aus der Zeit, in der der Server stand, kommen nicht an

Während des Abonnements habe ich den Server angehalten und nach etwa 15 Sekunden neu gestartet. ManagedSession dieser Bibliothek verbindet sich bei einem Verbindungsabbruch automatisch neu20. Während der Server stand, versuchte der Client immer wieder, sich neu zu verbinden.

warn: Opc.Ua.ChannelManager[6]
      Reconnect failed. Endpoint=opc.tcp://localhost:4840/demo/, Attempt=3, Outcome=transient-failure, Status=BadNotConnected, ErrorMessage=Could not connect to the remote endpoint.

Als der Server zurück war, kamen auch die Benachrichtigungen zurück. Ein Auszug aus der Ausgabe des Abonnements:

  13:41:13.364  Line1.Temperature  25.61  Good [0x00000000]
  13:41:13.865  Line1.Temperature  25.12  Good [0x00000000]
  13:41:34.285  Line1.Temperature  26.55  Good [0x00000000]
  13:41:33.784  Line1.PartCount        2  Good [0x00000000]
  13:41:34.786  Line1.Temperature  26.16  Good [0x00000000]

Von 13:41:13 bis 13:41:34 ist kein einziger Wert angekommen. Die Stückzahl ist von 78 auf 2 zurückgegangen. Das liegt daran, dass der simulierte Server neu gestartet wurde und wieder bei 0 zu zählen begann.

In diesem Beispiel stand der Server selbst, deshalb sind die Werte aus dieser Zeit nirgends gespeichert. Der Wiederverbindungsmechanismus stellt die Verbindung wieder her, füllt aber die Werte aus der Ausfallzeit nicht nachträglich auf.

Anders ist es, wenn nur die Kommunikation unterbrochen war und der Server weiterlief. Die Spezifikation hat den Dienst Republish, mit dem man sich nicht angekommene Benachrichtigungsnachrichten aus der Wiederholungswarteschlange des Servers erneut senden lässt21. Auch in der Warteschlange des MonitoredItem sammeln sich Benachrichtigungen, aber was über die Warteschlangengröße hinausgeht, wird verworfen13. Diesen Fall habe ich im Experiment nicht getestet. In beiden Fällen sollten Sie die Zeiten von Verbindungsabbruch und Wiederverbindung aufzeichnen, damit erkennbar ist, in welchem Zeitraum keine Werte kamen. Wiederverbindung und Log-Design bei Anlagen mit serieller Kommunikation behandle ich auch in „Fallstricke bei seriellen Kommunikationsanwendungen“.

Aufzeichnung beim Anhalten des ServersEin Diagramm, das zeigt, dass der Client bei angehaltenem Server während des Abonnements wiederholt versucht, sich neu zu verbinden, dass mit der Rückkehr des Servers auch die Benachrichtigungen zurückkommen, die Werte aus der Ausfallzeit aber nicht ankommen, weshalb man die Zeiten von Verbindungsabbruch und Wiederverbindung aufzeichnet.Während des AbonnementsServer stopptClient versucht wiederholt neu zu verbindenServer zurück, Benachrichtigungen auchWerte aus der Ausfallzeit kommen nicht anZeiten von Abbruch und Wiederverbindung aufzeichnen

Abbildung 9: Die Wiederverbindung stellt die Verbindung her, aber die Werte aus der Ausfallzeit kommen nicht an.

Welchen Zeitstempel man aufzeichnet

Laut Spezifikation ist der SourceTimestamp die Zeit, die die Datenquelle dem Wert gegeben hat, und soll die Zeit angeben, zu der sich der Wert geändert hat. Der ServerTimestamp ist die Zeit, zu der der Server den Wert erhalten hat oder wusste, dass er korrekt ist9. Der simulierte Server hier erzeugt die Werte selbst, deshalb waren beide Zeiten gleich.

Wenn Sie wissen wollen, wann sich der Wert in der Anlage geändert hat, verwenden Sie den SourceTimestamp. Diese Zeit wird aber mit der Uhr der Quelle gesetzt, und geht die Uhr der Anlage oder des Servers falsch, ist auch die Aufzeichnung verschoben. Wenn Sie die erfassten Daten mit anderen Aufzeichnungen abgleichen, speichern Sie die Zeit des empfangenden PCs daneben, dann können Sie die Abweichung später prüfen. Die Zeitsynchronisation auf der Windows-Seite fasse ich in „Windows-Zeitsynchronisation (w32time) und Unternehmenssysteme“ zusammen.

Werte mit einem anderen Status als Good nicht einfach als Zahlen behandeln

Bei einem Bad-Statuscode soll der Wert ignoriert werden9. Bei Uncertain ist der Wert je nach Zweck nicht verwendbar10. Sortieren Sie die Werte vor Diagrammen und Auswertungen nach dem Statuscode und legen Sie fest, wie Werte behandelt werden, die nicht Good sind. Auch das Overflow-Bit bei einem Warteschlangenüberlauf wird im Statuscode gesetzt13. Wie man den Zustand von Anlagen auf dem Bildschirm darstellt, behandle ich in „Best Practices für die Prüfung und Anzeige des Zustands externer Geräte“.

Den Port in der Firewall öffnen

Der bei der IANA registrierte Port für OPC UA ist 4840/TCP3. Allerdings verwenden nicht alle Server diese Nummer. Die .NET-Beispiele der OPC Foundation verwenden unter anderem den Port 625414. Prüfen Sie die Portnummer in der Endpunkt-URL der Anlage und erlauben Sie Verbindungen zu diesem Port. Läuft der Server unter Windows, geschieht das in dessen Windows-Firewall, und liegt Netzwerktechnik dazwischen, auf diesen Geräten. Wie man eingehende Regeln anlegt, fasse ich in „Windows-Firewall und Fachanwendungen“ zusammen.

Wo die Datenerfassung haktEin Diagramm, das die Stellen, an denen die Erfassung von Anlagendaten hakt, aufteilt in den Inhalt der Aufzeichnung (Werte aus der Ausfallzeit des Servers kommen nicht an, welcher Zeitstempel aufgezeichnet wird, Umgang mit Werten, die nicht Good sind) und die Vorbereitung der Verbindung (Port in der Firewall, Ablauf der Zertifikate).Wo die Datenerfassung haktInhalt der AufzeichnungVorbereitung der VerbindungWerte aus der Ausfallzeit fehlenWelcher ZeitstempelUmgang mit Werten außer GoodPort in der FirewallAblauf der Zertifikate

Abbildung 10: Auch wenn die Verbindung steht, prüfen Sie Lücken in der Aufzeichnung, Zeiten, Status, Ports und Zertifikate.

Abgrenzung zu Modbus/TCP und herstellereigenen Protokollen

Für die Kommunikation mit SPS und Anlagen gibt es neben OPC UA auch Modbus/TCP und Protokolle, die der SPS-Hersteller bereitstellt.

Die Modbus-Spezifikation behandelt Daten in vier Tabellen: schreibgeschützte 1-Bit-Discrete-Inputs, lesbare und beschreibbare 1-Bit-Coils, schreibgeschützte 16-Bit-Input-Register und lesbare und beschreibbare 16-Bit-Holding-Register22. Bei der IANA ist 502/TCP für das Modbus-Anwendungsprotokoll (mbap) registriert3.

In diese Tabellen passen nur Bits und 16-Bit-Wörter, deshalb prüft man in den Unterlagen der Anlage, welcher Wert in welchem Format an welcher Adresse liegt, und hinterlegt das auf der Client-Seite. Bei OPC UA stellt der Server, wie gesehen, benannte Variablen im Adressraum bereit, und der Client kann sie mit Browse ermitteln. Zu den Werten gehören Statuscode und Zeitstempel, und die Verbindung wird per Zertifikat freigegeben.

Was man verwendet, hängt oft davon ab, was die Anlage anbietet. Hat die Anlage einen OPC-UA-Server, nutzt man ihn; gibt es nur Modbus/TCP, erstellt man eine Adresszuordnung und liest danach. Gibt es nur das Herstellerprotokoll oder ein SDK, folgt man dessen Unterlagen. Wie man den Kommunikationsweg mit USB-Geräten wählt, fasse ich in „USB-Geräte aus einer Windows-App ansprechen“ zusammen.

Den Kommunikationsweg zur Anlage wählenEin Diagramm, das zeigt, dass man bei einer Anlage mit OPC-UA-Server die Variablen per OPC UA browst und erfasst, bei nur Modbus/TCP Adressen und Formate in den Unterlagen prüft und die Register liest und bei nur einem Herstellerprotokoll dessen Unterlagen folgt.OPC-UA-ServerNur Modbus/TCPNur HerstellerprotokollKommunikation, die die Anlage anbietetWas gibt es?Variablen browsen und erfassenAdressen und Formate in den Unterlagen prüfenUnterlagen und SDK des Herstellers folgenRegister lesen und in Werte umrechnen

Abbildung 11: Womit man erfasst, hängt oft davon ab, was die Anlage anbietet.

Verwandte Artikel

  1. OPC Foundation, Classic. Dass OPC Classic auf COM/DCOM beruht, die Spezifikationen DA, A&E und HDA, die Veröffentlichung von OPC UA 2008 und die Gründe für die Neuentwicklung. ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. Teil 1 der Normenreihe IEC 62541 zu Konzepten und Überblick von OPC UA (inzwischen durch eine neuere Ausgabe ersetzt). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. Die Registrierung von opcua-tcp auf 4840/TCP und mbap auf 502/TCP. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. Wie man einen Client der 2.0-Reihe schreibt, dass ReadValueAsync bei Bad eine Ausnahme auslöst, die Speicher für abgelehnte und vertrauenswürdige Zertifikate, dass Server None standardmäßig nicht anbieten, und die Portnummern der Beispiele. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). Die Python-Bibliothek für OPC UA, mit der der simulierte Anlagenserver gebaut wurde. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Der Dienst, der den Referenzen eines Knotens folgt. ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. Dass Namespace-URIs innerhalb der Dienste als Zahlen dargestellt werden und diese Zahl einer Position im NamespaceArray entspricht. ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Die Dienste Read und Write. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Die Definitionen von Wert, Statuscode, SourceTimestamp und ServerTimestamp. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain und Bad, angezeigt durch die oberen zwei Bits. ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. Dass das Abtastintervall ein Best-Effort-Zyklus ist. ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. Dass ohne Filter bei einer Änderung von Wert oder Status benachrichtigt wird. ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. Warteschlangengröße, Verwerfungsregel und Overflow-Bit. ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. Publishing-Intervall und Keep-alives. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync sowie Abtastintervall und Warteschlangengröße in MonitoredItemOptions. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. Die Bedeutung von None, Sign und SignAndEncrypt. ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. Das Verschieben aus dem Speicher rejected in den vertrauenswürdigen Speicher und dass AutoAcceptUntrustedCertificates in der Produktion nicht verwendet werden soll. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. Die vier Arten: anonym, Benutzername, X.509 und ausgestellte Tokens. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. Die Liste der Benutzer-Tokens, die ein Endpunkt akzeptiert (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. Dass ManagedSession einen eingebauten Wiederverbindungsmechanismus hat. ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. Der Dienst, mit dem man sich Benachrichtigungsnachrichten aus der Wiederholungswarteschlange eines Abonnements erneut senden lässt. ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b (eine Kopie auf einem Server der University of Chicago). Das Datenmodell in Abschnitt 4.3 (Discrete Input, Coil, Input Register, Holding Register). ↩

Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.

Dies sind Themenseiten, die zu diesem Thema passen. Ausgehend vom Artikel gelangen Sie zu verwandten Leistungen und weiteren Artikeln.

Dieser Artikel führt zu den folgenden Leistungsseiten. Beginnen Sie gern mit einem Einstieg, der Ihrem Anliegen nahekommt.

Windows-App-Entwicklung

Eine Windows-App, die Daten von Anlagen erfasst, einschließlich der Abonnement-Einstellungen, der Wiederverbindung und der Aufzeichnung von Zeitstempeln und Status, lässt sich als Windows-App-Entwicklung umsetzen.

Häufige Fragen

Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.

Was ist der Unterschied zwischen OPC UA und OPC Classic (OPC DA)?
OPC Classic ist eine Spezifikation, die Daten über COM/DCOM von Windows austauscht. OPC UA ist eine 2008 veröffentlichte Spezifikation, die von keinem bestimmten Betriebssystem abhängt und die Funktionen von Classic zusammenfasst. OPC-UA-Clients laufen auch außerhalb von Windows, und eine DCOM-Konfiguration ist nicht nötig.
Reicht es, die Werte bei OPC UA in einem festen Intervall zu lesen?
Wenn es wenige Werte sind und das Intervall lang ist, kann man sie auch durch Lesen erfassen. Will man Änderungen der Werte verfolgen, kann man die Abtastung per Abonnement dem Server überlassen und sich Änderungen melden lassen. In beiden Fällen zeichnen Sie den Statuscode und die Zeitstempel auf, die zusammen mit dem Wert zurückkommen.
Warum erhalte ich beim Verbinden BadCertificateUntrusted?
Weil der Client dem Zertifikat des Servers noch nicht vertraut. Die .NET-Bibliothek der OPC Foundation speichert abgelehnte Zertifikate im Speicher rejected. Prüfen Sie den Fingerabdruck des Zertifikats mit dem Administrator des Servers und verschieben Sie es dann in den Speicher trusted. Einstellungen, die Zertifikate automatisch akzeptieren, gehören nur in Entwicklungsumgebungen.
Welche Portnummer verwendet OPC UA?
Bei der IANA ist 4840/TCP als opcua-tcp registriert. Reale Server verwenden aber teils einen anderen Port, deshalb prüfen Sie die Einstellungen der Anlage. Läuft der Server unter Windows, muss die Verbindung zu diesem Port in der Windows-Firewall erlaubt werden, und liegt Netzwerktechnik dazwischen, auch auf diesen Geräten.

Autorenprofil

Profilseite des Artikelautors.

Go Komura

Vertretungsberechtigter von KomuraSoft LLC

Schwerpunkte sind Windows-Softwareentwicklung, technische Beratung und Fehleruntersuchung. Besondere Stärken liegen bei Projekten mit bestehenden Assets und bei der Untersuchung von Störungen, deren Ursache schwer erkennbar ist.

Zurück zum Blog