Tutoriales

Construye un Unified Namespace (UNS) con MQTT

Cómo construir un Unified Namespace con MQTT: jerarquía de topics ISA-95, estado retenido, convenciones de payload, el papel de Sparkplug B y scripts para probarlo.

Actualizado · 9 min de lectura

Construye un Unified Namespace (UNS) con MQTT

Un Unified Namespace (UNS) es un lugar único y orientado a eventos donde cada sistema de una empresa publica su estado actual, organizado según dónde están las cosas en el negocio y no según qué software las produjo. En la práctica es un broker MQTT más una jerarquía de topics —normalmente basada en ISA-95: empresa / planta / área / línea / celda— con mensajes retenidos que guardan el último valor de todo.

Este tutorial muestra cómo diseñar esa jerarquía, decidir qué retener, elegir convenciones de payload, encajar Sparkplug B y probar el namespace con unos pocos scripts.

Por qué un UNS en lugar de integraciones punto a punto

Las plantas clásicas integran sistema a sistema: el MES consulta al SCADA, el ERP importa un CSV del MES, un dashboard consulta un historiador. Cada nuevo consumidor implica una nueva integración y nadie tiene un único lugar para ver el estado actual de la operación.

Un UNS lo sustituye por un hub:

Punto a puntoUnified Namespace
IntegracionesUna por cada par de sistemasUna por sistema (hacia el broker)
Añadir un consumidorNueva interfaz en el origenSuscribirse a un topic
Estado actualRepartido en bases de datosRetenido en el broker
Flujo de datosPolling, batchOrientado a eventos, por excepción
PropiedadCada interfaz por separadoUn namespace, gobernado de forma central

Los productores —pasarelas de PLC, MES, sistemas de calidad, adaptadores de ERP— publican en su rama del árbol. Los consumidores se suscriben a lo que necesiten. Nadie necesita saber quién más está conectado.

Diseña la jerarquía ISA-95

ISA-95 te da un vocabulario para la estructura física y organizativa de una planta. Mapearlo directamente a niveles de topic hace que el namespace sea legible tanto para la gente de OT como de IT:

NivelEjemploNivel de topic
EmpresaAcme Foodsacme
PlantaPlanta de Dallasdallas
ÁreaEnvasadopackaging
Línea (work center)Línea 1line1
Celda (work unit)Llenadorafiller
DatoVelocidad, estado, contadoresspeed, state, good-count

Así, la velocidad de la llenadora vive en acme/dallas/packaging/line1/filler/speed. Debajo de los niveles ISA-95, añade un pequeño conjunto de subespacios por tipo de dato para que los consumidores distingan estado de eventos:

Una rama del 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

Reglas que mantienen sano un namespace durante años:

  • Minúsculas, sin espacios, solo ASCII, con guiones dentro de un nivel. Los topics distinguen mayúsculas y minúsculas, y mezclarlas garantiza duplicados.
  • Nombra por lugar, no por origen. line1/filler/speed, nunca kepware3/channel2/tag17. Los orígenes cambian; la línea se queda.
  • Mantén fija la profundidad de los niveles dentro de una rama, para que acme/+/packaging/+/filler/state funcione en todas las plantas.
  • Separa los comandos bajo cmd/ y protégelos con ACL; la mayoría de los productores nunca deberían poder escribir ahí.
  • No pongas valores en los nombres de topic (nada de .../temp/21.5); el valor va en el payload.

Los principios generales están en buenas prácticas de diseño de topics. Antes de comprometerte con una estructura, prueba tus patrones de suscripción en el topic matcher.

El estado retenido es el corazón de un UNS

El namespace debe responder "¿qué está pasando ahora mismo?" a un cliente que se conectó hace un segundo. Eso es exactamente lo que hacen los mensajes retenidos: el broker guarda el último mensaje retenido de cada topic y lo entrega al instante a cada nuevo suscriptor.

  • Retén el estado: valores, estados, orden de trabajo actual, último resultado de calidad, consignas.
  • No retengas eventos: fallos, piezas rechazadas, comandos. Un nuevo suscriptor no debe ver el fallo de ayer como si acabara de ocurrir.
  • Limpia los topics obsoletos publicando un payload retenido vacío cuando se retire una máquina, o el namespace se irá llenando de fantasmas.
  • Usa la expiración de mensajes de MQTT 5 en el estado que deja de tener sentido si no se refresca, para que los valores viejos desaparezcan en lugar de mentir.

Acompaña a cada productor con un topic status retenido y un Last Will (offline), como en el tutorial de pasarela Modbus. Así los consumidores distinguen un valor estable de una fuente muerta.

Convenciones de payload

Acordad una única forma de payload para el estado y mantenedla. Un JSON simple y autodescriptivo funciona bien:

Payload de estado
{ "v": 118, "unit": "bpm", "ts": "2026-10-17T08:41:07.214Z", "q": "good" }

v es el valor, unit evita adivinar, ts es el timestamp de origen en UTC y q la calidad. Los metadatos estáticos (descripciones, rangos, unidades de ingeniería) pueden vivir en un topic hermano _meta retenido para que el camino caliente siga siendo ligero.

Siembra y explora un namespace de prueba

Dos pequeños scripts con paho-mqtt 2.x te permiten experimentar antes de que exista ningún productor real. El primero publica una instantánea retenida de una planta diminuta con expiración de una hora:

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

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

El segundo se conecta como un cliente totalmente nuevo, recoge durante unos segundos lo que le entrega el broker e imprime el namespace como un árbol, justo lo que vería un consumidor nuevo:

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 el explorador imprime cada topic sembrado marcado como (retained), tu capa de estado funciona. Puedes hacer la misma comprobación de forma visual en el cliente MQTT online:

Explora el namespace de prueba en tu navegador

El endpoint compartido sirve para probar la estructura. Un namespace real contiene el estado en vivo de tu operación, así que ejecútalo en un broker que controles.

Dónde encaja Sparkplug B

Sparkplug B es una especificación sobre MQTT para dispositivos industriales: una estructura de topics fija (spBv1.0/<group>/<message type>/<edge node>/<device>), payloads Protobuf, certificados birth/death y un host primario que sigue el estado. Resuelve muy bien el problema del edge, pero sus topics se organizan por tipo de mensaje y edge node, no por ISA-95.

EnfoqueCómo funcionaConviene cuando
UNS con MQTT simpleLos productores publican JSON directamente en topics ISA-95Consumidores mixtos IT/OT, herramientas sencillas, topics legibles
Sparkplug en el edge, UNS por encimaLos edge nodes hablan Sparkplug; un host o puente republica en topics ISA-95Muchos dispositivos, necesidad de estado estándar y payloads compactos
Solo SparkplugTodos los consumidores entienden SparkplugEcosistema SCADA homogéneo

El modelo híbrido es popular por algo: los dispositivos obtienen la gestión de estado de Sparkplug y el resto de la empresa un namespace legible. Las ventajas e inconvenientes se analizan en Sparkplug vs MQTT simple. Los datos de servidores OPC UA pueden entrar en el mismo árbol mediante un puente OPC UA a MQTT.

Checklist de pruebas para tu namespace

  1. Prueba de suscriptor nuevo: un cliente nuevo suscrito a acme/<site>/# recibe al instante el estado actual de cada activo (todo retenido).
  2. Prueba de eventos: los eventos llegan en vivo pero no se reproducen a un suscriptor nuevo.
  3. Prueba de cambio de origen: sustituye un productor (pasarela simulada → pasarela real); los consumidores no notan nada.
  4. Prueba de origen caído: mata un productor; su status pasa a offline gracias al Last Will.
  5. Prueba de ACL: intenta publicar en la rama de otra línea con las credenciales de una pasarela; debe ser rechazado.
  6. Prueba de wildcards: cada filtro de dashboard coincide exactamente con los topics previstos en el topic matcher.

Un Unified Namespace tiene menos que ver con la tecnología que con el acuerdo: un broker, una jerarquía que todos entienden, estado retenido para el presente y reglas claras sobre quién publica dónde. Empieza con una línea, siémbrala, explórala, rómpela a propósito, y haz crecer el árbol solo cuando la primera rama haya demostrado su valor.

Preguntas frecuentes

¿Un Unified Namespace es un producto?

No. Un UNS es un patrón de arquitectura: un hub orientado a eventos, normalmente un broker MQTT, donde cada sistema publica su estado actual en una jerarquía de topics compartida y con forma de negocio. Se construye con un broker, reglas de nombres y los clientes que publican y consumen.

¿Necesito Sparkplug B para un Unified Namespace?

No. Muchas implementaciones de UNS usan MQTT simple con payloads JSON y topics con forma ISA-95. Sparkplug B es útil en el edge por su estado birth/death estándar y sus payloads compactos; un patrón común es Sparkplug desde los dispositivos hasta un host, que republica en el namespace ISA-95.

¿Debe ser retenido cada mensaje del UNS?

Retén el estado actual (valores, estados, consignas) para que cualquier consumidor nuevo vea el presente al instante. No retengas eventos como alarmas, piezas producidas o comandos: pertenecen al momento en que ocurren y, después, a un historiador.