IoT

Rede LoRa Integrada com Web Server

Eletrogate 25 de julho de 2024

Introdução

No post Rede ESP-NOW integrada com Web Server, apresentamos o cenário específico com estações coletoras comunicando com o nó central via o protocolo ESP-NOW e servindo numa rede local como Web Server.

Neste post, apresentaremos um cenário similar utilizando o protocolo LORA (Long Range) para a comunicação entre as estações, mantendo a funcionalidade como Web Server. Utilizaremos o módulo LORA, modelo E32- 433T20D, para transmitir e receber os dados de temperatura, umidade, pressão e altitude coletados através do sensor BME-280.

Utilizaremos o módulo E32-433T20D no seu Modo Transparente, ou seja, com a configuração default que vem de fábrica. Normalmente, na configuração default, o módulo é usado para a comunicação ponto a ponto. Faremos uma prova de conceito como cenário inicial para demonstrar que o Modo Transparente pode ser usado na comunicação multiponto, tornando a implementação mais simples, sem a necessidade de configuração de cada módulo para atender uma rede mais complexa.

Existe um artigo no Blog da Eletrogate, Monitoramento de Temperatura com Heltec ESP32 LoRa, que explora diversos aspectos e cenários da Rede Lora, abordando os conceitos e pré-requisitos para a implementação. Inclusive, o artigo mostra a utilização de uma placa integrada com ESP32 + Lora + Display OLED da empresa Heltec. Portanto, sugerimos fortemente ao leitor dar uma estudada no artigo mencionado.

No nosso exemplo, utilizaremos as placas ESP32 e Arduino NANO bastante conhecidas no mercado com o módulo Lora em separado.


Bibliotecas

Antes de iniciar a programação, é preciso fazer a instalação das placas da Espressif e das seguintes bibliotecas:

  • ESPAsyncWebServer (disponível no GitHub do desenvolvedor);
  • Adafruit_Sensor;
  • Adafruit_BME280;
  • SoftwareSerial;
  • HardwareSerial;
  • ESPmDNS;
  • WiFi;
  • Wire;
  • ArduinoJson

Com exceção da biblioteca ESPAsyncWebServer, você encontra as demais no próprio Gerenciador de Bibliotecas da Arduino IDE, sem contar que algumas, como as relacionadas ao Wi-Fi, são instaladas juntamente com o pacote de placas da Espressif.

Se tem dúvidas na instalação das placas, siga nosso tutorial de instalação.


O que é LoRa?

LoRa é a abreviação para “long range”. Basicamente, essa é uma tecnologia de radiofrequência sem fio de longo alcance e baixa potência, sendo ideal para a adoção de sistemas relacionados à Internet das Coisas (IoT). Ela utiliza uma técnica de modulação de espectro de propagação de chirp (CSS), o que a torna atrativa para aplicações IoT.

Propósito e Aplicações da Rede LoRa:

  • Baixo Consumo Energético: A rede LoRa é flexível e atende bem tanto locais bastante povoados quanto áreas rurais, onde o sinal de celular não tem alcance. Sua capacidade de transmitir informações é baixa, mas isso a torna útil em aplicações com poucos dispositivos IoT concentrados e baixa taxa de transmissão de dados.
  • Aplicações Diversas: A LoRa é utilizada em diversos setores, como agricultura, áreas hospitalares, cidades inteligentes e logística de negócios. Ela é especialmente útil quando você precisa de uma solução com ótimo alcance, mas que não dependa de um volume significativo de informações em tempo real.

Resumo:

  • Se você busca uma solução de baixa dependência de provedores e com excelente alcance, a LoRa pode ser uma boa escolha. Caso contrário, outras opções massivamente disponíveis, como ESP-NOW, 4G e Wi-Fi, podem ser mais adequadas.
  • A proposta da Rede LORA é ter o alcance maior do que a Rede ESP-NOW (sem depender de provedores ou concessionárias) passando do patamar de 220 m à faixa de 1 km até 10 km, dependendo do modelo do módulo escolhido e o uso de antenas apropriadas (lembrando que existem diversos modelos com potências diferentes no mercado). O modulo escolhido para este post se propõe a atingir o patamar de 3 km.

Sobre o Módulo E32-433T20D

O E32-433T20D é um módulo de comunicação sem fio da série E32, operando na frequência de 433 MHz, com capacidade de transmissão e recepção, potência de 20 dBm e outras características específicas. Ele é amplamente utilizado em projetos de IoT, automação e redes de sensores. O tamanho do pacote trafegado deve ser até 58 bytes.

  1. E32: Este é o prefixo do módulo e se refere à série de módulos de comunicação sem fio da fabricante Ebyte. O “E32” indica que o módulo pertence a essa série específica.
  2. 433: Esse número representa a frequência de operação do módulo. No caso do E32-433T20D, ele opera na faixa de 433 MHz. Essa frequência é comumente usada para comunicação sem fio em sistemas de controle remoto, sensores e redes de longo alcance.
  1. T20D: Essa parte do nome contém informações adicionais sobre o módulo:
    • “T”: Indica que o módulo possui transceptor, ou seja, ele pode tanto transmitir quanto receber dados.
    • “20”: Refere-se à potência de transmissão do módulo, que é de 20 dBm (ou 100 mW). Isso influencia o alcance da comunicação (cerca de 3 Km).
    • “D”: Refere-se ao tipo de antena, no caso, SMA-K.

 

Uso da Frequência de 433 Mhz no Brasil:

