Datas do diário
06/06/2020 07/06/2020 08/06/2020 09/06/2020 10/06/2020 13/06/2020 14/06/2020 15/06/2020 16/06/2020 17/06/2020 27/06/2020 28/06/2020Hoje decidi mudar meu projeto final de novo!
Eu precisava da irrigação funcionando na fazenda, e não precisava de mais galinhas agora, então, bom, mudei de novo. Isso não quer dizer que desisti da chocadeira de ovos de galinha. Só quer dizer que talvez eu tenha DOIS projetos finais :-). Bom, talvez não, a realidade é que vou focar na irrigação: minhas árvores frutíferas precisam de água, a estufa também. Preciso tanto disso agora, e preciso que funcione bem, que a mudança vale muito a pena.
Enquanto escrevo isto, não há arquivos de design no momento; há um esboço do código e há um protótipo numa breadboard!
Por outro lado, há requisitos para o hardware e para o software:
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 Wi-Fi, o que causava quedas no Wi-Fi que deixavam o sistema quase impossível de usar. Foi com esse código que comecei.
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 em juntar os códigos e fazê-los funcionar no ESP32.
Portar o código para o ESP32 pode ser simples para um programador, mas para mim levou muitas horas.
Passos:
O código é grande demais para mostrar no HTML, então vou deixar um link para o estado do código nesta madrugada: código aqui.
Neste vídeo dá para ver a montagem de teste do sistema de irrigação atual da fazenda.
Hoje, depois de testar os relés, percebi que alguns relés funcionam e outros não. Depois de testar na fonte de bancada do lab, com 5 volts aplicados direto no pino de sinal, acho que isso é por causa da tensão baixa fornecida pelo ESP32, ou da tensão do sinal.
Aí assisti ao vídeo do GreatScott! no YouTube sobre relés e optoacopladores.
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 medir a corrente até depurar.
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:
RTCtimeIntervalconfigTimeIntervalVou colocar a montagem para funcionar de novo hoje de manhã, definir horários para cada irrigação e testar em campo o módulo de irrigação com ESP32.
Estranhamente, o relógio RTC externo não está mantendo 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 de issue fechada no repositório do ESP32 no GitHub e este tópico sobre a precisão do ESP, o relógio interno vai derivar ("Internal RTC clock frequency error is about 5%"); dá para resolver isso com um cristal externo de 32,768 kHz, montado segundo 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.
É assim que o protótipo está agora:
Depois dos testes voltei ao código para personalizar mais.
Primeiro assisti a uma série de vídeos no YouTube sobre como as funções funcionam 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 disponível 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 essa 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 trocar só esse 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 trocando 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 isso e depois testar, mas antes vou assistir de novo ao vídeo de funções para entender melhor.
Argh... OK, muito tempo sem atualizações. Bom, o último código enviado funciona e tenho algumas funções, mas ainda não estou contente, 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 com mais detalhe.
Leia o seguinte:
Acabei conseguindo 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 isso, tive que configurar o arquivo platformio.ini e a árvore de diretórios seguindo uma série de passos, então vamos ver:
pio run -t uploadfsFuncionou.
Com isso comecei a entender o quanto eu não sabia o que estava fazendo. O código com que comecei tinha o HTML incluído no sketch do Arduino. Isso me incomodava. Agora, com esse novo entendimento, posso deixar o HTML separado do arquivo do sketch.
Depois achei este tutorial: ESP32 Web Server using SPIFFS
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!
/****************************************************************************
* 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 "root" 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 é &&.
Versão atual deste código (E-SPlash no GitHub)
<!DOCTYPE HTML><html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
html {font-family: Arial; display: inline-block; text-align: center;}
h2 {font-size: 3.0rem;}
p {font-size: 3.0rem;}
body {max-width: 600px; margin:0px auto; padding-bottom: 25px;}
.switch {position: relative; display: inline-block; width: 120px; height: 68px}
.switch input {display: none}
.slider {position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; border-radius: 34px}
.slider:before {position: absolute; content: ""; height: 52px; width: 52px; left: 8px; bottom: 8px; background-color: #fff; -webkit-transition: .4s; transition: .4s; border-radius: 68px}
input:checked+.slider {background-color: #2196F3}
input:checked+.slider:before {-webkit-transform: translateX(52px); -ms-transform: translateX(52px); transform: translateX(52px)}
</style>
</head>
<body>
<h2>Fab Farm Irrigation System</h2>
%BUTTONPLACEHOLDER%
<script>function toggleCheckbox(element) {
var xhr = new XMLHttpRequest();
if(element.checked){ xhr.open("GET", "/update?relay="+element.id+"&state=1", true); }
else { xhr.open("GET", "/update?relay="+element.id+"&state=0", true); }
xhr.send();
}</script>
</body>
</html>
Versão atual deste código (E-SPlash no GitHub)
No momento está assim:
A aparência e o código agora são praticamente iguais aos do site Random Nerd Tutorials sobre relés controlados por ESP32.
A principal diferença do 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 que está instalado na fazenda. O motivo é que o código está bagunçado e repetitivo demais; as abordagens que achei nas fontes de ontem me fizeram perceber que é mais rápido adicionar os recursos ao último código um por um do que reescrever tudo a partir do código que roda hoje na fazenda.
Hoje vou tentar adicionar informações atualizadas, como temperatura e umidade. De novo, estou me baseando no trabalho do Rui Santos nesta página sobre uma estação meteorológica. Com essa abordagem, acho que também vou conseguir incluir a hora do dia usando um relógio RTC.
O esquema de ligação do DHT22 é do site dele:
Ele usa um resistor de 4,7 kOhm como pull-up.
Depois de algum trabalho, a página ficou assim:
Acho que o motivo de a temperatura agora estar negativa é que o sensor de temperatura e umidade está com defeito ou a minha fiação está meio frouxa. Acho que quando eu fresar minha própria placa e soldar tudo, em vez de espetar numa breadboard, 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 de rede; agora preciso achar um jeito de pegar o valor da hora e mostrar na interface web. No momento estou mandando a palavra TESTE pelo código, e é isso que aparece no lugar da hora.
Hoje vou focar em mostrar a hora na interface web e talvez criar uma função para ligar o relé num horário definido.
Atualização das 20:44:
- Depois do dia inteiro tentando, consegui mostrar a hora da máquina e a hora do sistema de irrigação (ESP32).
Agora a interface está assim:
Quando eu crescer quero ser programador :-) e não passar um dia inteiro numa coisa que alguém monta em poucas linhas!
Meu sofrimento acabou graças a um santo, com esta resposta no Arduino Stack Exchange.
Aqui vai a função original postada lá:
void printLocalTime()
{
time_t rawtime;
struct tm timeinfo;
if(!getLocalTime(&timeinfo))
{
Serial.println("Failed to obtain time");
return;
}
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);
//Optional: Construct String object
String asString(timeStringBuff);
}
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...
Não, não trabalhei nisso, estava cansado, então hoje fiz alguns esboços.
Neste esboço mostro como vai ser o formato da caixa e como vai ser a interface web também.
Também estou lendo sobre String e sobre a sua manipulação.
Eu poderia seguir a abordagem de dividir as datas como String e converter para outros tipos de valores para as operações que pretendo fazer. Outra possibilidade é pegar os dados já no tipo que preciso desde o começo. Mesmo gostando mais da segunda opção, ainda não achei solução para ela, porque ainda não entendo bem a função getLocalTime(). Então vou ler mais sobre ela agora.
Espera... essa foi rápida! Achei 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 achar um jeito mais simples de pegar as horas, mas não consegui, porque estou apanhando para entender o tipo (struct). Bom, talvez eu volte nisso no futuro, mas por enquanto está bom.
Atualização de madrugada:
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 não testei.
Atualização da manhã:
Então agora vou olhar outro exemplo para configurar um timer, de novo do Rui Santos, está aqui; talvez eu consiga mudar um pouco, integrar no meu código e adicionar uma hora de início, e depois usar o slider para definir quanto tempo, em minutos, vai rodar. Isso já bastaria 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 salvar no SPIFFS que descrevi em 13/06/2020.
Links: