IoT

Salvando Preferências no ESP32

Eletrogate 1 de dezembro de 2021

Introdução

Neste post iremos aprender à salvar dados na memória do microcontrolador ESP32  e obtermos os dados salvos mesmo após uma queda de energia.

Como demonstração, iremos desenvolver um projeto de Web Server  para visualização de dados de temperatura do sensor DHT11, podendo as medidas serem modificadas pelo usuário entre a escala preferencial escolhida (Celsius ou Fahrenheit).

Ainda neste projeto de demonstração,  poderá ser visualizado e manipulado os horários aos quais o usuário cadastrou, que poderá ser utilizado para projetos futuros, sendo o horário atual fornecido por um serviço da web chamado Public NTP, ao qual, nestes horários, é acionado um evento. Nesta demonstração, é acionado o evento de acender o LED onboard do ESP32, mas poderia ser qualquer outro evento, como o envio de um e-mail ao usuário.


Materiais Necessários para o Projeto Salvando Preferências no ESP32

Para montar o projeto serão necessários os seguintes materiais:

Para a conexão com o serviço de horário NTP é necessário se ter uma conexão WiFi com Internet.


Salvando Dados no ESP32

O salvamento de dados que resistam à quedas de energia é feito através da biblioteca Preferences.h, que gerencia a NVS (Non-Volatile Storage). A memória NVS, atualmente, usa uma parte da memória flash principal por meio de uma API interna. Veja mais sobre a memória NVS acessando este link.

Esta biblioteca salva dados em um invólucro em torno do armazenamento não volátil no processador ESP32. O armazenamento não volátil consiste no tipo de armazenamento em que se pode persistir dados, isto é, uma vez  que sejam gravados, os dados irão ser conservados mesmo se houver perda da fonte de energia.

Salvar dados resistentes à queda de energia é útil para lembrar do último estado de uma variável, para salvar configurações ou para qualquer outro tipo de dado que necessite salvar permanentemente.

Os dados salvos utilizando esta biblioteca são estruturados dentro de um namespace. Dentro deste namespace há chaves (key) que propriamente irão estar armazenando os dados (value). Veja abaixo a estrutura:

Estrutura namespace

As chaves (key) podem ter um comprimento máximo de 15 caracteres.

O valor (value) podem ter um dos seguintes tipos:

  • tipos inteiros:
    • Unsigned Short(uint16_t),
    • Short (int16_t),
    • Unsigned Int(uint32_t),
    • int (int32_t),
    • Unsigned Long(uint32_t),
    • Long(int32_t),
    • Unsigned Long64 (uint64_t),
    • Long64 (int64_t);
  • tipos decimais:
    • float,
    • Double;
  • tipos booleanos:
    • bool;
  • tipos textos:
    • Unsigned Char (uint8_t),
    • Char (int8_t),
    • String;
  • outros tipos:
    • Bytes.

Dentro de um namespace pode haver tantas chaves quanto necessário. O nome de namespace deve ser limitado a 15 caracteres, além de, obrigatoriamente, possuir um nome único. Ou seja, dos namespaces devem possui nomes distintos. Veja o exemplo abaixo em que são armazenados os dados de Rede à qual o ESP32 irá se conectar.

Namespace Exemplo

Também há a possibilidade de haver múltiplos namespaces com chaves de mesmos nomes, sem ocorrer nenhuma invalidação:

Namespace com múltiplas chaves com mesmo nome

Abrindo um namespace

Para abrir um namespace utilize a função begin: preferences.begin("nome_namespace", false);

Deve-se passar como parâmetros:

  1. Nome: o nome do namespace;
  2. Somente leitura: true indica abrir ou criar o namespace no modo somente leitura e false indica abrir ou criar o namespace no modo de leitura/gravação.

Limpar todas chaves de um namespace

Para limpar todas chaves de um namespace utilize a função clear: preferences.clear();

Esta função remove somente as chaves do namespace aberto, e não apaga o namespace.

Obter o valor de uma determinada chave

Para obter o valor de uma chave, existe funções de acordo com o tipo da variável requerida. O primeiro parâmetro destas funções é onde deve ser informado o nome da chave. O segundo parâmetro, que é opcional, é onde é informado o valor padrão para retorno caso a chave não exista.

