Usar un gemelo digital con mensajes retenidos MQTT es la forma más ligera de dar memoria a un gemelo: cada propiedad vive en su propio topic retenido, así que cualquier visor que se suscriba recibe al instante el estado actual completo, sin base de datos ni API. En este tutorial construirás un pequeño gemelo de máquina con un simulador en Python, un visor en Node.js que reconstruye el estado completo al conectarse y los comandos que necesitas para inspeccionar y limpiar los topics retenidos.
Para el concepto general, consulta la guía de tecnología de gemelo digital.
Cómo funcionan los retained messages
Normalmente, un broker MQTT reenvía un mensaje a los suscriptores actuales y lo olvida. Cuando un publicador activa el flag retain, el broker además guarda ese mensaje como último valor conocido del topic. Cualquier cliente que se suscriba más tarde a un topic coincidente recibe de inmediato el mensaje guardado, marcado como retenido.
Reglas clave:
- El broker guarda un retained message por topic: el más reciente sustituye al anterior.
- Las suscripciones con comodines reciben el retained message de todos los topics coincidentes.
- Publicar un payload vacío con el flag retain borra el retained message.
- Los retained messages se entregan con el menor de los QoS de publicación y de suscripción.
Esa combinación es exactamente lo que necesita un gemelo: un almacén clave-valor del estado actual, direccionado por topic y legible con comodines. Para ver dónde encaja esto entre otros enfoques de estado, consulta arquitectura de gemelo digital.
Diseño del árbol de topics: una propiedad por topic
Puedes retener un documento JSON por activo o un valor por propiedad. Este tutorial usa un topic por propiedad, lo que tiene ventajas reales:
| Enfoque | Ventajas | Inconvenientes |
|---|---|---|
| Un JSON retenido por activo | Menos mensajes, instantánea atómica | Cada cambio reenvía todos los campos; los escritores concurrentes se sobrescriben entre sí |
| Un topic retenido por propiedad | Actualizaciones pequeñas, escritores independientes, suscripción sencilla a un solo valor | Más topics; no hay instantánea atómica entre propiedades |
Para una máquina CNC en el broker público, el árbol queda así (sin pasar de 8 niveles de topic):
connectmq/demo/f0c3/cell1/cnc1/spindle_rpm
connectmq/demo/f0c3/cell1/cnc1/feed_mm_min
connectmq/demo/f0c3/cell1/cnc1/tool
connectmq/demo/f0c3/cell1/cnc1/mode
connectmq/demo/f0c3/cell1/cnc1/alarm
connectmq/demo/f0c3/cell1/cnc1/status
Los payloads son valores simples, opcionalmente con una marca de tiempo. Aquí usamos un pequeño objeto JSON {"v": 8000, "ts": 1792152000000} para que los visores sepan lo reciente que es cada valor.
Sustituye f0c3 por tu propia cadena aleatoria. Cualquiera puede leer el servidor MQTT público gratuito, así que publica solo datos de demostración.
Paso 1: publica las propiedades del gemelo como retained messages
Este simulador en Python publica cada propiedad solo cuando cambia, siempre con retain=True, y usa un Last Will para marcar la máquina como desconectada:
import json
import random
import time
import paho.mqtt.client as mqtt
BASE = "connectmq/demo/f0c3/cell1/cnc1"
last_sent = {}
def publish_property(client, name, value):
if last_sent.get(name) == value:
return
payload = json.dumps({"v": value, "ts": int(time.time() * 1000)})
client.publish(f"{BASE}/{name}", payload, qos=1, retain=True)
last_sent[name] = value
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="cnc1-twin-f0c3")
client.will_set(f"{BASE}/status", json.dumps({"v": "offline"}), qos=1, retain=True)
client.connect("broker.connectmq.com", 1883)
client.loop_start()
publish_property(client, "status", "online")
publish_property(client, "tool", 3)
try:
while True:
running = random.random() > 0.1
publish_property(client, "mode", "AUTO" if running else "HOLD")
publish_property(client, "spindle_rpm", random.choice([7950, 8000, 8050]) if running else 0)
publish_property(client, "feed_mm_min", 1000 if running else 0)
publish_property(client, "alarm", None if running else "E-STOP")
time.sleep(2)
except KeyboardInterrupt:
publish_property(client, "status", "offline")
time.sleep(0.5)
client.loop_stop()
client.disconnect()
Publicar cuando cambia el valor en lugar de con un temporizador mantiene bajo el tráfico y respeta el límite del broker público de 10 mensajes por segundo por cliente. El estado retenido garantiza que los visores sigan viendo los valores que no han cambiado.
Paso 2: construye un visor que cargue el estado completo al instante
El visor se suscribe con un comodín, reúne los retained messages en un objeto de gemelo y después sigue aplicando las actualizaciones en vivo. Con MQTT.js en Node.js:
const mqtt = require("mqtt");
const base = "connectmq/demo/f0c3/cell1/cnc1";
const twin = {};
const client = mqtt.connect("mqtt://broker.connectmq.com:1883", {
clientId: "cnc1-viewer-" + Math.random().toString(16).slice(2, 10),
protocolVersion: 5,
});
client.on("connect", () => {
client.subscribe(`${base}/+`, { qos: 1 });
});
client.on("message", (topic, payload, packet) => {
const property = topic.slice(base.length + 1);
if (payload.length === 0) {
delete twin[property];
return;
}
const { v, ts } = JSON.parse(payload.toString());
twin[property] = { value: v, ts, fromRetained: packet.retain };
console.clear();
console.table(twin);
});
Ejecuta el visor después de haber arrancado el simulador. La tabla se llena con todas las propiedades a la vez, porque cada retained message se entrega al suscribirse con packet.retain a true. Las actualizaciones posteriores llegan como mensajes en vivo. En un navegador, el mismo código funciona con wss://broker.connectmq.com:8084/mqtt, que es como los visores de un gemelo digital 3D cargan una escena completa en un solo viaje de ida y vuelta; gemelo digital con MQTT muestra la variante para navegador.
Paso 3: inspecciona y limpia los topics retenidos
Los retained messages permanecen hasta que se sustituyen o se borran, lo cual es una ventaja hasta que cambias el nombre de una propiedad. Inspecciona todo lo que hay bajo tu prefijo:
mosquitto_sub -h broker.connectmq.com -p 1883 -i inspect-f0c3 \
-t "connectmq/demo/f0c3/#" -v
Borra una propiedad que ya no uses publicando un mensaje retenido vacío:
mosquitto_pub -h broker.connectmq.com -p 1883 -i cleanup-f0c3 \
-t "connectmq/demo/f0c3/cell1/cnc1/alarm" -r -n
El flag -n envía un payload nulo (de longitud cero); junto con -r elimina el retained message.
Escalar el patrón a muchos activos
Una máquina es fácil; una planta entera necesita algunas convenciones más. Mantén los mismos nombres de propiedad entre activos del mismo tipo, para que un visor pueda renderizar cualquier CNC con la misma plantilla. Pon la jerarquía de activos en la ruta del topic, por ejemplo site/area/line/asset/property, para que los visores puedan suscribirse exactamente a la rama que muestran: una vista de línea se suscribe a .../line2/+/+ y una página de una sola máquina a .../line2/cnc4/+. En el broker público, recuerda el límite de 8 niveles de topic y 10 suscripciones por cliente, más que suficiente para una demo pero un motivo para usar tu propio broker en despliegues reales.
Los visores que abren una rama grande reciben a la vez un retained message por propiedad. Normalmente no hay problema, pero analízalos y aplícalos en lote antes del primer renderizado, en lugar de volver a renderizar la escena con cada mensaje. El resultado es un gemelo que aparece completo desde el primer fotograma.
Opciones de MQTT 5 que mejoran los gemelos retenidos
MQTT 5 añade controles que hacen más preciso el estado retenido:
- Message expiry interval: fija una expiración en las propiedades volátiles para que un valor retenido desaparezca si no se refresca, en lugar de mostrar para siempre una lectura obsoleta.
- Retain handling al suscribirse:
0envía los retained messages en cada suscripción,1solo en las suscripciones nuevas,2nunca. Usa2para los consumidores que solo quieren cambios en vivo. - Retain as published: permite que un bridge o relé sepa si el mensaje original estaba retenido.
En MQTT.js, una suscripción con { qos: 1, rh: 2 } omite los retained messages, y publicar con { retain: true, properties: { messageExpiryInterval: 300 } } hace que un valor expire a los cinco minutos. Consulta funciones de MQTT 5 para más información.
Errores habituales y resolución de problemas
- Propiedades fantasma: los topics renombrados dejan atrás valores retenidos antiguos. Bórralos explícitamente.
- Comandos retenidos: nunca retengas topics de comandos; los dispositivos los volverán a ejecutar al reconectarse.
- Valores obsoletos mostrados como en vivo: incluye marcas de tiempo y respeta el topic de estado.
- Payloads retenidos demasiado grandes: mantén cada valor pequeño; el broker público acepta hasta 64 KB por mensaje y el plan Serverless gratuito, 256 KB.
- Sin instantánea atómica: si varias propiedades deben cambiar juntas, publícalas en su lugar como un único documento JSON retenido.
- Reinicios del broker: planifica que los dispositivos vuelvan a publicar el estado retenido al reconectarse, en lugar de suponer que sobrevivirá para siempre a los reinicios del broker. Que los dispositivos publiquen el estado completo al conectarse hace que el gemelo se autorrepare.
Cuándo no bastan los topics retenidos
Los topics retenidos son ideales para el estado actual. Añade otros componentes cuando necesites histórico (un suscriptor de series temporales), consultas entre activos o control de acceso por gemelo (un servicio de gemelos como Eclipse Ditto con MQTT), o gestión de estado deseado frente a reportado a escala. Para el patrón deseado/reportado sobre topics retenidos, lee sincronización de estado del gemelo digital.
Ejecuta tu gemelo con estado retenido en ConnectMQ
Para activos reales, pasa del broker público a un broker privado en ConnectMQ, un broker MQTT gestionado con MQTT 3.1.1 y 5.0, retained messages, expiración de mensajes y TLS. Sustituye el host por el de tu broker, añade YOUR_USERNAME, YOUR_PASSWORD y YOUR_CLIENT_ID desde el portal y cambia al puerto 8883, o al 8084 para navegadores. Regístrate en app.connectmq.com con el plan Serverless gratuito, sin tarjeta de crédito, y observa los topics retenidos de tu gemelo en el explorador de topics en vivo.
Preguntas frecuentes
¿Qué es un retained message en MQTT?
Es un mensaje que el broker guarda como último valor conocido de su topic y que entrega de inmediato a cualquier nuevo suscriptor de ese topic.
¿Cómo borro un retained message de MQTT?
Publica un payload de longitud cero con el flag retain en el mismo topic, por ejemplo mosquitto_pub -t <topic> -r -n.
¿Debe tener cada propiedad del gemelo su propio topic retenido?
Funciona bien para propiedades independientes y actualizaciones pequeñas. Usa un documento JSON retenido por activo cuando varios valores deban cambiar juntos de forma atómica.
¿Pueden los retained messages sustituir a una base de datos en un gemelo digital?
Para el estado actual, a menudo sí. Para el histórico, las consultas entre activos o los registros de auditoría, añade un suscriptor con base de datos junto al broker.



