Cronologia delle revisioni (prima versione, pubblicata il 1 Nov 2026)
- Prima pubblicazione
Citare questo articolo(DOI: 10.5281/zenodo.23968394)
Questo articolo è archiviato su Zenodo. Qui sotto trovi sia il DOI che rimanda sempre all'ultima versione sia quello fissato alla versione che stai leggendo.
Go Komura (2026). Introduzione a OPC UA ── Raccogliere i dati di PLC e macchinari con un'app Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968394 https://comcomponent.com/it/blog/opc-ua-windows-data-collection/
- DOI (ultima versione)
- 10.5281/zenodo.23968394
- DOI (questa versione)
- 10.5281/zenodo.23968395
Quando si raccolgono i dati di PLC e macchinari con un’app Windows e il macchinario ha un server OPC UA, a volte si può evitare di realizzare una comunicazione diversa per ogni produttore. Collegarsi e leggere i valori, però, non basta. Senza conoscere lo stato e i timestamp restituiti insieme al valore, le sottoscrizioni con cui farsi notificare le variazioni, l’autorizzazione della connessione tramite certificati e il modo di riprendersi da una disconnessione, in seguito non ci si potrà fidare dei dati raccolti.
Questo articolo spiega le parti di OPC UA necessarie alla raccolta dei dati, insieme ai risultati ottenuti facendo girare davvero un client in C#. Per il client ho usato la libreria .NET della OPC Foundation e, come controparte, un server che simula un macchinario. Non ho provato il funzionamento con un PLC o un macchinario specifico. Le impostazioni dei singoli macchinari vanno verificate nella loro documentazione.
L’acquisizione di immagini da telecamere GigE Vision e USB3 Vision è trattata in «Dropped Frames with GigE Vision and USB3 Vision Cameras», i dispositivi con comunicazione seriale in «Pitfall delle app di comunicazione seriale».
Differenze tra OPC UA e OPC Classic
Le specifiche di OPC Classic scambiano dati tra componenti software tramite COM/DCOM di Windows. OPC DA per i dati di processo, OPC A&E per gli allarmi e OPC HDA per i dati storici sono definite come specifiche separate1.
Nel 2008 la OPC Foundation ha pubblicato OPC UA (Unified Architecture). OPC UA è un’architettura orientata ai servizi che non dipende da una piattaforma specifica e riunisce le funzioni delle singole specifiche Classic. Come motivi della riprogettazione, la OPC Foundation cita tra l’altro il fatto che Microsoft aveva iniziato a privilegiare meccanismi multipiattaforma rispetto a COM/DCOM, la necessità di funzionare anche su sistemi non Microsoft e su dispositivi embedded e la necessità di collegarsi in modo sicuro attraverso i firewall1.
La specifica OPC UA è normata anche dalla IEC come serie IEC 625412. Per la comunicazione, presso IANA la porta 4840/TCP è registrata come opcua-tcp3.
flowchart TB
accTitle: Differenze tra OPC Classic e OPC UA
accDescr: Diagramma che mostra che OPC Classic si basa su COM e DCOM di Windows ed è diviso nelle specifiche separate DA, A&E e HDA, mentre OPC UA riunisce le loro funzioni in un'unica specifica indipendente dalla piattaforma.
C["OPC Classic"] --> D["Funziona su COM/DCOM (Windows)"]
D --> DA["Specifiche separate DA, A&E, HDA"]
DA -->|"Riunisce le funzioni"| U["OPC UA (2008)"]
U --> P["Indipendente da una piattaforma specifica"]
P --> S["Normato come IEC 62541"]
Figura 1: OPC Classic è un insieme di specifiche separate su COM/DCOM, OPC UA le riunisce in una sola.
Se un impianto esistente ha solo un server OPC DA, sul lato client serve un meccanismo che usi COM/DCOM. Questo articolo tratta solo OPC UA.
Nel diagramma, una linea continua indica una relazione che vale sempre e una linea tratteggiata indica una relazione condizionale (le condizioni sono nella spiegazione di ciascuna relazione nella pagina di dettaglio). L’elenco completo delle relazioni (in totale 13, con evidenza e livello di certezza) e le definizioni dei concetti principali sono raccolti nella pagina di dettaglio della mappa della conoscenza (in giapponese). Dati: JSON-LD / Turtle
La configurazione provata
Ho fatto girare la configurazione seguente.
| Ruolo | Cosa ho usato |
|---|---|
| Client | C#, .NET 10 SDK 10.0.114, pacchetto NuGet OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1 |
| Server | Un macchinario simulato realizzato con Python 3.13 e asyncua 2.1.0 (porta 4840 sullo stesso PC) |
| Sistema operativo | Linux (l’esecuzione su Windows non è stata provata) |
La libreria client è l’implementazione .NET pubblicata dalla OPC Foundation. Per l’uso della serie 2.0 mi sono attenuto a Getting started nello stesso repository4.
flowchart TB
accTitle: La configurazione provata
accDescr: Diagramma della configurazione in cui un client C# usa la libreria .NET della OPC Foundation per collegarsi con una connessione cifrata a un server asyncua che simula un macchinario in ascolto sulla porta 4840.
A["Client C# (LineCollector)"] -->|"opc.tcp, SignAndEncrypt"| B["Server che simula il macchinario (asyncua)"]
A -.-> L["Libreria .NET della OPC Foundation 2.0.1"]
B -.-> V["Quattro variabili sotto Line1"]
Figura 2: Sullo stesso PC, il client C# si collegava al server che simula il macchinario.
Il server che simula il macchinario
Al posto di un macchinario ho realizzato un piccolo server con asyncua5. Sotto l’oggetto Line1 ci sono quattro variabili: temperatura (Temperature), numero di pezzi (PartCount), in funzione (Running) e valore di riferimento (Setpoint). La temperatura cambia ogni 0,5 secondi, il numero di pezzi ogni 2 secondi. Per la sicurezza ho previsto due varianti: senza cifratura (None) e con firma e cifratura (SignAndEncrypt di Basic256Sha256).
# Macchinario simulato per i test. Non sostituisce macchinari o PLC reali.
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())
Questo server accetta i client che si collegano senza verificarne i certificati. È un’impostazione di test e non corrisponde al comportamento di un macchinario reale.
Il client C#
Nel file di progetto ho inserito i due pacchetti seguenti.
<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>
Il client esegue nell’ordine connessione, esplorazione (Browse), lettura, scrittura e sottoscrizione. Il significato di ciascuna parte è spiegato in ordine nelle sezioni seguenti.
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}");
// L'indice del namespace lo decide il server. Lo si ricava dall'URI e da lì si costruiscono i 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);
// Browse: elenca le variabili sotto 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}");
}
// Lettura: guardare non solo il valore ma anche lo stato e i due timestamp
DataValue t = await session.ReadValueAsync(temperature, ct);
Console.WriteLine($"Temperature = {t.WrappedValue} 状態 {t.StatusCode}");
Console.WriteLine($" Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");
// Leggendo un nodo inesistente si ottiene uno stato Bad invece di un valore
try
{
await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
}
catch (ServiceResultException ex)
{
Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
}
// Scrittura: impostare Setpoint a 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]}");
// Sottoscrizione: chiedere al server un campionamento ogni 500 ms e farsi notificare le variazioni
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");
Alla prima esecuzione non è stato possibile collegarsi. Il motivo è spiegato nella sezione «Autorizzare le connessioni con i certificati». Questo è l’output dopo aver reso attendibile il certificato del server. I log di avviso della libreria sono omessi.
接続: 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]
Gli orari sono in UTC.
I dati del macchinario appaiono come nodi dello spazio degli indirizzi
Un server OPC UA espone i dati che possiede come un insieme di nodi. Questo insieme si chiama spazio degli indirizzi. I nodi sono di vari tipi, come gli oggetti che rappresentano cartelle o macchinari e le variabili che hanno valori, e sono collegati tra loro da riferimenti. Il client segue i riferimenti con il servizio Browse per scoprire quali nodi esistono6.
Nell’output qui sopra il client segue da Line1 i riferimenti gerarchici e trova le quattro variabili.
flowchart TB
accTitle: Lo spazio degli indirizzi del macchinario simulato
accDescr: Diagramma che mostra che sotto la cartella Objects c'è l'oggetto Line1 e sotto di esso le quattro variabili Temperature, PartCount, Running e Setpoint.
O["Objects"] --> L["Line1 (oggetto)"]
L --> T["Temperature (variabile)"]
L --> P["PartCount (variabile)"]
L --> R["Running (variabile)"]
L --> S["Setpoint (variabile, scrivibile)"]
Figura 3: Lo spazio degli indirizzi del macchinario simulato. Il client trova i nodi seguendo i riferimenti con Browse.
Un NodeId è composto da namespace e identificatore
I nodi si identificano con il NodeId. ns=2;s=Line1.Temperature nell’output indica che l’indice del namespace è 2 e che l’identificatore è la stringa Line1.Temperature.
Dietro un namespace c’è un URI. La specifica stabilisce che all’interno dei servizi gli URI dei namespace sono rappresentati da numeri e che quel numero corrisponde alla posizione nel NamespaceArray del server. L’indice 0 è il namespace di OPC UA http://opcfoundation.org/UA/7.
Per questo il valore dell’indice non si scrive nel codice: si ricava l’indice dall’URI. Nel codice qui sopra session.NamespaceUris.GetIndex(LineNamespace) ricava 2 dall’URI e da lì si costruiscono i NodeId.
flowchart TB
accTitle: Come si ottiene un NodeId dall'URI del namespace
accDescr: Diagramma che mostra che il client cerca l'URI del namespace nel NamespaceArray del server, ottiene l'indice e combina quell'indice con l'identificatore per costruire il NodeId.
U["URI del namespace"] --> A["NamespaceArray del server"]
A --> I["Indice (in questo esempio 2)"]
I --> N["NodeId ns=2;s=Line1.Temperature"]
K["Identificatore Line1.Temperature"] --> N
Figura 4: L’indice del namespace si ricava dall’URI e si combina con l’identificatore per ottenere il NodeId.
Lettura e scrittura restituiscono insieme al valore lo stato e i timestamp
I valori delle variabili si leggono e si scrivono con i servizi Read e Write8. La lettura non restituisce solo il valore. In una struttura chiamata DataValue vengono restituiti il valore, il codice di stato, il timestamp della sorgente e il timestamp del server9.
| Campo | Significato |
|---|---|
| Valore | Il valore della variabile. Quando il codice di stato indica un errore, il valore va ignorato e il server lo imposta a null |
| Codice di stato (StatusCode) | Se il server è riuscito a ottenere e fornire quel valore |
| SourceTimestamp | L’orario che la sorgente dei dati ha assegnato al valore |
| ServerTimestamp | L’orario in cui il server ha ricevuto il valore o ha saputo che era corretto |
I due bit più alti del codice di stato indicano Good (successo), Uncertain (successo parziale, a seconda dell’uso il valore può non essere utilizzabile) e Bad (errore)10. Good [0x00000000] nell’output è Good.
Leggendo Line1.Pressure, che non esiste, invece di un valore è stato restituito BadNodeIdUnknown [0x80340000]. Il metodo ReadValueAsync di questa libreria solleva un’eccezione quando lo stato è Bad4. Se un sistema che registra valori conserva anche il codice di stato accanto al valore, in seguito si possono distinguere i valori registrati durante uno stato Bad.
flowchart TB
accTitle: Il contenuto di un DataValue
accDescr: Diagramma che mostra che il DataValue restituito dalle letture e dalle notifiche ha quattro elementi, valore, codice di stato, timestamp della sorgente e timestamp del server, e che se il codice di stato è Bad il valore va ignorato.
D["DataValue"] --> V["Valore"]
D --> S["Codice di stato"]
D --> TS["SourceTimestamp (orario della sorgente)"]
D --> TV["ServerTimestamp (orario del server)"]
S -->|"se Bad"| X["Ignorare il valore"]
Figura 5: Il risultato di una lettura contiene non solo il valore ma anche un codice di stato e due timestamp.
In scrittura ho scritto 32.5 in Setpoint e come codice di stato del risultato è stato restituito Good. Se una variabile sia scrivibile lo decide il server. In questo server simulato è scrivibile solo Setpoint. Se rendere modificabili dall’esterno i valori di riferimento di un macchinario riguarda la progettazione della sicurezza del macchinario e non rientra nell’ambito di questo articolo.
Farsi notificare le variazioni: sottoscrizioni e MonitoredItem
Per seguire le variazioni dei valori, il client può ripetere Read a intervalli regolari. OPC UA offre in alternativa un meccanismo che affida al server il monitoraggio dei valori e si fa notificare le variazioni: le sottoscrizioni (Subscription) e i MonitoredItem.
Un MonitoredItem è un singolo oggetto monitorato e ha un intervallo di campionamento. L’intervallo di campionamento è l’intervallo più rapido con cui il server campiona i valori dalla sorgente, e la specifica lo definisce un ciclo «best effort»11. Quando un campione soddisfa la condizione del filtro, viene generata una notifica. Se non si specifica un filtro, la notifica arriva quando cambia il valore o lo stato12.
Le notifiche entrano prima nella coda del MonitoredItem. Per il caso in cui la coda trabocchi si stabilisce se scartare le più vecchie o le più nuove e, se la dimensione della coda è 2 o più, nel codice di stato viene impostato il bit Overflow, che indica lo scarto13.
Una sottoscrizione raggruppa più MonitoredItem e a ogni intervallo di pubblicazione invia al client le notifiche non ancora inviate. Se si susseguono cicli senza notifiche da inviare, invia dei keep-alive che comunicano che la sottoscrizione è ancora attiva14.
flowchart TB
accTitle: Il flusso di sottoscrizioni e MonitoredItem
accDescr: Diagramma che mostra che un MonitoredItem campiona i valori della sorgente all'intervallo di campionamento e mette in coda le notifiche che superano il filtro, che la sottoscrizione le invia insieme al client all'intervallo di pubblicazione e che, se non ci sono notifiche, invia un keep-alive.
S["Sorgente (valori del macchinario)"] --> M["MonitoredItem"]
M -->|"Intervallo di campionamento"| F["Filtro (predefinito: variazione di valore o stato)"]
F --> Q["Coda (se trabocca, Overflow)"]
Q --> SUB["Sottoscrizione"]
SUB -->|"Intervallo di pubblicazione"| C["Client"]
SUB -.->|"se non ci sono notifiche"| K["Keep-alive"]
Figura 6: Il MonitoredItem campiona e accumula in coda, la sottoscrizione invia tutto insieme al client.
Nel codice qui sopra ho sottoscritto Temperature e PartCount con un intervallo di campionamento di 500 millisecondi e una dimensione della coda di 515. Nell’output la temperatura arriva circa ogni 0,5 secondi, il numero di pezzi circa ogni 2 secondi. Le prime due voci erano i valori che le variabili avevano al momento dell’avvio della sottoscrizione. 13:40:50.801 del numero di pezzi è l’orario in cui il valore era cambiato prima dell’avvio della sottoscrizione. Per le notifiche viene mostrato il timestamp della sorgente, quindi compare non l’orario di ricezione della notifica ma l’orario in cui il valore è cambiato.
Quando ripetere la lettura e quando sottoscrivere
Ripetendo la lettura, è il client a stabilire l’intervallo e il meccanismo è semplice. In compenso richiesta e risposta viaggiano ogni volta anche se il valore non è cambiato, e le variazioni più brevi dell’intervallo vengono perse. Con la sottoscrizione il server campiona e invia solo le variazioni, quindi anche per valori che cambiano raramente il traffico aumenta poco. Anche con la sottoscrizione, però, le variazioni più brevi dell’intervallo di campionamento possono non essere notificate. La specifica consente di campionare più velocemente di quanto indicato, ma non lo richiede11.
flowchart TB
accTitle: Differenza tra lettura ripetuta e sottoscrizione
accDescr: Diagramma che mostra che con la lettura ripetuta il client stabilisce l'intervallo e invia ogni volta una richiesta, con la sottoscrizione il server campiona all'intervallo di campionamento e invia solo le variazioni, e che in entrambi i casi le variazioni più brevi dell'intervallo possono non essere rilevate.
A["Seguire le variazioni di un valore"] --> R["Ripetere la lettura"]
A --> S["Sottoscrivere"]
R --> R1["Il client stabilisce l'intervallo e chiede ogni volta"]
S --> S1["Il server campiona e invia solo le variazioni"]
R1 --> X["Le variazioni più brevi dell'intervallo possono sfuggire"]
S1 --> X
Figura 7: Con entrambi i metodi, le variazioni più brevi dell’intervallo possono sfuggire.
Autorizzare le connessioni con i certificati
In una connessione OPC UA il livello di protezione dei messaggi si sceglie con MessageSecurityMode. I valori sono None senza protezione, Sign con sola firma e SignAndEncrypt con firma e cifratura16. La combinazione di algoritmi usati per firma e cifratura è stabilita dal criterio di sicurezza (security policy); in questo articolo ho usato Basic256Sha256.
Per firma e cifratura si usa un certificato per ciascuna applicazione. Client e server verificano reciprocamente i certificati e rifiutano le connessioni con controparti che non considerano attendibili. Un server realizzato con la libreria .NET della OPC Foundation, per impostazione predefinita, non offre il criterio None senza cifratura4.
La prima connessione si è fermata con BadCertificateUntrusted
Alla prima esecuzione il client non considerava attendibile il certificato del server e la connessione è stata rifiutata. Una parte dell’output:
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.'
Questa libreria salva i certificati rifiutati nell’archivio rejected. Quando un amministratore li sposta da lì all’archivio trusted, la libreria considera attendibile quella controparte17. In questo caso ho spostato il certificato del server da pki/rejected/certs a pki/trusted/certs, ho eseguito di nuovo e la connessione è riuscita. Prima di spostarlo si verifica con l’amministratore del server l’identificazione personale (thumbprint), che compare anche nel nome del file4.
flowchart TB
accTitle: Il percorso per rendere attendibile un certificato
accDescr: Diagramma che mostra che alla prima connessione il client rifiuta il certificato del server e lo salva nell'archivio rejected, e che dopo aver verificato l'identificazione personale e averlo spostato nell'archivio trusted, alla connessione successiva lo considera attendibile e si collega.
A["Prima connessione"] --> B["BadCertificateUntrusted"]
B --> C["Certificato salvato nell'archivio rejected"]
C --> D["Verificare il thumbprint con l'amministratore del server"]
D --> E["Spostarlo nell'archivio trusted"]
E --> F["Connessione successiva: attendibile e collegata"]
Figura 8: Verificando un certificato rifiutato e spostandolo in trusted, il client considera attendibile quella controparte.
Esiste anche l’impostazione AutoAcceptUntrustedCertificates, che accetta automaticamente i certificati non attendibili. Il valore predefinito è false, e la documentazione della libreria scrive che è utile negli ambienti di sviluppo ma non va usata in produzione17.
NotAfter=2027-11-01 nel log è la data di scadenza del certificato del server simulato. È un certificato autofirmato creato con la funzione di asyncua e scadeva un anno dopo la data di creazione. Anche i certificati dei macchinari hanno una scadenza, quindi stabilite nella gestione operativa una procedura per sostituirli prima della scadenza e farli considerare attendibili dalla controparte. La gestione dell’archivio certificati di Windows è riassunta in «Guida pratica all’archivio certificati di Windows».
Autenticazione dell’utente
Con i certificati si verificano le applicazioni tra loro. Separatamente, all’apertura di una sessione si passa un token che identifica l’utente. La specifica prevede quattro tipi: anonimo, nome utente e password, certificato X.509 e token emesso esternamente18. Il client di questo esempio si è collegato in modo anonimo. Quali token accetta un server è indicato nelle informazioni sugli endpoint del server19.
Dove si inciampa nella raccolta dei dati sul campo
Di seguito elenco, basandomi su questo esperimento e sulla specifica, i problemi che capitano spesso nei sistemi che raccolgono dati dai macchinari.
I valori del periodo in cui il server era fermo non arrivano
Durante la sottoscrizione ho fermato il server e l’ho riavviato dopo circa 15 secondi. ManagedSession di questa libreria si riconnette automaticamente quando la connessione cade20. Mentre il server era fermo, il client tentava ripetutamente di riconnettersi.
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.
Quando il server è tornato, sono tornate anche le notifiche. Una parte dell’output della sottoscrizione:
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]
Dalle 13:41:13 alle 13:41:34 non è arrivato nemmeno un valore. Il numero di pezzi è tornato da 78 a 2. Questo perché il server simulato è stato riavviato e ha ricominciato a contare da 0.
In questo esempio era fermo il server stesso, quindi i valori di quel periodo non sono conservati da nessuna parte. Il meccanismo di riconnessione ripristina la connessione, ma non riempie a posteriori i valori del periodo di fermo.
La situazione è diversa se si è interrotta solo la comunicazione e il server ha continuato a funzionare. La specifica prevede il servizio Republish, con cui ci si fa reinviare dalla coda di ritrasmissione del server i messaggi di notifica non arrivati21. Le notifiche si accumulano anche nella coda del MonitoredItem, ma quelle oltre la dimensione della coda vengono scartate13. Questo caso non l’ho provato nell’esperimento. In entrambi i casi conservate nei registri gli orari di disconnessione e riconnessione, in modo che si capisca in quale intervallo non sono arrivati valori. La riconnessione e la progettazione dei log per i dispositivi con comunicazione seriale sono trattate anche in «Pitfall delle app di comunicazione seriale».
flowchart TB
accTitle: Cosa registrare quando il server si ferma
accDescr: Diagramma che mostra che se durante la sottoscrizione il server si ferma, il client tenta ripetutamente di riconnettersi, che quando il server torna tornano anche le notifiche ma i valori del periodo di fermo non arrivano, e che quindi si registrano gli orari di disconnessione e riconnessione.
A["Durante la sottoscrizione"] --> B["Il server si ferma"]
B --> C["Il client tenta ripetutamente di riconnettersi"]
C --> D["Il server torna e con lui le notifiche"]
B -.-> E["I valori del periodo di fermo non arrivano"]
E -.-> F["Registrare gli orari di disconnessione e riconnessione"]
Figura 9: La riconnessione ripristina la connessione, ma i valori del periodo di fermo non arrivano.
Quale timestamp registrare
Secondo la specifica, il SourceTimestamp è l’orario che la sorgente dei dati ha assegnato al valore e dovrebbe indicare il momento in cui il valore è cambiato. Il ServerTimestamp è l’orario in cui il server ha ricevuto il valore o ha saputo che era corretto9. Il server simulato di questo esempio genera da sé i valori, quindi i due orari coincidevano.
Se volete sapere quando il valore è cambiato nel macchinario, usate il SourceTimestamp. Quell’orario però viene assegnato con l’orologio della sorgente, e se l’orologio del macchinario o del server è sfasato, anche la registrazione sarà sfasata. Se confrontate i dati raccolti con altre registrazioni, conservate accanto anche l’orario del PC che li ha ricevuti: in seguito potrete verificare lo sfasamento. La sincronizzazione dell’ora sul lato Windows è riassunta in «Windows Time Synchronization (w32time) and Business Systems».
Non trattare come semplici numeri i valori con stato diverso da Good
Quando il codice di stato è Bad, il valore va ignorato9. Quando è Uncertain, a seconda dell’uso il valore può non essere utilizzabile10. Prima di inserirli in grafici o aggregazioni, smistate i valori in base al codice di stato e stabilite come trattare quelli diversi da Good. Anche il bit Overflow, in caso di trabocco della coda, viene impostato nel codice di stato13. Come mostrare a schermo lo stato dei macchinari è trattato in «Best practice per controllare e visualizzare lo stato di dispositivi esterni».
Aprire la porta nel firewall
La porta di OPC UA registrata presso IANA è 4840/TCP3. Non tutti i server, però, usano questo numero. Gli esempi .NET della OPC Foundation usano tra l’altro la porta 625414. Controllate il numero di porta nell’URL dell’endpoint del macchinario e consentite le connessioni a quella porta. Se il server gira su Windows, lo si fa nel Windows Firewall di quella macchina e, se in mezzo ci sono apparati di rete, su quegli apparati. Come creare le regole in ingresso è riassunto in «Il firewall di Windows e le applicazioni aziendali».
flowchart TB
accTitle: Dove si inciampa nella raccolta dei dati
accDescr: Diagramma che divide i punti in cui si inciampa nella raccolta dei dati dai macchinari in contenuto delle registrazioni (i valori del periodo di fermo del server non arrivano, quale timestamp registrare, come trattare i valori diversi da Good) e preparazione della connessione (porta nel firewall, scadenza dei certificati).
A["Dove si inciampa nella raccolta dei dati"] --> R["Contenuto delle registrazioni"]
A --> N["Preparazione della connessione"]
R --> B["I valori del periodo di fermo non arrivano"]
B ~~~ C["Quale timestamp"]
C ~~~ D["Valori diversi da Good"]
N --> E["Porta nel firewall"]
E ~~~ F["Scadenza dei certificati"]
Figura 10: Anche dopo essersi collegati, controllate lacune nelle registrazioni, orari, stato, porte e certificati.
Quando usare Modbus/TCP o i protocolli proprietari del produttore
Per comunicare con PLC e macchinari, oltre a OPC UA esistono Modbus/TCP e i protocolli forniti dal produttore del PLC.
La specifica Modbus gestisce i dati in quattro tabelle: i Discrete Input da 1 bit in sola lettura, i Coil da 1 bit in lettura e scrittura, gli Input Register da 16 bit in sola lettura e gli Holding Register da 16 bit in lettura e scrittura22. Presso IANA la porta 502/TCP è registrata per il protocollo applicativo Modbus (mbap)3.
In queste tabelle entrano solo bit e parole da 16 bit, quindi si verifica nella documentazione del macchinario quale valore, in quale formato, si trova a quale indirizzo, e lo si scrive sul lato client. Con OPC UA, come abbiamo visto, il server espone nello spazio degli indirizzi variabili con un nome e il client può scoprirle con Browse. Ai valori sono associati codice di stato e timestamp, e la connessione è autorizzata tramite certificati.
Cosa usare dipende spesso da ciò che il macchinario offre. Se il macchinario ha un server OPC UA si usa quello; se c’è solo Modbus/TCP si prepara una tabella di corrispondenza degli indirizzi e si legge in base a quella. Se c’è solo il protocollo del produttore o un SDK, si segue la relativa documentazione. Come scegliere il metodo di comunicazione con i dispositivi USB è riassunto in «How to Work with USB Devices from a Windows App».
flowchart TB
accTitle: Come scegliere la comunicazione con il macchinario
accDescr: Diagramma che mostra che se il macchinario ha un server OPC UA si esplorano e raccolgono le variabili con OPC UA, se c'è solo Modbus/TCP si verificano indirizzi e formati nella documentazione e si leggono i registri, e se c'è solo il protocollo del produttore si segue la sua documentazione.
A["Comunicazione offerta dal macchinario"] --> B{"Cosa c'è?"}
B -->|"Server OPC UA"| C["Esplorare e raccogliere le variabili"]
B -->|"Solo Modbus/TCP"| D["Verificare indirizzi e formati nella documentazione"]
B -->|"Solo protocollo del produttore"| E["Seguire documentazione e SDK del produttore"]
D --> F["Leggere i registri e convertirli in valori"]
Figura 11: Con cosa raccogliere i dati dipende spesso da ciò che il macchinario offre.
Articoli correlati
- Dropped Frames with GigE Vision and USB3 Vision Cameras — Locating the Cause and the Windows Settings to Review
- Pitfall delle app di comunicazione seriale - Riconnessione e progettazione dei log
- Best practice per controllare e visualizzare lo stato di dispositivi esterni
- How to Work with USB Devices from a Windows App
- Il firewall di Windows e le applicazioni aziendali
- Guida pratica all’archivio certificati di Windows
- Windows Time Synchronization (w32time) and Business Systems
Riferimenti
-
OPC Foundation, Classic. Il fatto che OPC Classic si basa su COM/DCOM, le specifiche DA, A&E e HDA, la pubblicazione di OPC UA nel 2008 e i motivi della riprogettazione. ↩ ↩2
-
IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. La parte 1 della serie IEC 62541, dedicata ai concetti e alla panoramica di OPC UA (sostituita da un’edizione successiva). ↩
-
IANA, Service Name and Transport Protocol Port Number Registry. La registrazione di
opcua-tcpsu 4840/TCP e dimbapsu 502/TCP. ↩ ↩2 ↩3 -
OPC Foundation, UA-.NETStandard 2.0.1: Getting started. Come scrivere un client della serie 2.0, il fatto che
ReadValueAsyncsolleva un’eccezione in caso di Bad, gli archivi dei certificati rifiutati e attendibili, il fatto che i server non offronoNoneper impostazione predefinita e i numeri di porta degli esempi. ↩ ↩2 ↩3 ↩4 ↩5 -
FreeOpcUa, asyncua (PyPI). La libreria OPC UA per Python usata per il server che simula il macchinario. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Il servizio che segue i riferimenti di un nodo. ↩
-
OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. Il fatto che all’interno dei servizi gli URI dei namespace sono rappresentati da numeri e che quel numero corrisponde a una posizione nel NamespaceArray. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. I servizi Read e Write. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Le definizioni di valore, codice di stato, SourceTimestamp e ServerTimestamp. ↩ ↩2 ↩3
-
OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain e Bad indicati dai due bit più alti. ↩ ↩2
-
OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. Il fatto che l’intervallo di campionamento è un ciclo best effort. ↩ ↩2
-
OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. Il fatto che senza filtro la notifica avviene alla variazione di valore o stato. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. La dimensione della coda, il criterio di scarto e il bit Overflow. ↩ ↩2 ↩3
-
OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. L’intervallo di pubblicazione e i keep-alive. ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set.
SubscribeDataChangesAsynce l’intervallo di campionamento e la dimensione della coda inMonitoredItemOptions. ↩ -
OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. Il significato di None, Sign e SignAndEncrypt. ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Certificates. La prassi di spostare i certificati dall’archivio rejected all’archivio attendibile e il fatto di non usare
AutoAcceptUntrustedCertificatesin produzione. ↩ ↩2 -
OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. I quattro tipi: anonimo, nome utente, X.509 e token emesso. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. L’elenco dei token utente accettati da un endpoint (userIdentityTokens). ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. Il fatto che
ManagedSessionintegra un meccanismo di riconnessione. ↩ -
OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. Il servizio con cui ci si fa reinviare i messaggi di notifica dalla coda di ritrasmissione di una sottoscrizione. ↩
-
Modbus-IDA, MODBUS Application Protocol Specification V1.1b (una copia ospitata su un server dell’Università di Chicago). Il modello dei dati della sezione 4.3 (Discrete Input, Coil, Input Register, Holding Register). ↩
Articoli correlati
Articoli recenti con gli stessi tag per approfondire argomenti vicini.
Best practice pratiche sul multithreading: edizione .NET — Cosa decidere prima di aggiungere altri thread
Le regole di progettazione per .NET/C# che impediscono che, dopo aver avviato un thread, l'app «crashi o si blocchi di tanto in tanto». L...
Usare WMI/CIM da C# e PowerShell — Guida pratica a lettura hardware, monitoraggio processi e interrogazioni remote
Il modo classico per leggere il numero di serie del PC, monitorare lo spazio disco e rilevare l'avvio dei processi è WMI/CIM. L'articolo ...
Le profondità dell'I/O di Windows (4ª puntata) — Cache Manager: quando il vostro WriteFile arriva sul disco
Quarta puntata della serie che spiega con i diagrammi il Cache Manager di Windows. Si organizzano la cache implementata come file mapping...
Come comprendere l'isolamento delle sessioni di Windows — Session 0, RDP ed esecuzione simultanea di più utenti
Questo articolo chiarisce il concetto di «sessione» di Windows, un argomento che genera costante confusione tra gli sviluppatori di appli...
Checklist per gestire in sicurezza processi figli nelle app Windows - Job Object, propagazione exit, stdio e watchdog
Come far morire i processi figli con il padre su Windows: Job Object, propagazione exit, drenaggio stdout/stderr e posizionamento del wat...
Argomenti correlati
Queste sono pagine di argomenti vicini a questo tema. A partire dall'articolo, potete passare ai servizi correlati e ad altri articoli.
Argomenti tecnici Windows
Portale che riunisce gli argomenti tecnici su sviluppo Windows, indagine sui difetti e valorizzazione delle risorse esistenti.
Servizi collegati all’argomento
Questo articolo si collega alle pagine dei servizi seguenti. Partite da un ingresso vicino alla vostra situazione.
Sviluppo di applicazioni Windows
Un'app Windows che raccoglie dati dai macchinari, comprese le impostazioni delle sottoscrizioni, la riconnessione e la registrazione di timestamp e stato, può essere realizzata come sviluppo di app Windows.
Consulenza tecnica e revisione del progetto
Se volete decidere se raccogliere i dati con OPC UA o usare Modbus/TCP o il protocollo del produttore, e come gestire i certificati, possiamo affrontarlo come consulenza tecnica e revisione del progetto.
Domande frequenti
Domande che ricorrono nelle consulenze sull’argomento dell’articolo.
- Qual è la differenza tra OPC UA e OPC Classic (OPC DA)?
- OPC Classic è una specifica che scambia dati tramite COM/DCOM di Windows. OPC UA è una specifica pubblicata nel 2008 che non dipende da un sistema operativo specifico e riunisce le funzioni di Classic. I client OPC UA funzionano anche al di fuori di Windows e non serve configurare DCOM.
- Per raccogliere valori con OPC UA basta leggerli a intervalli regolari?
- Se i valori sono pochi e l'intervallo è lungo, si possono raccogliere anche con la lettura. Per seguire le variazioni dei valori si può invece affidare il campionamento al server con una sottoscrizione e farsi notificare le variazioni. In entrambi i casi registrate il codice di stato e i timestamp restituiti insieme al valore.
- Perché alla connessione ottengo BadCertificateUntrusted?
- Perché il client non considera ancora attendibile il certificato del server. La libreria .NET della OPC Foundation salva i certificati rifiutati nell'archivio rejected. Verificate l'identificazione personale (thumbprint) del certificato con l'amministratore del server e solo dopo spostatelo nell'archivio trusted. Le impostazioni che accettano automaticamente i certificati vanno limitate agli ambienti di sviluppo.
- Qual è il numero di porta di OPC UA?
- Presso IANA la porta 4840/TCP è registrata come opcua-tcp. Tuttavia i server reali a volte usano un'altra porta, quindi controllate le impostazioni del macchinario. Se il server gira su Windows, occorre consentire le connessioni a quella porta in Windows Firewall e, se in mezzo ci sono apparati di rete, anche su quegli apparati.
Profilo dell’autore
Pagina di presentazione dell’autore dell’articolo.
Go Komura
Rappresentante di KomuraSoft LLC
Si occupa soprattutto di sviluppo di software Windows, consulenza tecnica e indagine sui malfunzionamenti, con punti di forza nei progetti in cui restano risorse esistenti e nelle indagini su guasti dalla causa difficile da individuare.