Wprowadzenie do OPC UA ── Zbieranie danych ze sterowników PLC i urządzeń w aplikacji Windows

· · Windows, OPC UA, Produkcja, PLC, C#, .NET, Komunikacja, Certyfikaty

Opublikuj w X
Historia zmian (tylko pierwsza wersja, opublikowana 01.11.2026)
Pierwsza publikacja
Jak cytować ten artykuł(DOI: 10.5281/zenodo.23968417)

Ten artykuł jest zarchiwizowany w Zenodo. Poniżej podano zarówno identyfikator DOI, który zawsze wskazuje najnowszą wersję, jak i DOI przypisany do wersji aktualnie wyświetlanej.

Go Komura (2026). Wprowadzenie do OPC UA ── Zbieranie danych ze sterowników PLC i urządzeń w aplikacji Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968417 https://comcomponent.com/pl/blog/opc-ua-windows-data-collection/

DOI (najnowsza wersja)
10.5281/zenodo.23968417
DOI (ta wersja)
10.5281/zenodo.23968418

Gdy dane ze sterownika PLC lub urządzenia mają trafiać do aplikacji Windows, a urządzenie ma serwer OPC UA, czasem można uniknąć pisania osobnej komunikacji dla każdego producenta. Samo połączenie i odczyt wartości to jednak za mało. Jeśli nie zna się stanu i znaczników czasu zwracanych razem z wartością, subskrypcji powiadamiających o zmianach, zezwalania na połączenie za pomocą certyfikatów ani sposobu powrotu po rozłączeniu, później nie da się ufać zebranym danym.

Ten artykuł omawia te elementy OPC UA, które są potrzebne do zbierania danych, wraz z wynikami rzeczywistego uruchomienia klienta w C#. Klient korzysta z biblioteki .NET od OPC Foundation, a po drugiej stronie działa symulowany serwer urządzenia. Nie sprawdzano działania z żadnym konkretnym sterownikiem PLC ani urządzeniem. Ustawienia właściwe dla danego urządzenia sprawdź w jego dokumentacji.

Zbieranie obrazów z kamer GigE Vision i USB3 Vision opisano w artykule „Dropped Frames with GigE Vision and USB3 Vision Cameras”, a urządzenia korzystające z komunikacji szeregowej w artykule „Serial Communication App Pitfalls”.

Różnice między OPC UA a OPC Classic

Specyfikacje OPC Classic wymieniają dane między komponentami oprogramowania za pomocą COM/DCOM systemu Windows. OPC DA dla danych procesowych, OPC A&E dla alarmów i OPC HDA dla danych historycznych zdefiniowano jako osobne specyfikacje1.

W 2008 roku OPC Foundation opublikowała OPC UA (Unified Architecture). OPC UA to architektura zorientowana na usługi, niezależna od konkretnej platformy, która łączy funkcje poszczególnych specyfikacji Classic. Jako powody przeprojektowania OPC Foundation podaje między innymi to, że Microsoft zaczął stawiać mechanizmy wieloplatformowe ponad COM/DCOM, że trzeba było działać w systemach spoza Microsoftu i w urządzeniach wbudowanych, oraz że potrzebne było bezpieczne łączenie się przez zapory sieciowe1.

Specyfikacja OPC UA została też znormalizowana przez IEC jako seria IEC 625412. Jeśli chodzi o port komunikacyjny, IANA zarejestrowała 4840/TCP jako opcua-tcp3.

Różnice między OPC Classic a OPC UADiagram pokazujący, że OPC Classic opiera się na COM i DCOM systemu Windows i dzieli się na osobne specyfikacje DA, A&E i HDA, a OPC UA łączy ich funkcje w jedną specyfikację niezależną od platformy.Połączenie funkcjiOPC ClassicDziała na COM/DCOM (Windows)Specyfikacje podzielone na DA, A&E i HDAOPC UA (2008)Niezależne od konkretnej platformyZnormalizowane jako IEC 62541

Rys. 1: OPC Classic to osobne specyfikacje oparte na COM/DCOM, a OPC UA łączy je w jedną.

Jeśli istniejące urządzenia mają tylko serwer OPC DA, po swojej stronie potrzebujesz mechanizmu korzystającego z COM/DCOM. Ten artykuł dotyczy wyłącznie OPC UA.

