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.
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.
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:
As chaves (key) podem ter um comprimento máximo de 15 caracteres.
O valor (value) podem ter um dos seguintes tipos:
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.
Também há a possibilidade de haver múltiplos namespaces com chaves de mesmos nomes, sem ocorrer nenhuma invalidação:
Para abrir um namespace utilize a função begin: preferences.begin("nome_namespace", false);
Deve-se passar como parâmetros:
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.
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); |
String | preferences.getString("nome_chave", String()); |
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 ‘valor
‘ deve 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); |
String | preferences.putString("nome_chave", valor); |
Após manipular o namespace, deve-se fecha-lo. Para fechar o namespace, usamos a função preferences.end();
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.
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.
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
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.
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.
Mensagem Connecting…….._____….._____.
Botão Boot
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.
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 true
ou 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.
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
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.
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:
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:
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:
Fonte: cplusplus.com
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).
Veja no vídeo abaixo o funcionamento final:
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!
|
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!