Introdução
Após a inicialização e construção da rede mesh da parte 1 deste tutorial (ESP-WIFI-MESH: Construindo uma rede mesh com o ESP32 – Parte 1) resta pôr em prática o seu uso.
Em relação à comunicação há duas possibilidades: comunicação interna e externa à rede mesh. A comunicação interna consiste na comunicação entre nós da própria rede mesh. As principais APIs para lidar com essa comunicação são a esp_mesh_send para envio de pacotes e esp_mesh_recv para recebimento de pacotes.
Toda a comunicação externa deverá passar pelo nó principal, pois será apenas que este que terá contato direto com o roteador. É previsto na API sp_mesh_send a possibilidade de realizar a comunicação com a rede externa, mas aparentemente esse recurso não é totalmente funcional até a versão v5.1 do ESP-IDF. A solução que a comunidade desenvolveu para superar esse problema é realizar uma comunicação por sockets TCP/IP ou qualquer outro meio, como instanciando um cliente HTTP para enviar solicitações, etc. Do ponto de vista da rede interna o nó principal se encarregaria de concentrar todas as solicitações dos demais nós da rede e encaminhar para a aplicação remota na rede externa, como também distribuir os pacotes da rede externa para os nós apropriados.
De cara, nota-se uma limitação nesse tipo de rede, pois como apenas o nó principal concentra toda a comunicação externa, acabaria criando um gargalo para a velocidade efetiva da comunicação. Mas isso é um risco que deve ser avaliado na concepção do projeto.
Além da comunicação, outra parte importante da rede mesh do protocolo ESP-WIFI-MESH é sua capacidade de correção automática de falhas. As duas principais falhas consistem no desaparecimento de nós da rede ou instabilidade na comunicação.
No caso do desaparecimento de nós da rede, se este for o nó principal, será necessário convocar uma nova eleição para estabelecimento desse nó. Em topologias mais complexas, com vários nós pais intermediários, os nós filhos irão estabelecer conexão com outros pais.
No caso da instabilidade, há a possibilidade de que a RSSI do nó principal degrade com o tempo, fazendo com que o nó não seja mais a escolha ideal para comandar a rede, porém este ainda continuará sendo o nó principal. É ideal que o nó principal, quando identificado a degradação da RSSI, solicite uma nova votação para garantir a qualidade da rede.
O objetivo desta segunda parte do tutorial é estabelecer a comunicação entre a rede mesh do ponto de vista interno e externo a ela, bem como compreender os mecanismos de correção automática de falhas em prática.
Material
Para esse tutorial você vai precisar de:
- 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;
Como este tutorial dá continuidade ao Construindo uma rede WiFi Mesh com o ESP32 – Parte 1, o código-fonte apresentado aqui será uma continuidade direta do que foi trabalhado antes. Inclusive também será usada a mesma topologia de rede que foi apresentada na Figura 3 do primeiro tutorial.
Comunicação
Comunicação interna
Para a comunicação serão usadas duas tarefas, uma de envio (tx_task) e outra de recebimento (rx_task). Estas tarefas serão inicializas dentro do handler para o evento MESH_EVENT_PARENT_CONNECTED dentro da função mesh_event_handler. Se você está seguindo as orientações desse tutorial desde sua primeira parte, significa dizer que a implementação da sua função mesh_event_handler está igual à do exemplo examples/mesh/internal_communication. Esse exemplo usa a função esp_mesh_comm_p2p_start para inicializar suas tarefas de envio/recebimento, mas neste caso essa função não será necessária e poderemos comentá-la fora. Então a inicialização das nossas tarefas deverá ser feita da seguinte forma (apenas modificando o trecho que lida com o evento MESH_EVENT_PARENT_CONNECTED):
void mesh_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
...
case MESH_EVENT_PARENT_CONNECTED: {
...
// esp_mesh_comm_p2p_start();
xTaskCreate(tx_task, "TX_TASK", configMINIMAL_STACK_SIZE+2048, NULL, configMAX_PRIORITIES-3, NULL);
xTaskCreate(rx_task, "RX_TASK", configMINIMAL_STACK_SIZE+2048, NULL, configMAX_PRIORITIES-3, NULL);
}
break;
...
O evento MESH_EVENT_PARENT_CONNECTED foi utilizado, pois independente do nó, seja principal ou não, ele terá um nó pai, que pode ser outro dispositivo da rede ou o roteador (no caso do nó principal).
A implementação da tarefa de recebimento será simples, pois consistirá apenas em esperar por mensagens com a função esp_mesh_recv e imprimi-las nos logs:
#define RX_BUF_SIZE 256
void rx_task(void *param)
{
uint8_t rx_buf[RX_BUF_SIZE] = {0};
int flag = 0;
mesh_addr_t sender;
mesh_data_t data;
data.data = rx_buf;
data.size = RX_BUF_SIZE;
while(1)
{
if(esp_mesh_recv(&sender, &data, portMAX_DELAY, &flag, NULL, 0) == ESP_OK){
ESP_LOGI(MESH_TAG, "remetente: "MACSTR", msg: %s", MAC2STR(sender.addr), data.data);
} else {
ESP_LOGE(MESH_TAG, "Erro em inicializar tarefa RX.");
break;
}
}
vTaskDelete(NULL);
}
A estrutura mesh_addr_t é usada para armazenar o endereço MAC do remetente e a estrutura mesh_data_t é usada para receber os dados enviados (o buffer pré-alocado é passado para essa estrutura). A macro portMAX_DELAY do FreeRTOS garante que a chamada da função fique presa infinitamente até que haja dados para serem retornados. A variável flag é usada para receber os valores MESH_DATA_FROMDS ou MESH_DATA_TODS, permitindo identificar se o pacote recebido veio da rede externa ou interna, respectivamente.
É importante notar que no trecho que imprime a mensagem no terminal há duas macros: MACSTR e MAC2STR. Como o endereço MAC é um array de 6 hexadecimais, essas macros são usadas, respectivamente, para indicar que serão impressos 6 hexadecimais e para repartir o array de 6 posições em seus valores nas posições de 0 a 5, funcionando como utilitários para lidar mais facilmente com endereços MAC
Para a comunicação interna entre os nós conhecidos, pode-se assumir que os endereços MAC de todos os nós se conhecem. Então os pacotes poderão ser enviados diretamente para cada nó por meio da função esp_mesh_send. A tarefa de envio se parecerá com o seguinte:
#define TX_BUF_SIZE 128
void tx_task(void *param)
{
uint8_t tx_buf[TX_BUF_SIZE] = {0};
static const uint8_t MAC_A[6] = {mac_node_a};
static const uint8_t MAC_B[6] = {mac_node_b};
static const uint8_t MAC_C[6] = {mac_node_c};
mesh_addr_t A, B, C;
memcpy((uint8_t *) &A, MAC_A, 6);
memcpy((uint8_t *) &B, MAC_B, 6);
memcpy((uint8_t *) &C, MAC_C, 6);
static const char msg[] = "Hello!";
memcpy((uint8_t *) &tx_buf, msg, strlen(msg));
mesh_data_t data;
data.data = tx_buf;
data.size = TX_BUF_SIZE;
while(1)
{
esp_mesh_send(&A, &data, MESH_DATA_P2P, NULL, 0);
esp_mesh_send(&B, &data, MESH_DATA_P2P, NULL, 0);
esp_mesh_send(&C, &data, MESH_DATA_P2P, NULL, 0);
vTaskDelay(pdMS_TO_TICKS(10000));
}
}
As mensagens são enviadas a cada 10 segundos. A flag MESH_DATA_P2P é utilizada quando o pacote é destinado para um dispositivo na rede interna. Existem várias outras opções, que no geral tratam de opções de otimização para facilitar encontrar a melhor rota para o pacote.
Como cada nó envia o “Hello!” (modifique essa mensagem de acordo com as necessidades da sua aplicação) para seus pares na rede e eles mesmos, seus logs serão bem semelhantes:
I (12599) mesh_main: remetente: MAC_NODE_B, msg: Hello!
I (18949) mesh_main: remetente: MAC_NODE_C, msg: Hello!
I (19719) mesh_main: remetente: MAC_NODE_A, msg: Hello!
I (23009) mesh_main: remetente: MAC_NODE_B, msg: Hello!
I (29349) mesh_main: remetente: MAC_NODE_C, msg: Hello!
I (29859) mesh_main: remetente: MAC_NODE_A, msg: Hello!
I (33239) mesh_main: remetente: MAC_NODE_B, msg: Hello!Esse tipo de comunicação pode ser utilizada em uma aplicação em que os nós internos precisam comunicar entre si e estão separados de forma que não se enxergariam diretamente. A Figura 1 ilustra a ideia por trás dessa aplicação, pois como B e C não estão conectados diretamente, eles se comunicam através do nó A.
Em uma aplicação real seria ideal que novos nós que se conectassem à rede comunicassem sua presença para os demais nós, seja por meio de um broadcast ou, para o caso do ESP-WIFI-MESH, poderiam alertar os pais intermediários e estes alertariam os demais nós, incluindo outros nós filhos, nós pais intermediários ou o nó principal.
Comunicação externa
Para o caso de uma comunicação externa, significa dizer que cada nó pode enviar ou receber pacotes de dispositivos fora da rede mesh. Em resumo, seria um tipo de aplicação de acesso à internet (e.g. solicitações a um servidor remoto, acesso a um banco de dados em nuvem, etc.), em que os nós normalmente estariam fora do raio de cobertura do roteador.
Para esse exemplo serão utilizados dois sockets na máquina de desenvolvimento (rede externa à rede mesh), uma para receber pacotes da rede mesh e outro para enviar pacotes para a rede mesh. Para isso basta utilizar a aplicação Netcat para termos de simplicidade, mas também poderia ser usada uma simples aplicação com sockets em Python.
Em relação à rede mesh, como apenas o nó principal possui acesso ao roteador, será apenas ele que vai interagir com a máquina de desenvolvimento para envio e recebimento de pacotes no caso de uma comunicação bidirecional entre a rede externa. Para distinguir o nó principal dos demais, serão criadas tarefas de envio e recebimento distintas.
O nó principal irá usar a tarefa rx_root_task para receber pacotes internamente e redirecioná-los para a rede externa; e a tarefa tx_root_task para receber pacotes da rede externa e destiná-los internamente. Já os demais nós irão usar as tarefas rx_child_task e tx_child_task para receber e enviar pacotes, respectivamente.
Durante a inicialização de cada nó, essas tarefas serão inicializadas dentro do tratamento do evento MESH_EVENT_PARENT_CONNECTED, como foi feito anteriormente para a comunicação interna. Porém, neste caso, deve-se diferenciar o nó principal dos demais. Para isso existe a API esp_mesh_is_root que retorna positivo se aquele nó for o principal. Assim, a inicialização das tarefas pode ser comandada com a seguinte estrutura condicional:
void mesh_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
...
case MESH_EVENT_PARENT_CONNECTED: {
...
// esp_mesh_comm_p2p_start();
if(esp_mesh_is_root()){
xTaskCreate(tx_root_task, "TX_TASK", configMINIMAL_STACK_SIZE+2048, NULL, configMAX_PRIORITIES-3, NULL);
xTaskCreate(rx_root_task, "RX_TASK", configMINIMAL_STACK_SIZE+2048, NULL, configMAX_PRIORITIES-3, NULL);
} else {
xTaskCreate(tx_child_task, "TX_TASK", configMINIMAL_STACK_SIZE+2048, NULL, configMAX_PRIORITIES-3, NULL);
xTaskCreate(rx_child_task, "RX_TASK", configMINIMAL_STACK_SIZE+2048, NULL, configMAX_PRIORITIES-3, NULL);
}
}
break;
...
A escolha do tamanho da stack e da prioridade das tarefas fica à critério das necessidades da aplicação, porém deve-se ter cuidado para não causar um stack overflow com um tamanho de stack deveras pequeno e ter cuidado para a prioridade dessas tarefas não superar aquela das tarefas mais prioritárias da aplicação de forma que prejudique seu funcionamento.
Por ordem de simplicidade, a implementação da tarefa tx_child_task dos nós filhos segue abaixo:
void tx_child_task(void *param)
{
uint8_t tx_buf[TX_BUF_SIZE] = {0};
static const char msg[] = "Hello!";
mesh_data_t data;
memcpy((uint8_t *) &tx_buf, msg, strlen(msg));
data.data = tx_buf;
data.size = TX_BUF_SIZE;
while(1)
{
esp_mesh_send(NULL, &data, 0, NULL, 0); // enviar mensagem para node principal
vTaskDelay(pdMS_TO_TICKS(10000));
}
vTaskDelete(NULL);
}
Essa tarefa de envio se diferencia da anterior (tx_task) pois o destino dos dados é NULL (primeiro argumento da API esp_mesh_send) ao invés de ser uma estrutura mesh_addr_t com o endereço MAC do nó de destino. Quando esse argumento é nulo, significa dizer que o pacote vai ser roteado diretamente para o nó principal de forma automática. O objetivo da tarefa é enviar a mensagem “Hello!” para o nó principal para que seja direcionado à rede externa.
Já a tarefa de recebimento rx_child_task não é tão diferente da anterior (rx_task), ela só imprime a mensagem e o remetente do pacote recebido (as checagens de erro foram omitidas para termos de simplicidade, porém em uma aplicação real é importante garantir uma correta checagem):
void rx_child_task(void *param)
{
uint8_t rx_buf[RX_BUF_SIZE];
int flag = 0;
mesh_addr_t sender;
mesh_data_t data;
data.data = rx_buf;
data.size = RX_BUF_SIZE;
while(1)
{
if(esp_mesh_recv(&sender, &data, portMAX_DELAY, &flag, NULL, 0) == ESP_OK)
ESP_LOGI(MESH_TAG, "remetente: "MACSTR", msg: %s", MAC2STR(sender.addr), data.data);
}
vTaskDelete(NULL);
}
Em relação às tarefas de envio e recebimento do nó principal, será necessário incluir as bibliotecas de sockets da stack LwIP:
// sockets TCP/IP
#include "lwip/sockets.h"
#include "lwip/sys.h"A tarefa de recebimento de dados da rede interna é implementada da seguinte forma:
void rx_root_task(void *param) // receber pacotes internamente e enviar para rede externa
{
uint8_t rx_buf[RX_BUF_SIZE];
char *tx_buf;
int flag = 0;
mesh_addr_t sender;
mesh_data_t data;
data.data = rx_buf;
data.size = RX_BUF_SIZE;
// configurando endereco do servidor remoto
struct sockaddr_in dest_addr = {
.sin_addr.s_addr = inet_addr("<ip_do_app_da_rede_externa>"),
.sin_family = AF_INET,
.sin_port = htons(<porta_do_app_da_rede_externa_rx>),
};
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
while(1)
{
if(esp_mesh_recv(&sender, &data, portMAX_DELAY, &flag, NULL, 0) == ESP_OK){
asprintf(&tx_buf, "remetente: "MACSTR", msg: %s\n", MAC2STR(sender.addr), data.data); // prepara buffer para envio
ESP_LOGI(MESH_TAG, "%s", tx_buf); // imprime buffer nos logs
sendto(sock, tx_buf, strlen(tx_buf), 0, (struct sockaddr *)&dest_addr, sizeof(dest_addr)); // enviar buffer para servidor remoto
free(tx_buf);
}
}
vTaskDelete(NULL);
}Para essa aplicação estão sendo usados sockets UDP. Para que o socket do nó principal conheça o socket da aplicação externa, é necessário preencher a estrutura dest_addr com as informações de endereço do servidor remoto, ou seja, seu IPV4 (pode ser obtido com o comando ifconfig) e a porta da aplicação. A porta é definida arbitrariamente, mas é recomendado que seja um valor alto para não ter conflito com os demais serviços que rodam em portas de números menores.
Dentro do loop infinito da tarefa, o nó irá esperar por pacotes da rede interna e, quando recebido algum, irá usar a função asprintf (essa função vai escrever a string formada pela expressão para o ponteiro passado como argumento e toda a tarefa de alocação de memória será feita automaticamente) para construir a mensagem; em seguida irá imprimi-la e enviá-la para a rede externa. Por fim a memória alocada para o ponteiro é liberada para o próximo uso. Novamente, as checagens de erro foram omitidas para manter o código simples.
A aplicação do servidor remoto pode ser feita usando o Netcat na máquina de desenvolvimento como o comando a seguir: nc -ukl 10.42.0.1 9999; que neste caso diz respeito a um servidor UDP (-u) que escuta por pacotes (-kl) no IP 10.42.0.1 (esse seria o valor da string <ip_do_app_da_rede_externa> na tarefa rx_root_task) e porta 9999 (<porta_do_app_da_rede_externa_rx>).
Já a tarefa de envio é implementada da seguinte forma:
void tx_root_task(void *param) // receber pacotes da rede externa e enviar internamente
{
uint8_t rx_buf[RX_BUF_SIZE] = {0};
// variavies para rotear pacotes
const int node_num = 3; // quantidade max. de nodes na rede
mesh_addr_t route_table[node_num]; // tabela de roteamento
int route_table_size = 0; // tamanho da tabela de roteamento
mesh_data_t data; // dados a serem enviados
data.data = rx_buf;
data.size = RX_BUF_SIZE;
// variaveis para identificar a fonte do pacote recebido
struct sockaddr_storage source_addr;
socklen_t socklen = sizeof(source_addr);
// configurando endereco do servidor local
struct sockaddr_in dest_addr = {
.sin_addr.s_addr = inet_addr("0.0.0.0"),
.sin_family = AF_INET,
.sin_port = htons(<porta_do_app_da_rede_externa_tx>),
};
// criar socket e vincula-lo ao ip:porta deste node
int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
bind(sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr));
while(1)
{
recvfrom(sock, rx_buf, sizeof(rx_buf)-1, 0, (struct sockaddr *)&source_addr, &socklen); // esperar mensagem
ESP_LOGI(MESH_TAG, "%s", rx_buf); // imprime buffer nos logs
// preencher tabela de roteamento com endereco MAC dos nodes filhos
esp_mesh_get_routing_table((mesh_addr_t *) &route_table, node_num*6, &route_table_size);
// iterar pela tabela e enviar dados obtidos de aplicacao externa para todos os nodes
for (int i=0; i < route_table_size; i++)
esp_mesh_send(&route_table[i], &data, MESH_DATA_P2P, NULL, 0);
memset(rx_buf, 0, sizeof(rx_buf)); // limpar buffer para novo uso
}
vTaskDelete(NULL);
}
Essa tarefa irá receber os dados da aplicação externa em um socket UDP. A configuração do socket é feito de forma semelhante ao socket da tarefa anterior, só que dessa vez o endereço IP será do dispositivo local, que neste caso será 0.0.0.0 e a porta deverá ser diferente da tarefa anterior (<porta_do_app_da_rede_externa_tx>).
Para essa tarefa serão inicializadas algumas variáveis para a comunicação interna. O array mesh_addr_t route_table[node_num] será responsável por manter o endereço MAC dos nós filhos que receberão as mensagens redirecionadas da rede externa.
No loop infinito da tarefa, a função recvfrom irá receber as mensagens destinadas ao socket vinculado ao IP local e a porta, então preencherá o buffer de recebimento. A mensagem será impressa no console do nó principal, mas também será destinada aos demais nós. Mas antes disso a API esp_mesh_get_routing_table é usada para preencher a tabela de roteamento com o endereço MAC dos nós filhos. Assim a mensagem será transmitida para todos os nós com a API esp_mesh_send. Por fim, o buffer é limpo com a função memset para seu próximo uso.
A aplicação do cliente remoto com o Netcat na máquina de desenvolvimento pode ser chamada com o comando a seguir: nc -u 10.42.0.127 9998; que neste caso diz respeito a um cliente UDP (-u) que tenta acessar o IP 10.42.0.90 (esse é o IP do nó principal da rede mesh e pode ser descoberto pelos logs ou, em um caso mais próximo da realidade, seria enviado pelo nó principal para a aplicação externa e depois incorporado em outras aplicações) e porta 9998 (<porta_do_app_da_rede_externa_tx> que deve ser diferente da porta usada anteriormente).
Com esse novo firmware carregado nos nós da rede, a construção automática da rede irá acontecer e, quando definido o nó principal, ele irá receber as mensagens e redirecioná-las para a aplicação externa (nesse momento também é importante notar o IP do nó principal para que depois sejam enviados os dados para ele:
I (13300) esp_netif_handlers: sta ip: 10.42.0.127, mask: 255.255.255.0, gw: 10.42.0.1
I (13300) mesh_main: <IP_EVENT_STA_GOT_IP>IP:10.42.0.127
I (20360) mesh_main: remetente: MAC_NODE_C, msg: Hello!
I (20800) mesh_main: remetente: MAC_NODE_B, msg: Hello!
I (30670) mesh_main: remetente: MAC_NODE_C, msg: Hello!
I (30950) mesh_main: remetente: MAC_NODE_B, msg: Hello!Na aplicação externa serão recebidas as mesmas mensagens:
$ nc -ukl 10.42.0.1 9999
remetente: MAC_NODE_C, msg: Hello!
remetente: MAC_NODE_B, msg: Hello!
remetente: MAC_NODE_C, msg: Hello!
remetente: MAC_NODE_B, msg: Hello!
Em relação ao recebimento de mensagens externas, pode-se ter algo como segue abaixo:
$ echo "Goodbye!" | nc -u 10.42.0.127 9998Essa mensagem irá chegar primeiramente no nó principal da rede:
I (330200) mesh_main: Goodbye!Que em seguida será redirecionado para os demais nós (B e C, partindo do nó A). Vide abaixo o log dos nós B e C:
I (329710) mesh_main: remetente: MAC_NODE_A, msg: Goodbye!Com isso está completa a comunicação da rede mesh interna com a rede externa. Essa aplicação está exemplificada na Figura 2.
Correção automática de falhas
Devido à dificuldade em rastrear o resultado individual de cada nó da rede, ainda será mantida a topologia inicial de 3 nós para facilitar a análise, porém isso nos restringirá a análise de problemas que estão relacionados com o nó principal. Em uma topologia mais complexa, com vários nós pais intermediários, também existem mecanismos para que seus nós filhos lidem automaticamente com o desaparecimento e degradação do sinal de um nó pai intermediário. Para mais detalhes consulte a documentação oficial da biblioteca.
Desaparecimento do nó principal
Vejamos um exemplo em que o nó principal é desligado e perde conexão total com a rede. Inicialmente o nó A está conectado como nó principal e tem os nós B e C como seus filhos:
I (8847) mesh_main: <MESH_EVENT_PARENT_CONNECTED>layer:0-->1, parent:MAC_ROTEADOR<ROOT>, ID:77:77:77:77:77:77, duty:0
I (8867) mesh_main: <MESH_EVENT_ROOT_ADDRESS>root address:MAC_NODE_A
I (9857) mesh_main: <IP_EVENT_STA_GOT_IP>IP:10.42.0.90
W (22387) mesh_main: <MESH_EVENT_ROUTING_TABLE_ADD>add 1, new:2, layer:1
I (22397) mesh_main: <MESH_EVENT_CHILD_CONNECTED>aid:1, MAC_NODE_B
W (23677) mesh_main: <MESH_EVENT_ROUTING_TABLE_ADD>add 1, new:3, layer:1
I (23687) mesh_main: <MESH_EVENT_CHILD_CONNECTED>aid:2, MAC_NODE_COs nós filhos sinalizam para o nó pai de tempos em tempos. Quando o nó A é desligado, os logs dos nós filhos irão emitir mensagens de desconexão, pois não conseguiram sinalizar pacotes para o nó pai (beacon timeout). Após algum tempo a característica de correção automática de falhas da rede mesh irá disparar ([healing]looking for a new parent) e uma nova eleição acontecerá, pois a rede está sem nó principal (<MESH_EVENT_NETWORK_STATE>is_rootless:1). A eleição vai acontecer normalmente como foi discutido no tutorial Construindo uma rede WiFi Mesh com o ESP32 – Parte 1 e a rede mesh irá continuar seu funcionamento normal. Os logs abaixo dizem respeito ao nó B seguindo os passos descritos anteriormente após a desconexão do nó principal:
I (1900427) mesh_main: <MESH_EVENT_PARENT_DISCONNECTED>reason:200
I (1900427) mesh: [wifi]disconnected reason:200(beacon timeout), continuous:1/max:12, non-root, vote(,stopped)<><>
...
I (1906527) mesh_main: <MESH_EVENT_PARENT_DISCONNECTED>reason:2
I (1906637) mesh: 5201[healing]looking for a new parent, [L:2]try layer:1[revote][scan]
I (1906637) mesh_main: <MESH_EVENT_NETWORK_STATE>is_rootless:1
I (1907247) mesh: 1368, vote myself, router rssi:-43 > voted rc_rssi:-120
I (1907257) mesh: [SCAN:1/10]rc[128][MAC_NODE_B,-42], self[MAC_NODE_B,-43,reason:2,votes:1,idle][mine:1,voter:1(1.00)percent:0.90][128,1,MAC_NODE_B]
...
I (1912837) mesh: [SCAN:10/10]rc[128][MAC_NODE_B,-42], self[MAC_NODE_B,-43,reason:2,votes:2,idle][mine:2,voter:2(1.00)percent:0.90][128,2,MAC_NODE_B]
I (1913227) mesh_main: <MESH_EVENT_PARENT_CONNECTED>layer:2-->1, parent:MAC_ROTEADOR<ROOT>, ID:77:77:77:77:77:77, duty:10
I (1913247) mesh_main: <MESH_EVENT_ROOT_ADDRESS>root address:MAC_NODE_B
W (1914597) mesh_main: <MESH_EVENT_ROUTING_TABLE_ADD>add 1, new:2, layer:1
I (1914607) mesh_main: <MESH_EVENT_CHILD_CONNECTED>aid:1, MAC_NODE_C
I (1914747) esp_netif_handlers: sta ip: 10.42.0.90, mask: 255.255.255.0, gw: 10.42.0.1
I (1914747) mesh_main: <IP_EVENT_STA_GOT_IP>IP:10.42.0.90A Figura 3 ilustra essa situação.
Degradação do sinal do nó principal
Quando um nó pai intermediário possui instabilidade na conexão com seus filhos, a reconexão com outros nós pais é feita automaticamente. Mas no caso da instabilidade do nó principal com o roteador, a eleição para um novo nó principal só vai ser acionada pela chamada da API esp_mesh_waive_root pelo nó principal atual (e somente ele poderá chamar essa nova eleição).
Apesar de após chamada a API esp_mesh_waive_root todo o processo de eleição ser feito automaticamente, ainda há a necessidade de a pessoa desenvolvedora do firmware prever uma situação em que o nó principal deve reconhecer que será necessária uma nova eleição.
Para isso foi criada uma tarefa check_health_task que pode ser disparada na conexão do nó principal:
void mesh_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data)
{
...
case MESH_EVENT_PARENT_CONNECTED: {
...
// esp_mesh_comm_p2p_start();
if(esp_mesh_is_root())
xTaskCreate(check_health_task, "HEALTH_TASK", configMINIMAL_STACK_SIZE+1024, NULL, configMAX_PRIORITIES-3, NULL);
}
break;
...
Essa tarefa será responsável por monitorar a saúde do nó principal e disparar uma nova votação, se necessário. Segue abaixo sua implementação:
void check_health_task(void *param)
{
wifi_ap_record_t ap;
mesh_vote_t vote = {
.percentage = 0.9,
.is_rc_specified = false,
.config = {
.attempts = 15
}
};
while(1)
{
// parar autoconfiguracao da rede antes de chamar API do Wi-Fi
ESP_ERROR_CHECK(esp_mesh_set_self_organized(false, false));
// coletar informacoes do access point (roteador)
esp_wifi_sta_get_ap_info(&ap);
// retornando autoconfiguracao
ESP_ERROR_CHECK(esp_mesh_set_self_organized(true, false));
ESP_LOGI(MESH_TAG, "rssi: %d", ap.rssi);
if (ap.rssi < -90) {
ESP_LOGI(MESH_TAG, "RSSI menor que -90, disparando nova eleicao!");
esp_mesh_waive_root(&vote, MESH_VOTE_REASON_ROOT_INITIATED);
break;
}
vTaskDelay(pdMS_TO_TICKS(10000));
}
vTaskDelete(NULL);
}
Antes de tudo é criada uma variável do tipo mesh_vote_t que consiste em uma estrutura que determina as condições da eleição, como porcentagem de votos e tentativas. Dentro do loop infinito da tarefa são coletadas informações sobre o ponto de acesso Wi-Fi que o nó principal se conectou, ou seja, o roteador. A única informação de interesse, nesse caso, é o RSSI que será utilizado como métrica para decidir se uma nova eleição será disparada (nesse exemplo foi arbitrada uma RSSI de -90 como limiar para disparar a eleição). Veja que antes e depois da chamada da API do Wi-Fi, a autoconfiguração da rede mesh foi desativada e reativada, respectivamente. Isso vai evitar conflitos, pois a própria rede mesh utiliza essa API.
Em relação ao funcionamento, pode-se observar pelos logs abaixo que uma nova eleição foi disparada quando o limiar de RSSI foi atingido:
I (4314) mesh: [IO]disable self-organizing<adaptive>
I (4314) mesh: [IO]enable self-organizing<adaptive>
I (4314) mesh_main: rssi: -77
I (5314) mesh: [IO]disable self-organizing<adaptive>
I (5314) mesh: [IO]enable self-organizing<adaptive>
I (5314) mesh_main: rssi: -70
I (6314) mesh: [IO]disable self-organizing<adaptive>
I (6314) mesh: [IO]enable self-organizing<adaptive>
I (6314) mesh_main: rssi: -91
I (6314) mesh_main: RSSI menor que -90, disparando nova eleicao!
I (6315) mesh: <MESH_NEW_ROOT_VOTE_START>percentage threshold:0.900000, attempts:15, reason:1<by root>[time:61100ms]
I (6316) mesh_main: <MESH_EVENT_VOTE_STARTED>attempts:15, reason:1, rc_addr:00:00:00:00:00:00
I (6637) mesh: [VOTE:1]by root, vote[128][MAC_NODE_A,-53], self[MAC_NODE_A,-53,reason:1,votes:1,root][mine:1,voter:1(1.00)percent:0.90][128,1,MAC_NODE_A]
...
I (15307) mesh: [VOTE:15]by root, vote[127][MAC_NODE_C,-34], self[MAC_NODE_A,-35,reason:1,votes:0,root][mine:0,voter:3(0.00)percent:0.90][127,3,MAC_NODE_C]
I (15327) mesh: <MESH_NEW_ROOT_VOTE_DONE>reason:reach max times, s_vote_scan_times:15
I (15327) mesh_main: <MESH_EVENT_VOTE_STOPPED>
O processo de eleição é igual ao que foi trabalho anteriormente. A eleição findou com a escolha do nó C para nó principal. A Figura 4 ilustra o processo por trás desses logs.
Apesar de apenas o nó principal ser capaz de disparar a nova eleição, nada impede que os nós filhos comuniquem ao nó principal a necessidade de uma nova eleição, seja por perda de pacotes, atraso na comunicação, etc. Fica a critério da aplicação.
Conclusão
Uma vez mais foram apresentadas características do protocolo ESP-WIFI-MESH que mostram a sua facilidade de uso, tanto no quesito comunicação, quanto no quesito de correção de falhas. Apesar da comunicação externa ser dificultada pela falta de APIs amplamente funcionais, a comunicação interna é facilitada pelo uso de apenas duas APIs para envio e recebimento de mensagem por toda a rede sem preocupação de como o pacote será roteado. Em relação à correção de falhas, a rede possui mecanismos para quando o nó principal desaparecer ou degradar a qualidade da conexão em relação ao roteador, permitindo manter a integridade da rede em momentos críticos.
Assim termina-se essa série de tutoriais em duas parte sobre uma básica introdução ao uso da biblioteca que implementa o protocolo ESP-WIFI-MESH. Para a consulta de tópicos mais complexos, recomenda-se consultar a documentação listada nas referências.
Código completo em: https://github.com/Lwao/esp-wifi-mesh-demo
Referências
Para maiores informações, visite os links (a documentação da Espressif sobre o ESP-WIFI-MESH é rica em detalhes e mais que suficiente 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






Gostei muito do post, parabéns! Só fiquei com uma dúvida, a parte que tem o mac_adress_a … tenho que definir o mac no código de cada nó/esp? Seria legal se colocasse uma descrição melhor de como fez pra conectar com o socket pelo Netcat, tentei replicar e não consegui :/
Fico feliz que tenha gostado do post. Obrigado! Para facilitar, eu assumi que todos os nós se conheciam. Mas em uma situação mais próxima da realidade, em que nós podem aparecer e desaparecer aleatoriamente, seria bom implementar um mecanismo para comunicar que novos nós entraram na rede e quando saíram. Por exemplo, você pode fazer com que, sempre que um nó se conecta à rede mesh, ele vai fazer um broadcast para “se apresentar” para todos os outros nós. Cada nó que escutou esse broadcast pode anotar internamente o endereço MAC desse novo nó. Para desconexão, você pode usar os… Leia mais »