Manual

do

Maker

.

com

Como usar socket com ESP32

Como usar socket com ESP32

Seja o protocolo que for, por mais simples que seja, nada vai ser tão leve quanto abrir uma comunicação via socket. Se for UDP, mais leve ainda, pois o UDP não tem compromisso com a entrega dos pacotes, não verifica se foi entregue e também não entrega ordenadamente. O TCP inclui algumas entradas no header e tem todos os ítens supracitados. Mas ainda assim, nada é mais leve. Já o controle fica por conta do programador, mas ainda assim é uma boa experiência. Vamos ver como utilizar socket com ESP32?

Artigos relacionados

Já escrevi alguns artigos de comunicação por socket. O legal de utilizar socket é que fica fácil testar com qualquer coisa. Um simples comando telnet já pode acionar um relé, por exemplo.

Se desejar ver uma comunicação socket com Qt (que é um framework de C++, e minha paixão), tem esse artigo. Socket era bastante utilizado para comunicação local, mas com a chegada dos IPCs, os recursos primários do UNIX foram ficando para trás - e não estou dizendo que outras plataformas não tenham sockets. É que o UNIX tem um outro tipo de socket, justamente para fazer IPC; o UNIX sockets. Ele não utiliza a camada de rede, é bem interessante. Se quiser ver a respeito, escrevi esse artigo.

Socket com ESP32

Eu não encontrei na documentação oficial nenhuma referência à comunicação via socket. Passei 2 dias fazendo testes, até encontrar um exemplo de socket no ESP32 em um repositório no github. Eu já havia tentado de todas as formas imagináveis, iniciei o projeto no CodeBlocks várias e várias vezes, escrevendo linha por linha. No final, o exemplo era para utilizar diretamente no ESP-IDF, todo escrito em C. Implementei devagar até conseguir fazer funcionar, portanto, sugiro que guarde com carinho essa dica porque não foi fácil nem é simples.

Algumas funções eu já tinha implementado; a conexão WiFi também fiz de um jeito diferente, invés de usar a biblioteca WiFi do Arduino. A task que controla o socket eu coloquei no núcleo 0 para evitar qualquer problema com o loop no núcleo 1 (porque se o socket ficar preso tempo demais, pode interferir inclusive na própria conexão WiFi e daí começa o espetáculo de "insetos").

Socket server em Python

No Linux eu fiz um socket server em Python. Porque? Bem, todos os testes iniciais foram focados em criar um socket server, mas nada funcionou. Tive erros relacionados à compilação da API do Arduino para o ESP, pra ter uma ideia. Daí achei melhor (mais fácil) implementar um client, já que o server pode responder à requisição. Então o client manda uma mensagem qualquer (coloquei sendMeCommand pra ficar bonitinho) e o server responde com um comando, que pode ser passado pelo usuário de diversas maneiras. Vou fazer de um jeito pouco usual porque estou extremamente desgastado e nem publiquei outro artigo porque dediquei todo meu tempo livre a resolver essa problemática; ficaria muito frustrado se não conseguisse resolver essa questão, afinal, a coisa mais básica em qualquer linguagem ou sistema é abrir um socket.

Placa de desenvolvimento para ESP32

Para escrever esse artigo, utilizei a placa de desenvolvimento da imagem de destaque (e também do video), vendida na CurtoCircuito (clique para vê-la no site). Vou dar minha opinião sobre ela.

Para profissionais

Bem, se você tiver um produto com ESP32 e a gravação for por FTDI, é a pior das situações; desconectar e conectar DTR, TX, RX, VCC e GND não é lá uma tarefa muito divertida. Agora, fazer isso para, digamos, 50 placas, tem que ter muita paciência. Com uma placa de desenvolvimento você poderá apenas ir trocando os processadores e gravando. Além do mais, ele tem um botão de liga/desliga que dispensa a necessidade de desconectar o cabo toda a vez que for trocar de processador.

Para hobistas