Embora o Brasil adote oficialmente a frequência de 915 MHz para comunicações sem fio, o uso da faixa de 433 MHz não apresenta problemas significativos para aplicações locais. A Agência Nacional de Telecomunicações (ANATEL) regula o espectro de radiofrequência no país, e embora o 433 MHz não seja uma banda ISM (Industrial, Scientific and Medical), ele é amplamente utilizado em dispositivos de controle remoto industrial e sistemas de automação. Essa popularidade se deve ao fato de que, embora não seja uma faixa licenciada, não há falta de dispositivos operando nessa frequência no Brasil. Portanto, para aplicações locais de baixa potência e alcance limitado, o uso do 433 MHz pode ser uma alternativa viável sem depender de aprovação específica da ANATEL. No entanto, caso você esteja desenvolvendo um produto a ser comercializado, o uso da frequência 915 Mhz é mandatório.

Os pinos M0 e M1 são para a configuração do modo de operação do módulo (4 opções). O Modo Transparente, mencionado anteriormente, considera os pinos M0=M1=aterrados e utiliza somente os pinos RXD, TXD, VCC e GND numa comunicação serial comum e mais simples, usando a biblioteca SoftwareSerial (Arduino NANO) e HardwareSerial (ESP32) sem depender da biblioteca específica do fabricante, que é mais complexa de usar pois foi desenvolvida para atender a todas as funcionalidades do módulo.


Sobre o Módulo BME-280

O BME-280 é um sensor ambiental integrado desenvolvido especialmente para aplicações móveis. Ele combina sensores de alta linearidade e alta precisão para medição de umidade relativa, pressão barométrica e temperatura ambiente. O tamanho compacto e o baixo consumo de energia tornam o BME-280 ideal para projetos onde esses fatores são essenciais. A tensão de alimentação está na faixa de 1.8 – 5V DC. Normalmente, a versão com comunicação I2C é mais utilizada no mercado.

Propósito e Aplicações do BME-280:

  • Medição de Umidade e Pressão: O BME-280 fornece leituras precisas de umidade relativa e pressão atmosférica. Isso é útil em aplicações como previsão do tempo, monitoramento ambiental e controle de umidade.
  • Estimativa de Altitude: Como a pressão varia com a altitude, o BME-280 também permite estimar a altitude em relação ao nível do mar.
  • Baixo Consumo de Energia: Sua eficiência energética é crucial para dispositivos portáteis, como smartwatches, rastreadores de fitness e wearables em geral.
  • Contexto Consciente: Com sua resposta rápida, o BME-280 atende aos requisitos de consciência de contexto em aplicações emergentes.


Prova de Conceito do Modo Transparente com multiponto

Utilizaremos três estações (1 x ESP32 + 2 x Arduino NANO). Cada estação terá um LED, o módulo E32-433T20D e um PUSH BUTTON. Quando o botão for pressionado, o estado do LED será invertido e a estação transmitirá o comando ON ou OFF (dependendo do novo estado do LED) pela Rede LORA, como se fosse um broadcast, para que as demais estações sincronizem com o mesmo estado do LED da estação emissora.

Componentes usados:

 

Diagrama do Circuito para o ESP32

Diagrama do circuito para o Arduino NANO

Pontos de máxima atenção sobre o módulo E32-433T20d:

  1. A tensão de alimentação do E32-433T20D é de 5V (de 2,3V a 5,2V, nunca permanecendo no limite superior por muito tempo pois pode danificar o módulo), mas o nível de comunicação é em 3,3V (de 2,5V a 3,6V). Por isso, usamos um divisor de tensão entre o TX do Arduino NANO com o RXD do módulo usando dois resistores (R330 + R470). No ESP32 não teremos tal preocupação pois a tensão de operação já é 3,3V.
  2. Nunca energize o E32-433T20D sem antena pois pode queimar o módulo. Há diversas referências a este fato em vídeos na Internet. Instale a antena antes de fazer as ligações.

Fizemos um teste de alcance com os circuitos da Prova de Conceito. Distanciamos uma estação da outra por cerca de 2.155 m com visada direta aproveitando um vale que existe na região que moro, conforme pode ser visto na imagem obtida pelo Google Earth a seguir. Infelizmente, não conseguimos avançar até os 3.000 m especificado no datasheet do módulo pois começamos a descer em relação ao topo e com isso a visada passou a não ser direta e a comunicação não foi possível.

Demonstração da Prova de Conceito

 

A seguir, apresentamos o código usado na Prova de Conceito para o ESP32/NANO:

//-------------------------------------------------------------------------------------------
// Função : Este sketch tem como objetivo implementar um nó da Rede Lora para acionar um LED 
//          transmitindo ON ou OFF para os demais participantes da rede seguirem. 
//          O LED é acionado através de um PUSH BUTTON.
//
// Autor  : Dailton Menezes
//
// Versão : V1.0 Jun/2024
//
// Componentes : 1) 1 x módulo Lora E32 433T20D com a configuração default
//               2) 1 x LED + 1 x Resistor 330 Ohms
//               3) 1 x PUSH BUTTON em PULL-UP
//               4) 1 x Protoboard 400 pontos
//               5) 1 x Arduino Nano
//               6) Jumpers diversos 
//
// Observações : 1) Usaremos um divisor de tensão para reduzir 5V para 3,18V usando um
//                  R1=330 Ohms e R2=470 Ohms => Vsaída = Ventrada*[R2/(R1+R2)]
//               2) O TX do NANO será ligado ao RX do Módulo passando pelo divisor pois
//                  pois a tensão da parte lógica do módulo é 3,3V até o limite de 3,6V.
//
// Referências : BME280
//               1) https://randomnerdtutorials.com/bme280-sensor-arduino-pressure-temperature-humidity/
//               2) https://randomnerdtutorials.com/esp32-bme280-arduino-ide-pressure-temperature-humidity/
//               3) https://microcontrollerslab.com/bme280-esp32-display-values-oled-arduino-ide/
//------------------------------------------------------------------------------------------

#define BAUDRATE          9600            // Baudarate do interface Lora

//----------------------------
// Inclusão das bibliotecas
//----------------------------

#ifdef ARDUINO_AVR_NANO || ARDUINO_AVR_UNO
  #include <SoftwareSerial.h>
#elif defined(ESP32)
  #include <HardwareSerial.h>
#endif

//----------------------------
// Definições dos Pinos Usados
//----------------------------

#ifdef ARDUINO_AVR_NANO || ARDUINO_AVR_UNO
  #define pinRX    2
  #define pinTX    3
  #define pinBot   4
  #define pinLed   5
#elif defined(ESP32)
  #define pinRX    16
  #define pinTX    17
  #define pinBot   12
  #define pinLed   14
#endif

//----------------------------
// Definições de Variáveis
//----------------------------

bool estadoLed = false;
String comando[2] = {"OFF", "ON"};

//---------------------------
// Objeto para Comunicar
//---------------------------

#ifdef ARDUINO_AVR_NANO || ARDUINO_AVR_UNO
  SoftwareSerial lora(pinRX, pinTX);
#endif

void acionaLed(bool estado)
{
   estadoLed = estado;
   digitalWrite(pinLed,estadoLed);
}

void setup() 
{

   // Inicializa da Serial

   Serial.begin(BAUDRATE);
   while (!Serial) ;

   // Inicializa o Módulo Lora

#ifdef ARDUINO_AVR_NANO || ARDUINO_AVR_UNO
   lora.begin(BAUDRATE);
#elif ESP32
   Serial1.begin(BAUDRATE, SERIAL_8N1, pinRX, pinTX);
#endif

   // Inicializa o Botão

   pinMode(pinBot, INPUT_PULLUP);
   
   // Inicializa o LED 

   pinMode(pinLed, OUTPUT);
   acionaLed(estadoLed);

   // Hello na Console

   Serial.println("Transmissor Lora V1.0 Jun/2024");
   Serial.println("Pressione o botão para ligar/desligar o Led e enviar o comando para rede Lora...");

}

void loop() 
{

   // Verifica se pressionou o botão

   if ( digitalRead(pinBot) == LOW )
   {
      // Alterna o estado do Led e Aciona o Led
  
      acionaLed(!estadoLed);

      // Envia o comando para a Rede Lora

#ifdef ARDUINO_AVR_NANO || ARDUINO_AVR_UNO
      lora.print(comando[estadoLed]);
#elif ESP32   
      Serial1.print(comando[estadoLed]);   
#endif       
      Serial.print("Comando transmitido: ");
      Serial.println(comando[estadoLed]); 
      delay(250);  

   }   

   // Verifica se chegou comando da Rede Lora

#ifdef ARDUINO_AVR_NANO || ARDUINO_AVR_UNO
   if ( lora.available() ) 
#elif ESP32
   if ( Serial1.available() > 0 )
#endif   
   {
#ifdef ARDUINO_AVR_NANO || ARDUINO_AVR_UNO    
      String comando = lora.readString();
#elif ESP32
      String comando = Serial1.readString();
#endif        
      Serial.print("Comando recebido: ");
      Serial.println(comando);

      if (comando.equalsIgnoreCase("OFF")) acionaLed(false);
      else  if (comando.equalsIgnoreCase("ON")) acionaLed(true);
      else Serial.println("Comando inválido...");
   }

}

Detalhes da Implementação com Web Server

Utilizaremos no nó central/receptor o ESP32 30 pinos e dois Arduinos NANO como satélites/transmissores. Todas as estações serão dotadas de um módulo LORA E32-433T20D e um sensor BME-280. O obtivo é coletar os dados de temperatura, umidade, pressão atmosférica e a altitude aproximada através das três estações. O ESP32 terá a função estendida para desempenhar os papeis de coletor, receptor geral e publicador do conteúdo numa página HTML através da rede local usando o serviço de Web Server na porta 80. Os dois Arduinos NANO apenas coletarão os dados e enviarão pela interface LORA.

Componentes usados:

Teremos dois tipos de comunicação:

  1. Entre os Arduinos NANO e o ESP32 usando a comunicação serial através do módulo LORA.
  2. Entre o ESP32 e a página WEB usando a comunicação através de web socket.

Os pacotes a serem trafegados estarão no formato JSON, conforme layout a seguir:

Ex:  {“id”:2,”t”:”24″,”u”:”45″,”p”:”923″,”a”:”803″}

 Onde:

id  => identificação da estação que enviou
t    => temperatura em °C
u   => umidade em %
p   => pressão atmosférica em hPa
a   => altitude em m

 

