Premiers pas avec OPC UA ── Collecter les données des automates et des équipements dans une application Windows

· · Windows, OPC UA, Industrie manufacturière, Automate programmable, C#, .NET, Communication, Certificats

Publier sur X Partager sur LinkedIn Partager sur Telegram Partager sur WhatsApp
Historique des révisions (première version, publiée le 1 Nov 2026)
Première publication
Citer cet article(DOI: 10.5281/zenodo.23968390)

Cet article est archivé sur Zenodo. Vous trouverez ci-dessous le DOI qui renvoie toujours à la dernière version et celui qui est figé sur la version que vous lisez.

Go Komura (2026). Premiers pas avec OPC UA ── Collecter les données des automates et des équipements dans une application Windows. KomuraSoft LLC. https://doi.org/10.5281/zenodo.23968390 https://comcomponent.com/fr/blog/opc-ua-windows-data-collection/

DOI (dernière version)
10.5281/zenodo.23968390
DOI (cette version)
10.5281/zenodo.23968391

Lorsque vous collectez les données d’un automate programmable (PLC) ou d’un équipement dans une application Windows et que l’équipement dispose d’un serveur OPC UA, vous pouvez parfois éviter d’écrire un protocole différent pour chaque constructeur. Se connecter et lire les valeurs ne suffit pas pour autant. Si vous ne connaissez pas l’état et les horodatages renvoyés avec chaque valeur, les abonnements qui notifient les changements, l’autorisation des connexions par certificat et la façon de se rétablir après une déconnexion, vous ne pourrez plus vous fier plus tard aux données collectées.

Cet article explique les parties d’OPC UA nécessaires à la collecte de données, avec les résultats de l’exécution réelle d’un client C#. Le client utilise la bibliothèque .NET de l’OPC Foundation, et l’autre extrémité est un serveur qui simule un équipement. Je n’ai essayé aucun automate ni aucun équipement en particulier. Vérifiez les réglages propres à chaque équipement dans sa documentation.

La collecte d’images depuis des caméras GigE Vision et USB3 Vision est traitée dans « Dropped Frames with GigE Vision and USB3 Vision Cameras », et les équipements en communication série dans « Pièges des applications de communication série ».

Différence entre OPC UA et OPC Classic

Les spécifications OPC Classic échangent des données entre composants logiciels au moyen de COM/DCOM de Windows. OPC DA pour les données de procédé, OPC A&E pour les alarmes et OPC HDA pour l’historique sont définies comme des spécifications distinctes1.

L’OPC Foundation a publié OPC UA (Unified Architecture) en 2008. OPC UA est une architecture orientée services qui ne dépend d’aucune plateforme particulière et réunit les fonctions de chacune des spécifications Classic. Parmi les raisons de cette refonte, l’OPC Foundation cite le fait que Microsoft a privilégié des mécanismes multiplateformes plutôt que COM/DCOM, la nécessité de fonctionner sur des systèmes non Microsoft et des équipements embarqués, et celle de se connecter de manière sûre à travers les pare-feu1.

La spécification OPC UA est aussi normalisée par l’IEC sous la forme de la série IEC 625412. Pour le port de communication, l’IANA a enregistré 4840/TCP sous le nom opcua-tcp3.

Différence entre OPC Classic et OPC UASchéma montrant qu'OPC Classic repose sur COM et DCOM de Windows et se divise en spécifications DA, A&E et HDA distinctes, tandis qu'OPC UA réunit leurs fonctions en une seule spécification indépendante de la plateforme.Réunit les fonctionsOPC ClassicFonctionne sur COM/DCOM (Windows)Spécifications séparées DA, A&E, HDAOPC UA (2008)Indépendant de la plateformeNormalisé sous IEC 62541

Figure 1 : OPC Classic se compose de spécifications séparées reposant sur COM/DCOM, et OPC UA les réunit en une seule.

Si un équipement existant ne dispose que d’un serveur OPC DA, il faut du côté client un mécanisme qui utilise COM/DCOM. Cet article ne traite que d’OPC UA.

Dans le diagramme, un trait continu marque une relation qui vaut toujours et un trait pointillé une relation conditionnelle (les conditions figurent dans l’explication de chaque relation sur la page de détail). La liste complète des relations (13 au total, avec preuve et niveau de certitude) et les définitions des concepts principaux sont rassemblées sur la page de détail de la carte des connaissances (en japonais). Données : JSON-LD / Turtle

