Introduzione a OPC UA ── Raccogliere i dati di PLC e macchinari con un'app Windows

· · Windows, OPC UA, Manifattura, PLC, C#, .NET, Comunicazione, Certificati

Pubblica su X Condividi su LinkedIn Condividi su Telegram Condividi su WhatsApp
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.

Differenze tra OPC Classic e OPC UADiagramma 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.Riunisce le funzioniOPC ClassicFunziona su COM/DCOM (Windows)Specifiche separate DA, A&E, HDAOPC UA (2008)Indipendente da una piattaforma specificaNormato 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.

La configurazione provataDiagramma 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.opc.tcp, SignAndEncryptClient C# (LineCollector)Server che simula il macchinario (asyncua)Libreria .NET della OPC Foundation 2.0.1Quattro 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.

Lo spazio degli indirizzi del macchinario simulatoDiagramma che mostra che sotto la cartella Objects c'è l'oggetto Line1 e sotto di esso le quattro variabili Temperature, PartCount, Running e Setpoint.ObjectsLine1 (oggetto)Temperature (variabile)PartCount (variabile)Running (variabile)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.

Come si ottiene un NodeId dall'URI del namespaceDiagramma 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.URI del namespaceNamespaceArray del serverIndice (in questo esempio 2)NodeId ns=2;s=Line1.TemperatureIdentificatore Line1.Temperature

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.

Il contenuto di un DataValueDiagramma 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.se BadDataValueValoreCodice di statoSourceTimestamp (orario della sorgente)ServerTimestamp (orario del server)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.

Il flusso di sottoscrizioni e MonitoredItemDiagramma 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.Intervallo di campionamentoIntervallo di pubblicazionese non ci sono notificheSorgente (valori del macchinario)MonitoredItemFiltro (predefinito: variazione di valore o stato)Coda (se trabocca, Overflow)SottoscrizioneClientKeep-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.

Differenza tra lettura ripetuta e sottoscrizioneDiagramma 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.Seguire le variazioni di un valoreRipetere la letturaSottoscrivereIl client stabilisce l'intervallo e chiede ogni voltaIl server campiona e invia solo le variazioniLe variazioni più brevi dell'intervallo possono sfuggire

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.

Il percorso per rendere attendibile un certificatoDiagramma 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.Prima connessioneBadCertificateUntrustedCertificato salvato nell'archivio rejectedVerificare il thumbprint con l'amministratore del serverSpostarlo nell'archivio trustedConnessione 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».

Cosa registrare quando il server si fermaDiagramma 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.Durante la sottoscrizioneIl server si fermaIl client tenta ripetutamente di riconnettersiIl server torna e con lui le notificheI valori del periodo di fermo non arrivanoRegistrare 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».

Dove si inciampa nella raccolta dei datiDiagramma 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).Dove si inciampa nella raccolta dei datiContenuto delle registrazioniPreparazione della connessioneI valori del periodo di fermo non arrivanoQuale timestampValori diversi da GoodPorta nel firewallScadenza 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».

Come scegliere la comunicazione con il macchinarioDiagramma 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.Server OPC UASolo Modbus/TCPSolo protocollo del produttoreComunicazione offerta dal macchinarioCosa c'è?Esplorare e raccogliere le variabiliVerificare indirizzi e formati nella documentazioneSeguire documentazione e SDK del produttoreLeggere i registri e convertirli in valori

Figura 11: Con cosa raccogliere i dati dipende spesso da ciò che il macchinario offre.

Articoli correlati

Riferimenti

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

  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). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. La registrazione di opcua-tcp su 4840/TCP e di mbap su 502/TCP. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. Come scrivere un client della serie 2.0, il fatto che ReadValueAsync solleva un’eccezione in caso di Bad, gli archivi dei certificati rifiutati e attendibili, il fatto che i server non offrono None per impostazione predefinita e i numeri di porta degli esempi. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). La libreria OPC UA per Python usata per il server che simula il macchinario. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Il servizio che segue i riferimenti di un nodo. ↩

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

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. I servizi Read e Write. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Le definizioni di valore, codice di stato, SourceTimestamp e ServerTimestamp. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain e Bad indicati dai due bit più alti. ↩ ↩2

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

  12. 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. ↩

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

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. L’intervallo di pubblicazione e i keep-alive. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync e l’intervallo di campionamento e la dimensione della coda in MonitoredItemOptions. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. Il significato di None, Sign e SignAndEncrypt. ↩

  17. 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 AutoAcceptUntrustedCertificates in produzione. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. I quattro tipi: anonimo, nome utente, X.509 e token emesso. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. L’elenco dei token utente accettati da un endpoint (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. Il fatto che ManagedSession integra un meccanismo di riconnessione. ↩

  21. 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. ↩

  22. 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 recenti con gli stessi tag per approfondire argomenti vicini.

Queste sono pagine di argomenti vicini a questo tema. A partire dall'articolo, potete passare ai servizi correlati e ad altri articoli.

Questo articolo si collega alle pagine dei servizi seguenti. Partite da un ingresso vicino alla vostra situazione.

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.

Torna al blog