Usar mensagens retidas MQTT em um gêmeo digital é a forma mais leve de dar memória a um gêmeo: cada propriedade fica em seu próprio topic retido, então qualquer visualizador que assine recebe instantaneamente o estado atual completo, sem banco de dados nem API. Neste tutorial você vai construir um pequeno gêmeo de máquina com um simulador Python, um visualizador Node.js que reconstrói o estado completo ao se conectar e os comandos necessários para inspecionar e limpar topics retidos.
Para o conceito mais amplo, veja o guia de tecnologia de gêmeo digital.
Como as Retained Messages Funcionam
Normalmente, um broker MQTT encaminha uma mensagem aos assinantes atuais e a esquece. Quando um publicador define a flag retain, o broker também armazena essa mensagem como o último valor conhecido do topic. Qualquer cliente que assine depois um topic correspondente recebe a mensagem armazenada imediatamente, marcada como retida.
Regras principais:
- O broker mantém uma retained message por topic: a mais recente substitui a anterior.
- Assinaturas com wildcard recebem a retained message de cada topic correspondente.
- Publicar um payload vazio com a flag retain apaga a retained message.
- As retained messages são entregues com o menor entre o QoS de publicação e o de assinatura.
Essa combinação é exatamente o que um gêmeo precisa: um armazenamento chave-valor do estado atual, endereçado por topic e legível por wildcard. Para uma visão de onde isso se encaixa entre outras abordagens de estado, veja arquitetura de gêmeo digital.
Projetando a Árvore de Topics: Uma Propriedade por Topic
Você pode reter um documento JSON por ativo ou um valor por propriedade. Este tutorial usa um topic por propriedade, o que traz vantagens reais:
| Abordagem | Prós | Contras |
|---|---|---|
| Um JSON retido por ativo | Menos mensagens, snapshot atômico | Cada mudança reenvia todos os campos; escritores concorrentes sobrescrevem uns aos outros |
| Um topic retido por propriedade | Atualizações pequenas, escritores independentes, assinatura fácil de um único valor | Mais topics; sem snapshot atômico entre propriedades |
Para uma máquina CNC no servidor MQTT público, a árvore fica assim (mantenha dentro de 8 níveis 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
Os payloads são valores simples, opcionalmente com um timestamp. Aqui usamos um pequeno objeto JSON {"v": 8000, "ts": 1792152000000} para que os visualizadores saibam quão recente é cada valor.
Substitua f0c3 pela sua própria string aleatória. O servidor MQTT público (gratuito) pode ser lido por qualquer pessoa e serve apenas para testes e protótipos, então publique apenas dados de demonstração.
Passo 1: Publique as Propriedades do Gêmeo como Retained Messages
Este simulador Python publica cada propriedade apenas quando ela muda, sempre com retain=True, e usa um Last Will para marcar a máquina como offline:
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 na mudança, e não por temporizador, mantém o tráfego baixo e respeita o limite de taxa do servidor MQTT público de 10 mensagens por segundo por cliente. O estado retido garante que os visualizadores continuem vendo os valores que não mudaram.
Passo 2: Construa um Visualizador que Carrega o Estado Completo na Hora
O visualizador assina com um wildcard, reúne as retained messages em um objeto do gêmeo e depois continua aplicando as atualizações ao vivo. Com MQTT.js no 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);
});
Rode o visualizador depois que o simulador tiver começado. A tabela se preenche com todas as propriedades de uma vez, porque cada retained message é entregue na assinatura com packet.retain definido como true. As atualizações seguintes chegam como mensagens ao vivo. No navegador, o mesmo código funciona com wss://broker.connectmq.com:8084/mqtt, e é assim que visualizadores de gêmeos 3D carregam uma cena inteira em uma única ida e volta; gêmeo digital com MQTT mostra a variante para navegador.
Passo 3: Inspecione e Limpe os Topics Retidos
As retained messages permanecem até serem substituídas ou apagadas, o que é um recurso até o dia em que você renomeia uma propriedade. Inspecione tudo sob o seu prefixo:
mosquitto_sub -h broker.connectmq.com -p 1883 -i inspect-f0c3 \
-t "connectmq/demo/f0c3/#" -v
Apague uma propriedade que você não usa mais publicando uma retained message vazia:
mosquitto_pub -h broker.connectmq.com -p 1883 -i cleanup-f0c3 \
-t "connectmq/demo/f0c3/cell1/cnc1/alarm" -r -n
A flag -n envia um payload nulo (de tamanho zero); junto com -r, ela remove a retained message.
Escalando o Padrão para Muitos Ativos
Uma máquina é fácil; uma planta inteira, como uma usinagem em Campinas com dezenas de CNCs, precisa de algumas convenções extras. Mantenha os mesmos nomes de propriedades entre ativos do mesmo tipo, para que um visualizador possa renderizar qualquer CNC a partir do mesmo template. Coloque a hierarquia de ativos no caminho do topic, por exemplo site/area/line/asset/property, para que os visualizadores assinem exatamente o ramo que exibem: uma visão de linha assina .../line2/+/+, uma página de máquina única assina .../line2/cnc4/+. No servidor MQTT público, lembre-se do limite de 8 níveis de topic e 10 assinaturas por cliente, o que é suficiente para uma demonstração, mas é um motivo para usar seu próprio broker em implantações reais.
Visualizadores que abrem um ramo grande recebem uma retained message por propriedade de uma só vez. Normalmente isso não é problema, mas processe e aplique as mensagens em lote antes da primeira renderização, em vez de renderizar a cena novamente a cada mensagem. O resultado é um gêmeo que aparece completo já no primeiro frame.
Opções do MQTT 5 que Melhoram Gêmeos com Estado Retido
O MQTT 5 acrescenta controles que tornam o estado retido mais preciso:
- Message expiry interval: defina uma expiração em propriedades voláteis para que um valor retido desapareça se não for atualizado, em vez de exibir para sempre uma leitura desatualizada.
- Retain handling na assinatura:
0envia as retained messages a cada assinatura,1apenas para novas assinaturas,2nunca. Use2para consumidores que só querem mudanças ao vivo. - Retain as published: permite que uma bridge ou relay saiba se a mensagem original era retida.
No MQTT.js, uma assinatura com { qos: 1, rh: 2 } ignora as retained messages, e publicar com { retain: true, properties: { messageExpiryInterval: 300 } } faz um valor expirar após cinco minutos. Veja recursos do MQTT 5 para mais detalhes.
Armadilhas e Solução de Problemas
- Propriedades fantasmas: topics renomeados deixam valores retidos antigos para trás. Apague-os explicitamente.
- Comandos retidos: nunca retenha topics de comando; os dispositivos vão reexecutá-los ao se reconectar.
- Valores desatualizados exibidos como ao vivo: inclua timestamps e respeite o topic de status.
- Payloads retidos grandes demais: mantenha cada valor pequeno; o servidor MQTT público aceita até 64 KB por mensagem e o plano Serverless gratuito, 256 KB.
- Sem snapshot atômico: se várias propriedades precisam mudar juntas, publique-as como um único documento JSON retido.
- Reinícios do broker: planeje para que o estado retido seja republicado pelos dispositivos na reconexão, em vez de presumir que ele sobrevive para sempre a reinícios do broker. Dispositivos que publicam o estado completo ao se conectar tornam o gêmeo autorrecuperável.
Quando Topics Retidos Não São Suficientes
Topics retidos são ideais para o estado atual. Acrescente outros componentes quando precisar de histórico (um assinante de séries temporais), consultas entre ativos ou controle de acesso por gêmeo (um serviço de gêmeo como o Eclipse Ditto com MQTT) ou tratamento de desejado vs reportado em escala. Para o padrão desejado/reportado com topics retidos, leia sincronização de estado de gêmeo digital.
Rode Seu Gêmeo com Estado Retido no ConnectMQ
Para ativos reais, saia do servidor MQTT público e vá para um broker privado no ConnectMQ, um broker MQTT gerenciado com MQTT 3.1.1 e 5.0, retained messages, expiração de mensagens e TLS. Substitua o host pelo do seu broker, acrescente YOUR_USERNAME, YOUR_PASSWORD e YOUR_CLIENT_ID obtidos no portal e mude para a porta 8883, ou 8084 para navegadores. Cadastre-se em app.connectmq.com no plano Serverless gratuito, sem cartão de crédito, e acompanhe os topics retidos do seu gêmeo no explorador de topics ao vivo.
Perguntas frequentes
O que é uma retained message no MQTT?
É uma mensagem que o broker armazena como o último valor conhecido do seu topic e entrega imediatamente a qualquer novo assinante desse topic.
Como apagar uma retained message MQTT?
Publique um payload de tamanho zero com a flag retain no mesmo topic, por exemplo mosquitto_pub -t <topic> -r -n.
Cada propriedade do gêmeo deve ter seu próprio topic retido?
Funciona bem para propriedades independentes e atualizações pequenas. Use um documento JSON retido por ativo quando vários valores precisarem mudar juntos de forma atômica.
Retained messages podem substituir um banco de dados em um gêmeo digital?
Para o estado atual, muitas vezes sim. Para histórico, consultas entre ativos ou trilhas de auditoria, acrescente um assinante que grave em banco de dados ao lado do broker.