Configuration testée

Voici la configuration que j’ai fait fonctionner.

Rôle Ce que j’ai utilisé
Client C#, .NET 10 SDK 10.0.114, paquet NuGet OPCFoundation.NetStandard.Opc.Ua.Client 2.0.1
Serveur Un équipement simulé réalisé avec Python 3.13 et asyncua 2.1.0 (port 4840 du même PC)
OS Linux (l’exécution sous Windows n’a pas été testée)

La bibliothèque cliente est l’implémentation .NET publiée par l’OPC Foundation. Pour l’utilisation de la branche 2.0, j’ai suivi la manière d’écrire de Getting started dans le même dépôt4.

Configuration testéeSchéma montrant un client C# qui utilise la bibliothèque .NET de l'OPC Foundation et se connecte par une connexion chiffrée à un serveur asyncua simulant un équipement, en écoute sur le port 4840.opc.tcp, SignAndEncryptClient C# (LineCollector)Serveur d'équipement simulé (asyncua)Bibliothèque .NET de l'OPC Foundation 2.0.1Quatre variables sous Line1

Figure 2 : Sur le même PC, le client C# s’est connecté au serveur d’équipement simulé.

Le serveur d’équipement simulé

À la place d’un équipement, j’ai réalisé un petit serveur avec asyncua5. Sous un objet nommé Line1, il comporte quatre variables : la température (Temperature), le nombre de pièces produites (PartCount), l’état de marche (Running) et la consigne (Setpoint). La température change toutes les 0,5 seconde et le nombre de pièces toutes les 2 secondes. Pour la sécurité, j’ai prévu deux options : sans chiffrement (None) et avec signature et chiffrement (SignAndEncrypt de Basic256Sha256).

# Équipement simulé pour les essais. Il ne remplace pas un équipement ou un automate de production.
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())

Ce serveur accepte les certificats des clients qui s’y connectent sans les vérifier. C’est un réglage d’essai, différent du comportement d’un équipement de production.

Le client C#

J’ai ajouté les deux paquets suivants au fichier de projet.

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

Le client se connecte, parcourt l’espace d’adressage, lit, écrit et s’abonne, dans cet ordre. Les sections suivantes expliquent le sens de chaque partie.

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

        // C'est le serveur qui fixe l'index de l'espace de noms. On le retrouve à partir de l'URI pour construire les 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);

        // Parcours (Browse) : liste les variables sous 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}");
        }

        // Lecture : on regarde l'état et les deux horodatages, pas seulement la valeur
        DataValue t = await session.ReadValueAsync(temperature, ct);
        Console.WriteLine($"Temperature = {t.WrappedValue}  状態 {t.StatusCode}");
        Console.WriteLine($"  Source {Hms(t.SourceTimestamp)} / Server {Hms(t.ServerTimestamp)}");

        // La lecture d'un nœud inexistant renvoie un état Bad au lieu d'une valeur
        try
        {
            await session.ReadValueAsync(new NodeId("Line1.Pressure", ns), ct);
        }
        catch (ServiceResultException ex)
        {
            Console.WriteLine($"Line1.Pressure の読み取り: {ex.StatusCode}");
        }

        // Écriture : on passe Setpoint à 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]}");

        // Abonnement : on demande au serveur un échantillonnage toutes les 500 ms et la notification des changements
        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 première exécution, la connexion a échoué. La raison est expliquée dans la section « Autoriser les connexions par certificat ». Voici la sortie obtenue après avoir fait confiance au certificat du serveur. Les journaux d’avertissement de la bibliothèque sont omis.

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

Les heures sont en UTC.

Les données de l’équipement apparaissent comme des nœuds de l’espace d’adressage

Un serveur OPC UA expose les données qu’il détient sous la forme d’un ensemble de nœuds. Cet ensemble s’appelle l’espace d’adressage. Les nœuds sont de plusieurs types, comme les objets, qui représentent des dossiers ou des équipements, et les variables, qui portent des valeurs ; les nœuds sont reliés entre eux par des références. Le client suit les références avec le service Browse pour découvrir quels nœuds existent6.

Dans la sortie ci-dessus, le client suit les références hiérarchiques depuis Line1 et trouve quatre variables.

