Automação Residencial

Sistema de Monitoramento de Vida Selvagem

Eletrogate 12 de março de 2024

Introdução

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.


Materiais Necessários para o Projeto Sistema de Monitoramento de Vida Selvagem

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


Bibliotecas

ESPAsyncWebServer
AsyncTCP
Wifi, FS, SD_MMC, Time, ArduinoJson, ESP32_FTPClient (Instalados através do Gerenciador de Bibliotecas)


Descrição

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.
/listmostrar o conteúdo da pasta de imagens JPG no MicroSD e permitir as funções de visualizar, baixar ou remover.
/ftpmostrar o formulário de definição dos parâmetros do FTP.
/uploadfazer a transferência de todos os arquivos .jpg para uma pasta de um servidor FTP e persistir os parâmetros no MicroSD.
/configmostrar o formulário de definição dos parâmetros do PIR.
/setpirpersistir os parâmetros no MicroSD.
/beep?delay=<nnnn>tocar o BUZZER/Ligar FLASH por <nnnn> ms.
/shottirar uma foto e armazenar no MicroSD.
/statusinformar o status do servidor no Navegador.
/wifimostrar as informações da Rede WiFi no Navegador.
/streamentrar 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.


Observações

  1. O módulo FTDI é usado para alimentação do circuito com 5V e no UPLOAD do programa durante o desenvolvimento e testes. As duas baterias 18650 em série, de 4,2V 8800mAh cada (8,4V), servem para alimentar a fonte de protoboard no local definitivo (que regula para 5V) e garantem uma autonomia de cerca de 22 horas de funcionamento contínuo no campo, sem a necessidade de uma fonte alimentada em 127V  (já 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 horas, que é a autonomia esperada.
  2. O uso simultâneo da CÂMERA, MICROSD, SERIAL e FTDI praticamente esgota todos os GPIO’s do micro-controlador 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 não tenhamos achado nada explícito na documentação do ESP32-CAM, da Câmera e MicroSD. Ao tentar usar, o micro-controlador fica rebootando continuamente.
  3. Neste projeto utilizaremos o GPIO4 (Flash/LED) compartilhando com o BUZZER e inicializaremos o MicroSD no modo 1Bit para que sejam liberados os GPIO’s 4, 12 e 13 para uso no programa. Isso causa maior lentidão na leitura e gravação, pois será usado apenas o GPIO2 para as duas operações, mas 3 portas importantes são assim liberadas para as funcionalidades esperadas do projeto. Apesar de a documentação do modo 1Bit afirmar que o GPIO13 seria liberado (veja a Referência 4), não conseguimos fazer funcionar usando o BUZZER nesta porta, pois a inicialização do SD sempre falhava. Passamos então a usar o GPIO4 compartilhado com o FLASH e BUFFER e o PIR usará o GPIO12.
  4. O cartão micro SD precisa ser formatado a priori antes do uso no ESP32-CAM (formatação usando 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 incêndio. 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. Importante: Não se esqueça de mudar o <Nome da Rede> e a <Senha da Rede> no código antes de compilar no seu ambiente. Tais definições ficam nas linhas 213 e 214 do código.
  9. Opcional: Você pode ajustar a resolução da câmera antes da compilação na linha 200 do código, selecionando uma das resoluções indicadas nos comentários. Outra maneira de mudar a resolução, sem a compilação, seria editando o arquivo config.json na raiz do cartão SD e inserir mais um parâmetro: Ex: “frameSize”:”UXGA”. Veja os comentários no Código nas linhas 2392 a 2396.
  10. Insucessos no projeto: Não conseguimos implementar o BUZZER numa porta separada. O GPIO13 seria o pino elegível, mas não funcionou, mesmo adotando o modo 1Bit do MicroSD; Não conseguimos usar a biblioteca Wifi Manager para evitar o uso da credencial da rede explicitamente no código. A coexistência dos recursos Wifi Manager, ESPAsyncWebServer, Câmera e MicroSD gerou incompatibilidade tornando o código instável e com reboot contínuo.
  11. Funções:
    a) Listar o conteúdo da pasta definida para fotos no Navegador ordenando por nome e permitir visualizar, baixar e remover. Tirar uma foto e armazenar na pasta do MicroSD;
    b) Fazer streaming de vídeo;
    c) Mostrar o status do servidor no Navegador;
    d) Mostrar as informações da Rede Wifi;
    e) Enviar as fotos da pasta do MicroSD para um servidor FTP em bloco;
    f) Configurar os parâ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;
    g) Ativar o Buzzer por um tempo definido.

Código

//------------------------------------------------------------------------------------------------
// 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";
   }
}

Vídeo Demonstrativo

A demonstração é conferida em https://youtu.be/c2bNQMVJiaY.


Imagens e Ilustrações

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.

Blog-Eletrogate-Pinos-Do-ESP32

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.

Blog-Eletrogate-Diagrama-FTDI

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.

Blog-Eletrogate-Requisicao-/

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.

Blog-Eletrogate-Requisicao-List

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.

Blog-Eletrogate-Requisicao-Stream

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).

Blog-Eletrogate-Requisicao-wifi

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.

Blog-Eletrogate-Requisicao-ftp

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.

Blog-Eletrogate-Requisicao-config

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.

Blog-Eletrogate-Requisicao-visualizacao-de-foto

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.

Blog-Eletrogate-configuracao-para-upload-do-codigo

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.

Blog-Eletrogate-configuracao-de-campo-sem-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.

Blog-Eletrogate-componentes-usados-e-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.

Blog-Eletrogate-parametros-de-compilacao

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.

Blog-Eletrogate-console-com-mensagens-informativas

Figura 16 – Console com mensagens informativas


Conclusão

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.


Sobre Os Autores


Alberto de Almeida Menezes
[email protected]

Bacharel em Engenharia de Áudio e Produção Musical pela Berklee College of Music.


Dailton de Oliveira Menezes
[email protected]

Bacharel em Ciência da Computação pela Universidade Federal de Minas Gerais.


Eletrogate

12 de março de 2024

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

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

Eletrogate Robô

Assine nossa newsletter e
receba  10% OFF  na sua
primeira compra!