Observações:

  • O formato JSON tende a aumentar o número de bytes do pacote por causa dos caracteres da sintaxe. Como temos o limite de 58 bytes da Rede LORA, tivemos que usar abreviaturas reduzidas no JSON para as variáveis, visando economizar espaço.
  • Cada estação deverá ter um ID único, definido em tempo de compilação dos sketch’s.
  • O ID será usado pelo JAVASCRIPT para localizar e atualizar os dados no painel correspondente ao ID no Navegador.
  • No Javascript do HTML, há a criação do evento que escutará o envio feito pelo nó central. Desta forma, não há necessidade de dar REFRESH na página a cada atualização de dados. Criaremos um painel por linha no HTML para representar os dados de cada nó, como se fosse uma lista de painéis dentro do container principal da página web (veja a Figura). Na chegada dos dados de um nó, apenas o painel correspondente será atualizado. Isso otimiza o processo e alivia o ESP32 de REFRESH constantes. Não existe limitação para o número de nós representados no HTML, pois teremos cada nó em uma linha de representação dentro do navegador.
  • Adicionaremos ao DNS da rede o HOSTNAME = LORAWEB para a estação ESP32, para não ser necessário descobrir o IP ao acessar a página HTML publicada. Bastará utilizar a URL http://loraweb.local  no Navegador. O sufixo .local utilizado significa que o nome LORAWEB vai ser procurado apenas na rede local.
  • Relembramos que a tensão de alimentação do E32-433T20D é de 5V (de 2,3V a 5,2V nunca permanecendo no limite superior por muito tempo pois pode danificar o módulo), mas o nível de comunicação é em 3,3V (de 2,5V a 3,6V). Por isso, usamos um divisor de tensão entre o TX do Arduino NANO com o RXD do módulo usando dois resistores (R330 + R470). No ESP32 não teremos tal preocupação pois a tensão de operação já é 3,3V.
  • O sensor BMW-280 pode ser encontrado no mercado com diferentes configurações. No nosso caso, estamos usando o modelo que tem a tensão de alimentação entre 1.8 – 5V DC e o endereço I2C = 0x76. No Arduino NANO, alimentaremos em 5V e no ESP32 com 3,3V.
  • O Modo Transparente permite que o módulo E32-433T20D transmita e receba dados, ou seja, todas as estações receberão os pacotes transmitidos pelas demais estações (broadcast) . Para as estações transmissoras, a recepção de dados será ignorada, ou seja, receberemos e simplesmente mostraremos na console.

HTML renderizado no Celular

HTML renderizado no Desktop

Circuito do Arduino NANO em Bancada

Diagrama do Circuito do Arduino NANO

Circuito do ESP32 em Bancada

 

Diagrama do Circuito do ESP32

Demonstração da Rede LORA com Web Server

 

Código para o Nó Transmissor (Arduino NANO)

//-------------------------------------------------------------------------------------------
// Função : Este sketch tem como objetivo implementar um nó da Rede Lora para transmitir 
//          dados de um sensor BME280 (temperatura, umidade, pressão e altitude) para o nó 
//          central ESP32 que publicará os dados recebidos como Web Server na porta 80. O
//          nó central também coletará dados do mesmo sensor.
//
// Autor  : Dailton Menezes
//
// Versão : V1.0 Jun/2024
//
// Componentes : 1) 1 x Módulo Lora E32 433T20D com a configuração default
//               2) 1 x Módulo BME280
//               3) 1 x Arduino Nano ou UNO
//               4) 1 x Resistor de 330 Ohms
//               5) 1 x Resistor de 470 Ohms
//               6) Jumpers diversos 
//
// Observações : 1) Usaremos um divisor de tensão para reduzir 5V para 3,18V usando um
//                  R1=330 Ohms e R2=470 Ohms => Vsaída = Ventrada*[R2/(R1+R2)]
//               2) O TX do NANO será ligado ao RX do Módulo passando pelo divisor pois
//                  pois a tensão da parte lógica do módulo é 3,3V até o limite de 3,6V.
//               3) O pacote a transmitir será no formato JSON, bem otimizado por causa
//                  restrição do tamanho do pacote do módulo E32-433T20D Lora que permite
//                  58 bytes. 
//                  Ex:  {"id":2,"t":"24","u":"51","p":"925","a":"786"}
//                  Onde id => identificação da estação que enviou
//                       t  => temperatura em *C
//                       u  => umidade em %
//                       p  => pressão atmomosférica em hPa 
//                       a  => altitude em m
//               4) O módulo BME280 usado neste projeto usa o endereço 0x76 da interface I2C
//
// Referências : 1) https://randomnerdtutorials.com/bme280-sensor-arduino-pressure-temperature-humidity/
//               2) https://www.youtube.com/watch?v=fhfKB7Tzm4c
//------------------------------------------------------------------------------------------

#define BAUDRATE_LORA     9600           // Baudarate do interface Lora
#define BAUDRATE_SERIAL   115200         // Baudarate do interface Lora
#define VARREDURA         5000           // Intervalo de varredura em msec 
#define BME280_ADDR       0x76           // Endereço I2C do BME280 (depende do módulo adquirido)
#define ALARME            500            // Tempo para piscar Led como alarme
#define TAXA_ERRO_ALT     2.75F          // Taxa de erro da Altitude 2,75%
#define SEALEVELPRESSURE_HPA (1013.25)   // Valor de referência para o nível do mar
#define ESTACAO_ID        2              // <<Atenção>> : Informa o N. da Estação de Coleta.  
                                         // Deve ser diferente para cada nó da rede

//------------------------------
// Inclusão das bibliotecas
//------------------------------

#include <Wire.h>                        // Biblioteca de comunicação I2C
#include <Adafruit_Sensor.h>             // Biblioteca pré-requisito para o BME280
#include <Adafruit_BME280.h>             // Biblioteca para tratar o sensor BME280
#include <SoftwareSerial.h>              // Biblioteca paara a comunicação serial
#include <ArduinoJson.h>                 // Biblioteca JSON para comunicação e parãmetros

//------------------------------
// Definições dos Pinos Usados
//------------------------------

#define pinRX    2                       // Pino RX para a comunicação serial
#define pinTX    3                       // Pino TX para a comunicação serial

//-------------------------------
// Objeto para Comunicação Serial
//-------------------------------