Espace d'adressage de l'équipement simuléSchéma montrant un objet Line1 sous le dossier Objects, avec sous lui quatre variables, Temperature, PartCount, Running et Setpoint.ObjectsLine1 (objet)Temperature (variable)PartCount (variable)Running (variable)Setpoint (variable, inscriptible)

Figure 3 : Espace d’adressage de l’équipement simulé. Le client trouve les nœuds en suivant les références avec Browse.

Un NodeId se compose d’un espace de noms et d’un identifiant

Les nœuds sont identifiés par un NodeId. Dans la sortie, ns=2;s=Line1.Temperature signifie que l’index de l’espace de noms est 2 et que l’identifiant est la chaîne Line1.Temperature.

Un espace de noms est en réalité un URI. La spécification prévoit que les URI des espaces de noms sont représentés par des nombres dans les services, et que ce nombre correspond à une position dans le NamespaceArray du serveur. L’index 0 est l’espace de noms d’OPC UA, http://opcfoundation.org/UA/7.

C’est pourquoi on n’inscrit pas l’index en dur dans le code : on le retrouve à partir de l’URI. Le code ci-dessus obtient 2 à partir de l’URI avec session.NamespaceUris.GetIndex(LineNamespace) et construit les NodeId à partir de cette valeur.

Construire un NodeId à partir de l'URI d'un espace de nomsSchéma montrant que le client recherche l'URI de l'espace de noms dans le NamespaceArray du serveur pour obtenir l'index, puis combine cet index avec l'identifiant pour construire le NodeId.URI de l'espace de nomsNamespaceArray du serveurIndex (2 dans cet exemple)NodeId ns=2;s=Line1.TemperatureIdentifiant Line1.Temperature

Figure 4 : On retrouve l’index de l’espace de noms à partir de l’URI et on le combine avec l’identifiant pour former le NodeId.

La lecture et l’écriture renvoient un état et des horodatages avec la valeur

Les valeurs des variables se lisent et s’écrivent avec les services Read et Write8. La lecture ne renvoie pas seulement la valeur. Elle renvoie une structure appelée DataValue, qui contient la valeur, un code d’état, un horodatage de source et un horodatage de serveur9.

Élément Signification
Valeur La valeur de la variable. Quand le code d’état indique une erreur, la valeur est ignorée et le serveur la met à null
Code d’état (StatusCode) Si le serveur a pu obtenir et fournir la valeur
SourceTimestamp L’heure que la source des données a attachée à la valeur
ServerTimestamp L’heure à laquelle le serveur a reçu la valeur ou a su qu’elle était correcte

Les deux bits de poids fort du code d’état indiquent Good (succès), Uncertain (succès partiel, la valeur peut être inutilisable selon l’usage) ou Bad (échec)10. Le Good [0x00000000] de la sortie correspond à Good.

Lorsque j’ai lu Line1.Pressure, qui n’existe pas, c’est BadNodeIdUnknown [0x80340000] qui est revenu à la place d’une valeur. Dans cette bibliothèque, ReadValueAsync lève une exception quand l’état est Bad4. Si le système qui enregistre les valeurs conserve aussi le code d’état à côté de chaque valeur, on peut distinguer après coup les valeurs enregistrées pendant que l’état était Bad.

Contenu d'une DataValueSchéma montrant que la DataValue renvoyée par une lecture ou une notification contient quatre éléments, la valeur, le code d'état, l'horodatage de source et l'horodatage de serveur, et que la valeur est ignorée si le code d'état est Bad.Si BadDataValueValeurCode d'étatSourceTimestamp (heure à la source)ServerTimestamp (heure du serveur)Ignorer la valeur

Figure 5 : Le résultat d’une lecture contient non seulement la valeur, mais aussi un code d’état et deux horodatages.

Pour l’écriture, j’ai écrit 32,5 dans Setpoint, et le code d’état renvoyé était Good. C’est le serveur qui décide quelles variables sont inscriptibles. Dans ce serveur simulé, seule Setpoint l’est. Permettre ou non de modifier depuis l’extérieur les consignes d’un équipement relève de la conception de sa sécurité ; cet article ne traite pas cette question.

Se faire notifier les changements : abonnements et MonitoredItem

Pour suivre les variations des valeurs, le client peut répéter Read à intervalle fixe. OPC UA dispose en outre d’un mécanisme qui confie au serveur la surveillance des valeurs et lui fait notifier les changements : ce sont les abonnements (subscriptions) et les MonitoredItem.

Un MonitoredItem est un élément surveillé, doté d’un intervalle d’échantillonnage. L’intervalle d’échantillonnage est la cadence la plus rapide à laquelle le serveur échantillonne les valeurs depuis la source ; la spécification le décrit comme un cycle « au mieux » (best effort)11. Quand un échantillon remplit les conditions du filtre, une notification est créée. Si aucun filtre n’est indiqué, une notification est envoyée lorsque la valeur ou l’état change12.

Les notifications passent d’abord par la file d’attente du MonitoredItem. On décide à l’avance s’il faut rejeter la plus ancienne ou la plus récente quand la file déborde, et si la taille de la file est de 2 ou plus, le bit Overflow qui signale le rejet est positionné dans le code d’état13.

Un abonnement regroupe plusieurs MonitoredItem et, à chaque intervalle de publication, envoie au client les notifications pas encore envoyées. Quand plusieurs cycles passent sans notification à envoyer, il envoie un keep-alive pour indiquer que l’abonnement est toujours actif14.

Fonctionnement des abonnements et des MonitoredItemSchéma montrant qu'un MonitoredItem échantillonne la valeur de la source à l'intervalle d'échantillonnage et place dans une file les notifications qui passent le filtre, et que l'abonnement les envoie ensemble au client à l'intervalle de publication, en envoyant un keep-alive s'il n'y a pas de notification.Intervalle d'échantillonnageIntervalle de publicationSans notificationSource (valeur de l'équipement)MonitoredItemFiltre (par défaut : changement de valeur ou d'état)File (Overflow si elle déborde)AbonnementClientKeep-alive

Figure 6 : Le MonitoredItem échantillonne et met en file, et l’abonnement envoie les notifications ensemble au client.

Le code ci-dessus s’abonne à Temperature et à PartCount avec un intervalle d’échantillonnage de 500 millisecondes et une taille de file de 515. Dans la sortie, la température arrive environ toutes les 0,5 seconde et le nombre de pièces environ toutes les 2 secondes. Les deux premières notifications étaient les valeurs que chaque variable avait au début de l’abonnement. Le 13:40:50.801 du nombre de pièces est l’heure à laquelle sa valeur a changé, avant le début de l’abonnement. Comme les notifications affichent l’horodatage de source, c’est l’heure du changement de valeur qui apparaît, et non l’heure de réception de la notification.

Lectures répétées ou abonnement

Répéter les lectures permet au client de choisir l’intervalle, et le mécanisme est simple. En contrepartie, une requête et une réponse circulent à chaque fois, même si la valeur n’a pas changé, et les changements plus courts que l’intervalle sont manqués. Avec un abonnement, le serveur échantillonne et n’envoie que les changements, si bien que le trafic augmente peu, même pour des valeurs qui changent rarement. Même avec un abonnement, cependant, les changements plus courts que l’intervalle d’échantillonnage peuvent ne pas être notifiés. La spécification autorise un échantillonnage plus rapide que celui demandé, mais ne l’exige pas11.

Différence entre lectures répétées et abonnementSchéma montrant qu'avec les lectures répétées le client fixe l'intervalle et envoie une requête à chaque fois, qu'avec l'abonnement le serveur échantillonne à l'intervalle d'échantillonnage et n'envoie que les changements, et que les deux méthodes peuvent manquer des changements plus courts que l'intervalle.Suivre les variations des valeursRépéter les lecturesS'abonnerLe client fixe l'intervalle et interroge à chaque foisLe serveur échantillonne et n'envoie que les changementsPeut manquer des changements plus courts que l'intervalle

Figure 7 : Avec l’une ou l’autre méthode, les changements plus courts que l’intervalle peuvent être manqués.

Autoriser les connexions par certificat

Dans une connexion OPC UA, on choisit le niveau de protection des messages avec MessageSecurityMode. Les valeurs sont None, sans protection, Sign, qui signe seulement, et SignAndEncrypt, qui signe et chiffre16. La combinaison d’algorithmes utilisée pour la signature et le chiffrement est fixée par la politique de sécurité ; cet article utilise Basic256Sha256.

