Introducere în OPC UA ── Colectarea datelor de la PLC-uri și echipamente într-o aplicație Windows

· · Windows, OPC UA, Producție, PLC, C#, .NET, Comunicații, Certificate

Postați pe X
Istoricul modificărilor (doar versiunea inițială, publicată la 01.11.2026)
Prima publicare
Citați acest articol(DOI: 10.5281/zenodo.23968442)

Acest articol este arhivat pe Zenodo. Mai jos sunt indicate atât DOI-ul care trimite întotdeauna la cea mai recentă versiune, cât și DOI-ul fixat pe versiunea afișată acum.

Go Komura (2026). „Introducere în OPC UA ── Colectarea datelor de la PLC-uri și echipamente într-o aplicație Windows”. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968442 https://comcomponent.com/ro/blog/opc-ua-windows-data-collection/

DOI (cea mai recentă versiune)
10.5281/zenodo.23968442
DOI (această versiune)
10.5281/zenodo.23968443

Când colectați date de la PLC-uri sau echipamente într-o aplicație Windows și echipamentul are un server OPC UA, uneori puteți evita să scrieți comunicația separat pentru fiecare producător. Totuși, nu este suficient să vă conectați și să citiți valorile. Dacă nu cunoașteți starea și marcajele de timp care vin odată cu valoarea, abonamentele care vă notifică schimbările, permiterea conexiunii prin certificate și modul de revenire după deconectare, nu veți putea avea încredere ulterior în datele colectate.

Acest articol explică partea din OPC UA necesară pentru colectarea datelor, împreună cu rezultatele rulării efective a unui client C#. Pentru client am folosit biblioteca .NET a OPC Foundation, iar la celălalt capăt un server care simulează un echipament. Nu am încercat cu un anumit PLC sau echipament. Verificați setările fiecărui echipament în documentația acestuia.

Colectarea imaginilor de la camerele GigE Vision și USB3 Vision este descrisă în „Dropped Frames with GigE Vision and USB3 Vision Cameras”, iar echipamentele cu comunicație serială în „Serial Communication App Pitfalls”.

Diferența dintre OPC UA și OPC Classic

Specificațiile OPC Classic schimbă date între componente software folosind COM/DCOM din Windows. OPC DA pentru datele de proces, OPC A&E pentru alarme și OPC HDA pentru istoric sunt definite ca specificații separate1.

OPC Foundation a publicat OPC UA (Unified Architecture) în 2008. OPC UA este o arhitectură orientată pe servicii care nu depinde de o anumită platformă și reunește funcțiile fiecărei specificații Classic. Printre motivele reproiectării, OPC Foundation menționează că Microsoft a început să pună accent pe mecanisme multiplatformă mai mult decât pe COM/DCOM, că era nevoie să ruleze și pe sisteme din afara Microsoft și pe dispozitive încorporate și că era nevoie de conectare sigură prin paravane de protecție1.

Specificația OPC UA este standardizată și de IEC, ca seria IEC 625412. Pentru portul de comunicație, IANA a înregistrat 4840/TCP ca opcua-tcp3.

Diferența dintre OPC Classic și OPC UADiagramă care arată că OPC Classic se bazează pe COM și DCOM din Windows și este împărțit în specificațiile separate DA, A&E și HDA, în timp ce OPC UA reunește funcțiile lor într-o singură specificație independentă de platformă.Reunirea funcțiilorOPC ClassicRulează pe COM/DCOM (Windows)Specificații împărțite în DA, A&E și HDAOPC UA (2008)Nu depinde de o anumită platformăStandardizat ca IEC 62541

Figura 1: OPC Classic constă din specificații separate peste COM/DCOM, iar OPC UA le reunește într-una singură.

Dacă un echipament existent are doar un server OPC DA, aveți nevoie pe partea clientului de un mecanism care folosește COM/DCOM. Acest articol tratează doar OPC UA.