Na diagramie linia ciągła oznacza relację, która zachodzi zawsze, a linia przerywana relację warunkową (warunki podano przy każdej relacji na stronie szczegółów). Pełną listę relacji (łącznie 13, wraz z uzasadnieniem i pewnością) oraz definicje głównych pojęć zebrano na stronie szczegółów mapy wiedzy (w języku japońskim). Dane: JSON-LD / Turtle

Sprawdzana konfiguracja

Uruchomiono następującą konfigurację.

Rola Użyte elementy
Klient C#, .NET 10 SDK 10.0.114, pakiet NuGet OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1
Serwer Symulowane urządzenie napisane w Pythonie 3.13 z asyncua 2.1.0 (port 4840 na tym samym komputerze)
System operacyjny Linux (uruchomienia w Windows nie sprawdzano)

Biblioteka klienta to implementacja .NET publikowana przez OPC Foundation. Sposób użycia linii 2.0 dopasowano do opisu w dokumencie Getting started z tego samego repozytorium4.

Sprawdzana konfiguracjaDiagram pokazujący klienta w C#, który korzysta z biblioteki .NET od OPC Foundation i łączy się szyfrowanym połączeniem z symulowanym serwerem urządzenia asyncua nasłuchującym na porcie 4840.opc.tcp, SignAndEncryptKlient w C# (LineCollector)Symulowany serwer urządzenia (asyncua)Biblioteka .NET od OPC Foundation 2.0.1Cztery zmienne pod Line1

Rys. 2: Na tym samym komputerze klient w C# połączył się z symulowanym serwerem urządzenia.

Symulowany serwer urządzenia

Zamiast prawdziwego urządzenia przygotowano mały serwer w asyncua5. Pod obiektem o nazwie Line1 znajdują się cztery zmienne: temperatura (Temperature), liczba wyrobów (PartCount), praca (Running) i wartość zadana (Setpoint). Temperatura zmienia się co 0,5 sekundy, a liczba wyrobów co 2 sekundy. Pod względem bezpieczeństwa przygotowano dwie opcje: bez szyfrowania (None) oraz podpis i szyfrowanie (SignAndEncrypt w Basic256Sha256).

# Symulowane urządzenie do sprawdzenia działania. Nie zastępuje rzeczywistych urządzeń ani sterowników PLC.
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())

Ten serwer akceptuje certyfikaty łączących się klientów bez ich sprawdzania. To ustawienie testowe, które różni się od zachowania rzeczywistych urządzeń.

Klient w C#

Do pliku projektu dodano dwa następujące pakiety.

<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>

Klient kolejno łączy się, przegląda (browse), odczytuje, zapisuje i subskrybuje. Znaczenie poszczególnych części wyjaśniają kolejne sekcje.

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

        // Indeks przestrzeni nazw ustala serwer. Odczytaj go z URI i na tej podstawie utwórz NodeId
        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);

        // Przeglądanie: wypisz zmienne znajdujące się pod Line1
        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}");
        }

        // Odczyt: sprawdź nie tylko wartość, ale też stan i dwa znaczniki czasu
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // Odczyt nieistniejącego węzła zwraca stan Bad zamiast wartości
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // Zapis: ustaw Setpoint na 32.5
        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]}");

        // Subskrypcja: poproś serwer o próbkowanie co 500 ms i powiadamianie o zmianach
        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");

Za pierwszym uruchomieniem nie udało się połączyć. Powód wyjaśnia sekcja „Zezwalanie na połączenie za pomocą certyfikatów”. Poniżej wynik po zaufaniu certyfikatowi serwera. Pominięto ostrzeżenia z dziennika biblioteki.

接続: 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]

Czasy podano w UTC.

Dane urządzenia są widoczne jako węzły w przestrzeni adresowej

Serwer OPC UA udostępnia posiadane dane jako zbiór węzłów. Ten zbiór nazywa się przestrzenią adresową (address space). Węzły mają różne rodzaje, na przykład obiekty reprezentujące foldery lub urządzenia oraz zmienne przechowujące wartości, a węzły łączą się ze sobą referencjami. Klient podąża za referencjami za pomocą usługi Browse, aby sprawdzić, jakie węzły istnieją6.