La signature et le chiffrement utilisent un certificat propre à chaque application. Le client et le serveur vérifient mutuellement leurs certificats et refusent la connexion d’un correspondant auquel ils ne font pas confiance. Un serveur réalisé avec la bibliothèque .NET de l’OPC Foundation ne propose pas par défaut la politique None, sans chiffrement4.

La première connexion s’est arrêtée sur BadCertificateUntrusted

À la première exécution, le client ne faisait pas confiance au certificat du serveur, et la connexion a été refusée. Voici une partie de la sortie.

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

Cette bibliothèque enregistre les certificats refusés dans le magasin rejected. Quand un administrateur déplace un certificat de là vers le magasin trusted, la bibliothèque fait confiance à ce correspondant17. Ici, j’ai déplacé le certificat du serveur de pki/rejected/certs vers pki/trusted/certs, j’ai relancé l’exécution, et la connexion a réussi. Avant de le déplacer, vérifiez avec l’administrateur du serveur l’empreinte, qui figure aussi dans le nom du fichier4.

Étapes pour faire confiance à un certificatSchéma montrant qu'à la première connexion le client refuse le certificat du serveur et l'enregistre dans le magasin rejected, et qu'après vérification de l'empreinte et déplacement vers le magasin trusted, la connexion suivante lui fait confiance et réussit.Première connexionBadCertificateUntrustedCertificat enregistré dans le magasin rejectedVérifier l'empreinte avec l'administrateur du serveurLe déplacer vers le magasin trustedLa connexion suivante lui fait confiance et réussit

Figure 8 : Après avoir vérifié un certificat refusé et l’avoir déplacé vers trusted, le client fait confiance à ce correspondant.

Il existe aussi un réglage, AutoAcceptUntrustedCertificates, qui accepte automatiquement les certificats non approuvés. Sa valeur par défaut est false, et la documentation de la bibliothèque indique qu’il est utile en environnement de développement mais ne doit pas être utilisé en production17.

Le NotAfter=2027-11-01 du journal est la date d’expiration du certificat du serveur simulé. C’est un certificat auto-signé créé avec une fonction d’asyncua, qui expirait un an après sa date de création. Les certificats des équipements expirent eux aussi : fixez dans l’exploitation la procédure pour les remplacer avant l’expiration et faire approuver les nouveaux par le correspondant. La gestion du magasin de certificats de Windows est traitée dans « Guide pratique du magasin de certificats Windows ».

Authentification de l’utilisateur

Ce que vérifient les certificats, ce sont les applications entre elles. En plus de cela, un jeton qui identifie l’utilisateur est transmis à l’ouverture de la session. La spécification en prévoit quatre types : anonyme, nom d’utilisateur et mot de passe, certificat X.509 et jeton émis à l’extérieur18. Le client de cet article s’est connecté en anonyme. Les jetons qu’un serveur accepte figurent dans les informations de ses points de terminaison (endpoints)19.

Les écueils de la collecte de données sur le terrain

À partir d’ici, j’énumère, en m’appuyant sur cette expérience et sur la spécification, les problèmes qui surviennent souvent dans les systèmes de collecte de données d’équipements.

Les valeurs de la période d’arrêt du serveur n’arrivent pas

Pendant l’abonnement, j’ai arrêté le serveur et l’ai redémarré environ 15 secondes plus tard. Le ManagedSession de cette bibliothèque se reconnecte automatiquement quand la connexion est coupée20. Pendant l’arrêt, le client a tenté de se reconnecter à plusieurs reprises.

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.

Quand le serveur est revenu, les notifications sont revenues aussi. Voici une partie de la sortie de l’abonnement.

  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]

Aucune valeur n’est arrivée entre 13 h 41 min 13 s et 13 h 41 min 34 s. Le nombre de pièces est revenu de 78 à 2, parce que le redémarrage du serveur simulé l’a fait recompter à partir de 0.

Dans cet exemple, c’est le serveur lui-même qui était arrêté : les valeurs de cette période ne sont conservées nulle part. Le mécanisme de reconnexion rétablit la connexion, mais il ne comble pas après coup les valeurs de la période d’arrêt.

