Streaming com ESP32CAM, AsyncWebServer, PAN-TILT e Atualização OTA

Eletrogate 30 de agosto de 2024

Introdução

Este projeto tem como objetivo implementar uma câmera de monitoramento utilizando os recursos de streaming do ESP32CAM e usando um PAN TILT com dois servos para permitir a movimentação horizontal (de 0 a 180º) e vertical (de 0 a 90º). A visualização do streaming e o posicionamento dos servos serão feitos através de uma interface WEB.

Controle PAN-TILT com ESP32CAM

Para aumentar a flexibilidade e o alcance da câmera de monitoramento, adicionaremos um suporte PAN-TILT, permitindo a rotação horizontal (PAN) e vertical (TILT) da câmera. Isso é especialmente útil para aplicações de vigilância, onde é necessário monitorar uma área maior. A Referência 2 mostra um vídeo que ensina a montar o PAN-TILT.

Figura 1 – PAN-TILT montado

Como Funciona: Os servos são controlados pelo ESP32CAM usando sinais PWM (Pulse Width Modulation) através de uma interface WEB em um navegador. A biblioteca ESP32Servo facilita o controle desses servos, permitindo que o usuário ajuste a posição da câmera com precisão.

Aplicações Práticas:

  • Vigilância Residencial: Monitore diferentes áreas da sua casa com uma única câmera.
  • Robótica: Use a câmera PAN-TILT em robôs para melhorar a navegação e a detecção de objetos.
  • Projetos DIY (Do It Yourself): Ideal para entusiastas de eletrônica que desejam explorar novas possibilidades com o ESP32CAM.

Atualizações OTA com ElegantOTA

Uma das grandes vantagens de utilizar o ESP32CAM é a possibilidade de realizar atualizações de firmware Over-The-Air (OTA). Devido à ausência de uma interface USB nativa, a atualização tradicional do firmware no ESP32CAM pode ser complicada, exigindo jumpers ou adaptadores USB. Isso se torna ainda mais desafiador quando o dispositivo está instalado em locais de difícil acesso, como dentro de containers ou montagens complexas.

Com a biblioteca ElegantOTA, esse processo se torna significativamente mais simples e intuitivo. A ElegantOTA oferece uma interface de usuário elegante e fácil de usar, permitindo que o firmware do dispositivo seja atualizado remotamente, sem a necessidade de desconectar ou mover o ESP32CAM. Isso não só economiza tempo, mas também reduz o risco de danos ao hardware durante a manipulação.

Além disso, a ElegantOTA é compatível com o AsyncWebServer, permitindo que as atualizações sejam realizadas de forma assíncrona, sem interromper outras funcionalidades do dispositivo. Para mais detalhes sobre a implementação, consulte a Referência 6.

Benefícios do ElegantOTA:

  • Interface Intuitiva: A ElegantOTA fornece uma interface web amigável para realizar as atualizações.
  • Status em Tempo Real: Acompanhe o progresso da atualização em tempo real, garantindo que tudo esteja ocorrendo conforme o esperado.
  • Facilidade de Implementação: Com apenas algumas linhas de código, você pode integrar a ElegantOTA ao seu projeto.

Figura 2 – Interface OTA para atualização com autenticação

 


Materiais Necessários

Figura 3 – Materiais Utilizados

 


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 a página principal para ativação/desativação do streaming, movimentação dos servos e ligar/desligar o FLASH.
    • /update => para atualizar o firmware via OTA
  2. Atualizar o relógio interno do ESP32CAM sincronizado com o servidor NTP do Brasil.
  3. Inserir um nome DNS para a estação para evitar ter que descobrir o IP. A URL http://<dnsname>.local poderá ser usada para acessar a página principal.

