ESP32 e Arduino

ESP32 FreeRTOS MQTT: Tarefas Separadas de Sensor e Publicação com Filas

Divida a amostragem do sensor e a publicação MQTT em duas tarefas FreeRTOS no ESP32, conecte-as com uma fila e mantenha as leituras fluindo mesmo enquanto o Wi-Fi ou o broker reconecta.

Diagrama de duas tarefas FreeRTOS no ESP32 passando leituras de sensor por uma fila até um publicador MQTT em uma configuração esp32 freertos mqtt
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

Um projeto ESP32 FreeRTOS MQTT coloca a amostragem do sensor e a publicação na rede em tarefas separadas, para que uma reconexão lenta do Wi-Fi nunca atrase uma leitura. Neste tutorial, você vai montar um sketch com uma tarefa de sensor, uma tarefa MQTT e uma fila entre elas, testá-lo na área pública gratuita do broker.connectmq.com e depois passá-lo para TLS com suas próprias credenciais.

Por que usar tarefas FreeRTOS para MQTT no ESP32

A maioria dos sketches de iniciante faz tudo dentro do loop(): lê o sensor, verifica o Wi-Fi, chama client.loop(), publica e depois delay(). Isso funciona até a rede se comportar mal. Uma chamada connect() para um broker inacessível pode bloquear por vários segundos e, nesse tempo, nada é amostrado. Se você está contando pulsos, tirando a média de um sinal analógico ou monitorando um limite, essas lacunas importam.

O core Arduino do ESP32 já roda sobre o FreeRTOS. O próprio loop() é apenas uma tarefa fixada no core 1. Nada impede que você crie mais tarefas e, ao fazer isso, o tempo de um trabalho deixa de depender do outro:

  • A tarefa de sensor acorda em um período fixo com vTaskDelayUntil(), lê o hardware e coloca uma pequena struct em uma fila. Ela nunca toca na rede.
  • A tarefa MQTT é dona do Wi-Fi e do cliente MQTT. Ela esvazia a fila, publica, cuida dos keep-alives e reconecta quando necessário.
  • A fila é o único objeto compartilhado. As filas do FreeRTOS copiam os dados e são seguras para uso entre tarefas, então você não precisa de mutex para as leituras.

Há uma regra que torna esse design robusto: apenas uma tarefa chama o cliente MQTT. O PubSubClient não é thread-safe, e chamar publish() de duas tarefas ao mesmo tempo corrompe seu buffer interno. Em vez disso, canalize tudo pela fila.

O que você precisa

  • Qualquer placa ESP32 com dois núcleos (ESP32 DevKit, ESP32-S3). Chips de um núcleo, como ESP32-C3 ou ESP32-C6, também funcionam se você fixar as duas tarefas no core 0.
  • Arduino IDE 2.x ou PlatformIO com o core esp32 3.x.
  • A biblioteca PubSubClient de Nick O'Leary.
  • Opcional: qualquer sensor. O sketch simula temperatura e umidade, então roda em uma placa sem nada conectado.
  • mosquitto_sub no seu computador para testes.

Organização e prioridades das tarefas

Tarefa Core Prioridade Stack Função
sensorTask 1 3 3072 bytes Amostrar a cada 1 s, enviar para a fila
mqttTask 0 2 6144 bytes Wi-Fi, conexão MQTT, publicação, keep-alive
loop() do Arduino 1 1 padrão Excluída, não é necessária

A tarefa de sensor recebe a prioridade mais alta porque é curta e crítica em tempo; ela roda, enfileira um item e dorme. A tarefa MQTT roda no core 0 junto com a pilha Wi-Fi, que também fica lá, e faz o trabalho lento. No core Arduino do ESP32, os tamanhos de stack são informados em bytes, e não em words como no FreeRTOS original.

O sketch ESP32 FreeRTOS MQTT completo

Substitua as credenciais de Wi-Fi e a parte aleatória do prefixo de topic, depois faça o upload.

#include <Arduino.h>
#include <WiFi.h>
#include <PubSubClient.h>

// ---------- configuração ----------
const char* WIFI_SSID     = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 1883;
const char* TOPIC_PREFIX  = "connectmq/demo/k3v9q1";   // torne isto único

// ---------- objetos compartilhados ----------
struct Reading {
  uint32_t seq;
  uint32_t uptimeMs;
  float    temperature;
  float    humidity;
};

QueueHandle_t readingQueue;
WiFiClient    net;
PubSubClient  mqtt(net);
String        clientId;

// ---------- tarefa de sensor ----------
void sensorTask(void* param) {
  const TickType_t period = pdMS_TO_TICKS(1000);
  TickType_t lastWake = xTaskGetTickCount();
  uint32_t seq = 0;

  for (;;) {
    Reading r;
    r.seq         = seq++;
    r.uptimeMs    = millis();
    // Substitua pela leitura de um sensor real (DHT22, BME280, ADC...)
    r.temperature = 21.0f + (esp_random() % 400) / 100.0f;
    r.humidity    = 45.0f + (esp_random() % 1000) / 100.0f;

    // Não bloquear: se a fila estiver cheia, descarta o item mais antigo e tenta de novo
    if (xQueueSend(readingQueue, &r, 0) != pdTRUE) {
      Reading discarded;
      xQueueReceive(readingQueue, &discarded, 0);
      xQueueSend(readingQueue, &r, 0);
    }
    vTaskDelayUntil(&lastWake, period);
  }
}

// ---------- auxiliares MQTT (chamados apenas pela mqttTask) ----------
bool ensureWifi() {
  if (WiFi.status() == WL_CONNECTED) return true;
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) {
    vTaskDelay(pdMS_TO_TICKS(250));
  }
  return WiFi.status() == WL_CONNECTED;
}

bool ensureMqtt() {
  if (mqtt.connected()) return true;
  String willTopic = String(TOPIC_PREFIX) + "/status";
  bool ok = mqtt.connect(clientId.c_str(), willTopic.c_str(), 1, true, "offline");
  if (ok) {
    mqtt.publish(willTopic.c_str(), "online", true);
  }
  return ok;
}

// ---------- tarefa MQTT ----------
void mqttTask(void* param) {
  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setBufferSize(512);
  String topic = String(TOPIC_PREFIX) + "/telemetry";
  uint32_t backoffMs = 1000;

  for (;;) {
    if (!ensureWifi() || !ensureMqtt()) {
      vTaskDelay(pdMS_TO_TICKS(backoffMs));
      backoffMs = min<uint32_t>(backoffMs * 2, 30000);
      continue;
    }
    backoffMs = 1000;
    mqtt.loop();

    Reading r;
    // Espera brevemente por dados para que a tarefa libere a CPU quando ociosa
    if (xQueueReceive(readingQueue, &r, pdMS_TO_TICKS(50)) == pdTRUE) {
      char payload[160];
      snprintf(payload, sizeof(payload),
               "{\"seq\":%lu,\"uptimeMs\":%lu,\"temperature\":%.2f,\"humidity\":%.2f,\"queued\":%u}",
               (unsigned long)r.seq, (unsigned long)r.uptimeMs,
               r.temperature, r.humidity,
               (unsigned)uxQueueMessagesWaiting(readingQueue));
      if (!mqtt.publish(topic.c_str(), payload)) {
        // Devolve ao início da fila e tenta de novo após reconectar
        xQueueSendToFront(readingQueue, &r, 0);
      }
    }
  }
}

void setup() {
  Serial.begin(115200);
  clientId = "esp32-rtos-" + String((uint32_t)ESP.getEfuseMac(), HEX);

  readingQueue = xQueueCreate(60, sizeof(Reading));   // um minuto de buffer
  if (readingQueue == nullptr) {
    Serial.println("Queue allocation failed");
    while (true) delay(1000);
  }

  xTaskCreatePinnedToCore(sensorTask, "sensor", 3072, nullptr, 3, nullptr, 1);
  xTaskCreatePinnedToCore(mqttTask,   "mqtt",   6144, nullptr, 2, nullptr, 0);
}

