⚡
VITORNMS
>Início>Projetos>Bancada>Notas>Blog>Contato
GitHubLinkedIn
status: construindo
>Início>Projetos>Bancada>Notas>Blog>Contato
status: construindo
VITORNMS logoVITORNMS

Transformando ideias em soluções inovadoras de software & hardware. Construindo o futuro, um commit de cada vez.

Navegação

>Início>Projetos>Bancada>Blog>Contato

Contato

vitornms@gmail.com+55 31 98415-2360Belo Horizonte, MG, Brasil
enviar um sinal→
Language
Forjado com& código

© 2026 VITORNMS — Todos os experimentos reservados

back to blog
iotfeatured

ESP32 + LoRa: Comunicação de Longo Alcance para IoT

A frustração de tentar fazer a horta 'falar' com a casa. Como 915 MHz, paredes e árvores transformaram um projeto simples num ecossistema distribuído de 3 nós autônomos.

VN

Vitor Neuenschwander

CS Student & Developer

Jan 15, 202516 min
#esp32#lora#iot#c++#hardware

O Desafio: Fazer a Horta "Falar" com a Casa

Tudo começou com uma ideia simples: monitorar a umidade do solo da horta e acionar a irrigação automaticamente. Mas entre a horta e a casa havia paredes, árvores e uns bons 50 metros de distância. WiFi? Bluetooth? Nenhum chegava.

Foi aí que descobri o LoRa — Long Range, uma modulação sem fio que promete quilômetros de alcance com consumo mínimo. Parecia perfeito. Não era tão simples.

O projeto que começou como "um sensor e um motor" se transformou no BioChallenge2025-LIPS: um ecossistema distribuído de 3 nós (Jardim, Fogo, Central) com ~1.500 linhas de C++ e um protocolo de comunicação inventado do zero.


O Hardware: Três Nós, Três Missões

O ecossistema BioChallenge tem três dispositivos que conversam entre si via LoRa a 915 MHz:

┌─────────────────────────────────────────────────────────┐
│                    ECOSSISTEMA LIPS                      │
├─────────────────┬──────────────────┬────────────────────┤
│   NÓ JARDIM     │   NÓ FOGO        │   CENTRAL          │
│   648 linhas    │   465 linhas     │   411 linhas       │
│                 │                  │                    │
│ Umidade Solo ×2 │ Gás MQ-2         │ Receptor LoRa      │
│ Motor DC ×2     │ Chama KY-026     │ Servidor Web       │
│ OLED 128×64     │ OLED 128×64      │ OLED 128×64        │
│ LoRa TX/RX      │ LoRa TX/RX       │ LoRa RX + API REST │
└─────────────────┴──────────────────┴────────────────────┘

Nó Jardim (Irrigação)

O coração do sistema. Lê a umidade do solo com 2 sensores analógicos (ADC GPIO36 e GPIO37) e, se estiver abaixo do limiar calibrado, aciona 2 motores DC de irrigação (GPIO34, GPIO35, GPIO38, GPIO39).

// Leitura dos sensores de umidade
float umidade1 = analogRead(GPIO36);
float umidade2 = analogRead(GPIO37);

// Calibração: o limiar é definido na inicialização
if (umidade1 < LIMIAR_UMIDO) {
  digitalWrite(MOTOR1_A, HIGH);  // Liga irrigação
  digitalWrite(MOTOR1_B, LOW);
}

Nó Fogo (Segurança)

Sensor de gás MQ-2 (analógico GPIO36) detecta fumo e gases inflamáveis. O sensor de chama KY-026 (digital GPIO2 com INPUT_PULLUP) funciona com lógica invertida: LOW = chama detectada. Quando algo é detectado, transmite imediatamente via LoRa — aqui não há sleep.

Central (Gateway)

Recebe dados de todos os nós, exibe no OLED em tempo real, e serve uma API REST via HTTP na porta 80 usando ESPAsyncWebServer. IP estático: 192.168.0.65.


A Dança do Handshake: Quem é o Chefe?

O primeiro momento "Aha!" foi perceber que a topologia não podia ser rígida. E se a central caísse? Os nós ficariam cegos, sem interface web, sem acesso remoto.

A solução: negociação dinâmica Mestre/Escravo. Quando um nó liga, ele manda um PING_CENTRAL. Se não recebe um ACK_CENTRAL de volta em 5 segundos, ele mesmo sobe um servidor web e assume o papel de gateway!

// Negociação: quem responde primeiro é a Central
LoRa.beginPacket();
LoRa.print("PING_CENTRAL");
LoRa.endPacket();

unsigned long timeout = millis() + 5000;
while (millis() < timeout) {
  int packetSize = LoRa.parsePacket();
  if (packetSize > 0) {
    String response = LoRa.readString();
    if (response == "ACK_CENTRAL") {
      isCentral = false;  // Já existe central
      return;
    }
  }
}
// Ninguém respondeu — EU sou a central agora
startWebServer();
isCentral = true;

Isso significa que qualquer nó pode virar gateway — Jardim, Fogo, tanto faz. Se a Central "oficial" cair, o sistema se reorganiza sozinho. Resiliência zero-config.


Packet Sniffing Caseiro: Diferenciando Dados pelo sizeof()

A Central recebe pacotes LoRa de vários nós. Mas como sabe se é umidade do jardim ou um alarme de fogo? Em vez de cabeçalhos complexos ou JSON (impossível em LoRa raw), usei uma técnica em C++ elegantemente brutal: diferenciação por tamanho do pacote em bytes.

Cada nó envia uma struct diferente. Como JardimData tem campos diferentes de GasData, seus tamanhos em bytes são diferentes:

// Structs com tamanhos distintos em memória
struct JardimData {
  float umidade[2];   // 8 bytes
};

struct GasData {
  float gas_level;     // 4 bytes
  float flame_status;  // 4 bytes (+ padding = tamanho diferente)
};

// Na Central: diferenciação pelo sizeof
int packetSize = LoRa.parsePacket();

if (packetSize == sizeof(JardimData)) {
  JardimData dados;
  LoRa.readBytes((uint8_t*)&dados, sizeof(dados));
  // Atualiza dashboard de irrigação
  atualizaOled("Jardim", dados.umidade[0], dados.umidade[1]);
} else if (packetSize == sizeof(GasData)) {
  GasData dados;
  LoRa.readBytes((uint8_t*)&dados, sizeof(dados));
  if (dados.flame_status == 1.0) {
    // ALARME! Chama detectada!
    triggerAlarm();
  }
}

Zero overhead de parsing. Sem strings, sem JSON, sem delimitadores. Apenas bytes contados pelo compilador. Eficiência máxima num microcontrolador com 520 KB de RAM.


API REST sobre LittleFS: Web Server Embarcado

A Central (ou qualquer nó que assuma o papel) sobe um servidor HTTP assíncrono na porta 80. Os arquivos HTML são servidos via LittleFS — um filesystem embarcado na flash do ESP32.

// Endpoints da API REST
// GET /         → Página HTML (servida do LittleFS)
// GET /umidade  → JSON com leituras de umidade
// GET /gasflame → JSON com dados de gás e chama

server.on("/umidade", HTTP_GET, [](AsyncWebServerRequest *request) {
  String json = "{";
  json += "\"sensor1\": " + String(umidade[0]) + ",";
  json += "\"sensor2\": " + String(umidade[1]);
  json += "}";
  request->send(200, "application/json", json);
});

server.on("/gasflame", HTTP_GET, [](AsyncWebServerRequest *request) {
  String json = "{";
  json += "\"gas_level\": " + String(gasData.gas_level) + ",";
  json += "\"flame_status\": " + String(gasData.flame_status);
  json += "}";
  request->send(200, "application/json", json);
});

Com isso, qualquer dispositivo na rede WiFi local pode abrir http://192.168.0.65/umidade e ver os dados em tempo real — celular, notebook, outro ESP32.


O Paradoxo da Energia: Bateria que Dura Meses

"Como fazer uma bateria durar meses num ESP32 que consome 240 mA em operação?"

A resposta: Deep Sleep sincronizado com NTP. O sensor acorda, pergunta as horas a um servidor NTP, e toma decisões inteligentes:

  • 18h–08h → Volta a dormir até de manhã (plantas não precisam de irrigação à noite)
  • 08h–18h → Mede, transmite, dorme 2 horas
  • Wi-Fi falhando? → Fallback de 5 minutos de sono curto
  • Nó como componente LoRa? → Ciclo de 1 minuto (sem WiFi overhead)
// Gestão de energia inteligente
time_t now = time(nullptr);
struct tm *timeinfo = localtime(&now);
int hora = timeinfo->tm_hour;

if (hora >= 18 || hora < 8) {
  // Noite: dormir até as 08h do dia seguinte
  int horasAteManha = (hora >= 18) ? (24 - hora + 8) : (8 - hora);
  esp_deep_sleep(horasAteManha * 3600ULL * 1000000ULL);
} else {
  // Dia: dormir 2 horas
  esp_deep_sleep(2 * 3600ULL * 1000000ULL);
}

No Deep Sleep, o ESP32 consome apenas ~10 µA. A diferença entre 1 dia de bateria e 6 meses.


As Bibliotecas: O Ecossistema C++

Cada nó carrega uma combinação destas bibliotecas:

  • WiFi.h — Conexão à rede local (modo STA)
  • AsyncTCP + ESPAsyncWebServer — Servidor HTTP não-bloqueante
  • LittleFS — Filesystem na flash para servir HTML
  • LoRa — Comunicação SX1276 a 915 MHz / 14 dBm
  • Adafruit_SSD1306 + Adafruit_GFX — Display OLED I2C (SDA GPIO4, SCL GPIO15)
  • esp_sleep.h + time.h — Deep sleep e sincronização NTP

Testes de Alcance

Resultados com antena padrão (sem amplificador externo), Spreading Factor 12:

  • Campo aberto: ~3 km
  • Ambiente urbano: ~800 m com obstáculos
  • Dentro de prédio: ~200 m (2 paredes de concreto)
  • Potência de transmissão: 14 dBm

O que Levei deste Projeto

LoRa não é WiFi — é lento (~300 bps), limitado, e não serve para streaming. Mas para IoT é imbatível. Este projeto me ensinou três coisas:

  1. Protocolos simples são protocolos bons — sizeof() como diferenciador de pacotes é genial na sua brutalidade
  2. Resiliência > Perfeição — O handshake dinâmico faz qualquer nó virar gateway. Nenhum ponto único de falha
  3. Energia é o verdadeiro boss fight — A melhor feature do projeto não é o LoRa, é o deep sleep de 10 µA sincronizado com NTP
Contents
share
share: