Introdução
Imagine a arquitetura tradicional de uma rede Wi-Fi em que é desejável que todos os dispositivos de uma rede local estejam conectados a um roteador para que permita o acesso à Internet ou entre dispositivos da mesma rede (Figura 1). Parece uma topologia aceitável, até que não seja mais.
Agora, imagine que alguns dispositivos dessa rede local estão fora do raio de cobertura do roteador e que não conseguem estabelecer uma comunicação confiável com ele, portanto ficando sem acesso à internet (Figura 2). A depender da aplicação, eles não podem simplesmente ficar sem destinar seus pacotes, certo? E se eles forem responsáveis por indicar a pane em algum sistema crítico, como ficaria?
Uma solução para isso reside na topologia de uma rede mesh, em que esses dispositivos fora da área de cobertura do roteador são capazes de comunicar com seus pares mais próximos para que estabeleçam uma rota de dados até o roteador (Figura 3).
Esse tipo de rede não é limitado apenas a redes Wi-Fi. Elas podem ser usadas em redes Bluetooth, ZigBee, Thread, LoRa e etc. Em geral, há a necessidade de uma aplicação cuja rota de comunicação é descentralizada. Como estamos na era da Internet das Coisas (IoT), com cada vez mais dispositivos móveis, essa situação tende a ser cada vez mais comum. Então, que tal praticarmos um pouco?
O tema de roteamento e configuração de redes mesh é complexo e pode se estender o suficiente para ficar cansativo. Desse modo, o objetivo deste tutorial é elaborar um guia simples para que você possa dar o pontapé inicial para trabalhar com redes mesh usando o ESP32. A topologia de rede da Figura 3 será usada como base para o exemplo.
Para isso, a proposição desse tutorial é utilizar o protocolo ESP-WIFI-MESH da Espressif para redes mesh, que é capaz de interconectar dispositivos de uma rede local sem fio (WLAN). Esse protocolo é desenvolvido em cima do protocolo Wi-Fi, portanto, vai utilizar o próprio rádio de 2.4GHz do ESP32 e os frames do Wi-Fi para sua implementação. O melhor de tudo é que ele tem autonomia de auto reconfigurar a rede e detectar e corrigir falhas de roteamento, permitindo a pessoa desenvolvedora de firmware focar no código da sua aplicação enquanto a parte de conectividade é resolvida automaticamente.
O objetivo deste tutorial é explorar os recursos automáticos de configuração e manutenção do ESP-WIFI-MESH. Mas é importante ressaltar que é possível fazer isso manualmente de acordo com as necessidades da sua aplicação.
Este tutorial será dividido em duas partes. Na primeira parte (ESP-WIFI-MESH: Construindo uma rede mesh com o ESP32 – Parte 1) será abordada a parte de inicialização da rede e a formação da topologia (construção) da rede. A segunda parte (ESP-WIFI-MESH Funcionamento de uma rede mesh com ESP32 – Parte 2) abordará a comunicação interna e externa à rede, bem como os mecanismos de correção automática de falhas (funcionamento em regime permanente).
Material
Este tutorial irá abordar a primeira parte e para segui-lo à risca será necessário:
- Máquina de desenvolvimento (testes realizados em uma máquina Ubuntu 20.04) com funcionalidade Wi-Fi Hotspot para funcionar como o roteador da rede mesh e para programação dos dispositivos;
- ESP-IDF v5.1;
- Recomendado três (3) kits de desenvolvimento do ESP32;
- Cabos USB para programar e alimentar os kits do ESP32;
No momento, não há preocupação com o tipo de dado que vai trafegar na rede, se são leituras de sensores ou requisições a um servidor remoto. O importante é estabelecer a estrutura da rede mesh e garantir que seus mecanismos de manutenção estão funcionando corretamente.
Inicializando a rede Mesh
Para esta demonstração será usado como base o exemplo examples/mesh/internal_communication do ESP-IDF. Este exemplo realiza a configuração inicial da rede, possui rotinas para comunicação entre dispositivos e tem um procedimento pronto para lidar com a maioria dos eventos suportados pelo módulo mesh.
Existem cerca de 26 tipos de eventos que tratam de conexão/desconexão de nós e configurações da rede e são mapeados no arquivo de cabeçalho esp_mesh.h. Implementar o código para lidar com todos esses eventos é uma tarefa cansativa, portanto é válido usar o que está disponível no exemplo e editá-lo conforme necessário.
O código para inicialização da rede mesh é executado na função principal (void app_main(void)) e pode ser comum entre todos os nós da rede. Antes de tudo, as bibliotecas abaixo serão necessárias para a aplicação e vale a pena manter uma das variáveis globais do exemplo base para garantir a compatibilidade com as funções do exemplo base:
// bibliotecas base
#include <string.h>
#include <inttypes.h>
#include "esp_log.h"
#include "esp_event.h"
// conectividade wi-fi e mesh
#include "nvs_flash.h"
#include "esp_wifi.h"
#include "esp_mac.h"
#include "esp_mesh.h"
#include "esp_mesh_internal.h"
// tarefas do freeRTOS
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *MESH_TAG = "mesh_main";
static bool is_mesh_connected = false;
static mesh_addr_t mesh_parent_addr;
static int mesh_layer = -1;
static esp_netif_t *netif_sta = NULL;O próximo passo é inicializar a pilha de protocolos TCP/IP do LwIP e o Wi-Fi com as configurações padrão. É importante cadastrar a função ip_event_handler para lidar com os eventos IP (função já implementada no exemplo base:examples/mesh/internal_communication).
Não será necessário passar as configurações da rede sem fio do roteador, neste momento, pois elas serão submetidas juntamente com as configurações da rede mesh:
ESP_ERROR_CHECK(nvs_flash_init()); // inicializar particao NVS
ESP_ERROR_CHECK(esp_netif_init()); // inicializar pilha de protocolos TCP/IP (LwIP)
ESP_ERROR_CHECK(esp_event_loop_create_default()); // inicializar modulo de eventos para lidar com eventos IP e mesh
ESP_ERROR_CHECK(esp_netif_create_default_wifi_mesh_netifs(&netif_sta, NULL)); // criar interface de rede
wifi_init_config_t cfg_wifi = WIFI_INIT_CONFIG_DEFAULT(); // configuracao padrao para wifi
ESP_ERROR_CHECK(esp_wifi_init(&cfg_wifi)); // submeter configuracoes wi-fi
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &ip_event_handler, NULL)); // registrar funcao para lidar com eventos na stack IP
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_FLASH)); // definir o tipo de armazenamento utilizado pela API do Wi-Fi
ESP_ERROR_CHECK(esp_wifi_start()); // iniciar wi-fiEm seguida será inicializada a rede mesh, mas antes algumas variáveis devem ser definidas:
static const uint8_t MESH_ID[6] = { 0x77, 0x77, 0x77, 0x77, 0x77, 0x77}; // endereço MAC da rede mesh (mesh ID)
static const char ROUTER_SSID[] = "<ssi_da_sua_rede_wifi>"; // SSID da rede Wi-Fi do roteador
static const char ROUTER_PWD[] = "<senha_da_sua_rede_wifi>"; // senha da rede Wi-Fi do roteador
static const char MESH_AP_PWD[] = "<senha_para_o_AP_deste_dispositivo"; // senha do ponto de acesso (AP) deste dispositivoO identificador da rede mesh é um endereço MAC que auxilia na distinção de uma rede quando múltiplas coexistem. Nesse momento são definidas as credenciais da rede sem fio do roteador (sinta-se à vontade para usar o menuconfig do ESP32 para defini-las ou qualquer outro método de escolha). É essencial que todos os nós da rede mesh possuam ciência da rede do roteador para que façam os cálculos de qualidade do sinal relativos ao roteador e que, eventualmente, se comuniquem com a internet. Por fim é importante definir a senha do ponto de acesso (AP) do dispositivo em questão. Essa é uma medida de segurança mínima para que apenas os nós da rede interajam entre eles para quando um nó no modo estação entre em contato com um nó no modo softAP.
Então a rede é inicializada e a rotina mesh_event_handler é cadastrada para lidar com os eventos mesh. Para esta demonstração será utilizada a implementação base dessa rotina feita pelo exemplo examples/mesh/internal_communication e ela será edita conforme necessário:
ESP_ERROR_CHECK(esp_mesh_init()); // inicializar mesh
ESP_ERROR_CHECK(esp_event_handler_register(MESH_EVENT, ESP_EVENT_ANY_ID, &mesh_event_handler, NULL)); // registrar funcao para lidar com eventos meshEm seguida serão realizadas as configurações da rede mesh. Portanto, inicialize a estrutura mesh_cfg_t de configuração com os valores padrões e edite em cima do que for necessário:
mesh_cfg_t cfg_mesh = MESH_INIT_CONFIG_DEFAULT(); // configuracao padrao para a meshAtribua o identificador MAC da rede mesh especificado anteriormente (use memcpy para copiar o array inteiro para o campo de destino):
// configuracoes da rede mesh
memcpy((uint8_t *) &cfg_mesh.mesh_id, MESH_ID, 6); // atribuindo mesh ID para struct de configuracaoEm seguida são as configurações do roteador, como o canal Wi-Fi a ser usado e as credenciais de rede:
cfg_mesh.channel = 0; // canal do roteador
cfg_mesh.router.ssid_len = strlen(ROUTER_SSID); // tamanho da string do SSID
memcpy((uint8_t *) &cfg_mesh.router.ssid, ROUTER_SSID, cfg_mesh.router.ssid_len); // atribuindo SSID da rede do roteador para struct de configuracao
memcpy((uint8_t *) &cfg_mesh.router.password, ROUTER_PWD, strlen(ROUTER_PWD)); // atribuindo senha da rede do roteador para struct de configuracaoPor fim são estabelecidas as configurações do ponto de acesso (softAP), que incluem o modo de autenticação (será usado WPA2-PSK, mas outros modos são suportados), a quantidade máxima de conexões mesh (se baseando na arquitetura da Figura 3, como o nó principal possui duas conexões, esse valor também será 2) e não-mesh (não está previsto na arquitetura desta demonstração a conexão desse tipo de nó) suportadas e a atribuição da senha do softAP:
ESP_ERROR_CHECK(esp_mesh_set_ap_authmode(WIFI_AUTH_WPA2_PSK)); // modo de autenticacao do softAP
cfg_mesh.mesh_ap.max_connection = 2; // numero max. de estacoes conectadas ao softAP
cfg_mesh.mesh_ap.nonmesh_max_connection = 0; // numero max. de conexoes nao-mesh
memcpy((uint8_t *) &cfg_mesh.mesh_ap.password, MESH_AP_PWD, strlen(MESH_AP_PWD)); // atribuir senha do softAPSubmeta as configurações e inicie a rede mesh:
ESP_ERROR_CHECK(esp_mesh_set_config(&cfg_mesh)); // submeter configuracoes mesh
ESP_ERROR_CHECK(esp_mesh_start()); // iniciar mesh
ESP_ERROR_CHECK(esp_mesh_set_self_organized(true, true)); // rede autoconfiguravel (padrao)
ESP_LOGI(MESH_TAG, "rede mesh inicializada com sucesso");Após seu início, a rede será gerenciada por seus eventos que serão tratados na rotina cadastrada para tal (mesh_event_handler) e tarefas que eventualmente podem ser criadas de acordo com a necessidade da aplicação. Aqui a rotina principal encontra seu fim.
Construção automática da topologia da rede
Os nós da rede serão dispostos para garantir que a topologia inicial da rede se assemelhe a da Figura 3. A rede mesh admite apenas um nó principal (root) e este é o nó que está conectado diretamente ao roteador e dá vazão aos pacotes da rede local para a internet. Abaixo do nó principal estão os nós pais (parents) e cada um desses podem ter nós filhos (child), que são nós pais intermediários e o último nó filho é chamado de leaf.
No exemplo da Figura 3 o nó A seria o nó principal e subordinados a ele estão os nós B e C. Se a profundidade dessa rede for definida como 2, ou seja, permitindo apenas duas camadas, isso significa dizer que B e C são nós do tipo leaf (pois estão na última camada) e não aceitam conexões com novos filhos. Mas se a profundidade for maior que 2, esses nós serão pais intermediários, pois em qualquer momento podem aceitar novas conexões à jusante (downstream). Nessa topologia a comunicação entre nó e roteador ou nó e nó deve passar pelo nó principal, que acaba sendo o único nó pai dessa rede.
A construção da rede mesh inicia pela definição do nó principal, seguido das conexões à jusante desse nó. Em termos da construção automática da rede, esse nó principal é definido a partir de uma votação com a participação de todos os nós presentes para eleger aquele que possui a melhor RSSI (received signal strength indicator) em relação ao roteador.
A capacidade da rede ser autoconfigurável é habilitada por padrão, mas pode ser ativada/desativada com a função esp_mesh_set_self_organized. Um detalhe importante é que não é recomendado a chamada de funções da API de Wi-Fi enquanto a rede mesh estiver autoconfigurável, portanto, é necessário desativar e reativar após o uso da API de Wi-Fi, para evitar que as APIs interfiram entre si, pois a rede mesh também usa a API de Wi-Fi internamente.
Durante a fase de eleição, cada nó irá transmitir seu endereço MAC (identificador único de cada dispositivo) e RSSI em relação ao roteador (métrica de qualidade). Além disso, cada nó também fará uma varredura pelos pacotes de nós vizinhos por pacotes de outro nó com RSSI melhor que a do nó em varredura. Quando um nó encontra outro melhor que ele (maior RSSI), ele passará a transmitir os pacotes desse nó, ao invés do dele (essa é a forma que o nó vai decidir seu voto).
Vamos ilustrar o processo de eleição por meio de diagramas e logs automáticos da rede mesh (logs com tag mesh são internos da implementação da API) para esclarecer melhor o procedimento. Antes de tudo, os endereços MAC serão substituídos por nomes como MAC_NODE_A, para o nó A e assim por diante; com o intuito de facilitar a visualização.
Vide os logs do nó que foi eleito o principal (nó A na Figura 3):
I (5768) mesh: [SCAN:1/10]rc[128][MAC_NODE_A,-28], self[MAC_NODE_A,-28,reason:0,votes:1,idle][mine:1,voter:3(0.33)percent:0.90][128,1,MAC_NODE_A]
I (7038) mesh: [SCAN:5/10]rc[128][MAC_NODE_A,-28], self[MAC_NODE_A,-29,reason:0,votes:2,idle][mine:2,voter:3(0.67)percent:0.90][128,2,MAC_NODE_A]
I (8638) mesh: [SCAN:10/10]rc[128][MAC_NODE_A,-28], self[MAC_NODE_A,-29,reason:0,votes:3,idle][mine:3,voter:3(1.00)percent:0.90][128,3,MAC_NODE_A]O trecho self[MAC_NODE_A,-28,reason:0,votes:1,idle][mine:1,voter:3(0.33)percent:0.90] mostra as informações principais do nó analisado, i.e. MAC, RSSI, os votos que o nó está recebendo (votes ou mine), a quantidade de eleitores (voter) e a porcentagem de votos para eleger como nó principal (percent), que é definido como 90% por padrão. Nesse primeiro ciclo de varredura o nó apenas recebeu seu próprio voto, que lhe rendeu 33% dos votos. Observe que após mais alguns ciclos de varredura (5/10) o nó A possui 2 votos (67% dos votos) e no final (10/10) possui todos os votos (100%), portanto sendo eleito como nó principal.
Do ponto de vista de um dos outros nós, os logs de varredura são como mostrados abaixo:
I (5758) mesh: [SCAN:1/10]rc[128][MAC_NODE_B,-33], self[MAC_NODE_B,-33,reason:0,votes:1,idle][mine:1,voter:1(1.00)percent:0.90][128,1,MAC_NODE_B]
I (6088) mesh: [SCAN:2/10]rc[127][MAC_NODE_C,-33], self[MAC_NODE_B,-32,reason:0,votes:0,idle][mine:0,voter:2(0.00)percent:0.90][127,1,MAC_NODE_C]
I (6418) mesh: [SCAN:3/10]rc[127][MAC_NODE_A,-33], self[MAC_NODE_B,-32,reason:0,votes:0,idle][mine:0,voter:3(0.00)percent:0.90][127,1,MAC_NODE_A]
I (8698) mesh: [SCAN:10/10]rc[127][MAC_NODE_A,-33], self[MAC_NODE_B,-33,reason:0,votes:0,idle][mine:0,voter:3(0.00)percent:0.90][127,3,MAC_NODE_A]Esse se trata do nó B. O endereço MAC mais à esquerda é aquele em que o nó em questão está votando. Quando o nó A foi analisado, esse endereço MAC sempre foi o dele, pois ele sempre se julgou ideal para ser o nó principal. Mas no caso desse nó B, pode-se observar que na primeira varredura ele votou em si próprio (1/10). Na segunda varredura (2/10) ele votou no nó C. Apesar de termos visto que o nó A possui melhor RSSI, essa decisão do nó B votar no C foi porque no momento da eleição só existiam esses dois ativados. Podemos conferir isso a partir do campo da quantidade de eleitores que é apenas 2 (voter). Mas nas varreduras seguintes (3/10 até 10/10) o nó A foi a opção para nó principal do ponto de vista do nó B.
Para finalizar, vejamos como foi o processo de eleição do ponto de vista do nó C:
I (5768) mesh: [SCAN:1/10]rc[128][MAC_NODE_C,-30], self[MAC_NODE_C,-30,reason:0,votes:1,idle][mine:1,voter:1(1.00)percent:0.90][128,1,MAC_NODE_C]
I (6088) mesh: [SCAN:2/10]rc[128][MAC_NODE_C,-29], self[MAC_NODE_C,-29,reason:0,votes:1,idle][mine:1,voter:2(0.50)percent:0.90][128,1,MAC_NODE_C]
I (7048) mesh: [SCAN:5/10]rc[128][MAC_NODE_C,-28], self[MAC_NODE_C,-29,reason:0,votes:1,idle][mine:1,voter:3(0.33)percent:0.90][128,1,MAC_NODE_C]
I (7368) mesh: [SCAN:6/10]rc[127][MAC_NODE_A,-28], self[MAC_NODE_C,-29,reason:0,votes:0,idle][mine:0,voter:3(0.00)percent:0.90][127,2,MAC_NODE_A]
I (8648) mesh: [SCAN:10/10]rc[127][MAC_NODE_A,-28], self[MAC_NODE_C,-30,reason:0,votes:0,idle][mine:0,voter:3(0.00)percent:0.90][127,3,MAC_NODE_A]Apenas na sexta varredura (6/10) o nó C decidiu votar no A ao invés de si próprio. A disputa foi acirrada, pois ambos possuem RSSI semelhante (entre -29 e -28) e que pode flutuar durante os ciclos de varredura.
A Figura 4 exemplifica o processo de eleição em três simples etapas. (1) Inicialmente o nó A não está conectado à rede, portanto B e C estão decidindo o melhor entre si. (2) Porém quando A se conecta à rede, os demais nós retém seus votos para si próprios e analisam a nova situação da rede. (3) Por fim, julgam que o nó A deve ser o principal.
Após a definição do nó principal, os demais vão tentar se conectar a ele e, caso não estejam na sua área de cobertura, vão estabelecer conexões entre os pais intermediários seguindo esse padrão até que a rede se complete (respeitando a quantidade máxima de conexões por nó). A rede vai tentar se manter com a menor quantidade de camadas possíveis.
Dando continuidade ao exemplo acima, quando o nó principal foi decidido, este primeiramente se conectou ao nó pai, que seria o roteador. Esse evento dispara o evento MESH_EVENT_PARENT_CONNECTED na função mesh_event_handler implementada no exemplo examples/mesh/internal_communication. Isso faz com que o seguinte log apareça:
I (11428) mesh_main: <MESH_EVENT_PARENT_CONNECTED>layer:0-->1, parent:MAC_ROTEADOR<ROOT>, ID:77:77:77:77:77:77, duty:0
I (11468) mesh_main: <MESH_EVENT_ROOT_ADDRESS>root address:MAC_NODE_AAssim o nó se conectou diretamente ao endereço MAC do roteador (MAC_ROTEADOR). O roteador está na camada 0 e esse nó principal está na camada 1. Os demais nós serão conectados em camadas mais profundas.
Em seguida, o nó A assimila B e C na sua tabela de roteamento como nós filhos na camada 2 (observe que o nó também se conectou ao roteador no modo station):
W (11978) mesh_main: <MESH_EVENT_ROUTING_TABLE_ADD>add 1, new:2, layer:1
I (11978) mesh_main: <MESH_EVENT_CHILD_CONNECTED>aid:1, MAC_NODE_B
W (12018) mesh_main: <MESH_EVENT_ROUTING_TABLE_ADD>add 1, new:3, layer:1
I (12098) mesh_main: <MESH_EVENT_CHILD_CONNECTED>aid:2, MAC_NODE_C
I (37107) mesh_main: <IP_EVENT_STA_GOT_IP>IP:<ip_do_node_principal>Dessa forma a topologia de rede apresenta na Figura 3 está formada e pronta para uso.
Conclusão
A biblioteca do ESP-WIFI-MESH traz uma grande facilidade em desenvolver aplicações com uma rede mesh com o ESP32, pois esta permite que a topologia da rede seja formada automaticamente sem que a pessoa desenvolvedora de firmware se preocupe com os detalhes dos algoritmos de formação da rede. Se desejável, é possível configurar a rede manualmente, porém será necessário mais trabalho no desenvolvimento do firmware (essa abordagem não foi coberta aqui, pois o propósito foi envolver a rede mesh de uma forma sutil em uma aplicação com ESP32). Se encaixariam nesses casos aplicações mais criteriosas em relação à rede mesh, porém a abordagem automática é suficiente para aplicações simples cuja principal prioridade é estabelecer uma rota até a internet.
Na segunda parte desse tutorial (ESP-WIFI-MESH Funcionamento de uma rede mesh com ESP32 – Parte 2) será abordada a comunicação interna e externa dos nós da rede mesh e formas e os tipos de problemas que podem ocorrer e como o protocolo ESP-WIFI-MESH lida com eles.
Código completo em: https://github.com/Lwao/esp-wifi-mesh-demo
Referências
Para mais informações, visite os links (a documentação da Espressif sobre o ESP-WIFI-MESH é rica em detalhes para uma compreensão total do módulo):
- ESP-WIFI-MESH
- ESP-WIFI-MESH Programming Guide
- ESP-WIFI-MESH Guide
- ESP-IDF Mesh Internal Communication Example
Imagem de destaque retirada de Espressif