În diagramă, linia continuă marchează o relație care este întotdeauna valabilă, iar linia punctată una condiționată (condițiile sunt indicate pentru fiecare relație pe pagina de detalii). Lista completă a relațiilor (13 în total, cu dovezi și grad de certitudine) și definițiile conceptelor principale sunt reunite în pagina de detalii a hărții cunoștințelor (în japoneză). Date: JSON-LD / Turtle

Configurația încercată

Am rulat următoarea configurație.

Rol Ce am folosit
Client C#, .NET 10 SDK 10.0.114, pachetul NuGet OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1
Server Un echipament simulat realizat cu Python 3.13 și asyncua 2.1.0 (portul 4840 pe același PC)
Sistem de operare Linux (nu am încercat rularea pe Windows)

Biblioteca clientului este implementarea .NET publicată de OPC Foundation. Pentru folosirea versiunii 2.0 am urmat modul de scriere din Getting started, din același depozit4.

Configurația încercatăDiagramă a configurației în care un client C# folosește biblioteca .NET a OPC Foundation și se conectează printr-o conexiune criptată la serverul asyncua care simulează un echipament și ascultă pe portul 4840.opc.tcp, SignAndEncryptClient C# (LineCollector)Server simulat al echipamentului (asyncua)Biblioteca .NET a OPC Foundation 2.0.1Cele patru variabile ale Line1

Figura 2: Pe același PC, m-am conectat din clientul C# la serverul care simulează echipamentul.

Serverul care simulează echipamentul

În locul unui echipament, am creat un server mic cu asyncua5. Sub un obiect numit Line1 se află patru variabile: temperatura (Temperature), numărul de piese produse (PartCount), funcționarea (Running) și valoarea de referință (Setpoint). Temperatura se schimbă la fiecare 0,5 secunde, iar numărul de piese la fiecare 2 secunde. Am pregătit două niveluri de securitate: fără criptare (None) și cu semnare și criptare (SignAndEncrypt cu politica Basic256Sha256).

# Echipament simulat pentru verificarea funcționării. Nu înlocuiește echipamentele sau PLC-urile reale.
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())

Acest server acceptă certificatele clienților care se conectează fără să le verifice. Este o setare pentru testare și diferă de comportamentul unui echipament real.

Clientul C#

În fișierul de proiect am adăugat următoarele două pachete.

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

Clientul efectuează pe rând conectarea, răsfoirea (Browse), citirea, scrierea și abonarea. Sensul fiecărei părți îl explic pe rând în secțiunile următoare.

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

        // Indexul spațiului de nume îl stabilește serverul. Îl aflăm din URI și construim din el NodeId-urile
        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);

        // Răsfoire: listează variabilele de sub 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}");
        }

        // Citire: pe lângă valoare, ne uităm la stare și la cele două marcaje de timp
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // La citirea unui nod care nu există se întoarce o stare Bad în loc de valoare
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // Scriere: setează Setpoint la 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]}");

        // Abonare: cerem serverului eșantionare la fiecare 500 ms și notificarea schimbărilor
        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");

La prima rulare nu m-am putut conecta. Motivul îl explic în secțiunea „Permiterea conexiunii prin certificate”. După ce am acordat încredere certificatului serverului, rezultatul a fost cel de mai jos. Am omis jurnalele de avertizare ale bibliotecii.

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

Orele sunt în UTC.

Datele echipamentului apar ca noduri ale spațiului de adrese

Un server OPC UA publică datele pe care le deține ca o colecție de noduri. Această colecție se numește spațiul de adrese. Nodurile sunt de mai multe tipuri, de exemplu obiecte care reprezintă dosare sau echipamente și variabile care au valori, iar nodurile sunt legate între ele prin referințe. Clientul parcurge referințele cu serviciul Browse și află ce noduri există6.

În rezultatul de mai sus, clientul a parcurs referințele ierarhice pornind de la Line1 și a găsit patru variabile.

Spațiul de adrese al echipamentului simulatDiagramă care arată că sub dosarul Objects se află obiectul Line1, iar sub acesta cele patru variabile Temperature, PartCount, Running și Setpoint.ObjectsLine1 (obiect)Temperature (variabilă)PartCount (variabilă)Running (variabilă)Setpoint (variabilă, care poate fi scrisă)

Figura 3: Spațiul de adrese al echipamentului simulat. Clientul găsește nodurile parcurgând referințele cu Browse.

NodeId este alcătuit dintr-un spațiu de nume și un identificator

Nodurile se identifică prin NodeId. ns=2;s=Line1.Temperature din rezultat înseamnă că indexul spațiului de nume este 2, iar identificatorul este șirul Line1.Temperature.

Spațiul de nume este de fapt un URI. Specificația stabilește că URI-ul spațiului de nume se exprimă în cadrul serviciilor printr-un număr, iar acest număr corespunde poziției din NamespaceArray al serverului. Indexul 0 este spațiul de nume OPC UA http://opcfoundation.org/UA/7.

De aceea, nu scrieți numărul indexului direct în cod, ci aflați indexul din URI. Codul de mai sus obține 2 din URI cu session.NamespaceUris.GetIndex(LineNamespace) și construiește din el NodeId-urile.

Construirea unui NodeId din URI-ul spațiului de numeDiagramă care arată că clientul caută URI-ul spațiului de nume în NamespaceArray al serverului, obține indexul și combină acest index cu identificatorul pentru a construi NodeId-ul.URI-ul spațiului de numeNamespaceArray al serveruluiIndex (în acest exemplu 2)NodeId ns=2;s=Line1.TemperatureIdentificator Line1.Temperature

Figura 4: Indexul spațiului de nume se află din URI și se combină cu identificatorul pentru a forma NodeId-ul.

Citirea și scrierea întorc, odată cu valoarea, starea și marcajele de timp

Valorile variabilelor se citesc și se scriu cu serviciile Read și Write8. O citire nu întoarce doar valoarea. Întoarce o structură numită DataValue, care conține valoarea, codul de stare, marcajul de timp al sursei și marcajul de timp al serverului9.

Element Semnificație
Valoare Valoarea variabilei. Când codul de stare indică o eroare, valoarea se ignoră, iar serverul o setează la null
Cod de stare (StatusCode) Dacă serverul a putut obține și furniza valoarea
SourceTimestamp Ora pe care sursa datelor a atașat-o valorii
ServerTimestamp Ora la care serverul a primit valoarea sau a aflat că este corectă

Primii doi biți ai codului de stare indică Good (succes), Uncertain (succes parțial, pentru unele scopuri inutilizabil) sau Bad (eșec)10. Good [0x00000000] din rezultat este Good.

La citirea lui Line1.Pressure, care nu există, în loc de valoare s-a întors BadNodeIdUnknown [0x80340000]. Metoda ReadValueAsync a acestei biblioteci aruncă o excepție când starea este Bad4. Dacă un sistem care înregistrează valori păstrează alături de fiecare valoare și codul de stare, puteți deosebi ulterior valorile înregistrate în timp ce starea era Bad.

Conținutul unui DataValueDiagramă care arată că DataValue întors de o citire sau o notificare conține patru elemente, valoarea, codul de stare, marcajul de timp al sursei și marcajul de timp al serverului, și că valoarea se ignoră când codul de stare este Bad.Dacă e BadDataValueValoareCod de stareSourceTimestamp (ora sursei)ServerTimestamp (ora serverului)Valoarea se ignoră

Figura 5: Rezultatul unei citiri conține pe lângă valoare și codul de stare și două marcaje de timp.

La scriere, am scris 32,5 în Setpoint, iar ca cod de stare rezultat s-a întors Good. Ce variabile pot fi scrise decide serverul. În acest server simulat, doar Setpoint poate fi scrisă. Dacă să permiteți modificarea din exterior a valorilor de referință ale echipamentului ține de proiectarea siguranței echipamentului, așa că acest articol nu tratează subiectul.

