OPC UA PubSub sur MQTT associe le modèle d'information d'OPC UA à la distribution par broker de MQTT : un appareil OPC UA peut publier ses données une seule fois et n'importe quel nombre d'abonnés peut les recevoir via un broker standard. Dans cet article, vous verrez comment la partie 14 de la spécification OPC UA structure PubSub, la différence entre les encodages JSON et UADP, comment les topics et les métadonnées sont organisés et comment ces messages transitent par un broker MQTT ordinaire. Vous publierez et décoderez aussi vous-même un NetworkMessage JSON.
Pourquoi OPC UA a ajouté PubSub
L'OPC UA classique est client/serveur : un client ouvre une session, crée des abonnements et reçoit des notifications via cette session. C'est puissant, mais cela passe mal à l'échelle quand de nombreux consommateurs veulent les mêmes données, et cela exige que les clients joignent directement les serveurs, ce qui est malcommode à travers les pare-feu et vers le cloud. Notre comparatif OPC UA vs MQTT détaille cette tension.
La partie 14 de la spécification, introduite avec OPC UA 1.04 et étendue en 1.05, ajoute un modèle publish/subscribe. Les éditeurs envoient des messages sans savoir qui les reçoit ; les abonnés filtrent ce dont ils ont besoin. La partie 14 définit deux familles de transport :
- Sans broker, comme le multicast UDP ou Ethernet (TSN compris), pour une communication déterministe à faible latence entre contrôleurs d'un même réseau.
- Avec broker, via MQTT ou AMQP, pour distribuer les données à de nombreux consommateurs, à travers les réseaux et jusqu'au cloud.
Cet article porte sur la seconde famille, avec MQTT comme transport.
Les briques de PubSub
La partie 14 introduit un vocabulaire qu'il vaut la peine de connaître avant d'examiner les messages.
| Concept | Définition |
|---|---|
| PublishedDataSet | Un ensemble nommé de variables ou d'événements à publier, par exemple toutes les valeurs de broche d'une CNC |
| DataSetWriter | Transforme un PublishedDataSet en DataSetMessages, avec un DataSetWriterId numérique |
| WriterGroup | Regroupe des DataSetWriters partageant des paramètres comme l'intervalle de publication et l'encodage ; produit des NetworkMessages |
| PublisherId | Identifie l'application qui publie, sous forme de nombre ou de chaîne |
| NetworkMessage | L'unité qui transite sur le transport : un en-tête plus un ou plusieurs DataSetMessages |
| DataSetMessage | Les valeurs d'un dataset à un instant donné, sous forme de key frame complète ou de delta frame |
| DataSetMetaData | Décrit les champs d'un dataset : noms, types de données, versions |
| DataSetReader | Côté abonné, filtre et décode les messages d'un dataset |
L'idée importante est que le payload n'est pas un JSON arbitraire inventé par chaque fabricant. C'est une structure de message définie, avec numéros de séquence, versions de métadonnées et moyen de décrire les champs, de sorte qu'un abonné générique peut interpréter les données de n'importe quel éditeur conforme.
Encodage JSON vs UADP
La partie 14 définit deux encodages de messages, et les deux peuvent être transportés sur MQTT.
| Aspect | Encodage JSON | Encodage UADP (binaire) |
|---|---|---|
| Format | JSON lisible par un humain | Binaire compact, types binaires OPC UA |
| Taille | Plus volumineux | Beaucoup plus petit |
| Lisibilité | Lisible dans n'importe quel client MQTT ou journal | Nécessite un décodeur OPC UA PubSub |
| Consommateurs typiques | Services cloud, outils IT, tableaux de bord, scripts | Piles OPC UA, contrôleurs, liaisons à bande passante limitée |
| Noms de champs | Peuvent être inclus dans le message | Nécessitent généralement les métadonnées pour être interprétés |
| Idéal pour | Intégration et interopérabilité avec l'IT | Efficacité et déterminisme |
Pour l'intégration cloud, JSON est le choix courant, car tous les langages savent le lire et vous pouvez inspecter directement les messages dans un explorateur de topics. UADP se justifie quand la bande passante compte ou quand les deux extrémités sont des piles OPC UA.
Anatomie d'un NetworkMessage JSON
Un message de données encodé en JSON ressemble à ceci (ensemble de champs simplifié pour la lisibilité) :
{
"MessageId": "8f1c2e7a-0b44-4c55-9a1e-3f0d9d2b6c11",
"MessageType": "ua-data",
"PublisherId": "cnc-cell-07",
"Messages": [
{
"DataSetWriterId": 101,
"SequenceNumber": 4521,
"MetaDataVersion": { "MajorVersion": 1, "MinorVersion": 3 },
"Timestamp": "2026-10-16T09:30:12.512Z",
"MessageType": "ua-keyframe",
"Payload": {
"SpindleTemp": 47.3,
"SpindleSpeed": 12000,
"PartCount": 1843
}
}
]
}
Les champs clés à comprendre :
- MessageType au niveau supérieur distingue les données (
ua-data) des métadonnées (ua-metadata) et, dans les versions récentes, des messages de statut et d'autres messages de découverte. - PublisherId et DataSetWriterId identifient ensemble précisément de quel dataset il s'agit.
- SequenceNumber permet aux abonnés de détecter les trous et les doublons.
- MetaDataVersion indique à l'abonné quelle version de la description des champs s'applique. Si elle change, l'abonné doit récupérer les nouvelles métadonnées avant de se fier aux positions ou aux types des champs.
- Key frames et delta frames : une key frame transporte tous les champs ; une delta frame ne transporte que les champs modifiés, ce qui réduit le trafic pour les données qui évoluent lentement.
Les valeurs du payload peuvent être encodées sous une forme plus compacte ou plus verbeuse (la spécification distingue encodages réversibles et non réversibles, et la version 1.05 affine ce point). Une forme compacte donne des valeurs simples comme ci-dessus ; une forme verbeuse enveloppe chaque valeur avec son type, son statut et ses horodatages afin de pouvoir la redécoder en un Variant OPC UA exact.
Topics et métadonnées sur le broker
Dans OPC UA 1.04, le topic MQTT (appelé queue name) est simplement configuré sur chaque WriterGroup ou DataSetWriter. OPC UA 1.05 ajoute une arborescence de topics par défaut recommandée, pour que les abonnés puissent découvrir les éditeurs de manière prévisible. Elle a cette forme :
<prefix>/<encoding>/<message type>/<PublisherId>/<WriterGroup>/<DataSetWriter>
opcua/json/data/cnc-cell-07/fast/spindle
opcua/json/metadata/cnc-cell-07/fast/spindle
opcua/json/status/cnc-cell-07
Le préfixe vaut opcua par défaut mais il est configurable, ce qui compte lorsque vous partagez un broker avec d'autres systèmes. Trois conventions méritent d'être suivies :
- Les messages de métadonnées sont publiés en retained, pour qu'un abonné qui se connecte plus tard reçoive immédiatement la description des champs.
- Les messages de statut sont retenus et associés à un Last Will, pour que les abonnés sachent si un éditeur est en ligne.
- La QoS est configurable par writer group, et correspond aux QoS MQTT 0, 1 ou 2 (au mieux, au moins une fois, exactement une fois).
Rien de tout cela n'exige de prise en charge particulière par le broker. Retained messages, Last Will, wildcards et QoS sont des fonctionnalités MQTT standard, et c'est précisément pourquoi la partie 14 a choisi MQTT comme transport. Pour un rappel sur les garanties de livraison, consultez les niveaux de QoS MQTT expliqués.
Comment les messages transitent par un broker standard
Du point de vue du broker, un éditeur OPC UA PubSub n'est qu'un client MQTT de plus. Il se connecte, s'authentifie éventuellement, définit un Last Will et publie des payloads sur des topics. Le broker n'analyse pas la structure OPC UA ; il achemine des octets selon les topics et les abonnements. Cela signifie que n'importe quel broker MQTT conforme aux standards peut transporter du trafic OPC UA PubSub, y compris un broker MQTT managé comme ConnectMQ.
Un abonné procède généralement ainsi :
- Il s'abonne au topic de métadonnées, reçoit les métadonnées retenues et construit une table des champs.
- Il s'abonne au topic de données ou à un wildcard comme
opcua/json/data/#. - Il compare
MetaDataVersionà ses métadonnées en cache et vérifie les trous dansSequenceNumber. - Il applique les delta frames à sa dernière key frame connue pour maintenir l'état actuel.
En pratique : publier et décoder un NetworkMessage JSON
L'exemple utilise le serveur MQTT public gratuit broker.connectmq.com, réservé aux tests. Ce serveur public limite les topics à huit niveaux ; l'exemple remplace donc le préfixe opcua par un préfixe unique qui lui est propre. Installez d'abord paho-mqtt 2.x avec pip install "paho-mqtt>=2.0".
L'éditeur envoie un message de métadonnées retenu, puis une key frame toutes les deux secondes :
# pubsub_publisher.py - émet des messages JSON de style OPC UA partie 14 sur MQTT
import json, random, time, uuid
from datetime import datetime, timezone
import paho.mqtt.client as mqtt
PREFIX = "connectmq/demo/c3d9/json" # remplace le préfixe par défaut "opcua/json"
PUBLISHER_ID = "cnc-cell-07"
DATA_TOPIC = f"{PREFIX}/data/{PUBLISHER_ID}/fast/spindle"
META_TOPIC = f"{PREFIX}/metadata/{PUBLISHER_ID}/fast/spindle"
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"uapub-{uuid.uuid4().hex[:8]}")
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_start()
metadata = {
"MessageId": str(uuid.uuid4()), "MessageType": "ua-metadata",
"PublisherId": PUBLISHER_ID, "DataSetWriterId": 101,
"MetaData": {
"Name": "Spindle",
"Fields": [
{"Name": "SpindleTemp", "BuiltInType": 11}, # Double
{"Name": "SpindleSpeed", "BuiltInType": 11},
{"Name": "PartCount", "BuiltInType": 7} # UInt32
],
"ConfigurationVersion": {"MajorVersion": 1, "MinorVersion": 3}
}
}
client.publish(META_TOPIC, json.dumps(metadata), qos=1, retain=True)
seq, parts = 0, 1840
while True:
seq += 1
parts += random.choice([0, 0, 1])
msg = {
"MessageId": str(uuid.uuid4()), "MessageType": "ua-data", "PublisherId": PUBLISHER_ID,
"Messages": [{
"DataSetWriterId": 101, "SequenceNumber": seq,
"MetaDataVersion": {"MajorVersion": 1, "MinorVersion": 3},
"Timestamp": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
"MessageType": "ua-keyframe",
"Payload": {"SpindleTemp": round(random.uniform(44, 50), 2),
"SpindleSpeed": random.choice([0.0, 8000.0, 12000.0]),
"PartCount": parts}
}]
}
client.publish(DATA_TOPIC, json.dumps(msg), qos=1)
time.sleep(2)
L'abonné met les métadonnées en cache, vérifie les versions et détecte les trous de séquence :
# pubsub_subscriber.py - décode les NetworkMessages JSON OPC UA reçus via MQTT
import json, uuid
import paho.mqtt.client as mqtt
PREFIX = "connectmq/demo/c3d9/json"
meta_versions, last_seq = {}, {}
def on_connect(client, userdata, flags, reason_code, properties):
client.subscribe(f"{PREFIX}/metadata/#", qos=1)
client.subscribe(f"{PREFIX}/data/#", qos=1)
def on_message(client, userdata, message):
nm = json.loads(message.payload)
if nm["MessageType"] == "ua-metadata":
key = (nm["PublisherId"], nm["DataSetWriterId"])
meta_versions[key] = nm["MetaData"]["ConfigurationVersion"]
print("metadata", key, [f["Name"] for f in nm["MetaData"]["Fields"]])
return
for dsm in nm.get("Messages", []):
key = (nm["PublisherId"], dsm["DataSetWriterId"])
if meta_versions.get(key) != dsm["MetaDataVersion"]:
print("warning: metadata missing or outdated for", key)
prev = last_seq.get(key)
if prev is not None and dsm["SequenceNumber"] != prev + 1:
print(f"gap detected: {prev} -> {dsm['SequenceNumber']}")
last_seq[key] = dsm["SequenceNumber"]
print(dsm["Timestamp"], dsm["Payload"])
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"uasub-{uuid.uuid4().hex[:8]}")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_forever()
Lancez l'éditeur, puis démarrez l'abonné quand vous le souhaitez. Comme les métadonnées sont retenues, l'abonné affiche d'abord la liste des champs, puis les données. Remplacez c3d9 par votre propre chaîne aléatoire.
Considérations pratiques
- Cohabitation avec un Unified Namespace. Les topics OPC UA PubSub sont organisés par éditeur et par writer group, et non selon la hiérarchie de l'usine. Beaucoup d'équipes conservent l'arborescence PubSub telle quelle et exécutent un petit service qui republie les champs sélectionnés dans un namespace de style ISA-95 ; consultez notre guide sur les topics MQTT ISA-95.
- Taille des messages. Les key frames comportant de nombreux champs peuvent devenir volumineuses. Vérifiez la limite de taille de message du broker pour votre offre et privilégiez les delta frames ou plusieurs writer groups pour les datasets étendus.
- Sécurité. Sur MQTT, vous sécurisez le transport avec TLS et les identifiants du broker. La partie 14 définit aussi la signature et le chiffrement au niveau des messages avec un service de clés, surtout utilisés avec UADP.
- Outillage. Des piles open source comme open62541 incluent PubSub avec prise en charge de MQTT, et plusieurs automates et passerelles commerciaux peuvent publier directement du JSON partie 14.
Pour situer ce sujet dans une architecture d'usine plus large, lisez notre guide de l'IoT industriel avec MQTT.
Démarrer avec ConnectMQ
Pointez votre éditeur OPC UA PubSub vers un broker que vous n'avez pas à exploiter. Créez-en un gratuitement sur app.connectmq.com avec l'offre Serverless, sans carte bancaire, utilisez le nom d'utilisateur, le mot de passe et le client ID générés avec TLS sur le port 8883, et inspectez vos NetworkMessages dans l'explorateur de topics en temps réel du portail. ConnectMQ prend en charge MQTT 3.1.1 et 5.0, les retained messages et le Last Will, soit tout ce sur quoi repose la partie 14.
Questions fréquentes
Le broker MQTT doit-il comprendre OPC UA pour que PubSub fonctionne ?
Non. Le broker achemine les messages selon les topics et traite les payloads comme des octets. Tout broker MQTT conforme aux standards peut transporter du trafic OPC UA PubSub.
Faut-il utiliser l'encodage JSON ou UADP pour OPC UA PubSub sur MQTT ?
Utilisez JSON lorsque des systèmes IT ou cloud consomment les données, car il est lisible et facile à analyser. Utilisez UADP lorsque la bande passante est limitée ou que les deux extrémités sont des piles OPC UA.
Quelle est la différence entre une key frame et une delta frame ?
Une key frame contient tous les champs du dataset ; une delta frame ne contient que les champs modifiés. Les abonnés appliquent les delta frames à la dernière key frame pour conserver l'état complet.
Pourquoi les messages de métadonnées doivent-ils être retenus ?
Pour qu'un abonné qui se connecte après le démarrage de l'éditeur reçoive immédiatement la description des champs, sans attendre que l'éditeur la renvoie.