Observações:

  • Utilizaremos 2 x GPIO’s (GPIO14 e GPIO15) para a movimentação dos dois servos.
  • O LED frontal poderá ser acionado para ligar/desligar o FLASH para ajudar na luminosidade.
  • O LED VERMELHO, que fica na parte de trás do ESP32CAM, será usado para sinalizar quando conectado ou não ao WiFi. Lembrando que este LED possui polaridade invertida, LOW liga e o HIGH desliga.
  • Utilizaremos uma bateria 18650 para alimentar o ESP32CAM e os servos. Recomendamos atenção no manuseio desse tipo de bateria por causa de risco de superaquecimento que pode gerar potencialmente incêndio. Nesse projeto usamos uma bateria 18650 de 4,2V 9800 mAh. Supondo um consumo médio constante de 1A para os servos e o ESP32CAM, teremos uma autonomia de cerca de 9 h. Lembrando que a movimentação do motor é o maior consumidor de corrente. Só o ESP32CAM tem um consumo médio de 350 mA com WiFi ativado.
  • Utilizaremos um SHIELD para a bateria 18650 para permitir alimentar o ESP32CAM e os servos (em 5V) e não sobrecarregar a alimentação via porta de 5V do ESP32CAM. O SHIELD permitirá recarregar a bateria sem a necessidade de remoção e interrupção do funcionamento do circuito. Adicionalmente, o SHIELD tem um circuito de proteção para controlar a carga excessiva ou a descarga excessiva aumentando a segurança e a vida útil da bateria.
  • Optamos por ativar a antena externa do ESP32CAM para permitir afastar o circuito do roteador e ter o servidor Web ativo pela rede WiFi (veja a Referência 7). A antena interna tem menor alcance.
  • O HTML usado pela aplicação está definido com a palavra reserva PROGMEM significando que será armazenado na memória FLASH para não comprometer a memória RAM. No ESP32CAM a FLASH é de 4 Mb.
  • A Barra de pinos de 1×40 servirá para extrair os pinos para os dois servos (3x para o servo1 + 3x para o servo2) e os pinos para a alimentação (2x).
  • As duas barras empilháveis, de 8 pinos cada, serão usadas para formar a base para encaixar o ESP32CAM na placa.
  • O firmware do ESP32 pode ser atualizado over-the-air através da OTA. Basta usar a URL /update para cair no FORM de atualização (o botão ATUALIZAR no HTML faz este acionamento) . Será necessário ter a usuário/senha para atualizar (veja o default no código fonte). O arquivo .bin do sketch pode ser obtido pelo IDE do Arduino através do Menu Sketch | Exportar Binário Compilado. Isso facilita atualizações de versão do programa sem a necessidade de se ter a placa conectada fisicamente no computador para a carga. Muitas vezes a placa pode estar num lugar de difícil remoção para ligar no computador. Além disso, o desenvolvedor pode enviar o arquivo .bin de uma nova versão para o usuário/cliente fazer a carga via OTA, podendo até publicar o .bin num site para o download pelo usuário/cliente. Portanto, isso facilita a atualização em qualquer lugar do mundo. Utilizaremos a biblioteca ElegantOTA com Async Mode para a coexistência estável com o Async Web Server. Veja a Referência 6 para maiores detalhes.
  • A figura a seguir mostra os pré-requisitos importantes para a compilação do aplicativo que precisam ser ajustados antes da compilação. A biblioteca ElegantOTA precisa de espaço na partição SPIFFS.

Figura 4 – Opções de Compilação importantes

  • A figura a seguir mostra como gerar o arquivo .bin no IDE do Arduino para ser usado na atualização OTA. Importante: a primeira compilação deve ser feita usando o módulo FTDI ou a base para o ESP32CAM. A partir daí, as próximas cargas podem ser feitas via a interface OTA uma vez que o código OTA já está carregado na partição do ESP32CAM.

Figura 5 – Como gerar o arquivo .bin para a atualização no IDE ao Arduino

  • A figura a seguir mostra a pinagem do ESP32CAM:

Figura 6 – Pinagem do ESP32CAM

  • O módulo FTDI pode ser usado para carregar o sketch no ESP32CAM durante o desenvolvimento do aplicativo. A figura a seguir mostra a configuração dos pinos:

Figura 7 – Ligações do Módulo FTDI

  • A outra maneira de carregar o sketch seria usando o Módulo Adaptador ESP32 que torna a operação mais simples e direta sem a necessidade de jumper entre o GND e a porta 001, usando a USB:

Figura 8 – Módulo Adaptador para o ESP32CAM

  • A comunicação entre a aplicação no ESP32CAM e o Navegador é feita via Web Socket numa mão dupla. Num sentido, o Streaming é enviado para o Navegador em forma de array de bytes e, no outro sentido, as informações de posicionamento do PAN-TILT e do estado do FLASH são enviadas para o ESP32CAM usando o formato JSON:

 

Posicionamentos Estados do Flash

{“type”:”hori”,”angle”:”45″}

{“type”:”vert”,”angle”:”20″}

{“type”:”flash”,”flash”:true}

{“type”:”flash”,”flash”:false}

 

  • Os controles de posicionamento dos servos e do Estado do FLASH na interface Web só farão efeito quando o modo streaming estiver ativado. Adicionalmente, o envio do posicionamento via web socket terá um atraso para evitar a sobrecarga da camada TCP/IP e dos servos. Os eventos de mudança nos SLIDER’s do HTML são muito rápidos e podem gerar congestionamento na camada TCP/IP do ESP32CAM e dos servos. Para isso, definimos uma constante WS_ATRASO=120ms  no código e que pode ser ajustado em tempo de compilação.
  • Utilizaremos uma Placa Universal de 3 cm x 7 cm para acomodar o circuito ao invés de usar protoboard. Com isso, o encaixe na plataforma do PAN-TILT fica mais facilitado (simples encaixe). Adicionalmente, alguns pontos de solda serão necessários. A Antena foi fixada utilizando-se uma presilha. Veja as figuras a seguir:

Figura 9 – Placa Universal com componentes soldados

Figura 10 – Placa Universal montada no PAN-TILT

Figura 11 – Diagrama do Circuito

Figura 12 – Tela Principal sem/com streaming ativado

Figura 13 – Console com as mensagens de inicialização

 


Bibliotecas Utilizadas

Antes de iniciar a programação, é preciso fazer a instalação das placas da Espressif e das seguintes bibliotecas:

  • ESPAsyncWebServer (disponível no GitHub do desenvolvedor);
  • ESP32Servo (Versão 3.0.5);
  • ElegantOTA (Versão 3.1.1);
  • AsyncTCP (Versão 1.1.4);
  • ESPmDNS;
  • WiFi;
  • time;
  • ArduinoJson (Versão 7.0.4);

Com exceção da biblioteca ESPAsyncWebServer, você encontra as demais no próprio Gerenciador de Bibliotecas da Arduino IDE, sem contar que algumas, como as relacionadas ao WiFi, são instaladas juntamente com o pacote de placas da Espressif. Importante: utilizamos a versão 2.0.17 (última da versão 2) da placa ESP32 pois tivemos problemas de compatibilidade usando a versão 3.

Se tem dúvidas na instalação das placas, siga nosso tutorial de instalação.

 


Código Fonte

//------------------------------------------------------------------------------------------------
// Função    : Este programa tem como objetivo implementar uma câmera de monitoramento utilizando 
//             os recursos de streaming do ESP32CAM e usando um PAN TILT com dois servos para  
//             permitir a rotação horizontal (de 0 a 180º) e vertical (de 0 a 90º). 
//
// 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 a página principal para ativação/desativação do streaming,
//                   movimentação dos servos e ligar/desligar o FLASH.
//       /update  => para atualizar o firmware via OTA
//
//    2) Atualizar o relógio interno do ESP32CAM sincronizado com o servidor NTP do Brasil.
//
//    3) Inserir um nome DNS para a estação para evitar ter que descobrir o IP e a URL
//       http://<dnsname>.local poderá ser usada para acessar a página principal.
//
// Componentes : 1) 1 x Placa ESP32CAM com câmera (antena wifi opcional)
//               2) 1 x Módulo FTDI ou Módulo Adaptador ESP32 para carga do programa
//               3) 1 x PAN TILT
//               4) 2 x Servos SG90
//               5) 1 x Bateria 18650
//               6) 1 x Shield para uma bateria 18650
//               7) 1 x Placa Universal de 3cm x 7cm
//               8) 1 x Barra de Pinos Macho 1x40 - 180º 
//               9) 2 x Barra de Pinos Macho/Fêmea Empilhável (8 pinos)
//              10) Jumpers diversos
//
// Observações: 1) Utilizaremos 2 x GPIO's para a movimentação dos dois servos. 
//              2) O LED/FLASH poderá ser acionado para ligar/desligar o FLASH para ajudar
//                 na luminosidade.
//              3) O LED VERMELHO, que fica na parte de trás do ESP32CAM, será usado para 
//                 sinalizar quando conectado ou não no Wifi.
//              4) Utilizaremos uma bateria 18650 para alimentar o ESP32CAM e os servos. 
//                 Recomendamos atenção no manuseio desse tipo de bateria por causa de risco de 
//                 superaquecimento que pode gerar potencialmente incêndio.
//              5) Utilizaremos um SHIELD para a bateria 18650 para permitir alimentar o ESP32CAM
//                 e os servos e não sobrecarregar a alimentação via porta de 5V do ESP32CAM.
//                 Adicionamente, o SHIELD permitirá recarregar a bateria sem a necessidade de
//                 remoção e interrupção do funcionamento do circuito.
//              6) Optamos por ativar a antena externa do ESP32CAM para permitir afastar o circuito do roteador 
//                 e ter o servidor Web ativo pela rede wifi (veja a Referência 13). 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 ESP32CAM a FLASH é de 4 Mb.
//              8) Utilizaremos a Placa Universal de 3cm x 7cm para subtituir a Protoboard e 
//                 se encaixar bem na plataforma do PAN-TILT. 
//              9) A Barra de pinos de 1x40 servirá para extrair os pinos para os dois servos 
//                 (3x para o servo1 + 3x para o servo2) e os pinos para a alimentação (2x).
//             10) As duas barras empilháveis, de 8 pinos cada, serão usadas para formar a base 
//                 para encaixar o ESP32CAM na placa.
//             11) O firmware do ESP32 pode ser atualizado over-the-air através da OTA. Basta
//                 usar a URL /update para cair no FORM de atualização. Será necessário ter a
//                 senha para atualizar. O arquivo .bin do sketch pode ser obtido pelo IDE do
//                 Arduino através do Menu Sketch | Exportar Binário Compilado. Isso facilita
//                 atualizações de versão do programa sem a necessidade de se ter a placa
//                 conectada fisicamente no desktop para a carga. Muitas vezes a placa pode 
//                 estar num lugar de difícil remoção para ligar num desktop. Adicionalmente,
//                 o desenvolvedor pode enviar o arquivo .bin de uma nova versão para o usuário
//                 fazer a carga via OTA. Portanto, isso facilita a atualização em qualquer 
//                 lugar do mundo. Utilizaremos a biblioteca ElegantOTA com Async Mode para a estável
//                 coexistência como Async Web Server. Veja a referência (6) para maiores detalhes.
//             12) Para a atualização OTA funcionar no ESP32CAM é necessário ajustar a opção de
//                 compilação "Minimal SPIFFS (1.9MB APP with OTA/190KB SPIFFS". A placa selecionada
//                 deve ser "Al Thinker ESP32-CAM". 
//
// Autor       : Dailton Menezes         
//
// Referências : 1) https://randomnerdtutorials.com/esp32-cam-pan-and-tilt-2-axis/
//               2) https://www.youtube.com/watch?v=rQmITwTDZHE&t=5s
//               3) https://randomnerdtutorials.com/esp32-ntp-timezones-daylight-saving/
//               4) https://randomnerdtutorials.com/esp32-async-web-server-espasyncwebserver-library/
//               5) https://docs.elegantota.pro/
//               6) https://docs.elegantota.pro/async-mode/#enabling-async-mode
//               7) https://github.com/sigmdel/ESP32-CAM_OTA
//              
// Versão  : 1.0 Jul/2024
//------------------------------------------------------------------------------------------------

#include <WiFi.h>                                      // Biblioteca WiFi
#include <time.h>                                      // Biblioteca Time para manipulação de data/hora
#include <AsyncTCP.h>                                  // Biblioteca AsyncTCP usado pelo Web
#include <ESPAsyncWebServer.h>                         // Biblioteca Asynch Web Server
#include <ArduinoJson.h>                               // Biblioteca JSON para comunicação e parãmetros   
#include <ESPmDNS.h>                                   // Biblioteca para inclusão do hostname no mDNS 
#include <ESP32Servo.h>                                // Biblioteca para manipulação de servos
#include <ElegantOTA.h>                                // Biblioteca para atualização via Web
         