Notificarea schimbărilor: abonamente și MonitoredItem

Pentru a urmări schimbările valorilor, clientul poate repeta Read la un interval fix. Separat de aceasta, OPC UA are un mecanism prin care lăsați monitorizarea valorilor în seama serverului, iar acesta vă notifică schimbările. Acest mecanism îl formează abonamentele (subscription) și MonitoredItem.

Un MonitoredItem este un singur element monitorizat și are un interval de eșantionare. Intervalul de eșantionare este cel mai rapid interval la care serverul eșantionează valorile de la sursă, iar specificația îl descrie ca un ciclu de tip „best effort”11. Când un eșantion îndeplinește condițiile filtrului, se creează o notificare. Dacă nu se specifică un filtru, notificarea se trimite când se schimbă valoarea sau starea12.

Notificările intră mai întâi în coada MonitoredItem. Stabiliți dinainte dacă la depășirea cozii se aruncă cea mai veche sau cea mai nouă notificare, iar dacă dimensiunea cozii este 2 sau mai mare, în codul de stare se setează bitul Overflow, care indică aruncarea13.

Un abonament grupează mai multe MonitoredItem și, la fiecare interval de publicare, trimite clientului notificările care nu au fost încă trimise. Dacă trec cicluri în care nu are ce trimite, trimite un keep-alive care arată că abonamentul este încă activ14.

Cum funcționează abonamentele și MonitoredItemDiagramă care arată că MonitoredItem eșantionează valoarea sursei la intervalul de eșantionare și pune în coadă notificările care trec de filtru, iar abonamentul le trimite împreună clientului la intervalul de publicare și trimite un keep-alive când nu există notificări.Interval de eșantionareInterval de publicareDacă nu există notificăriSursa (valoarea echipamentului)MonitoredItemFiltru (implicit schimbarea valorii sau a stării)Coadă (la depășire, Overflow)AbonamentClientKeep-alive

Figura 6: MonitoredItem eșantionează și pune în coadă, iar abonamentul trimite notificările împreună clientului.

Codul de mai sus se abonează la Temperature și PartCount cu un interval de eșantionare de 500 de milisecunde și o dimensiune a cozii de 515. În rezultat, temperatura sosește aproximativ la fiecare 0,5 secunde, iar numărul de piese aproximativ la fiecare 2 secunde. Primele două notificări au fost valorile pe care le aveau variabilele în momentul începerii abonamentului. Ora 13:40:50.801 a numărului de piese este momentul în care valoarea s-a schimbat, înainte de începerea abonamentului. Deoarece la notificări se afișează marcajul de timp al sursei, apare ora la care s-a schimbat valoarea, nu ora la care a fost primită notificarea.

Când citirea repetată și când abonamentul

La citirea repetată, intervalul îl stabilește clientul, iar mecanismul este simplu. În schimb, cererea și răspunsul circulă de fiecare dată, chiar dacă valoarea nu s-a schimbat, iar schimbările mai scurte decât intervalul sunt ratate. La abonament, serverul eșantionează și trimite doar schimbările, așa că traficul nu crește prea mult nici pentru valori care se schimbă rar. Totuși, și la abonament, schimbările mai scurte decât intervalul de eșantionare pot să nu fie notificate. Specificația permite eșantionarea mai rapidă decât cea cerută, dar nu o impune11.

Diferența dintre citirea repetată și abonamentDiagramă care arată că la citirea repetată clientul stabilește intervalul și trimite de fiecare dată o cerere, la abonament serverul eșantionează la intervalul de eșantionare și trimite doar schimbările și că în ambele cazuri schimbările mai scurte decât intervalul pot fi ratate.Urmărirea schimbărilor valorilorCitire repetatăAbonamentClientul stabilește intervalul și cere de fiecare datăServerul eșantionează și trimite doar schimbărileSchimbările mai scurte decât intervalul pot fi ratate