unsigned short(uint16_t):preferences.getUShort("nome_chave", 0);
short (int16_t):preferences.getShort("nome_chave", 0);
unsigned int(uint32_t):preferences.getUInt("nome_chave", 0);
int (int32_t):preferences.getInt("nome_chave", 0);
unsigned long(uint32_t):preferences.getULong("nome_chave", 0);
long(int32_t):preferences.getLong("nome_chave", 0);
unsigned long64 (uint64_t):preferences.getULong64("nome_chave", 0);
long64 (int64_t):preferences.getLong64("nome_chave", 0);
float:preferences.getFloat("nome_chave", NAN);
double:preferences.getDouble("nome_chave", NAN);
bool:preferences.getBool("nome_chave", false);
unsigned char (uint8_t):preferences.getUChar("nome_chave", 0);
char (int8_t):preferences.getChar("nome_chave", 0);
Stringpreferences.getString("nome_chave", String());

Colocar um determinado valor em uma chave

Para colocar um valor em uma chave, existe funções de acordo com o tipo da variável requerida. O primeiro parâmetro destas funções é onde deve ser informado o nome da chave. O segundo parâmetro é onde é informado o valor para ser atribuído à chave informada. NOTA: a variável exemplo ‘valordeve possuir um tipo de acordo com o tipo da função.

unsigned short(uint16_t):preferences.putUShort("nome_chave", valor);
short (int16_t):preferences.putShort("nome_chave", valor);
unsigned int(uint32_t):preferences.putUInt("nome_chave", valor);
int (int32_t):preferences.putInt("nome_chave", valor);
unsigned long(uint32_t):preferences.putULong("nome_chave", valor);
long(int32_t):preferences.putLong("nome_chave", valor);
unsigned long64 (uint64_t):preferences.putULong64("nome_chave", valor);
long64 (int64_t):preferences.putLong64("nome_chave", valor);
float:preferences.putFloat("nome_chave", valor);
double:preferences.putDouble("nome_chave", valor);
bool:preferences.putBool("nome_chave", valor);
unsigned char (uint8_t):preferences.putUChar("nome_chave", valor);
char (int8_t):preferences.putChar("nome_chave", valor);
Stringpreferences.putString("nome_chave", valor);

Fechar um namespace

Após manipular o namespace, deve-se fecha-lo. Para fechar o namespace, usamos a função preferences.end();

Apagando todos namespace’s

Para apagar completamente todos dados de todos namespace’s, devemos antes incluir uma biblioteca:  #include <nvs_flash.h>. A biblioteca nvs_flash permite apagar toda a partição NVS (Non-Volatile Storage library) do ESP32. Não é necessário fazer nenhuma instalação, pois a mesma é nativa do pacote ESP32.

Para apagar todos os namespace’s, execute a seguinte função:

void deletaTodosNamespaces() {
    Serial.println("Partição NVS sendo limpa...");
    nvs_flash_erase(); // apague a partição NVS
    nvs_flash_init(); // inicializa a partição NVS
    while (1);
}

Após executar este código no ESP32, deve-se carregar outro sketch para não desgastar a vida útil da memória do ESP32. Execute esta função somente quando necessário.

Notas

Quando a memória flash estiver em um estado inconsistente a biblioteca Preferences.h tenta se recuperar. Ou seja, quando se desligar o ESP32 abruptamente e após ligá-lo novamente, não deve resultar em perda de dados, com exceção quando se estiver sendo efetuada a gravação de novas chaves no momento do desligamento. A biblioteca também deve ser capaz de inicializar corretamente com quaisquer dados aleatórios presentes na memória flash.

Atenção: A memória do ESP32 utilizada pela biblioteca Preferences.h (NVS – Non-volatile storage) possui um limite de ciclos escrita/apagar, por volta de 10.000 ciclos (dez mil ciclos). A memória também possui, obviamente, um espaço máximo para o armazenamento (tamanho variável de acordo com o esquema de partição do ESP32, o qual possui tamanho mínimo de 4096 bytes). Por estes motivos, faça o mínimo possível de gravações na memória para tanto para aumentar a vida útil da memória quanto para não sobrecarregar (overload) a memória.

 