SoftwareSerial lora(pinRX, pinTX);       // Para a comunicação serial

//-------------------------------
// Objeto para tratar o BME280
//-------------------------------

Adafruit_BME280 bme;                     // I2C

//-------------------------------
// Variáveis utilizadas
//-------------------------------

unsigned long ultimaVarredura=0;         // ùltima coleta de dados

//-------------------------------
// Prototipação de Rotinas
//-------------------------------

void coleta_imprime_envia();                         // Rotina para coletar os dados e transmitir 
String formataValor(float valor, int size, int dec); // Rotina para formatar um número 

//--------------------------------
// Inicialização Geral do Programa
//--------------------------------

void setup() 
{

   // Inicializa da Serial

   Serial.begin(BAUDRATE_SERIAL);
   while (!Serial) ;

   // Inicializa o Led Interno

   pinMode(LED_BUILTIN, OUTPUT);

   // Inicializa o Módulo Lora

   lora.begin(BAUDRATE_LORA);

  // Inicializa o BME280. Atenção: este BMW280 veio configurado na porta x76.
  // É importante saber qual o endereço do seu módulo para alterar na linha seguinte

  if (!bme.begin(BME280_ADDR, &Wire)) 
  {
     Serial.println("Módulo BME280 não encontrado. Verifique o endereço ou a ligação das portas");
     while (1)
     {
        // Pisca o LED Interno para alertar para o erro
       
        digitalWrite(LED_BUILTIN, HIGH);
        delay(ALARME);
        digitalWrite(LED_BUILTIN, LOW);
        delay(ALARME);
     }
  }   

   // Hello na Console

   Serial.print("Transmissor Lora V1.0 Jun/2024 - Estação ");
   Serial.println(ESTACAO_ID);
   Serial.println();

}

//--------------------------------
// Loop para coletar e enviar 
//--------------------------------

void loop() 
{

   // Verifica se chegou dados da Rede Lora

   if ( lora.available() ) 
   {
      // Vamos simplesmente ler e imprimir pois não trataremos os dados 
      // que chegaram pois somos apenas transmissores. 
      
      String dados = lora.readString();
      Serial.print("Dados recebidos: ");
      Serial.println(dados);
      Serial.println();
   }

   // Verifica se deve fazer a coleta de dados 

   if (millis()-ultimaVarredura > VARREDURA)
   {
      coleta_imprime_envia();
   }

}

//--------------------------------
// Rotina para ler os dados do 
// BME280 e enviar pela interface
// Lora
//--------------------------------

void coleta_imprime_envia()
{

   JsonDocument json;                    // Para conter os dados a enviar
   float temp;                           // Temperatura lida
   float umid;                           // Umidade lida
   float pressure;                       // Pressão atmosférica lida
   float altitude;                       // Altitude aproximada inferida através da pressão

   // Define a estação que enviará os dados

   json["id"] = ESTACAO_ID;

   // Trata a Temperatura sem casas decimais
   
   temp = bme.readTemperature();
   Serial.print("Temperature = ");
   Serial.print(temp,0);
   Serial.println(" *C");
   json["t"] = formataValor(temp,3,0);

   // Trata a Umidade sem casas decimais
   
   umid = bme.readHumidity(); 
   Serial.print("Humidity = ");
   Serial.print(umid,0);
   Serial.println(" %");
   json["u"] = formataValor(umid,3,0);

   // Trata a Pressão sem casas decimais
   
   pressure = bme.readPressure()/100.0F;
   Serial.print("Pressure = ");
   Serial.print(pressure,0);
   Serial.println(" hPa");
   json["p"] = formataValor(pressure,5,0);

   // Trata a Pressão sem casas decimais
   
   altitude = bme.readAltitude(SEALEVELPRESSURE_HPA)/(1-TAXA_ERRO_ALT/100); 
   Serial.print("Approx. Altitude = ");
   Serial.print(altitude,0);
   Serial.println(" m");
   json["a"] = formataValor(altitude,5,0);

   // Dá uma linha de espaço
   
   Serial.println();

   // Serializando o JSON sem formatação por economia por causa 
   // do limite de 58 bytes da rede LORA
   
   String jsonStr;
   serializeJson(json, jsonStr); 

   // Enviando o JSON para console

   Serial.print("Transmitindo: ");
   Serial.println(jsonStr); 
   Serial.print("Tamanho: ");
   Serial.println(jsonStr.length()); 

   // Dá uma linha de espaço 

   Serial.println();

   // Envia pela Interface LORA

   lora.print(jsonStr);

   // Atualiza a última varredura

   ultimaVarredura = millis();
   
}

//--------------------------------
// Rotina para formatar um número
//--------------------------------

String formataValor(float valor, int tam, int dec)
{
   char buf[tam+2];                    // Buffer par aformatação
   String result;                      // Resultado depois da formatação

   dtostrf(valor, tam, dec, buf); 
   result = String(buf);
   result.trim();
   return result;
   
}

Código para o Nó Receptor (ESP32)

//-------------------------------------------------------------------------------------------
// Função : Este sketch tem como objetivo implementar um nó central da Rede Lora para receber 
//          dados enviados pelas estações transmissoras contendo (temperatura, umidade, 
//          pressão e altitude) obtidos através de um sensor BME280. As informações das 
//          estações serão mostradas num html via Web Server escutando na porta 80.
//
// Autor  : Dailton Menezes
//
// Versão : V1.0 Jun/2024
//
// Componentes : 1) 1 x Módulo Lora E32 433T20D com a configuração default
//               2) 1 x Módulo BME280
//               3) 1 x ESP32
//               4) Jumpers diversos 
//
// Observações : 1) O pacote a receber virá no formato JSON, bem otimizado por causa
//                  restrição do tamanho do pacote do módulo E32-433T20D Lora que permite
//                  58 bytes. 
//                  Ex:  {"id":2,"t":"24","u":"51","p":"925","a":"786"}
//                  Onde id => identificação da estação que enviou
//                       t  => temperatura em *C
//                       u  => umidade em %
//                       p  => pressão atmomosférica em hPa 
//                       a  => altitude em m
//               2) Este nó central também coletará dados do BME280 mas só enviará para 
//                  a página html.
//
// Referências : 1) https://randomnerdtutorials.com/esp32-bme280-arduino-ide-pressure-temperature-humidity/
//               2) https://randomnerdtutorials.com/esp32-web-server-with-bme280-mini-weather-station/
//------------------------------------------------------------------------------------------

#define LED_BUILTIN       2              // Pino para o Led Interno do ESP32/ESP8266
#define BAUDRATE_LORA     9600           // Baudarate do interface Lora
#define BAUDRATE_SERIAL   115200         // Baudarate do interface Lora
#define VARREDURA         5000           // Intervalo de varredura em msec 
#define BME280_ADDR       0x76           // Endereço I2C do BME280 (depende do módulo adquirido)
#define ALARME            500            // Tempo para piscar Led como alarme
#define TAXA_ERRO_ALT     2.75F          // Taxa de erro da Altitude 2,75%
#define SEALEVELPRESSURE_HPA (1013.25)   // Valor de referência para o nível do mar
#define ESTACAO_ID        0              // <<Atenção>> : Informa o N. da Estação de Coleta.  
                                         // O nó cenytral será o de n. zero.
#define NOME_ESTACAO      "LORAWEB"      // Nome desta Estação para efeito de DNS

//------------------------------
// Inclusão das bibliotecas
//------------------------------

#include <Wire.h>                        // Biblioteca de comunicação I2C
#include <Adafruit_Sensor.h>             // Biblioteca pré-requisito para o BME280
#include <Adafruit_BME280.h>             // Biblioteca para tratar o sensor BME280
#include <HardwareSerial.h>              // Biblioteca paara a comunicação serial
#include <ArduinoJson.h>                 // Biblioteca JSON para comunicação e parãmetros
#include <ESPAsyncWebServer.h>           // Biblioteca Asynch Web Server
#include <ESPmDNS.h>                     // Biblioteca para inclusão do hostname no mDNS

//------------------------------
// Definições dos Pinos Usados
//------------------------------

#define pinRX    16                      // Pino RX para a comunicação serial
#define pinTX    17                      // Pino TX para a comunicação serial

//------------------------------------------------------
// Ajuste as linhas abaixo para a sua credencial de rede
//------------------------------------------------------

const char* ssid     = "<não se esqueça de informar o SSID de sua Rede Wi-Fi>";
const char* password = "<não se esqueça de informar s SENHA de sua Rede Wi-Fi>";

//-------------------------------
// Objeto para tratar o BME280
//-------------------------------

Adafruit_BME280 bme;                     // I2C

//-------------------------------
// Variáveis utilizadas
//-------------------------------

unsigned long ultimaVarredura=0;         // ùltima coleta de dados

//------------------------------------
// Define o HTML para Página Principal
//------------------------------------

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LORA Data</title>
    <style>
       header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;}
//       body {
//           font-family: Arial, sans-serif;
//           background-color: #f0f0f0;
//           margin: 0;
//           padding: 0;
//           display: flex;
//           justify-content: center;
//           align-items: center;
//           height: 100vh;
//       }
       .container {
           width: 100%;
           max-width: 600px;
           margin: 20px;
       }
       .panel {
           background-color: white;
           border-radius: 8px;
           box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
           margin-bottom: 10px;
           padding: 10px;
       }
       .panel .header {
           font-size: 14px;
//           font-weight: bold;
           color: #888;
           margin-bottom: 10px;
       }
       .panel .content {
           font-size: 24px;
           margin-bottom: 10px;
       }
       .panel .timestamp {
           font-size: 14px;
           color: #888;
       }   
       .blue-light { color: lightblue; }
       .blue-dark { color: darkblue; }
       .green { color: green; }
       .red-light { color: lightcoral; }
       .red-dark { color: darkred; }
       .orange { color: orange; }
       .yellow { color: yellow; }           
    </style>       
