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:
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:

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

Figura 3 – Materiais Utilizados
Observações:

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

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

Figura 6 – Pinagem do ESP32CAM

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

Figura 8 – Módulo Adaptador para o ESP32CAM
| Posicionamentos | Estados do Flash |
{“type”:”hori”,”angle”:”45″} {“type”:”vert”,”angle”:”20″} | {“type”:”flash”,”flash”:true} {“type”:”flash”,”flash”:false} |

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
Antes de iniciar a programação, é preciso fazer a instalação das placas da Espressif e das seguintes bibliotecas:
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.
//------------------------------------------------------------------------------------------------
// 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%;
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%;
max-height: 100%;
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%;
}
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ção nã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();
}
}
}
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:
|
|
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!