Concluir o seu projeto final registrando o seu progresso.
Resultados de aprendizagem:
Executar o plano do projeto
Aplicar técnicas de gestão do tempo
Resumir e comunicar a essência do desenvolvimento do seu projeto
Você respondeu a estas perguntas?
Que tarefas foram concluídas e que tarefas faltam?
O que funcionou? O que não funcionou?
Que questões precisam ser resolvidas?
O que vai acontecer e quando?
O que você aprendeu?
Desenvolvimento do Projeto
Estou fazendo esta tarefa com o projeto final já pronto e apresentado. Por isso, me parece sem sentido criar um plano imaginário para terminar uma coisa que já está feita. Então decidi mostrar as competências pedidas nesta tarefa na conclusão da minha Fab Academy como um todo.
Planejamento
Que tarefas foram concluídas e que tarefas faltam?
Todas as tarefas para concluir o meu projeto final estão feitas, e eu já apresentei o projeto para o Neil. Agora só faltam a documentação e os exercícios, que eu mostro na planilha abaixo. A planilha original do Excel está aqui.
O que vai acontecer e quando?
Para concluir as tarefas acima, propus o cronograma abaixo. A planilha original do Excel está aqui.
O que funcionou?
Consigo me conectar pelo celular à interface web do controlador de irrigação.
Consigo ver os horários programados e as zonas de irrigação pela interface web.
Consigo mudar o horário e a duração pela interface web.
Ele mostra a umidade e a temperatura.
Ele irriga as plantas de acordo com a programação.
O LED acrescenta mais um nível de interface para ver o que está ligado, mesmo sem celular.
A operação e a configuração são fáceis o bastante para qualquer pessoa sem conhecimento prévio conseguir operar.
Acredito que o meu projeto final melhoraria a vida de um pequeno agricultor/apaixonado por plantas, reduzindo, com um custo relativamente baixo, o tempo gasto regando as plantas. Pelo menos melhorou a vida da minha família.
O que não funcionou?
A instalação não é tão fácil; precisa de algum conhecimento e é melhor ser feita por um eletricista, porque mexe com a rede elétrica.
Que questões precisam ser resolvidas?
Preciso melhorar os aspectos de implantação. Acredito que, se eu integrar uma tomada de 230 V para a bomba, a instalação fica mais simples para o usuário.
O que você aprendeu?
Melhorei minhas habilidades em C++ fazendo este projeto.
Melhorei minhas habilidades de design eletrônico fazendo este projeto.
Abaixo documentei o meu processo de aprendizado, desde instalar a IDE PlatformIO no VS Code até melhorar as minhas habilidades de programação em C++.
Diário: datas no ambiente de teste antes da placa final
Diário de progresso do projeto final, de 6 a 17 de junho
6 de junho de 2020
Enquanto escrevo isto, não existem arquivos de design atuais, existe um esboço de código e existe um protótipo numa protoboard!
Por outro lado, existem requisitos para o hardware e para o software:
Requisitos e estágio do software
Simplificar o código criando mais funções;
Tentar separar a parte do HTML para ter um código mais limpo;
Melhorar a aparência/interface do código;
Adicionar as leituras ao HTML;
Adicionar um registro de ocorrências, como sobrecorrente;
Adicionar mais segurança para os equipamentos;
Adicionar uma interface de celular (APP);
Adicionar uma função para acertar a hora atual;
Adicionar uma função para renomear cada relé, para poder relacionar o relé com a área de interesse, ou pelo menos renomear os relés com as áreas reais da fazenda.
Requisitos e situação do design
Precisa ser pelo menos resistente à água;
7 de junho de 2020
Hoje, 7 de junho de 2020, o desenvolvimento está assim.
O código que roda nessa montagem está no GitHub da Fabfarm: Autovalve
O Shakeel, um dos nossos voluntários, conseguiu criar uma versão que roda num ESP8266, que está no mesmo repositório, aqui. Por causa da pandemia ele teve que ir embora às pressas e não teve tempo de testar o código; então funcionava, mas não era estável, porque ele heroicamente tentou compartilhar o pino analógico com o WiFi, o que causava quedas no WiFi que deixavam o sistema quase impossível de usar. Foi com esse código que eu comecei.
8 de junho de 2020
Depois de muitas horas tentando entender o código, consegui portar o código do Shakeel para um ESP32 e também dividir o arquivo, com cada função num arquivo separado.
Neste vídeo mostro o progresso que fiz até agora juntando os códigos e fazendo tudo funcionar no ESP32.
Portar o código para o ESP32 pode ser simples para um programador, mas para mim levou muitas horas.
Passos:
separei tudo o que já era função em arquivo próprio, para ficar mais legível;
depois abri outra sessão da Arduino IDE e comecei a copiar cada parte do código, linha por linha, para a nova sessão; nessa sessão renomeei o arquivo principal para "esp32irrigation"
comentei as partes do código que imprimem erros no monitor serial
O código é grande demais para mostrar no HTML, então vou deixar um link para o estado do código hoje de madrugada: código aqui.
Neste vídeo dá para ver a montagem de teste do sistema de irrigação atual da fazenda.
9 de junho de 2020
Hoje, depois de testar os relés, percebi que alguns relés funcionam e outros não. Depois de testar direto na fonte de bancada, com 5 volts aplicados no pino de sinal, acredito que isso seja por causa da tensão baixa fornecida pelo ESP32 ou da tensão do sinal.
Vou garantir que os relés tenham uma alimentação de pelo menos 5 volts, que é a tensão exigida segundo o datasheet. O sinal deve funcionar bem com 3,3 V. Vai ser testado no local.
O outro ponto é a leitura de corrente, que está completamente errada. Para ter o sistema de irrigação funcionando, vou ter que rodar sem a medição de corrente até depurar isso.
10 de junho de 2020
Depois dos testes, a conclusão sobre o problema de tensão é que o ESP32 não deve alimentar o relé; isso deve vir de uma fonte externa, ajustada em 5 volts. O sinal pode vir do ESP32 sem mudança, mas o ESP32 tem que compartilhar o mesmo terra.
Mais mudanças no código:
aumentei o intervalo em que a hora é mostrada de 3000 para 5000 ms (5 s), na variável RTCtimeInterval
aumentei o intervalo em que a configuração é mostrada de 10000 para 30000 ms (30 s), na variável configTimeInterval
coloquei nomes reconhecíveis na interface HTML dos relés
limpei mais um pouco o monitor serial, tirando os segundos da exibição das configurações
Hoje de manhã vou colocar a montagem para trabalhar de novo, programar os horários de cada irrigação e testar em campo o módulo de irrigação com ESP32.
Estranhamente, o relógio RTC externo não está marcando a hora certa. Vou ver como usar o relógio interno com uma bateria externa para o caso de falta de energia.
Segundo esta discussão encerrada no repositório do ESP no GitHub e este tópico sobre a precisão do ESP, o relógio interno vai atrasar/adiantar: "Internal RTC clock frequency error is about 5%" (o erro de frequência do relógio RTC interno é de cerca de 5%). Isso pode ser resolvido com um cristal externo de 32,768 kHz montado conforme o esquema abaixo:
Outra solução seria deixar o ESP sempre conectado à Internet, pegando a hora da Internet para compensar a imprecisão.
Atualização depois de acordar
Hoje fui instalar a montagem de teste temporária. Resolvi alguns problemas.
A montagem não funcionava quando desconectada do computador. Solução: a ligação da alimentação não estava certa; acabei ligando 2 cabos USB, um para o ESP32 e outro para os relés, e os dois compartilham o mesmo terra. Talvez eu também tenha resolvido o problema do relógio RTC com a mesma abordagem.
É assim que o protótipo está agora:
Depois dos testes voltei para o código para personalizar mais.
Primeiro assisti a uma série de vídeos no YouTube sobre funções, criando funções de verdade, e depois comecei a criar ainda mais funções.
Basicamente fui ver como reduzir a repetição de código e como mandar um valor para uma função.
A partir do código que eu tinha, criei esta função:
void turnOffRelay(int valveHere){
// wait then turn valve relay OFF
Serial.print("Waiting ");
Serial.print(waitTimeValveOff / 1000);
Serial.println("s before deactivating Valve Relay 3.");
delay(waitTimeValveOff);
digitalWrite(valveHere, LOW);
valveHere = 0;
Serial.print("*** Valve Relay ");
Serial.print(valveHere);
Serial.println("3 turned OFF ***");
}
Atenção:valveHere = 0; roda antes de Serial.print(valveHere);, então a mensagem sai sempre "Valve Relay 03 turned OFF", seja qual for a válvula. O "3" também está fixo nas duas mensagens. Imprima valveHere antes de zerar (ou tire essa linha) e tire o "3" fixo dos textos.
Toda vez que quero chamar esta função, uso:
{
int valveHere = valveRelay3;
turnOffRelay (valveHere);
}
O motivo de eu usar as {} é que eu tinha que declarar a variável local valveHere; assim eu podia substituir só aquele pedaço de código cada vez que chamava a função.
Tecnicamente, chamar essa função seria só ter este código aqui: turnOffRelay (valveHere); mas isso não funcionaria sozinho, porque estou sempre substituindo o pino do relé da válvula com int valveHere = valveRelay3;
Atenção: as {} e a variável local não são necessárias: o pino já vai como argumento, então só turnOffRelay(valveRelay3); faz a mesma coisa.
Ainda não testei o comportamento, mas pelo menos compila :-)
Gravei no ESP32 e o comportamento é inesperado. A função fica imprimindo sem parar. Acho que preciso devolver o estado de uma variável e não fiz isso. Vou tentar usar o código return variable; para fazer isso e depois testar, mas antes vou assistir de novo ao vídeo sobre funções para entender melhor.
13 de junho de 2020
Argh... OK, faz tempo que não atualizo. Bom, o último código que subi funciona e tenho algumas funções, mas ainda não estou satisfeito, porque funciona no Arduino mas não no PlatformIO. O que aprendi até agora é que a Arduino IDE faz muita coisa "por baixo dos panos", e no PlatformIO você precisa configurar tudo com mais detalhe.
No fim consegui gravar um arquivo no SPIFFS usando o PlatformIO. Eu já conseguia fazer isso com o Arduino, mas sem entender direito como. Agora acho que talvez tenha entendido. Para fazer isso, tive que configurar o arquivo platformio.ini e a árvore de diretórios seguindo uma série de passos, então vamos ver:
Criei um novo arquivo de projeto na área da aba "projects" do PlatformIO
Depois criei uma pasta data no mesmo nível da pasta src. Outra opção é colocar no arquivo platformio.ini um caminho diferente para a pasta data.
Depois, com a placa conectada, rodei pio run -t uploadfs
Tive que tirar a biblioteca Time do PlatformIO, porque ela estava em conflito com a ESPAsyncWebServer, e foi aqui que encontrei a informação.
Funcionou.
Com isso comecei a entender como eu não sabia o que estava fazendo. O código com que comecei tinha o HTML dentro do sketch do Arduino. Isso me incomodava. Agora, com esse novo entendimento, consigo deixar o HTML separado do arquivo do sketch.
E este, caso eu queira usar o plugin da Arduino IDE para o upload.
Queria muito ter entendido isso melhor antes! Passei tantos dias nisso!
O código C++ até agora.
/****************************************************************************
* Aknowledments *
* by LucioPGN *
****************************************************************************/
/* Up to this date: 07th of June 2020 I don't consider myself a programer
* so I need to stand on top of giants sholders for my programing projects:
* A Portion of this code was based on Rui Santos Code;
* A Portion of this code was based on losely based Shakeels code for ESP8266;
* A Portion of this code was based on several websites I lost track of....!
* My contributions:
* -So far I made it work on platformio :), that took me quite a lot of time
* -That means:
* +created a new project;
* +created a folder named data under the main folder (fabfarm_irrigation)
* +linked the platformio.ini to the folder of the project + the data folder
* +linked the needed libraries to their github repo in the platformio.ini
* +found a conflict with time library and ESPAsyncWebserverLibrary can be solved by renaming time library or by removing it
* +Made it work with a separate HTML file under the data folder using SPIFFS
* +I can load the data to Spiffs inside platformio
* Things I still want to program for my final project:
* -so far I ported Shakeels code into ESP32;
* -simplify the code creating functions rather than repeating the code;
* -try to separate the HTML part for a cleaner code (this applies to maybe mix up the other previous code to this);
* -Improve the appearance/Interface of the code
* -Add readings to HTML
* -Add a log of occurrences like over current
* -Add more safety for the equipment
* -Add a phone interface (APP)
* -Add function to set current time
* -Add renaming function to each relay so one can relate the relay to the area of interest or at least rename relays to actual areas of the farm.
*
****************************************************************************/
//Required Libraries
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include "SPIFFS.h"
#include <AsyncTCP.h>
// Network Credentials
const char* ssid = "rato";
const char* password = "imakestuff";
//Start the Async Web Server listening on port 80
AsyncWebServer server(80);
// Set to true to define Relay as Normally Open (NO)
#define RELAY_NO false
// Set number of relays, will be used in the array
#define NUM_RELAYS 4
// Assign each GPIO to a relay
int relayGPIOs[NUM_RELAYS] = {26, 25, 33, 32};
//
const char* PARAM_INPUT_1 = "relay";
const char* PARAM_INPUT_2 = "state";
// Replaces placeholder with button section in your web page
String processor(const String& var){
//Serial.println(var);
if(var == "BUTTONPLACEHOLDER"){
String buttons ="";
for(int i=1; i<=NUM_RELAYS; i++){
String relayStateValue = relayState(i);
//Here parts of the HTML will be parsed to index.html like Relay # followed by its value in variable for the GPIO numbers
buttons+= "<h4>Turn on water on " + String(i) + "</h4><h4>Valve (relay) #" + String(i) + " - GPIO " + relayGPIOs[i-1] + "</h4><label class=\"switch\"><input type=\"checkbox\" onchange=\"toggleCheckbox(this)\" id=\"" + String(i) + "\" "+ relayStateValue +"><span class=\"slider\"></span></label>";
}
return buttons;
}
return String();
}
String relayState(int valveRelayNum){
if(RELAY_NO){
if(digitalRead(relayGPIOs[valveRelayNum-1])){
return "";
}
else {
return "checked";
}
}
else {
if(digitalRead(relayGPIOs[valveRelayNum-1])){
return "checked";
}
else {
return "";
}
}
return "";
}
void setup(){
// Serial port for debugging purposes
Serial.begin(9600);
// Initialize SPIFFS
if(!SPIFFS.begin(true)){
Serial.println("An Error has occurred while mounting SPIFFS");
return;
}
// Set all relays to off when the program starts - if set to Normally Open (NO), the relay is off when you set the relay to HIGH
for(int i=1; i<=NUM_RELAYS; i++){
pinMode(relayGPIOs[i-1], OUTPUT);
if(RELAY_NO){
digitalWrite(relayGPIOs[i-1], HIGH);
}
else{
digitalWrite(relayGPIOs[i-1], LOW);
}
}
// Connect the ESP to the Wi-Fi using the credentials entered before
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(1000);
Serial.println("Connecting to WiFi..");
}
// Print ESP32 Local IP Address
Serial.println(WiFi.localIP());
/*
*Now we are going to configure the route where server will be listening for incoming HTTP requests
and a function that will be executed when a request is received on that route.
We specify this by calling the "on" method on the server object. With server.on(){};
As first input, this method receives a string with the path where it will be listening.
We are going to set it to listen for requests on the "/" route. This could be anything.
It is basically what you write after the ip adress when in the browser or an APP.
This website has a great explanation of the ESP32 Arduino: Asynchronous HTTP web server
https://techtutorialsx.com/2017/12/01/esp32-arduino-asynchronous-http-webserver/
So...
- First parameter here is: "/" thats the root directory.
- Second parameter is HTTP_GET thats an enum of type WebRequestMethod a method defined in the library here --> https://github.com/me-no-dev/ESPAsyncWebServer/blob/63b5303880023f17e1bca517ac593d8a33955e94/src/ESPAsyncWebServer.h
- Third parameter is a the function AsyncWebServerRequest
So there is this c++ lambda function used here. My litle understanding is that they are locally declared unamed function this means they dont have a name and are declared locally :-)
I don't grasp the concept fully haha.
the syntax is [captures](params){body} where in here [] is empity
*/
// Route for root / web page
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
request->send(SPIFFS, "/index.html", String(), false, processor);
});
// Send a GET request to <ESP_IP>/update?relay=<inputMessage>&state=<inputMessage2>
server.on("/update", HTTP_GET, [] (AsyncWebServerRequest *request) {
String inputMessage;
String inputParam;
String inputMessage2;
String inputParam2;
// GET input1 value on <ESP_IP>/update?relay=<inputMessage>
if (request->hasParam(PARAM_INPUT_1) & request->hasParam(PARAM_INPUT_2)) {
inputMessage = request->getParam(PARAM_INPUT_1)->value();
inputParam = PARAM_INPUT_1;
inputMessage2 = request->getParam(PARAM_INPUT_2)->value();
inputParam2 = PARAM_INPUT_2;
if(RELAY_NO){
Serial.print("NO ");
digitalWrite(relayGPIOs[inputMessage.toInt()-1], !inputMessage2.toInt());
}
else{
Serial.print("NC ");
digitalWrite(relayGPIOs[inputMessage.toInt()-1], inputMessage2.toInt());
}
}
else {
inputMessage = "No message sent";
inputParam = "none";
}
Serial.println(inputMessage + inputMessage2);
//This is the last part of the lambda function.
//This method receives as first input the HTTP response code, which will be 200 in our case. This is the HTTP response code for "OK".
request->send_P(200, "text/plain", "OK");
});
// Start server here
server.begin();
}
void loop(){
}
Atenção:hasParam(PARAM_INPUT_1) & hasParam(PARAM_INPUT_2) usa o & bit a bit; o E lógico é &&. Com dois bool o resultado acaba sendo o mesmo, mas não faz curto-circuito, e o operador certo é &&.
A principal diferença em relação ao código original é que ele roda no PlatformIO para VS Code e também usa uma estrutura de pastas separada para o arquivo HTML, que é gravado no SPIFFS do ESP.
Depois de toda essa descoberta pessoal, decidi que preciso começar o código do zero e usar só o algoritmo geral do sistema de irrigação atual instalado na fazenda. O motivo é que o código está bagunçado e repetitivo demais; as abordagens que encontrei nas fontes de ontem me fizeram perceber que é mais rápido adicionar as funcionalidades ao último código, uma por uma, do que recodificar tudo a partir do código que roda hoje na fazenda.
Hoje vou tentar adicionar informações atualizadas, como temperatura e umidade. Estou me baseando de novo no trabalho do Rui Santos, nesta página sobre uma estação meteorológica. Com essa abordagem acredito que também vou conseguir incluir a hora do dia usando um relógio RTC.
Depois de um pouco de trabalho, a página está assim:
Acredito que a temperatura agora está negativa porque o sensor de temperatura e umidade está com defeito ou a minha fiação está meio frouxa. Acho que, quando eu fresar a minha própria placa e soldar tudo em vez de espetar numa protoboard, vai ficar mais confiável. De qualquer forma, estou recebendo os valores e consigo mostrá-los na interface web.
No monitor serial já consigo mostrar a hora da Internet e as informações da rede; agora preciso descobrir um jeito de pegar o valor da hora e mostrar na interface web. No momento estou mandando pelo código a palavra TESTE, e é ela que aparece no lugar da hora.
Com pequenas modificações, mudando de uma função void para uma função String e adicionando um return que chamei de "timeAsAString":
//this function was found here https://arduino.stackexchange.com/questions/52676/how-do-you-convert-a-formatted-print-statement-into-a-string-variable
//I did a minor change so instead of a void function it now returns a string to be used to show time in the webinterface
String printFarmTime()
{
time_t rawtime;
struct tm timeinfo;
getLocalTime(&timeinfo);
char timeStringBuff[50]; //50 chars should be enough
strftime(timeStringBuff, sizeof(timeStringBuff), "%A, %B %d %Y %H:%M:%S", &timeinfo);
//print like "const char*"
Serial.println(timeStringBuff);
//Construct to create the String object
String timeAsAString(timeStringBuff);
return timeAsAString;
}
Atenção: esta versão tirou a verificação if(!getLocalTime(&timeinfo)), então, se o NTP ainda não respondeu, ela formata uma hora vazia (lixo). Mantenha a verificação e devolva algo como "time not set" quando falhar.
Então, com isso resolvido, vou tentar criar as funções para ligar e desligar os relés de acordo com o horário programado. Vamos ver se consigo terminar hoje...
Eu poderia seguir a abordagem de dividir as datas como String e convertê-las para outros tipos de valor para as operações que pretendo fazer. Outra possibilidade é pegar os dados já no tipo de que preciso desde o começo; embora eu prefira a segunda, ainda não encontrei uma solução, porque ainda não entendo bem a função getLocalTime(). Então vou ler mais sobre ela agora.
Peraí... essa foi rápida! Encontrei um post que resolve o que eu preciso. Aqui está:
Então me baseei no post acima para criar a função abaixo:
int gimeTime(char what) {
int OnlyYear;
int onlyMonth;
int onlyDay;
int onlyHour;
int onlyMin;
int onlySec;
struct tm timeinfo;
if(!getLocalTime(&timeinfo)){
Serial.println("Failed to obtain time");
//return;
}
onlyHour = timeinfo.tm_hour;
onlyMin = timeinfo.tm_min;
onlySec = timeinfo.tm_sec;
onlyDay = timeinfo.tm_mday;
onlyMonth = timeinfo.tm_mon + 1;
OnlyYear = timeinfo.tm_year +1900;
switch (what) {
case 1:
return onlyHour;
break;
case 2:
return onlyMin;
break;
case 3:
return onlySec;
break;
default:
// if nothing else matches, do the default
// default is optional
break;
}
}
Atenção: o default termina sem return, então a função devolve um valor indefinido para qualquer coisa que não seja 1, 2 ou 3 (ponha return -1; ali). O mesmo quando o getLocalTime() falha: com o return comentado, ela segue lendo um timeinfo sem hora válida.
É isso, agora finalmente posso trabalhar no código de agendamento dos timers. Aliás, passei quase o dia inteiro tentando um jeito mais simples de pegar os horários, mas simplesmente não consegui, porque estou penando para entender o tipo (struct). Bom, talvez eu volte a isso no futuro, mas por enquanto está bom.
Estou muito frustrado; preciso passar para os átomos e fechar a parte dos bits do meu projeto.
Tarde da noite, o máximo que avancei foi fazer uma pequena função para desligar os relés; aqui está:
int zoneStartStop (int zone, int startStop){
int waitTime = 1000;
if (startStop == 1){
digitalWrite(relayGPIOs[zone], startStop);
Serial.print("*** Zone Valve");
Serial.print(zone);
Serial.println("ON ***");
delay(waitTime);
digitalWrite(pumpGpio, startStop);
Serial.print("*** Pump turned ON ***");
}
else{
digitalWrite(pumpGpio, startStop);
Serial.println("*** Pump turned OFF ***");
delay(waitTime);
digitalWrite(relayGPIOs[zone], startStop);
Serial.print("*** Zone Valve");
Serial.print(zone);
Serial.println("OFF ***");
}
}
(Código corrigido depois: ao parar, agora a bomba desliga antes de a válvula fechar, para nunca empurrar contra a válvula fechada, e a mensagem diz OFF.)
É um começo, mas ainda não chegou lá, e eu ainda não testei.
Atualização da manhã:
Então agora vou olhar outro exemplo para programar um timer, de novo do Rui Santos; está aqui. Talvez eu consiga mudar um pouco, integrar ao meu código e adicionar um horário de início, e depois usar o slider para definir quantos minutos vai rodar. Isso já seria suficiente por enquanto!
Até agora deixei o código compatível com o PlatformIO e também separei o arquivo index.html do código C++ e coloquei numa pasta própria, para ser gravado pela rotina de gravação no SPIFFS que descrevi em 13/06/2020.
Depois de brincar um tempo com um kit de desenvolvimento ESP32, eu já tinha uma ideia do que precisava na minha placa. Então vamos ver a lista de capacidades que eu quero nela:
Controle:
3 relés de válvula;
1 relé da bomba;
Sensoriamento:
Sensor de temperatura e umidade;
Header de expansão para amperímetro (da bomba);
Carga/medição da bateria;
Header de expansão ou pad de solda para relógio RTC;
Com os requisitos em mãos, fui procurar uma biblioteca do Eagle para o ESP-WROOM-32 e encontrei várias:
Depois de avaliar todas, decidi usar a do MacroYau/MacroYau.
Como quero o máximo de compatibilidade com os dev kits de ESP32 com que eu estava acostumado, fui procurar e encontrei uma pinagem do ESP32 WROOM, que usei para nomear os pinos.
Encontrada de novo no site Random Nerd Tutorials :-)
O passo seguinte foi ligar fios de net em cada pino. Queria ter feito isso na ordem em que estou apresentando, porque teria me poupado de refazer muitos fios que liguei errado da primeira vez. Bom, a lição é que o número lógico do pino e o número físico do pino nem sempre são iguais; então, com isso em mente, desta vez liguei tudo certo.
Este é o resultado final:
O relé que encontrei na SnapEDA não é o mesmo modelo que eu tenho, mas o footprint é o mesmo.
O circuito do relé encontrei online, aqui, no web.archive; o esquema está abaixo.
Com o esquema em mãos, desenhei o seguinte no Eagle:
Como por enquanto não tenho o módulo TP4056, o meu circuito vai ter só a parte de medição.
Os resistores do divisor de tensão foram calculados com a calculadora online daqui.
Para o futuro, encontrei a biblioteca do TP4056 aqui, graças a uma dica do Luis Carvão, o meu instrutor remoto.
A biblioteca do sensor de umidade está aqui; com ela em mãos, foi só ligar alguns fios de net ao pino 32 do ESP32.
Para o sensor de corrente escolhi usar o ACS712; a biblioteca encontrei neste GitHub. O esquema pronto está abaixo:
Também peguei a biblioteca do relógio de tempo real na SnapEDA, mas por enquanto não vou usar, então o esquema completo que desenhei está abaixo:
Depois de um tempo olhando para o esquema, percebi alguns erros: esqueci de ligar o resistor do LED dos relés ao VCC, também esqueci de incluir um botão de reset e um botão de programação, necessários para colocar o ESP em modo de programação, e também os headers para o modo serial e para expansão; e, por fim, decidi incluir um cristal para marcar melhor a hora.
Depois desenhei os botões de Reset e de Programação:
Aqui corrigi os relés, adicionando o VCC, e também coloquei um optoacoplador entre o sinal e os relés para evitar qualquer retorno vindo dos relés.
Aqui adicionei o header TX RX, um cristal para o RTC interno marcar melhor a hora e headers de compatibilidade com o ESP32 DevKit.
Então, espero que finalmente, o meu esquema final...
Agora vou começar o roteamento da placa. Tenho que dizer que estou um pouco ansioso, porque nunca fiz uma placa tão complexa!
Olha que animador o que eu vejo quando passo para o modo board no Eagle!!!
A primeira coisa que faço é mover o footprint do DevKit para o centro junto com o ESP32.
Depois de tentar um tempo ligar na mão, dei uma chance ao autorouter. Tive que parar depois de alguns minutos e o resultado é ridículo. Vou tentar de novo movendo o ESP32 para cima e, se não der, não vou tentar a compatibilidade com o ESP32 DevKit.
Então desisti da compatibilidade com o DevKit e decidi ir devagar desta vez. Fiz um backup do meu trabalho, que abri em outra janela do Eagle, e apaguei todos os componentes em que eu não estava trabalhando; o esquema e a placa ficaram assim. A minha ideia é copiar os componentes de volta do backup à medida que eu termino cada passo. Então abaixo está como ficou o passo um.
Na placa dá para ver o #1, o relé, o #2, o circuito do sensor de corrente, e o #3, o LED da placa.
Então agora copiei o primeiro relé de válvula e, quando fui para a placa, o Eagle me mostrou os componentes colados. Acho que assim é bem mais fácil.
Até agora a placa está assim. Ainda não roteei nenhum fio de alimentação, incluindo GND, 3.3V e 5V. Vou tentar o autorouter agora para ter uma ideia e talvez rotear na mão. Todo o processo de roteamento está mais bem descrito na semana de dispositivos de entrada.
Depois de terminar o layout da placa, decidi adicionar o logo. Clique em File, Import e Bitmap (por enquanto o Eagle só aceita .bmp).
Escolha a cor a ser importada.
Depois selecione a unidade, mm no meu caso, e o tamanho, e OK.
Como eu queria fresar o logo em vez de carimbar, tive que movê-lo para a camada top.
O layout final da placa, com furos e contornos de fresagem, como descrito na semana de dispositivos de saída.
Fresagem
Aqui mostro a visualização das rotas do G-code, com o vermelho sendo a trilha mais perto do cobre que vai ficar.
Como sempre, e de novo mais bem descrito na semana de dispositivos de entrada, aqui vai uma olhada no software de autolevel.
Soldagem e teste/depuração
Inspecionando a placa contra a luz.
Para verificar se as trilhas estão todas boas, coloco a luz embaixo da placa e reviso tudo, procurando curtos ou trilhas faltando.
O lado de baixo sendo montado.
Coloco os componentes grandes por último, para não dificultar a soldagem dos componentes SMD pequenos.
Lendo o monitor serial/esquema para gravar o firmware
Depois de soldar tudo, conecto a placa ao meu adaptador serial. Abaixo está o esquema.
Colei a primeira saída do ESP32 com o firmware de fábrica. Ele dá boot!
--- Available filters and text transformations: colorize, debug, default, direct, esp32_exception_decoder, hexlify, log2file, nocontrol, printable, send_on_enter, time
--- More details at http://bit.ly/pio-monitor-filters
--- Miniterm on COM5 115200,8,N,1 ---
--- Quit: Ctrl+C | Menu: Ctrl+T | Help: Ctrl+T followed by Ctrl+H ---
ets Jun 8 2016 00:22:57
rst:0x1 (POWERON_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
configsip: 0, SPIWP:0x00
clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
mode:DIO, clock div:2
load:0x3fff0008,len:8
load:0x3fff0010,len:3480
load:0x40078000,len:7804
ho 0 tail 12 room 4
load:0x40080000,len:252
entry 0x40080034
␛[0;32mI (45) boot: ESP-IDF v2.0-3-gbef9896 2nd stage bootloader␛[0m
␛[0;32mI (45) boot: compile time 05:59:45␛[0m
␛[0;32mI (45) boot: Enabling RNG early entropy source...␛[0m
␛[0;32mI (64) boot: SPI Speed : 40MHz␛[0m
␛[0;32mI (77) boot: SPI Mode : DIO␛[0m
␛[0;32mI (89) boot: SPI Flash Size : 4MB␛[0m
␛[0;32mI (101) boot: Partition Table:␛[0m
␛[0;32mI (113) boot: ## Label Usage Type ST Offset Length␛[0m
␛[0;32mI (136) boot: 0 phy_init RF data 01 01 0000f000 00001000␛[0m
␛[0;32mI (159) boot: 1 otadata OTA data 01 00 00010000 00002000␛[0m
␛[0;32mI (182) boot: 2 nvs WiFi data 01 02 00012000 0000e000␛[0m
␛[0;32mI (205) boot: 3 at_customize unknown 40 00 00020000 000e0000␛[0m
␛[0;32mI (228) boot: 4 ota_0 OTA app 00 10 00100000 00180000␛[0m
␛[0;32mI (252) boot: 5 ota_1 OTA app 00 11 00280000 00180000␛[0m
␛[0;32mI (275) boot: End of partition table␛[0m
␛[0;32mI (288) boot: Disabling RNG early entropy source...␛[0m
␛[0;32mI (305) boot: Loading app partition at offset 00100000␛[0m
␛[0;32mI (1481) boot: segment 0: paddr=0x00100018 vaddr=0x00000000 size=0x0ffe8 ( 65512) ␛[0m
␛[0;32mI (1481) boot: segment 1: paddr=0x00110008 vaddr=0x3f400010 size=0x1c5f0 (116208) map␛[0m
␛[0;32mI (1498) boot: segment 2: paddr=0x0012c600 vaddr=0x3ffb0000 size=0x0215c ( 8540) load␛[0m
␛[0;32mI (1528) boot: segment 3: paddr=0x0012e764 vaddr=0x40080000 size=0x00400 ( 1024) load␛[0m
␛[0;32mI (1551) boot: segment 4: paddr=0x0012eb6c vaddr=0x40080400 size=0x1b028 (110632) load␛[0m
␛[0;32mI (1630) boot: segment 5: paddr=0x00149b9c vaddr=0x400c0000 size=0x00034 ( 52) load␛[0m
␛[0;32mI (1631) boot: segment 6: paddr=0x00149bd8 vaddr=0x00000000 size=0x06430 ( 25648) ␛[0m
␛[0;32mI (1648) boot: segment 7: paddr=0x00150010 vaddr=0x400d0018 size=0x7a56c (501100) map␛[0m
␛[0;32mI (1675) heap_alloc_caps: Initializing. RAM available for dynamic allocation:␛[0m
␛[0;32mI (1697) heap_alloc_caps: At 3FFBA6B8 len 00025948 (150 KiB): DRAM␛[0m
␛[0;32mI (1719) heap_alloc_caps: At 3FFE8000 len 00018000 (96 KiB): D/IRAM␛[0m
␛[0;32mI (1740) heap_alloc_caps: At 4009B428 len 00004BD8 (18 KiB): IRAM␛[0m
␛[0;32mI (1761) cpu_start: Pro cpu up.␛[0m
␛[0;32mI (1773) cpu_start: Single core mode␛[0m
␛[0;32mI (1786) cpu_start: Pro cpu start user code␛[0m
␛[0;32mI (1846) cpu_start: Starting scheduler on PRO CPU.␛[0m
␛[0;32mI (2085) uart: queue free spaces: 10␛[0m
Bin version:0.10.0
I (2087) wifi: wifi firmware version: c604573
I (2087) wifi: config NVS flash: enabled
I (2088) wifi: config nano formating: disabled
I (2096) wifi: Init dynamic tx buffer num: 32
I (2097) wifi: wifi driver task: 3ffc4eac, prio:23, stack:3584
I (2102) wifi: Init static rx buffer num: 10
I (2106) wifi: Init dynamic rx buffer num: 0
I (2110) wifi: Init rx ampdu len mblock:7
I (2114) wifi: Init lldesc rx ampdu entry mblock:4
I (2118) wifi: wifi power manager task: 0x3ffca254 prio: 21 stack: 2560
I (2125) wifi: wifi timer task: 3ffcb2d4, prio:22, stack:3584
␛[0;31mE (2130) phy_init: PHY data partition validated␛[0m
␛[0;32mI (2152) phy: phy_version: 329, Feb 22 2017, 15:58:07, 0, 0␛[0m
I (2152) wifi: mode : softAP (3c:71:bf:10:66:e1)
I (2155) wifi: mode : sta (3c:71:bf:10:66:e0) + softAP (3c:71:bf:10:66:e1)
I (2159) wifi: mode : softAP (3c:71:bf:10:66:e1)
Gravando o firmware:
Para gravar o meu firmware uso o Microsoft VS Code com o PlatformIO.
Antes de começar a gravação, primeiro tive que configurar o ambiente do PlatformIO. Descrevo isso aqui, na semana de desenvolvimento do projeto.
Depois seguro o botão do GPIO0 e aperto o botão de reset para entrar no modo de gravação do firmware. Aí clico no ícone de gravar o firmware e ele grava. Sucesso!
Depurando a placa.
Acabei tirando todos os componentes para analisar os sinais produzidos diretamente pelo microprocessador.
O resultado da depuração foi que descobri que não havia nada de errado com o código, com a soldagem do microprocessador ou com as trilhas próximas.
O que descobri foi que eu precisava colocar resistores de 220 ohms entre o sinal e o anodo, o pino número 1, de cada optoacoplador. Acontece que o optoacoplador tem um LED interno, e a corrente usada para acioná-lo precisa ser limitada.
Então R = (3,3 V - 1,2 V) / 20 mA = 105 ohms, ou
Aí cortei as trilhas e soldei os resistores de 220 ohms, porque era o que eu tinha em mãos, e funcionou, mas o valor certo é 120 ohms, como a calculadora de resistores online mostra.
E é assim que ficou o esquema do relé:
Código de rede no firmware
Há dois tipos de comunicação acontecendo na minha placa que posso descrever:
A comunicação entre o celular e o ESP32 (protocolo TCP/IP)
"Enables network connection (local and Internet) using the ESP32 built-in WiFi. With this library you can initiate Servers, Clients and send/receive UDP packets through WiFi. The shield can connect either to open or encrypted networks (WEP, WPA). The IP address can be assigned statically or through a DHCP. The library can also manage DNS." (Permite conexão de rede, local e com a Internet, usando o WiFi embutido do ESP32. Com esta biblioteca você pode iniciar servidores e clientes e enviar/receber pacotes UDP pelo WiFi. O shield pode se conectar a redes abertas ou criptografadas (WEP, WPA). O endereço IP pode ser atribuído de forma estática ou por DHCP. A biblioteca também pode gerenciar DNS.)
"This is a fully asynchronous TCP library, aimed at enabling trouble-free, multi-connection network environment for Espressif's ESP32 MCUs. This library is the base for ESPAsyncWebServer" (Esta é uma biblioteca TCP totalmente assíncrona, feita para permitir um ambiente de rede sem problemas e com múltiplas conexões para os microcontroladores ESP32 da Espressif. Esta biblioteca é a base da ESPAsyncWebServer.)
"Async HTTP and WebSocket Server for ESP8266 Arduino" (Servidor HTTP e WebSocket assíncrono para ESP8266 Arduino)
Então, basicamente, no meu projeto essas bibliotecas são responsáveis por estabelecer a conexão de rede com um roteador e também por criar um ponto de acesso.
Elas também criam o servidor web e um servidor DHCP, e com isso ele fornece um endereço IP caso alguém se conecte pelo ponto de acesso criado com este código:
//Soft Wifi Access point setup
WiFi.softAP("softap", "password_here");
IPAddress IP = WiFi.softAPIP();
A conexão com o roteador é estabelecida com este código:
Atenção: o ssid e o password lidos do JSON são sobrescritos logo em seguida pelos valores fixos das duas linhas seguintes, então as credenciais do arquivo JSON nunca são usadas. Tire as duas atribuições fixas se quiser que os valores do JSON valham.
O servidor web é iniciado com este código: AsyncWebServer server(81);
Ligando o C++ e o HTML com a ArduinoJson
JSON é o formato que escolhi para a comunicação entre a interface web e o microprocessador.
A ArduinoJson.h é descrita assim na sua documentação:
ArduinoJson is a C++ JSON library for Arduino and IoT (Internet Of Things) (ArduinoJson é uma biblioteca JSON em C++ para Arduino e IoT, a Internet das Coisas)
Uma ótima fonte de documentação e um ótimo assistente para criar o programa de parsing deserializeJson() e o programa de serialização serializeJson(doc, Serial) é o site da ArduinoJson.
Para usar a biblioteca, primeiro tive que criar um arquivo json. Esse arquivo vai ser usado pelo assistente para gerar o código em C++.
Com o arquivo JSON criado na mão, é hora de criar os programas em C++. O assistente então cria o código C++ para serializar e também fazer o parsing dos dados de e para o arquivo JSON que está dentro do sistema de arquivos.
Na caixa de diálogo da esquerda colei o conteúdo do arquivo json.
Com a ajuda do meu amigo Jeff Knight, sem quem eu não teria conseguido, o código gerado foi simplificado para o meu programa. Depois criei algumas funções, e a imagem abaixo mostra o monitor serial imprimindo o resultado da desserialização do arquivo JSON lido.
Fiz este diagrama lógico para não esquecer como funciona:
Fresando a caixa
Projetei a caixa no SolidWorks e animei a placa dentro dela, abaixo.
Aqui fresei o interior da caixa, com os furos dos parafusos e os espaçadores todos integrados.
Depois de virar a caixa dentro de um gabarito, consigo alinhar a caixa perfeitamente, então fresei o lado da frente.
Aqui a placa e a caixa.
Aqui testo o encaixe antes de ligar os fios na placa.
Ligando a placa dentro da caixa.
Uso uma passagem de fios à prova d'água para entrar na caixa.
Aqui testo o encaixe da placa dentro da caixa com os fios.
Aqui os fios estão presos e prontos para ser fechados dentro.
Aqui a caixa está parafusada e pronta para ser ligada.
Aplicando epóxi.
Passei fita por fora e coloquei camadas de epóxi dentro dos furos, para a luz dos LEDs passar para fora mesmo eles não estando totalmente alinhados com os LEDs lá dentro.