void loop() {
  vTaskDelete(nullptr);   // a tarefa loop do Arduino não é necessária
}

Acompanhe a saída pelo seu computador:

mosquitto_sub -h broker.connectmq.com -p 1883 -i sub-k3v9q1 -t "connectmq/demo/k3v9q1/#" -v

Você deve ver uma mensagem status com online e uma mensagem de telemetria por segundo. Agora desligue o roteador por vinte segundos. O campo queued cresce enquanto a placa está offline e, quando a conexão volta, a tarefa MQTT esvazia o acúmulo em ordem. Esse é o objetivo do design: as leituras foram feitas no horário certo, mesmo com a publicação parada.

A área pública do broker pode ser lida por qualquer pessoa, então mantenha os dados de demonstração genéricos. Ela também limita cada cliente a 10 mensagens por segundo, o que o esvaziamento de um acúmulo pode atingir. Se você armazena muitas leituras, adicione um breve vTaskDelay(pdMS_TO_TICKS(120)) após cada publicação enquanto uxQueueMessagesWaiting() estiver alto. O guia do servidor MQTT público (gratuito) lista os outros limites.

Como as peças funcionam

A fila como buffer

xQueueCreate(60, sizeof(Reading)) aloca 60 slots de tamanho fixo no heap. Cada xQueueSend copia a struct de 16 bytes, então a tarefa de sensor pode reutilizar sua variável local imediatamente. Dimensionar a fila é um equilíbrio entre RAM e a duração da queda que você quer suportar. Sessenta slots de 16 bytes ocupam menos de 1 KB; até alguns milhares de slots cabem com facilidade em um ESP32 clássico, mas lembre-se de que os dados se perdem em uma reinicialização.

Quando a fila está cheia, o sketch descarta a leitura mais antiga, e não a mais nova. Para telemetria, o valor mais recente costuma ser o mais útil. Se cada amostra importa, bloqueie por alguns ticks ou agregue as leituras (mínimo, máximo, média) antes de enfileirar.

vTaskDelayUntil versus delay

vTaskDelayUntil() acorda a tarefa em uma taxa fixa medida a partir do último despertar, então o período não deriva pelo tempo que a leitura do sensor levou. Um delay(1000) simples depois de uma leitura de 30 ms resulta em um período de 1,03 s que vai escorregando aos poucos. As duas funções liberam a CPU, ao contrário de um loop ocupado com millis().

Reconexão com backoff

A tarefa MQTT tenta novamente com backoff exponencial limitado a 30 segundos. Bombardear o broker com tentativas imediatas desperdiça energia e, em um broker MQTT gerenciado, pode acionar a proteção contra flood ou o bloqueio de IP por falhas repetidas. O guia de Last Will MQTT no ESP32 explica por que a mensagem will offline retida é registrada no momento da conexão, para que os dashboards percebam um dispositivo inativo.

Armadilhas e solução de problemas

  1. Guru Meditation "Stack canary watchpoint triggered". O stack da tarefa é pequeno demais. snprintf com floats e TLS precisam de folga. Imprima uxTaskGetStackHighWaterMark(nullptr) dentro de cada tarefa durante o desenvolvimento; ele retorna o mínimo de bytes livres já observado. Mantenha pelo menos 512 bytes de sobra.
  2. Resets do task watchdog. Uma tarefa que roda em loop sem nunca bloquear priva a tarefa idle do seu core. Toda iteração precisa chamar algo que ceda a CPU: vTaskDelay, xQueueReceive com timeout ou uma chamada de socket bloqueante.
  3. Saída serial embaralhada. Duas tarefas imprimindo ao mesmo tempo intercalam caracteres. Imprima de uma única tarefa ou proteja o Serial com um mutex criado por xSemaphoreCreateMutex().
  4. Publicar a partir da tarefa de sensor. Vai parecer que funciona e depois falhar aleatoriamente sob carga. Mantenha o cliente MQTT em uma única tarefa.
  5. Callbacks rodam na tarefa MQTT. Se você assina comandos, o callback do PubSubClient roda dentro de mqtt.loop(). Não faça trabalho lento ali; coloque o comando em uma segunda fila para outra tarefa tratar.
  6. JSON grande truncado. O buffer padrão do PubSubClient é de 256 bytes, incluindo o topic. O sketch o aumenta com setBufferSize(512).