Figura 7: Cu oricare dintre metode, schimbările mai scurte decât intervalul pot fi ratate.

Permiterea conexiunii prin certificate

La o conexiune OPC UA, nivelul de protecție al mesajelor se alege cu MessageSecurityMode. Valorile sunt None, fără protecție, Sign, care doar semnează, și SignAndEncrypt, care semnează și criptează16. Combinația de algoritmi pentru semnare și criptare o stabilește politica de securitate; în acest articol am folosit Basic256Sha256.

Pentru semnare și criptare se folosește un certificat al fiecărei aplicații. Clientul și serverul își verifică reciproc certificatele și refuză conexiunea cu o parte în care nu au încredere. Un server construit cu biblioteca .NET a OPC Foundation nu oferă implicit politica necriptată None4.

Prima conexiune s-a oprit la BadCertificateUntrusted

La prima rulare, clientul nu avea încredere în certificatul serverului, așa că conexiunea a fost refuzată. Iată o parte din rezultat.

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

Această bibliotecă salvează certificatele respinse în depozitul rejected. Când un administrator le mută de acolo în depozitul trusted, biblioteca începe să aibă încredere în acea parte17. De data aceasta am mutat certificatul serverului din pki/rejected/certs în pki/trusted/certs, am rulat din nou și m-am putut conecta. Înainte de mutare, verificați cu administratorul serverului amprenta, care apare și în numele fișierului4.

Pașii până la încrederea în certificatDiagramă care arată că la prima conexiune clientul respinge certificatul serverului și îl salvează în depozitul rejected, iar după verificarea amprentei și mutarea în depozitul trusted, la următoarea conexiune are încredere în el și se conectează.Prima conexiuneBadCertificateUntrustedCertificatul se salvează în depozitul rejectedSe verifică amprenta cu administratorul serveruluiSe mută în depozitul trustedLa următoarea conexiune are încredere și se conectează

Figura 8: După ce verificați un certificat respins și îl mutați în trusted, clientul are încredere în acea parte.

Există și setarea AutoAcceptUntrustedCertificates, care acceptă automat certificatele în care nu există încredere. Valoarea implicită este false, iar documentația bibliotecii spune că este utilă în mediul de dezvoltare, dar nu trebuie folosită în producție17.

NotAfter=2027-11-01 din jurnal este data de expirare a certificatului serverului simulat. Este un certificat autosemnat creat cu o funcție asyncua, iar valabilitatea lui se încheia la un an după ziua creării. Și certificatele echipamentelor expiră, așa că stabiliți în cadrul operării procedura de înlocuire înainte de expirare și de acordare a încrederii de către cealaltă parte. Lucrul cu depozitul de certificate Windows este descris în „The Windows Certificate Store in Practice”.

Autentificarea utilizatorului

Prin certificate se verifică aplicațiile între ele. Separat de aceasta, la deschiderea unei sesiuni se transmite un token care identifică utilizatorul. Specificația definește patru tipuri: anonim, nume de utilizator și parolă, certificat X.509 și token emis extern18. Clientul din acest articol s-a conectat anonim. Ce tokenuri acceptă serverul este inclus în informațiile despre punctul final (endpoint) al serverului19.

Unde se împotmolește colectarea datelor în practică

În continuare enumăr probleme care apar frecvent în sistemele care colectează date de la echipamente, pe baza acestui experiment și a specificației.

Valorile din perioada în care serverul era oprit nu sosesc

În timpul abonamentului am oprit serverul și l-am repornit după aproximativ 15 secunde. ManagedSession din această bibliotecă se reconectează automat când conexiunea se întrerupe20. Cât timp serverul a fost oprit, clientul a încercat în mod repetat să se reconecteze.

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.

Când serverul a revenit, au revenit și notificările. Iată o parte din rezultatul abonamentului.

  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]