//--------------------------------------
// 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 pinWIFI     GPIO_NUM_33                        // Usado para ligar o LED se conectado no Wifi
#define pinFLASH    GPIO_NUM_4                         // Usado para acionar o Flash
#define pinPAN      GPIO_NUM_14                        // Usado para movimentar o Servo Horizontal
#define pinTILT     GPIO_NUM_15                        // USado para movimentar o Servo Vertical

//----------------------------------------------------------
// 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 MAX_HORIZONTAL       180                       // Máximo Angulo Horizontal
#define MAX_VERTICAL         90                        // Máximo Ângulo Vertical
#define WS_ATRASO            120                       // Atraso para envio via web socket (ms)
#define cleanupInterval      10                        // N. de passadas para chamar cleanupClients
#define USER_UPDATE          "admin"                   // Usuário para atualização via OTA
#define PASS_UPDATE          "esp32@cam"               // Senha para atualização via OTA
#define timeoutWifi          15*1000                   // Timeout para reconhecimento do wifi ativo
#define defaultDNSNAME       "camera"                  // Nome default para DNSNAME/HOSTNAME
#define ESP_getChipId()   ((uint32_t)ESP.getEfuseMac() // Simular ID da placa ESP

//---------------------------------------
// Definições para Streaming
//---------------------------------------

String modo_streaming[2] = {"Iniciar", "Terminar"};    // Modo do Streaming
unsigned lastFrame = 0;                                // Momento do último frame enviado  
framesize_t default_resolution = FRAMESIZE_VGA;        // 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 da sua Rede";     // Nome da rede Wifi 
char password[]    = "Informe a senha da sua Rede";    // Senha da rede Wifi
String dnsName     = defaultDNSNAME;                   // Nome default para DNS NAME

//-------------------------------
// 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
unsigned long ultimaDesconexao = 0;                    // Última desconeção do Wifi
String modo_ligado[] = {"OFF", "ON"};                  // Estado ligado ON/OFF
bool estado_cam = false;                               // Se a câmera foi inicilizada
bool estado_flash = false;                             // Estado do Led Flash ON/OFF
time_t startup;                                        // Horário da inicialização
char esp_id[50];                                       // Id do ESP32CAM
int panAngulo=0;                                       // Atual angulo do PAN (horizontal)
int tiltAngulo=0;                                      // Atual angulo do TILT (vertical)
int cleanupCounter = 0;                                // Contador de passadas para chamar cleanupClients()

//-------------------------------
// Definição dos Servos usados
//-------------------------------

Servo panServo;                                        // Servo para o movimento horizontal
Servo tiltServo;                                       // Servo para o movimento vertical

//---------------------------------------------
// Variáveis para controle do OTA
//---------------------------------------------

bool autoRebootOTA     = true;                         // Se deve fazer autoreboot após a atualização OTA
char user_OTA[16]      = USER_UPDATE;                  // Usuário para atualização OTA
char pass_OTA[16]      = PASS_UPDATE;                  // Senha para atualização OTA