Exemplo de utilização de Preferências ESP32

Na IDE Arduino, cole o seguinte código:

/***************************************************
  Exemplo de Salvamento de Prefêrencias no ESP32

  Criado em 03 de Novembro de 2021 por Michel Galvão
 ****************************************************/

// Inclusão da(s) biblioteca(s)
#include <Preferences.h>

// Criação de objeto(s) da(s) classe(s)
Preferences preferences;

// Comente as linhas de acordo com o modo desejado
//#define MODO_LEITURA
#define MODO_ESCRITA
//#define MODO_DELETE true
//#define MODO_LIMPAR_NAMESPACES

// Inclusão de bibioteca somente no modo MODO_LIMPAR_NAMESPACES
#ifdef MODO_LIMPAR_NAMESPACES

// somente é necessário incluir esta biblioteca quando for limpar todos namespaces
#include <nvs_flash.h>

#endif

// Variáveis Globais
char caractere;
int numeroInteiro;
uint32_t numeroInteiroPositivo;
float numeroDecimal;
bool booleano;
String palavra;

/**
  setup(): Código de configuração aqui, para executar uma vez
*/
void setup() {
  Serial.begin(115200); // Configura a taxa de transferência em bits por
  //                      segundo (baud rate) para transmissão serial.
  delay(1000);
  Serial.println();

  /**
    Modo para impar todos namespaces
  */
#ifdef MODO_LIMPAR_NAMESPACES

  Serial.println("Partição NVS sendo limpa...");
  nvs_flash_erase(); // apague a partição NVS
  nvs_flash_init(); // inicializa a partição NVS
  while (1);

#endif

  /**
    bool begin(const char * name, bool readOnly=false);

    Abra Preferências com o nome de namespace informado.
    @param name:      indica o nome do namespace
    @param readOnly:  opcional, true indica abrir ou criar o namespace no modo somente leitura E
                      false indica abrir ou criar o namespace  no modo de leitura/gravação
  */
  preferences.begin("config_aleats", false);

#ifdef MODO_LEITURA

  // Obtenha os valores-chave (valor de leitura)
  /**
    int8_t getChar(const char* key, int8_t defaultValue = 0);

    Obtenha o valor da chave informada no tipo char(int8_t).
    @param key:           indica o nome da chave
    @param defaultValue:  opcional, valor para retornar caso não seja encontrado valor
  */
  caractere = preferences.getChar("key_carac", '-');
  numeroInteiro = preferences.getInt("key_nInt", 0);
  numeroInteiroPositivo = preferences.getUInt("key_nIntPosi", 0);  
  numeroDecimal = preferences.getFloat("key_nDec", 0);
  booleano = preferences.getBool("key_boolean", false);
  palavra = preferences.getString("key_palavra", "NULL");

#elif defined(MODO_ESCRITA)
  // Coloque valores-chave (salve um valor)
  /**
    size_t putChar(const char* key, int8_t value);

    Coloca o valor informado da chave no tipo char(int8_t).
    @param key:    indica o nome da chave
    @param value:  valor para setar
  */
  preferences.putChar("key_carac", 'e');
  preferences.putInt("key_nInt", 23);
  preferences.putUInt("key_nIntPosi", 8764);
  preferences.putFloat("key_nDec", 3.1415);
  preferences.putBool("key_boolean", true);
  preferences.putString("key_palavra", "Eletrogate - eletrogate.com");

  // Obtenha os valores-chave (valor de leitura)
  caractere = preferences.getChar("key_carac", '-');
  numeroInteiro = preferences.getInt("key_nInt", 0);
  numeroInteiroPositivo = preferences.getUInt("key_nIntPosi", 0);
  numeroDecimal = preferences.getFloat("key_nDec", 0);
  booleano = preferences.getBool("key_boolean", false);
  palavra = preferences.getString("key_palavra", "NULL");

#elif defined(MODO_DELETE)
  Serial.print("MODO_DELETE: ");
  Serial.println(MODO_DELETE);
  if (MODO_DELETE == true) {
    // Limpa todas as preferências no namespace aberto (não exclui o namespace)
    preferences.clear();
  } else {
    // Remova as chaves dos namespace's
    preferences.remove("key_carac");
    preferences.remove("key_nInt");
    preferences.remove("key_nIntPosi");
    preferences.remove("key_nDec");
    preferences.remove("key_boolean");
    //preferences.remove("key_palavra");
  }

  // Obtenha os valores-chave (valor de leitura)
  caractere = preferences.getChar("key_carac", '-');
  numeroInteiro = preferences.getInt("key_nInt", 0);
  numeroInteiroPositivo = preferences.getUInt("key_nIntPosi", 0);
  numeroDecimal = preferences.getFloat("key_nDec", 0);
  booleano = preferences.getBool("key_boolean", false);
  palavra = preferences.getString("key_palavra", "NULL");

#endif

  /**
    void end();

    Fecha as preferências no namespace aberto.
  */
  preferences.end();

  // Exibe no monitor Serial
  Serial.print("caractere : ");
  Serial.println(caractere);

  Serial.print("numeroInteiro : ");
  Serial.println(numeroInteiro);

  Serial.print("numeroInteiroPositivo : ");
  Serial.println(numeroInteiroPositivo);

  Serial.print("numeroDecimal : ");
  Serial.println(numeroDecimal);

  Serial.print("booleano : ");
  Serial.println(booleano);

  Serial.print("palavra : ");
  Serial.println(palavra);

  while (1); // estrutura de repetição infinita

}

