Premiers pas avec OPC UA ── Collecter les données des automates et des équipements dans une application Windows
· Go Komura · Windows, OPC UA, Industrie manufacturière, Automate programmable, C#, .NET, Communication, Certificats
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.
flowchart TB
accTitle: Différence entre OPC Classic et OPC UA
accDescr: Sché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.
C["OPC Classic"] --> D["Fonctionne sur COM/DCOM (Windows)"]
D --> DA["Spécifications séparées DA, A&E, HDA"]
DA -->|"Réunit les fonctions"| U["OPC UA (2008)"]
U --> P["Indépendant de la plateforme"]
P --> S["Normalisé 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.
flowchart TB
accTitle: Configuration testée
accDescr: Sché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.
A["Client C# (LineCollector)"] -->|"opc.tcp, SignAndEncrypt"| B["Serveur d'équipement simulé (asyncua)"]
A -.-> L["Bibliothèque .NET de l'OPC Foundation 2.0.1"]
B -.-> V["Quatre 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.
flowchart TB
accTitle: Espace d'adressage de l'équipement simulé
accDescr: Schéma montrant un objet Line1 sous le dossier Objects, avec sous lui quatre variables, Temperature, PartCount, Running et Setpoint.
O["Objects"] --> L["Line1 (objet)"]
L --> T["Temperature (variable)"]
L --> P["PartCount (variable)"]
L --> R["Running (variable)"]
L --> S["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.
flowchart TB
accTitle: Construire un NodeId à partir de l'URI d'un espace de noms
accDescr: Sché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.
U["URI de l'espace de noms"] --> A["NamespaceArray du serveur"]
A --> I["Index (2 dans cet exemple)"]
I --> N["NodeId ns=2;s=Line1.Temperature"]
K["Identifiant Line1.Temperature"] --> N
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.
flowchart TB
accTitle: Contenu d'une DataValue
accDescr: Sché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.
D["DataValue"] --> V["Valeur"]
D --> S["Code d'état"]
D --> TS["SourceTimestamp (heure à la source)"]
D --> TV["ServerTimestamp (heure du serveur)"]
S -->|"Si Bad"| X["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.
flowchart TB
accTitle: Fonctionnement des abonnements et des MonitoredItem
accDescr: Sché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.
S["Source (valeur de l'équipement)"] --> M["MonitoredItem"]
M -->|"Intervalle d'échantillonnage"| F["Filtre (par défaut : changement de valeur ou d'état)"]
F --> Q["File (Overflow si elle déborde)"]
Q --> SUB["Abonnement"]
SUB -->|"Intervalle de publication"| C["Client"]
SUB -.->|"Sans notification"| K["Keep-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.
flowchart TB
accTitle: Différence entre lectures répétées et abonnement
accDescr: Sché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.
A["Suivre les variations des valeurs"] --> R["Répéter les lectures"]
A --> S["S'abonner"]
R --> R1["Le client fixe l'intervalle et interroge à chaque fois"]
S --> S1["Le serveur échantillonne et n'envoie que les changements"]
R1 --> X["Peut manquer des changements plus courts que l'intervalle"]
S1 --> X
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.
flowchart TB
accTitle: Étapes pour faire confiance à un certificat
accDescr: Sché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.
A["Première connexion"] --> B["BadCertificateUntrusted"]
B --> C["Certificat enregistré dans le magasin rejected"]
C --> D["Vérifier l'empreinte avec l'administrateur du serveur"]
D --> E["Le déplacer vers le magasin trusted"]
E --> F["La 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 ».
flowchart TB
accTitle: Ce qu'il faut enregistrer quand le serveur s'arrête
accDescr: Sché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.
A["Abonnement en cours"] --> B["Le serveur s'arrête"]
B --> C["Le client tente de se reconnecter"]
C --> D["Le serveur revient et les notifications reprennent"]
B -.-> E["Les valeurs de la période d'arrêt n'arrivent pas"]
E -.-> F["Enregistrer 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 ».
flowchart TB
accTitle: Les écueils de la collecte de données
accDescr: Sché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).
A["Écueils de la collecte de données"] --> R["Contenu des enregistrements"]
A --> N["Préparation de la connexion"]
R --> B["Les valeurs pendant l'arrêt n'arrivent pas"]
B ~~~ C["Quel horodatage"]
C ~~~ D["Traitement des valeurs non Good"]
N --> E["Ports du pare-feu"]
E ~~~ F["Expiration 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 ».
flowchart TB
accTitle: Choisir comment communiquer avec un équipement
accDescr: Sché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.
A["Communication proposée par l'équipement"] --> B{"Que propose-t-il ?"}
B -->|"Serveur OPC UA"| C["Parcourir et collecter les variables"]
B -->|"Modbus/TCP seulement"| D["Vérifier adresses et formats dans la documentation"]
B -->|"Protocole du constructeur seulement"| E["Suivre la documentation et le SDK du constructeur"]
D --> F["Lire les registres et les convertir en valeurs"]
Figure 11 : Le moyen de collecte est souvent dicté par ce que l’équipement propose.
Articles connexes
- Dropped Frames with GigE Vision and USB3 Vision Cameras — Locating the Cause and the Windows Settings to Review
- Pièges des applications de communication série — de la reconnexion à la conception des journaux
- Bonnes pratiques pour vérifier et afficher l’état des équipements externes
- Comment gérer les périphériques USB dans une application Windows
- Windows Defender Firewall et les applications métier
- Guide pratique du magasin de certificats Windows
- La synchronisation de l’heure sous Windows (w32time) et les systèmes métier
Références
-
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
-
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). ↩
-
IANA, Service Name and Transport Protocol Port Number Registry. Les enregistrements de
opcua-tcpsur 4840/TCP et dembapsur 502/TCP. ↩ ↩2 ↩3 -
OPC Foundation, UA-.NETStandard 2.0.1: Getting started. La manière d’écrire un client de la branche 2.0, le fait que
ReadValueAsynclève une exception sur Bad, les magasins rejected et trusted, le fait que les serveurs ne proposent pasNonepar défaut, et les numéros de port des exemples. ↩ ↩2 ↩3 ↩4 ↩5 -
FreeOpcUa, asyncua (PyPI). La bibliothèque OPC UA pour Python utilisée pour le serveur d’équipement simulé. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.9.2 Browse. Le service qui suit les références d’un nœud. ↩
-
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. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 5.11 Attribute Service Set. Les services Read et Write. ↩
-
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
-
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
-
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
-
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. ↩
-
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
-
OPC Foundation, OPC 10000-4 v1.05, 5.14.1.1 Subscription model. L’intervalle de publication et les keep-alive. ↩
-
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 deMonitoredItemOptions. ↩ -
OPC Foundation, OPC 10000-4 v1.05, 7.20 MessageSecurityMode. La signification de None, Sign et SignAndEncrypt. ↩
-
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
AutoAcceptUntrustedCertificatesen production. ↩ ↩2 -
OPC Foundation, OPC 10000-4 v1.05, 7.40 UserIdentityToken parameters. Les quatre types : anonyme, nom d’utilisateur, X.509 et jeton émis. ↩
-
OPC Foundation, OPC 10000-4 v1.05, 7.14 EndpointDescription. La liste des jetons d’utilisateur qu’accepte un point de terminaison (userIdentityTokens). ↩
-
OPC Foundation, UA-.NETStandard 2.0.1: Sessions, Reconnection, and Subscription Engines. Le fait que
ManagedSessionintègre un mécanisme de reconnexion. ↩ -
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. ↩
-
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 associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Pourquoi les arguments se cassent — Les règles des arguments de ligne de commande Windows
Windows passe à CreateProcess une seule chaîne que le destinataire découpe. Traite les règles de CommandLineToArgvW, du CRT et de .NET, A...
Fin de la maintenance des pilotes d'imprimante Windows — Comment les applications métier doivent préparer l'impression des rapports et des étiquettes
Microsoft met progressivement fin aux pilotes d'imprimante v3/v4. Ce que Windows protected print mode retire, et comment inventorier et p...
Bonnes pratiques du multithreading : édition .NET — ce qu'il faut décider avant d'ajouter des threads
Empêcher les threads .NET/C# de planter ou de figer. S'appuyer sur Task, réduire l'état mutable partagé, verrouiller avec discipline, arr...
Utiliser WMI/CIM depuis C# et PowerShell — guide pratique d'inventaire matériel, de surveillance des processus et d'interrogations distantes
WMI/CIM est le moyen standard de lire le numéro de série d'un PC, de surveiller l'espace disque et de détecter le démarrage des processus...
Jusqu'à quand peut-on utiliser MSMQ ? — La décision de migration d'une file d'attente héritée « qui n'est même pas dépréciée »
MSMQ ne figure sur aucune liste officielle de dépréciation, mais System.Messaging n'existe que dans .NET Framework, ce qui bloque la migr...
Sujets associés
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.
Thèmes techniques Windows
Portail réunissant les thèmes techniques sur le développement Windows, l'analyse des défauts et la valorisation des actifs existants.
Services liés à ce sujet
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.
Conseil technique et revue de conception
Si vous voulez décider s'il faut collecter avec OPC UA ou utiliser Modbus/TCP ou le protocole du constructeur, et comment gérer les certificats, nous pouvons le traiter comme une consultation technique et une revue de conception.
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.