Din intervalul 13:41:13–13:41:34 nu a sosit nicio valoare. Numărul de piese a revenit de la 78 la 2. Aceasta deoarece, după repornire, serverul simulat a început să numere din nou de la 0.

În acest exemplu serverul însuși era oprit, așa că valorile din acea perioadă nu sunt păstrate nicăieri. Mecanismul de reconectare restabilește conexiunea, dar nu completează ulterior valorile din perioada opririi.

Situația este alta când se întrerupe doar comunicația, iar serverul continuă să funcționeze. Specificația are serviciul Republish, prin care serverul retrimite, din coada sa de retransmitere, mesajele cu notificări care nu au sosit21. Notificările se acumulează și în coada MonitoredItem, dar ce depășește dimensiunea cozii se aruncă13. În acest experiment nu am încercat acest caz. În ambele cazuri, înregistrați orele deconectării și reconectării, ca să se vadă în ce perioadă nu au sosit valori. Reconectarea și proiectarea jurnalelor pentru echipamentele cu comunicație serială sunt tratate și în „Serial Communication App Pitfalls”.

Înregistrarea la oprirea serveruluiDiagramă care arată că, dacă serverul se oprește în timpul abonamentului, clientul încearcă în mod repetat să se reconecteze, iar la revenirea serverului revin și notificările, dar valorile din perioada opririi nu sosesc, așa că trebuie înregistrate orele deconectării și reconectării.În timpul abonamentuluiServerul se opreșteClientul încearcă repetat să se reconectezeServerul revine și revin și notificărileValorile din perioada opririi nu sosescSe înregistrează orele deconectării și reconectării

Figura 9: Reconectarea restabilește conexiunea, dar valorile din perioada în care serverul era oprit nu sosesc.

Ce marcaj de timp să înregistrați

Conform specificației, SourceTimestamp este ora pe care sursa datelor a atașat-o valorii și ar trebui să indice momentul schimbării valorii. ServerTimestamp este ora la care serverul a primit valoarea sau a aflat că este corectă9. Serverul simulat din acest articol generează singur valorile, așa că cele două ore au fost aceleași.

Dacă vreți să știți când s-a schimbat o valoare pe echipament, folosiți SourceTimestamp. Această oră este însă atașată de ceasul sursei, așa că, dacă ceasul echipamentului sau al serverului este decalat, și înregistrarea va fi decalată. Dacă veți compara datele colectate cu alte înregistrări, păstrați alături și ora PC-ului care le-a primit, ca să puteți verifica ulterior decalajul. Sincronizarea orei pe partea Windows este descrisă în „Windows Time Synchronization (w32time) and Business Systems”.

Nu tratați ca simple numere valorile cu altă stare decât Good

Când codul de stare este Bad, valoarea trebuie ignorată9. Când este Uncertain, valoarea poate fi inutilizabilă pentru anumite scopuri10. Înainte de a pune valorile în grafice sau totalizări, sortați-le după codul de stare și stabiliți dinainte cum tratați valorile care nu sunt Good. Și bitul Overflow, la depășirea cozii, se setează în codul de stare13. Modul de afișare pe ecran a stării echipamentelor este tratat în „Best Practices for Checking and Displaying External Device State”.

Deschiderea portului în paravanul de protecție

Portul OPC UA înregistrat la IANA este 4840/TCP3. Totuși, nu toate serverele folosesc acest număr. Exemplele .NET ale OPC Foundation folosesc porturi precum 625414. Verificați numărul portului în URL-ul punctului final al echipamentului și permiteți conexiunea spre acel port. Dacă serverul rulează pe Windows, permiteți-o în Paravanul de protecție Windows al acelui computer, iar dacă între ele se află un echipament de rețea, și pe acesta. Crearea regulilor de intrare este descrisă în „The Windows Firewall and Business Applications”.

