Introducción a OPC UA ── Recopilar datos de PLC y equipos en una aplicación de Windows

· · Windows, OPC UA, Fabricación, PLC, C#, .NET, Comunicaciones, Certificados

Publicar en X Enviar a Menéame
Historial de revisiones (primera versión, publicada el 1 Nov 2026)
Primera publicación
Citar este artículo(DOI: 10.5281/zenodo.23968401)

Este artículo está archivado en Zenodo. A continuación se muestran tanto el DOI que siempre resuelve a la última versión como el DOI fijado a la versión que está leyendo.

Go Komura (2026). Introducción a OPC UA ── Recopilar datos de PLC y equipos en una aplicación de Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968401 https://comcomponent.com/es/blog/opc-ua-windows-data-collection/

DOI (última versión)
10.5281/zenodo.23968401
DOI (esta versión)
10.5281/zenodo.23968402

Cuando se recopilan datos de PLC o de equipos en una aplicación de Windows y el equipo tiene un servidor OPC UA, a veces se puede evitar programar una comunicación distinta para cada fabricante. Pero no basta con conectarse y leer valores. Si no se conocen el estado y las marcas de tiempo que llegan con cada valor, las suscripciones que notifican los cambios, la autorización de la conexión mediante certificados y la forma de recuperarse de una desconexión, después no se podrá confiar en los datos recopilados.

En este artículo se explican las partes de OPC UA que hacen falta para la recopilación de datos, junto con los resultados de ejecutar realmente un cliente en C#. Como cliente se usó la biblioteca .NET de la OPC Foundation y, al otro lado, un servidor que simula un equipo. No se ha probado con ningún PLC ni equipo concreto. Compruebe la configuración propia de cada equipo en su documentación.

La recopilación de imágenes desde cámaras GigE Vision y USB3 Vision se trata en «Dropped Frames with GigE Vision and USB3 Vision Cameras», y los equipos que usan comunicación serie, en «Puntos débiles de las aplicaciones de comunicación serie».

Diferencias entre OPC UA y OPC Classic

Las especificaciones de OPC Classic intercambian datos entre componentes de software mediante COM/DCOM de Windows. OPC DA para los datos de proceso, OPC A&E para las alarmas y OPC HDA para el historial están definidas como especificaciones separadas1.

La OPC Foundation publicó OPC UA (Unified Architecture) en 2008. OPC UA es una arquitectura orientada a servicios que no depende de una plataforma concreta y reúne las funciones de cada especificación de Classic. Como motivos del rediseño, la OPC Foundation menciona que Microsoft pasó a dar más importancia a los mecanismos multiplataforma que a COM/DCOM, que hacía falta funcionar también en sistemas que no fueran de Microsoft y en dispositivos embebidos, y que había que conectarse de forma segura a través de cortafuegos1.

La especificación de OPC UA también se ha normalizado en la IEC como la serie IEC 625412. En cuanto al puerto de comunicación, la IANA tiene registrado el 4840/TCP como opcua-tcp3.

Diferencias entre OPC Classic y OPC UADiagrama que muestra que OPC Classic se basa en COM y DCOM de Windows y se divide en las especificaciones separadas DA, A&E y HDA, mientras que OPC UA reúne esas funciones en una única especificación independiente de la plataforma.Reúne las funcionesOPC ClassicFunciona sobre COM/DCOM (Windows)Especificaciones separadas DA, A&E y HDAOPC UA (2008)No depende de una plataforma concretaNormalizado como IEC 62541

Figura 1: OPC Classic son especificaciones separadas sobre COM/DCOM, y OPC UA las reúne en una sola.

Si una instalación existente solo tiene un servidor OPC DA, hace falta un mecanismo en el lado que usa COM/DCOM. Este artículo trata solo OPC UA.

En el diagrama, una línea continua marca una relación que siempre se cumple y una línea discontinua una relación condicional (las condiciones están en la explicación de cada relación en la página de detalle). La lista completa de relaciones (13 en total, con evidencia y grado de certeza) y las definiciones de los conceptos principales están reunidas en la página de detalle del mapa de conocimiento (en japonés). Datos: JSON-LD / Turtle

La configuración probada

Esta es la configuración que se ejecutó.

Función Qué se usó
Cliente C#, .NET 10 SDK 10.0.114, paquete NuGet OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1
Servidor Un equipo simulado hecho con Python 3.13 y asyncua 2.1.0 (puerto 4840 del mismo PC)
Sistema operativo Linux (no se probó la ejecución en Windows)

La biblioteca del cliente es la implementación .NET que publica la OPC Foundation. Para la forma de usar la serie 2.0 se siguió lo descrito en Getting started, del mismo repositorio4.

La configuración probadaDiagrama que muestra un cliente en C# que usa la biblioteca .NET de la OPC Foundation y se conecta con una conexión cifrada a un servidor de equipo simulado con asyncua que escucha en el puerto 4840.opc.tcp, SignAndEncryptCliente en C# (LineCollector)Servidor de equipo simulado (asyncua)Biblioteca .NET de la OPC Foundation 2.0.1Cuatro variables de Line1

Figura 2: Dentro del mismo PC, el cliente en C# se conectó al servidor de equipo simulado.

El servidor de equipo simulado

En lugar de un equipo real, se creó un pequeño servidor con asyncua5. Bajo un objeto llamado Line1 hay cuatro variables: la temperatura (Temperature), el número de piezas producidas (PartCount), si está en marcha (Running) y el valor de consigna (Setpoint). La temperatura cambia cada 0,5 segundos y el número de piezas cada 2 segundos. En cuanto a la seguridad, se prepararon dos opciones: sin cifrado (None) y con firma y cifrado (SignAndEncrypt de Basic256Sha256).

# Equipo simulado para pruebas. No sustituye a equipos ni PLC de producción.
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())

Este servidor acepta el certificado de los clientes que se conectan sin comprobarlo. Es una configuración de prueba y no se comporta como un equipo de producción.

El cliente en C#

En el archivo de proyecto se incluyeron estos dos paquetes.

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

El cliente se conecta, explora, lee, escribe y se suscribe, en ese orden. El significado de cada parte se explica en las secciones siguientes.

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

        // El índice del espacio de nombres lo decide el servidor. Se obtiene a partir de la URI y con él se crean los 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);

        // Exploración: lista las variables que hay bajo 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}");
        }

        // Lectura: se mira no solo el valor, sino también el estado y las dos marcas de tiempo
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // Al leer un nodo que no existe, no se devuelve un valor sino un estado Bad
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // Escritura: pone 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]}");

        // Suscripción: se pide al servidor que muestree cada 500 ms y notifique los cambios
        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 primera vez que se ejecutó, no se pudo conectar. El motivo se explica en la sección «Autorizar la conexión mediante certificados». Esta es la salida tras confiar en el certificado del servidor. Se han omitido los registros de advertencia de la biblioteca.

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

Las horas están en UTC.

Los datos del equipo aparecen como nodos del espacio de direcciones

Un servidor OPC UA publica los datos que tiene como un conjunto de nodos. Este conjunto se llama espacio de direcciones. Hay distintos tipos de nodos, como los objetos, que representan carpetas o equipos, y las variables, que tienen valores, y los nodos se enlazan entre sí mediante referencias. El cliente recorre las referencias con el servicio Browse para averiguar qué nodos hay6.

En la salida anterior, el cliente recorre las referencias jerárquicas desde Line1 y encuentra cuatro variables.

Espacio de direcciones del equipo simuladoDiagrama que muestra que bajo la carpeta Objects hay un objeto Line1 y que bajo él hay cuatro variables, Temperature, PartCount, Running y Setpoint.ObjectsLine1 (objeto)Temperature (variable)PartCount (variable)Running (variable)Setpoint (variable, escribible)

Figura 3: Espacio de direcciones del equipo simulado. El cliente encuentra los nodos recorriendo las referencias con Browse.

Un NodeId se compone de un espacio de nombres y un identificador

Los nodos se identifican con un NodeId. El ns=2;s=Line1.Temperature de la salida indica que el índice del espacio de nombres es 2 y que el identificador es la cadena Line1.Temperature.

Lo que hay detrás de un espacio de nombres es una URI. La especificación establece que, dentro de los servicios, la URI del espacio de nombres se representa con un número, y que ese número corresponde a una posición del NamespaceArray del servidor. El índice 0 es el espacio de nombres de OPC UA, http://opcfoundation.org/UA/7.

