Integração MQTT

Guia passo a passo para autenticação, envio de telemetria e controle de dispositivos no SimpIoT.

Integração MQTT

O SimpIoT utiliza o protocolo MQTT (Message Queuing Telemetry Transport) como padrão para a comunicação entre dispositivos de borda e a plataforma. Este guia detalha o fluxo de integração necessário para que seu hardware opere de forma segura e eficiente.


1. Configurações de Autenticação

Cada dispositivo possui credenciais exclusivas geradas durante o provisionamento no painel administrativo. A autenticação é obrigatória e garante o isolamento lógico do seu projeto.

  • Username: Deve ser preenchido com o seu mqttClientId.
  • Password: Deve ser preenchido com o seu mqttSecretId.
  • Client ID: Recomenda-se o uso do próprio mqttClientId para identificação única da sessão.

2. Primeira Conexão e LWT (Last Will and Testament)

A plataforma exige que o firmware seja o responsável por declarar seu estado de disponibilidade. É obrigatório configurar a mensagem de Last Will and Testament (LWT) no momento da conexão para que o sistema detecte quedas inesperadas de sinal.

  • Tópico LWT: {mqttClientId}/telemetry/state
  • Payload LWT:
    {
    	"deviceType": "self",
    	"data": {
    		"state": {
    			"status": "OFFLINE"
    		}
    	}
    }
    

    Nota Importante: Caso não envie dtState, o servidor do SimpIoT registrará automaticamente o momento de recebimento da mensagem.

Exemplo de conexão (ESP32)

Exemplo em ESP32 com PubSubClient + ArduinoJson. A LWT é registrada no próprio connect(), como mensagem retida (retained) no tópico de estado, e ao conectar o dispositivo publica ONLINE e se inscreve no tópico de comandos:

#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>

const char* MQTT_CLIENT_ID = "<mqttClientId>";
const char* MQTT_SECRET    = "<mqttSecret>";

WiFiClientSecure espClient;
PubSubClient client(espClient);

String topicState = String(MQTT_CLIENT_ID) + "/telemetry/state";
String topicCmd   = String(MQTT_CLIENT_ID) + "/cmd";

String buildStatePayload(const char* status) {
  JsonDocument doc;
  doc["deviceType"] = "self";
  doc["data"]["state"]["status"] = status;
  String out;
  serializeJson(doc, out);
  return out;
}

void setupMqtt() {
  espClient.setInsecure();  // TLS na porta 8883
  client.setServer("mqtt.simpiot.com.br", 8883);
  client.setKeepAlive(60);
  client.setBufferSize(1024);
}

void reconnect() {
  String lwt = buildStatePayload("OFFLINE");  // payload da LWT

  while (!client.connected()) {
    // connect(clientId, user, pass, willTopic, willQoS, willRetain, willMessage)
    bool ok = client.connect(
      MQTT_CLIENT_ID, MQTT_CLIENT_ID, MQTT_SECRET,
      topicState.c_str(), 1, true, lwt.c_str()
    );

    if (ok) {
      // anuncia presença (retido) e escuta comandos
      client.publish(topicState.c_str(), buildStatePayload("ONLINE").c_str(), true);
      client.subscribe(topicCmd.c_str(), 1);
    } else {
      delay(5000);
    }
  }
}

Sobre o setInsecure(): ele estabelece uma conexão TLS criptografada, mas não valida o certificado do servidor contra uma CA raiz. Usamos isso por enquanto para simplificar a primeira integração — assim você não precisa embarcar nem manter atualizado o certificado raiz no firmware. O tráfego continua cifrado e a autenticação por mqttClientId + secret permanece válida. Para produção, recomenda-se validar o certificado do broker — veja Conexão segura (validando o certificado) logo abaixo.


2.1. Conexão segura (validando o certificado)

A porta 8883 já usa TLS — o tráfego é sempre criptografado. A diferença entre setInsecure() e validar o certificado é se o dispositivo também confirma que está mesmo falando com o broker do SimpIoT (e não com um impostor).

Para validar, o dispositivo precisa conhecer a CA raiz que assina o certificado do broker. O broker do SimpIoT usa Let’s Encrypt, cuja raiz é a ISRG Root X1 — um certificado público, igual para todos os dispositivos e válido até 2035.

Por que a renovação do servidor não exige atualizar o firmware: o certificado do broker é renovado automaticamente a cada ~60 dias, mas ele continua sendo assinado pela mesma ISRG Root X1. A raiz embarcada no dispositivo só precisaria mudar lá por 2035 (ou se o SimpIoT trocar de CA) — por isso é prática recomendada manter um mecanismo de atualização remota (OTA) no firmware.

Certificado raiz (ISRG Root X1)

Embarque este certificado no seu dispositivo. É o mesmo para todos:

-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----

Quer confirmar você mesmo? Rode openssl s_client -connect mqtt.simpiot.com.br:8883 -CAfile isrgrootx1.pem — se aparecer Verify return code: 0 (ok), a raiz acima valida o broker.

Arduino / ESP32