W powyższym wyniku klient podąża za referencjami hierarchicznymi od Line1 i znajduje cztery zmienne.

Przestrzeń adresowa symulowanego urządzeniaDiagram pokazujący obiekt Line1 pod folderem Objects, a pod nim cztery zmienne: Temperature, PartCount, Running i Setpoint.ObjectsLine1 (obiekt)Temperature (zmienna)PartCount (zmienna)Running (zmienna)Setpoint (zmienna, zapisywalna)

Rys. 3: Przestrzeń adresowa symulowanego urządzenia. Klient znajduje węzły, podążając za referencjami za pomocą Browse.

NodeId składa się z przestrzeni nazw i identyfikatora

Węzły identyfikuje się za pomocą NodeId. Zapis ns=2;s=Line1.Temperature w wyniku oznacza, że indeks przestrzeni nazw wynosi 2, a identyfikatorem jest ciąg znaków Line1.Temperature.

W rzeczywistości przestrzeń nazw to URI. Specyfikacja określa, że w usługach URI przestrzeni nazw są reprezentowane liczbami, a liczba odpowiada pozycji w tablicy NamespaceArray serwera. Indeks 0 to przestrzeń nazw OPC UA, http://opcfoundation.org/UA/7.

Dlatego zamiast wpisywać numer indeksu na stałe w kodzie, należy odczytywać indeks na podstawie URI. Powyższy kod za pomocą session.NamespaceUris.GetIndex(LineNamespace) uzyskuje z URI wartość 2 i na jej podstawie tworzy NodeId.

Tworzenie NodeId na podstawie URI przestrzeni nazwDiagram pokazujący, że klient wyszukuje URI przestrzeni nazw w tablicy NamespaceArray serwera, uzyskuje indeks, a następnie łączy ten indeks z identyfikatorem, tworząc NodeId.URI przestrzeni nazwNamespaceArray serweraIndeks (w tym przykładzie 2)NodeId ns=2;s=Line1.TemperatureIdentyfikator Line1.Temperature

Rys. 4: Indeks przestrzeni nazw odczytuje się z URI i łączy z identyfikatorem w NodeId.

Odczyt i zapis zwracają razem z wartością stan i znaczniki czasu

Wartości zmiennych odczytuje się i zapisuje za pomocą usług Read i Write8. Odczyt zwraca nie tylko wartość. Zwraca strukturę o nazwie DataValue, zawierającą wartość, kod stanu, znacznik czasu źródła i znacznik czasu serwera9.

Element Znaczenie
Wartość Wartość zmiennej. Gdy kod stanu wskazuje błąd, wartość się pomija, a serwer ustawia ją na null
Kod stanu (StatusCode) Czy serwer zdołał uzyskać i dostarczyć wartość
SourceTimestamp Czas, który źródło danych nadało wartości
ServerTimestamp Czas, w którym serwer otrzymał wartość lub stwierdził, że jest poprawna

Dwa najstarsze bity kodu stanu oznaczają Good (sukces), Uncertain (częściowy sukces, wartość może się nie nadawać do niektórych zastosowań) lub Bad (niepowodzenie)10. Good [0x00000000] w wyniku to Good.

Przy odczycie nieistniejącego Line1.Pressure zamiast wartości zwrócono BadNodeIdUnknown [0x80340000]. Metoda ReadValueAsync tej biblioteki zgłasza wyjątek, gdy stan to Bad4. Jeśli system zapisujący wartości przechowuje obok nich także kod stanu, można później odróżnić wartości zapisane w czasie, gdy stan był Bad.

Zawartość DataValueDiagram pokazujący, że DataValue zwracana przy odczycie lub w powiadomieniu zawiera cztery elementy: wartość, kod stanu, znacznik czasu źródła i znacznik czasu serwera, a jeśli kod stanu to Bad, wartość się pomija.Jeśli BadDataValueWartośćKod stanuSourceTimestamp (czas w źródle)ServerTimestamp (czas na serwerze)Pomiń wartość

Rys. 5: Wynik odczytu zawiera nie tylko wartość, ale też kod stanu i dwa znaczniki czasu.