La situation est différente quand seule la communication est coupée et que le serveur continue de fonctionner. La spécification prévoit un service Republish, qui fait renvoyer par le serveur, depuis sa file de retransmission, les messages de notification non reçus21. Les notifications s’accumulent aussi dans la file du MonitoredItem, mais tout ce qui dépasse sa taille est rejeté13. Je n’ai pas testé ce cas dans cette expérience. Dans les deux cas, conservez une trace des heures de déconnexion et de reconnexion pour savoir pendant quelle période aucune valeur n’est arrivée. La reconnexion et la conception des journaux pour les équipements en communication série sont aussi traitées dans « Pièges des applications de communication série ».

Ce qu'il faut enregistrer quand le serveur s'arrêteSchéma montrant que si le serveur s'arrête pendant un abonnement, le client tente de se reconnecter à plusieurs reprises et que les notifications reprennent au retour du serveur, mais que les valeurs de la période d'arrêt n'arrivent pas, d'où la nécessité d'enregistrer les heures de déconnexion et de reconnexion.Abonnement en coursLe serveur s'arrêteLe client tente de se reconnecterLe serveur revient et les notifications reprennentLes valeurs de la période d'arrêt n'arrivent pasEnregistrer les heures de déconnexion et de reconnexion

Figure 9 : La reconnexion rétablit la connexion, mais les valeurs de la période d’arrêt du serveur n’arrivent pas.

Quel horodatage enregistrer

Selon la spécification, SourceTimestamp est l’heure que la source des données a attachée à la valeur, et devrait indiquer le moment où la valeur a changé. ServerTimestamp est l’heure à laquelle le serveur a reçu la valeur ou a su qu’elle était correcte9. Le serveur simulé de cet article génère lui-même les valeurs, si bien que les deux heures étaient identiques.

Pour savoir quand une valeur a changé sur l’équipement, utilisez SourceTimestamp. Cette heure est cependant fournie par l’horloge de la source : si l’horloge de l’équipement ou du serveur dérive, l’enregistrement dérive aussi. Si vous comptez rapprocher les données collectées d’autres enregistrements, conservez aussi à côté l’heure du PC qui les a reçues, afin de pouvoir vérifier les écarts plus tard. La synchronisation de l’heure côté Windows est traitée dans « La synchronisation de l’heure sous Windows (w32time) et les systèmes métier ».

Ne pas traiter comme de simples nombres les valeurs dont l’état n’est pas Good

Quand le code d’état est Bad, la valeur doit être ignorée9. Quand il est Uncertain, la valeur peut être inutilisable selon l’usage10. Avant d’intégrer les valeurs à des graphiques ou à des agrégats, triez-les selon leur code d’état et décidez comment traiter celles qui ne sont pas Good. Le bit Overflow d’une file qui déborde est lui aussi positionné dans le code d’état13. La façon d’afficher l’état des équipements à l’écran est traitée dans « Bonnes pratiques pour vérifier et afficher l’état des équipements externes ».

Ouvrir le port dans le pare-feu

Le port d’OPC UA enregistré auprès de l’IANA est 4840/TCP3. Tous les serveurs n’utilisent cependant pas ce numéro. Les exemples .NET de l’OPC Foundation utilisent par exemple le port 625414. Vérifiez le numéro de port dans l’URL du point de terminaison de l’équipement et autorisez les connexions vers ce port. Si le serveur fonctionne sous Windows, autorisez-les dans le Pare-feu Windows de cette machine, et s’il y a des équipements réseau entre les deux, sur ces équipements. La création des règles de trafic entrant est traitée dans « Windows Defender Firewall et les applications métier ».

Les écueils de la collecte de donnéesSchéma présentant les écueils de la collecte de données d'équipements, répartis entre le contenu des enregistrements (les valeurs de la période d'arrêt du serveur n'arrivent pas, quel horodatage enregistrer, traitement des valeurs qui ne sont pas Good) et la préparation de la connexion (ports du pare-feu, expiration des certificats).Écueils de la collecte de donnéesContenu des enregistrementsPréparation de la connexionLes valeurs pendant l'arrêt n'arrivent pasQuel horodatageTraitement des valeurs non GoodPorts du pare-feuExpiration des certificats

Figure 10 : Même une fois la connexion établie, vérifiez les lacunes des enregistrements, les heures, les états, les ports et les certificats.

Choisir entre OPC UA, Modbus/TCP et les protocoles propres aux constructeurs

Pour communiquer avec des automates et des équipements, il existe, outre OPC UA, Modbus/TCP et les protocoles fournis par les fabricants d’automates.