</head>
<body>
    <header>
      <h2>Receptor LORA Web</h2>
    </header>
    <div id="transmitter-data"></div>

    <script>

       // Função para determinar a cor da Temperatura
       
       function getTemperatureClass(temp) 
       {
            if (temp < 10) return 'blue-light';
            if (temp < 20) return 'blue-dark';
            if (temp < 25) return 'green';
            if (temp < 30) return 'red-light';
            return 'red-dark';
        }

        // Função para determinar a cor da Umidade
        
        function getHumidityClass(humid) 
        {
            if (humid <= 15) return 'red-dark';
            if (humid <= 30) return 'orange';
            if (humid <= 55) return 'blue-light';
            return 'green';
        }

        // Função para determinar a cor da Pressão/Altitude
        
        function getPressureClass(pressure) 
        {
            if (pressure > 1013) return 'red-dark';
            if (pressure > 800)  return 'green';
            return 'blue-dark';
        }

        if (!!window.EventSource) 
        {
           // Lista para manter controle dos elementos por ID
           var transmitterElements = {}; 

           // Evento para receber os dados
           var source = new EventSource('/events');
           
           source.addEventListener('open', function(e) {
            console.log("Events Connected");
           }, false);
           source.addEventListener('error', function(e) {
            if (e.target.readyState != EventSource.OPEN) {
              console.log("Events Disconnected");
            }
           }, false);
           
           source.addEventListener('message', function(e) {
            console.log("message", e.data);
           }, false);
           
           source.addEventListener('new_readings', function(e) {
            console.log("new_readings", e.data);
            // Obtenha os dados do evento
            var eventData = JSON.parse(e.data);
            var idEstacao   = eventData.id;
            var temp        = eventData.t;
            var umid        = eventData.u;
            var pressure    = eventData.p;
            var altitude    = eventData.a;
            var timestamp   = new Date().toLocaleString();

            // Obtenha as cores para a temperatura e umidade
            var tempClass = getTemperatureClass(temp);
            var humidClass = getHumidityClass(umid);
            var pressureClass = getPressureClass(pressure);

            // Verifique se o elemento para este ID já existe na lista
            if (transmitterElements[idEstacao]) {
                // Atualizar o conteúdo do painel existente
                var contentElement = transmitterElements[idEstacao].querySelector('.content');
                var timestampElement = transmitterElements[idEstacao].querySelector('.timestamp');

                contentElement.innerHTML = `Temp: <span class="${tempClass}">${temp}°C</span>, Umid: <span class="${humidClass}">${umid}%</span>, Press: <span class="${pressureClass}">${pressure} hPa</span>, Alt&asymp; <span class="${pressureClass}">${altitude} m</span>`;
                timestampElement.innerHTML = `Timestamp: ${timestamp}`;
            } else {
                // Caso contrário, crie um novo elemento e adicione-o na lista
                var newTransmitterDataElement = document.createElement("div");
                newTransmitterDataElement.classList.add("panel");
                newTransmitterDataElement.id = idEstacao;
                newTransmitterDataElement.innerHTML = `
                    <div class="header">ID: ${idEstacao}</div>
                    <div class="content">Temp: <span class="${tempClass}">${temp}°C</span>,  Umid: <span class="${humidClass}">${umid}%</span>, Press: <span class="${pressureClass}">${pressure} hPa</span>, Alt&asymp; <span class="${pressureClass}">${altitude} m</span></div>
                    <div class="timestamp">Timestamp: ${timestamp}</div>
                `;
                // Adiciona o novo painel na página
                document.getElementById("transmitter-data").appendChild(newTransmitterDataElement);
                // Adiciona a refer~encia do painel na lista
                transmitterElements[idEstacao] = newTransmitterDataElement;
            }
           }, false);
        }
    </script>
</body>
</html>
)rawliteral";

//---------------------------------------------
// Variáveis para controle do Server http
//---------------------------------------------

AsyncWebServer server(80);
AsyncEventSource events("/events");

//-------------------------------
// Prototipação de Rotinas
//-------------------------------

void coleta_imprime_envia();                         // Rotina para coletar os dados e transmitir 
String formataValor(float valor, int size, int dec); // Rotina para formatar um número 

//--------------------------------
// Inicialização Geral do Programa
//--------------------------------

void setup() 
{

   // Inicializa da Serial

   Serial.begin(BAUDRATE_SERIAL);
   while (!Serial) ;

   // Inicializa o Led Interno

   pinMode(LED_BUILTIN, OUTPUT);

   // Inicializa o Módulo Lora

   Serial1.begin(BAUDRATE_LORA, SERIAL_8N1, pinRX, pinTX);

   // Hello na Console

   Serial.print("Transmissor Lora V1.0 Jun/2024 - Estação ");
   Serial.println(ESTACAO_ID);
   Serial.println();   

   // Inicializa o BME280. Atenção: este BMW280 veio configurado na porta x76.
   // É importante saber qual o endereço do seu módulo para alterar na linha seguinte

   if (!bme.begin(BME280_ADDR, &Wire)) 
   {
      Serial.println("Módulo BME280 não encontrado. Verifique o endereço ou a ligação das portas");
      while (1)
      {
         // Pisca o LED Interno para alertar para o erro
       
         digitalWrite(LED_BUILTIN, HIGH);
         delay(ALARME);
         digitalWrite(LED_BUILTIN, LOW);
         delay(ALARME);
      }
   }   

    // Inicializa a estação Wi-Fi
    
    WiFi.begin(ssid, password);
    Serial.print("Configurando o Modo Estação Wi-Fi");
    while (WiFi.status() != WL_CONNECTED) {
      delay(1000);
      Serial.print(".");
    }
    Serial.println("\n");

    // Mostra o IP adquirido da rede Wi-Fi
   
    Serial.print("Endereço IP : ");
    Serial.println(WiFi.localIP());

    // Mostra o MAC do Esp32

    Serial.print("Endereço MAC: ");
    Serial.println(WiFi.macAddress()); 

    // Mostra o canal associado pela rede Wi-Fi. Este mesmo canal
    // deverá ser utilizado pelo Transmissor

    Serial.print("Canal Wi-Fi : ");
    Serial.println(WiFi.channel());
    Serial.println();

    // Define o HostName para o servidor web para facilitar o acesso na rede local
    // sem conhecer o IP previamente

    WiFi.setHostname(String(NOME_ESTACAO).c_str());

    Serial.print("Adicionando " + String(NOME_ESTACAO) + " no MDNS... ");
    if (MDNS.begin(String(NOME_ESTACAO).c_str()))
    {
        Serial.println("adicionado corretamente no MDNS!");
        Serial.println("Use http://" + String(NOME_ESTACAO) + ".local no seu navegador...");  
        Serial.println("Ou opcionalmente...");    
    }
    else 
    {
        Serial.println("Erro ao adicionar no MDNS!");
    }
    Serial.println("Use http://" + WiFi.localIP().toString() + " no seu navegador...\n");
    
    MDNS.addService("http", "tcp", 80);    
  
    // Configurar rota para a página HTML
    
    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send_P(200, "text/html", index_html);
    });

    // Configura onConnect dos Eventos
    
    events.onConnect([](AsyncEventSourceClient *client)
    {
      if(client->lastId())
      {
        Serial.printf("Cliente reconectado! Último ID obtido: %u\n", client->lastId());
      }
    });
    server.addHandler(&events);

    // Iniciar o servidor Web
    
    server.begin();   

}