Przy zapisie do Setpoint wpisano 32,5, a jako wynikowy kod stanu zwrócono Good. To serwer decyduje, które zmienne można zapisywać. W tym symulowanym serwerze zapisywalny jest tylko Setpoint. Czy pozwolić na zmianę wartości zadanych urządzenia z zewnątrz, to kwestia projektu bezpieczeństwa urządzenia, dlatego ten artykuł jej nie omawia.

Powiadamianie o zmianach: subskrypcje i MonitoredItem

Aby śledzić zmiany wartości, klient może powtarzać Read w stałych odstępach. Niezależnie od tego OPC UA ma mechanizm, w którym monitorowanie wartości przejmuje serwer i powiadamia o zmianach. Tym mechanizmem są subskrypcje i MonitoredItem.

MonitoredItem to pojedynczy monitorowany element, który ma interwał próbkowania (sampling interval). Interwał próbkowania to najszybsze tempo, w jakim serwer próbkuje wartości ze źródła; specyfikacja opisuje go jako cykl „best effort”11. Gdy próbka spełnia warunki filtra, tworzone jest powiadomienie. Jeśli filtr nie jest określony, powiadomienie wysyłane jest przy zmianie wartości lub stanu12.

Powiadomienia najpierw trafiają do kolejki MonitoredItem. Z góry ustala się, czy przy przepełnieniu kolejki odrzucane są najstarsze, czy najnowsze powiadomienia, a jeśli rozmiar kolejki wynosi co najmniej 2, w kodzie stanu ustawiany jest bit Overflow, który sygnalizuje odrzucenie13.

Subskrypcja grupuje wiele MonitoredItem i w każdym interwale publikacji (publishing interval) wysyła klientowi jeszcze niewysłane powiadomienia. Gdy przez kolejne cykle nie ma powiadomień do wysłania, wysyła keep-alive, aby pokazać, że subskrypcja nadal działa14.

Działanie subskrypcji i MonitoredItemDiagram pokazujący, że MonitoredItem próbkuje wartość źródła w interwale próbkowania i umieszcza w kolejce powiadomienia, które przeszły przez filtr, a subskrypcja wysyła je razem do klienta w interwale publikacji, a gdy powiadomień nie ma, wysyła keep-alive.Interwał próbkowaniaInterwał publikacjiGdy brak powiadomieńŹródło (wartość urządzenia)MonitoredItemFiltr (domyślnie zmiana wartości lub stanu)Kolejka (przy przepełnieniu Overflow)SubskrypcjaKlientKeep-alive

Rys. 6: MonitoredItem próbkuje i gromadzi powiadomienia w kolejce, a subskrypcja wysyła je razem do klienta.

Powyższy kod subskrybuje Temperature i PartCount z interwałem próbkowania 500 milisekund i rozmiarem kolejki 515. W wyniku temperatura przychodzi mniej więcej co 0,5 sekundy, a liczba wyrobów mniej więcej co 2 sekundy. Pierwsze dwa powiadomienia to wartości, które każda zmienna miała w chwili rozpoczęcia subskrypcji. 13:40:50.801 przy liczbie wyrobów to czas zmiany wartości sprzed rozpoczęcia subskrypcji. Ponieważ przy powiadomieniach wyświetlany jest znacznik czasu źródła, widać czas zmiany wartości, a nie czas otrzymania powiadomienia.

Kiedy powtarzać odczyt, a kiedy subskrybować

Powtarzanie odczytu pozwala klientowi samodzielnie ustalić odstęp, a sam mechanizm jest prosty. W zamian za każdym razem przesyłane jest żądanie i odpowiedź, nawet gdy wartość się nie zmieniła, a zmiany krótsze niż odstęp zostają pominięte. Przy subskrypcji serwer próbkuje i wysyła tylko zmiany, więc ruch nie rośnie znacząco nawet dla rzadko zmieniających się wartości. Jednak również przy subskrypcji zmiany krótsze niż interwał próbkowania mogą nie zostać zgłoszone. Specyfikacja dopuszcza próbkowanie szybsze niż zadane, ale tego nie wymaga11.

Różnice między powtarzaniem odczytu a subskrypcjąDiagram pokazujący, że przy powtarzaniu odczytu klient ustala odstęp i za każdym razem wysyła żądanie, przy subskrypcji serwer próbkuje w interwale próbkowania i wysyła tylko zmiany, a w obu przypadkach zmiany krótsze niż odstęp mogą zostać pominięte.Śledzenie zmian wartościPowtarzanie odczytuSubskrypcjaKlient ustala odstęp i za każdym razem wysyła żądanieSerwer próbkuje i wysyła tylko zmianyZmiany krótsze niż odstęp mogą zostać pominięte

Rys. 7: W obu metodach zmiany krótsze niż odstęp mogą zostać pominięte.

Zezwalanie na połączenie za pomocą certyfikatów

W połączeniu OPC UA stopień ochrony komunikatów wybiera się za pomocą MessageSecurityMode. Dostępne wartości to None bez ochrony, Sign tylko z podpisem oraz SignAndEncrypt z podpisem i szyfrowaniem16. Zestaw algorytmów używanych do podpisu i szyfrowania określa zasada zabezpieczeń (security policy); w tym artykule użyto Basic256Sha256.

Do podpisu i szyfrowania używa się certyfikatu każdej aplikacji. Klient i serwer sprawdzają nawzajem swoje certyfikaty i odrzucają połączenia ze stronami, którym nie ufają. Serwer zbudowany z biblioteką .NET od OPC Foundation domyślnie nie oferuje nieszyfrowanej zasady None4.

Pierwsze połączenie zatrzymało się na BadCertificateUntrusted

Przy pierwszym uruchomieniu klient nie ufał certyfikatowi serwera, więc połączenie zostało odrzucone. Oto fragment wyniku.

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.'

Ta biblioteka zapisuje odrzucone certyfikaty w magazynie rejected. Gdy administrator przeniesie certyfikat stamtąd do magazynu trusted, biblioteka zaczyna ufać tej stronie17. Tym razem certyfikat serwera przeniesiono z pki/rejected/certs do pki/trusted/certs, uruchomiono program ponownie i połączenie się udało. Przed przeniesieniem potwierdź z administratorem serwera odcisk palca, który jest też częścią nazwy pliku4.

Kroki prowadzące do zaufania certyfikatowiDiagram pokazujący, że przy pierwszym połączeniu klient odrzuca certyfikat serwera i zapisuje go w magazynie rejected, a po potwierdzeniu odcisku palca i przeniesieniu do magazynu trusted następne połączenie ufa mu i się udaje.Pierwsze połączenieBadCertificateUntrustedCertyfikat zapisany w magazynie rejectedPotwierdzenie odcisku palca z administratorem serweraPrzeniesienie do magazynu trustedNastępne połączenie ufa i się udaje

Rys. 8: Po sprawdzeniu odrzuconego certyfikatu i przeniesieniu go do trusted klient ufa tej stronie.

Istnieje też ustawienie AutoAcceptUntrustedCertificates, które automatycznie akceptuje niezaufane certyfikaty. Domyślnie ma wartość false, a dokumentacja biblioteki podaje, że przydaje się w środowisku deweloperskim, ale nie należy go używać w środowisku produkcyjnym17.

NotAfter=2027-11-01 w dzienniku to data wygaśnięcia certyfikatu symulowanego serwera. To certyfikat z podpisem własnym utworzony funkcją asyncua, ważny przez rok od dnia utworzenia. Certyfikaty urządzeń także wygasają, dlatego w procedurach eksploatacji ustal, jak wymieniać je przed wygaśnięciem i sprawić, by druga strona zaufała nowym. Obsługę magazynu certyfikatów Windows opisano w artykule „The Windows Certificate Store in Practice”.

Uwierzytelnianie użytkownika

Certyfikaty potwierdzają tożsamość aplikacji. Niezależnie od tego przy otwieraniu sesji przekazuje się token wskazujący użytkownika. Specyfikacja przewiduje cztery rodzaje: anonimowy, nazwę użytkownika i hasło, certyfikat X.509 oraz token wydany zewnętrznie18. Klient w tym przykładzie połączył się anonimowo. Informacja o tym, jakie tokeny serwer akceptuje, znajduje się w informacjach o punkcie końcowym (endpoint) serwera19.

Gdzie zbieranie danych w zakładzie napotyka problemy

Poniżej wymieniono problemy, które łatwo pojawiają się w systemach zbierających dane z urządzeń, na podstawie tego eksperymentu i specyfikacji.

Wartości z okresu zatrzymania serwera nie docierają

W trakcie subskrypcji serwer zatrzymano i uruchomiono ponownie po około 15 sekundach. ManagedSession tej biblioteki automatycznie łączy się ponownie po zerwaniu połączenia20. Gdy serwer był zatrzymany, klient wielokrotnie próbował się ponownie połączyć.

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.

Gdy serwer wrócił, wróciły też powiadomienia. Oto fragment wyniku subskrypcji.

  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]

Nie dotarła ani jedna wartość z okresu od 13:41:13 do 13:41:34. Liczba wyrobów spadła z 78 do 2. Dzieje się tak, ponieważ po ponownym uruchomieniu symulowany serwer zaczął liczyć od 0.

W tym przykładzie zatrzymany był sam serwer, więc wartości z tego okresu nie zostały nigdzie zachowane. Mechanizm ponownego łączenia przywraca połączenie, ale nie uzupełnia wartości z okresu, gdy serwer nie działał.

Inaczej jest, gdy zerwana zostaje tylko komunikacja, a serwer nadal działa. Specyfikacja przewiduje usługę Republish, która pozwala ponownie otrzymać z kolejki retransmisji serwera komunikaty z powiadomieniami, które nie dotarły21. Powiadomienia gromadzą się też w kolejce MonitoredItem, ale to, co przekracza rozmiar kolejki, zostaje odrzucone13. Tego przypadku nie sprawdzano w tym eksperymencie. W obu przypadkach zapisuj czasy rozłączenia i ponownego połączenia, aby wiedzieć, w jakim okresie wartości nie docierały. Projektowanie ponownego łączenia i dzienników dla urządzeń z komunikacją szeregową omówiono także w artykule „Serial Communication App Pitfalls”.

Zapis po zatrzymaniu serweraDiagram pokazujący, że gdy serwer zatrzyma się w trakcie subskrypcji, klient wielokrotnie próbuje się ponownie połączyć, a gdy serwer wraca, wracają powiadomienia, ale wartości z okresu zatrzymania nie docierają, dlatego należy zapisywać czasy rozłączenia i ponownego połączenia.Trwa subskrypcjaSerwer się zatrzymujeKlient wielokrotnie próbuje się połączyćSerwer wraca, powiadomienia teżWartości z okresu zatrzymania nie docierająZapisz czasy rozłączenia i ponownego połączenia

Rys. 9: Ponowne łączenie przywraca połączenie, ale wartości z okresu zatrzymania serwera nie docierają.

Który znacznik czasu zapisywać

Według specyfikacji SourceTimestamp to czas, który źródło danych nadało wartości, i powinien wskazywać chwilę zmiany wartości. ServerTimestamp to czas, w którym serwer otrzymał wartość lub stwierdził, że jest poprawna9. Symulowany serwer w tym przykładzie sam generuje wartości, więc oba czasy były takie same.

Jeśli chcesz znać chwilę zmiany wartości w urządzeniu, używaj SourceTimestamp. Ten czas nadaje jednak zegar źródła, więc jeśli zegar urządzenia lub serwera się rozjeżdża, zapis też jest przesunięty. Jeśli zebrane dane mają być porównywane z innymi zapisami, przechowuj obok także czas komputera, który je odebrał, aby później móc sprawdzić rozbieżność. Synchronizację czasu po stronie Windows opisano w artykule „Windows Time Synchronization (w32time) and Business Systems”.

Nie traktuj wartości o stanie innym niż Good jak zwykłych liczb

Gdy kod stanu to Bad, wartość należy pominąć9. Gdy to Uncertain, wartość może się nie nadawać do niektórych zastosowań10. Zanim wartości trafią do wykresów lub zestawień, rozdziel je według kodu stanu i ustal, jak traktować wartości inne niż Good. Bit Overflow przy przepełnionej kolejce również jest ustawiany w kodzie stanu13. To, jak pokazywać stan urządzeń na ekranie, omówiono w artykule „Best Practices for Checking and Displaying External Device State”.

Otwórz port w zaporze

Port OPC UA zarejestrowany w IANA to 4840/TCP3. Nie każdy serwer używa jednak tego numeru. Przykłady .NET od OPC Foundation korzystają z portów takich jak 625414. Sprawdź numer portu w adresie URL punktu końcowego urządzenia i zezwól na połączenia z tym portem. Jeśli serwer działa w Windows, zezwól na to w Zaporze systemu Windows na tym komputerze, a jeśli po drodze są urządzenia sieciowe, zezwól na nich. Sposób tworzenia reguł ruchu przychodzącego opisano w artykule „The Windows Firewall and Business Applications”.

Gdzie zbieranie danych napotyka problemyDiagram dzielący miejsca, w których zbieranie danych z urządzeń napotyka problemy, na zawartość zapisów (wartości z okresu zatrzymania serwera nie docierają, który znacznik czasu zapisywać, traktowanie wartości innych niż Good) i przygotowanie połączenia (porty w zaporze, wygaśnięcie certyfikatów).Gdzie zbieranie danych napotyka problemyZawartość zapisówPrzygotowanie połączeniaWartości z przestoju nie docierająKtóry znacznik czasuWartości inne niż GoodPorty w zaporzeWygaśnięcie certyfikatów

Rys. 10: Nawet gdy połączenie już działa, sprawdź luki w zapisach, czasy, stan, porty i certyfikaty.

Wybór między OPC UA, Modbus/TCP a protokołami producentów

Do komunikacji ze sterownikami PLC i urządzeniami oprócz OPC UA można też użyć Modbus/TCP lub protokołów udostępnianych przez producentów sterowników PLC.

Specyfikacja Modbus przechowuje dane w czterech tabelach: Discrete Input (1 bit, tylko do odczytu), Coil (1 bit, odczyt i zapis), Input Register (16 bitów, tylko do odczytu) i Holding Register (16 bitów, odczyt i zapis)22. IANA rejestruje port 502/TCP dla protokołu aplikacyjnego Modbus (mbap)3.

W tych tabelach znajdują się bity i 16-bitowe słowa, więc to, jaka wartość w jakim formacie leży pod danym adresem, trzeba sprawdzić w dokumentacji urządzenia i zapisać po stronie klienta. W OPC UA, jak pokazano wyżej, serwer udostępnia nazwane zmienne w przestrzeni adresowej, a klient może je odkryć za pomocą Browse. Wartości mają kod stanu i znaczniki czasu, a na połączenie zezwala się za pomocą certyfikatów.

Wybór często zależy od tego, co udostępnia urządzenie. Jeśli urządzenie ma serwer OPC UA, użyj go; jeśli ma tylko Modbus/TCP, przygotuj mapę adresów i czytaj według niej. Jeśli dostępny jest tylko protokół lub SDK producenta, postępuj zgodnie z jego dokumentacją. Wybór sposobu komunikacji z urządzeniami USB opisano w artykule „How to Work with USB Devices from a Windows App”.

Wybór sposobu komunikacji z urządzeniemDiagram pokazujący, że jeśli urządzenie ma serwer OPC UA, przegląda się i zbiera zmienne przez OPC UA, jeśli ma tylko Modbus/TCP, sprawdza się adresy i formaty w dokumentacji i odczytuje rejestry, a jeśli ma tylko protokół producenta, postępuje się zgodnie z jego dokumentacją.Serwer OPC UATylko Modbus/TCPTylko protokół producentaKomunikacja udostępniana przez urządzenieCo jest dostępne?Przeglądaj i zbieraj zmienneSprawdź adresy i formaty w dokumentacjiPostępuj zgodnie z dokumentacją i SDK producentaOdczytaj rejestry i przelicz je na wartości

Rys. 11: Sposób zbierania danych często zależy od tego, co udostępnia urządzenie.

Powiązane artykuły