Por eso, en lugar de escribir el número del índice en el código, se obtiene el índice a partir de la URI. En el código anterior, session.NamespaceUris.GetIndex(LineNamespace) obtiene el 2 a partir de la URI y con él se crean los NodeId.

Cómo se crea un NodeId a partir de la URI del espacio de nombresDiagrama que muestra que el cliente busca la URI del espacio de nombres en el NamespaceArray del servidor para obtener el índice, y combina ese índice con el identificador para crear el NodeId.URI del espacio de nombresNamespaceArray del servidorÍndice (2 en este ejemplo)NodeId ns=2;s=Line1.TemperatureIdentificador Line1.Temperature

Figura 4: El índice del espacio de nombres se obtiene a partir de la URI y se combina con el identificador para formar el NodeId.

En la lectura y la escritura, el valor llega con un estado y marcas de tiempo

Los valores de las variables se leen y escriben con los servicios Read y Write8. La lectura no devuelve solo el valor. Devuelve una estructura llamada DataValue, con el valor, el código de estado, la marca de tiempo de origen y la marca de tiempo del servidor9.

Elemento Significado
Valor El valor de la variable. Cuando el código de estado indica un error, el valor se ignora y el servidor lo pone a null
Código de estado (StatusCode) Si el servidor pudo obtener y proporcionar ese valor
SourceTimestamp La hora que el origen de los datos asignó al valor
ServerTimestamp La hora en que el servidor recibió el valor o supo que era correcto

Los dos bits superiores del código de estado indican Good (correcto), Uncertain (correcto en parte; según el uso puede no servir) o Bad (error)10. El Good [0x00000000] de la salida es Good.

Al leer Line1.Pressure, que no existe, se devolvió BadNodeIdUnknown [0x80340000] en lugar de un valor. El ReadValueAsync de esta biblioteca lanza una excepción cuando el estado es Bad4. En un sistema que registra valores, guardar el código de estado junto al valor permite distinguir después los valores registrados mientras el estado era Bad.

El contenido de un DataValueDiagrama que muestra que el DataValue que devuelven las lecturas y las notificaciones tiene cuatro elementos, el valor, el código de estado, la marca de tiempo de origen y la marca de tiempo del servidor, y que el valor se ignora si el código de estado es Bad.Si es BadDataValueValorCódigo de estadoSourceTimestamp (hora del origen)ServerTimestamp (hora del servidor)Se ignora el valor

Figura 5: El resultado de una lectura no tiene solo el valor, sino también un código de estado y dos marcas de tiempo.

En la escritura se escribió 32.5 en Setpoint, y como código de estado del resultado se devolvió Good. Si una variable se puede escribir o no lo decide el servidor. En este servidor simulado solo Setpoint es escribible. Si conviene permitir que los valores de consigna de un equipo se cambien desde fuera es una cuestión del diseño de seguridad del equipo, y no se trata en este artículo.

Recibir notificaciones de los cambios: suscripciones y MonitoredItem

Para seguir los cambios de un valor, el cliente puede repetir Read a intervalos fijos. OPC UA ofrece además otro mecanismo, en el que se deja la vigilancia del valor en manos del servidor y este notifica los cambios. Son las suscripciones y los MonitoredItem.

Un MonitoredItem es un elemento que se vigila, y tiene un intervalo de muestreo. El intervalo de muestreo es el intervalo más rápido con el que el servidor muestrea el valor desde el origen, y la especificación lo define como un periodo de «mejor esfuerzo»11. Cuando una muestra cumple la condición del filtro, se genera una notificación. Si no se especifica un filtro, se notifica cuando cambia el valor o el estado12.

Las notificaciones entran primero en la cola del MonitoredItem. Para cuando la cola se desborda, se decide si se descartan las más antiguas o las más nuevas, y si el tamaño de la cola es 2 o más, se activa en el código de estado el bit Overflow, que indica que se ha descartado algo13.

Una suscripción agrupa varios MonitoredItem y, en cada intervalo de publicación, envía al cliente las notificaciones que todavía no ha enviado. Si se suceden periodos sin notificaciones que enviar, envía un keep-alive para indicar que la suscripción sigue viva14.

El flujo de las suscripciones y los MonitoredItemDiagrama que muestra que un MonitoredItem muestrea el valor del origen con el intervalo de muestreo, pone en la cola las notificaciones que pasan el filtro, la suscripción las envía agrupadas al cliente en cada intervalo de publicación y, si no hay notificaciones, envía un keep-alive.Intervalo de muestreoIntervalo de publicaciónSi no hay notificacionesOrigen (valor del equipo)MonitoredItemFiltro (por defecto, cambio de valor o de estado)Cola (si se desborda, Overflow)SuscripciónClienteKeep-alive

Figura 6: El MonitoredItem muestrea y acumula en la cola, y la suscripción lo envía agrupado al cliente.

En el código anterior se suscribió a Temperature y PartCount con un intervalo de muestreo de 500 milisegundos y un tamaño de cola de 515. En la salida, la temperatura llega aproximadamente cada 0,5 segundos y el número de piezas aproximadamente cada 2 segundos. Las dos primeras notificaciones eran los valores que tenía cada variable al empezar la suscripción. El 13:40:50.801 del número de piezas es la hora en que el valor cambió antes de empezar la suscripción. Como en las notificaciones se muestra la marca de tiempo de origen, aparece la hora en que cambió el valor, no la hora en que se recibió la notificación.

Cuándo repetir lecturas y cuándo suscribirse

Repetir lecturas permite al cliente decidir el intervalo, y el mecanismo es sencillo. A cambio, aunque el valor no haya cambiado, cada vez van y vienen una petición y una respuesta, y se pierden los cambios más cortos que el intervalo. Con una suscripción, el servidor muestrea y envía solo los cambios, así que la comunicación crece poco aunque el valor cambie poco. Aun así, con una suscripción tampoco se notifican siempre los cambios más cortos que el intervalo de muestreo. La especificación permite muestrear más rápido de lo indicado, pero no lo exige11.

Diferencias entre repetir lecturas y suscribirseDiagrama que muestra que al repetir lecturas el cliente decide el intervalo y hace una petición cada vez, que con la suscripción el servidor muestrea con el intervalo de muestreo y envía solo los cambios, y que en ambos casos se pueden perder los cambios más cortos que el intervalo.Seguir los cambios del valorRepetir lecturasSuscribirseEl cliente decide el intervalo y pide cada vezEl servidor muestrea y envía solo los cambiosSe pueden perder cambios más cortos que el intervalo

Figura 7: Con cualquiera de los dos métodos se pueden perder los cambios más cortos que el intervalo.

Autorizar la conexión mediante certificados

En una conexión OPC UA, el grado de protección de los mensajes se elige con MessageSecurityMode. Los valores son None, sin protección; Sign, solo con firma; y SignAndEncrypt, con firma y cifrado16. La combinación de algoritmos para la firma y el cifrado se decide con la directiva de seguridad, y en este artículo se usó Basic256Sha256.

Para la firma y el cifrado se usa un certificado propio de cada aplicación. El cliente y el servidor comprueban mutuamente sus certificados y rechazan la conexión con quien no es de confianza. Los servidores creados con la biblioteca .NET de la OPC Foundation no ofrecen por defecto la directiva None, sin cifrado4.

La primera conexión se detuvo con BadCertificateUntrusted

En la primera ejecución, el cliente no confiaba en el certificado del servidor y la conexión se rechazó. Este es un fragmento de la salida.

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

Esta biblioteca guarda los certificados rechazados en el almacén rejected. Cuando un administrador los mueve de ahí al almacén trusted, la biblioteca pasa a confiar en ese interlocutor17. En esta prueba, se movió el certificado del servidor que estaba en pki/rejected/certs a pki/trusted/certs y, al volver a ejecutar, se pudo conectar. Antes de moverlo, compruebe con el administrador del servidor la huella digital, que también figura en el nombre del archivo4.

El flujo hasta confiar en el certificadoDiagrama que muestra que en la primera conexión el cliente rechaza el certificado del servidor y lo guarda en el almacén rejected, y que tras comprobar la huella digital y moverlo al almacén trusted, en la siguiente conexión confía en él y puede conectarse.Primera conexiónBadCertificateUntrustedEl certificado se guarda en el almacén rejectedSe comprueba la huella digital con el administrador del servidorSe mueve al almacén trustedEn la siguiente conexión confía y se conecta