Unde se împotmolește colectarea datelorDiagramă care împarte locurile în care se împotmolește colectarea datelor de la echipamente în conținutul înregistrărilor (valorile din perioada opririi serverului nu sosesc, ce marcaj de timp se înregistrează, tratarea valorilor care nu sunt Good) și pregătirea conexiunii (porturile din paravanul de protecție, expirarea certificatelor).Unde se împotmolește colectarea datelorConținutul înregistrărilorPregătirea conexiuniiValorile din timpul opririi nu sosescCe marcaj de timpValorile care nu sunt GoodPorturile din paravanul de protecțieExpirarea certificatelor

Figura 10: Chiar și după ce v-ați putut conecta, verificați lipsurile din înregistrări, orele, starea, porturile și certificatele.

Când OPC UA și când Modbus/TCP sau comunicația producătorului

Pentru comunicarea cu PLC-uri și echipamente, pe lângă OPC UA există și Modbus/TCP, precum și comunicația oferită de producătorul PLC-ului.

Specificația Modbus tratează datele în patru tabele: Discrete Input, un bit doar pentru citire; Coil, un bit pentru citire și scriere; Input Register, 16 biți doar pentru citire; și Holding Register, 16 biți pentru citire și scriere22. La IANA, portul 502/TCP este înregistrat ca protocolul de aplicație Modbus (mbap)3.

În aceste tabele intră biți și cuvinte de 16 biți, așa că la ce adresă se află ce valoare și în ce format trebuie verificat în documentația echipamentului și scris pe partea clientului. În OPC UA, așa cum am văzut, serverul publică variabile cu nume în spațiul de adrese, iar clientul le poate afla cu Browse. Valorile au cod de stare și marcaje de timp, iar conexiunea se permite prin certificate.

Ce folosiți este adesea decis de ce oferă echipamentul. Dacă echipamentul are un server OPC UA, folosiți-l; dacă are doar Modbus/TCP, creați un tabel de corespondență a adreselor și citiți după el. Dacă există doar comunicația sau SDK-ul producătorului, urmați documentația acestuia. Alegerea metodei de comunicare cu dispozitivele USB este descrisă în „How to Work with USB Devices from a Windows App”.

Alegerea modului de comunicare cu echipamentulDiagramă care arată că, dacă echipamentul are un server OPC UA, variabilele se colectează prin răsfoire cu OPC UA, dacă are doar Modbus/TCP se verifică în documentație adresele și formatele și se citesc registrele, iar dacă are doar comunicația producătorului se urmează documentația acestuia.Server OPC UADoar Modbus/TCPDoar comunicația producătoruluiComunicația oferită de echipamentCe este disponibil?Se colectează variabilele prin răsfoireSe verifică adresele și formatele în documentațieSe urmează documentația și SDK-ul producătoruluiSe citesc registrele și se convertesc în valori

Figura 11: Cu ce colectați este adesea decis de ce oferă echipamentul.

Articole conexe