Ótimo para colecionar ESP32. Eu quis muito essa board porque assim posso adquirir diversos módulos e gravar programas específicos em cada um deles. Depois é só colocar de novo na board e brincar com o respectivo programa, bastando rotular o processador para saber sua função. Além do mais, fica fácil de armazenar um monte de ESP32, porque ele é pequeno fora de um circuito. E não tem que não goste dessa placa, causa uma ótima impressão; com alguns módulos o investimento já retornou.

Módulo relé

Para fazer essa comunicação e testá-la, a primeira coisa que pensei foi em bater um relé. Convenhamos, é o mesmo processo de acender um LED, mas é mais legal ouvir os "plecs" do que acender um LED.

Conectei o módulo em 3V3 porque os pinos do ESP não são tolerantes a 5V. Consegui drivar sem problemas. Também, o módulo tem lógica invertida, que acende quando aterrado no pino do processador, e é bem melhor assim no meu caso porque o módulo não é opto-acoplado e ao contrário poderia consumir corrente demais do pino de GPIO.

Esse módulo relé é comum, nada de especial, exceto pelo fato de que é vendido na AutocoreRobotica. Agora tem diversos modelos, e o desse link é opto-acoplado, que garante um isolamento do circuto e é acionado com baixíssima corrente, por isso recomendo.

Pinout do ESP32

Como estamos tratando diretamente com o módulo castellated (esse é o nome do acabamento na borda do módulo com o processador; presume-se o porquê), precisamos ter uma referência sobre os recursos dos GPIO. Eis:

ESP32-pinout-without-board.webp

Na placa de desenvolvimento estão dispostos os pinos de GPIO. Eu optei por usar o 26 e o 27 para os acionadores dos relés.

Criação do projeto

Para criar esse projeto, utilizei o CodeBlocks com PlatformIO. Já mostrei nesse artigo como utilizar o CodeBlocks  para programar Arduino, mas agora vou explicar um pouco mais sobre os comandos para O PlatformIO.

Instalação da board

Quando criamos um projeto com uma board que a configuração de hardware e toolchain não estão disponíveis, o download é feito automaticamente. Isso é ótimo, pois não precisamos nos preocupar com o risco de esquecer alguma coisa e o projeto não funcionar.

Para criar esse projeto, o procedimento é o seguinte:

mkdir mySockComm
cd mySockComm
platformio init --ide codeblocks --board nodemcu-32s

E aí é só contemplar o processo. Para ver as opções utilizando ESP32, fiz uma pesquisa na base de placas do PlatformIO, dessa maneira:

platformio boards|egrep -i esp32

Vou dar uma sugestão que pode parecer meio boba, mas pode evitar problemas. Sempre que for utilizar o programa platformio, esteja primeiramente dentro do diretório do respectivo projeto.

Instale o VS Code e no menu de plugins, instale o PlatformIO. É a melhor opção para desenvolvimento, com auto-completion e outras referências.

Instalação de bibliotecas

Usando PlatformIO no VS Code, esqueça os comandos dispostos aqui.

É fácil instalar uma biblioteca por linha de comando. Podemos pesquisar previamente com o comando:

platformio lib search biblioteca_de_exemplo

Existem diversos comandos de manipulação de biblioteca, mas não se preocupe em decorar nada, simplesmente digite platformio lib para ver as opções relacionadas. Se esquecer os parâmetros que procedem ao platformio, digite apenas platformio para ver as opções. Não tem segredo.

Só para deixar claro, o comando platformio lib search procura no repositório oficial do Arduino, não é local.

Código

Como citei mais acima, procurei o caminho mais fácil. Coloquei dentro do socket server uma chamada; cada vez que ele recebe uma mensagem, verifica em /dev/shm (que é um sistema de arquivos na memória, utilizando o ramfs) se tem um arquivo chamado command. Se tiver, ele abre o arquivo, lê o comando, fecha e remove o arquivo. Se não, envia  simplesmente "-".

