La sincronización de estado del gemelo digital es el problema de mantener de acuerdo lo que cree el gemelo y lo que realmente es el activo, incluso con desconexiones, retrasos y comandos contradictorios. Este artículo explica el modelo de estado reportado frente a deseado, el versionado y la gestión de conflictos, y cómo implementarlo con topics MQTT retenidos, para terminar con un dispositivo funcional en Python que converge al estado deseado.
Para el contexto general, empieza por la guía de tecnología de gemelo digital; si además quieres visualizar el estado sincronizado, un gemelo digital 3D en el navegador puede mostrar a la vez el estado reportado y el deseado.
Por qué la sincronización es difícil
Un gemelo solo es útil si es correcto. En la práctica, varias cosas conspiran en contra:
- Latencia: el gemelo ve un valor unos milisegundos o segundos después de que cambiara.
- Desconexiones: un dispositivo se desconecta y vuelve con un estado distinto.
- Escritores concurrentes: un operario y una automatización cambian a la vez una consigna.
- Orden: los mensajes que llegan por caminos distintos lo hacen desordenados.
- Fallos parciales: un comando se entrega, pero la confirmación se pierde.
Un buen diseño de sincronización no elimina estos problemas; los hace visibles y recuperables.
El modelo de estado reportado y deseado
El patrón más extendido, popularizado por los servicios de device shadow y por frameworks de gemelos como Eclipse Ditto, divide el estado en dos documentos:
| Documento | Quién lo escribe | Significado |
|---|---|---|
| Reported | Solo el dispositivo | Lo que el activo es realmente ahora mismo |
| Desired | Aplicaciones, operarios, automatizaciones | Lo que el activo debería pasar a ser |
| Delta (derivado) | Calculado | Campos en los que el deseado difiere del reportado |
La regla de oro: solo el dispositivo escribe el estado reportado. Una aplicación que quiere una bomba a 1.450 rpm escribe desired.speed = 1450. El dispositivo ve el delta, actúa y después reporta reported.speed = 1450. La interfaz del gemelo muestra ambos, para que los operarios puedan distinguir lo "solicitado" de lo "conseguido".
Esta separación elimina toda una clase de errores en los que la interfaz muestra un valor que la máquina nunca llegó a alcanzar. Compara cómo aplican esto los shadows ligeros y los gemelos completos en device shadow vs gemelo digital.
Implementar la sincronización de estado del gemelo digital con MQTT
MQTT se ajusta limpiamente a este modelo usando topics retenidos:
connectmq/demo/<id>/plant1/pump7/state/reported retained, written by device
connectmq/demo/<id>/plant1/pump7/state/desired retained, written by apps
connectmq/demo/<id>/plant1/pump7/status retained, last will "offline"
Como ambos topics de estado están retenidos, el broker entrega la última versión a cualquiera que se suscriba:
- Un visor que abre el gemelo recibe al instante el estado reportado y el deseado.
- Un dispositivo que se reconecta tras una caída recibe al instante el último estado deseado y puede converger, aunque el cambio deseado se hiciera mientras estaba desconectado.
Esa segunda propiedad es la clave de una sincronización robusta. No necesitas un almacén aparte de "comandos pendientes" para los cambios de tipo consigna; el documento deseado retenido es el comando pendiente. Consulta gemelo digital con retained messages MQTT para un tutorial completo sobre el estado retenido del gemelo.
Estado deseado frente a comandos
Usa el estado deseado para objetivos idempotentes: consignas, modos, configuración, encendido/apagado. Aplicarlos dos veces tiene el mismo efecto que una. Usa comandos no retenidos para acciones: reiniciar un contador, abrir una puerta durante cinco segundos, iniciar una calibración. Retener un comando de acción haría que se repitiera tras cada reconexión, lo cual es peligroso.
Versionado y gestión de conflictos
Cada documento de estado debería llevar una versión y una marca de tiempo:
{
"version": 42,
"ts": 1792152000000,
"source": "operator:maria",
"speed_rpm": 1450,
"mode": "AUTO"
}
Úsalas para gestionar los conflictos:
- Versiones monótonas en el estado deseado. Los escritores leen el documento deseado actual, incrementan la versión y publican. Los consumidores ignoran cualquier mensaje deseado con una versión inferior a la ya aplicada.
- El estado reportado repite la versión deseada aplicada. El dispositivo incluye
desired_version: 42en su documento reportado, para que el gemelo sepa qué petición se ha satisfecho. - Gana el último escritor, de forma explícita. Para la mayoría de las consignas, gana la versión más reciente. Registra
sourcepara que la interfaz pueda mostrar quién lo cambió. - Fusiones a nivel de campo cuando haga falta. Si una aplicación es dueña de
speedy otra demode, fusiona a nivel de campo en lugar de sustituir el documento entero, o sepáralos en topics retenidos distintos. - Protecciones en el dispositivo. El dispositivo rechaza valores fuera de los límites seguros e informa del rechazo con un motivo, en lugar de ignorarlos en silencio.
Los brokers no imponen las versiones por ti; es lógica de tus publicadores y del dispositivo. Las user properties de MQTT 5 son un lugar cómodo para llevar los metadatos de versión sin tocar el payload.
Gestión de reconexiones y estado obsoleto
Cuando un dispositivo se reconecta, sigue una secuencia fija:
- Conéctate con un Last Will en el topic de estado.
- Publica
onlineen el topic de estado, retenido. - Publica el estado reportado actual completo, retenido, a partir de los sensores locales, no del recuerdo de lo que envió por última vez.
- Suscríbete al topic deseado y recibe el documento deseado retenido.
- Calcula el delta, aplícalo y publica el estado reportado actualizado.
Que el paso 3 vaya antes que el 4 importa: el gemelo conoce primero la verdad y después el dispositivo converge hacia el objetivo. Si los inviertes, un visor podría ver brevemente un estado reportado obsoleto etiquetado como actual.
En el lado del gemelo, trata el estado reportado como obsoleto cuando el topic de estado diga offline. Muéstralo en gris en lugar de presentar valores antiguos como si fueran en vivo.
Un dispositivo funcional que converge al estado deseado
Este dispositivo en Python usa el servidor MQTT público gratuito (solo para pruebas, nunca para datos de producción) y paho-mqtt 2.x. Sustituye 9c4e por tu propio prefijo aleatorio.
import json
import time
import paho.mqtt.client as mqtt
BASE = "connectmq/demo/9c4e/plant1/pump7"
LIMITS = {"speed_rpm": (0, 3000)}
reported = {"version": 0, "desired_version": 0, "speed_rpm": 0, "mode": "MANUAL"}
def publish_reported(client):
reported["version"] += 1
reported["ts"] = int(time.time() * 1000)
client.publish(f"{BASE}/state/reported", json.dumps(reported), qos=1, retain=True)
def on_connect(client, userdata, flags, reason_code, properties):
client.publish(f"{BASE}/status", "online", qos=1, retain=True)
publish_reported(client)
client.subscribe(f"{BASE}/state/desired", qos=1)
def on_message(client, userdata, msg):
if not msg.payload:
return
desired = json.loads(msg.payload)
if desired.get("version", 0) <= reported["desired_version"]:
return
errors = []
for key, value in desired.items():
if key in ("version", "ts", "source"):
continue
low, high = LIMITS.get(key, (None, None))
if low is not None and not (low <= value <= high):
errors.append(f"{key} out of range")
continue
reported[key] = value
reported["desired_version"] = desired["version"]
reported["errors"] = errors
publish_reported(client)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="pump7-9c4e")
client.will_set(f"{BASE}/status", "offline", qos=1, retain=True)
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883)
client.loop_forever()
Fija un estado deseado desde otra terminal:
mosquitto_pub -h broker.connectmq.com -p 1883 -i pump7-ops-9c4e -q 1 -r \
-t "connectmq/demo/9c4e/plant1/pump7/state/desired" \
-m '{"version": 1, "source": "operator", "speed_rpm": 1450, "mode": "AUTO"}'
Detén el dispositivo, publica la versión 2 con una nueva velocidad y vuelve a arrancar el dispositivo. Recibe el documento deseado retenido al suscribirse y converge de inmediato. Obsérvalo todo con mosquitto_sub -t "connectmq/demo/9c4e/plant1/pump7/#" -v.
Mostrar el estado de sincronización en la interfaz del gemelo
La lógica de sincronización es solo la mitad del trabajo; los operarios necesitan verla. Una buena interfaz de gemelo hace explícito el estado de sincronización en lugar de ocultarlo:
- Pendiente: el deseado difiere del reportado y el dispositivo está conectado. Muestra el valor objetivo junto al real, por ejemplo "1.450 rpm solicitadas, 1.200 rpm reales".
- Aplicado: el reportado repite la última versión deseada. Muestra solo el valor real.
- Rechazado: el dispositivo informó de un error para esa versión. Muestra el motivo y conserva el valor anterior.
- Obsoleto: el topic de estado dice offline. Muestra los valores en gris e indica cuándo se actualizaron por última vez.
Añade también un timeout. Si un cambio deseado sigue pendiente más tiempo del que el dispositivo necesita normalmente, márcalo para que alguien lo investigue en lugar de suponer que la máquina obedeció. Estos estados se derivan directamente de los documentos reportado y deseado versionados descritos arriba, así que no necesitan infraestructura adicional.
Checklist de sincronización de estado
- Estado reportado escrito solo por el dispositivo, estado deseado solo por las aplicaciones.
- Ambos documentos retenidos, con versión y marca de tiempo.
- El reportado repite la última versión deseada aplicada.
- Las acciones se envían como comandos no retenidos con confirmaciones.
- Last Will en un topic de estado retenido.
- Al reconectarse, el dispositivo publica el reportado antes de aplicar el deseado.
- El dispositivo valida los límites e informa de los rechazos.
- La interfaz muestra deseado frente a reportado y marca como obsoletos los gemelos desconectados.
Sincroniza tus gemelos digitales en ConnectMQ
ConnectMQ es un broker MQTT gestionado que soporta retained messages, Last Will, QoS de 0 a 2 según el plan y funciones de MQTT 5 como user properties, expiración de mensajes y expiración de sesión. El explorador de topics en vivo del portal facilita inspeccionar en paralelo los documentos reportado y deseado. Tu lógica de sincronización vive en los dispositivos y aplicaciones que se conectan como clientes MQTT normales, mientras el broker MQTT en la nube se encarga de la entrega. Empieza gratis con el plan Serverless en app.connectmq.com, sin tarjeta de crédito.
Preguntas frecuentes
¿Qué diferencia hay entre el estado reportado y el deseado?
El estado reportado es lo que el dispositivo es realmente, y solo lo escribe el dispositivo. El estado deseado es lo que las aplicaciones quieren que pase a ser. La diferencia entre ambos es el delta sobre el que debe actuar el dispositivo.
¿Por qué usar retained messages para el estado deseado?
Un documento deseado retenido se entrega a un dispositivo en cuanto se suscribe, así que los cambios hechos mientras estaba desconectado se aplican automáticamente al reconectarse.
¿Cómo resuelvo actualizaciones contradictorias de un gemelo digital?
Añade versiones y orígenes a los documentos deseados, ignora las versiones antiguas, fusiona a nivel de campo cuando la propiedad sea de distintos dueños y deja que el dispositivo valide e informe de lo que realmente aplicó.
¿Deben retenerse los comandos?
No. Retén solo el estado deseado idempotente. Los comandos de acción deben ser no retenidos, idealmente con expiración de mensaje y una confirmación.



