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.
flowchart TB
accTitle: Diferența dintre OPC Classic și OPC UA
accDescr: Diagramă 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ă.
C["OPC Classic"] --> D["Rulează pe COM/DCOM (Windows)"]
D --> DA["Specificații împărțite în DA, A&E și HDA"]
DA -->|"Reunirea funcțiilor"| U["OPC UA (2008)"]
U --> P["Nu depinde de o anumită platformă"]
P --> S["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.
flowchart TB
accTitle: Configurația încercată
accDescr: 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.
A["Client C# (LineCollector)"] -->|"opc.tcp, SignAndEncrypt"| B["Server simulat al echipamentului (asyncua)"]
A -.-> L["Biblioteca .NET a OPC Foundation 2.0.1"]
B -.-> V["Cele 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.
flowchart TB
accTitle: Spațiul de adrese al echipamentului simulat
accDescr: Diagramă care arată că sub dosarul Objects se află obiectul Line1, iar sub acesta cele patru variabile Temperature, PartCount, Running și Setpoint.
O["Objects"] --> L["Line1 (obiect)"]
L --> T["Temperature (variabilă)"]
L --> P["PartCount (variabilă)"]
L --> R["Running (variabilă)"]
L --> S["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.
flowchart TB
accTitle: Construirea unui NodeId din URI-ul spațiului de nume
accDescr: Diagramă 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.
U["URI-ul spațiului de nume"] --> A["NamespaceArray al serverului"]
A --> I["Index (în acest exemplu 2)"]
I --> N["NodeId ns=2;s=Line1.Temperature"]
K["Identificator Line1.Temperature"] --> N
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.
flowchart TB
accTitle: Conținutul unui DataValue
accDescr: Diagramă 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.
D["DataValue"] --> V["Valoare"]
D --> S["Cod de stare"]
D --> TS["SourceTimestamp (ora sursei)"]
D --> TV["ServerTimestamp (ora serverului)"]
S -->|"Dacă e Bad"| X["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.
flowchart TB
accTitle: Cum funcționează abonamentele și MonitoredItem
accDescr: Diagramă 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.
S["Sursa (valoarea echipamentului)"] --> M["MonitoredItem"]
M -->|"Interval de eșantionare"| F["Filtru (implicit schimbarea valorii sau a stării)"]
F --> Q["Coadă (la depășire, Overflow)"]
Q --> SUB["Abonament"]
SUB -->|"Interval de publicare"| C["Client"]
SUB -.->|"Dacă nu există notificări"| K["Keep-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.
flowchart TB
accTitle: Diferența dintre citirea repetată și abonament
accDescr: Diagramă 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.
A["Urmărirea schimbărilor valorilor"] --> R["Citire repetată"]
A --> S["Abonament"]
R --> R1["Clientul stabilește intervalul și cere de fiecare dată"]
S --> S1["Serverul eșantionează și trimite doar schimbările"]
R1 --> X["Schimbările mai scurte decât intervalul pot fi ratate"]
S1 --> X
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.
flowchart TB
accTitle: Pașii până la încrederea în certificat
accDescr: Diagramă 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ă.
A["Prima conexiune"] --> B["BadCertificateUntrusted"]
B --> C["Certificatul se salvează în depozitul rejected"]
C --> D["Se verifică amprenta cu administratorul serverului"]
D --> E["Se mută în depozitul trusted"]
E --> F["La 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”.
flowchart TB
accTitle: Înregistrarea la oprirea serverului
accDescr: Diagramă 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.
A["În timpul abonamentului"] --> B["Serverul se oprește"]
B --> C["Clientul încearcă repetat să se reconecteze"]
C --> D["Serverul revine și revin și notificările"]
B -.-> E["Valorile din perioada opririi nu sosesc"]
E -.-> F["Se î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”.
flowchart TB
accTitle: Unde se împotmolește colectarea datelor
accDescr: Diagramă 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).
A["Unde se împotmolește colectarea datelor"] --> R["Conținutul înregistrărilor"]
A --> N["Pregătirea conexiunii"]
R --> B["Valorile din timpul opririi nu sosesc"]
B ~~~ C["Ce marcaj de timp"]
C ~~~ D["Valorile care nu sunt Good"]
N --> E["Porturile din paravanul de protecție"]
E ~~~ F["Expirarea 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”.
flowchart TB
accTitle: Alegerea modului de comunicare cu echipamentul
accDescr: Diagramă 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.
A["Comunicația oferită de echipament"] --> B{"Ce este disponibil?"}
B -->|"Server OPC UA"| C["Se colectează variabilele prin răsfoire"]
B -->|"Doar Modbus/TCP"| D["Se verifică adresele și formatele în documentație"]
B -->|"Doar comunicația producătorului"| E["Se urmează documentația și SDK-ul producătorului"]
D --> F["Se citesc registrele și se convertesc în valori"]
Figura 11: Cu ce colectați este adesea decis de ce oferă echipamentul.
Articole conexe
- Dropped Frames with GigE Vision and USB3 Vision Cameras
- Serial Communication App Pitfalls
- Best Practices for Checking and Displaying External Device State
- How to Work with USB Devices from a Windows App
- The Windows Firewall and Business Applications
- The Windows Certificate Store in Practice
- Windows Time Synchronization (w32time) and Business Systems
Linkuri de referință
-
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
-
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ă). ↩
-
IANA, Service Name and Transport Protocol Port Number Registry. Înregistrarea
opcua-tcppe 4840/TCP și ambappe 502/TCP. ↩ ↩2 ↩3 -
OPC Foundation, UA-.NETStandard 2.0.1: Getting started. Cum se scrie un client din seria 2.0, faptul că
ReadValueAsyncaruncă o excepție la Bad, depozitele pentru certificate respinse și de încredere, faptul că serverul nu oferă implicitNoneși numerele de port din exemple. ↩ ↩2 ↩3 ↩4 ↩5 -
FreeOpcUa, asyncua (PyPI). Biblioteca OPC UA pentru Python folosită pentru serverul care simulează echipamentul. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Serviciul care parcurge referințele nodurilor. ↩
-
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. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Serviciile Read și Write. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Definițiile valorii, codului de stare, SourceTimestamp și ServerTimestamp. ↩ ↩2 ↩3
-
OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain și Bad, indicate de primii doi biți. ↩ ↩2
-
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
-
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. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. Dimensiunea cozii, modul de aruncare și bitul Overflow. ↩ ↩2 ↩3
-
OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. Intervalul de publicare și keep-alive. ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set.
SubscribeDataChangesAsyncși intervalul de eșantionare și dimensiunea cozii dinMonitoredItemOptions. ↩ -
OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. Semnificația None, Sign și SignAndEncrypt. ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Certificates. Practica mutării certificatelor din depozitul rejected în depozitul de încredere și faptul că
AutoAcceptUntrustedCertificatesnu trebuie folosită în producție. ↩ ↩2 -
OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. Cele patru tipuri: anonim, nume de utilizator, X.509 și token emis. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. Lista tokenurilor de utilizator acceptate de punctul final (userIdentityTokens). ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. Faptul că
ManagedSessionare încorporat mecanismul de reconectare. ↩ -
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. ↩
-
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). ↩
Articole conexe
Cele mai recente articole care au aceleași etichete. Aprofundați cunoștințele prin subiecte apropiate.
Ce se oprește când activați „conexiunea tarifată după utilizare”? ── Efectul asupra Windows Update, OneDrive și aplicațiilor din Store
Când activați în Windows conexiunea tarifată după utilizare, Windows Update descarcă doar actualizările prioritare, iar OneDrive întrerup...
Se pot șterge Windows.old și „Fișierele temporare”? ── Când folosiți Stocare inteligentă și când Curățare disc
Pentru Windows.old și fișierele temporare decide dacă pierdeți ceva definitiv după ștergere. Cele 10 zile pentru revenirea la Windows ant...
Ce schimbă modul de alimentare din Windows? ── Diferența dintre Cea mai bună performanță, Echilibrat și Cea mai bună eficiență energetică și cum alegeți
Explicăm ce schimbă modurile de alimentare din Windows 11 Cea mai bună performanță, Echilibrat și Cea mai bună eficiență energetică: setă...
Ce este „modul de joc” din Windows? ── Ce se schimbă când îl activați și dacă afectează și altceva decât jocurile
Modul de joc din Windows oprește, cât timp rulează un joc, instalarea driverelor și notificările de repornire de la Windows Update și urm...
Ce sunt „memoria GPU dedicată” și „memoria GPU partajată” din Managerul de activități? ── De ce numerele diferă atât de mult la GPU-urile integrate și ce se întâmplă când memoria nu ajunge
Explicăm pentru utilizatorii obișnuiți ce înseamnă memoria GPU dedicată și memoria GPU partajată din Managerul de activități. Rezumăm de ...
Subiecte conexe
Pagini de subiect apropiate de această temă. Pornind de la articol, puteți ajunge la servicii conexe și la alte articole.
Subiecte tehnice Windows
Punct de intrare către subiectele tehnice despre dezvoltarea de software Windows, investigarea defectelor și valorificarea resurselor existente.
Servicii legate de această temă
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.
Consultanță tehnică și revizuirea designului
Dacă vreți să decideți dacă să colectați prin OPC UA sau să folosiți Modbus/TCP ori comunicația producătorului și cum să gestionați certificatele, putem trata acest lucru ca consultanță tehnică și revizuire de proiect.
Î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.