Modbus transporte des nombres dont le sens reste implicite : à vous de savoir que « le registre 0 vaut la température en dixièmes de degré ». OPC-UA renverse cette logique. Une variable y porte son nom, son type, son unité, sa structure — un véritable modèle d’information que n’importe quel client peut explorer sans documentation externe. C’est ce qui en fait l’épine dorsale de l’usine connectée. Dans ce tutoriel, vous allez exposer une machine en OPC-UA, vous y connecter, lire et écrire ses variables, puis vous abonner à leurs changements en temps réel.
🎯 Ce que vous allez apprendre
- Comprendre ce qu’apporte OPC-UA par rapport à un protocole de registres.
- Monter un serveur OPC-UA qui expose un modèle de machine en Python.
- Vous y connecter avec un client, lire et écrire des variables.
- Vous abonner aux changements de valeur, sans interroger en boucle.
- Comprendre où placer l’authentification et le chiffrement.
🛠️ Ce que vous allez construire
Un serveur OPC-UA qui représente un moteur industriel : il expose une température mesurée (qui évolue), une consigne réglable et un état de marche. Puis un client qui lit la température, modifie la consigne, démarre le moteur, et enfin s’abonne pour être prévenu dès qu’une valeur change. C’est l’ossature d’un système de supervision moderne, où les données ont un sens et où l’on réagit aux événements plutôt que de sonder sans cesse.
Prérequis
- Python 3.10 ou plus récent.
- Des bases de Python, y compris la notion de fonction asynchrone (
async/await). Test express : si le motawaitne vous effraie pas, vous êtes prêt ; sinon, le code reste lisible et commenté. - ⏱️ Temps estimé : 45 minutes.
Étape 1 — Installer asyncua
La bibliothèque de référence pour OPC-UA en Python est asyncua, bâtie sur le moteur asynchrone de Python. On l’installe dans un environnement virtuel.
python3 -m venv ~/opcua_env
source ~/opcua_env/bin/activate
pip install asyncua
L’installation tire quelques dépendances liées à la cryptographie, ce qui est normal : OPC-UA intègre la sécurité jusque dans ses fondations. Une fois l’invite préfixée par (opcua_env), on peut écrire le serveur.
Étape 2 — Construire le serveur et son modèle
Le serveur OPC-UA expose un espace d’adressage : une arborescence d’objets et de variables. On va y créer un objet « Moteur » contenant trois variables. Créez serveur_opcua.py :
import asyncio
from asyncua import Server
async def main():
server = Server()
await server.init()
server.set_endpoint("opc.tcp://0.0.0.0:4840/cellule/")
server.set_server_name("Cellule de production")
# Déclare un espace de noms propre à notre application.
uri = "http://cellule.exemple"
idx = await server.register_namespace(uri)
# Crée l'objet Moteur et ses variables.
moteur = await server.nodes.objects.add_object(idx, "Moteur")
temperature = await moteur.add_variable(idx, "Temperature", 23.5)
consigne = await moteur.add_variable(idx, "Consigne", 25.0)
en_marche = await moteur.add_variable(idx, "EnMarche", False)
# Par défaut une variable est en lecture seule : on autorise l'écriture
# de la consigne et de l'état moteur par les clients.
await consigne.set_writable()
await en_marche.set_writable()
print("Serveur OPC-UA en ecoute sur opc.tcp://0.0.0.0:4840/cellule/")
async with server:
while True:
await asyncio.sleep(1)
# Simule une température qui dérive lentement.
valeur = await temperature.read_value()
await temperature.write_value(round(valeur + 0.1, 1))
if __name__ == "__main__":
asyncio.run(main())
Décortiquons. On instancie un Server, on l’initialise, on fixe son point de terminaison (l’adresse opc.tcp:// sur le port standard 4840) et son nom. On enregistre ensuite un espace de noms propre à notre application : c’est lui qui distingue nos variables de celles du serveur. On crée l’objet Moteur sous le dossier des objets, puis trois variables avec une valeur initiale qui fixe leur type (un flottant pour la température, un booléen pour l’état). Comme toute variable est en lecture seule par défaut, on rend explicitement la consigne et l’état modifiables par set_writable. Enfin, async with server démarre le serveur, et la boucle fait évoluer la température. Lancez python serveur_opcua.py : il annonce son écoute et reste actif.
✅ Point d’étape — Le serveur tourne. Pour l’explorer visuellement, des outils gratuits comme un client OPC-UA graphique permettent de naviguer dans l’arborescence et de voir la température grimper. Mais nous allons le faire en code, ce qui est l’objet de la suite.
Étape 3 — Se connecter et lire avec un client
Dans un second terminal (environnement virtuel activé), on écrit le client. Première tâche : se connecter et lire la température. Le client doit retrouver la variable par son chemin dans l’arborescence. Créez client_opcua.py :
import asyncio
from asyncua import Client
async def main():
url = "opc.tcp://127.0.0.1:4840/cellule/"
async with Client(url=url) as client:
# Retrouve l'index de notre espace de noms.
idx = await client.get_namespace_index("http://cellule.exemple")
# Navigue jusqu'à la variable Temperature sous l'objet Moteur.
temperature = await client.nodes.objects.get_child(
[f"{idx}:Moteur", f"{idx}:Temperature"])
valeur = await temperature.read_value()
print("Temperature lue :", valeur, "C")
if __name__ == "__main__":
asyncio.run(main())
Le client se connecte via le gestionnaire de contexte async with, qui ouvre et ferme proprement la session. On récupère l’index de notre espace de noms — indispensable car il peut différer d’un serveur à l’autre. Puis get_child descend l’arborescence en suivant un chemin nommé : l’objet Moteur, puis sa variable Temperature. La syntaxe "index:Nom" est la façon standard de désigner un nœud. read_value renvoie la valeur courante. Lancez le client : il affiche la température lue, légèrement supérieure à 23,5 puisqu’elle dérive.
Étape 4 — Écrire une consigne et commander le moteur
Un superviseur agit aussi sur la machine. On modifie la consigne et on démarre le moteur. Complétez le corps du async with, après la lecture :
# Modifier la consigne (variable accessible en écriture).
consigne = await client.nodes.objects.get_child(
[f"{idx}:Moteur", f"{idx}:Consigne"])
await consigne.write_value(26.0)
print("Consigne ecrite : 26.0 C")
# Démarrer le moteur.
en_marche = await client.nodes.objects.get_child(
[f"{idx}:Moteur", f"{idx}:EnMarche"])
await en_marche.write_value(True)
print("Moteur demarre :", await en_marche.read_value())
On retrouve les variables Consigne et EnMarche de la même manière, puis on appelle write_value. Comme on les a rendues modifiables côté serveur, l’écriture réussit ; si on avait oublié le set_writable, le serveur refuserait avec une erreur explicite. La relecture confirme que le moteur est en marche. Vous commandez désormais une machine via un modèle de données nommé, sans jamais manipuler d’adresse numérique brute.
Étape 5 — S’abonner aux changements
Sonder une valeur en boucle gaspille le réseau et la latence. OPC-UA propose mieux : l’abonnement. Le serveur prévient le client uniquement quand une valeur change. On définit pour cela un gestionnaire qui réagit aux notifications. Ajoutez ce gestionnaire en haut du fichier, et l’abonnement dans le async with :
class Gestionnaire:
def datachange_notification(self, node, valeur, data):
print("Changement detecte ->", valeur)
# ... à l'intérieur du async with, après les écritures :
handler = Gestionnaire()
abonnement = await client.create_subscription(500, handler)
await abonnement.subscribe_data_change(temperature)
print("Abonnement actif, attente des changements...")
await asyncio.sleep(5) # laisse le temps de recevoir des notifications
La méthode datachange_notification du gestionnaire est appelée par asyncua à chaque changement. On crée un abonnement avec un intervalle de publication de 500 millisecondes, puis on s’abonne à la variable temperature. Pendant les cinq secondes d’attente, comme le serveur incrémente la température chaque seconde, le client reçoit et affiche plusieurs notifications sans avoir rien demandé activement. C’est le modèle événementiel qui rend les supervisions OPC-UA efficaces, même avec des milliers de variables.
✅ Point d’étape final — Si des lignes « Changement detecte » défilent toutes les secondes, l’abonnement fonctionne. Vous savez maintenant lire, écrire et réagir aux événements d’un serveur OPC-UA.
Étape 6 — Où placer la sécurité
OPC-UA n’est pas qu’un protocole de données : c’est aussi un cadre de sécurité, et c’est ce qui le distingue de Modbus. Trois niveaux se combinent. L’authentification identifie le client (par nom d’utilisateur et mot de passe, ou par certificat). La signature garantit qu’un message n’a pas été altéré. Le chiffrement rend les échanges illisibles pour un tiers. Ces garanties reposent sur des certificats X.509, négociés à la connexion via une « politique de sécurité ».
En développement local, on travaille souvent sans sécurité pour aller vite, comme dans ce tutoriel. Mais dès qu’un serveur OPC-UA sort d’un réseau de confiance — typiquement pour remonter des données vers une supervision distante — la sécurité devient obligatoire. Côté client, cela se règle en chargeant un certificat et en exigeant une politique chiffrée avant la connexion ; côté serveur, en n’acceptant que les certificats approuvés. La règle à retenir : Modbus reste cantonné au réseau de terrain protégé, et c’est OPC-UA, correctement sécurisé, qui franchit les frontières vers l’extérieur.
Naviguer dans l’espace d’adressage
La richesse d’OPC-UA tient à son espace d’adressage : une arborescence où tout est un « nœud », relié aux autres par des références typées. C’est ce qui le distingue radicalement d’une simple liste de registres. Comprendre cette structure éclaire la façon dont on retrouve une donnée et permet d’explorer n’importe quel serveur, même inconnu.
Chaque nœud possède un identifiant unique, le NodeId, composé de l’index d’espace de noms et d’un identifiant propre. Quand notre client appelait get_child avec un chemin nommé, asyncua résolvait en réalité ces NodeId pour nous. On distingue plusieurs catégories de nœuds : les objets (comme notre « Moteur »), qui regroupent ; les variables (« Temperature »), qui portent une valeur ; les types, qui définissent la structure ; et les méthodes, qui sont des fonctions appelables à distance. Cette typologie permet de modéliser une machine aussi fidèlement qu’on le souhaite : un moteur contenant des capteurs, eux-mêmes contenant des mesures avec leurs unités.
En pratique, deux façons d’atteindre une donnée coexistent. Le parcours par chemin, que nous avons utilisé, est lisible et robuste aux changements d’identifiants : on suit les noms. L’accès direct par NodeId est plus rapide et stable si l’on connaît l’identifiant à l’avance. Pour découvrir ce que contient un serveur, on « navigue » : on part du nœud racine et on liste récursivement les enfants. asyncua propose pour cela des méthodes comme get_children, et les clients graphiques rendent cette exploration visuelle. Cette capacité d’auto-description — un serveur qui explique lui-même sa structure — est la raison profonde pour laquelle OPC-UA s’est imposé comme langage commun de l’industrie : un nouveau client peut comprendre une machine sans documentation préalable, simplement en parcourant son modèle.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
| Connexion refusée | Serveur non démarré ou URL erronée | Vérifier que le serveur tourne et que l’URL opc.tcp:// est identique |
BadNoMatch sur get_child |
Chemin ou index d’espace de noms incorrect | Récupérer l’index avec get_namespace_index et respecter la syntaxe index:Nom |
| Écriture refusée | Variable non rendue accessible en écriture | Appeler set_writable côté serveur |
| Type incompatible à l’écriture | On écrit un entier dans une variable flottante | Respecter le type initial (26.0 et non 26) |
| Aucune notification reçue | Abonnement non maintenu (programme terminé trop vite) | Laisser le programme vivre avec un sleep ou une boucle |
✅ Récapitulatif
Vous avez monté un serveur OPC-UA exposant un modèle de moteur, vous y êtes connecté en client, vous avez lu une mesure, écrit une consigne, commandé une sortie, puis souscrit à des changements en temps réel. Vous avez enfin situé les trois niveaux de sécurité d’OPC-UA et compris quand les activer. Là où Modbus échangeait des nombres anonymes, OPC-UA manipule un modèle nommé et sécurisable : c’est le langage qui relie le terrain à la supervision, et que la passerelle finale de la série fera dialoguer avec ROS 2.
🧾 Aide-mémoire
| Appel | Rôle |
|---|---|
Server() / await server.init() |
Créer et initialiser le serveur |
register_namespace(uri) |
Déclarer son espace de noms |
add_object / add_variable |
Construire le modèle d’information |
set_writable() |
Autoriser l’écriture d’une variable |
read_value() / write_value() |
Lire / écrire une variable côté client |
create_subscription + subscribe_data_change |
Réagir aux changements sans sonder |
💪 À vous de jouer
Ajoutez au moteur une variable « VitesseRotation » accessible en écriture, et faites varier la température simulée en fonction de cette vitesse côté serveur (plus la vitesse est haute, plus la température monte). Côté client, abonnez-vous à la fois à la température et à la vitesse pour observer la corrélation. Vous obtenez un mini-jumeau numérique réactif.
Voir la piste
Sur le serveur, créez vitesse = await moteur.add_variable(idx, "VitesseRotation", 0.0) puis await vitesse.set_writable(). Dans la boucle, lisez la vitesse et ajoutez à la température un incrément proportionnel : increment = await vitesse.read_value() * 0.001. Côté client, appelez subscribe_data_change sur les deux nœuds.
Tutoriels frères
- Lire et écrire des registres en Modbus TCP avec Python — le protocole de terrain, plus simple, à voir avant celui-ci.
- La passerelle ROS 2 ↔ automate — relier ce modèle au robot.
Pour aller plus loin
- 🔝 Retour au guide principal : Robotique industrielle open source
- Documentation de la bibliothèque : opcua-asyncio (FreeOpcUa). Spécification : OPC Foundation et norme IEC 62541.
FAQ
OPC-UA remplace-t-il Modbus ?
Pas vraiment : ils cohabitent. Modbus reste imbattable de simplicité au plus près des capteurs et automates. OPC-UA apporte le modèle de données et la sécurité aux niveaux supérieurs. Une architecture saine parle Modbus en bas et OPC-UA aux frontières.
Pourquoi du code asynchrone ?
Parce qu’un serveur OPC-UA gère simultanément de nombreux clients et abonnements. Le modèle asynchrone de Python permet de tout traiter sans bloquer, avec un code plus lisible que des fils d’exécution multiples. asyncua propose aussi une interface synchrone si vous préférez.
Comment explorer un serveur sans écrire de client ?
Des clients graphiques gratuits permettent de se connecter à un serveur OPC-UA et de naviguer dans son arborescence à la souris. C’est très pratique pour découvrir le modèle d’un équipement avant de coder.
Le port 4840 est-il obligatoire ?
C’est le port standard d’OPC-UA, mais on peut en choisir un autre dans l’endpoint. L’essentiel est que client et serveur s’accordent sur la même adresse complète.