Como moramos numa área rural, resolvemos nos divertir tentando desenvolver uma solução (SW/HW) para espantar os gambás que tentarem subir para o telhado.
Nossa solução foi um pequeno sistema de monitoramento baseado nos recursos do micro-controlador ESP32-CAM com câmera e sensor de presença para detectar e tentar espantar gambás do telhado da nossa casa produzindo ruído através de um BUZZER e ligando o Flash/LED. Além disso, o sistema permite tirar fotos e armazená-las no cartão SD para posterior visualização ou transferência para um site FTP.
1 x Módulo ESP32-CAM com Camera OV2640 2MP
1 x Buzzer Ativo 3V
1 x Protoboard 400 pontos
1 x Fonte Ajustável Para Protoboard
1 x Cartão MicroSD de 4Gb
2 x Baterias 18650 de 4,2V 8800 mAh
1 x Carregador de bateria 18650 Universal
1 x Suporte para duas baterias 18650 com P4 Macho
1 x Sensor de Movimento Presença PIR HC-SR501
1 x Módulo Conversor USB para RS232 TTL – FTDI FT232RL
Jumpers – Macho/Macho – 65 Unidades
Jumpers – Macho/Femea – 20 Unidades de 20cm
Kit com 140 Jumpers Rígidos em U para Protoboard
Opcional: Caixa Organizadora 14 cm x 9 cm x 4,5 cm que vem com o Kit 525 Resistores – 17 valores
Opcional: Módulo Shield Duplo Bateria 18650 para ESP32 V8
Opcional: Antena externa 2.4 GHZ com cabo conector Ipex
Opcional: Módulo Adaptador Esp32 CAM
ESPAsyncWebServer
AsyncTCP
Wifi, FS, SD_MMC, Time, ArduinoJson, ESP32_FTPClient (Instalados através do Gerenciador de Bibliotecas)
Os objetivos específicos deste projeto são:
1) Implementar um servidor http para responder na porta 80 através da conexão WiFi respondendo às seguintes requisições:
| / | mostrar as funções disponíveis na página principal. |
| /list | mostrar o conteúdo da pasta de imagens JPG no MicroSD e permitir as funções de visualizar, baixar ou remover. |
| /ftp | mostrar o formulário de definição dos parâmetros do FTP. |
| /upload | fazer a transferência de todos os arquivos .jpg para uma pasta de um servidor FTP e persistir os parâmetros no MicroSD. |
| /config | mostrar o formulário de definição dos parâmetros do PIR. |
| /setpir | persistir os parâmetros no MicroSD. |
| /beep?delay=<nnnn> | tocar o BUZZER/Ligar FLASH por <nnnn> ms. |
| /shot | tirar uma foto e armazenar no MicroSD. |
| /status | informar o status do servidor no Navegador. |
| /wifi | mostrar as informações da Rede WiFi no Navegador. |
| /stream | entrar na tela de streaming de vídeo para o Navegador. |
2) Usar um sensor de presença do tipo PIR para detectar o movimento de algum ser vivo para, opcionalmente, acionar o buzzer e/ou tirar uma foto, armazenando no cartão MicroSD.
3) Atualizar o relógio interno do ESP32-CAM, sincronizando com o servidor NTP do Brasil. Utilizaremos o relógio interno para gerar o nome das fotos com o horário do momento, para gravar no cartão MicroSD (ex: 2023_11_10_20_55_45.jpg). Com isso, geraremos um nome único para diferenciação de cada foto.
//------------------------------------------------------------------------------------------------
// Função : Este programa tem como objetivo implementar um pequeno sistema de monitoramento
// baseado nos recursos do microcontrolador ESP32-CAM com câmera para detectar e
// tentar espantar gambás do telhado da nossa casa produzindo um sinal sonoro através
// do BUZZER, ligando o Flash/LED e, opcionalmente, tirar uma foto armazenando no cartão
// SD para posterior visualização ou transferência para um site FTP.
//
// Motivação : Como moramos numa área rural, resolvemos procurar uma diversão para desenvolver
// uma solução (SW/HW) para tentar espantar os gambás que tentarem subir para o telhado.
//
// Objetivos Específicos :
//
// 1) Implementar um servidor http para responder na porta 80 através da conexão WiFi respondendo
// às seguintes requisições:
//
// . / => mostrar as funções disponíveis na página principal.
// . /list => mostrar o conteúdo da pasta de images JPG no MicroSD e
// permitir as funcões de : visualizar, girar, baixar ou remover.
// . /ftp => mostrar a página de definição dos parâmetros do FTP.
// . /upload => fazer a transferência de todos os arquivos .jpg para
// uma pasta de um servidor FTP e persistir os parâmetros
// no MicroSD.
// . /config => mostrar a página de definição dos parâmetros do PIR.
// . /setpir => persistir os parâmetros no MicroSD.
// . /beep?delay=<nnnn> => tocar o BUZZER/Ligar por <nnnn> millis.
// . /shot => tirar uma foto e armazenar no MicroSD.
// . /status => informar o status do servidor.
// . /wifi => mostrar as informações da Rede WiFi no Navegador.
// . /stream => entrar na tela de streaming de vídeo para o navegador
// . /visualizar?fn=<nome.jpg> => entrar na tela de visualizar/girar uma foto .jpg do MicroSD
// . /photo?fn=<nome.jpg> => devolver a foto de nome fornecido como images/jpg
//
// 2) Usar um sensor de presença do tipo PIR para detectar o movimento de algum ser vivo
// para opcionalmente acionar o BUZZER e/ou tirar uma foto.
//
// 3) Atualizar o relógio interno do ESP32-CAM sincronizado com o servidor NTP do Brasil.
// Utilizaremos o relógio interno para gerar o nome das fotos com o horário do momento,
// para gravar no MicroSD (ex: 2023_11_10_20_55_45.jpg).
//
// Componentes : 1) 1 x Placa ESP32-CAM
// 2) 1 x Buzzer Ativo 3V
// 3) 1 x Protoboard de 400 pontos
// 4) 1 x Fonte de Protoboard 3,3/5 V
// 5) 1 x Cartão MicroSD de 4Gb
// 6) 2 x Baterias 18650 de 4,2V 8800 mAh
// 7) 1 x Carregador de bateria 18650 Universal
// 8) 1 x Suporte para duas baterias 18650 com P4 Macho
// 9) 1 x Sensor de Presença PIR
// 10) 1 x Módulo FTDI para UPLOAD do Código e testes do programa
// 11) Jumpers diversos
// 12) Opcional: Caixa Organizadora 14 cm x 9 cm x 4,5 cm
// 13) Opcional: Shield para bateria 18650
// 14) Opcional: Antena externa 2.4 GHZ com cabo conector Ipex
// 15) Opcional: Módulo Adaptador Esp32 CAM
//
// Observações: 1) O módulo FTDI é usado para alimentação do circuito e no UPLOAD do programa durante
// o desenvolvimento e testes. As duas baterias 18650 em série, de 4,2V 8800mAh cada,
// servem para alimentar o sistema no local definitivo e garantem uma automonia de
// cerca de 22 h de funcionamento contínuo no campo sem a necessidade de uma fonte
// alimentada em 127V, sabendo-se que o consumo total do ESP32-CAM ativo fica na
// ordem de 400 mA, ou seja, no limite da saída da fonte de protoboard.
// Dividindo-se 8800mAh/400mA =~ 22 h, que é a autonomia esperada.
// 2) O uso simultâneo da CÂMERA, MICROSD, SERIAL e FTDI praticamente esgota todos os
// GPIO's do microcontrolador ESP32-CAM. Restando os GPIO0 e GPIO16, sendo que o
// GPIO0 é usado para o modo de carga do programa (interligando o GPIO0 e GND em
// tempo de reset) e, no modo normal, sem o JUMPER, é usado pela Câmera, portanto,
// está ocupado também. O GPIO16 também é utilizado, embora eu não tenha achado nada
// explícito na documentação do ESP32-CAM, da Câmera e MicroSD. Se tentar usar,
// o microcontrolador ficará rebootando continuamente.
// 3) Neste projeto inicializaremos o MicroSD no modo 1Bit para que sejam liberados
// os GPIO's 4, 12 e 13 para uso no programa, fica mais lento na leitura e gravação
// pois usará apenas o GPIO2 para as duas operações, mas libera 3 portas importantes
// para as funcionalidades esperadas do projeto. O PIR usará o GPIO12. Como não conseguimos
// fazer funcionar o BUZZER no GPIO13 (na prática não foi liberado pelo modo 1Bit), fomos
// forçados a compartilhar o GPIO4 com o FLASH e o BUZZER.
// 4) O cartão micro SD precisa ser formatado a priori antes do uso no ESP32-CAM usando-se a
// FAT32. A pasta para conter as imagens também precisa ser criada antes do uso.
// 5) Recomendamos atenção no manuseio de baterias de lítio como a sugerida neste projeto (18650).
// Uma sobrecarga no carregamento pode gerar superaquecimento aumentando o risco de incendiar.
// No módulo opcional (Shield) mencionado, há uma proteção nativa para evitar sobrecargas.
// 6) Optamos por ativar a antena externa do ESP32-CAM para permitir afastar o circuito do roteador
// e ter o servidor Web ativo pela rede wifi (veja a Referência 12). A antena interna tem menor alcance.
// 7) Os HTML's usados pela aplicação são definidos com a palavra reserva PROGMEM significando que serão
// armazenados na memória FLASH para não comprometer a memória RAM. No ESP32-CAM a FLASH é de 4 Mb.
// 8) Funções específicas:
// . Listar o conteúdo da pasta definida para fotos no Navegador, ordenando por nome,
// e permitir visualizar, girar, baixar e remover.
// . Tirar uma foto e armazenar na pasta do MicroSD
// . Fazer streaming de vídeo para o Navegador
// . Mostrar o status do servidor no Navegador
// . Mostrar as informações da Rede Wifi
// . Enviar as fotos da pasta do MicroSD para um servidor FTP em bloco
// . Configurar os prâmetros da detecção via PIR: Alarme SIM/NÂO, Tirar Foto SIM/NÂO,
// usar Flash SIM/NÂO, Tempo do Alarme, intervalo entre detecções e a pasta para
// imagens no MicroSD
// . Ativar o Buzzer por um tempo definido
//
// Autores : Alberto Menezes
// Dailton Menezes
//
// Referências : 1) https://blog.eletrogate.com/introducao-ao-esp32-cam/
// 2) Exemplo do GitHub : https://github.com/s60sc/ESP32-CAM_MJPEG2SD
// 3) https://dr-mntn.net/2021/02/using-the-sd-card-in-1-bit-mode-on-the-esp32-cam-from-ai-thinker
// 4) https://randomnerdtutorials.com/esp32-ntp-timezones-daylight-saving/
// 5) https://randomnerdtutorials.com/esp32-async-web-server-espasyncwebserver-library/
// 6) https://randomnerdtutorials.com/esp32-cam-ai-thinker-pinout/
// 7) https://www.youtube.com/watch?v=k_PJLkfqDuI&t=298s
// 8) https://www.youtube.com/watch?v=Ul0h5Maeoeg
// 9) https://www.youtube.com/watch?v=5KszL2Opuo0&t=1514s
// 10) https://www.youtube.com/watch?v=visj0KE5VtY&t=22s
// 11) https://www.youtube.com/watch?v=k_PJLkfqDuI&t=302s
// 12) https://www.youtube.com/watch?v=aBTZuvg5sM8&t=3s
//
// Versão : 1.0 Nov/2023
//------------------------------------------------------------------------------------------------
#include <WiFi.h>
#include <FS.h>
#include <SD_MMC.h>
#include <time.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <ESP32_FTPClient.h>
#include <ArduinoJson.h>
#include <map>
#include <string>
//--------------------------------------
// Camera libraries
//--------------------------------------
#include "esp_camera.h"
#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "driver/rtc_io.h"
//-------------------------------------
// Define os Pinos usados pelo programa
//-------------------------------------
#define LED_RED GPIO_NUM_33 // Usado para ligar o LED se conectado no Wifi
#define pinFLASH GPIO_NUM_4 // Usado para acionar o Flash
#define pinBUZ GPIO_NUM_4 // Usado para acionar o Buzzer
#define pinPIR GPIO_NUM_12 // Usado para ler o PIR
//-------------------------------------
// Define Config no MicroSD
//-------------------------------------
#define JSON_CONFIG_FILE "/config.json" // Nome do arquivo JSON de configuração no SD
//----------------------------------------------------------
// Definição dos pinos para a câmera CAMERA_MODEL_AI_THINKER
//----------------------------------------------------------
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
//-----------------------------------------
// Definições gerais
//-----------------------------------------
#define timeoutWifi 15*1000 // Timeout para reconhecimento do wifi ativo
#define defaultPIR 5000 // Tempo defaut para alarmar pelo PIR
#define defaultIntervaloPIR 10000 // Intervalo para aceitação de detecções do PIR
#define defaultSDDir "/images/" // Pasta default no SD para armazenamento fotos
#define defaultftpServer "10.0.0.145"; // IP default do servidor FTP para envio das fotos
#define defaultftpPort 21 // Porta default do servidor FTP
#define defaultftpUser "ftp" // Usuário default para logon no servidor FTP
#define defaultftpPass "ftp@2023"; // Senha default para logon no servidor FTP
#define defaultftpDir "/" // Pasta default no destino mo servidor FTP
#define defaultRemoverFotosFtp false // Flag default se deve remover as fotos após FTP
#define defaultAtrasoSD 200 // Atraso default para leituras no SD com 1Bit
#define defaultUsarFlash false // Flag default se deve usar o flash nas fotos
#define ESP_getChipId() ((uint32_t)ESP.getEfuseMac() // Simular ID da placa ESP
//---------------------------------------
// Definições para Streaming
//---------------------------------------
#define INTERVALO_STREAMING 250 // Intervalo em milliseg entre envios de frames
int num_streaming = 0; // Para informar se está no modo streaming
String modo_streaming[2] = {"Iniciar", "Terminar"}; // Modo do Streaming
unsigned lastFrame = 0; // Momento do último frame enviado
framesize_t default_resolution = FRAMESIZE_QVGA; // Resolução default
// FRAMESIZE_UXGA 1600x1200 pixels
// FRAMESIZE_SXGA 1280x1024 pixels
// FRAMESIZE_XGA 1024x768 pixels
// FRAMESIZE_SVGA 800x600 pixels
// FRAMESIZE_VGA, 640x480 pixels
// FRAMESIZE_QVGA 320x240 pixels
// FRAMESIZE_CIF 352x288 pixels
//---------------------------------------
// Definições da rede WiFi
//---------------------------------------
char ssid[] = "<informe o ssid>"; // Nome da rede Wifi
char password[] = "<informe a senha>"; // Senha da rede Wifi
//--------------------------------------
// Defina as credenciais do servidor FTP
//--------------------------------------
String ftpServer = defaultftpServer; // Endereço do servidor FTP
String ftpUser = defaultftpUser; // Usuário para logon no servidor FTP
int ftpPort = defaultftpPort; // Porta do servidor FTP
String ftpPassword = defaultftpPass; // Senha para logon no servidor FTP
String ftpDir = defaultftpDir; // Diretório destino no servidor FTP
//-----------------------------------
// Definições da Detecção de Presença
//-----------------------------------
bool alarmarPIR = false; // Se deve alamar na detecção do PIR
bool fotoPIR = false; // Se deve tirar foto na detecção do PIR
bool estadoPIR = false; // Para leitura do estado do PIR
int tempoPIR = defaultPIR; // Tempo do alarme do PIR
int intervaloPIR = defaultIntervaloPIR; // Intervalo entre detecções do PIR
//-------------------------------
// Definições para o Servidor NTP
//-------------------------------
const char* NTP_SERVER = "a.st1.ntp.br"; // Dados do Servidor NTP do Brasil
//const char* TZ_INFO = "BRST+3BRDT+2,M10.3.0,M2.3.0";// Informações do Timezone do Brasil
const char* TZ_INFO = "<-03>3"; // Fuso Horário do Brasil em relação ao GNT
//-------------------------------
// Definições Gerais do Programa
//-------------------------------
AsyncWebServer server(80); // Servidor http na porta 80
AsyncWebSocket ws("/ws"); // Socket para fazer streaming
unsigned long disparo = 0; // Momento do disparo do Buzzer
bool estado_buz = LOW; // Estado do Buzzer ON/OFF
bool ativar_pir = false; // Se deve considerar as detecções do PIR
int delayValue=0; // Valor do tempo do Buzzer
String htmlResposta = ""; // Para resposta nos requests html
String dirFotos = defaultSDDir; // Diretório para fotos no SD
unsigned long ultimaDesconexao = 0; // Última desconeção do Wifi
unsigned long ultimaDeteccao = 0; // Última detecção do PIR
String unidades[] = {" Bytes", " KB", " MB", " GB"}; // Unidades de capacidade do SD a ser usada
String modo_ligado[] = {"OFF", "ON"}; // Estado ligado ON/OFF
bool estado_cam = false; // Se a câmera foi inicilizada
bool estado_sd = false; // Se o SD foi montado
uint64_t capacity; // Capacidade do SD
uint64_t used; // Bytes usados no SD
time_t startup; // Horário da inicialização
bool usarFlash = defaultUsarFlash; // Se deve usar o Flash nas fotos
char esp_id[50]; // Id do ESP32-CAM
String nomeFoto=""; // Nome da foto a ser visualizada
//--------------------------------
// Definições para controle do ftp
//--------------------------------
char buf[10*1024]; // Buffer para envio da foto no FTP
bool estado_ftp = false; // Se o FTP deve ser acionado
bool removerfotos_ftp = false; // Se deve remover as fotos após o FTP
String resultado_ftp = ""; // Mensagem de sucesso ou erro no FTP
//------------------------------------
// Define o HTML para Página Principal
//------------------------------------
const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Esp32-CAM Server</title>
<style>
body { font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f4f4f4;}
header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;}
main { display: flex; flex-direction: column; align-items: center; padding: 20px;}
button {width: 280px; margin: 5px; padding: 10px; font-size: 16px; background-color: #4CAF50; color: #fff; border: none; border-radius: 5px; cursor: pointer;}
p {font-size: 20px; color: #444444; margin-bottom: 10px;}
form, input, ol {font-size: 16px; color: #444444;}
table, th, td {border: none;}
th, td {color: #444444;}
table {margin-left: auto; margin-right: auto;}
th {background-color: #4CAF50; color: white;}
</style>
</head>
<body>
<div id="webpage">
<header>
<h2>Câmera Serial %espid%</h2>
</header>
<main>
<button onclick="acaoBotao('/list')">Listar as Fotos</button>
<button onclick="acaoBotao('/shot')">Tirar uma foto</button>
<button onclick="acaoBotao('/stream')">Fazer streaming</button>
<button onclick="acaoBotao('/status')">Mostrar o Status do Servidor</button>
<button onclick="acaoBotao('/wifi')">Mostrar Informações do Wi-Fi</button>
<button onclick="acaoBotao('/ftp')">Enviar Fotos para Servidor FTP</button>
<button onclick="acaoBotao('/config')">Config Detecção de Presença</button>
</main>
<script>
function acaoBotao(acao)
{
window.location.href = acao;
}
</script>
<center>
<form action='/beep' method='get'>
Delay (ms): <input type='text' name='delay' value='%delayvalue%'>
<br><br>
<input type='submit' value='Ativar Buzzer'>
</form>
%BUTTONPLACEHOLDER%
</center>
</div>
</body>
</html>
)rawliteral";
//---------------------------------------
// Define o HTML para a Transferência FTP
//---------------------------------------
const char ftp_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Configuração FTP</title>
<style>
body { font-family: Arial, sans-serif; font-size: 10px; margin: 0; padding: 0; background-color: #f4f4f4;}
header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;}
p {font-size: 20px; color: #444444; margin-bottom: 10px;}
form, input, ol {font-size: 16px; color: #444444;}
/* Estilo para o botão "Enviar FTP" */
.btn-ftp {
background-color: #4CAF50; /* Cor de fundo verde */
color: white; /* Cor do texto branco */
padding: 10px 20px; /* Preenchimento interno */
font-size: 16px;
width: 280px;
border: none;
border-radius: 5px;
cursor: pointer; /* Cursor de ponteiro ao passar o mouse */
}
/* Estilo para o botão "Home" */
.btn-home {
background-color: #0074E4; /* Cor de fundo azul */
color: white; /* Cor do texto branco */
padding: 10px 20px; /* Preenchimento interno */
font-size: 16px;
width: 280px;
border: none;
border-radius: 5px;
cursor: pointer; /* Cursor de ponteiro ao passar o mouse */
}
</style>
</head>
<body>
<div id="ftppage">
<header>
<h2>Transferência FTP</h2>
</header>
<br>
<center>
<form action='/upload' method='post'>
<label for='ipftp'>Endereço Servidor FTP:</label>
<input type='text' id='ipftp' name='ipftp' value='%ftpserver%' required><br><br>
<label for='portftp'>Porta Servidor FTP:</label>
<input type='text' id='portftp' name='portftp' value='%ftpport%' required><br><br>
<label for='user'>Nome de Usuário:</label>
<input type='text' id='user' name='user' value='%ftpuser%' required><br><br>
<label for='pass'>Senha:</label>
<input type='password' type='password' id='pass' name='pass' value='%ftppassword%' required><br><br>
<label for='dir'>Diretório de Destino:</label>
<input type='text' id='dir' name='dir' value='%ftpdir%' required><br><br>
<label for='removerFotos'>Remover Fotos após FTP:</label>
<select id='removerFotos' name='removerFotos'>
<option value='yes' %selyesremoverfotos%>Sim</option>
<option value='no' %selnoremoverfotos% >Não</option>
</select><br><br>
<input type='submit' class='btn-ftp' value='Enviar FTP'>
</form>
<br><br>
<form action='/'>
<input type='submit' class='btn-home' value='Voltar para a Página Inicial'>
</form>
%BUTTONPLACEHOLDER%
</center>
</div>
</body>
</html>
)rawliteral";
//----------------------------------------
// Define o HTML para configurar o PIR
//----------------------------------------
const char pir_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Configuração PIR</title>
<style>
body { font-family: Arial, sans-serif; font-size: 10px; margin: 0; padding: 0; background-color: #f4f4f4;}
header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;}
p {font-size: 20px; color: #444444; margin-bottom: 10px;}
form, input, ol {font-size: 16px; color: #444444;}
/* Estilo para o botão "Enviar FTP" */
.btn-pir {
background-color: #4CAF50; /* Cor de fundo verde */
color: white; /* Cor do texto branco */
padding: 10px 20px; /* Preenchimento interno */
font-size: 16px;
width: 280px;
border: none;
border-radius: 5px;
cursor: pointer; /* Cursor de ponteiro ao passar o mouse */
}
/* Estilo para o botão "Home" */
.btn-home {
background-color: #0074E4; /* Cor de fundo azul */
color: white; /* Cor do texto branco */
padding: 10px 20px; /* Preenchimento interno */
font-size: 16px;
width: 280px;
border: none;
border-radius: 5px;
cursor: pointer; /* Cursor de ponteiro ao passar o mouse */
}
</style>
</head>
<body>
<div id="pirpage">
<header>
<h2>Configuração do PIR</h2>
</header>
<br>
<center>
<form action='/setpir' method='post'>
<label for='alarm'>Alarme:</label>
<select id='alarm' name='alarm'>
<option value='yes' %selyesalarm%>Sim</option>
<option value='no' %selnoalarm% >Não</option>
</select><br><br>
<label for='photo'>Tirar Foto:</label>
<select id='photo' name='photo'>
<option value='yes' %selyesfoto%>Sim</option>
<option value='no' %selnofoto% >Não</option>
</select><br><br>
<label for='flash'>Usar Flash na Foto:</label>
<select id='flash' name='flash'>
<option value='yes' %selyesflash%>Sim</option>
<option value='no' %selnoflash% >Não</option>
</select><br><br>
<label for='time'>Tempo do Alarme (ms):</label>
<input type='number' id='time' name='time' value='%tempopir%'><br><br>
<label for='intervalo'>Intervalo entre Detecções (ms):</label>
<input type='number' id='intervalo' name='intervalo' value='%intervalopir%'><br><br>
<label for='time'>Pasta no SD:</label>
<input type='text' id='dirsd' name='dirsd' value='%dirfotos%'><br><br>
<input type='submit' class='btn-pir' value='Confirmar Alterações'>
</form>
<br><br>
<form action='/'>
<input type='submit' class='btn-home' value='Voltar para a Página Inicial'>
</form>
%BUTTONPLACEHOLDER%
</center>
</div>
</body>
</html>
)rawliteral";
//----------------------------------------
// Define o HTML para fazer streaming
//----------------------------------------
const char stream_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>ESP32-CAM Video Streaming</title>
<style>
body { font-family: Arial, sans-serif; font-size: 10px; margin: 0; padding: 0; background-color: #f4f4f4; overflow: hidden; }
header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;}
p {font-size: 20px; color: #444444; margin-bottom: 10px;}
form, input, ol {font-size: 20px; color: #444444;}
button {width: 280px; margin: 5px; padding: 10px; font-size: 16px; border: none; border-radius: 5px; cursor: pointer;}
/* Estilo para o botão "Iniciar Stream" */
.btn-stream {
background-color: #4CAF50; /* Cor de fundo verde */
color: white; /* Cor do texto branco */
padding: 10px 20px; /* Preenchimento interno */
border: none; /* Sem borda */
cursor: pointer; /* Cursor de ponteiro ao passar o mouse */
}
/* Estilo para o botão "Home" */
.btn-home {
background-color: #0074E4; /* Cor de fundo azul */
color: white; /* Cor do texto branco */
padding: 10px 20px; /* Preenchimento interno */
border: none; /* Sem borda */
cursor: pointer; /* Cursor de ponteiro ao passar o mouse */
}
#stream-container {
/* display: flex; */
justify-content: center;
align-items: center;
border: 2px solid #ccc; /* Cor e largura da borda */
padding: 10px; /* Adiciona algum espaço interno à borda */
display: inline-block; /* Faz com que a borda se ajuste ao tamanho do conteúdo */
overflow: hidden; /* Impede a barra de rolagem */
}
#stream-container img {
max-width: 100%;
max-height: 100%;
width: auto;
height: auto;
}
</style>
<script>
var socket;
function toggleStreaming()
{
var toggleButton = document.getElementById("toggleButton");
if (toggleButton.innerText === "Ativar")
{
initWebSocket();
toggleButton.innerText = "Desativar";
}
else
{
toggleButton.innerText = "Ativar";
stopStreaming();
}
}
function initWebSocket()
{
if (!socket || socket.readyState === WebSocket.CLOSED)
{
socket = new WebSocket('ws://' + window.location.hostname + '/ws');
socket.binaryType = 'arraybuffer';
socket.onmessage = function(event)
{
var container = document.getElementById('stream-container');
var img = container.querySelector('img');
var tamanhoMinimo = Math.min(window.innerWidth, window.innerHeight);
container.style.width = tamanhoMinimo + 'px';
container.style.height = tamanhoMinimo + 'px';
if (img)
{
img.style.maxWidth = tamanhoMinimo + 'px';
img.style.maxHeight = tamanhoMinimo + 'px';
var arrayBuffer = event.data;
var blob = new Blob([new Uint8Array(arrayBuffer)], { type: 'image/jpeg' });
img.src = URL.createObjectURL(blob);
}
};
}
}
function stopStreaming()
{
if (socket && socket.readyState !== WebSocket.CLOSED)
{
socket.close();
}
}
function returnToHome()
{
window.location = '/';
}
// Evento antes da página ser descarregada
window.addEventListener('beforeunload', function()
{
stopStreaming();
});
</script>
</head>
<body>
<header>
<h2>ESP32-CAM Video Streaming</h2>
</header>
<center>
<!-- Botão para iniciar ou parar o streaming -->
<button id='toggleButton' onclick='toggleStreaming()' class='btn-stream'>Ativar</button>
<button onclick='returnToHome()' class='btn-home'>Retornar</button>
<br>
<div id='stream-container'>
<!-- Elemento para exibir o stream de vídeo -->
<img id='stream' alt='stream'>
</div>
</center>
</body>
</html>
)rawliteral";
//----------------------------------------
// Define o HTML para listagem das fotos
//----------------------------------------
const char fotos_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Lista de Fotos</title>
<style>
body { font-family: Arial, sans-serif; font-size: 10px; margin: 0; padding: 0; background-color: #f4f4f4;}
header { background-color: #333; color: #fff; text-align: center; padding: 1px; font-size: 16px;}
table { margin: 5px auto; border-collapse: collapse; box-shadow: 0 0 5px rgba(0, 0, 0, 0.1); background-color: #fff;}
th, td { padding: 2px; text-align: center; border-bottom: 1px solid #ddd;}
th { background-color: #333; color: #fff;}
button { border: none; border-radius: 5px; cursor: pointer;}
.view-btn { width: 50px; padding: 2px 2px; margin: 0px; font-size: 10px; background-color: #4CAF50; color: #fff;}
.download-btn { width: 50px; padding: 2px 2px; margin: 0px; font-size: 10px; background-color: #008CBA; color: #fff;}
.remove-btn { width: 50px; padding: 2px 2px; margin: 0px; font-size: 10px; background-color: #FF5252; color: #fff;}
.btn-home { width: 250px; background-color: #0074E4; color: white; padding: 10px 20px;}
</style>
</head>
<body>
%fotos%
<br>
<br>
<center>
<button onclick="acaoBotao('/')" class="btn-home">Voltar para a Página Inicial</button>
</center>
<script>
function acaoBotao(acao)
{
window.location.href = acao;
}
</script>
</body>
</html>
)rawliteral";
//----------------------------------------
// Define o HTML para visualizar uma foto
//----------------------------------------
const char show_html[] PROGMEM = R"rawliteral(
<!DOCTYPE html>
<html lang="pt-br">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Visualização de Foto</title>
<style>
header {
background-color: #333;
color: #fff;
text-align: center;
padding: 1px;
font-size: 16px;
}
body {
margin: 0;
padding: 0;
height: 100vh;
background-color: #f4f4f4;
}
#foto-container {
margin: 0;
}
img {
max-width: 720px;
max-height: calc(100vh - 90px);
object-fit: contain;
}
button {
width: 94px;
margin: 5px;
padding: 10px;
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
}
.btn-girar {
background-color: #4CAF50;
color: white;
}
.btn-home {
background-color: #0074E4; /* Cor de fundo azul */
color: white; /* Cor do texto branco */
}
</style>
</head>
<body>
<div id='visualizar'>
<header>
<h3>Visualização de %nomefoto%</h3>
</header>
<center>
<button onclick='rotateImage(90)' class='btn-girar'>Girar +90°</button>
<button onclick='rotateImage(-90)' class='btn-girar'>Girar -90°</button>
<button onclick='returnBack()' class='btn-home'>Retornar</button>
<br>
<div id='foto-container'>
<img id='rotatableImage' src='/photo?fn=%nomefoto%'/>
</div>
<script>
function returnBack()
{
window.history.back();
}
function adjustMargins() {
var fotoContainer = document.getElementById('foto-container');
var currentMargin = parseInt(fotoContainer.style.margin) || 0;
var newMargin = (currentMargin === 0) ? 100 : 0; // Ajuste para a altura dos botões
fotoContainer.style.margin = newMargin + 'px';
}
function rotateImage(angulo) {
var img = document.getElementById('rotatableImage');
var currentRotation = parseInt(img.getAttribute('data-rotation') || 0, 10);
var newRotation = (currentRotation + angulo) % 360;
img.style.transform = 'rotate(' + newRotation + 'deg)';
img.setAttribute('data-rotation', newRotation);
adjustMargins();
}
</script>
</center>
</div>
</body>
</html>
)rawliteral";
//--------------------------------
// Prototipação das funções usadas
//--------------------------------
String listFiles(fs::FS &fs); // Lista as fotos existentes na pasta no cartão
void WiFiEvent(WiFiEvent_t event); // Trata os eventos do Wifi
bool getNTPtime(int sec); // Sincroniza o relógio interno com o servidor NTP
bool configESPCamera(); // Inicializa a câmera do ESP32-CAM
bool initMicroSDCard(); // Inicializa o cartão MicroSD
String takeNewPhoto(String path); // Tira uma foto instantânea
String timeToString(time_t tempo); // Formata uma variável time para string
String getTimeStamp(); // Obtém a data no formato dd/mm/yyyy hh:mm:ss
String getTimeStampFname(); // Obtém a data/hora para uso em nome de arquivo
String textToHtml(String texto); // Converte uma mensagem com tags html
String processor(const String& var); // Faz a expansão de variáveis no HTML
// Envia todos as fotos para o servidoro FTP
bool EnviaFotosFtp(char *server, int porta, char *user, char *pass, char *dest, fs::FS &fs, char *source, String *msg);
bool loadConfigFile(fs::FS &fs); // Carrega as configurações salvas no MicroSD
void saveConfigFile(fs::FS &fs); // Salva as configurações no MicroSD
void displayRequest(AsyncWebServerRequest *request); // Mostra informações da requisição na Console
void capturarFrame(); // Captura e Envia o Frame via WebSocket
String melhorUnidade(int size); // Calcula a melhor representação para tamanho de arquivo
String getFrameSizeName(framesize_t size); // Devolve o nome da resolução da câmera
//------------------------------------
// Setup do ESP32-CAM
//------------------------------------
void setup()
{
// Desabilita o brownout detector
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0);
// Inicializa a Serial
Serial.begin(115200);
while (!Serial) ;
// Define o RED LED do ESP32-CAM. Será ligado pela rotina de
// eventos quando conectado no WiFi ou desligado quando fora
pinMode(LED_RED,OUTPUT);
// Conecta-se à rede WiFi
Serial.print("Conectando ao Wi-Fi");
// Define o handle para tratar os eventos do Wifi
WiFi.onEvent(WiFiEvent);
// Tenta a conexão WiFi
WiFi.begin(ssid, password);
int cont=0;
while (WiFi.status() != WL_CONNECTED)
{
if (++cont % 80 == 0) Serial.println();
else Serial.print(".");
delay(1000);
}
// Mostra as informações da rede Wifi
Serial.print("\nConectado em ");
Serial.print(WiFi.SSID());
Serial.print(" no IP ");
Serial.print(WiFi.localIP());
Serial.print(" com MAC ");
Serial.print(WiFi.macAddress());
Serial.print(" e sinal de ");
Serial.print(WiFi.RSSI());
Serial.println(" db");
// Sincroniza o horário interno com o Servidor NTP nacional
Serial.print("Tentando sincronismo com o servidor NTP ");
Serial.print(NTP_SERVER);
Serial.print(" com TimeZone ");
Serial.println(TZ_INFO);
configTime(0, 0, NTP_SERVER);
setenv("TZ", TZ_INFO, 1);
tzset();
if (getNTPtime(10))
{ // espera 10sec para sincronizar
Serial.println("Relógio interno foi sincronizado com o servidor NTP");
}
else
{
Serial.println("\nErro ao atualizar o Relógio interno");
}
// Inicializa o cartão MicroSD
Serial.print("Inicializando o cartão MicroSD... ");
estado_sd = initMicroSDCard();
if (estado_sd)
{
Serial.println("Ok!");
// Tenta carregar as definições do FTP
capacity = SD_MMC.totalBytes();
used = SD_MMC.usedBytes();
if (loadConfigFile(SD_MMC))
Serial.println("Configurações recuperadas com sucesso...");
else Serial.println("Configurações default serão usadas...");
}
else Serial.println("Falhou...");
// Inicializa a Câmera
Serial.print("Inicializando a Câmera...");
estado_cam = configESPCamera();
if (estado_cam) Serial.println(" Camera OK!");
else Serial.println(" Falhou...");
// Define uma página inicial com links para listagem, transferência e acionamento do buzzer.
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição principal
displayRequest(request);
htmlResposta = "";
request->send_P(200, "text/html", index_html, processor);
});
// Define uma rota para listar os arquivos
server.on("/list", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição de listar as fotos
displayRequest(request);
if (estado_sd)
{
//htmlResposta = listFiles(SD_MMC);
request->send_P(200, "text/html", fotos_html, processor);
}
else
{
htmlResposta = textToHtml("<b>Erro</b> = MicroSD não montado");
request->send_P(200, "text/html", index_html, processor);
}
});
// Define uma rota para Tirar uma foto
server.on("/shot", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição de tirar uma foto
displayRequest(request);
if (!estado_cam)
{
htmlResposta = textToHtml("<b>Erro</b> = Câmera não inicializada");
request->send_P(200, "text/html", index_html, processor);
return;
}
if (!estado_sd)
{
htmlResposta = textToHtml("<b>Erro</b> = MicroSD não montado");
request->send_P(200, "text/html", index_html, processor);
return;
}
String fn = getTimeStampFname() + ".jpg";
String path = dirFotos + fn;
Serial.printf("Arquivo da Foto: %s\n", path.c_str());
// Decide se Liga o Flash
if (usarFlash)
{
digitalWrite(pinFLASH,HIGH);
delay(200);
}
// Take and Save Photo
String msg = takeNewPhoto(path);
// decide se Desliga o Flash
if (usarFlash)
{
digitalWrite(pinFLASH,LOW);
}
String resposta;
if (msg.length()==0)
resposta = "<b>Arquivo</b> = " + fn + " foi gerado no MicroSD";
else resposta = "<b>Erro</b>: " + msg;
htmlResposta = textToHtml(resposta);
request->send_P(200, "text/html", index_html, processor);
});
// Define uma rota para status do servidor
server.on("/status", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição para mostrar a data/hora do servidor
displayRequest(request);
String resposta = "<b>Inicialização</b> = " + timeToString(startup) + "\t" +
"<b>Data/Hora</b> = " + getTimeStamp() + "\t" +
"<b>Câmera</b> = " + modo_ligado[estado_cam] + "\t" +
"<b>MicroSD</b> = " + modo_ligado[estado_sd] + "\t" +
"<b>Foto</b> = " + modo_ligado[fotoPIR] + "\t" +
"<b>Alarmar</b> = " + modo_ligado[alarmarPIR] + "\t" +
"<b>Remover Fotos</b> = " + modo_ligado[removerfotos_ftp] + "\t" +
"<b>Flash Fotos</b> = " + modo_ligado[usarFlash] + "\t" +
"<b>CAM</b> = " + getFrameSizeName(default_resolution) + "\t" ;
htmlResposta = textToHtml(resposta);
request->send_P(200, "text/html", index_html, processor);
});
// Define uma rota para informações do wifi
server.on("/wifi", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição para mostrar as informações da conexão WiFi
displayRequest(request);
IPAddress ip = WiFi.localIP();
String resposta = "<b>SSID</b>=" + WiFi.SSID() + "\t" +
"<b>IP</b>=" + ip.toString() + "\t" +
"<b>MAC</b>=" + WiFi.macAddress()+ "\t" +
"<b>DB</b>=" + WiFi.RSSI();
htmlResposta = textToHtml(resposta);
request->send_P(200, "text/html", index_html, processor);
});
// Define uma rota para acionar o buzzer
server.on("/beep", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição de dar um beep no Buzzer
displayRequest(request);
if (request->hasParam("delay"))
{
delayValue = request->getParam("delay")->value().toInt();
if (delayValue > 0)
{
Serial.print("Delay=");
Serial.println(delayValue);
disparo = millis();
estado_buz = HIGH;
digitalWrite(pinBUZ,estado_buz);
htmlResposta = textToHtml("Buzzer ativado com sucesso.");
request->send_P(200, "text/html", index_html,processor);
}
else
{
htmlResposta = textToHtml("<b>Erro</b>: Delay Inválido.");
request->send_P(200, "text/html", index_html, processor);
}
}
else
{
htmlResposta = textToHtml("<b>Erro</b>: Delay Inválido.");
request->send_P(200, "text/html", index_html, processor);
}
});
// Definir rota para devolver o arquivo JPG
server.on("/photo", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição para enviar a foto para o Navegador
displayRequest(request);
if (request->hasParam("fn"))
{
String fn= dirFotos;
fn += request->getParam("fn")->value();
request->send(SD_MMC, fn, "image/jpeg");
}
else
{
htmlResposta = textToHtml("<b>Erro</b>: Arquivo não encontrado");
request->send_P(200, "text/html", index_html, processor);
}
});
// Definir rota para visualização do arquivo JPG
server.on("/visualizar", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição para visualização da foto no Navegador
displayRequest(request);
if (request->hasParam("fn"))
{
nomeFoto = request->getParam("fn")->value();
request->send_P(200, "text/html", show_html, processor);
}
else
{
htmlResposta = textToHtml("<b>Erro</b>: Arquivo não encontrado");
request->send_P(200, "text/html", index_html, processor);
}
});
// Definir rota para baixar o arquivo JPG
server.on("/baixar", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição para baixar a foto no Navegador
displayRequest(request);
if (request->hasParam("fn"))
{
String fn= dirFotos;
fn += request->getParam("fn")->value();
request->send(SD_MMC, fn, "image/jpeg",true);
}
else
{
htmlResposta = textToHtml("<b>Erro</b>: Arquivo não encontrado");
request->send_P(200, "text/html", index_html, processor);
}
});
// Definir rota para remover o arquivo JPG
server.on("/remover", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição para remover a foto do cartão SD
displayRequest(request);
if (request->hasParam("fn"))
{
String fn= dirFotos;
fn += request->getParam("fn")->value();
Serial.print("Vou remover: ");
Serial.println(fn);
if (SD_MMC.remove(fn))
{
Serial.println("Removido com sucesso.. vou fazer redirect...");
used = SD_MMC.usedBytes();
request->redirect("/list");
}
else
{
Serial.println("Erro ao tentar remover.. vou para página principal...");
htmlResposta = textToHtml("<b>Erro</b>: Não foi possivel remover "+request->getParam("fn")->value());
request->send_P(200, "text/html", index_html, processor);
}
}
else
{
htmlResposta = textToHtml("<b>Erro</b>: Arquivo não encontrado");
request->send_P(200, "text/html", index_html, processor);
}
});
// Define uma página para transferência FTP
server.on("/ftp", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição para mostrar o diálogo de definição do FTP
displayRequest(request);
if (!estado_sd)
{
htmlResposta = textToHtml("<b>Erro</b> = MicroSD não montado");
request->send_P(200, "text/html", index_html, processor);
return;
}
if (resultado_ftp.length()!=0)
{
htmlResposta = textToHtml(resultado_ftp);
resultado_ftp = "";
}
else htmlResposta = "";
request->send_P(200, "text/html", ftp_html, processor);
});
// Define uma página para o upload das fotos para o site FTP
server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
{
// Atende a requisição para fazer a transferência FTP
displayRequest(request);
if (estado_ftp)
{
htmlResposta = textToHtml("<b>Erro</b>: FTP em andamento");
request->send_P(200, "text/html", ftp_html, processor);
}
else if (resultado_ftp.length()!=0)
{
htmlResposta = textToHtml(resultado_ftp);
resultado_ftp = "";
request->send_P(200, "text/html", ftp_html, processor);
}
else if(request->hasParam("ipftp", true) && request->hasParam("portftp", true) &&
request->hasParam("user", true) && request->hasParam("pass", true) &&
request->hasParam("dir", true))
{
// Extrai os parâmetros do html
ftpServer = request->getParam("ipftp", true)->value();
String strPort = request->getParam("portftp", true)->value();
ftpUser = request->getParam("user", true)->value();
ftpPassword = request->getParam("pass", true)->value();
ftpDir = request->getParam("dir", true)->value();
String strRemoverFotos = request->getParam("removerFotos", true)->value();
removerfotos_ftp = strRemoverFotos == "yes" ? true : false;
// Converte para o char e int
try
{
ftpPort = strPort.toInt();
}
catch (const std::invalid_argument& e) { ftpPort = 21; }
catch (const std::out_of_range& e) { ftpPort = 21; }
// Escalona o FTP para o Loop Principal
estado_ftp = true;
htmlResposta = textToHtml("FTP foi escalonado para execução em paralelo");
request->send_P(200, "text/html", ftp_html, processor);
}
else
{
htmlResposta = textToHtml("<b>Erro</b>: Parâmetros de FTP inválidos");
request->send_P(200, "text/html", ftp_html, processor);
}
});
// Define uma página para configuração do PIR
server.on("/config", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição para mostrar o diálogo de definição da Configuração do PIR
displayRequest(request);
if (!estado_sd)
{
htmlResposta = textToHtml("<b>Erro</b> = MicroSD não montado");
request->send_P(200, "text/html", index_html, processor);
return;
}
htmlResposta = "";
request->send_P(200, "text/html", pir_html, processor);
});
// Define uma página para a trativa da configuração do PIR
server.on("/setpir", HTTP_POST, [](AsyncWebServerRequest *request)
{
// Atende a requisição para persistir as definições do PIR
displayRequest(request);
// Extrai os parâmetros do html
if(request->hasParam("alarm", true) && request->hasParam("photo", true) &&
request->hasParam("time", true) && request->hasParam("dirsd", true))
{
String strAlarm = request->getParam("alarm", true)->value();
String strFoto = request->getParam("photo", true)->value();
String strFlash = request->getParam("flash", true)->value();
String strTempo = request->getParam("time", true)->value();
String strIntPir= request->getParam("intervalo", true)->value();
dirFotos = request->getParam("dirsd", true)->value();
if (!dirFotos.endsWith("/")) dirFotos += "/";
alarmarPIR = strAlarm == "yes" ? true : false;
fotoPIR = strFoto == "yes" ? true : false;
usarFlash = strFlash == "yes" ? true : false;
estadoPIR = false;
// Converte o tempo para int
try
{
tempoPIR = strTempo.toInt();
}
catch (const std::invalid_argument& e) { tempoPIR = defaultPIR; }
catch (const std::out_of_range& e) { tempoPIR = defaultPIR; }
// Converte o time para int
try
{
intervaloPIR = strIntPir.toInt();
}
catch (const std::invalid_argument& e) { intervaloPIR = defaultIntervaloPIR; }
catch (const std::out_of_range& e) { intervaloPIR = defaultIntervaloPIR; }
saveConfigFile(SD_MMC);
htmlResposta = textToHtml("<b>Configuração do PIR concluída</b>");
request->send_P(200, "text/html", pir_html, processor);
}
else
{
htmlResposta = textToHtml("<b>Erro</b>: Parâmetros do PIR inválidos");
request->send_P(200, "text/html", pir_html, processor);
}
});
// Define a rota para fazer streaming
server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
{
// Atende a requisição para mostrar o diálogo de streaming
displayRequest(request);
if (!estado_cam)
{
htmlResposta = textToHtml("<b>Erro</b> = Câmera não inicializada");
request->send_P(200, "text/html", index_html, processor);
}
else request->send_P(200, "text/html", stream_html, processor);
});
// Inicializa o WebSocket Event para conectar/desconectar clientes de streaming
ws.onEvent([](AsyncWebSocket *server, AsyncWebSocketClient *client, AwsEventType type, void *arg, uint8_t *data, size_t len)
{
// Registra na Console a conexão e desconexão de clientes socket's
if (type == WS_EVT_CONNECT)
{
Serial.print("Cliente Socket conectado no IP ");
Serial.println(client->remoteIP());
num_streaming++; // <== Incrementa o n. de sessões de streaming
}
else if (type == WS_EVT_DISCONNECT)
{
if (--num_streaming<0) num_streaming=0; // <== Decrementa o n. de sessões de streaming
Serial.print("Cliente Socket desconectado do IP ");
Serial.println(client->remoteIP());
}
});
// Adiciona o manipulador de WebSocket ao servidor
server.addHandler(&ws);
// Inicia o servidor web
server.begin();
/////////////////////////////////////////////////////////////
// Importante: Os GPIO's 4, 12 e 13 devem ser definidos após
// a inicialização do MicroSD pois são usados neste módulo.
// Utilizaremos o modo 1Bit da inicialização para forçar a
// liberação dos GPIO's, por isso, devemos redefiní-los após
// a inicialização do MicroSD. Com isso, deixamos para o
// final do setup().
////////////////////////////////////////////////////////////
// Define o Pino do FLASH como output (compartilhado com o BUZZER)
pinMode(pinFLASH, OUTPUT);
// Define Pin do PIR
pinMode(pinPIR, INPUT);
// Pega a hora do startup
time(&startup);
localtime(&startup);
// Obtém o id do ESP32-CAM
sprintf(esp_id, "%X",(uint32_t)ESP.getEfuseMac());
// Mostra o status de algumas variáveis no startup
Serial.print("Esp32 Serial = "); Serial.println(esp_id);
Serial.print("Inicialização = "); Serial.println(timeToString(startup));
Serial.print("Data/Hora = "); Serial.println(getTimeStamp());
Serial.print("Câmera = "); Serial.println(modo_ligado[estado_cam]);
Serial.print("MicroSD = "); Serial.println(modo_ligado[estado_sd]);
Serial.print("Foto = "); Serial.println(modo_ligado[fotoPIR]);
Serial.print("Alarmar = "); Serial.println(modo_ligado[alarmarPIR]);
Serial.print("Remover Fotos = "); Serial.println(modo_ligado[removerfotos_ftp]);
Serial.print("Flash Fotos = "); Serial.println(modo_ligado[usarFlash]);
// Aguardando request http na porta 80
Serial.print("\nAguardando requisições http na porta 80... acesse http://");
Serial.println(WiFi.localIP());
}
//------------------------------------
// Loop Principal
//------------------------------------
void loop()
{
// Verifica se está no modo streaming para capturar um frame e enviar via socket
if (num_streaming > 0 && millis()-lastFrame>INTERVALO_STREAMING)
{
capturarFrame();
lastFrame = millis();
}
// Verifica o estado do BUZZER para saber se deve desligar
if (estado_buz==HIGH && millis()-disparo > delayValue)
{
estado_buz = LOW;
digitalWrite(pinBUZ,estado_buz);
Serial.println("Buzzer foi desativado");
}
// Verifica se desconectou da Internet para tentar reconexão
if (WiFi.status() != WL_CONNECTED)
{
// Tenta a conexão WiFi
Serial.println("Tentando reconectar ao WiFi...");
WiFi.begin(ssid, password);
unsigned long ultimaVez = millis();
int cont=0;
while (WiFi.status() != WL_CONNECTED && (millis()-ultimaVez)<timeoutWifi)
{
if (++cont % 80 == 0 ) Serial.println();
else Serial.print(".");
}
Serial.println( WiFi.status() == WL_CONNECTED ? "\nReconectado" : "\nReconexão Falhou" );
}
// Verifica o estado do PIR para ligar o Buzzer e/ou tirar foto
estadoPIR = digitalRead(pinPIR);
if (estadoPIR && (millis()-ultimaDeteccao) > intervaloPIR)
{
Serial.println("Presença detectada...");
ultimaDeteccao = millis();
// Dá um tempo para o objeto passar em frente à câmera já que a deteccção é antecipada
delay(1500);
if (alarmarPIR)
{
delayValue = tempoPIR;
Serial.print("Delay=");
Serial.println(delayValue);
disparo = millis();
estado_buz = HIGH;
digitalWrite(pinBUZ,estado_buz);
}
if (fotoPIR)
{
// Define o nome do arquivo da foto
String fn = getTimeStampFname() + ".jpg";
String path = dirFotos + fn;
Serial.printf("Arquivo da Foto: %s\n", path.c_str());
// Liga o Flash
if (usarFlash)
{
digitalWrite(pinFLASH,HIGH);
}
// Take and Save Photo
takeNewPhoto(path);
// Desliga o Flash
if (usarFlash)
{
digitalWrite(pinFLASH,LOW);
}
}
}
// Verifica se houve escalonamento do FTP para fazer a transferência
if (estado_ftp)
{
// Chama a rotina para fazer as transferências via FTP
String msg="";
char server[ftpServer.length()+1]; // = strServer.toCharArray(ftpServer, sizeof(ftpServer));
char user[ftpUser.length()+1];
char pass[ftpPassword.length()+1];
char dirftp[ftpDir.length()+1];
char dirfotos[dirFotos.length()+1];
ftpServer.toCharArray(server, sizeof(server));
ftpUser.toCharArray(user, sizeof(user));
ftpPassword.toCharArray(pass, sizeof(pass));
ftpDir.toCharArray(dirftp, sizeof(dirftp));
dirFotos.toCharArray(dirfotos, sizeof(dirfotos));
if (EnviaFotosFtp(server, ftpPort, user, pass, dirftp, SD_MMC, dirfotos, &msg))
{
resultado_ftp = textToHtml("<b>FTP Sucesso</b>: "+msg);
saveConfigFile(SD_MMC);
}
else
{
resultado_ftp = textToHtml("<b>FTP fracasso</b>: "+msg);
}
estado_ftp = false;
}
}
//------------------------------------------
// Lista em ordem o conteúdo da pasta de JPG
//------------------------------------------
String listFiles(fs::FS &fs)
{
String fileList=""; // Para conter o html gerado
std::map<std::string, int> fotos; // Para ordenar em ordem crescente pelo nome do arquivo
char pasta[dirFotos.length()]; // Nome da pasta como char[] para a API do SD
dirFotos.toCharArray(pasta, sizeof(pasta));
File dir = fs.open(pasta);
File file = dir.openNextFile();
fileList += "<header>\n";
fileList += " <h2>Relação de Fotos</h2>\n";
fileList += "</header>\n";
fileList += "<table>\n";
fileList += "<tbody>\n";
int cont=0;
uint64_t totalSize = 0;
// Percorre o diretório e adiciona os pares FileName=Size na lista ordenada
while (file)
{
if (!file.isDirectory() && String(file.name()).endsWith(".jpg"))
{
//String fn=file.name();
totalSize += file.size();
int tamanho = static_cast<int>(file.size() / 1024); // Convertendo para int
fotos[file.name()]=tamanho;
}
file = dir.openNextFile();
}
// Percorre a lista ordenada gerando o html
for (const auto& par : fotos)
{
String fn = par.first.c_str();
String tam= std::to_string(par.second).c_str();
cont++;
fileList += "<tr>\n";
fileList += " <td>" + String(cont) + "</td>\n";
fileList += " <td>" + fn + "</td>\n";
fileList += " <td>" + tam + "K</td>\n";
fileList += " <td><button class=\"view-btn\" onclick=\"acaoBotao('/visualizar?fn=" + fn + "')\">Visualizar</button></td>\n";
fileList += " <td><button class=\"download-btn\" onclick=\"acaoBotao('/baixar?fn=" + fn + "')\">Baixar</button></td>\n";
fileList += " <td><button class=\"remove-btn\" onclick=\"acaoBotao('/remover?fn=" + fn + "')\">Remover</button></td>\n";
fileList += "</tr>\n";
}
fileList += "</tbody>\n";
fileList += "</table>\n";
// Monta a parte de capacidade do SD
if (capacity!=0)
{
// Obtém informações do sistema de arquivos
float ocup = used;
ocup /= capacity;
ocup *= 100;
fileList += "<header>\n";
fileList += " <h2>Resumo</h2>\n";
fileList += "</header>\n";
fileList += "<table>\n";
fileList += "<tbody>\n";
fileList += "<tr>\n";
fileList += " <th>N. de JPG</th>\n";
fileList += " <th>Ocupação dos JPG</th>\n";
fileList += " <th>Tamanho do Volume SD</th>\n";
fileList += " <th>Ocupação do SD</th>\n";
fileList += "</tr>\n";
fileList += "<tr>\n";
fileList += " <td>";
fileList += String(cont);
fileList += "</td>\n";
fileList += " <td>";
fileList += melhorUnidade(totalSize);
fileList += "</td>\n";
fileList += " <td>";
fileList += melhorUnidade(capacity);
fileList += "</td>\n";
fileList += " <td>";
fileList += String(ocup) + "%";
fileList += "</td>\n";
fileList += "</tr>\n";
fileList += "</tbody>\n";
fileList += "</table>\n";
}
dir.close();
return fileList;
}
//------------------------------------------------
// Evento chamado no processo de conexão do Wifi
//------------------------------------------------
void WiFiEvent(WiFiEvent_t event)
{
Serial.printf("[Evento Wi-Fi] evento: %d\n", event);
switch (event)
{
case SYSTEM_EVENT_WIFI_READY:
Serial.println("interface WiFi pronta");
break;
case SYSTEM_EVENT_SCAN_DONE:
Serial.println("Pesquisa por AP completada");
break;
case SYSTEM_EVENT_STA_START:
Serial.println("Cliente WiFi iniciado");
break;
case SYSTEM_EVENT_STA_STOP:
Serial.println("Clientes WiFi cancelados");
break;
case SYSTEM_EVENT_STA_CONNECTED:
Serial.println("Conectado ao AP");
digitalWrite(LED_RED,LOW); // Liga o LED Vermelho para mostrar a conexão com WiFi
break;
case SYSTEM_EVENT_STA_DISCONNECTED:
Serial.println("Desconectado do AP WiFi");
digitalWrite(LED_RED,HIGH); // Desliga o LED Vermelho para mostrar a desconexão com WiFi
//Check_WiFiManager(false);
break;
case SYSTEM_EVENT_STA_AUTHMODE_CHANGE:
Serial.println("Modo de Autenticação do AP mudou");
break;
case SYSTEM_EVENT_STA_GOT_IP:
Serial.print("Endereço IP obtido: ");
Serial.println(WiFi.localIP());
break;
case SYSTEM_EVENT_STA_LOST_IP:
Serial.println("Endereço IP perdido e foi resetado para 0");
break;
case SYSTEM_EVENT_STA_WPS_ER_SUCCESS:
Serial.println("WPS: modo enrollee bem sucedido");
break;
case SYSTEM_EVENT_STA_WPS_ER_FAILED:
Serial.println("WPS: modo enrollee falhou");
break;
case SYSTEM_EVENT_STA_WPS_ER_TIMEOUT:
Serial.println("WPS: timeout no modo enrollee");
break;
case SYSTEM_EVENT_STA_WPS_ER_PIN:
Serial.println("WPS: pin code no modo enrollee");
break;
case SYSTEM_EVENT_AP_START:
Serial.println("AP Wifi Iniciado");
break;
case SYSTEM_EVENT_AP_STOP:
Serial.println("AP Wifi parado");
break;
case SYSTEM_EVENT_AP_STACONNECTED:
Serial.println("Cliente conectado");
break;
case SYSTEM_EVENT_AP_STADISCONNECTED:
Serial.println("Cliente desconectado");
break;
case SYSTEM_EVENT_AP_STAIPASSIGNED:
Serial.println("IP associado ao Cliente");
break;
case SYSTEM_EVENT_AP_PROBEREQRECVED:
Serial.println("Requisição de probe recebida");
break;
case SYSTEM_EVENT_GOT_IP6:
Serial.println("IPv6 é preferencial");
break;
case SYSTEM_EVENT_ETH_START:
Serial.println("Interface Ethernet iniciada");
break;
case SYSTEM_EVENT_ETH_STOP:
Serial.println("Interface Ethernet parada");
break;
case SYSTEM_EVENT_ETH_CONNECTED:
Serial.println("Interface Ethernet conectada");
break;
case SYSTEM_EVENT_ETH_DISCONNECTED:
Serial.println("Interface Ethernet desconectada");
break;
case SYSTEM_EVENT_ETH_GOT_IP:
Serial.println("Endereço IP obtido");
break;
default: break;
}
}
//---------------------------------------------------------
// Sincroniza o horário do ESP32 com NTP server brasileiro
//---------------------------------------------------------
bool getNTPtime(int sec)
{
{
uint32_t start = millis();
tm timeinfo;
time_t now;
int cont=0;
do
{
time(&now);
localtime_r(&now, &timeinfo);
if (++cont % 80 == 0) Serial.println();
else Serial.print(".");
delay(10);
} while (((millis() - start) <= (1000 * sec)) && (timeinfo.tm_year < (2016 - 1900)));
if (timeinfo.tm_year <= (2016 - 1900)) return false; // the NTP call was not successful
Serial.print("\nnow ");
Serial.println(now);
Serial.print("Time ");
Serial.println(getTimeStamp());
}
return true;
}
//------------------------------------------------
// Inicializa a Câmera para tirar fotos
//------------------------------------------------
bool configESPCamera()
{
// Objeto para configuração da Câmera
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG; // Choices are YUV422, GRAYSCALE, RGB565, JPEG
config.grab_mode = CAMERA_GRAB_LATEST;
// Seleciona a resolução da Câmera
if (psramFound())
{
config.frame_size = default_resolution; // FRAMESIZE_ + QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
config.jpeg_quality = 10; //10-63 lower number means higher quality
config.fb_count = 2;
}
else
{
default_resolution = FRAMESIZE_SVGA;
config.frame_size = default_resolution;
config.jpeg_quality = 12;
config.fb_count = 2;
}
Serial.print("Mode1 ");
Serial.print(getFrameSizeName(default_resolution));
Serial.print(" ...");
// Inicializa a Câmera
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK)
{
Serial.printf("Camera init failed with error 0x%x", err);
return false;
}
// Faz ajuste nos parãmetros de qualidade da imagem
sensor_t * s = esp_camera_sensor_get();
// BRIGHTNESS (-2 to 2)
s->set_brightness(s, 2); // era 0
// CONTRAST (-2 to 2)
s->set_contrast(s, 2); // era 0
// SATURATION (-2 to 2)
s->set_saturation(s, -2);// era 0
// SPECIAL EFFECTS (0 - No Effect, 1 - Negative, 2 - Grayscale, 3 - Red Tint, 4 - Green Tint, 5 - Blue Tint, 6 - Sepia)
s->set_special_effect(s, 0);
// WHITE BALANCE (0 = Disable , 1 = Enable)
s->set_whitebal(s, 1);
// AWB GAIN (0 = Disable , 1 = Enable)
s->set_awb_gain(s, 1);
// WB MODES (0 - Auto, 1 - Sunny, 2 - Cloudy, 3 - Office, 4 - Home)
s->set_wb_mode(s, 0);
// EXPOSURE CONTROLS (0 = Disable , 1 = Enable)
s->set_exposure_ctrl(s, 1);
// AEC2 (0 = Disable , 1 = Enable)
s->set_aec2(s, 0);
// AE LEVELS (-2 to 2)
s->set_ae_level(s, 0);
// AEC VALUES (0 to 1200)
s->set_aec_value(s, 300);
// GAIN CONTROLS (0 = Disable , 1 = Enable)
s->set_gain_ctrl(s, 1);
// AGC GAIN (0 to 30)
s->set_agc_gain(s, 0);
// GAIN CEILING (0 to 6)
s->set_gainceiling(s, (gainceiling_t)0);
// BPC (0 = Disable , 1 = Enable)
s->set_bpc(s, 0);
// WPC (0 = Disable , 1 = Enable)
s->set_wpc(s, 1);
// RAW GMA (0 = Disable , 1 = Enable)
s->set_raw_gma(s, 1);
// LENC (0 = Disable , 1 = Enable)
s->set_lenc(s, 1);
// HORIZ MIRROR (0 = Disable , 1 = Enable)
s->set_hmirror(s, 0);
// VERT FLIP (0 = Disable , 1 = Enable)
s->set_vflip(s, 0); // era 0
// DCW (0 = Disable , 1 = Enable)
s->set_dcw(s, 1);
// COLOR BAR PATTERN (0 = Disable , 1 = Enable)
s->set_colorbar(s, 0);
return true;
}
//------------------------------------------------
// Inicializa o cartão SD
//------------------------------------------------
bool initMicroSDCard()
{
// Start the MicroSD card
//--------------------------------------------------------------
if (!SD_MMC.begin("/sdcard", true)) // <==== atenção: parâmetros são IMPORTANTES para o SD-MMC não
// usar os GPIO4, GPIO12 e GPIO13 e liberar para uso do usuário.
// O GPIO4 para o FLASH (LED), o GPIO12 para o PIR e o
// GPIO13 para o BUZZER.
//--------------------------------------------------------------
{
Serial.println("Falha na montagem do cartão SD");
return false;
}
uint8_t cardType = SD_MMC.cardType();
if (cardType == CARD_NONE)
{
Serial.println("Nenhum cartão SD encontrado");
return false;
}
return true;
}
//------------------------------------------------
// Tira uma Foto instantânea e salva no SD
//------------------------------------------------
String takeNewPhoto(String path)
{
String erro="";
// Tira a foto no buffer
camera_fb_t * fb = esp_camera_fb_get();
if (!fb)
{
erro = "Falha na captura da imagem";
Serial.println(erro);
return erro;
}
// Salva a foto no cartão SD
fs::FS &fs = SD_MMC;
File file = fs.open(path.c_str(), FILE_WRITE);
if (!file)
{
erro = "Falha ao abrir o arquivo para salvar a foto";
Serial.println(erro);
}
else
{
file.write(fb->buf, fb->len); // payload (image), payload length
Serial.printf("Foto foi salva no arquivo: %s\n", path.c_str());
}
// Fecha o arquivo
file.close();
used = SD_MMC.usedBytes();
// Devolve o bufefr para reuso
esp_camera_fb_return(fb);
return erro;
}
//-------------------------------------------------------
// Formata um varável time_t para string
//-------------------------------------------------------
String timeToString(time_t tempo)
{
char timestamp[30];
strftime(timestamp, 30, "%d/%m/%Y %T", localtime(&tempo));
return String(timestamp);
}
//------------------------------------------------
// Devolve o localtime dd/mm/aaaa hh:mm:ss
//------------------------------------------------
String getTimeStamp()
{
time_t now;
time(&now);
return String(timeToString(now));
}
//------------------------------------------------
// Devolve o localtime para nome de arquivo ou log
//------------------------------------------------
String getTimeStampFname()
{
time_t now;
time(&now);
char timestamp[30];
strftime(timestamp, 30, "%Y_%m_%d_%H_%M_%S", localtime(&now));
return String(timestamp);
}
//------------------------------------------------
// Encapsula o texto em html
//------------------------------------------------
String textToHtml(String texto)
{
return "<hr><br>" + texto + "\n";
}
//------------------------------------------------
// Macro expansão dos HTML's
//------------------------------------------------
String processor(const String& var)
{
//Serial.print("Var=");
//Serial.println(var);
if (var.equalsIgnoreCase("BUTTONPLACEHOLDER") && htmlResposta !="")
{
String resp = htmlResposta;
htmlResposta = "";
return resp;
}
else if (var.equalsIgnoreCase("espid"))
{
return String(esp_id);
}
else if (var.equalsIgnoreCase("delayvalue"))
{
return delayValue==0 ? "" : String(delayValue);
}
else if (var.equalsIgnoreCase("ftpserver"))
{
return ftpServer;
}
else if (var.equalsIgnoreCase("ftpport"))
{
return String(ftpPort);
}
else if (var.equalsIgnoreCase("ftpUser"))
{
return ftpUser;
}
else if (var.equalsIgnoreCase("ftppassword"))
{
return ftpPassword;
}
else if (var.equalsIgnoreCase("ftpdir"))
{
return ftpDir;
}
else if (var.equalsIgnoreCase("selyesalarm"))
{
return alarmarPIR ? "selected" : "";
}
else if (var.equalsIgnoreCase("selnoalarm"))
{
return !alarmarPIR ? "selected" : "";
}
else if (var.equalsIgnoreCase("selyesfoto"))
{
return fotoPIR ? "selected" : "";
}
else if (var.equalsIgnoreCase("selnofoto"))
{
return !fotoPIR ? "selected" : "";
}
else if (var.equalsIgnoreCase("selyesflash"))
{
return usarFlash ? "selected" : "";
}
else if (var.equalsIgnoreCase("selnoflash"))
{
return !usarFlash ? "selected" : "";
}
else if (var.equalsIgnoreCase("selyesremoverfotos"))
{
return removerfotos_ftp ? "selected" : "";
}
else if (var.equalsIgnoreCase("selnoremoverfotos"))
{
return !removerfotos_ftp ? "selected" : "";
}
else if (var.equalsIgnoreCase("tempopir"))
{
return String(tempoPIR);
}
else if (var.equalsIgnoreCase("intervalopir"))
{
return String(intervaloPIR);
}
else if (var.equalsIgnoreCase("dirfotos"))
{
return dirFotos;
}
else if (var.equalsIgnoreCase("fotos"))
{
return listFiles(SD_MMC);
}
else if (var.equalsIgnoreCase("nomefoto"))
{
return nomeFoto;
}
return String();
}
//------------------------------------------------
// Envia as Fotos do SD para o servidor FTP
//------------------------------------------------
bool EnviaFotosFtp(char *server, int porta, char *user, char *pass, char *dest, fs::FS &fs, char *source, String *msg)
{
ESP32_FTPClient ftp(server, porta, user, pass, 5000, 1);
// Tenta conectar ao servidor FTP
Serial.println("Conectando no Servidor FTP");
Serial.print("Server: "); Serial.println(server);
Serial.print("Port : "); Serial.println(porta);
Serial.print("User : "); Serial.println(user);
Serial.print("Pass : "); Serial.println(pass);
ftp.OpenConnection();
if (!ftp.isConnected())
{
*msg = "Não foi possível conectar ao servidor FTP";
return false;
}
// Muda o destino no servidor FTP
Serial.print("Mudando o Diretório no Servidor FTP: ");
Serial.println(dest);
ftp.ChangeWorkDir(dest);
// Faz a varredura das fotos no diretório do MicroSD
Serial.println("Varrendo o SD");
char pasta[dirFotos.length()];
dirFotos.toCharArray(pasta, sizeof(pasta));
File dir = fs.open(pasta);
File file = dir.openNextFile();
int nFotos=0;
int sucessos=0;
int falhas=0;
// Loop en todos os arquivos da pasta no SD
while (file)
{
if (!file.isDirectory() && String(file.name()).endsWith(".jpg"))
{
Serial.print("Processando: ");
Serial.print(file.name());
Serial.print(" => ");
nFotos++;
String path=dirFotos + "/" + String(file.name());
// Abre e tenta ler o arquivo
File foto = fs.open(path, FILE_READ);
// Abre o arquivo no destino
ftp.InitFile("Type I");
ftp.NewFile(file.name());
// Inicializações
int len=0;
int lidos;
// Loop varrendo todos os bytes do arquivo a cada buffer
while (foto.available()!=0)
{
lidos = foto.readBytes(buf,sizeof(buf));
if (lidos==0)
{
falhas++;
*msg = "Erro ao enviar " + String(file.name()) + " => Sucessos = " + String(sucessos) + " - Falhas = " + String(falhas);
foto.close();
ftp.CloseFile();
return false;
}
// Grava o buffer no ftp
ftp.WriteData((unsigned char*)buf, lidos);
len += lidos;
Serial.print(len);
Serial.print(", ");
// Dá um tempo para o SD
delay(defaultAtrasoSD);
}
// Encerra o processamento do arquivo
Serial.println("finalizado");
foto.close();
ftp.CloseFile();
sucessos++;
}
file = dir.openNextFile();
}
// Verifica se deve remover as fotos depois do FTP
int removidos = 0;
if (sucessos>0 && removerfotos_ftp)
{
File diretorio = SD_MMC.open(pasta);
// Percorre todos os arquivos no diretório
File arquivo = diretorio.openNextFile();
while (arquivo)
{
// Verifica se o arquivo é um arquivo .jpg
if (arquivo.isDirectory() || arquivo.name()[0] == '.' || !String(arquivo.name()).endsWith(".jpg"))
{
// Dá um tempo para o SD
delay(defaultAtrasoSD);
// Avança no diretório
arquivo = diretorio.openNextFile();
continue;
}
// Exclui o arquivo
if (SD_MMC.remove(arquivo.path()))
{
Serial.print("Removido : ");
Serial.println(arquivo.name());
removidos++;
}
// Dá um tempo para o SD
delay(defaultAtrasoSD);
// Abre o próximo arquivo
arquivo = diretorio.openNextFile();
}
diretorio.close();
}
*msg = "Ftp concluído com sucesso. N. de Fotos enviadas = " +
String(sucessos) + " N. de Fotos removidas = " +
String(removidos);
return true;
}
//------------------------------------------------
// Persiste Parâmetros do Servidor FTP no MicroSD
//------------------------------------------------
void saveConfigFile(fs::FS &fs)
{
// O arquivo de Config é salvo no formato JSON
Serial.println(F("Persistindo a configuração..."));
// Cria um documento JSON
//StaticJsonDocument<512> json;
JsonDocument json;
json["ftpServer"] = ftpServer;
json["ftpPort"] = ftpPort;
json["ftpUser"] = ftpUser;
json["ftpPass"] = ftpPassword;
json["ftpDir"] = ftpDir;
json["alarmarPIR"]= alarmarPIR;
json["fotoPIR"] = fotoPIR;
json["usarFlash"] = usarFlash;
json["tempoPIR"] = tempoPIR;
json["intervaloPIR"] = intervaloPIR;
json["dirFotos"] = dirFotos;
json["removerfotosFTP"] = removerfotos_ftp;
// Abre o arquivo de configuração
File configFile = fs.open(JSON_CONFIG_FILE, "w");
if (!configFile)
{
// Erro, arquino não foi aberto
Serial.println("Erro ao persistir a configuração");
return;
}
// Serializa os dados do JSON no arquivo
serializeJsonPretty(json, Serial);
Serial.println();
if (serializeJson(json, configFile) == 0)
{
// Erro ai gravar o arquivo
Serial.println(F("Erro ao gravar o arquivo de configuração"));
}
// Fecha o Arquivo
configFile.close();
}
//------------------------------------------------
// Recupera Parâmetros do Servidor FTP do MicroSD
//------------------------------------------------
bool loadConfigFile(fs::FS &fs)
{
// Carrega o arquivo de Configuração
if (fs.exists(JSON_CONFIG_FILE))
{
// o arquivo existe, vamos ler
Serial.println("Lendo o arquivo de configuração");
File configFile = fs.open(JSON_CONFIG_FILE, "r");
if (configFile)
{
Serial.println("Arquivo de configuração aberto...");
//StaticJsonDocument<512> json;
JsonDocument json;
DeserializationError error = deserializeJson(json, configFile);
serializeJsonPretty(json, Serial);
Serial.println();
if (!error)
{
Serial.println("Recuperando o JSON...");
if (json.containsKey("ftpServer")) ftpServer = json["ftpServer"].as<String>();
else ftpServer = defaultftpServer;
if (json.containsKey("ftpPort")) ftpPort = json["ftpPort"].as<int>();
else ftpPort = defaultftpPort;
if (json.containsKey("ftpUser")) ftpUser = json["ftpUser"].as<String>();
else ftpUser = defaultftpUser;
if (json.containsKey("ftpPass")) ftpPassword = json["ftpPass"].as<String>();
else ftpPassword = defaultftpPass;
if (json.containsKey("ftpDir")) ftpDir = json["ftpDir"].as<String>();
else ftpDir = defaultftpDir;
if (json.containsKey("alarmarPIR")) alarmarPIR = json["alarmarPIR"].as<bool>();
else alarmarPIR = false;
if (json.containsKey("fotoPIR")) fotoPIR = json["fotoPIR"].as<bool>();
else fotoPIR = false;
if (json.containsKey("tempoPIR")) tempoPIR = json["tempoPIR"].as<int>();
else tempoPIR = defaultPIR;
if (json.containsKey("usarFlash")) usarFlash = json["usarFlash"].as<bool>();
else intervaloPIR = defaultUsarFlash;
if (json.containsKey("intervaloPIR")) intervaloPIR = json["intervaloPIR"].as<int>();
else intervaloPIR = defaultIntervaloPIR;
dirFotos = json["dirFotos"].as<String>();
if (json.containsKey("removerfotosFTP")) removerfotos_ftp = json["removerfotosFTP"].as<bool>();
else removerfotos_ftp = defaultRemoverFotosFtp;
//---------------------------------------------------------------------------------------------
// Caso o JSON contenha o parâmetro "frameSize":"UXGA"|"SXGA"|"XGA"|"SVGA"|"VGA"|"QVGA"|"CEF",
// inserido manualmente, a resolução será adotada para evitar recompilação e upload do código
// no dispositivo. Ex: "frameSize": "QVGA"
//-------------------------------------------------------------------------------------
if (json.containsKey("frameSize"))
{
String frameSize = json["frameSize"].as<String>();
if (frameSize.equalsIgnoreCase("UXGA")) default_resolution = FRAMESIZE_UXGA;
else if (frameSize.equalsIgnoreCase("SXGA")) default_resolution = FRAMESIZE_SXGA;
else if (frameSize.equalsIgnoreCase("XGA")) default_resolution = FRAMESIZE_XGA;
else if (frameSize.equalsIgnoreCase("SVGA")) default_resolution = FRAMESIZE_SVGA;
else if (frameSize.equalsIgnoreCase("VGA")) default_resolution = FRAMESIZE_VGA;
else if (frameSize.equalsIgnoreCase("QVGA")) default_resolution = FRAMESIZE_QVGA;
else if (frameSize.equalsIgnoreCase("CIF")) default_resolution = FRAMESIZE_CIF;
else default_resolution = FRAMESIZE_QVGA;
}
return true;
}
else
{
// Erro ao ler o JSON
Serial.println("Erro ao carregar o JSON da configuração...");
}
}
else
{
// Erro ao abrir o JSON
Serial.println("Erro ao abrir o JSON da configuração...");
}
}
else
{
// Arquivo JSON não encontrado
Serial.println("Arquivo da configuração JSON não encontrado...");
}
return false;
}
//------------------------------------------------
// Mostra informações da Requisição na Console
//------------------------------------------------
void displayRequest(AsyncWebServerRequest *request)
{
Serial.print("Método: ");
Serial.print(request->methodToString());
Serial.print("\t| URL: ");
Serial.print(request->url());
Serial.print("\t| IP: ");
Serial.println(request->client()->remoteIP());
}
//-------------------------------------------------------
// Captura o Frame e envia via Websocket
//-------------------------------------------------------
void capturarFrame()
{
//Serial.println("capturando um Frame...");
camera_fb_t * fb = NULL;
fb = esp_camera_fb_get();
if (!fb)
{
Serial.println("Camera capture failed");
return;
}
if(fb->format != PIXFORMAT_JPEG)
{
Serial.println("Formato não JPEG");
return;
}
// Envia o frame
//Serial.print("Vou enviar via Socket...");
//Serial.println(ws.count());
ws.binaryAll(fb->buf, fb->len);
//Serial.println("Foi enviado via Socket...");
// Cleanup final
esp_camera_fb_return(fb);
}
//-------------------------------------------------------
// Calcula a melhor representação para tamanho de arquivo
//-------------------------------------------------------
String melhorUnidade(uint64_t size)
{
int ind = 0;
uint64_t valor=size;
do
{
if (valor < 1024)
{
return String(valor) + unidades[ind];
}
valor /= 1024;
ind++;
}
while (ind<3);
return String(valor) + unidades[ind];
}
//-------------------------------------------------------
// Devolve a resolução QVGA|CIF|VGA|SVGA|XGA|SXGA|UXGA
//-------------------------------------------------------
String getFrameSizeName(framesize_t size)
{
switch (size)
{
case FRAMESIZE_UXGA: return "UXGA-1600x1200";
case FRAMESIZE_SXGA: return "SXGA-1280x1024";
case FRAMESIZE_XGA: return "XGA-1024x768";
case FRAMESIZE_SVGA: return "SVGA-800x600";
case FRAMESIZE_VGA: return "VGA-640x480";
case FRAMESIZE_QVGA: return "QVGA-320x240";
case FRAMESIZE_CIF: return "CIF-352x288";
default: return "Desconhecido";
}
}
1) A figura 1 nos mostra a pinagem disponível para o usuário no ESP32-CAM. Observem que não são muitos pinos e, além disso, a câmera, o MicroSD e o modo de upload utilizam tais pinos, praticamente esgotando para outros usos como, por exemplo, sensores.

Figura 1 – Pinos do ESP32-CAM
2) A Figura 2 mostra o esquema de ligação do módulo FTDI. Este módulo é usado para alimentar o circuito durante o desenvolvimento do código e para permitir o upload para o ESP32-CAM, que não possui uma interface micro USB.

Figura 2 – Diagrama de LIgação do FTDI
3) A Figura 3 nos mostra o diagrama dos componentes usados no projeto. Temos dois sensores: o PIR para detecção de presença e o BUZZER para emitir sinal sonoro. Além disso, temos o micro-controlador em si, a fonte de protoboard para permitir alimentar o circuito em 5V e um conjunto de duas baterias 18650 de 4.2V. As baterias são ligadas em série gerando 8.4V na entrada da fonte de protoboard, que faz a regulagem para 5V para consumo do micro-controlador.

4) A Figura 4 nos mostra a tela principal da aplicação com as opções existentes. Observem que no cabeçalho o número serial do ESP32-CAM.

Figura 4 – Requisição “/”
5) A Figura 5 nos mostra a relação de fotos do tipo .jpg existentes no cartão MicroSD e as operações que podem ser executadas com cada foto. Mais abaixo há um resumo da ocupação do SD.

Figura 5 – Requisição “/list”
6) A Figura 6 nos mostra o formulário para a ativação/desativação do modo streaming na tela do Navegador. O vídeo é enviado à taxa de 4 frames/s.