/**
  loop(): Código principal aqui, para executar repetidamente
*/
void loop() {
  //Nenhum código necessário: toda operação é feita no setup
}

Estaremos utilizando esta versão do ESP32:

Módulo WiFi ESP32s 38 pinos. Fonte da imagem: eletrogate.com

Recomendações importantes

Para programar uma placa ESP32, deve-se ter instalado na IDE Arduino o pacote de placas “esp32” da Espressif. Caso não possua este pacote de placas, consulte o seguinte artigo, aqui mesmo do blog da Eletrogate:

Então, para fazer o upload do código para a placa, na Arduino IDE, no menu Ferramentas → Placa → ESP32 Arduino,  Selecione a placa correspondente à sua. No nosso caso é a Node32s.

Selecione a placa Node32s

Ao fazer upload, nesta placa Node32s, quando aparecer, no quadro de avisos da Arduino IDE, a mensagem Connecting…….._____….._____ é necessário ficar pressionando o botão BOOT da placa.

Connecting........_____....._____.

Mensagem Connecting…….._____….._____.

esp32s botão boot está no canto inferior direito da placa

Botão Boot

Explicação Sketch

Para manipularmos as funções que fazem o controle dos dados não-voláteis na memória do ESP32, devemos incluir a biblioteca <Preferences.h>. Em seguida criamos um objeto da classe Preferences para fazer a manipulação dos mesmos.

Em seguida, definimos o modo que o programa irá ser operado para demonstração, sendo necessário tirar o comentário da linha com o modo desejado e comentar as demais linhas com os outros modos. No caso abaixo, temos operando o modo de escrita

Caso o modo desejado selecionado seja o MODO_LIMPAR_NAMESPACES, é incluída a biblioteca <nvs_flash.h>, a qual é responsável para limpar todos os dados incluídos através da biblioteca Preferences.h.

Em seguida declaramos as variáveis globais à serem utilizadas.

Dentro de void setup, configuramos a taxa de baud rate da Serial na velocidade 115200.

Em seguida verificamos através da diretiva de compilação #ifdef se o modo atual de funcionamento é MODO_LIMPAR_NAMESPACES. As diretivas de compilação não são compiladas pois são dirigidas ao pré-processador, que é executado pelo compilador antes da execução do processo de compilação propriamente dito. Pode-se ver mais sobre diretivas de compilação no seguinte link: https://docs.microsoft.com/pt-br/cpp/preprocessor/hash-if-hash-elif-hash-else-and-hash-endif-directives-c-cpp?view=msvc-170

Caso o modo atual de funcionamento seja MODO_LIMPAR_NAMESPACES, apagamos a partição NVS e a inicializamos novamente, sendo entrado em um laço de repetição infinito após isso, sendo necessário trocar de modo carregando outro código.

Atenção: A partição NVS – Non-volatile storage da memória do ESP32 possui um limite de ciclos escrita/apagar. Por este motivo, faça o mínimo possível de erase (apagar a memória) através de nvs_flash_erase(); para aumentar a vida útil da memória.

Em seguida, abrimos um namespace no modo leitura/gravação.

Logo em seguida, caso o modo seja o MODO_LEITURA, obtemos os valores da memória NVS e passamos para as devidas variáveis.

Seguidamente, caso o modo seja o MODO_ESCRITA, escrevemos alguns valores na memória NVS. E após, passamos os valores da memória NVS para as devidas variáveis.

Depos, se o modo seja o MODO_DELETE, verificamos se o modo possui o valor trueou false. Caso seja true, limpamos todos os valores do namespace que está aberto atualmente. Caso seja false, limpamos apenas determinadas chaves do namespace atual. Também, em seguida, passamos os valores da memória NVS para as devidas variáveis.

Após o manuseio do namespace, temos que fechá-lo.

Em seguida, imprimimos na Serial os valores presentes nas variáveis. E após, entramos em um laço de repetição infinito.

No void loop, não fazemos nada.


Hardware do Projeto

Para desenvolver o projeto de demonstração, monte o seguinte circuito:

O capacitor é necessário para estabilizar a entrada de energia para o ESP32 para quando for utilizar o WiFi. O WiFi pode chegar, de acordo com o datasheet do ESP32, à consumir 240mA. O datasheet pode ser consultado no site da Espressif, no seguinte link: https://www.espressif.com/sites/default/files/documentation/esp32_datasheet_en.pdf


Software do Projeto

Clique nesta imagem e faça o download do código arduino

Faça o download do arquivo nesse link, descompacte-o e abra na IDE Arduino.

Após abrir o arquivo “Salvando_Preferencias_no_ESP32.ino” contendo o código à ser carregado no microcontrolador do Arduino, faça o upload para a placa, seguindo as mesmas recomendações passadas anteriormente.

Pontos importantes à destacar

Deve-se incluir a biblioteca "time.h", a qual é a responsável por obter o horário do servidor NTP.

Em seguida, antes do  void setup , definimos as configurações para se conectar ao servidor NTP.

  • ntpServer: é o endereço do servdor NTP. Neste exemplo, usamos o do Google. Veja mas neste link sobre o servidor NTP do Google:

  • gmtOffset_sec: variável que define o deslocamento em segundos do fuso horário local em relação ao GMT do Meridiano de Greenwich (Greenwich Mean Time: GMT +00: 00);
  • daylightOffset_sec: variável que define o deslocamento em segundos do fuso horário local. Este valor costuma ser 3600 para horário de verão +1h ou 0 para fusos sem horário de verão (wiki Horário de verão).

Ainda antes do void setup , criamos os objetos das classes necessárias. os objetos AP_local_ip, AP_gateway, e AP_subnet são para serem utilizadas pelo ESP32 no modo access point. O objeto server, é utilizado pelo ESP32 tanto pelo modo access point quanto pelo modo station.

Para se conectar ao ESP32 no modo Access Point, deve-se informar as seguintes credenciais:

  • SSID: ESP32 Ponto de Acesso
  • Senha: senha1234567890

NOTA IMPORTANTE: O valor de Senha deve ter entre 8 e 63 caracteres para que o modo Access Point funcione corretamente.

Em void setup, abrimos um namespace chamado "config_rede" no modo leitura.

Ainda em void setup, obtemos o valor de ssid da chave "ssid" e obtemos também o valor da senha da chave "pass". Logo após fechamos o namespace aberto.

Caso o valor obtido de "ssid" e "pass" sejam NULL, devemos fazer o processo que defina um valor para "ssid" e "pass". Fazemos a atribuição dos valores das credenciais executando um web server que está no modo access point. Neste modo, podemos nos conectar diretamente ao ESP32 sem intermédio de uma rede local. A função responsável por fazer a atualização das credenciais e ainda executar o web server é a void alteraCredenciais().

Esta função está na aba de nome AP_pages. Nesta, começamos nos desconectando de qualquer conexão WiFi existente. Após isso, nos conectamos no modo do WiFi access point através da função WiFi.softAP.

A função WiFi.softAP possui como parâmetros:

.softAP(const char* ssid, const char* password, int channel, int ssid_hidden, int max_connection);

  • ssid: ssid da rede com um máximo de 63 caracteres;
  • password: senha da rede com um mínimo de 8 caracteres. Caso queira um ponto de acesso aberto, defina o valor como NULL;
  • channel: número do canal Wi-Fi (1 a 13);
  • ssid_hidden: define se deve ou não esconder o SSID da rede (Network cloaking):
    • false = transmitir SSID,
    • true = ocultar SSID;
  • max_connection: define o número máximo de clientes conectados simultâneos (1 a 4).

Em seguida, configuramos os endereços IP do access point através da função WiFi.softAPConfig.

Esta função possui como parâmetros:

.softAPConfig(IPAddress local_IP, IPAddress gateway, IPAddress subnet);

  • local_IP: endereço IP do access point;
  • gateway: endereço IP do gateway;
  • subnet: máscara de sub-rede.

Após as configurações de rede efetuadas, definimos os endereços do servidor aos quais estarão as páginas html. Fazemos isso com a função on da classe WebServer. Nesta função, deve-se informar como parâmetros:

  • O endereço no servidor: este endereço é o que o usuário acessará pelo navegador;
  • A string (cadeia de caracteres) que forma a página html.

Passamos a string através de uma função chamada handle_alteraCredenciaisAP que retorna uma string contendo a página html:

<!DOCTYPE html>
<html lang='pt-br'>
    <head>
        <meta charset='UTF-8'>
        <meta name='viewport' content='width=device-width, initial-scale=1.0'/>
        <title>ESP32</title>
    </head>
    <body id='body'>
        <div id='telaPrincipal'>
            <div id='bloqueiaTela'></div>
            <h1>Informe as Credenciais de Conexão</h1>
            <div id='mensagens'>
                <p>Insira novas credenciais para acesso. Em seguida, clique em Salvar para enviar os dados para o ESP32: </p>
            </div>
            <div style='border-style:inset; width:200px; background-color: rgb(148, 187, 242)' id='divDoForm'>
                <form action='/' method='POST' style='margin:5px'>
                    <label for='id_ssid'>Informe o SSID: </label>
                    <input type='text' name='SSID' id='id_ssid' placeholder='SSID para conexão' required>
                    <br>
                    <br>
                    <label for='id_password'>Informe a Senha: </label>
                    <input type='text' name='PASS' id='id_password' placeholder='Senha para conexão' required>
                    <br>
                    <br>
                    <input type='submit' name='SUBMIT_SALVAR' value='Salvar' id='id_salvar'>
                </form>
            </div>
        </div>
    </body>
</html>

Veja abaixo na imagem como é o código html acima interpretado pelo navegador:

Nesta mesma função, temos a funcionalidade que recebe os dados enviados do navegador (client) para o ESP32 (server). Nesta função, ao receber os dados, os salvamos na memória não-volátil do ESP32. Também enviamos uma mensagem pop-up ao usuário informando que as credenciais foram salvas.

Em seguida, utilizamos a função onNotFound da classe WebServer, em que é necessário informar uma string contendo uma página html.

Passamos a string através de uma função chamada handle_NotFound que retorna uma string contendo a página html que indica que o servidor não encontrou uma página não existente tentada à ser acessada pelo usuário (Erro HTTP 404: File Not Found). Veja o código da página:

Veja abaixo na imagem como é o código html acima interpretado pelo navegador quando se tenta acessar: http://IP_DO_ESP_NA_REDE/testeDePagina, o qual é uma página inexistente no servidor:

Após este processo, iniciamos o servidor com a função begin da classe WebServer. Em seguida, após todos estes processos, entramos em um laço de repetição infinito que irá ficar servindo o servidor do ESP32 até que um SSID e uma senha sejam informados, salvos e após, dado um determinado comando (atribuição de true para reiniciaESP), reiniciar o ESP32.

 

Voltando ao void setup, após verificar se os valores obtido de "ssid" e "pass" sejam NULL, inicializamos o WiFi no modo station utilizando as credenciais obtidas da memória NVS (através da biblioteca Preferences).

Logo após, definimos os endereços do servidor aos quais estarão as páginas html (A principal e a de erro 404) e as inicializamos.

Seguidamente, inicializamos o namespace "UTC" e extraímos o valor que indica se o horário de verão deve ou não ser ativado (3600 para ativação 0 para desativação).

Abaixo, temos a atribuição às devidas variáveis das configurações do NTP server. A função configTime é a função que define as configurações de tempo.

Já dentro de void loop(), obtemos os dados de data e hora através da função getLocalTime(), devendo ser informado um endereço (&) da variável timeinfo, a qual é do tipo estrutura tm.

Essa estrutura de dados contém:

Documentação tm: cplusplus.com/reference/ctime/tm

Logo após, inicializamos o namespace "horarios" e extraímos todos os horários em que o evento deve ser acionado. Os horários estão em uma String e apresentam a seguinte forma: hora e minuto dentro de colchetes, dispostos lado a lado sem  espaços.

Visão da disposição dos horários

Também, após, inicializamos o namespace "qtnDeHorarios" e extraímos a quantidade de horários salvos.

Logo depois, criamos uma variável estática chamada cont, à qual irá armazenar um valor de controle de exibição do horário atual no Serial Monitor. Variáveis do tipo static persistem entre chamadas da função, ou no caso o loop, preservando seu valor.

Veja como fica no monitor serial:

Seguidamente, ainda dentro do void loop(), entramos em uma estrutura de repetição (for) que irá fazer a constante verificação de que se o horário atual é igual à algum horário armazenado na memória NVS. Para buscar um horário na lista, é necessário informar à função substring um índice inclusivo e um índice exclusivo de busca. Estes índices são determinados fazendo uma busca pelo caractere [ somado de 1 (para inclusivo) e ] (para exclusivo) através da função indexOf, informando como primeiro parâmetro o caractere à ser buscado. Como pode se ter vários horários na lista, devemos saber a partir de onde fazemos a busca dos caracteres [ e ]. Para isso, devemos informar à função indexOf como segundo parâmetro a posição para começar a busca. Informando o resultado do contador i multiplicado por 7, o qual é o número de caracteres por horário.

Após determinar o horário obtido da lista, verificamos se o horário atual é igual ao horário obtido da lista. Caso seja, chamamos a função acionaEvento() que executará o evento desejado.

Na função acionaEvento(), temos o acionamento do LED onboard por 2 segundos.

Continuando no void loop(), verificamos se há conexão com o WiFi. Caso não haja, é feita a tentativa de reconexão. Fazemos esta verificação e a tentativa de correção através da função tentaConexao(). Também lidamos com as conexões do servidor através da função server.handleClient(), pertencente da classe WebServer.h. Devemos ter pelo menos um delay de 1 milissegundo para não ocorrer reinicializações realizadas pelo WatchDog Timer. Em seguida, fazemos a verificação se foi solicitado que o ESP32 se reinicializasse.

A função tentaConexao(), presente na aba WiFi_functions, verifica se o status do WiFi é diferente de WL_CONNECTED (atribuído quando conectado a uma rede WiFi). Caso o status seja diferente, tentamos fazer a conexão novamente.

Na aba STA_pages, temos o html e as funções de recebimento de solicitações do navegador (client) para com o ESP32 (server).


Funcionamento Final

Veja no vídeo abaixo o funcionamento final:


Conclusão

Com este post, podemos concluir que utilizar a recurso de Preferência do ESP32  traz muitas vantagens em qualquer projeto, pois o mesmo permite armazenar configurações do usuário ou até mesmo credenciais de conexão.

Qualquer dúvida que tenha, utilize os comentários abaixo.

Espero que tenha gostado deste artigo e até a próxima.

Conheça a Metodologia Eletrogate e ofereça aulas de robótica em sua escola!


Sobre o Autor


Michel Galvão

Hobbysta em Sistemas Embarcados e IoT. Tem experiência em Automação Residencial e Agrícola.


Eletrogate

1 de dezembro de 2021

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

Conheça a Metodologia Eletrogate e Lecione um Curso de Robótica nas Escolas da sua Região!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!