Figura 8: Si se comprueba el certificado rechazado y se mueve a trusted, se confía en ese interlocutor.

También existe la opción AutoAcceptUntrustedCertificates, que acepta automáticamente los certificados que no son de confianza. Su valor por defecto es false, y la documentación de la biblioteca dice que es útil en entornos de desarrollo, pero que no debe usarse en producción17.

El NotAfter=2027-11-01 del registro es la fecha de caducidad del certificado del servidor simulado. Es un certificado autofirmado creado con una función de asyncua, y caducaba un año después del día en que se creó. Los certificados de los equipos también caducan, así que conviene fijar dentro de la operación el procedimiento para sustituirlos antes de que caduquen y hacer que el otro lado confíe en el nuevo. El manejo del almacén de certificados de Windows se resume en «Guía práctica del almacén de certificados de Windows».

La autenticación del usuario

Lo que se comprueba con los certificados es la relación entre aplicaciones. Aparte de eso, al abrir una sesión se entrega un token que identifica al usuario. La especificación define cuatro tipos: anónimo, nombre de usuario y contraseña, certificado X.509 y token emitido externamente18. El cliente de esta prueba se conectó de forma anónima. Qué tokens acepta un servidor figura en la información de sus endpoints19.

Dónde se tropieza al recopilar datos en planta

A partir de aquí se enumeran los problemas que suelen aparecer en los sistemas que recopilan datos de equipos, basándose en este experimento y en la especificación.

No llegan los valores del tiempo en que el servidor estuvo detenido

A mitad de la suscripción se detuvo el servidor y se volvió a arrancar unos 15 segundos después. El ManagedSession de esta biblioteca se reconecta automáticamente cuando se corta la conexión20. Mientras el servidor estuvo detenido, el cliente siguió intentando reconectarse.

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.

Cuando el servidor volvió, también volvieron las notificaciones. Este es un fragmento de la salida de la suscripción.

  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]

No llegó ni un solo valor de entre las 13:41:13 y las 13:41:34. El número de piezas volvió de 78 a 2. Esto se debe a que, al rearrancar el servidor simulado, el recuento empezó de nuevo desde 0.

En este ejemplo era el propio servidor el que estaba detenido, así que los valores de ese tiempo no quedaron guardados en ninguna parte. El mecanismo de reconexión restablece la conexión, pero no rellena después los valores del tiempo en que todo estuvo detenido.

Si solo se corta la comunicación y el servidor sigue funcionando, la situación es distinta. La especificación tiene el servicio Republish, que pide al servidor que vuelva a enviar desde su cola de reenvío los mensajes de notificación que no llegaron21. Las notificaciones también se acumulan en la cola del MonitoredItem, pero lo que supera el tamaño de la cola se descarta13. En este experimento no se probó este caso. En ambos casos, deje registradas las horas de desconexión y de reconexión, para que se sepa en qué intervalo no llegaron valores. La reconexión y el diseño de los registros en equipos de comunicación serie también se tratan en «Puntos débiles de las aplicaciones de comunicación serie».

El registro cuando se detiene el servidorDiagrama que muestra que, si el servidor se detiene durante la suscripción, el cliente repite la reconexión y, al volver el servidor, vuelven las notificaciones, pero los valores del tiempo detenido no llegan, por lo que se registran las horas de desconexión y reconexión.Durante la suscripciónEl servidor se detieneEl cliente repite la reconexiónVuelve el servidor y vuelven las notificacionesNo llegan los valores del tiempo detenidoSe registran las horas de desconexión y reconexión

Figura 9: La reconexión restablece la conexión, pero no llegan los valores del tiempo detenido.

Qué marca de tiempo registrar

Según la especificación, SourceTimestamp es la hora que el origen de los datos asignó al valor, y debería indicar la hora en que cambió el valor. ServerTimestamp es la hora en que el servidor recibió el valor o supo que era correcto9. El servidor simulado de esta prueba genera él mismo los valores, así que las dos horas coincidían.

Si se quiere saber la hora en que cambió el valor en el equipo, se usa SourceTimestamp. Pero esa hora la asigna el reloj del origen, así que, si el reloj del equipo o del servidor está desviado, el registro también lo estará. Si los datos recopilados se van a cotejar con otros registros, guardar también al lado la hora del PC que los recibió permite comprobar después la desviación. La sincronización horaria en Windows se resume en «Sincronización horaria de Windows (w32time) y los sistemas de negocio».

No tratar como números sin más los valores cuyo estado no es Good

Cuando el código de estado es Bad, el valor debe ignorarse9. Cuando es Uncertain, según el uso puede ser un valor que no sirva10. Antes de meter los valores en gráficos o totales, clasifíquelos por el código de estado y decida cómo tratar los que no sean Good. El bit Overflow de cuando la cola se desborda también se activa dentro del código de estado13. Cómo mostrar en pantalla el estado de los equipos se trata en «Buenas prácticas para verificar y mostrar el estado de dispositivos externos».

Abrir el puerto en el cortafuegos

El puerto de OPC UA registrado en la IANA es el 4840/TCP3. Sin embargo, no todos los servidores usan este número. Los ejemplos .NET de la OPC Foundation usan puertos como el 625414. Compruebe el número de puerto en la URL del endpoint del equipo y permita las conexiones a ese puerto. Si el servidor se ejecuta en Windows, se permiten en el Firewall de Windows de ese equipo, y si hay equipos de red por el camino, en esos equipos. Cómo crear las reglas de entrada se resume en «El firewall de Windows y las aplicaciones empresariales».

Dónde se tropieza al recopilar datosDiagrama que divide los puntos donde se tropieza al recopilar datos de equipos en el contenido del registro (que no lleguen los valores del tiempo en que el servidor estuvo detenido, qué marca de tiempo registrar, cómo tratar los valores que no son Good) y en la preparación de la conexión (el puerto del cortafuegos, la caducidad del certificado).Dónde se tropieza al recopilar datosContenido del registroPreparación de la conexiónNo llegan los valores del tiempo detenidoQué marca de tiempoValores que no son GoodEl puerto del cortafuegosLa caducidad del certificado

Figura 10: Incluso después de conectar, compruebe los huecos del registro, las horas, el estado, el puerto y el certificado.

Cuándo usar Modbus/TCP o la comunicación propia del fabricante

Para comunicarse con PLC y equipos, además de OPC UA existen Modbus/TCP y las comunicaciones que ofrece el fabricante del PLC.

La especificación de Modbus maneja los datos en cuatro tablas: Discrete Input, de 1 bit y solo lectura; Coil, de 1 bit y lectura y escritura; Input Register, de 16 bits y solo lectura; y Holding Register, de 16 bits y lectura y escritura22. En la IANA está registrado el 502/TCP como protocolo de aplicación de Modbus (mbap)3.

Lo que cabe en estas tablas son bits y palabras de 16 bits, así que qué valor hay en qué dirección y con qué formato se comprueba en la documentación del equipo y se escribe en el lado del cliente. En OPC UA, como hemos visto, el servidor publica variables con nombre en el espacio de direcciones y el cliente puede averiguarlas con Browse. Los valores llevan un código de estado y marcas de tiempo, y la conexión se autoriza mediante certificados.

Qué usar suele decidirse por lo que ofrece el equipo. Si el equipo tiene un servidor OPC UA, se usa; si solo tiene Modbus/TCP, se crea una tabla de correspondencia de direcciones y se lee. Si solo hay una comunicación o un SDK del fabricante, se sigue su documentación. Cómo elegir el método de comunicación con dispositivos USB se resume en «Cómo gestionar dispositivos USB en aplicaciones de Windows».

Cómo elegir la comunicación con el equipoDiagrama que muestra que, si el equipo tiene un servidor OPC UA, se recopilan los datos explorando las variables con OPC UA; si solo tiene Modbus/TCP, se comprueban en la documentación las direcciones y los formatos y se leen los registros; y si solo tiene la comunicación del fabricante, se sigue su documentación.Servidor OPC UASolo Modbus/TCPSolo la comunicación del fabricanteComunicación que ofrece el equipo¿Qué tiene?Explorar las variables y recopilarComprobar direcciones y formatos en la documentaciónSeguir la documentación y el SDK del fabricanteLeer los registros y convertirlos en valores

Figura 11: Con qué recopilar suele decidirse por lo que ofrece el equipo.

Artículos relacionados