La spécification Modbus gère les données dans quatre tables : les Discrete Inputs, bits uniques en lecture seule ; les Coils, bits uniques en lecture et écriture ; les Input Registers, mots de 16 bits en lecture seule ; et les Holding Registers, mots de 16 bits en lecture et écriture22. L’IANA enregistre 502/TCP comme protocole applicatif Modbus (mbap)3.

Ces tables contiennent des bits et des mots de 16 bits : quelle valeur se trouve à quelle adresse et sous quel format doit être vérifié dans la documentation de l’équipement et décrit côté client. Avec OPC UA, comme nous l’avons vu, le serveur expose des variables nommées dans son espace d’adressage, et le client peut les découvrir avec Browse. Les valeurs sont accompagnées d’un code d’état et d’horodatages, et les connexions sont autorisées par certificat.

Le choix sera souvent dicté par ce que l’équipement propose. Si l’équipement dispose d’un serveur OPC UA, utilisez-le ; s’il ne propose que Modbus/TCP, établissez une table de correspondance des adresses et lisez à partir de celle-ci. S’il n’y a que le protocole ou le SDK du constructeur, suivez sa documentation. Le choix d’une méthode de communication avec les périphériques USB est traité dans « Comment gérer les périphériques USB dans une application Windows ».

Choisir comment communiquer avec un équipementSchéma montrant que si l'équipement dispose d'un serveur OPC UA on parcourt et collecte les variables avec OPC UA, que s'il ne propose que Modbus/TCP on vérifie les adresses et les formats dans la documentation et on lit les registres, et que s'il ne propose que le protocole du constructeur on suit sa documentation.Serveur OPC UAModbus/TCP seulementProtocole du constructeur seulementCommunication proposée par l'équipementQue propose-t-il ?Parcourir et collecter les variablesVérifier adresses et formats dans la documentationSuivre la documentation et le SDK du constructeurLire les registres et les convertir en valeurs

Figure 11 : Le moyen de collecte est souvent dicté par ce que l’équipement propose.

Articles connexes

Références

  1. OPC Foundation, Classic. Le fait qu’OPC Classic repose sur COM/DCOM, les spécifications DA, A&E et HDA, la publication d’OPC UA en 2008 et les raisons de la refonte. ↩ ↩2

  2. IEC, IEC TR 62541-1:2020 OPC Unified Architecture - Part 1: Overview and concepts. Première partie de la série IEC 62541, consacrée aux concepts et à la vue d’ensemble d’OPC UA (remplacée depuis par une édition ultérieure). ↩

  3. IANA, Service Name and Transport Protocol Port Number Registry. Les enregistrements de opcua-tcp sur 4840/TCP et de mbap sur 502/TCP. ↩ ↩2 ↩3

  4. OPC Foundation, UA-.NETStandard 2.0.1: Getting started. La manière d’écrire un client de la branche 2.0, le fait que ReadValueAsync lève une exception sur Bad, les magasins rejected et trusted, le fait que les serveurs ne proposent pas None par défaut, et les numéros de port des exemples. ↩ ↩2 ↩3 ↩4 ↩5

  5. FreeOpcUa, asyncua (PyPI). La bibliothèque OPC UA pour Python utilisée pour le serveur d’équipement simulé. ↩

  6. OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Le service qui suit les références d’un nœud. ↩

  7. OPC Foundation, OPC 10000-3 v1.05, 8.2.2 NamespaceIndex. Le fait que les URI des espaces de noms sont représentés par des nombres dans les services, et que ce nombre correspond à une position dans le NamespaceArray. ↩

  8. OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Les services Read et Write. ↩

  9. OPC Foundation, OPC 10000-4 v1.05, 7.11 DataValue. Les définitions de la valeur, du code d’état, de SourceTimestamp et de ServerTimestamp. ↩ ↩2 ↩3

  10. OPC Foundation, OPC 10000-4 v1.05, 7.38.1 StatusCode General. Good, Uncertain et Bad, indiqués par les deux bits de poids fort. ↩ ↩2

  11. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.2 Sampling interval. Le fait que l’intervalle d’échantillonnage est un cycle « au mieux ». ↩ ↩2

  12. OPC Foundation, OPC 10000-4 v1.05, 7.22.2 DataChangeFilter. Le fait que, sans filtre, une notification est envoyée lors d’un changement de valeur ou d’état. ↩

  13. OPC Foundation, OPC 10000-4 v1.05, 5.13.1.5 Queue parameters. La taille de la file, la règle de rejet et le bit Overflow. ↩ ↩2 ↩3

  14. OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. L’intervalle de publication et les keep-alive. ↩

  15. OPC Foundation, UA-.NETStandard 2.0.1: Subscriptions and Monitored Items Service Set. SubscribeDataChangesAsync, ainsi que l’intervalle d’échantillonnage et la taille de file de MonitoredItemOptions. ↩

  16. OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. La signification de None, Sign et SignAndEncrypt. ↩

  17. OPC Foundation, UA-.NETStandard 2.0.1: Certificates. La pratique consistant à déplacer les certificats du magasin rejected vers le magasin approuvé, et le fait de ne pas utiliser AutoAcceptUntrustedCertificates en production. ↩ ↩2

  18. OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. Les quatre types : anonyme, nom d’utilisateur, X.509 et jeton émis. ↩

  19. OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. La liste des jetons d’utilisateur qu’accepte un point de terminaison (userIdentityTokens). ↩

  20. OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. Le fait que ManagedSession intègre un mécanisme de reconnexion. ↩

  21. OPC Foundation, OPC 10000-4 v1.05, 5.14.6.1 Republish. Le service qui fait renvoyer les messages de notification depuis la file de retransmission d’un abonnement. ↩

  22. Modbus-IDA, MODBUS Application Protocol Specification V1.1b (copie hébergée sur un serveur de l’université de Chicago). Le modèle de données de la section 4.3 (Discrete Input, Coil, Input Register, Holding Register). ↩

Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.