Migrando para TLS e seu próprio broker

O servidor MQTT público é ideal para testes e protótipos, mas dispositivos em produção devem autenticar e criptografar. Crie uma conta gratuita em app.connectmq.com, crie um broker e copie o usuário, a senha e o client ID gerados. Depois, altere apenas a parte de rede da tarefa MQTT:

#include <WiFiClientSecure.h>

// ISRG Root X1, a raiz do Let's Encrypt. Cole o PEM publicado pelo Let's Encrypt.
static const char ROOT_CA[] = R"PEM(
-----BEGIN CERTIFICATE-----
...paste ISRG Root X1 here...
-----END CERTIFICATE-----
)PEM";

WiFiClientSecure net;
PubSubClient mqtt(net);

// na mqttTask, antes do loop:
net.setCACert(ROOT_CA);
mqtt.setServer("broker.connectmq.com", 8883);

// em ensureMqtt():
mqtt.connect("YOUR_CLIENT_ID", "YOUR_USERNAME", "YOUR_PASSWORD",
             willTopic.c_str(), 1, true, "offline");

Aumente o stack da tarefa MQTT para cerca de 8192 bytes, porque o handshake TLS usa bem mais stack e heap. No ConnectMQ, o client ID fica vinculado às credenciais emitidas, então use exatamente o que o portal fornecer. Quando os dados estiverem fluindo, abra o explorador de topics ao vivo no portal para confirmar que as mensagens chegam e crie uma regra de alerta por limite no campo de temperatura.

O FreeRTOS no ESP32 é documentado em detalhes na referência FreeRTOS do ESP-IDF, incluindo as diferenças de SMP em relação ao FreeRTOS original. Para uma visão mais ampla de projetos com dispositivos, o tutorial de IoT com ESP32 reúne o restante desta série, e adicionar timestamps NTP à struct Reading é um próximo passo natural para que as amostras armazenadas carreguem o horário real de captura.

Comece com o ConnectMQ

Um sketch baseado em tarefas é apenas metade de um sistema confiável; a outra metade é um broker que permanece no ar enquanto seus dispositivos reconectam. O ConnectMQ é um broker MQTT gerenciado com MQTT 3.1.1 e 5.0, TLS na porta 8883 e um portal que mostra seus dispositivos e topics ao vivo. Comece no plano gratuito Serverless sem cartão de crédito e depois migre para um plano pago ou dedicado quando sua frota passar de 25 conexões. Com o firmware estável, o mesmo padrão funciona para sensores reais, como no projeto ESP32 com DHT22.

Perguntas frequentes

Posso chamar o PubSubClient de mais de uma tarefa FreeRTOS?

Não com segurança. O PubSubClient tem um único buffer compartilhado e nenhum bloqueio, então chamadas concorrentes corrompem os pacotes. Mantenha-o em uma tarefa e envie dados a ela por meio de uma fila.

Em qual core a tarefa MQTT deve rodar?

O core 0 é um bom padrão, porque as tarefas de Wi-Fi e lwIP já rodam nele. Em chips de um núcleo como ESP32-C3 ou ESP32-C6, fixe todas as tarefas no core 0 ou use xTaskCreate sem fixação.

Qual deve ser o tamanho da fila de leituras?

Dimensione-a para a maior queda que você quer cobrir. Multiplique a taxa de amostragem pela duração da queda e pelo tamanho da struct, e mantenha o total bem abaixo do heap livre informado por ESP.getFreeHeap().

Isso funciona com o AsyncMqttClient?

Sim, mas o AsyncMqttClient já roda sua própria tarefa de rede e callbacks, então você só precisa da tarefa de sensor e de uma fila. Publique a partir da tarefa que esvazia a fila, e não de dentro dos callbacks da biblioteca.

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.