Enlaces de referencia

  1. OPC Foundation, Classic. Que OPC Classic se basa en COM/DCOM, las especificaciones DA, A&E y HDA, la publicación de OPC UA en 2008 y los motivos del rediseño. ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. Primera parte de la serie IEC 62541, sobre los conceptos y la visión general de OPC UA (sustituida por una edición posterior). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. Los registros de opcua-tcp en el 4840/TCP y de mbap en el 502/TCP. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. Cómo escribir un cliente de la serie 2.0, que ReadValueAsync lanza una excepción con Bad, los almacenes de rechazados y de confianza, que el servidor no ofrece None por defecto y los números de puerto de los ejemplos. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). La biblioteca OPC UA de Python usada para el servidor de equipo simulado. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. El servicio que recorre las referencias de los nodos. ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. Que dentro de los servicios la URI del espacio de nombres se representa con un número y que ese número corresponde a una posición del NamespaceArray. ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Los servicios Read y Write. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Las definiciones del valor, el código de estado, SourceTimestamp y ServerTimestamp. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain y Bad, que indican los dos bits superiores. ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. Que el intervalo de muestreo es un periodo de mejor esfuerzo. ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. Que, sin filtro, se notifica cuando cambia el valor o el estado. ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. El tamaño de la cola, la forma de descartar y el bit Overflow. ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. El intervalo de publicación y el keep-alive. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync y el intervalo de muestreo y el tamaño de cola de MonitoredItemOptions. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. El significado de None, Sign y SignAndEncrypt. ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. La práctica de mover los certificados del almacén rejected al almacén de confianza, y que AutoAcceptUntrustedCertificates no debe usarse en producción. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. Los cuatro tipos: anónimo, nombre de usuario, X.509 y token emitido. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. La lista de tokens de usuario que acepta un endpoint (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. Que ManagedSession lleva incorporado el mecanismo de reconexión. ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. El servicio que pide que se vuelvan a enviar los mensajes de notificación desde la cola de reenvío de la suscripción. ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b (copia alojada en un servidor de la Universidad de Chicago). El modelo de datos de la sección 4.3 (Discrete Input, Coil, Input Register y Holding Register). ↩

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas son páginas de temas cercanos a este. A partir del artículo, puede pasar a los servicios relacionados y a otros artículos.

Este artículo enlaza con las siguientes páginas de servicios. Puede empezar por una entrada cercana a su situación.

Desarrollo de aplicaciones para Windows

Si quiere crear una aplicación de Windows que recopile datos de los equipos, incluida la configuración de las suscripciones, la reconexión y el registro de las marcas de tiempo y del estado, podemos tratarlo como desarrollo de aplicaciones para Windows.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿En qué se diferencian OPC UA y OPC Classic (OPC DA)?
OPC Classic es un conjunto de especificaciones que intercambian datos mediante COM/DCOM de Windows. OPC UA es una especificación publicada en 2008 que no depende de un sistema operativo concreto y reúne las funciones de Classic. Los clientes de OPC UA funcionan también fuera de Windows y no necesitan configurar DCOM.
Para recopilar valores con OPC UA, ¿basta con leerlos a intervalos fijos?
Si hay pocos valores y el intervalo es largo, también se pueden recopilar leyéndolos. Para seguir los cambios de los valores, se puede usar una suscripción, que deja el muestreo en manos del servidor y hace que notifique los cambios. En ambos casos, registre el código de estado y las marcas de tiempo que llegan junto con el valor.
¿Por qué al conectar aparece BadCertificateUntrusted?
Porque el cliente todavía no confía en el certificado del servidor. La biblioteca .NET de la OPC Foundation guarda los certificados rechazados en el almacén rejected. Compruebe la huella digital del certificado con el administrador del servidor y luego muévalo al almacén trusted. Limite a los entornos de desarrollo la opción de aceptar certificados automáticamente.
¿Qué número de puerto usa OPC UA?
La IANA tiene registrado el 4840/TCP como opcua-tcp. Sin embargo, los servidores reales pueden usar otro puerto, así que compruébelo en la configuración del equipo. Si el servidor se ejecuta en Windows, hay que permitir el tráfico hacia ese puerto en el Firewall de Windows, y si hay equipos de red por el camino, también en esos equipos.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, con especial solvencia en proyectos en los que perduran activos existentes y en investigaciones de fallos cuya causa cuesta ver.

Volver al blog