Tutoriels

Construire un Unified Namespace (UNS) avec MQTT

Construire un Unified Namespace avec MQTT : hiérarchie de topics ISA-95, état retenu, conventions de payload, place de Sparkplug B et scripts pour le tester.

Mis à jour · 9 min de lecture

Construire un Unified Namespace (UNS) avec MQTT

Un Unified Namespace (UNS) est un lieu unique et orienté événements où chaque système d'une entreprise publie son état courant, organisé selon l'emplacement des choses dans l'entreprise plutôt que selon le logiciel qui les a produites. En pratique, c'est un broker MQTT plus une hiérarchie de topics — généralement calquée sur l'ISA-95 : entreprise / site / zone / ligne / cellule — avec des messages retenus qui conservent la dernière valeur de tout.

Ce tutoriel montre comment concevoir cette hiérarchie, décider quoi retenir, choisir des conventions de payload, situer Sparkplug B et tester le namespace avec quelques petits scripts.

Pourquoi un UNS plutôt que des intégrations point à point

Les usines classiques intègrent système par système : le MES interroge le SCADA, l'ERP importe un CSV du MES, un tableau de bord interroge un historien. Chaque nouveau consommateur implique une nouvelle intégration, et personne ne dispose d'un endroit unique pour voir l'état actuel de l'exploitation.

Un UNS remplace cela par un hub :

Point à pointUnified Namespace
IntégrationsUne par paire de systèmesUne par système (vers le broker)
Ajouter un consommateurNouvelle interface sur la sourceS'abonner à un topic
État courantÉparpillé dans des bases de donnéesRetenu sur le broker
Flux de donnéesPolling, batchOrienté événements, sur changement
ResponsabilitéChaque interface séparémentUn namespace, gouverné de façon centrale

Les producteurs — passerelles d'automates, MES, systèmes qualité, adaptateurs ERP — publient dans leur branche de l'arbre. Les consommateurs s'abonnent à ce dont ils ont besoin. Personne n'a besoin de savoir qui d'autre est connecté.

Concevoir la hiérarchie ISA-95

L'ISA-95 fournit un vocabulaire pour la structure physique et organisationnelle d'une usine. La transposer directement en niveaux de topic rend le namespace lisible pour les équipes OT comme IT :

NiveauExempleNiveau de topic
EntrepriseAcme Foodsacme
SiteUsine de Dallasdallas
ZoneConditionnementpackaging
Ligne (work center)Ligne 1line1
Cellule (work unit)Remplisseusefiller
DonnéeVitesse, état, compteursspeed, state, good-count

La vitesse de la remplisseuse se trouve donc sous acme/dallas/packaging/line1/filler/speed. Sous les niveaux ISA-95, ajoutez un petit ensemble de sous-espaces par nature de donnée pour que les consommateurs distinguent l'état des événements :

Une branche du namespace
acme/dallas/packaging/line1/filler/state            "running"       retained
acme/dallas/packaging/line1/filler/speed            {"v":118,...}   retained
acme/dallas/packaging/line1/filler/good-count       {"v":48211,...} retained
acme/dallas/packaging/line1/filler/events/fault     {"code":"E14"}  not retained
acme/dallas/packaging/line1/mes/work-order          {"id":"WO-881"} retained
acme/dallas/packaging/line1/quality/last-check      {"result":"ok"} retained
acme/dallas/packaging/line1/filler/cmd/setpoint     (restricted)    not retained

Des règles qui gardent un namespace sain pendant des années :

  • Minuscules, sans espaces, ASCII uniquement, avec des tirets à l'intérieur d'un niveau. Les topics sont sensibles à la casse, et la mélanger garantit des doublons.
  • Nommer par lieu, pas par source. line1/filler/speed, jamais kepware3/channel2/tag17. Les sources changent ; la ligne reste.
  • Garder une profondeur fixe à l'intérieur d'une branche, pour que acme/+/packaging/+/filler/state fonctionne sur tous les sites.
  • Séparer les commandes sous cmd/ et les protéger par des ACL ; la plupart des producteurs ne devraient jamais pouvoir y écrire.
  • Ne pas mettre de valeurs dans les noms de topic (pas de .../temp/21.5) ; la valeur va dans le payload.

Les principes généraux sont détaillés dans bonnes pratiques de conception des topics. Avant de figer une arborescence, essayez vos motifs d'abonnement dans le topic matcher.

L'état retenu est le cœur d'un UNS

Le namespace doit répondre à « que se passe-t-il en ce moment ? » pour un client connecté il y a une seconde. C'est exactement le rôle des messages retenus : le broker conserve le dernier message retenu de chaque topic et le livre immédiatement à chaque nouvel abonné.

  • Retenir l'état : valeurs, statuts, ordre de fabrication en cours, dernier résultat qualité, consignes.
  • Ne pas retenir les événements : défauts déclenchés, pièces rejetées, commandes. Un nouvel abonné ne doit pas voir le défaut d'hier comme s'il venait d'arriver.
  • Nettoyer les topics obsolètes en publiant un payload retenu vide quand une machine est retirée, sinon le namespace se remplit lentement de fantômes.
  • Utiliser l'expiration des messages de MQTT 5 pour les états qui perdent leur sens sans rafraîchissement, afin que les valeurs périmées disparaissent au lieu de mentir.

Associez à chaque producteur un topic status retenu et un Last Will (offline), comme dans le tutoriel de passerelle Modbus. Les consommateurs peuvent alors distinguer une valeur stable d'une source morte.

Conventions de payload

Mettez-vous d'accord sur une seule forme de payload pour l'état et tenez-vous-y. Un JSON simple et autodescriptif fonctionne bien :

Payload d'état
{ "v": 118, "unit": "bpm", "ts": "2026-10-17T08:41:07.214Z", "q": "good" }

v est la valeur, unit évite de deviner, ts est l'horodatage source en UTC, q la qualité. Les métadonnées statiques (descriptions, plages, unités physiques) peuvent vivre dans un topic frère _meta retenu pour que le chemin chaud reste léger.

Peupler et parcourir un namespace de test

Deux petits scripts paho-mqtt 2.x permettent d'expérimenter avant l'arrivée du moindre producteur réel. Le premier publie un instantané retenu d'une mini-usine avec une expiration d'une heure :

seed_uns.py
import json, time
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

ROOT = "testmqtt/uns/acme/dallas/packaging/line1"
STATE = {
    "filler/state":      "running",
    "filler/speed":      {"v": 118, "unit": "bpm"},
    "filler/good-count": {"v": 48211, "unit": "pcs"},
    "capper/state":      "idle",
    "mes/work-order":    {"id": "WO-881", "product": "SKU-330ml"},
}

props = Properties(PacketTypes.PUBLISH)
props.MessageExpiryInterval = 3600          # secondes

c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
c.connect("broker.connectmq.com", 1883)
c.loop_start()
now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
for path, value in STATE.items():
    body = value if isinstance(value, str) else json.dumps({**value, "ts": now})
    c.publish(f"{ROOT}/{path}", body, qos=1, retain=True, properties=props).wait_for_publish()
c.loop_stop()
c.disconnect()
print("seeded", len(STATE), "topics")

Le second se connecte comme un tout nouveau client, collecte pendant quelques secondes ce que le broker lui transmet et affiche le namespace sous forme d'arbre — exactement ce que verrait un nouveau consommateur :

browse_uns.py
import time
import paho.mqtt.client as mqtt

FILTER = "testmqtt/uns/acme/#"
seen = {}

def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe(FILTER, qos=1)

def on_message(client, userdata, msg):
    seen[msg.topic] = (msg.payload.decode(errors="replace"), msg.retain)

c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
c.on_connect, c.on_message = on_connect, on_message
c.connect("broker.connectmq.com", 1883)
c.loop_start()
time.sleep(3)
c.loop_stop()

for topic in sorted(seen):
    payload, retained = seen[topic]
    depth = topic.count("/") - FILTER.count("/") + 1
    print("  " * depth + topic.rsplit("/", 1)[1], "=", payload, "(retained)" if retained else "")

Si le navigateur de namespace affiche chaque topic peuplé marqué (retained), votre couche d'état fonctionne. Vous pouvez faire la même vérification visuellement dans le client MQTT en ligne :

Parcourir le namespace de test dans le navigateur

L'endpoint partagé suffit pour essayer l'arborescence. Un vrai namespace contient l'état en direct de votre exploitation : faites-le tourner sur un broker que vous contrôlez.

La place de Sparkplug B

Sparkplug B est une spécification au-dessus de MQTT pour les équipements industriels : une structure de topics fixe (spBv1.0/<group>/<message type>/<edge node>/<device>), des payloads Protobuf, des certificats birth/death et un host primaire qui suit l'état. Il résout très bien le problème de l'edge, mais ses topics sont organisés par type de message et edge node, pas selon l'ISA-95.

ApprocheFonctionnementAdaptée quand
UNS en MQTT simpleLes producteurs publient du JSON directement dans des topics ISA-95Consommateurs IT/OT mixtes, outillage simple, topics lisibles
Sparkplug en edge, UNS au-dessusLes edge nodes parlent Sparkplug ; un host ou une passerelle republie dans des topics ISA-95Beaucoup d'équipements, besoin d'un état standard et de payloads compacts
Sparkplug uniquementTous les consommateurs comprennent SparkplugÉcosystème SCADA homogène

Le modèle hybride est populaire pour une bonne raison : les équipements bénéficient de la gestion d'état de Sparkplug et le reste de l'entreprise d'un namespace lisible. Les compromis sont détaillés dans Sparkplug vs MQTT simple. Les données des serveurs OPC UA peuvent entrer dans le même arbre via une passerelle OPC UA vers MQTT.

Checklist de test pour votre namespace

  1. Test du nouvel abonné : un nouveau client abonné à acme/<site>/# reçoit immédiatement l'état courant de chaque équipement (tout est retenu).
  2. Test des événements : les événements arrivent en direct mais ne sont pas rejoués à un nouvel abonné.
  3. Test de changement de source : remplacez un producteur (passerelle simulée → passerelle réelle) ; les consommateurs ne remarquent rien.
  4. Test de source morte : tuez un producteur ; son status passe à offline grâce au Last Will.
  5. Test d'ACL : tentez de publier dans la branche d'une autre ligne avec les identifiants d'une passerelle — cela doit être refusé.
  6. Test des wildcards : chaque filtre de tableau de bord correspond exactement aux topics prévus dans le topic matcher.

Un Unified Namespace relève moins de la technologie que de l'accord : un broker, une hiérarchie que tout le monde comprend, un état retenu pour le présent et des règles claires sur qui publie où. Commencez par une ligne, peuplez-la, parcourez-la, cassez-la volontairement — et ne faites grandir l'arbre que lorsque la première branche a fait ses preuves.

Questions fréquentes

Un Unified Namespace est-il un produit ?

Non. Un UNS est un modèle d'architecture : un hub orienté événements, en général un broker MQTT, où chaque système publie son état courant dans une hiérarchie de topics partagée, calquée sur l'entreprise. On le construit avec un broker, des règles de nommage et les clients qui publient et consomment.

Faut-il Sparkplug B pour un Unified Namespace ?

Non. Beaucoup d'implémentations UNS utilisent MQTT simple avec des payloads JSON et des topics de forme ISA-95. Sparkplug B est utile en edge pour son état birth/death standard et ses payloads compacts ; un schéma courant est Sparkplug des équipements vers un host, qui republie dans le namespace ISA-95.

Chaque message de l'UNS doit-il être retenu ?

Retenez l'état courant (valeurs, statuts, consignes) pour que tout nouveau consommateur voie le présent immédiatement. Ne retenez pas les événements comme les alarmes déclenchées, les pièces produites ou les commandes : ils appartiennent au moment où ils se produisent, puis à un historien.