Linkuri de referință

  1. OPC Foundation, Classic. Faptul că OPC Classic se bazează pe COM/DCOM, specificațiile DA, A&E și HDA, publicarea OPC UA în 2008 și motivele reproiectării. ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. Partea 1 a seriei IEC 62541, despre conceptele și prezentarea generală a OPC UA (înlocuită între timp de o ediție ulterioară). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. Înregistrarea opcua-tcp pe 4840/TCP și a mbap pe 502/TCP. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. Cum se scrie un client din seria 2.0, faptul că ReadValueAsync aruncă o excepție la Bad, depozitele pentru certificate respinse și de încredere, faptul că serverul nu oferă implicit None și numerele de port din exemple. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). Biblioteca OPC UA pentru Python folosită pentru serverul care simulează echipamentul. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Serviciul care parcurge referințele nodurilor. ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. Faptul că URI-ul spațiului de nume se exprimă în cadrul serviciilor printr-un număr, care corespunde poziției din NamespaceArray. ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Serviciile Read și Write. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Definițiile valorii, codului de stare, SourceTimestamp și ServerTimestamp. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain și Bad, indicate de primii doi biți. ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. Faptul că intervalul de eșantionare este un ciclu de tip best effort. ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. Faptul că, fără filtru, notificarea se trimite la schimbarea valorii sau a stării. ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. Dimensiunea cozii, modul de aruncare și bitul Overflow. ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. Intervalul de publicare și keep-alive. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync și intervalul de eșantionare și dimensiunea cozii din MonitoredItemOptions. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. Semnificația None, Sign și SignAndEncrypt. ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. Practica mutării certificatelor din depozitul rejected în depozitul de încredere și faptul că AutoAcceptUntrustedCertificates nu trebuie folosită în producție. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. Cele patru tipuri: anonim, nume de utilizator, X.509 și token emis. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. Lista tokenurilor de utilizator acceptate de punctul final (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. Faptul că ManagedSession are încorporat mecanismul de reconectare. ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. Serviciul prin care coada de retransmitere a abonamentului trimite din nou mesajele cu notificări. ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b (o copie găzduită pe un server al Universității din Chicago). Modelul de date din secțiunea 4.3 (Discrete Input, Coil, Input Register, Holding Register). ↩

Cele mai recente articole care au aceleași etichete. Aprofundați cunoștințele prin subiecte apropiate.

Pagini de subiect apropiate de această temă. Pornind de la articol, puteți ajunge la servicii conexe și la alte articole.

Acest articol se leagă de paginile de servicii de mai jos. Alegeți punctul de intrare cel mai apropiat de nevoia dumneavoastră.

Dezvoltare de aplicații Windows

Realizarea unei aplicații Windows care colectează date de la echipamente, inclusiv setarea abonamentelor, reconectarea și înregistrarea marcajelor de timp și a stării, poate fi tratată ca dezvoltare de aplicații Windows.

Întrebări frecvente

Întrebările pe care le primim cel mai des în consultanță pe tema acestui articol.

Care este diferența dintre OPC UA și OPC Classic (OPC DA)?
OPC Classic este un set de specificații care schimbă date prin COM/DCOM din Windows. OPC UA, publicat în 2008, este o specificație care nu depinde de un anumit sistem de operare și reunește funcțiile din Classic. Clienții OPC UA rulează și în afara Windows și nu au nevoie de configurarea DCOM.
Când colectez valori prin OPC UA, este suficient să le citesc la un interval fix?
Dacă valorile sunt puține și intervalul este lung, le puteți colecta și prin citire. Ca să urmăriți schimbările valorilor, puteți folosi un abonament, prin care lăsați eșantionarea în seama serverului și acesta vă notifică schimbările. În ambele cazuri, înregistrați și codul de stare și marcajele de timp care vin împreună cu valoarea.
De ce primesc BadCertificateUntrusted când mă conectez?
Pentru că clientul nu are încă încredere în certificatul serverului. Biblioteca .NET a OPC Foundation salvează certificatele respinse în depozitul rejected. Verificați amprenta certificatului cu administratorul serverului, apoi mutați-l în depozitul trusted. Folosiți setarea care acceptă automat certificatele doar în mediul de dezvoltare.
Ce număr de port folosește OPC UA?
La IANA, portul 4840/TCP este înregistrat ca opcua-tcp. Serverele reale folosesc însă uneori alt port, așa că verificați-l în setările echipamentului. Dacă serverul rulează pe Windows, trebuie să permiteți comunicația spre acel port în Paravanul de protecție Windows, iar dacă între ele se află un echipament de rețea, și pe acesta.

Profilul autorului

Pagina de profil a autorului articolului.

Go Komura

Reprezentant al KomuraSoft LLC

Specializat în dezvoltarea de software Windows, consultanță tehnică și investigarea defectelor, cu puncte forte în proiectele care pornesc de la resurse software existente și în investigarea avariilor ale căror cauze sunt greu de identificat.

Înapoi la lista de articole