Os comandos implementados foram:

  • 0 - para desligar ambos os relés
  • 1 -  para acionar ambos os relés
  • 2 - para acionar o relé 1
  • 3 - para acionar o relé 2

Isso já é o suficiente para a prova de conceito.

Quando o server está fora, não há danos ao socket client, ele simplesmente imprime uma mensagem relacionada à indisponibilidade da conexão, mas assim que o server estiver funcionando, a comunicação inicia (ou reinicia). Já do lado server, fica sempre escutando, mas se interrompido, o socket permanece um tempo indisponível até que o sistema nativo remova ele pelo timeout do socket, conforme os padrões configurados nativamente no sistema. Dá pra reduzir o timeout do socket pelo sysctl, mas o foco desse artigo não é Linux, por isso vou deixar para outra ocasião.

Socket server

Peguei um código prontinho de exemplo na própria documentação do Python, assim economizei tempo escrevendo esse código. Só modifiquei alguns parâmetros e adicionei as funcionalidades relacionadas à interação com com o sistema de arquivos. O código ficou assim:

#!/usr/bin/env python
import SocketServer
import sys
import os

COMMAND = "/dev/shm/command"

class MyTCPHandler(SocketServer.BaseRequestHandler):
    """
    The request handler class for our server.

    It is instantiated once per connection to the server, and must
    override the handle() method to implement communication to the
    client.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        self.data = self.request.recv(1024).strip()
        print "{} wrote:".format(self.client_address[0])
        print self.data
        # just send back the same data, but upper-cased
        #self.request.sendall(self.data.upper())
        content = "-\n"
        if os.path.isfile(COMMAND):
            f = open(COMMAND,"r")
            content = f.readline()
            f.close()
            os.remove(COMMAND)
            #content = content + "\n"
        self.request.sendall(content)


if __name__ == "__main__":
    #troque IP e porta do server, como desejado
    HOST, PORT = "192.168.1.7", 8888

    # Create the server, binding to localhost on port 9999
    server = SocketServer.TCPServer((HOST, PORT), MyTCPHandler)

    # Activate the server; this will keep running until you
    # interrupt the program with Ctrl-C
    server.serve_forever()

Para executar, basta chamar na linha de comando:

python boss.py

Chamei o script Python de boss.py, já que é ele quem manda.

Socket client

Esse, rodando no ESP32, deu um trabalho monstro, porque apesar de (após 2 dias) ter encontrado o código que servia, ainda tive que fazer alguns ajustes. Criei a função de controle dos relés fora da task, depois chamei a função na sequência da leitura de resposta.

Eu sei que o código vai assustar um pouco quem está acostumado com Arduino, mas antes de por o código completo, vou dissecar as funções para deixar claro o que fiz. Só os includes que são assustadores mesmo, mas se esquecer de colocar um que dependa do outro, é problema na certa.

Cabeçalhos

Os cabeçalhos são as bibliotecas que contém suas respectivas funcionalidades. Elas estão em lugares diferentes, conforme sua categora. Isso deu um bocado de trabalho também, porque além de não saber quais eram as dependências fazendo às escuras, algumas bibliotecas eu não tinha. E o CodeBlocks eu acho que poderia se chamar "CodeBugs", pois travou diversas vezes durante esses 2 dias de pesquisas e testes. Vou escrever um artigo com uma IDE muito melhor logo mais.

#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"
#include "lwip/dns.h"

#include "esp_wifi.h"
#include "esp_wpa2.h"
#include "esp_event_loop.h"

#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"

Defines

Logo após os includes tem alguns defines para constantes utilizadas no código:

#define PORT_NUMBER 8888
#define BUF_SIZE (1024)

#define DEFAULT_SSID "SeuSSID"
#define DEFAULT_PWD "SuaSenha"

#define TCPSERVER "IP.OU.URL.DO.TCPSERVER"

#define MESSAGE "sendMeCommand"

//definicao dos gpio do relay - 26 e 27
#define RELAY_1              GPIO_NUM_26
#define RELAY_2              GPIO_NUM_27

#define ON  (uint32_t) LOW
#define OFF (uint32_t) HIGH

Qualquer porta acima de 1024 é elegível, eu preferi utilizar 8888.

Defini uma constante de um buffer, mas pelo que estou vendo nem utilizei na última implementação.

DEFAULT_SSID e DEFAULT_PWD são redefinições, coloque suas credenciais para conectar o ESP32 à sua rede WiFi.

TCPSERVER é o endereço IP ou URL onde executará o script Python socket server.

MESSAGE é um mero gatilho pra buscar status no tcp server.

Manipulador de eventos

Uma função foi definida para tratar eventos do sistema. Ela retorna uma definição de tipo uint32_t, mas eu sequer me preocupei em guardar o retorno.

static esp_err_t event_handler(void *ctx, system_event_t *event){
    switch(event->event_id) {
    case SYSTEM_EVENT_STA_START:
      uart_write_bytes(UART_NUM_0, (const char *) "SYSTEM_EVENT_STA_START\n", 29);
      ESP_ERROR_CHECK(esp_wifi_connect());
      break;
    case SYSTEM_EVENT_STA_GOT_IP:
      uart_write_bytes(UART_NUM_0, (const char *) "SYSTEM_EVENT_STA_GOT_IP\n", 29);
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      uart_write_bytes(UART_NUM_0, (const char *) "SYSTEM_EVENT_STA_DISCONNECTED: ", 40);
      ESP_ERROR_CHECK(esp_wifi_connect());
      break;
    default:
        break;
    }
    return ESP_OK;
}

Conexão WiFi

Essa eu fiquei bastante feliz em fazer sem usar o "modo Arduino". Não se preocupe com a estrutura. Se quiser utilizar os recursos como estou fazendo, sugiro que guarde templates separados com cada funcionalidade para consulta posterior ou faça um bookmark do artigo.

static void initialise_wifi(void){
    tcpip_adapter_init();
    ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL));
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
    ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA) );
    ESP_ERROR_CHECK( esp_wifi_start() );
    uart_write_bytes(UART_NUM_0, (const char *) "WiFi configurado\n", 20);
}

UART (Serial)

Nesse outro artigo mostrei detalhes de como utilizar a UART do ESP32 sem precisar do recurso Serial do Arduino. Aqui só estou reimplementando:

uart_config_t uart_config = {
  .baud_rate = 115200,
  .data_bits = UART_DATA_8_BITS,
  .parity    = UART_PARITY_DISABLE,
  .stop_bits = UART_STOP_BITS_1,
  .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};

Depois tem mais 3 linhas na função setup(), como pode ser visto mais abaixo.

Task da comunicação TCP

Em uma das primeiras implementações de socket que tentei fazer no ESP32 não utilizei task, mas foi claramente um erro. Para ter uma ideia da gravidade, executei todo o código no núcleo 1, que é justamente onde roda a função loop da API do Arduino. Quando o socket entrava, causava um reset por WDT. Espero que esteja se acostumando a criar threads em seus programas com ESP32.

void tcp_client(void *pvParam){
    ESP_LOGI(TAG,"tcp_client task started \n");
    struct sockaddr_in tcpServerAddr;
    tcpServerAddr.sin_addr.s_addr = inet_addr(TCPSERVER);
    tcpServerAddr.sin_family = AF_INET;
    tcpServerAddr.sin_port = htons(8888);
    int s, r;
    char recv_buf[64];
    while(1){
        String command = "";
        //xEventGroupWaitBits(wifi_event_group,CONNECTED_BIT,false,true,portMAX_DELAY);
        s = socket(AF_INET, SOCK_STREAM, 0);
        if(s < 0) {
            ESP_LOGE(TAG, "... Failed to allocate socket.\n");
            vTaskDelay(1000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... allocated socket\n");
         if(connect(s, (struct sockaddr *)&tcpServerAddr, sizeof(tcpServerAddr)) != 0) {
            ESP_LOGE(TAG, "... socket connect failed errno=%d \n", errno);
            close(s);
            vTaskDelay(4000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... connected \n");
        if( write(s , MESSAGE , strlen(MESSAGE)) < 0)
        {
            ESP_LOGE(TAG, "... Send failed \n");
            close(s);
            vTaskDelay(4000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... socket send success");
        do {
            bzero(recv_buf, sizeof(recv_buf));
            r = read(s, recv_buf, sizeof(recv_buf)-1);
            for(int i = 0; i < r; i++) {
                 //putchar(recv_buf[i]);
                 if (recv_buf[i] == '0'){
                     uart_write_bytes(UART_NUM_0, (const char *) "Desligando ambos os reles\n", 27);
                     gpio_set_level(RELAY_1, (uint32_t) OFF);
                     gpio_set_level(RELAY_2, (uint32_t) OFF);
                 }
                 else if (recv_buf[i] == '1'){
                     uart_write_bytes(UART_NUM_0, (const char *) "Ligando relay 1\n", 16);
                     gpio_set_level(RELAY_1, (uint32_t) ON);
                 }
                 else if (recv_buf[i] == '2'){
                     uart_write_bytes(UART_NUM_0, (const char *) "Ligando relay 2\n", 16);
                     gpio_set_level(RELAY_2, (uint32_t) ON);
                 }
                 else if (recv_buf[i] == '3'){
                   uart_write_bytes(UART_NUM_0, (const char *) "Ligando ambos os reles\n", 28);
                   gpio_set_level(RELAY_1, (uint32_t) ON);
                   gpio_set_level(RELAY_2, (uint32_t) ON);
                 }
            }
        } while(r > 0);

        ESP_LOGI(TAG, "... done reading from socket. Last read return=%d errno=%d\r\n", r, errno);
        close(s);
        ESP_LOGI(TAG, "... new request in 5 seconds");
        vTaskDelay(5000 / portTICK_PERIOD_MS);
    }
    ESP_LOGI(TAG, "...tcp_client task closed\n");
}

setup()

A função setup() fica linda, não tem quase nada e é feita só por chamadas. As primeiras 4 linhas são relacionadas à configuração dos pinos de GPIO, seguido por 3 linhas de configuração da UART. Depois inicializamos a conexão com nossa rede WiFi e por fim, criamos a task.

void setup(){
    gpio_set_direction(RELAY_1, GPIO_MODE_OUTPUT);
    gpio_set_direction(RELAY_2, GPIO_MODE_OUTPUT);
    gpio_set_level(RELAY_1, (uint32_t) OFF);
    gpio_set_level(RELAY_2, (uint32_t) OFF);

    uart_param_config(UART_NUM_0, &uart_config);
    uart_set_pin(UART_NUM_0, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(UART_NUM_0, BUF_SIZE * 2, 0, 0, NULL, 0);

    initialise_wifi();
    //tcp_client
    xTaskCreatePinnedToCore(tcp_client,"tcp_client",8192,NULL,10,NULL,0);
}

loop()

Nadinha. Nadinha de nada; necas de pitibiriba; coisa nenhuma. Todos os serviços relacionados estão sendo executados em threads. A comunicação WiFi e UART estão rodando no núcleo 1. Isso porque setup()loop() rodam no núcleo 1 e tudo o que for chamado dentro deles, roda no mesmo núcleo. A excessão é a criação de tasks com especificação do núcleo a rodar.

Código completo do socket client

O código completo ficou assim:

#include <SPI.h>
#include <Arduino.h>
#include <lwip/sockets.h>
#include <errno.h>
#include "sdkconfig.h"
#include "driver/uart.h"

#include <stdio.h>
#include <string.h>
#include "freertos/event_groups.h"
#include "esp_log.h"
#include "lwip/err.h"
#include "lwip/sockets.h"
#include "lwip/sys.h"
#include "lwip/netdb.h"
#include "lwip/dns.h"

#include "esp_wifi.h"
#include "esp_wpa2.h"
#include "esp_event_loop.h"

#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"

#define PORT_NUMBER 8888
#define BUF_SIZE (1024)

#define DEFAULT_SSID "SuhankoFamily"
#define DEFAULT_PWD "fsjmr112"

#define TCPSERVER "192.168.1.7"

#define MESSAGE "sendMeCommand"

//definicao dos gpio do relay - 26 e 27
#define RELAY_1              GPIO_NUM_26
#define RELAY_2              GPIO_NUM_27

#define ON  (uint32_t) LOW
#define OFF (uint32_t) HIGH

void tcp_client(void *pvParam){
    ESP_LOGI(TAG,"tcp_client task started \n");
    struct sockaddr_in tcpServerAddr;
    tcpServerAddr.sin_addr.s_addr = inet_addr(TCPSERVER);
    tcpServerAddr.sin_family = AF_INET;
    tcpServerAddr.sin_port = htons(8888);
    int s, r;
    char recv_buf[64];
    while(1){
        String command = "";
        //xEventGroupWaitBits(wifi_event_group,CONNECTED_BIT,false,true,portMAX_DELAY);
        s = socket(AF_INET, SOCK_STREAM, 0);
        if(s < 0) {
            ESP_LOGE(TAG, "... Failed to allocate socket.\n");
            vTaskDelay(1000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... allocated socket\n");
         if(connect(s, (struct sockaddr *)&tcpServerAddr, sizeof(tcpServerAddr)) != 0) {
            ESP_LOGE(TAG, "... socket connect failed errno=%d \n", errno);
            close(s);
            vTaskDelay(4000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... connected \n");
        if( write(s , MESSAGE , strlen(MESSAGE)) < 0)
        {
            ESP_LOGE(TAG, "... Send failed \n");
            close(s);
            vTaskDelay(4000 / portTICK_PERIOD_MS);
            continue;
        }
        ESP_LOGI(TAG, "... socket send success");
        do {
            bzero(recv_buf, sizeof(recv_buf));
            r = read(s, recv_buf, sizeof(recv_buf)-1);
            for(int i = 0; i < r; i++) {
                 //putchar(recv_buf[i]);
                 if (recv_buf[i] == '0'){
                     uart_write_bytes(UART_NUM_0, (const char *) "Desligando ambos os reles\n", 27);
                     gpio_set_level(RELAY_1, (uint32_t) OFF);
                     gpio_set_level(RELAY_2, (uint32_t) OFF);
                 }
                 else if (recv_buf[i] == '1'){
                     uart_write_bytes(UART_NUM_0, (const char *) "Ligando relay 1\n", 16);
                     gpio_set_level(RELAY_1, (uint32_t) ON);
                 }
                 else if (recv_buf[i] == '2'){
                     uart_write_bytes(UART_NUM_0, (const char *) "Ligando relay 2\n", 16);
                     gpio_set_level(RELAY_2, (uint32_t) ON);
                 }
                 else if (recv_buf[i] == '3'){
                   uart_write_bytes(UART_NUM_0, (const char *) "Ligando ambos os reles\n", 28);
                   gpio_set_level(RELAY_1, (uint32_t) ON);
                   gpio_set_level(RELAY_2, (uint32_t) ON);
                 }
            }
        } while(r > 0);

        ESP_LOGI(TAG, "... done reading from socket. Last read return=%d errno=%d\r\n", r, errno);
        close(s);
        ESP_LOGI(TAG, "... new request in 5 seconds");
        vTaskDelay(5000 / portTICK_PERIOD_MS);
    }
    ESP_LOGI(TAG, "...tcp_client task closed\n");
}

//MANIPULADOR DE EVENTOS
static esp_err_t event_handler(void *ctx, system_event_t *event){
    switch(event->event_id) {
    case SYSTEM_EVENT_STA_START:
      uart_write_bytes(UART_NUM_0, (const char *) "SYSTEM_EVENT_STA_START\n", 29);
      ESP_ERROR_CHECK(esp_wifi_connect());
      break;
    case SYSTEM_EVENT_STA_GOT_IP:
      uart_write_bytes(UART_NUM_0, (const char *) "SYSTEM_EVENT_STA_GOT_IP\n", 29);
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      uart_write_bytes(UART_NUM_0, (const char *) "SYSTEM_EVENT_STA_DISCONNECTED: ", 40);
      ESP_ERROR_CHECK(esp_wifi_connect());
      break;
    default:
        break;
    }
    return ESP_OK;
}

//INICIALIZAÇÃO WIFI
static void initialise_wifi(void){
    tcpip_adapter_init();
    ESP_ERROR_CHECK(esp_event_loop_init(event_handler, NULL));
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK( esp_wifi_init(&cfg) );
    ESP_ERROR_CHECK( esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK( esp_wifi_start() );
    uart_write_bytes(UART_NUM_0, (const char *) "WiFi configurado\n", 20);
}

//CONFIGURANDO UART
uart_config_t uart_config = {
  .baud_rate = 115200,
  .data_bits = UART_DATA_8_BITS,
  .parity    = UART_PARITY_DISABLE,
  .stop_bits = UART_STOP_BITS_1,
  .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};

void setup(){
    gpio_set_direction(RELAY_1, GPIO_MODE_OUTPUT);
    gpio_set_direction(RELAY_2, GPIO_MODE_OUTPUT);
    gpio_set_level(RELAY_1, (uint32_t) OFF);
    gpio_set_level(RELAY_2, (uint32_t) OFF);

    uart_param_config(UART_NUM_0, &uart_config);
    uart_set_pin(UART_NUM_0, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(UART_NUM_0, BUF_SIZE * 2, 0, 0, NULL, 0);

    initialise_wifi();
    //tcp_client
    xTaskCreatePinnedToCore(tcp_client,"tcp_client",8192,NULL,10,NULL,0);
}

void loop(){

}

Agora, apesar de ser um monte de linhas, as funções são simples, certo?

Script shell para interagir com os GPIO

O socket server é um serviço. Ele não precisa (e certamente não deveria) fazer outra coisa que não o gerenciamento da conexão. Para acionar os relés, escrevi um programa à parte, bem simples, utilizando um menu em ncurses, De modo que ninguém precisa saber o comando que será enviado, fica uma interface de, digamos "alto nível" com o usuário.

Uma dependência é o programa dialog. Para instalar tudo adequadamente, leia esse artigo.

#!/bin/sh

while true; do
  PINS="0 'Desliga_todos_os_reles' 1 'Liga_o_rele_1' 2 'Liga_o_rele_2' 3 'Liga_todos_os_reles'"
  PIN=`dialog --title "Manual do Maker - Relays" --menu "Selecione um item" 15 55 5 $PINS  --stdout`

  [ $? -eq 1 ] && clear && return

  echo $PIN >/dev/shm/command
  VAL=5
  for i in `seq 1 5`; do
    VAL=`echo 5-$i|bc`
    dialog --infobox "aguarde $VAL segundos..." 3 55 --stdout
    sleep 1
  done
done

Colocado em loop infinito até que seja interrompido pelo botão Cancel, o script fará a interação necessária para o acionamento dos relés.

Quando selecionada uma opção, existe um delay na comunicação, então para não ficar parado em uma tela estática, coloquei um timer de 5 segundos decrementando o tempo para ter um informativo.

Programa gráfico para interagir com o GPIO

Fiz o programa gráfico em meu framework preferido de C++, o Qt. O programa é ridículamente simples.

O código desse programa está disponível no meu repositório do github. A janela é bem simples:

qt-esp32.webp

Fiz tudo na QMainWindow, não tem nada de especial.

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    this->status = "0";

    this->toLabel.insert("0","Status: Desligados");
    this->toLabel.insert("1","Status: Relay 1 ligado");
    this->toLabel.insert("2","Status: Relay 2 ligado");
    this->toLabel.insert("3","Status: Ligados");

    this->Server = new QTcpServer(this);
    this->socket = new QTcpSocket(this);

    this->reconnectTo();

    connect(Server, SIGNAL(newConnection()), this, SLOT(ReceiveData()));
    connect(ui->pbR1,SIGNAL(clicked(bool)),this,SLOT(r1()));
    connect(ui->pbR2,SIGNAL(clicked(bool)),this,SLOT(r2()));
    connect(ui->pbR1R2,SIGNAL(clicked(bool)),this,SLOT(on()));
    connect(ui->pbOff,SIGNAL(clicked(bool)),this,SLOT(off()));
}

void MainWindow::r1()
{
    this->status = "1";
    ui->label->setText(this->toLabel[this->status]);
    //this->getNext = true;
}

void MainWindow::r2()
{
    this->status = "2";
    ui->label->setText(this->toLabel[this->status]);
    //this->getNext = true;
}

void MainWindow::off()
{
    this->status = "0";
    ui->label->setText(this->toLabel[this->status]);
    this->getNext = true;
}

void MainWindow::on()
{
    this->status = "3";
    ui->label->setText(this->toLabel[this->status]);
    this->getNext = true;
}

void MainWindow::tellMeMore()
{
    QByteArray data = socket->readAll();
    if (data.contains("sendMeCommand")){
        socket->write(this->status);
        this->socket->close();
        this->Server->close();
    }
}
void MainWindow::ReceiveData()
{
    if (this->getNext){
        this->socket = Server->nextPendingConnection();
        this->getNext = false;
        connect(socket,SIGNAL(readyRead()),this,SLOT(tellMeMore()));
        connect(socket,SIGNAL(disconnected()),this,SLOT(reconnectTo()));
    }
    if (!socket){
        qDebug() << "saindo em ReceiveData";
        return;
    }
}
void MainWindow::reconnectTo()
{
    this->socket->close();
    this->Server->close();
    this->getNext = true;
    QHostAddress add;
    add.setAddress("192.168.1.7");
    if(!Server->listen(add, 8888)) {
        return;
    }
}
MainWindow::~MainWindow()
{
    delete ui;
}

E o header, dessa classe, o mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QTcpServer>
#include <QTcpSocket>
#include <QMap>

namespace Ui {
class MainWindow;
}
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();
private slots:
    void ReceiveData();
    void tellMeMore();
    void reconnectTo();

    void r1();
    void r2();
    void off();
    void on();
private:
    Ui::MainWindow *ui;
    QTcpServer *Server;
    QTcpSocket *socket;
    bool getNext=true;
    QByteArray status;
    QMap <QString,QString> toLabel;
};
#endif // MAINWINDOW_H

O desenho da janela foi feito no mainwindow.ui, mas não faz sentido dispor o código aqui.

No arquivo .pro do projeto, adicione o suporte a rede. A respectiva linha deve ficar assim:

QT       += core gui network

Baixando o projeto do github, bastará abrir e compilar.

IDEs utilizadas para programar

Para programar em Python, utilizei o PyCharm. Ele é gratuito e vem com as funcionalidades pagas durante 30 dias, se não me engano. Mas provavelmente você não sentirá falta dos recursos da versão paga.

Para programar o ESP32, utilizei o CodeBlocks. Para instalar ele e suas dependências, sugiro a leitura desse artigo.

Para programar o shell script eu utilizei o vim, mas se não tiver habilidades com o vim, você sequer conseguirá sair do modo de edição do arquivo, então recomendo que utilize o Atom.

Para programar em Qt, utilizei o QtCreator. Você pode instalá-lo pelo gerenciador de pacotes do Linux ou baixar o instalador para outras plataformas no site do Qt.

Video

O video é simples, mas mostro um pouco de cada programa, explico o payload da comunicação e você pode ver tudo isso rodando.

Inscreva-se no nosso canal Manual do Maker no YouTube.

Também estamos no Instagram.

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.