Źródła

  1. OPC Foundation, Classic. To, że OPC Classic opiera się na COM/DCOM, specyfikacje DA, A&E i HDA, opublikowanie OPC UA w 2008 roku i powody przeprojektowania. ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. Część 1 serii IEC 62541, obejmująca koncepcje i przegląd OPC UA (zastąpiona późniejszym wydaniem). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. Rejestracje opcua-tcp na 4840/TCP i mbap na 502/TCP. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. Sposób pisania klienta linii 2.0, to, że ReadValueAsync zgłasza wyjątek przy Bad, magazyny odrzuconych i zaufanych certyfikatów, to, że serwery domyślnie nie oferują None, oraz numery portów w przykładach. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). Biblioteka OPC UA dla Pythona użyta do symulowanego serwera urządzenia. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Usługa podążająca za referencjami węzła. ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. To, że w usługach URI przestrzeni nazw są reprezentowane liczbami, a liczba odpowiada pozycji w NamespaceArray. ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Usługi Read i Write. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Definicje wartości, kodu stanu, SourceTimestamp i ServerTimestamp. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain i Bad wskazywane przez dwa najstarsze bity. ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. To, że interwał próbkowania jest cyklem typu best effort. ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. To, że bez filtra powiadomienie wysyłane jest przy zmianie wartości lub stanu. ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. Rozmiar kolejki, sposób odrzucania i bit Overflow. ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. Interwał publikacji i keep-alive. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync oraz interwał próbkowania i rozmiar kolejki w MonitoredItemOptions. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. Znaczenie None, Sign i SignAndEncrypt. ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. Praktyka przenoszenia certyfikatów z magazynu odrzuconych do magazynu zaufanych oraz nieużywanie AutoAcceptUntrustedCertificates w środowisku produkcyjnym. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. Cztery rodzaje: anonimowy, nazwa użytkownika, X.509 i token wydany. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. Lista tokenów użytkownika akceptowanych przez punkt końcowy (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. To, że ManagedSession ma wbudowany mechanizm ponownego łączenia. ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. Usługa ponownego wysłania komunikatów z powiadomieniami z kolejki retransmisji subskrypcji. ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b (kopia na serwerze Uniwersytetu Chicagowskiego). Model danych z sekcji 4.3 (Discrete Input, Coil, Input Register, Holding Register). ↩

Najnowsze artykuły oznaczone tymi samymi tagami. Pozwalają pogłębić wiedzę na pokrewne tematy.

Strony tematyczne zbliżone do tego zagadnienia. Od artykułu mogą Państwo przejść do powiązanych usług i innych artykułów.

Ten artykuł prowadzi do poniższych stron usług. Prosimy wybrać najbliższy punkt wyjścia.

Najczęściej zadawane pytania

Pytania, które najczęściej padają podczas konsultacji na temat tego artykułu.

Czym OPC UA różni się od OPC Classic (OPC DA)?
OPC Classic to specyfikacje wymieniające dane przez COM/DCOM systemu Windows. OPC UA, opublikowane w 2008 roku, nie zależy od konkretnego systemu operacyjnego i łączy w sobie funkcje Classic. Klienty OPC UA działają także poza Windows i nie wymagają konfiguracji DCOM.
Czy przy zbieraniu wartości przez OPC UA wystarczy odczytywać je w stałych odstępach?
Jeśli wartości jest niewiele, a odstęp jest długi, odczyt wystarczy. Aby śledzić zmiany wartości, można zamiast tego użyć subskrypcji: próbkowanie przejmuje serwer, który powiadamia o zmianach. W obu przypadkach zapisuj kod stanu i znaczniki czasu zwracane razem z wartością.
Dlaczego przy połączeniu pojawia się BadCertificateUntrusted?
Ponieważ klient jeszcze nie ufa certyfikatowi serwera. Biblioteka .NET od OPC Foundation zapisuje odrzucone certyfikaty w magazynie rejected. Potwierdź odcisk palca (thumbprint) certyfikatu z administratorem serwera, a potem przenieś go do magazynu trusted. Ustawienia, które automatycznie akceptują certyfikaty, stosuj tylko w środowisku deweloperskim.
Jakiego numeru portu używa OPC UA?
IANA rejestruje port 4840/TCP jako opcua-tcp. Rzeczywiste serwery mogą jednak używać innego portu, więc sprawdź to w ustawieniach urządzenia. Jeśli serwer działa w Windows, trzeba zezwolić na ruch do tego portu w Zaporze systemu Windows, a jeśli po drodze są urządzenia sieciowe, także na nich.

Profil autora

Strona z profilem autora artykułu.

Go Komura

Przedstawiciel KomuraSoft LLC

Zajmuje się głównie tworzeniem oprogramowania dla systemu Windows, konsultacjami technicznymi i diagnostyką błędów. Szczególnie dobrze sprawdza się w projektach z istniejącymi zasobami oraz w analizie awarii o trudnych do wskazania przyczynach.

Publiczne odnośniki

Powrót do listy artykułów