No firmware Arduino você escolhe entre os dois modos. Guarde o certificado acima em uma constante (ex: const char* SIMPIOT_ROOT_CA = R"EOF( ... )EOF";):

// Opção A — desenvolvimento: TLS sem validar o certificado do servidor
espClient.setInsecure();

// Opção B — produção: valida o broker com a raiz pública do SimpIoT
espClient.setCACert(SIMPIOT_ROOT_CA);

Importante: a validação confere a validade do certificado, então o relógio precisa estar correto. Inicialize o NTP no setup() antes da primeira conexão: configTime(0, 0, "south-america.pool.ntp.org").

ESPHome

O ESPHome não possui um modo equivalente ao setInsecure() — fornecer a CA raiz é obrigatório para usar a porta 8883. Informe-a em certificate_authority (e adicione o componente time:, necessário para a validação):

time:
  - platform: sntp
    servers:
      - south-america.pool.ntp.org

mqtt:
  broker: mqtt.simpiot.com.br
  port: 8883
  username: !secret mqtt_client_id
  password: !secret mqtt_secret
  client_id: !secret mqtt_client_id
  certificate_authority: |
    -----BEGIN CERTIFICATE-----
    MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
    ... (cole o certificado completo da seção acima) ...
    emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
    -----END CERTIFICATE-----

Dica: o certificado é longo — para não poluir a config, coloque-o no secrets.yaml (mqtt_ca_cert: | ...) e use certificate_authority: !secret mqtt_ca_cert.


3. Enviando Status de Dispositivo

O dispositivo deve reportar transições de estado para alimentar o histórico operacional e disparar automações.

  • Tópico: {mqttClientId}/telemetry/state
  • Estados Suportados: ONLINE, OFFLINE, MEASURING, ERROR, MAINTENANCE.

Exemplo de Payload:

{
	"deviceType": "self",
	"data": {
		"state": {
			"status": "ONLINE",
			"battery": 88,
			"signal": -62
		},
		"dtState": "2026-05-13T09:49:00Z"
	}
}

O campo dtState é opcional. Se informado (ISO 8601 UTC), ajuda a manter maior sincronização com o instante real do evento no dispositivo. Se omitido, o backend usa o timestamp de recebimento.

Exemplo montando o payload de estado com ArduinoJson e publicando com QoS 1 e flag retained:

void publishState(const char* status) {
  JsonDocument doc;
  doc["deviceType"] = "self";
  doc["data"]["state"]["status"] = status;

  String dt = localTime();             // ISO 8601 UTC (ver seção 4)
  if (dt.length() > 0) {
    doc["data"]["dtState"] = dt;
  }

  String out;
  serializeJson(doc, out);
  client.publish(topicState.c_str(), out.c_str(), true);  // retained
}

4. Enviando Medidas (Telemetria)

Este é o fluxo principal de dados, onde as leituras dos sensores são enviadas para persistência e visualização em dashboards.

  • Tópico: {mqttClientId}/telemetry/measure

4.1 Dispositivo Autônomo (self)

Use deviceType: "self" quando o dispositivo conectado ao broker é o próprio coletor das leituras.

{
	"deviceType": "self",
	"measurements": [
		{
			"sensor": "temperature",
			"ch": 1,
			"value": 23.4,
			"dtMeasure": "2026-05-13T09:49:00Z"
		},
		{
			"sensor": "humidity",
			"ch": 1,
			"value": 68.0,
			"dtMeasure": "2026-05-13T09:49:00Z"
		}
	]
}

Você pode enviar várias leituras de sensores diferentes no mesmo publish — basta adicionar mais objetos ao array measurements.

4.2 Gateway com Múltiplos Nós (gateway)

Quando um gateway concentra dados de vários nós subordinados, ele publica no próprio tópico MQTT ({gatewayClientId}/telemetry/measure) com deviceType: "gateway". Cada leitura no array identifica o nó de origem pelo campo nodeId.

Importante: o nodeId de cada nó é definido durante o provisionamento no painel, na seção de dispositivos do cenário. Todos os nós referenciados precisam estar cadastrados como subordinados ao gateway que está publicando.

Exemplo — gateway agregando leituras de dois nós:

{
	"deviceType": "gateway",
	"measurements": [
		{
			"nodeId": "node-sala",
			"sensor": "temperature",
			"ch": 1,
			"value": 24.1,
			"dtMeasure": "2026-05-16T12:00:00Z"
		},
		{
			"nodeId": "node-sala",
			"sensor": "humidity",
			"ch": 1,
			"value": 65.0,
			"dtMeasure": "2026-05-16T12:00:00Z"
		},
		{
			"nodeId": "node-externo",
			"sensor": "temperature",
			"ch": 1,
			"value": 31.7,
			"dtMeasure": "2026-05-16T12:00:05Z"
		}
	]
}

Neste exemplo, o gateway envia em um único publish três leituras: temperatura e umidade do nó node-sala, e temperatura do nó node-externo. A plataforma desagrupa cada entrada e persiste as medidas vinculadas ao respectivo nó.

4.3 Referência dos Campos

CampoTipoObrigatórioDescrição
sensorstringSimChave do tipo de sensor cadastrado no dispositivo (ex: "temperature", "humidity")
chinteiroSimCanal do sensor no dispositivo — começa em 1
valuenúmeroSimValor da leitura (inteiro ou decimal)
dtMeasurestring ISO 8601 UTCNãoTimestamp da leitura. Se omitido, o backend usa o momento de recebimento
nodeIdstringApenas gatewayIdentificador do nó que originou a leitura, conforme cadastro no painel

4.4 Exemplo de Envio (ESP32)

Exemplo em ESP32 com ArduinoJson montando o array measurements a partir das leituras dos sensores. Note que entry["sensor"] recebe a key da categoria cadastrada no Modelo de Dispositivo — é o que vincula cada leitura ao sensor correto:

// O timestamp é obtido via NTP em formato ISO 8601 UTC
String localTime() {
  struct tm timeinfo;
  if (!getLocalTime(&timeinfo)) return "";
  char buf[25];
  strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%SZ", &timeinfo);
  return String(buf);
}

void sendMeasurements() {
  String dt = localTime();

  JsonDocument doc;
  doc["deviceType"] = "self";
  JsonArray arr = doc["measurements"].to<JsonArray>();

  // uma entrada por leitura — `sensor` é a `key` do Modelo de Dispositivo
  JsonObject temp = arr.add<JsonObject>();
  temp["sensor"]    = "temperature";
  temp["ch"]        = 1;
  temp["value"]     = 23.4;
  temp["dtMeasure"] = dt;

  JsonObject hum = arr.add<JsonObject>();
  hum["sensor"]    = "humidity";
  hum["ch"]        = 1;
  hum["value"]     = 68.0;
  hum["dtMeasure"] = dt;

  char buf[512];
  serializeJson(doc, buf);
  client.publish(topicMeasure.c_str(), buf);  // {clientId}/telemetry/measure
}

Para usar localTime(), inicialize o NTP uma vez no setup() com configTime(0, 0, "south-america.pool.ntp.org").


5. Recebendo Comandos e Retornando ACK

O sistema de comandos é bidirecional e exige uma confirmação ativa do dispositivo para garantir que a instrução foi executada com sucesso.

  • Tópico para Inscrição (CMD): {mqttClientId}/cmd
  • Tópico para Resposta (ACK): {mqttClientId}/cmd/ack

A Importância do ACK

Ao enviar um comando, a plataforma o marca como SENT. O dispositivo precisa retornar um ACK (Acknowledgement). Sem essa resposta, o sistema não confirmará a conclusão e a requisição passará para o status de TIMEOUT após o período configurado.

ACK de dispositivo autônomo ou do próprio gateway:

{
	"msgId": "id-recebido-no-header",
	"nodeId": null,
	"status": "COMPLETED",
	"responseMessage": "Relé acionado com sucesso",
	"error": null
}

ACK de um nó enviado através do gateway:

Quando o comando foi direcionado a um nó específico, o gateway deve preencher nodeId com o identificador do nó que executou o comando:

{
	"msgId": "id-recebido-no-header",
	"nodeId": "node-sala",
	"status": "COMPLETED",
	"responseMessage": "Atuador acionado",
	"error": null
}

Utilize COMPLETED para sucesso ou FAILED para erros no hardware. O campo error pode conter uma mensagem descritiva em caso de falha.

Exemplo de Tratamento (ESP32)

Exemplo do callback que recebe o comando no tópico {mqttClientId}/cmd, executa a ação e publica o ACK em {mqttClientId}/cmd/ack:

void publishAck(const String& msgId, bool success, const char* message) {
  JsonDocument doc;
  doc["msgId"]           = msgId;
  doc["nodeId"]          = nullptr;
  doc["status"]          = success ? "COMPLETED" : "FAILED";
  doc["responseMessage"] = message;
  doc["error"]           = nullptr;

  char buf[256];
  serializeJson(doc, buf);
  client.publish(topicCmdAck.c_str(), buf);  // {clientId}/cmd/ack
}

// registrado via client.setCallback(callback)
void callback(char* topic, byte* payload, unsigned int length) {
  if (strcmp(topic, topicCmd.c_str()) != 0) return;

  JsonDocument doc;
  if (deserializeJson(doc, payload, length)) return;  // JSON inválido

  String msgId = doc["header"]["msgId"] | "";
  String cmd   = doc["header"]["cmd"]   | "";

  if (cmd == "start_measure") {
    // ... inicia a rotina de medição ...
    publishAck(msgId, true, "Medicao iniciada");
  } else if (cmd == "stop_measure") {
    // ... para a rotina de medição ...
    publishAck(msgId, true, "Medicao parada");
  } else {
    publishAck(msgId, false, "Comando desconhecido");
  }
}

QoS e Confiabilidade

Para garantir a integridade dos dados sob condições instáveis de rede, utilize as configurações de QoS (Quality of Service) recomendadas:

FluxoQoS RecomendadoDescrição
Telemetria0Entrega rápida de medidas constantes.
Estados / LWT1Garante que mudanças de status sejam registradas.
Comandos1Essencial para garantir a entrega da instrução.
ACK1Garante que o painel receba a confirmação do comando.