Ce sont des pages de sujets proches de ce thème. À partir de l’article, vous pouvez poursuivre vers les services associés et vers d’autres articles.

Cet article est directement lié aux services suivants. Commencez par une entrée proche de votre situation.

Développement d'applications Windows

La réalisation d'une application Windows qui collecte les données des équipements, y compris le paramétrage des abonnements, la reconnexion et l'enregistrement des horodatages et des états, peut être prise en charge en tant que développement d'applications Windows.

Questions fréquentes

Questions souvent posées lors d’une consultation sur le sujet de cet article.

Quelle est la différence entre OPC UA et OPC Classic (OPC DA) ?
OPC Classic est un ensemble de spécifications qui échangent des données par COM/DCOM de Windows. OPC UA, publié en 2008, ne dépend d'aucun système d'exploitation particulier et réunit les fonctions de Classic. Les clients OPC UA fonctionnent aussi en dehors de Windows et ne demandent aucune configuration DCOM.
Pour collecter des valeurs avec OPC UA, suffit-il de les lire à intervalle fixe ?
Si les valeurs sont peu nombreuses et l'intervalle long, la lecture suffit. Pour suivre les variations des valeurs, on peut aussi utiliser un abonnement : on confie l'échantillonnage au serveur, qui notifie les changements. Dans les deux cas, enregistrez le code d'état et les horodatages renvoyés avec chaque valeur.
Pourquoi obtient-on BadCertificateUntrusted à la connexion ?
Parce que le client ne fait pas encore confiance au certificat du serveur. La bibliothèque .NET de l'OPC Foundation enregistre les certificats refusés dans le magasin rejected. Vérifiez l'empreinte du certificat avec l'administrateur du serveur, puis déplacez-le dans le magasin trusted. Réservez aux environnements de développement le réglage qui accepte automatiquement les certificats.
Quel numéro de port utilise OPC UA ?
L'IANA enregistre le port 4840/TCP sous le nom opcua-tcp. Les serveurs réels utilisent cependant parfois un autre port ; vérifiez-le donc dans les réglages de l'équipement. Si le serveur fonctionne sous Windows, il faut autoriser le trafic vers ce port dans le Pare-feu Windows, et s'il y a des équipements réseau entre les deux, sur ces équipements aussi.

Profil de l’auteur

Page de présentation de l’auteur de l’article.

Go Komura

Représentant de KomuraSoft LLC

Axé sur le développement de logiciels Windows, le conseil technique et l’analyse de dysfonctionnements, avec un point fort pour les projets où subsistent des actifs existants et pour les enquêtes sur des pannes dont la cause est difficile à discerner.

Retour au blog