Figura 6 – Requisição “/stream”
7) A Figura 7 nos mostra um conjunto de informações do servidor http e a parametrização de algumas funções do programa quando uma presença é detectada.

Figura 7 – Requisição “/status”
8) A Figura 8 nos mostra as informações da Rede Wifi. Entre elas o DB, que informa a qualidade do sinal Wifi (quanto mais próximo de zero, melhor).

Figura 8 – Requisição “/wifi”
9) A Figura 9 nos mostra um formulário de definição dos parâmetros de um servidor FTP que pode ser usado para enviar as fotos do cartão SD para o servidor. Existem muitos vídeos no Youtube que ensinam como configurar um servidor FTP num computador Windows, Linux ou MAC.

Figura 9 – Requisição “/ftp”
10) A Figura 10 nos mostra um formulário para a definição dos parâmetros de ação do programa quando uma presença for detectada, como o tempo do alarme sonoro e o local de armazenamento no cartão SD.

Figura 10 – Requisição “/config”
11) A Figura 11 nos mostra a visualização de uma foto do cartão SD com as opções de rotação no sentido horário ou anti-horário.

Figura 11 – Visualização de Foto
12) A Figura 12 nos mostra o circuito no modo de desenvolvimento alimentado pelo módulo FTDI. Observem que o ESP32-CAM foi configurado para usar uma antena externa para captar melhor o sinal da rede Wifi. Na referência de número 12, há uma indicação de vídeo que ensina como ativar a antena externa no ESP32-CAM. Por default, o ESP32-CAM vem configurado para a antena interna, que possui menor captação do sinal Wifi.

Figura 12 – Configuração para Upload do Código
13) A Figura 13 nos mostra o circuito alimentado pela bateria e a fonte de protoboard para operação em campo. Caso seja utilizado em local aberto, é imprescindível utilizar um case para proteger o circuito de intempéries. Na figura 14, mostramos uma sugestão de case.

Figura 13 – Configuração de Campo sem Case
14) A Figura 14 nos mostra todos os componentes usados e opcionais do projeto. No vídeo de demonstração do projeto mencionamos mais detalhes. Portanto, não deixe de assistir para obter maiores informações sobre os opcionais.

Figura 14 – Componentes Usados e Opcionais
15) A figura 15 nos mostra a tela do IDE do Arduino com as opções necessárias para a compilação do programa. A biblioteca do ESP32 precisa ser instalada previamente.

Figura 15 – Parâmetros de Compilação
16) A Figura 16 nos mostra a tela da console do IDE do Arduino com as mensagens emitidas pelo programa na inicialização. O IP do ESP32-CAM é importante para que seja possível acessar através de um Navegador. Existem alguns aplicativos que ajudam a identificar o IP do ESP32-CAM numa rede. O aplicativo FING é um deles, e possui versões para Android, Windows e Mac.

Figura 16 – Console com mensagens informativas
O ESP32-CAM é um excelente micro-controlador com câmera de vídeo integrada e um soquete para cartão MicroSD para armazenamento local. É fácil de usar e recomendado para aplicações de monitoramento, desde as mais simples, como também aplicações mais complexas com reconhecimento facial e sistemas de segurança.
1) Introdução: https://blog.eletrogate.com/introducao-ao-esp32-cam/
2) Exemplo mais avançado do GitHub : https://github.com/s60sc/ESP32-CAM_MJPEG2SD
3) Cartão SD no modo 1Bit: https://dr-mntn.net/2021/02/using-the-sd-card-in-1-bit-mode-on-the-esp32-cam-from-ai-thinker
4) Sincronismo do relógio interno: https://randomnerdtutorials.com/esp32-ntp-timezones-daylight-saving/
5) Servidor Web Assíncrono: https://randomnerdtutorials.com/esp32-async-web-server-espasyncwebserver-library/
6) Pinagem do Esp32-CAM: https://randomnerdtutorials.com/esp32-cam-ai-thinker-pinout/
7) Câmera de Segurança: https://www.youtube.com/watch?v=k_PJLkfqDuI
8) Câmera de Segurança: https://www.youtube.com/watch?v=Ul0h5Maeoeg
9) Câmera de Segurança: https://www.youtube.com/watch?v=k_PJLkfqDuI
10) Foto no SD com hibernação: https://www.youtube.com/watch?v=5KszL2Opuo0
11) Uso do ESP32-CAM: https://www.youtube.com/watch?v=visj0KE5VtY
12) Ativando a antena externa: https://www.youtube.com/watch?v=aBTZuvg5sM8
|
|
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!