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
mqttClientIdpara 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 pormqttClientId+ 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 aparecerVerify 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 usecertificate_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
nodeIdde 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
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
sensor | string | Sim | Chave do tipo de sensor cadastrado no dispositivo (ex: "temperature", "humidity") |
ch | inteiro | Sim | Canal do sensor no dispositivo — começa em 1 |
value | número | Sim | Valor da leitura (inteiro ou decimal) |
dtMeasure | string ISO 8601 UTC | Não | Timestamp da leitura. Se omitido, o backend usa o momento de recebimento |
nodeId | string | Apenas gateway | Identificador 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 nosetup()comconfigTime(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:
| Fluxo | QoS Recomendado | Descrição |
|---|---|---|
| Telemetria | 0 | Entrega rápida de medidas constantes. |
| Estados / LWT | 1 | Garante que mudanças de status sejam registradas. |
| Comandos | 1 | Essencial para garantir a entrega da instrução. |
| ACK | 1 | Garante que o painel receba a confirmação do comando. |