//--------------------------------
// Loop para coletar e enviar 
//--------------------------------

void loop() 
{

   // Verifica se chegou dados da Rede Lora

   if ( Serial1.available() )
   {

      // Liga o LED Interno na recepção

       digitalWrite(LED_BUILTIN, HIGH);    

      // Vamos simplesmente ler e imprimir pois não trataremos os dados 
      // que chegaram pois somos apenas transmissores. 
      
      String dados = Serial1.readString();
      Serial.print("Dados recebidos: ");
      Serial.println(dados);

      // Envia via Web Socket para o Navegador se for um JSON válido

      if (dados.startsWith("{\"id\":") && dados.endsWith("}"))
      {
         events.send(dados.c_str(), "new_readings", millis()); 
         Serial.println("Enviado para o html...");
      }
      else Serial.println("Ignorado não é o JSON esperado...");
      Serial.println();

      // Desliga o LED Interno na recepção

       digitalWrite(LED_BUILTIN, LOW);    
      
   }

   // Verifica se deve fazer a coleta de dados 

   if (millis()-ultimaVarredura > VARREDURA)
   {
      coleta_imprime_envia();
   }

}

//--------------------------------
// Rotina para ler os dados do 
// BME280 e enviar pela interface
// Lora
//--------------------------------

void coleta_imprime_envia()
{

   JsonDocument json;                    // Para conter os dados a enviar
   float temp;                           // Temperatura lida
   float umid;                           // Umidade lida
   float pressure;                       // Pressão atmosférica lida
   float altitude;                       // Altitude aproximada inferida através da pressão

   // Define a estação que enviará os dados

   json["id"] = ESTACAO_ID;

   // Trata a Temperatura sem casas decimais
   
   temp = bme.readTemperature();
   Serial.print("Temperature = ");
   Serial.print(temp,0);
   Serial.println(" *C");
   json["t"] = formataValor(temp,3,0);

   // Trata a Umidade sem casas decimais
   
   umid = bme.readHumidity(); 
   Serial.print("Humidity = ");
   Serial.print(umid,0);
   Serial.println(" %");
   json["u"] = formataValor(umid,3,0);

   // Trata a Pressão sem casas decimais
   
   pressure = bme.readPressure()/100.0F;
   Serial.print("Pressure = ");
   Serial.print(pressure,0);
   Serial.println(" hPa");
   json["p"] = formataValor(pressure,5,0);

   // Trata a Pressão sem casas decimais
   
   altitude = bme.readAltitude(SEALEVELPRESSURE_HPA)/(1-TAXA_ERRO_ALT/100); 
   Serial.print("Approx. Altitude = ");
   Serial.print(altitude,0);
   Serial.println(" m");
   json["a"] = formataValor(altitude,5,0);

   // Dá uma linha de espaço
   
   Serial.println();

   // Serializando o JSON sem formatação por economia por causa 
   // do limite de 58 bytes da rede LORA
   
   String jsonStr;
   serializeJson(json, jsonStr); 

   // Enviando o JSON para console

   Serial.print("Transmitindo: ");
   Serial.println(jsonStr); 
   Serial.print("Tamanho: ");
   Serial.println(jsonStr.length()); 

   // Dá uma linha de espaço 

   Serial.println();

   // Envia para o HTML 
   
   events.send(jsonStr.c_str(), "new_readings", millis()); 

   // Atualiza a última varredura

   ultimaVarredura = millis();
   
}

//--------------------------------
// Rotina para formatar um número
//--------------------------------

String formataValor(float valor, int tam, int dec)
{
   char buf[tam+2];                    // Buffer par aformatação
   String result;                      // Resultado depois da formatação

   dtostrf(valor, tam, dec, buf); 
   result = String(buf);
   result.trim();
   return result;
   
}

 


Conclusão

A Rede LORA é um excelente recurso para implementação de diversos cenários de comunicação entre microcontroladores, possuindo um alcance bastante significativo (na ordem de km dependendo do módulo escolhido), independência de Internet ativa e simplicidade na implementação. O máximo tamanho do pacote é 58 bytes, o que pode restringir a comunicação, mas tal limitação pode ser flexibilizada para uma gama variada de aplicações possíveis. Adicionalmente, recursos como Async Web Server pode enriquecer a usabilidade das soluções. Até a próxima!


Sobre o autor


Alberto de Almeida Menezes
tinho.menezes@gmail.com

Bacharel em Engenharia de Áudio e Produção Musical pela Berklee College of Music.


Dailton de Oliveira Menezes
dailton.menezes@gmail.com

Bacharel em Ciência da Computação pela Universidade Federal de Minas Gerais.


Eletrogate

25 de julho de 2024

A Eletrogate é uma loja virtual de componentes eletrônicos do Brasil e possui diversos produtos relacionados à Arduino, Automação, Robótica e Eletrônica em geral.

Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!