//----------------------------------------
// Define o HTML para fazer streaming
//----------------------------------------

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>ESP32CAM Video Streaming</title>
  <style>
      body { font-family: Arial, sans-serif; font-size: 10px; margin: 0; padding: 0; background-color: #f4f4f4; overflow: hidden; }
      .slider {
          width: 80&percnt;;
          margin: 20px auto;
      }
      input[type="range"] {
          accent-color: blue; /* Define a cor do slider como azul */
          width: 200px;
          height: 10px;
      }    
      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: 120px; margin: 5px; padding: 10px; font-size: 16px; border: none; border-radius: 5px; cursor: pointer;}

      .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 */
      }

     .btn-update {
       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 {
        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&percnt;;
        max-height: 100&percnt;;
        width: auto;
        height: auto;
      }
  
      .switch {
        position: relative;
        display: inline-block;
        width: 60px;
        height: 34px;
      }
  
      .switch input {
        opacity: 0;
        width: 0;
        height: 0;
      }
  
      .slider-switch {
        position: absolute;
        cursor: pointer;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #ccc;
        transition: .4s;
        border-radius: 34px;
      }
  
      .slider-switch:before {
        position: absolute;
        content: "";
        height: 26px;
        width: 26px;
        left: 4px;
        bottom: 4px;
        background-color: white;
        transition: .4s;
        border-radius: 50&percnt;;
      }
  
      input:checked + .slider-switch {
        background-color: #4CAF50;
      }
  
      input:checked + .slider-switch:before {
        transform: translateX(26px);
      }
  
      .switch-container {
        display: flex;
        align-items: center;
        justify-content: center;
        margin: 10px 0;
      }
  
      .switch-label {
        margin-left: 10px;
        font-size: 20px;
        color: #444444;
      }  
  </style>
</head>
<body>
  <header>
    <h2>ESP32CAM PAN-TILT</h2>
  </header>
  <center>
  <button id='toggleButton' onclick='toggleStreaming()' class='btn-stream'>Ativar</button>
  <button onclick="acaoBotao('/update')" class='btn-update'>Atualizar</button><br>
  <div class="switch-container">
    <label class="switch">
      <input type="checkbox" id="flashSwitch">
      <span class="slider-switch"></span>
    </label>
    <span class="switch-label">Estado FLASH</span>
  </div>
  <div class="slider">
      <label for="horizontalSlider">Ângulo Horizontal:</label><br>
      <input type="range" id="horizontalSlider" min="0" max="%maxhorizontal%" value="%panangulo%">
      <span id="horizontalValue">%panangulo%°</span>
  </div>
  <div class="slider">
      <label for="verticalSlider">Ângulo Vertical:</label><br>
      <input type="range" id="verticalSlider" min="0" max="%maxvertical%" value="%tiltangulo%">
      <span id="verticalValue">%tiltangulo%°</span>
  </div>  
  <br>
  <div id='stream-container'>
    <img id='stream' alt='stream'>
  </div>
  </center>
  <script>
    var socket;
    let debounceTimeout;
    const horizontalSlider=document.getElementById('horizontalSlider');
    const horizontalValue=document.getElementById('horizontalValue');
    const verticalSlider=document.getElementById('verticalSlider');
    const verticalValue=document.getElementById('verticalValue');
    const flashSwitch=document.getElementById('flashSwitch');

    function formatToThreeDigits(value)
    {
       return String(value).padStart(3,'0');
    }
    
    horizontalSlider.addEventListener('input', () => 
    {
        horizontalValue.textContent = `${horizontalSlider.value}°`;
        clearTimeout(debounceTimeout);
        debounceTimeout = setTimeout(() => {
          socket.send(JSON.stringify({ type: 'hori', angle: formatToThreeDigits(horizontalSlider.value) }));
        }, %WSATRASO%); // Ajuste o tempo de debounce conforme necessário
    });

    verticalSlider.addEventListener('input', () => 
    {
        verticalValue.textContent = `${verticalSlider.value}°`;
        clearTimeout(debounceTimeout);
        debounceTimeout = setTimeout(() => {
            socket.send(JSON.stringify({ type: 'vert', angle: formatToThreeDigits(verticalSlider.value) }));
        }, %WSATRASO%); // Ajuste o tempo de debounce conforme necessário
    });
    
    flashSwitch.addEventListener('change',()=>
    {
       socket.send(JSON.stringify({type:'flash',flash:flashSwitch.checked}));
    });
    
    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://%iplocal%/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 acaoBotao(acao) 
     {
        window.location.href = acao;
     }    

    // Evento antes da página ser descarregada
    
    window.addEventListener('beforeunload',function()
    {
      stopStreaming();
    });
    
  </script>
</body>
</html>
)rawliteral";

//--------------------------------
// Prototipação das funções usadas
//--------------------------------

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 ESP32CAM
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 processor(const String& var); // Faz a expansão de variáveis no HTML
void displayRequest(AsyncWebServerRequest *request);  // Mostra informações da requisição na Console
void capturarFrame();                // Captura e Envia o Frame via WebSocket
bool setDNSNAME(String nome);        // Define o HostName como DNSNAME
String getFrameSizeName(framesize_t size); // Devolve o nome da resolução da câmera

//------------------------------------
// Setup do ESP32CAM
//------------------------------------

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 Led da parte de trás no ESP32CAM. Será ligado pela rotina de
   // eventos quando conectado no WiFi ou desligado quando fora 

   pinMode(pinWIFI,OUTPUT);  

   // Define o Led Frontal/Flash como output 

   pinMode(pinFLASH, OUTPUT); 

   // Attacha os servos

   panServo.attach(pinPAN);
   tiltServo.attach(pinTILT);

   // Posiciona os servos a zero graus

   panServo.write(panAngulo);
   tiltServo.write(tiltAngulo);   

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

   // Define o HostName para o servidor web para facilitar o acesso na rede local
   // sem conhecer o IP previamente

   Serial.print("Adicionando " + String(dnsName) + " no MDNS... ");
   if (setDNSNAME(dnsName))
   {
       Serial.println("adicionado corretamente no MDNS!");
   }
   else 
   {
       Serial.println("Erro ao adicionar no MDNS!");
   }

   // 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 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); 
      request->send_P(200, "text/html", index_html, processor);
  });

  // Define uma página para links não encontrados
  
  server.onNotFound([](AsyncWebServerRequest *request)
  {
     // Atende a requisição NOT FOUND
  
     Serial.println("Requisição não encontrada");
     displayRequest(request);
  
     // Retorna a mensagem de erro em caso de um retorno 404
  
     request->send(404, "text/html", "<h1>Erro: Requisi&ccedil;&atilde;o n&atilde;o encontrada</h1>");
  });

  // 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.printf("Cliente Socket Id=%d conectado no IP %s\n",client->id(),client->remoteIP().toString());
     } 
     else if (type == WS_EVT_DISCONNECT) 
     {
        Serial.printf("Cliente Socket Id=%d desconectado do IP %s\n",client->id(),client->remoteIP().toString());
        Serial.println(client->remoteIP()); 
     }
     else if (type == WS_EVT_DATA) 
     {
        // Trate os dados recebidos via WebSocket
        data[len] = '\0'; 
        String message = String((char*)data);
        Serial.println("Mensagem recebida: " + message);

        // Tratamento do JSON para extrair a movimentação dos Servos

        JsonDocument json;
        DeserializationError error = deserializeJson(json, message);

        if (!error)
        {
           String tipo = json["type"].as<String>();
           if (tipo.startsWith("flash"))
           {
              estado_flash = json["flash"].as<bool>();
              digitalWrite(pinFLASH, estado_flash ? HIGH : LOW); 
           }
           else if (tipo.startsWith("hori")) 
           {
              panAngulo = json["angle"].as<int>();
              panServo.write(panAngulo);
           }
           else if (tipo.startsWith("vert"))
           {
              tiltAngulo = json["angle"].as<int>();
              tiltServo.write(tiltAngulo);
          }
        }
        
     }   

  });  

  // Adiciona o manipulador de WebSocket ao servidor
  
  server.addHandler(&ws);  

  // Credenciais para atualizações via OTA
   
  ElegantOTA.setAuth(user_OTA,pass_OTA);

  // Habilita/Desabilita AutoRebbot após a atualização
   
  ElegantOTA.setAutoReboot(autoRebootOTA);   

  // Inicia o OTA para atualização via Web

  ElegantOTA.begin(&server);      

  // Inicia o servidor web
  
  server.begin();  

  // Pega a hora do startup

  time(&startup);
  localtime(&startup);  

  // Obtém o id do ESP32CAM

  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]);

  // Aguardando request http na porta 80

  Serial.println("\nAguardando requisições http na porta 80...");
  Serial.println("Use http://" + String(dnsName) + ".local no seu navegador...");  
  Serial.println("Ou opcionalmente...");      
  Serial.println("Use http://" + WiFi.localIP().toString() + " no seu navegador...\n");  
  
}

//------------------------------------
// Loop Principal
//------------------------------------

void loop() 
{

  // Verifica se está no modo streaming para capturar um frame e enviar via socket

  if (estado_cam && ws.count() > 0)
  {  
     capturarFrame();
     //lastFrame = millis();
  }
  
  // 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 OTA para saber se há atualização

   ElegantOTA.loop();   
  
}

//------------------------------------------------
// 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(pinWIFI,LOW);   // Liga o LED Vermelho para mostrar a conexão com WiFi (invertido)
      break;
    case SYSTEM_EVENT_STA_DISCONNECTED:
      Serial.println("Desconectado do AP WiFi");
       digitalWrite(pinWIFI,HIGH); // Desliga o LED Vermelho para mostrar a desconexão com WiFi (invertido)
      //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 2
   // CONTRAST (-2 to 2)
   s->set_contrast(s, 2);   // era 2
   // SATURATION (-2 to 2)
   s->set_saturation(s, -2);// era -2
   // 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;

}

//-------------------------------------------------------
// Formata um variá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));
}

//------------------------------------------------
// Macro expansão dos HTML's
//------------------------------------------------

String processor(const String& var)
{
  //Serial.print("Var=");
  //Serial.println(var);

  if (var.equalsIgnoreCase("iplocal"))
  {
     return WiFi.localIP().toString();
  }   
  else if (var.equalsIgnoreCase("estadoflash"))
  {
     return estado_flash ? "checked" : "";
  }  
  else if (var.equalsIgnoreCase("maxhorizontal"))
  {
     return String(MAX_HORIZONTAL);
  } 
  else if (var.equalsIgnoreCase("maxvertical"))
  {
     return String(MAX_VERTICAL);
  }   
  else if (var.equalsIgnoreCase("panangulo"))
  {
     return String(panAngulo);
  }   
  else if (var.equalsIgnoreCase("tiltangulo"))
  {
     return String(tiltAngulo);
  }   
  else if (var.equalsIgnoreCase("wsatraso"))
  {
     return String(WS_ATRASO);
  }     
  return String();
}

//-------------------------------------------------------
// Define o HostName como DNS NAME
//-------------------------------------------------------

bool setDNSNAME(String nome)
{
   WiFi.setHostname(nome.c_str());
   bool ok = MDNS.begin(nome.c_str());
   if (ok) MDNS.addService("http", "tcp", 80);      
   return ok;
}

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

//------------------------------------------------
// 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);

   // Aguarde a entrega do frame para todos os clientes
   // Obs: Só chamaremos a rotina cleanupClients a cada cleanupInterval

   cleanupCounter = 0;
   while (ws.count()>0 && !ws.availableForWriteAll())
   {
      cleanupCounter = (cleanupCounter+1) % cleanupInterval;
      if (cleanupCounter == 0) 
      {
         ws.cleanupClients();
      } 
   }
   
}

 


Conclusão

O ESP32CAM é uma ferramenta poderosa para quem busca desenvolver projetos de monitoramento e vigilância, oferecendo uma combinação de flexibilidade, facilidade de uso e custo acessível. Com a adição de funcionalidades como PAN-TILT, AsyncWebServer e atualizações OTA, suas possibilidades se expandem ainda mais, tornando-o uma escolha excelente para uma ampla gama de aplicações. Entre os principais benefícios, destacam-se:

  • Flexibilidade e Alcance: A capacidade de mover a câmera horizontal e verticalmente permite cobrir uma área maior, ideal para vigilância e monitoramento.
  • Facilidade de Atualização: Com a ElegantOTA, as atualizações de firmware podem ser realizadas de forma remota, sem a necessidade de acesso físico ao dispositivo, economizando tempo e esforço.
  • Custo-Benefício: O ESP32CAM é uma solução acessível que oferece recursos avançados, tornando-o uma excelente escolha para projetos DIY (Do It Yourself) e profissionais.
  • Integração Simples: A implementação de funcionalidades como PAN-TILT e OTA é facilitada pelas bibliotecas disponíveis, permitindo que mesmo iniciantes possam aproveitar ao máximo o potencial do ESP32CAM.

Sobre o autor


Alberto de Almeida Menezes
tinho.menezes@gmail.com

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


Dailton de Oliveira Menezes
dailton.menezes@gmail.com

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


Eletrogate

30 de agosto 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.

Tenha a Metodologia Eletrogate dentro da sua Escola! Conheça nosso Programa de Robótica nas Escolas!

Eletrogate Robô

Cadastre-se e fique por
dentro de novidades!