Biblioteca C++ para comunicação com módulos LoRaMESH (Radioenge) via UART, projetada para ser o core nativo do ecossistema LoRaMESH em múltiplas plataformas.
Este projeto não foi pensado apenas para um único firmware ou para um único sistema operacional. A proposta é ter uma base técnica sólida, reutilizável e previsível, capaz de atender dois mundos ao mesmo tempo:
- microcontroladores e firmwares embarcados
- Linux, Raspberry Pi, gateways e aplicações de alto nível
- ports em MicroPython para prototipagem, integração e uso embarcado
Por isso, a biblioteca foi estruturada em torno de um núcleo C++ com uma abstração de transporte chamada ILoraTransport, permitindo reaproveitar a mesma lógica de protocolo em diferentes plataformas sem reescrever o comportamento principal do módulo a cada novo port.
Além disso, este core também é a base da SDK Python do projeto. A camada Python, instalada com:
pip install lorameshreplica a lógica desta biblioteca em uma interface mais amigável para scripts, automações, gateways Linux e Raspberry Pi. Em outras palavras, o C++ é a base e o Python é a extensão de alto nível construída em cima dessa base.
O mesmo ecossistema também já conta com uma frente em MicroPython, pensada para ampliar o alcance da biblioteca em ambientes embarcados mais leves sem quebrar a coerência conceitual do projeto. Assim, este repositório em C++ continua sendo o centro técnico da lógica, enquanto Python e MicroPython funcionam como derivações práticas desse mesmo núcleo.
- Visão geral
- Por que esta biblioteca existe
- Objetivo do projeto
- Relação com a SDK Python
- Plataformas e direção do projeto
- Organização conceitual do projeto
- Arquitetura da solução
- Conceitos importantes antes de começar
- Interface de transporte
ILoraTransport - Adapters por plataforma
- Inicialização rápida
- Fluxo recomendado de inicialização
- Debug da comunicação
- Como o core monta e recebe frames
- CRC utilizado pela biblioteca
- Configuração de rede
- Configuração de rádio
- Classe LoRa e janela de recepção
- Constantes principais
- Informações e estados mantidos pelo core
- Controle remoto de GPIO
- Leitura analógica e medições auxiliares
- Modo transparente e dupla UART
- Referência detalhada da API
- Exemplos completos
- Boas práticas
- Erros comuns e diagnóstico
- ESP-IDF e STM32 HAL
- Casos de uso reais
- Licença
- Contato e referências
- Considerações finais
O LoRaMESH C++ é o núcleo responsável por concentrar a lógica real de comunicação com os módulos LoRaMESH. É aqui que ficam as rotinas que efetivamente:
- montam frames de comando
- montam frames de modo transparente
- enviam bytes por um transporte abstrato
- recebem respostas do módulo
- validam CRC
- leem o estado local do rádio
- configuram
NetworkID - configuram senha
- leem e alteram parâmetros de rádio
- configuram classe e janela de recepção
- controlam GPIO remoto
- leem entradas digitais e analógicas remotas
- calculam resistência e temperatura a partir do ADC
- mantêm um fluxo único reaproveitável entre Arduino, Linux, Raspberry Pi, ESP-IDF, STM32 HAL e derivações como Python e MicroPython
A proposta desta biblioteca não é só encapsular um conjunto de comandos. Ela existe para ser o coração de um ecossistema maior, em que:
- o core fica em C++
- os adapters ligam o core à UART real da plataforma
- os examples mostram uso prático por ambiente
- a SDK Python replica essa base para automações e gateways
Quem já trabalhou com rádio em aplicações reais sabe que o problema raramente é apenas enviar uma string. O desafio de verdade aparece quando você precisa:
- manter vários nós com configuração consistente
- acionar dispositivos remotos em campo
- ler sensores analógicos e digitais distribuídos
- portar a mesma lógica para firmware e para Linux
- depurar serial, framing, tempo de resposta e estado da rede
- evitar reimplementar o protocolo em cada projeto
Esta biblioteca nasce justamente para resolver isso com uma base única e reaproveitável.
Em vez de criar uma implementação isolada para Arduino, outra para Linux, outra para Raspberry Pi e outra para Python, a ideia do projeto é concentrar a inteligência principal no core C++, desacoplando apenas a camada que fala com a UART da plataforma.
Isso reduz retrabalho, melhora a manutenção, facilita a evolução e mantém o comportamento mais previsível entre ambientes diferentes.
O objetivo desta biblioteca é servir como base para sistemas reais com módulos LoRaMESH, permitindo construir aplicações como:
- automação rural
- controle de bombas e válvulas
- redes privadas de monitoramento
- leitura remota de sensores distribuídos
- controle remoto de saídas em campo
- gateways Linux ou Raspberry Pi
- provisionamento e manutenção de nós
- integração entre microcontroladores e aplicações de alto nível
- testes, debug e automação com a mesma lógica central
Este ponto precisa ficar muito claro: a SDK Python não substitui este core; ela replica sua lógica em uma interface mais amigável para Python.
Na prática:
- o projeto em C++ é a base estrutural
- a SDK Python expõe essa base para automação, scripts e gateways
- a nomenclatura segue a mesma linha conceitual do core nativo
- o fluxo mental de uso continua alinhado com a versão C++
- evolução e manutenção continuam centradas no núcleo C++
A biblioteca Python já pode ser instalada com:
pip install lorameshIsso reforça a proposta do projeto: o C++ é a base e o Python é a camada de conveniência para ambientes em que testes, automações e integrações de mais alto nível fazem sentido.
A biblioteca foi pensada para funcionar em mais de um ambiente desde o início.
Arduino é um dos alvos principais da biblioteca. A proposta aqui é permitir uso direto em firmwares, com inicialização da UART da placa, adapter leve e integração simples ao fluxo de controle do módulo.
Esse cenário cobre, por exemplo:
- placas AVR
- placas ARM compatíveis com Arduino
- ESP32 rodando framework Arduino
- ambientes com
Serial,Serial1,Serial2ou derivados deStream
Linux é outro alvo central do projeto. Aqui a biblioteca pode ser usada diretamente em C++, principalmente em gateways, ferramentas de provisionamento, utilitários de diagnóstico e aplicações que falam com módulos LoRaMESH por serial.
No caso do Raspberry Pi, existem duas abordagens naturais:
- usar o core C++ diretamente com adapter Linux
- usar a SDK Python
loramesh, construída sobre esse mesmo core
ESP-IDF já foi testado e validado dentro da proposta da biblioteca. Isso é importante porque mostra que a arquitetura realmente funciona fora do universo Arduino e Linux, mantendo a mesma lógica central do core C++ em um framework nativo do ESP32.
STM32 HAL também já foi testado e validado, mas aqui existe uma observação importante: a integração pede alguns ajustes manuais no projeto para que o uso fique correto. Ou seja, não é uma ideia apenas teórica ou futura; ela já funciona, porém depende de preparação adequada do ambiente STM32 para compilar e integrar o core C++ da biblioteca.
Este README evita congelar uma árvore rígida de arquivos no texto, porque a organização do repositório pode evoluir. O que importa aqui é a ideia estrutural:
- existe um core C++
- existe uma interface de transporte
- existem adapters por plataforma
- existem examples por ambiente
- existe uma camada Python no mesmo ecossistema
Em vez de documentar uma árvore fixa de paths que pode mudar com o tempo, o mais importante é entender os blocos conceituais do projeto.
O core é onde mora a classe LoRaMESH e toda a lógica de protocolo, framing, CRC, configuração, leitura de estado do módulo e operações remotas.
A interface ILoraTransport define o contrato mínimo que qualquer plataforma precisa oferecer ao core.
Cada plataforma implementa sua forma concreta de:
- escrever bytes
- verificar bytes disponíveis
- ler dados
- fornecer uma base temporal em milissegundos
Os exemplos servem para mostrar uso real da biblioteca em cada ambiente, cobrindo desde inicialização básica até controle de múltiplos slaves.
A parte Python existe como extensão do ecossistema. Ela não compete com o core C++, mas sim o reaproveita para uso em scripts, automações e gateways.
A arquitetura do projeto foi desenhada para reuso.
Aplicação do usuário
↓
LoRaMESH (core C++)
↓
ILoraTransport
↓
Adapter da plataforma
↓
UART / serial real da plataforma
↓
Módulo LoRaMESH
Quando a camada Python entra na jogada, o desenho fica assim:
Aplicação Python
↓
Bindings Python
↓
LoRaMESH (core C++)
↓
ILoraTransport / serial Linux
↓
UART
↓
Módulo LoRaMESH
- a lógica principal do protocolo fica centralizada
- cada plataforma implementa apenas o mínimo necessário para transporte
- o comportamento tende a ser consistente entre ambientes
- a SDK Python não precisa reinventar o protocolo
- novos adapters podem ser adicionados sem reescrever o core
Antes de usar a biblioteca, alguns pontos precisam ficar muito claros.
A classe LoRaMESH não fala diretamente com Serial, HardwareSerial, termios, HAL do STM32 ou drivers do ESP-IDF. Ela fala com uma abstração chamada ILoraTransport.
É isso que torna o projeto realmente portável.
O construtor recebe:
LoRaMESH(ILoraTransport* cmdTransport, ILoraTransport* transpTransport)Ou seja, a arquitetura já foi pensada para separar:
- o canal de comandos
- o canal de modo transparente
Esses dois transports podem ser:
- o mesmo objeto
- dois objetos diferentes
- duas UARTs separadas
- duas interfaces lógicas na mesma plataforma
No core C++, o debug depende de uma função callback registrada com setDebug().
Isso significa que:
begin(true)sozinho não é suficiente para imprimir mensagens- o dump hexadecimal só aparece se houver callback configurado
- em ambientes embarcados, o callback normalmente aponta para
Serial.println
No estado atual do código, o construtor de LinuxSerialTransport recebe device e baud, mas a configuração do termios foi implementada com B9600 fixo.
Na prática, isso significa que:
- o adapter Linux atual deve ser tratado como 9600 até que a conversão completa do argumento
baudseja implementada - exemplos C++ usando
LinuxSerialTransportfazem mais sentido em9600 - isso é diferente da SDK Python, que pode usar outro backend de serial
Ao controlar nós remotos, existe latência natural. Ela depende de fatores como:
BWSFCR- topologia da rede
- quantidade de hops
- ruído
- classe do dispositivo
O fluxo mais seguro é sempre:
- inicializar a UART da plataforma
- criar o adapter
- criar a instância de
LoRaMESH - configurar debug, se necessário
- chamar
begin() - insistir em
localRead()até obter resposta válida - só então aplicar configurações e comandos
A base da portabilidade da biblioteca está aqui:
class ILoraTransport
{
public:
virtual ~ILoraTransport() {}
virtual size_t write(const uint8_t* data, size_t len) = 0;
virtual size_t available() = 0;
virtual size_t read(uint8_t* buffer, size_t maxLen) = 0;
virtual uint32_t millis() = 0;
};Envia bytes pela interface física da plataforma.
Retorna quantos bytes estão disponíveis para leitura.
Lê dados disponíveis para o buffer informado.
Fornece uma base temporal em milissegundos, usada pelo core para controlar timeouts de recepção.
Se uma plataforma consegue implementar corretamente esses quatro métodos, ela já consegue ser conectada ao core da biblioteca.
O adapter Arduino é um wrapper fino sobre Stream, o que permite uso com objetos como:
SerialSerial1Serial2HardwareSerial- implementações derivadas de
Stream
Essa escolha é muito boa porque mantém o adapter simples e compatível com o ecossistema Arduino.
O adapter Linux usa chamadas típicas de ambiente POSIX, como:
open()termiosioctl(FIONREAD)clock_gettime(CLOCK_MONOTONIC)
Isso torna o core utilizável em PCs, SBCs e gateways Linux.
No Raspberry Pi, você pode optar por duas estratégias:
- usar o core C++ diretamente com o adapter Linux
- usar a SDK Python
loramesh, que já replica esse core para Python
As duas abordagens fazem sentido. A escolha depende mais do tipo de aplicação do que da biblioteca em si.
A arquitetura já contempla um adapter dedicado para ESP-IDF, e essa integração já foi testada e validada. Isso reforça que o desenho baseado em ILoraTransport realmente cumpre o papel de tornar o core portátil entre diferentes ambientes.
STM32 HAL também já está contemplado na arquitetura e essa integração já foi testada e validada. A ideia aqui continua sendo a mesma: usar um adapter fino sobre a UART/HAL, reaproveitando integralmente o core C++.
Ao mesmo tempo, é importante deixar claro que o uso em STM32 costuma exigir alguns ajustes manuais no projeto, principalmente quando ele nasce a partir de um ambiente gerado pelo CubeIDE ou por templates que ainda não estão preparados para compilar a biblioteca como C++. Esses ajustes são detalhados mais à frente neste README.
O uso mais básico da biblioteca em C++ começa com:
- inicializar a UART da plataforma
- criar o transport
- instanciar
LoRaMESH - opcionalmente configurar debug
- chamar
begin()
#include "LoRaMESH.h"
#define LORA_RX 16
#define LORA_TX 17
#define LORA_BAUD 9600
ArduinoSerialTransport transportCmd(Serial2);
LoRaMESH lora(&transportCmd, &transportCmd);
static void debugPrint(const char* msg)
{
Serial.println(msg);
}
void setup()
{
Serial.begin(115200);
Serial2.begin(
LORA_BAUD,
SERIAL_8N1,
LORA_RX,
LORA_TX
);
lora.setDebug(debugPrint, true);
lora.begin(true);
}
void loop()
{
}#include "LoRaMESH.h"
#include "adapters/linux/LinuxSerialTransport.h"
#include <iostream>
#include <thread>
#include <chrono>
static void debugPrint(const char* msg)
{
std::cout << msg << std::endl;
}
int main()
{
LinuxSerialTransport transportCmd("/dev/ttyACM0", 9600);
LoRaMESH lora(&transportCmd, &transportCmd);
lora.setDebug(debugPrint, true);
lora.begin(true);
while (!lora.localRead())
{
std::cout << "Aguardando modulo..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
std::cout << "Modulo pronto" << std::endl;
return 0;
}Pelo comportamento atual do core, um fluxo robusto de inicialização costuma seguir esta ordem:
- iniciar a UART da plataforma
- criar o adapter correspondente
- criar a instância de
LoRaMESH - configurar debug, se quiser enxergar TX/RX
- chamar
begin() - insistir em
localRead()até o módulo responder - validar ou ajustar
NetworkID - consultar a configuração de rádio
- ajustar BPS se necessário
- consultar classe atual
- ajustar classe e janela se necessário
- ajustar senha, se necessário
- só então iniciar operações remotas
void initializeRadio()
{
Serial.println("Aguardando modulo...");
while (!lora.localRead())
{
Serial.println("Modulo nao respondeu ainda...");
delay(500);
}
Serial.println("Modulo pronto");
if (lora.localId != ID)
{
Serial.println("Configurando NetworkID");
for (int i = 0; i < 3; i++)
{
if (lora.setNetworkID(ID))
break;
delay(200);
}
}
for (int i = 0; i < 3; i++)
if (lora.getBPS())
break;
if (lora.BW != BW500 || lora.SF != SF7 || lora.CR != CR4_5)
{
Serial.println("Configurando BPS");
for (int i = 0; i < 3; i++)
if (lora.setBPS(BW500, SF7, CR4_5))
break;
}
for (int i = 0; i < 3; i++)
if (lora.getClass())
break;
if (lora.LoRa_class != CLASS_C || lora.LoRa_window != WINDOW_5s)
{
Serial.println("Configurando classe");
for (int i = 0; i < 3; i++)
if (lora.setClass(CLASS_C, WINDOW_5s))
break;
}
if (lora.registered_password != 123)
{
Serial.println("Configurando senha");
for (int i = 0; i < 3; i++)
if (lora.setPassword(123))
break;
}
Serial.println("Configuracao concluida");
}O debug é configurado por:
void setDebug(DebugFn fn, bool enable_hex_dump)Exemplo:
static void debugPrint(const char* msg)
{
Serial.println(msg);
}
lora.setDebug(debugPrint, true);Na implementação atual, begin(true) faz basicamente duas coisas:
- habilita o dump hexadecimal
- chama
localRead()uma vez
Isso significa que:
begin(true)não substitui um loop robusto comwhile(!localRead())- sem callback configurado, nada será impresso
- o comportamento seguro ainda é validar o módulo explicitamente
O core usa prefixos como:
TX CMD:
RX CMD:
TX TRP:
RX TRP:
Isso é muito útil porque permite visualizar:
- o frame real enviado
- a resposta real recebida
- o fluxo de comando separado do fluxo transparente
- diferenças entre o que a aplicação pretende enviar e o que de fato foi serializado
A biblioteca separa internamente a lógica entre:
prepareFrameCommand()PrepareFrameTransp()sendPacket()receivePacketCommand()receivePacketTransp()
Na prática, o frame de comando segue esta ideia:
[id_low][id_high][cmd][payload ...][crc_low][crc_high]
No modo transparente, a ideia é esta:
[id_low][id_high][payload ...]
O core já foi desenhado para tratar comando e transparente como fluxos distintos, inclusive com possibilidade de transports separados. Isso é uma das partes mais fortes do projeto.
O CRC usado no core atual é calculado com:
- seed inicial
0xC181 - operação XOR byte a byte
- polinômio aplicado como
0xA001no bit menos significativo
Isso importa porque:
- frames inválidos são descartados
- respostas corrompidas não devem ser tratadas como válidas
- o debug hexadecimal ajuda a entender quando a falha está no framing e não na lógica de alto nível
A configuração de rede é uma das primeiras etapas depois que o módulo responde corretamente.
lora.setNetworkID(0);No core atual, IDs acima de 2047 são rejeitados.
Na prática, é comum usar:
0para gateway, master ou nó principal- outros valores para nós remotos conforme a organização da rede
lora.setPassword(123);A senha faz parte da identidade operacional do módulo na rede.
Pelo comportamento observado no código atual, a verificação de setPassword() reconstrói a comparação usando a parte de 16 bits refletida na leitura local. Na prática, os exemplos atuais fazem mais sentido com senhas de até 16 bits.
Por isso, o exemplo de Arduino usa:
123como valor simples e previsível.
A configuração de rádio define alcance, robustez, taxa de transmissão e latência da rede.
if (lora.getBPS())
{
Serial.println("BW=" + String(lora.BW));
Serial.println("SF=" + String(lora.SF));
Serial.println("CR=" + String(lora.CR));
}A chamada getBPS() atualiza internamente os campos:
BWSFCR
lora.setBPS(BW500, SF7, CR4_5);- bandwidth maior tende a aumentar taxa de transmissão
- spreading factor maior tende a aumentar robustez e tempo no ar
- coding rate maior tende a aumentar redundância e robustez
O core atual rejeita:
- bandwidth acima de
BW500 - spreading factor fora de
SF_FSKouSF7aSF12 - coding rate fora de
CR4_5aCR4_8
if (lora.getClass())
{
Serial.println("Class=" + String(lora.LoRa_class));
Serial.println("Window=" + String(lora.LoRa_window));
}lora.setClass(CLASS_C, WINDOW_5s);CLASS_Atende a ser mais econômicaCLASS_Cmantém recepção contínua e é útil em sistemas energizados continuamente- a janela define o comportamento temporal implementado no módulo conforme os valores aceitos pelo core
No estado atual da implementação:
- apenas
CLASS_AeCLASS_Csão aceitas - janelas acima de
WINDOW_15ssão rejeitadas
Assim como no README da camada Python, vale documentar as principais constantes de forma explícita.
BW125 = 0x00
BW250 = 0x01
BW500 = 0x02Leitura prática:
BW125privilegia robustez relativa e menor taxaBW250fica no meio-termoBW500privilegia maior taxa de transmissão
SF_FSK = 0x00
SF7 = 0x07
SF8 = 0x08
SF9 = 0x09
SF10 = 0x0A
SF11 = 0x0B
SF12 = 0x0CLeitura prática:
SF7tende a ser mais rápidoSF12tende a ser mais robusto e mais lento- valores intermediários servem para equilibrar alcance e tempo no ar
SF_FSKrepresenta operação em FSK, não em LoRa tradicional
CR4_5 = 0x01
CR4_6 = 0x02
CR4_7 = 0x03
CR4_8 = 0x04Leitura prática:
CR4_5tende a ser mais eficienteCR4_8tende a ser mais robusto
CLASS_A = 0x00
CLASS_C = 0x02WINDOW_5s = 0x00
WINDOW_10s = 0x01
WINDOW_15s = 0x02LNOT_PULL = 0x00
LPULLUP = 0x01
LPULLDOWN = 0x02INOUT_DIGITAL_INPUT = 0x00
INOUT_DIGITAL_OUTPUT = 0x01
INOUT_ANALOG_INPUT = 0x03O core também aceita uma forma de uso mais próxima de Arduino:
INPUT
OUTPUT
INPUT_PULLUP
INPUT_PULLDOWNInternamente, esses aliases são convertidos para os modos e pulls corretos.
Uma característica importante da biblioteca atual é que ela mantém alguns estados que podem ser lidos após operações bem-sucedidas.
Entre os mais úteis no uso prático estão:
localIdlocalUniqueIdregistered_passwordBWSFCRLoRa_classLoRa_window
Esses campos são úteis para:
- debug
- verificação de provisionamento
- logs de inventário
- decisão condicional na inicialização
- confirmação do estado real do módulo após leitura
if (lora.localRead())
{
Serial.println("Local ID: " + String(lora.localId));
Serial.println("Password: " + String(lora.registered_password));
Serial.println("UID: " + String((unsigned long)lora.localUniqueId));
}Um dos maiores diferenciais do projeto é permitir tratar um nó remoto como extensão física do sistema principal.
Antes de ler ou escrever um GPIO remoto, ele precisa ser configurado.
lora.pinMode(1, 3, OUTPUT);Ou, de forma mais explícita com constantes internas:
lora.pinMode(1, 3, INOUT_DIGITAL_OUTPUT);lora.digitalWrite(1, 3, 1);
lora.digitalWrite(1, 3, 0);Interpretação:
- primeiro parâmetro: ID do nó remoto
- segundo parâmetro: GPIO remoto
- terceiro parâmetro: nível lógico
uint8_t estado = lora.digitalRead(1, 4);Você pode atuar diretamente em qualquer nó remoto conhecido pelo ID.
lora.digitalWrite(2, 0, 1);Nesse caso:
2é o ID do slave0é o GPIO1representa nível alto
No estado atual do core:
gpio > 0x07é rejeitadodigitalWrite()tenta confirmar a escrita verificando o retorno do módulo
Existe uma nuance importante no core atual: quando determinados fluxos configuram os GPIOs 5 e 6 como entrada, o comportamento passa a ser tratado como entrada analógica e analogEnabled é ativado.
Isso impacta diretamente:
analogRead()digitalRead()nesses canaisdigitalWrite(), que passa a ser bloqueado nesses GPIOs quando o modo analógico estiver ativo
Essa é uma característica importante do estado atual da biblioteca e precisa ser conhecida por quem vai usar IO remoto de forma mais avançada.
uint16_t adc = lora.analogRead(1, 5);A resposta é um valor bruto do ADC reconstruído a partir dos bytes recebidos do módulo.
Quando analogEnabled está ativo e o canal cai na regra dos GPIOs analógicos, digitalRead() usa analogRead() e aplica um limiar de meia escala.
Na prática, isso significa que a leitura digital nesses canais é derivada da leitura analógica.
uint16_t adc = lora.analogRead(1, 5);
int r1 = lora.getR1(adc, 10000);Essa função é útil quando o ADC está lendo um divisor resistivo.
uint16_t adc = lora.analogRead(1, 5);
double temp = lora.getTemp(adc, 3950, 10000, 10000);Essa função é útil para sensores resistivos baseados em NTC.
No estado atual da implementação:
getR1(0, R2)retorna um valor muito alto como sentinelagetR1(rawADC >= 4096, R2)retorna0getTemp()retorna um valor próximo de-273.15quando a resistência calculada é inválida
Mesmo que muitos exemplos atuais estejam mais focados em comandos, o core já foi desenhado para separar claramente:
- fluxo de comando
- fluxo transparente
A arquitetura permite:
LoRaMESH lora(&transportCmd, &transportTransp);Também é possível usar o mesmo objeto nos dois papéis:
LoRaMESH lora(&transportCmd, &transportCmd);Foi exatamente essa a abordagem usada no exemplo Arduino atual.
- uma UART dedicada a configuração e gerenciamento
- outra UART dedicada a payload transparente
- debug mais limpo por fluxo
- arquiteturas em que gateway e módulo convivem em dois modos distintos
Mesmo que sua aplicação inicial use só o canal de comando, vale manter em mente que a arquitetura já foi preparada para crescimento em direção ao modo transparente.
Abaixo está a referência organizada com base no comportamento observado no core atual.
LoRaMESH(ILoraTransport* cmdTransport, ILoraTransport* transpTransport)Descrição:
- cria a instância principal da biblioteca
- associa o transporte de comandos
- associa o transporte de modo transparente
Uso típico quando ambos compartilham a mesma UART:
ArduinoSerialTransport transportCmd(Serial2);
LoRaMESH lora(&transportCmd, &transportCmd);void setDebug(DebugFn fn, bool enable_hex_dump)Descrição:
- registra a função callback de debug
- habilita ou desabilita dump hexadecimal
void begin(bool debug)Descrição:
- habilita dump hexadecimal quando
debug == true - chama
localRead()uma vez internamente
Observação importante:
- isso não substitui um
while(!localRead()) - isso não imprime nada sem callback registrado
bool localRead()Descrição:
- envia o comando de leitura local
- atualiza
localId - atualiza
registered_password - atualiza
localUniqueId
bool setNetworkID(uint16_t id)Descrição:
- configura o
NetworkIDlógico do módulo - rejeita IDs acima de
2047
bool setPassword(uint32_t password)Descrição:
- envia o comando de configuração de senha
- executa
localRead()após a escrita - compara o valor retornado para verificar consistência
bool getBPS(bool ignore_cmd = false)Descrição:
- consulta a configuração atual de rádio
- atualiza
BW,SFeCR
bool setBPS(uint8_t bandwidth, uint8_t spreading_factor, uint8_t coding_rate)Descrição:
- valida os parâmetros
- envia a configuração
- chama
getBPS(true) - confirma se o estado interno corresponde ao solicitado
bool getClass(bool ignore_cmd = false)Descrição:
- consulta classe e janela
- atualiza
LoRa_classeLoRa_window
bool setClass(uint8_t lora_class, uint8_t lora_window)Descrição:
- valida classe e janela
- envia o comando
- chama
getClass(true) - confirma se o estado interno corresponde ao solicitado
bool pinMode(uint16_t id, uint8_t gpio, uint8_t inout, uint8_t logical_level = 0)Descrição:
- configura o comportamento de um pino remoto
- aceita aliases estilo Arduino e constantes internas do protocolo
bool digitalWrite(int16_t id, uint8_t gpio, uint8_t logical_level)Descrição:
- escreve em um GPIO remoto
- tenta confirmar a escrita com base no retorno do módulo
- internamente já executa mais de uma tentativa de confirmação
uint8_t digitalRead(int16_t id, uint8_t gpio)Descrição:
- consulta o estado lógico de um pino remoto
- pode derivar a leitura a partir do ADC nos canais híbridos
uint16_t analogRead(int16_t id, uint8_t gpio)Descrição:
- consulta o valor analógico remoto
- tenta múltiplas leituras antes de desistir
int getNoise(uint16_t id, uint8_t select)Descrição:
- consulta ruído do nó
- aceita
selectaté2 - retorna
255em caso de erro
int getR1(uint16_t rawADC, int R2)Descrição:
- calcula resistência equivalente a partir de um divisor resistivo
double getTemp(uint16_t rawADC, int beta, int Rt, int R2)Descrição:
- estima temperatura a partir do ADC e dos parâmetros do NTC
Esta seção é uma das mais importantes do projeto porque a biblioteca foi desenhada para existir em mais de um ambiente.
#include "LoRaMESH.h"
#define LORA_RX 16
#define LORA_TX 17
#define LORA_BAUD 9600
ArduinoSerialTransport transportCmd(Serial2);
LoRaMESH lora(&transportCmd, &transportCmd);
static void debugPrint(const char* msg)
{
Serial.println(msg);
}
void setup()
{
Serial.begin(115200);
delay(2000);
Serial2.begin(
LORA_BAUD,
SERIAL_8N1,
LORA_RX,
LORA_TX
);
lora.setDebug(debugPrint, true);
lora.begin(true);
while (!lora.localRead())
{
Serial.println("Aguardando modulo...");
delay(500);
}
Serial.println("Modulo pronto");
}
void loop()
{
}if (lora.localRead())
{
Serial.println("Local ID: " + String(lora.localId));
Serial.println("Password: " + String(lora.registered_password));
Serial.println("UID: " + String((unsigned long)lora.localUniqueId));
}void setupRadio()
{
if (lora.localId != 0)
lora.setNetworkID(0);
lora.setPassword(123);
lora.setBPS(BW500, SF7, CR4_5);
lora.setClass(CLASS_C, WINDOW_5s);
lora.getBPS();
lora.getClass();
Serial.println("BW=" + String(lora.BW));
Serial.println("SF=" + String(lora.SF));
Serial.println("CR=" + String(lora.CR));
Serial.println("CLASS=" + String(lora.LoRa_class));
Serial.println("WINDOW=" + String(lora.LoRa_window));
}void setup(){
...
lora.pinMode(1, 0, INOUT_DIGITAL_OUTPUT);
}
void loop()
{
Serial.println("Ligando GPIO0 do slave 1");
lora.digitalWrite(1, 0, 1);
delay(1000);
Serial.println("Desligando GPIO0 do slave 1");
lora.digitalWrite(1, 0, 0);
delay(1000);
}void setup(){
...
lora.pinMode(1, 4, INOUT_DIGITAL_INPUT);
}
void loop()
{
uint8_t estado = lora.digitalRead(1, 4);
Serial.println("Estado do GPIO4 remoto: " + String(estado));
delay(1000);
}void setup(){
...
lora.pinMode(1, 5, INOUT_ANALOG_INPUT);
}
void loop()
{
uint16_t adc = lora.analogRead(1, 5);
Serial.println("ADC remoto: " + String(adc));
delay(1000);
}void setup(){
...
lora.pinMode(1, 5, INOUT_ANALOG_INPUT);
}
void loop()
{
uint16_t adc = lora.analogRead(1, 5);
int r1 = lora.getR1(adc, 10000);
double temp = lora.getTemp(adc, 3950, 10000, 10000);
Serial.println("ADC: " + String(adc));
Serial.println("R1: " + String(r1));
Serial.println("Temp: " + String(temp));
delay(1000);
}Este exemplo segue a mesma linha do material atual do projeto: um master alterna o estado do GPIO 0 de múltiplos nós remotos.
#include "LoRaMESH.h"
#define LORA_RX 16
#define LORA_TX 17
#define LORA_BAUD 9600
#define MAX_REPLICATE_IN_SLAVES 3
#define DELAY_BETWEEN_SLAVES 250
#define MAX_RETRY 3
uint8_t ID = 0;
ArduinoSerialTransport transportCmd(Serial2);
LoRaMESH lora(&transportCmd, &transportCmd);
static void debugPrint(const char* msg)
{
Serial.println(msg);
}
void initializeRadio()
{
Serial.println("Aguardando modulo...");
while(!lora.localRead())
{
Serial.println("Modulo nao respondeu ainda...");
delay(500);
}
Serial.println("Modulo pronto");
if(lora.localId != ID)
{
Serial.println("Configurando NetworkID");
for(int i=0;i<3;i++)
{
if(lora.setNetworkID(ID))
break;
delay(200);
}
}
for(int i=0;i<3;i++)
if(lora.getBPS())
break;
if(lora.BW != BW500 || lora.SF != SF7 || lora.CR != CR4_5)
{
Serial.println("Configurando BPS");
for(int i=0;i<3;i++)
if(lora.setBPS(BW500,SF7,CR4_5))
break;
}
for(int i=0;i<3;i++)
if(lora.getClass())
break;
if(lora.LoRa_class != CLASS_C || lora.LoRa_window != WINDOW_5s)
{
Serial.println("Configurando classe");
for(int i=0;i<3;i++)
if(lora.setClass(CLASS_C,WINDOW_5s))
break;
}
if(lora.registered_password != 123)
{
Serial.println("Configurando senha");
for(int i=0;i<3;i++)
if(lora.setPassword(123))
break;
}
Serial.println("Configuracao concluida");
}
void setup()
{
Serial.begin(115200);
delay(2000);
Serial.println("Inicializando LoRaMesh...");
Serial2.begin(
LORA_BAUD,
SERIAL_8N1,
LORA_RX,
LORA_TX
);
lora.setDebug(debugPrint, true);
lora.begin(true);
initializeRadio();
Serial.println("Master pronto");
}
void loop()
{
static bool state = false;
state = !state;
for (uint16_t i = 1; i <= MAX_REPLICATE_IN_SLAVES; ++i)
{
for (uint8_t j = 0; j < MAX_RETRY; ++j)
{
if (lora.digitalWrite(i, 0, state ? 1 : 0))
break;
else
delay(1000 + random(500, 2500));
}
if (state)
Serial.println("GPIO0 SLAVE:" + String(i) + "=ON");
else
Serial.println("GPIO0 SLAVE:" + String(i) + "=OFF");
if (MAX_REPLICATE_IN_SLAVES > 1)
delay(DELAY_BETWEEN_SLAVES);
}
if (MAX_REPLICATE_IN_SLAVES == 1)
delay(DELAY_BETWEEN_SLAVES);
}#include "LoRaMESH.h"
#include "adapters/linux/LinuxSerialTransport.h"
#include <iostream>
#include <thread>
#include <chrono>
static void debugPrint(const char* msg)
{
std::cout << msg << std::endl;
}
int main()
{
LinuxSerialTransport transportCmd("/dev/ttyACM0", 9600);
LoRaMESH lora(&transportCmd, &transportCmd);
lora.setDebug(debugPrint, true);
lora.begin(true);
while (!lora.localRead())
{
std::cout << "Aguardando modulo..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500));
}
if (lora.localId != 0)
lora.setNetworkID(0);
lora.setPassword(123);
lora.setBPS(BW500, SF7, CR4_5);
lora.setClass(CLASS_C, WINDOW_5s);
std::cout << "Modulo pronto" << std::endl;
return 0;
}Mesmo este sendo o README do core C++, vale documentar o uso natural em Raspberry Pi através da camada Python, já que ela existe exatamente para reaproveitar este core em um ambiente mais produtivo para gateways e automações.
import loramesh
import time
import random
PORT = "/dev/ttyACM0"
BAUD = 9600
ID = 0
MAX_REPLICATE_IN_SLAVES = 3
DELAY_BETWEEN_SLAVES = 0.250
MAX_RETRY = 3
BW500 = 0x02
SF7 = 0x07
CR4_5 = 0x01
CLASS_C = 0x02
WINDOW_5s = 0x00
PASSWORD = 123
def initialize_radio(mesh):
print("Aguardando modulo...")
while not mesh.localRead():
print("Modulo nao respondeu ainda...")
time.sleep(0.5)
print("Modulo pronto")
if mesh.getLocalID() != ID:
print("Configurando NetworkID")
for _ in range(3):
if mesh.setNetworkID(ID):
break
time.sleep(0.2)
for _ in range(3):
if mesh.getBPS():
break
if mesh.getBW() != BW500 or mesh.getSF() != SF7 or mesh.getCR() != CR4_5:
print("Configurando BPS")
for _ in range(3):
if mesh.setBPS(BW500, SF7, CR4_5):
break
for _ in range(3):
if mesh.getClass():
break
if mesh.getClassValue() != CLASS_C or mesh.getWindow() != WINDOW_5s:
print("Configurando classe")
for _ in range(3):
if mesh.setClass(CLASS_C, WINDOW_5s):
break
if mesh.setPassword(PASSWORD):
print("Senha configurada")
print("Configuracao concluida")
def main():
print("Inicializando LoRaMesh...")
mesh = loramesh.LoRaMESH(PORT, BAUD)
mesh.begin(True, True)
initialize_radio(mesh)
print("Master pronto")
state = False
while True:
state = not state
for i in range(1, MAX_REPLICATE_IN_SLAVES + 1):
for j in range(MAX_RETRY):
if mesh.digitalWrite(i, 0, 1 if state else 0):
break
else:
delay = 1 + random.uniform(0.5, 2.5)
time.sleep(delay)
if state:
print(f"GPIO0 SLAVE:{i}=ON")
else:
print(f"GPIO0 SLAVE:{i}=OFF")
if MAX_REPLICATE_IN_SLAVES > 1:
time.sleep(DELAY_BETWEEN_SLAVES)
if MAX_REPLICATE_IN_SLAVES == 1:
time.sleep(DELAY_BETWEEN_SLAVES)
if __name__ == "__main__":
main()A integração com ESP-IDF já foi testada e validada, então esta seção não existe por ausência de suporte, e sim para reservar um espaço próprio para um exemplo dedicado e mais polido dentro do README.
O ideal é que este bloco cresça para documentar de forma separada:
- inicialização da UART no ESP-IDF
- criação do adapter da plataforma
- callback de debug
- laço principal de leitura e escrita
- exemplos equivalentes aos de Arduino
A integração com STM32 HAL já foi testada e validada, mas merece um bloco próprio porque, diferentemente de Arduino ou Linux, ela normalmente exige alguns ajustes manuais no projeto antes de funcionar de forma limpa.
O ideal é que esta seção evolua para documentar um exemplo dedicado cobrindo:
- inicialização da UART via HAL
- adapter sobre
UART_HandleTypeDef - integração de
millis()ou time base equivalente - callback de debug
- exemplos equivalentes aos de Arduino
- ajustes de build e include paths necessários para o projeto STM32
Esse é o ponto mais importante para evitar inicialização falsa.
Pequenos delays ajudam bastante, principalmente em redes reais.
Consultar BW, SF, CR, LoRa_class, LoRa_window e registered_password ajuda muito em diagnóstico e provisionamento.
Principalmente em acionamento remoto de GPIO.
Em sistemas seriais e de rádio, enxergar o TX/RX bruto economiza muito tempo.
Registre sempre:
- baudrate
NetworkID- senha
BWSFCR- classe
- janela
- IDs dos nós
No estado atual do código, essa é a abordagem mais segura para evitar diagnóstico enganoso.
Verifique:
- porta serial correta
- alimentação do módulo
- baudrate real do enlace
- permissões de acesso à serial
- fiação UART
- callback de debug configurado corretamente
- se o módulo realmente inicializou
Verifique:
- se o
device_idestá correto - se o pino foi configurado antes com
pinMode() - se o nó remoto está ativo na rede
- se os dois lados têm configuração de rádio compatível
- se a latência natural da rede está sendo respeitada
Verifique:
- o canal real usado
- se o GPIO entrou em fluxo analógico
- resolução efetiva do ADC do módulo
- circuito ligado ao pino
- valores usados em
getR1()egetTemp()
Verifique se o fluxo ativou analogEnabled. No estado atual do core, isso altera o comportamento de leitura e bloqueia escrita nesses canais quando passam a ser tratados como analógicos.
Verifique:
- se
setDebug()foi chamado - se a função callback realmente imprime a string recebida
- se
begin(true)foi usado quando você deseja dump hexadecimal
Essas duas plataformas não estão aqui apenas como intenção futura. O código já foi testado e validado tanto em ESP-IDF quanto em STM32 HAL. O que muda entre elas não é a viabilidade do core, e sim o nível de preparação manual exigido pelo projeto hospedeiro.
O código já foi testado e validado para ESP-IDF. Isso confirma que a proposta multiplataforma da biblioteca funciona também no framework nativo do ESP32, sem depender exclusivamente do ecossistema Arduino.
A integração segue a mesma filosofia do restante do projeto:
- adapter fino sobre UART do framework
- implementação de
write(),available(),read()emillis() - reaproveitamento integral do core
LoRaMESH - exemplos equivalentes aos de Arduino
Mesmo quando a documentação dedicada de ESP-IDF ainda não estiver tão extensa quanto a de Arduino, o ponto importante é que esta frente já foi colocada em prática e validada.
O código também já foi testado e validado para STM32 HAL. Porém, aqui existe uma observação prática importante: para colocar a biblioteca para rodar no STM32, normalmente é necessário preparar o projeto para receber corretamente um core em C++.
A lógica continua sendo a mesma:
- adapter fino sobre HAL UART
- time base compatível para
millis() - reaproveitamento do core sem reinventar o protocolo
- exemplos equivalentes aos de Arduino
Mas, além disso, em STM32 costuma ser necessário:
- habilitar a compilação do projeto para C++ também
- criar arquivos auxiliares como
loramesh.heloramesh.cpppara organizar o uso da biblioteca dentro do projeto gerado - adicionar a biblioteca ao projeto STM32
- remover a exclusão da biblioteca no build, caso ela tenha ficado marcada como excluída
Também é necessário adicionar estas paths de include no projeto:
${workspace_loc:/${ProjName}/LoRaMESH/src}
${workspace_loc:/${ProjName}/LoRaMESH/src/core}
${workspace_loc:/${ProjName}/LoRaMESH/src/adapters/stm32_hal}
Outro ajuste importante fica em adapters/stm32_hal/Stm32HalUartTransport.h. No estado atual, a linha de include da HAL está apontando para:
#include "stm32l4xx_hal.h"Esse include deve ser alterado conforme a família real do seu microcontrolador. Em outras palavras, se o seu projeto usa outra família STM32, você precisa trocar esse include para a HAL correspondente.
Porque o projeto já passou da fase em que ESP-IDF e STM32 HAL eram apenas intenção. Ambas as frentes já foram testadas e validadas, então o README precisa refletir isso.
Ao mesmo tempo, vale manter estas seções destacadas porque elas não têm exatamente o mesmo nível de fricção na integração:
- em ESP-IDF, a validação do core mostra que a arquitetura funciona bem nesse ambiente
- em STM32 HAL, além da validação, existe uma preparação manual importante do projeto
Deixar isso explícito no README ajuda a manter o direcionamento do repositório honesto, técnico e coerente com a realidade de uso.
O valor desta biblioteca aparece quando ela sai do exemplo simples e entra em aplicações reais. Alguns cenários muito naturais para o projeto são:
- automação rural com bombas e válvulas
- leitura remota de sensores analógicos e digitais
- monitoramento distribuído em áreas sem infraestrutura tradicional de rede
- gateways Linux ou Raspberry Pi para provisionamento e supervisão
- controle de saídas remotas em nós distribuídos
- telemetria de baixa taxa em redes privadas
- prototipagem rápida com a camada Python sobre o mesmo core
- ferramentas de manutenção, diagnóstico e inventário de módulos
MIT License
Autor: Gustavo Cereza
- GitHub: https://github.com/GustavoCereza
- LinkedIn: https://www.linkedin.com/in/gustavo-cereza/
- Site: https://elcereza.com
Empresa: Radioenge
- Site: https://www.radioenge.com.br
- Documentação e outras informações: https://www.radioenge.com.br/produto/modulo-loramesh/
- Produto LoRaMesh: https://meli.la/2dSTW3C
O LoRaMESH C++ não foi pensado como uma biblioteca superficial. Ele existe para ser a base técnica real do projeto, concentrando a lógica de protocolo, configuração, leitura de estado do módulo, controle remoto de IO e reaproveitamento entre plataformas.
Essa é justamente a força da biblioteca: você mantém uma base nativa sólida e coerente, mas consegue expandir esse mesmo núcleo para Arduino, Linux, Raspberry Pi, ESP-IDF, STM32 HAL e Python sem reinventar a lógica principal a cada novo ambiente.
Se a intenção é construir aplicações reais com módulos LoRaMESH, controlar nós remotos, ler sensores, acionar GPIOs, provisionar dispositivos e integrar firmware com gateways ou automações, este core C++ é o ponto central do projeto.