Gemelo digital

Sincronización de estado del gemelo digital: patrones de estado reportado, deseado y conflictos

Cómo mantener sincronizados un gemelo digital y su activo físico: estado reportado y deseado, números de versión, gestión de conflictos, recuperación tras reconexiones y topics MQTT retenidos, con un dispositivo funcional en Python.

Diagrama de sincronización de estado del gemelo digital con documentos de estado deseado y reportado que circulan entre una aplicación, un broker MQTT y un dispositivo
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

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:

  1. 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.
  2. El estado reportado repite la versión deseada aplicada. El dispositivo incluye desired_version: 42 en su documento reportado, para que el gemelo sepa qué petición se ha satisfecho.
  3. Gana el último escritor, de forma explícita. Para la mayoría de las consignas, gana la versión más reciente. Registra source para que la interfaz pueda mostrar quién lo cambió.
  4. Fusiones a nivel de campo cuando haga falta. Si una aplicación es dueña de speed y otra de mode, fusiona a nivel de campo en lugar de sustituir el documento entero, o sepáralos en topics retenidos distintos.
  5. 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:

  1. Conéctate con un Last Will en el topic de estado.
  2. Publica online en el topic de estado, retenido.
  3. Publica el estado reportado actual completo, retenido, a partir de los sensores locales, no del recuerdo de lo que envió por última vez.
  4. Suscríbete al topic deseado y recibe el documento deseado retenido.
  5. 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

  1. Estado reportado escrito solo por el dispositivo, estado deseado solo por las aplicaciones.
  2. Ambos documentos retenidos, con versión y marca de tiempo.
  3. El reportado repite la última versión deseada aplicada.
  4. Las acciones se envían como comandos no retenidos con confirmaciones.
  5. Last Will en un topic de estado retenido.
  6. Al reconectarse, el dispositivo publica el reportado antes de aplicar el deseado.
  7. El dispositivo valida los límites e informa de los rechazos.
  8. 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.

Artículos relacionados

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.