En este post te voy a hablar de mi experiencia con la ESP32-S3 WROOM N16R8 y sobre algunos experimentos que he llevado a cabo con ella.
Tal vez escriba una guía más adelante. Por ahora todo el código presentado aquí funciona correctamente pero se puede mejorar y limpiar en muchos aspectos.
Por cierto, fue muy complejo configurar la cámara y encontrar los pines adecuados. Desconozco si fue error mío o que el fabricante no deja la documentación clara, pero no te preocupes, aquí describiré todos los problemas que encontré y las soluciones.
Algunas cosas que resaltar son:
- Tiene función de I2S así que se puede grabar y reproducir audio
- Tiene función de tarjeta SD así que se puede leer y escribir
- Obviamente tiene WiFi, se puede usar como cliente o como servidor
- Trae cámara OV2640 integrada
- Tiene un LED RGB WS2812 muy guapo
- Tiene sensor de touch para cuando tocas un pin con tu dedo
- He logrado mandar fotos a Telegram, stremear el audio y reproducir audio en el mismo código
- Puedes conectarla como si fuera un teclado o dispositivo de interfaz humana, esto abre la puerta a automatizaciones
Una cosa muy importante es que mi modelo específico no cuenta con SRAM adicional, así que solo tiene los 512KB de fábrica.
Si tú vas a comprar esta tarjeta te recomiendo que preguntes al vendedor si realmente tiene SRAM.
Previamente he programado la ESP32-CAM y la NodeMCU ESP8266, así que solo he instalado el CH340, y no el CH343 como dice.
Programando ESP32-S3 WROOM por primera vez
La conecté encendió el LED verde.
Utilicé PlatformIO para programarla, pero no lo hice de la manera normal. Hice una nueva carpeta y coloqué ahí:
- Una carpeta llamada
src
- Un archivo llamado
main.cpp
dentro de la carpeta creada en el paso 1 - Un archivo llamado
platformio.ini
con el siguiente contenido
Después abrí esa carpeta desde PlatformIO.
[env:esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
board_build.flash_size = 16MB
board_build.psram = disabled
lib_deps =
adafruit/Adafruit NeoPixel
Código de main.cpp:
void loop() {
led.begin();
led.show();
led.setPixelColor(0, led.Color(0,150,0)); led.show(); delay(400);
led.setPixelColor(0, led.Color(150,150,150)); led.show(); delay(600);
led.setPixelColor(0, led.Color(150,0,0)); led.show(); delay(400);
led.clear(); led.show();
}
Y todo bien
Grabar audio con el 43434 o algo así
#include <driver/i2s.h>
#include "SD_MMC.h"
#define I2S_WS 5 // LRCK
#define I2S_SD 6 // DATA
#define I2S_SCK 7 // BCLK
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const int RECORD_TIME = 10; // segundos
File audioFile;
void writeWavHeader(File &file, int sampleRate, int bitsPerSample, int channels, uint32_t dataSize)
{
uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
uint16_t blockAlign = channels * bitsPerSample / 8;
uint8_t header[44] = {
'R', 'I', 'F', 'F',
0, 0, 0, 0, // tamaño total - 8
'W', 'A', 'V', 'E',
'f', 'm', 't', ' ',
16, 0, 0, 0, // fmt chunk size
1, 0, // audio format PCM
(uint8_t)channels, 0,
(uint8_t)(sampleRate & 0xFF), (uint8_t)((sampleRate >> 8) & 0xFF),
(uint8_t)((sampleRate >> 16) & 0xFF), (uint8_t)((sampleRate >> 24) & 0xFF),
(uint8_t)(byteRate & 0xFF), (uint8_t)((byteRate >> 8) & 0xFF),
(uint8_t)((byteRate >> 16) & 0xFF), (uint8_t)((byteRate >> 24) & 0xFF),
(uint8_t)(blockAlign & 0xFF), (uint8_t)((blockAlign >> 8) & 0xFF),
(uint8_t)bitsPerSample, 0,
'd', 'a', 't', 'a',
0, 0, 0, 0 // data chunk size
};
// tamaño total
uint32_t fileSize = dataSize + 36;
header[4] = fileSize & 0xFF;
header[5] = (fileSize >> 8) & 0xFF;
header[6] = (fileSize >> 16) & 0xFF;
header[7] = (fileSize >> 24) & 0xFF;
// tamaño data
header[40] = dataSize & 0xFF;
header[41] = (dataSize >> 8) & 0xFF;
header[42] = (dataSize >> 16) & 0xFF;
header[43] = (dataSize >> 24) & 0xFF;
file.seek(0);
file.write(header, 44);
}
void setup()
{
Serial.begin(115200);
// Esto es muy importante, si no me decía SDMMCFS: some SD pins are not set
bool ok = SD_MMC.setPins(39, 38, 40);
Serial.printf("set pins: %d", ok);
if (!SD_MMC.begin("/sdcard", true))
{
Serial.println("Error al inicializar SD_MMC");
while (1)
;
}
Serial.println("SD inicializada correctamente!");
// Configurar I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = 256,
.use_apll = false};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK,
.ws_io_num = I2S_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_SD};
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pin_config);
audioFile = SD_MMC.open("/test.wav", FILE_WRITE);
if (!audioFile)
{
Serial.println("No se pudo crear archivo");
while (1)
;
}
// Escribir header temporal
uint8_t emptyHeader[44] = {0};
audioFile.write(emptyHeader, 44);
Serial.println("Grabando...");
}
void loop()
{
static uint32_t bytesWritten = 0;
const uint32_t maxBytes = RECORD_TIME * SAMPLE_RATE * CHANNELS * (BITS_PER_SAMPLE / 8);
int16_t buffer[256];
size_t bytesRead;
i2s_read(I2S_NUM_0, buffer, sizeof(buffer), &bytesRead, portMAX_DELAY);
if (bytesWritten + bytesRead > maxBytes)
{
bytesRead = maxBytes - bytesWritten;
}
audioFile.write((uint8_t *)buffer, bytesRead);
bytesWritten += bytesRead;
if (bytesWritten >= maxBytes)
{
Serial.println("Grabación terminada!");
audioFile.flush();
writeWavHeader(audioFile, SAMPLE_RATE, BITS_PER_SAMPLE, CHANNELS, bytesWritten);
audioFile.close();
while (1)
;
}
}
Reproducir MP3
Este ejemplo toma una canción llamada z.mp3 de la Micro SD y la reproduce infinitamente usando un MAX98357A e I2S.
Por cierto, necesitas earlephilhower/ESP8266Audio@^2.0.0
#include <Arduino.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
#define I2S_DOUT 5
#define I2S_BCLK 6
#define I2S_LRC 7
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
void setup()
{
Serial.begin(115200);
Serial.println("Iniciando reproductor de MP3 con ESP32-S3...");
// Inicialización de la tarjeta SD con SD_MMC
// Esto es muy importante, si no me decía SDMMCFS: some SD pins are not set
bool ok = SD_MMC.setPins(39, 38, 40);
if (!SD_MMC.begin("/sdcard", true))
{
Serial.println("Error al inicializar la tarjeta SD.");
return;
}
Serial.println("Tarjeta SD inicializada correctamente.");
out = new AudioOutputI2S();
out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
out->SetChannels(2);
out->SetGain(1);
file = new AudioFileSourceFS(SD_MMC, "/z.mp3");
mp3 = new AudioGeneratorMP3();
mp3->begin(file, out);
}
void loop()
{
if (mp3->isRunning())
{
if (!mp3->loop())
{
Serial.println("Reproducción terminada. Reiniciando...");
mp3->stop();
delete mp3;
delete file;
file = new AudioFileSourceFS(SD_MMC, "/z.mp3");
mp3 = new AudioGeneratorMP3();
mp3->begin(file, out);
}
}
}
La conexión es:
MAX98357A | ESP32-S3 WROOM |
---|---|
LRC (Left/right clock) | 7 |
BCLK (Bit clock input) | 6 |
DIN (Digital input signal) | 5 |
GAIN | Ninguno |
SD | Ninguno |
GND | GND |
Vin | 3V3 |
Crear servidor, recibir MP3, almacenar en SD y reproducir
Mi platformio.ini va quedando así:
[env:esp32s3]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
board_build.flash_size = 16MB
board_build.psram = disabled
monitor_speed = 115200
lib_deps =
earlephilhower/ESP8266Audio@^2.0.0
esp32async/AsyncTCP@^3.3.6
esp32async/ESPAsyncWebServer@^3.7.2
El código es:
#include <Arduino.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#define I2S_DOUT 5
#define I2S_BCLK 6
#define I2S_LRC 7
const char *ssid = "Tu ssid";
const char *password = "Tu contraseña";
AsyncWebServer server(80);
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
void setup()
{
Serial.begin(115200);
Serial.println("Iniciando reproductor de MP3 con ESP32-S3...");
// Inicialización de la tarjeta SD con SD_MMC
// Esto es muy importante, si no me decía SDMMCFS: some SD pins are not set
bool ok = SD_MMC.setPins(39, 38, 40);
if (!SD_MMC.begin("/sdcard", true))
{
Serial.println("Error al inicializar la tarjeta SD.");
return;
}
Serial.println("Tarjeta SD inicializada correctamente.");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(300);
Serial.print('.');
}
Serial.println("\nWiFi connected");
Serial.print("Servidor corriendo en: http://");
Serial.println(WiFi.localIP());
out = new AudioOutputI2S();
out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
out->SetChannels(2);
out->SetGain(0.15);
server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
static File uploadFile;
if(index == 0){
Serial.println("Comenzando upload de: " + filename);
uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
}
if(uploadFile){
uploadFile.write(data, len);
}
if(final){
uploadFile.close();
Serial.println("Upload completado, iniciando reproducción...");
// iniciar reproducción
if(file) delete file;
if(mp3) delete mp3;
file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
mp3 = new AudioGeneratorMP3();
mp3->begin(file, out);
} });
server.begin();
}
void loop()
{
if (mp3 && mp3->isRunning())
{
if (!mp3->loop())
{
mp3->stop();
delete mp3;
delete file;
mp3 = nullptr;
file = nullptr;
}
}
}
Para probar podemos usar cURL:
curl -v -F "file=@t_.mp3" http://192.168.0.9/upload
En este caso t_.mp3
es la canción que quiero subir y 192.168.0.9
es la IP
de la ESP32-S3 que me imprimió en el monitor Serial.
Por cierto, uso esas versiones de las dependencias (AsyncTCP y ESPAsyncWebServer) porque al usar las versiones más recientes me decía cosas como error: ‘ip_addr_t’ {aka ‘struct ip_addr’} has no member named ‘addr’; did you mean ‘u_addr’?
Crear servidor para stremear audio usando HTTP y ICS 43434
En el lugar donde lo compré dice: Módulo Micrófono Omnidireccional Mh-et I2s Ics 43434 Esp32
#include <driver/i2s.h>
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#define I2S_WS 5 // LRCK
#define I2S_SD 6 // DATA
#define I2S_SCK 7 // BCLK
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
AsyncWebServer server(80);
void setup()
{
Serial.begin(115200);
// Configurar I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = 256,
.use_apll = false};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK,
.ws_io_num = I2S_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_SD};
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_0, &pin_config);
// Wifi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(300);
Serial.print('.');
}
Serial.println("\nWiFi connected");
Serial.print("Servidor corriendo en: http://");
Serial.println(WiFi.localIP());
server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Hola mundo"); });
server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
{
AsyncWebServerResponse *response = request->beginChunkedResponse(
"audio/wav",
[](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
static bool headerSent = false;
//float gain = 2.0f; // 2x volumen = +6dB
if (!headerSent) {
headerSent = true;
// Header WAV "infinito"
uint32_t sampleRate = 16000;
uint16_t bitsPerSample = 16;
uint16_t channels = 1;
uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
uint16_t blockAlign = channels * bitsPerSample / 8;
uint8_t header[44] = {
'R','I','F','F',
0xFF,0xFF,0xFF,0x7F, // tamaño fake
'W','A','V','E',
'f','m','t',' ',
16,0,0,0,
1,0,
(uint8_t)channels,0,
(uint8_t)(sampleRate & 0xFF),(uint8_t)(sampleRate >> 8),
(uint8_t)(sampleRate >> 16),(uint8_t)(sampleRate >> 24),
(uint8_t)(byteRate & 0xFF),(uint8_t)(byteRate >> 8),
(uint8_t)(byteRate >> 16),(uint8_t)(byteRate >> 24),
(uint8_t)(blockAlign & 0xFF),(uint8_t)(blockAlign >> 8),
(uint8_t)bitsPerSample,0,
'd','a','t','a',
0xFF,0xFF,0xFF,0x7F
};
memcpy(buffer, header, 44);
return 44;
}
// Leer audio del I2S
size_t bytesRead = 0;
i2s_read(I2S_NUM_0, buffer, maxLen, &bytesRead, portMAX_DELAY);
//Serial.printf("Bytes read: %d\n", bytesRead);
// Aplicar ganancia
/*
int16_t *samples = (int16_t *)buffer;
size_t nSamples = bytesRead / 2;
for (size_t i = 0; i < nSamples; i++) {
int32_t s = samples[i] * gain;
if (s > 32767) s = 32767;
if (s < -32768) s = -32768;
samples[i] = (int16_t)s;
}
*/
return bytesRead;
});
request->send(response); });
server.begin();
}
void loop() {}
La conexión es:
ICS43434 | ESP32-S3 WROOM |
---|---|
SD | 6 |
SCK | 7 |
WS | 5 |
L/R | Ninguno |
GND | GND |
VDD | 3V3 |
En el navegador no sirve pero sí sirve con VLC > Medio > Abrir emisión de red > http://192.168.0.9/stream > reproducir.
Aquí 192.168.0.9
es la IP que se muestra en el monitor serial
Crear servidor stremeando audio y también reproduciendo audio
Notar que es como la combinación de los anteriores pero aquí
he usado el I2S_NUM_1
para el micrófono porque infiero que
el reproductor de audio usa el I2S_NUM_0
ya que antes de cambiarlo
me daba un error que decía: E (54214) I2S: i2s_write(2129): TX mode is not enabled
Y bueno, aquí el código. Por cierto, he notado que le pasa algo al stream de audio cuando reproduce una canción muy larga, desconozco la razón, pero el “hola” sí responde aunque más tardado de lo normal
#include <driver/i2s.h>
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
// Pines Micrófono
#define I2S_WS 5 // LRCK
#define I2S_SD 6 // DATA
#define I2S_SCK 7 // BCL
// Pines DAC
#define I2S_DOUT 11
#define I2S_BCLK 9
#define I2S_LRC 10
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
AsyncWebServer server(80);
void setup()
{
Serial.begin(115200);
out = new AudioOutputI2S();
out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
out->SetChannels(2);
out->SetGain(1);
// Configurar I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = 256,
.use_apll = false};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK,
.ws_io_num = I2S_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_SD};
i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_1, &pin_config);
bool ok = SD_MMC.setPins(39, 38, 40);
if (!SD_MMC.begin("/sdcard", true))
{
Serial.println("Error al inicializar la tarjeta SD.");
return;
}
Serial.println("Tarjeta SD inicializada correctamente.");
// Wifi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(300);
Serial.print('.');
}
Serial.println("\nWiFi connected");
Serial.print("Servidor corriendo en: http://");
Serial.println(WiFi.localIP());
server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
static File uploadFile;
if(index == 0){
//Serial.println("Comenzando upload de: " + filename);
uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
}
if(uploadFile){
uploadFile.write(data, len);
}
if(final){
uploadFile.close();
//Serial.println("Upload completado, iniciando reproducción...");
/*Aquí reproducimos pero solo quiero probar*/
// iniciar reproducción
if(file) delete file;
if(mp3) delete mp3;
file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
mp3 = new AudioGeneratorMP3();
mp3->begin(file, out);
} });
server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Hola mundo"); });
server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
{
AsyncWebServerResponse *response = request->beginChunkedResponse(
"audio/wav",
[](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
static bool headerSent = false;
//float gain = 2.0f; // 2x volumen = +6dB
if (!headerSent) {
headerSent = true;
// Header WAV "infinito"
uint32_t sampleRate = 16000;
uint16_t bitsPerSample = 16;
uint16_t channels = 1;
uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
uint16_t blockAlign = channels * bitsPerSample / 8;
uint8_t header[44] = {
'R','I','F','F',
0xFF,0xFF,0xFF,0x7F, // tamaño fake
'W','A','V','E',
'f','m','t',' ',
16,0,0,0,
1,0,
(uint8_t)channels,0,
(uint8_t)(sampleRate & 0xFF),(uint8_t)(sampleRate >> 8),
(uint8_t)(sampleRate >> 16),(uint8_t)(sampleRate >> 24),
(uint8_t)(byteRate & 0xFF),(uint8_t)(byteRate >> 8),
(uint8_t)(byteRate >> 16),(uint8_t)(byteRate >> 24),
(uint8_t)(blockAlign & 0xFF),(uint8_t)(blockAlign >> 8),
(uint8_t)bitsPerSample,0,
'd','a','t','a',
0xFF,0xFF,0xFF,0x7F
};
memcpy(buffer, header, 44);
return 44;
}
// Leer audio del I2S
size_t bytesRead = 0;
i2s_read(I2S_NUM_1, buffer, maxLen, &bytesRead, portMAX_DELAY);
//Serial.printf("Bytes read: %d\n", bytesRead);
// Aplicar ganancia
/*
int16_t *samples = (int16_t *)buffer;
size_t nSamples = bytesRead / 2;
for (size_t i = 0; i < nSamples; i++) {
int32_t s = samples[i] * gain;
if (s > 32767) s = 32767;
if (s < -32768) s = -32768;
samples[i] = (int16_t)s;
}
*/
return bytesRead;
});
request->send(response); });
server.begin();
}
void loop()
{
if (mp3 && mp3->isRunning())
{
if (!mp3->loop())
{
mp3->stop();
delete mp3;
delete file;
mp3 = nullptr;
file = nullptr;
}
}
}
Servidor que stremea y reproduce audio intentando al mismo tiempo
Es casi lo mismo que la anterior porque el stream se pausa o corta cuando se reproduce audio, pero al menos sigue conectado dicho stream
Por cierto, probé con una grabación de 172KB duración 20 segundos
y funciona bien. Realmente no sé cuánto pese una grabación real porque
esta la convertí de ogg a mp3 con ffmpeg -i grabación.ogg salida.mp3
ya que usé Telegram para grabar y enviar el audio.
#include <driver/i2s.h>
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
// Pines Micrófono
#define I2S_WS 5 // LRCK
#define I2S_SD 6 // DATA
#define I2S_SCK 7 // BCL
// Pines DAC
#define I2S_DOUT 11
#define I2S_BCLK 9
#define I2S_LRC 10
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
AsyncWebServer server(80);
TaskHandle_t TareaReproducirMP3;
volatile bool nuevaCancion = false;
void Task1code(void *parameter)
{
Serial.println("Ejecutando tarea");
while (1)
{
if (nuevaCancion)
{
if (file)
{
delete file;
file = nullptr;
}
if (mp3)
{
delete mp3;
mp3 = nullptr;
}
file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
mp3 = new AudioGeneratorMP3();
mp3->begin(file, out);
nuevaCancion = false;
}
if (mp3 && mp3->isRunning())
{
if (!mp3->loop())
{
mp3->stop();
delete mp3;
mp3 = nullptr;
delete file;
file = nullptr;
}
}
vTaskDelay(1);
}
}
void setup()
{
Serial.begin(115200);
out = new AudioOutputI2S();
out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
out->SetChannels(2);
out->SetGain(1);
// Configurar I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = 256,
.use_apll = false};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK,
.ws_io_num = I2S_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_SD};
i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_1, &pin_config);
bool ok = SD_MMC.setPins(39, 38, 40);
if (!SD_MMC.begin("/sdcard", true))
{
Serial.println("Error al inicializar la tarjeta SD.");
return;
}
Serial.println("Tarjeta SD inicializada correctamente.");
// Wifi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(300);
Serial.print('.');
}
Serial.println("\nWiFi connected");
Serial.print("Servidor corriendo en: http://");
Serial.println(WiFi.localIP());
server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
static File uploadFile;
if(index == 0){
//Serial.println("Comenzando upload de: " + filename);
uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
}
if(uploadFile){
uploadFile.write(data, len);
}
if(final){
uploadFile.close();
nuevaCancion = true;
//Serial.println("Upload completado, iniciando reproducción...");
/*Aquí reproducimos pero solo quiero probar*/
// iniciar reproducción
//if(file) delete file;
//if(mp3) delete mp3;
//file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
//mp3 = new AudioGeneratorMP3();
//mp3->begin(file, out);
} });
server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Hola mundo" + String(xPortGetCoreID())); });
server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
{
AsyncWebServerResponse *response = request->beginChunkedResponse(
"audio/wav",
[](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
static bool headerSent = false;
//float gain = 2.0f; // 2x volumen = +6dB
if (!headerSent) {
headerSent = true;
// Header WAV "infinito"
uint32_t sampleRate = 16000;
uint16_t bitsPerSample = 16;
uint16_t channels = 1;
uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
uint16_t blockAlign = channels * bitsPerSample / 8;
uint8_t header[44] = {
'R','I','F','F',
0xFF,0xFF,0xFF,0x7F, // tamaño fake
'W','A','V','E',
'f','m','t',' ',
16,0,0,0,
1,0,
(uint8_t)channels,0,
(uint8_t)(sampleRate & 0xFF),(uint8_t)(sampleRate >> 8),
(uint8_t)(sampleRate >> 16),(uint8_t)(sampleRate >> 24),
(uint8_t)(byteRate & 0xFF),(uint8_t)(byteRate >> 8),
(uint8_t)(byteRate >> 16),(uint8_t)(byteRate >> 24),
(uint8_t)(blockAlign & 0xFF),(uint8_t)(blockAlign >> 8),
(uint8_t)bitsPerSample,0,
'd','a','t','a',
0xFF,0xFF,0xFF,0x7F
};
memcpy(buffer, header, 44);
return 44;
}
// Leer audio del I2S
size_t bytesRead = 0;
i2s_read(I2S_NUM_1, buffer, maxLen, &bytesRead, portMAX_DELAY);
//Serial.printf("Bytes read: %d\n", bytesRead);
// Aplicar ganancia
/*
int16_t *samples = (int16_t *)buffer;
size_t nSamples = bytesRead / 2;
for (size_t i = 0; i < nSamples; i++) {
int32_t s = samples[i] * gain;
if (s > 32767) s = 32767;
if (s < -32768) s = -32768;
samples[i] = (int16_t)s;
}
*/
return bytesRead;
});
request->send(response); });
xTaskCreate(
Task1code,
"ReproducirMp3",
10000,
NULL,
0,
&TareaReproducirMP3);
server.begin();
}
void loop()
{
/*
if (mp3 && mp3->isRunning())
{
if (!mp3->loop())
{
mp3->stop();
delete mp3;
delete file;
mp3 = nullptr;
file = nullptr;
}
}
*/
}
Inicializar cámara (todavía no he tomado foto)
Intenté revisando el pinout y no pude, no funcionaba. Revisé todo y me salía:
E (109) cam_hal: cam_dma_config(301): frame buffer malloc failed
E (109) cam_hal: cam_config(385): cam_dma_config failed
E (109) gdma: gdma_disconnect(299): no peripheral is connected to the channel
E (116) camera: Camera config failed with error 0xffffffff
Me parece que esta tarjeta no trae PSRAM (aunque su modelo es N16R8 y la R es de 8 MB de RAM) así que hay que modificar otro ajuste:
config.fb_location = CAMERA_FB_IN_DRAM;
Con la ESP32-CAM ni siquiera toqué ese ajuste porque esa sí trae PSRAM, pero esta tarjeta, desafortunadamente, no.
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_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 10000000;
config.frame_size = FRAMESIZE_96X96;
config.pixel_format = PIXFORMAT_JPEG; // for streaming
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_DRAM;
config.jpeg_quality = 30;
config.fb_count = 1;
int res = esp_camera_init(&config);
Serial.printf("Init: %d\n", res);
Salida:
El pwdn -1
Init: 0
Y con eso ya pude hacerle el init correcto. Por cierto, los pines quedan así:
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 4
#define SIOD_GPIO_NUM 18
#define SIOC_GPIO_NUM 23
#define Y9_GPIO_NUM 36
#define Y8_GPIO_NUM 37
#define Y7_GPIO_NUM 38
#define Y6_GPIO_NUM 39
#define Y5_GPIO_NUM 35
#define Y4_GPIO_NUM 14
#define Y3_GPIO_NUM 13
#define Y2_GPIO_NUM 34
#define VSYNC_GPIO_NUM 5
#define HREF_GPIO_NUM 27
#define PCLK_GPIO_NUM 25
De tantos sketches que he visto ya ni sé en cuál de
ellos me basé, pero solo como pista, supuestamente esta
tarjeta tiene la CAMERA_MODEL_ESP_EYE
. Los pines NO
coinciden con el pinout de la tienda donde compré la tarjeta ,
ya que ahí los CAM_
(por ejemplo
CAM_Y8
) son distintos.
Solo como ejemplo, en el código donde sí funciona el init tengo
el Y9_GPIO_NUM
en 36 mientras que el pinout de la tienda
dice que es el GPIO16
y el SIOC_GPIO_NUM
en el código es 23
pero en la imagen del pinout el CAM_SIOC
es el GPIO5
Seguiré revisando para ver si puedo tomar la foto, ya que el init funciona hasta el momento pero no sé si lo hará el tomado de la foto.
El init no indica que todo está bien
Con el código de arriba no funcionaba la toma de foto con esp_camera_fb_get
ya que me devolvía NULL
así que me puse a depurar, al final terminé transcribiendo otra vez el pinout quedando así:
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 11;
config.pin_d1 = 9;
config.pin_d2 = 8;
config.pin_d3 = 10;
config.pin_d4 = 12;
config.pin_d5 = 18;
config.pin_d6 = 17;
config.pin_d7 = 16;
config.pin_xclk = 15;
config.pin_pclk = 13;
config.pin_vsync = 6;
config.pin_href = 7;
config.pin_sccb_sda = 4;
config.pin_sccb_scl = 5;
config.pin_pwdn = -1;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_CIF;
config.pixel_format = PIXFORMAT_GRAYSCALE;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_DRAM;
config.jpeg_quality = 30;
config.fb_count = 1;
RESET_GPIO_NUM
es -1
. Los demás pines los dejé así como
lo dice la imagen del pinout.
Nota que ahora no me voy a fijar en calidad de foto, solo quiero una foto en cualquier formato y saber si la cámara funciona.
Pues lo siguiente para saber si la conexión es correcta es obtener información del sensor. Veamos el siguiente fragmento de código:
sensor_t *s = esp_camera_sensor_get();
if (!s) {
Serial.println("No se pudo obtener el sensor!");
return;
}
Intencionalmente desconecté la cámara y me da justamente ese error junto con algo como un error de memoria:
E (1028) camera: Camera probe failed with error 0x105(ESP_ERR_NOT_FOUND)
E (1028) gdma: gdma_disconnect(299): no peripheral is connected to the channel
Es distinto a ok
No se pudo obtener el sensor!
Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.
Core 1 register dump:
PC : 0x4200223f PS : 0x00060630 A0 : 0x82004079 A1 : 0x3fcebea0
A2 : 0x3fc96814 A3 : 0x00000000 A4 : 0x00000014 A5 : 0x00000004
A6 : 0x3fcec0c0 A7 : 0x80000001 A8 : 0x8200223f A9 : 0x3fcebe80
A10 : 0x00000000 A11 : 0x00000000 A12 : 0xf56d182c A13 : 0x3fcebe3c
A14 : 0x3fc96b64 A15 : 0x3fcebe3c SAR : 0x0000001a EXCCAUSE: 0x0000001c
EXCVADDR: 0x00000002 LBEG : 0x400556d5 LEND : 0x400556e5 LCOUNT : 0xffffffff
Backtrace: 0x4200223c:0x3fcebea0 0x42004076:0x3fcebf40
El mensaje de Es distinto a ok
es por lo siguiente:
int res = esp_camera_init(&config);
if (res != ESP_OK)
{
Serial.printf("Es distinto a ok");
}
Y el del sensor ya lo expliqué más arriba. Como lo dije, esto fue intencional para saber si realmente detecta errores. Volví a conectar la cámara, reinicié y ahora sí puse todo en la función:
void checkCamera() {
sensor_t *s = esp_camera_sensor_get();
if (!s) {
Serial.println("No se pudo obtener el sensor!");
return;
}
Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
La salida es:
Sensor PID: 0x26
Sensor VER: 0x42
Sensor MIDH: 0x7f, MIDL: 0xa2
Así que no basta con que el esp_camera_init
te devuelva ESP_OK
, también
es importante revisar el sensor solo para saber si la conexión es correcta.
Hay que invocar a checkCamera
después de esp_camera_init
obviamente.
Solo para comprobar que realmente todo está ok, voy a modificar la configuración y poner lo siguiente:
config.pin_sccb_sda = 18; // Originalmente es 4
Me da la siguiente salida:
E (1028) camera: Camera probe failed with error 0x105(ESP_ERR_NOT_FOUND)
E (1028) gdma: gdma_disconnect(299): no peripheral is connected to the channel
Es distinto a okNo se pudo obtener el sensor!
Guru Meditation Error: Core 1 panic'ed (LoadProhibited). Exception was unhandled.
Core 1 register dump:
PC : 0x420021e7 PS : 0x00060630 A0 : 0x82003fd9 A1 : 0x3fcebea0
A2 : 0x3fc96810 A3 : 0x00000000 A4 : 0x00000014 A5 : 0x00000004
A6 : 0x3fcec0c0 A7 : 0x80000001 A8 : 0x820021e7 A9 : 0x3fcebe80
A10 : 0x00000000 A11 : 0x3fc96810 A12 : 0x7a7bcd8e A13 : 0x3fcebe3c
A14 : 0x3fc96b60 A15 : 0x3fcebe3c SAR : 0x0000001a EXCCAUSE: 0x0000001c
EXCVADDR: 0x00000002 LBEG : 0x400556d5 LEND : 0x400556e5 LCOUNT : 0xffffffff
Backtrace: 0x420021e4:0x3fcebea0 0x42003fd6:0x3fcebf40
Así que sí: esp_camera_init
y esp_camera_sensor_get
en conjunto nos ayudan a
descartar errores.
Lo único que queda es probar si la foto es correcta.
Seguimos con pruebas: tomar foto y ver size
Sé que lo más obvio sería guardar en la SD o mostrar la foto en un servidor web, pero vamos paso por paso ya que no quiero cometer un error. Me ha costado llegar hasta aquí.
No sé si el problema está en la cantidad de RAM, por ello es que quiero hacer pruebas muy mínimas.
El siguiente código ya funciona y me muestra el tamaño del búfer. Es un código limpio donde ya se puede ver la conexión correcta que al menos no da error para el sensor ni para el init:
#include "esp_camera.h"
#include "Arduino.h"
void checkCamera()
{
sensor_t *s = esp_camera_sensor_get();
if (!s)
{
Serial.println("No se pudo obtener el sensor!");
return;
}
Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void setup()
{
Serial.begin(115200);
Serial.setDebugOutput(true);
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 11;
config.pin_d1 = 9;
config.pin_d2 = 8;
config.pin_d3 = 10;
config.pin_d4 = 12;
config.pin_d5 = 18;
config.pin_d6 = 17;
config.pin_d7 = 16;
config.pin_xclk = 15;
config.pin_pclk = 13;
config.pin_vsync = 6;
config.pin_href = 7;
config.pin_sccb_sda = 4;
config.pin_sccb_scl = 5;
config.pin_pwdn = -1;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_CIF;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_DRAM;
config.jpeg_quality = 30;
config.fb_count = 1;
int res = esp_camera_init(&config);
if (res != ESP_OK)
{
Serial.printf("Es distinto a ok");
}
checkCamera();
camera_fb_t *b = esp_camera_fb_get();
if (b == NULL)
{
Serial.println("b es null!");
}
else
{
Serial.printf("Foto funciona hasta el momento. A ver el size del buffer? %d\n", b->len);
}
}
void loop()
{
}
La salida es:
Sensor PID: 0x26
Sensor VER: 0x42
Sensor MIDH: 0x7f, MIDL: 0xa2
Foto funciona hasta el momento. A ver el size del buffer? 5096
Y siempre me da medidas aproximadas a 5KB.
Mostrar una foto en servidor web
Hagamos un servidor web que muestre la foto a ver qué sale. El código queda así:
#include "esp_camera.h"
#include "Arduino.h"
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <WiFi.h>
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
AsyncWebServer server(80);
void handlePhoto(AsyncWebServerRequest *request)
{
camera_fb_t *fb = esp_camera_fb_get();
if (!fb)
{
request->send(500, "text/plain", "No se pudo capturar la foto");
return;
}
AsyncWebServerResponse *response = request->beginResponse_P(
200, "image/jpeg", fb->buf, fb->len);
response->addHeader("Content-Disposition", "inline; filename=capture.jpg");
request->send(response);
esp_camera_fb_return(fb);
}
void checkCamera()
{
sensor_t *s = esp_camera_sensor_get();
if (!s)
{
Serial.println("No se pudo obtener el sensor!");
return;
}
Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void setup()
{
Serial.begin(115200);
Serial.setDebugOutput(true);
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 11;
config.pin_d1 = 9;
config.pin_d2 = 8;
config.pin_d3 = 10;
config.pin_d4 = 12;
config.pin_d5 = 18;
config.pin_d6 = 17;
config.pin_d7 = 16;
config.pin_xclk = 15;
config.pin_pclk = 13;
config.pin_vsync = 6;
config.pin_href = 7;
config.pin_sccb_sda = 4;
config.pin_sccb_scl = 5;
config.pin_pwdn = -1;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_CIF;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_DRAM;
config.jpeg_quality = 30;
config.fb_count = 1;
int res = esp_camera_init(&config);
if (res != ESP_OK)
{
Serial.printf("Es distinto a ok");
}
checkCamera();
WiFi.begin(ssid, password);
Serial.printf("Conectando a %s", ssid);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.printf("\nConectado! IP: %s\n", WiFi.localIP().toString().c_str());
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/html",
"<h1>ESP32-S3 Cam</h1><img src='/photo'>"); });
server.on("/photo", HTTP_GET, handlePhoto);
server.begin();
}
void loop()
{
}
Lo cargué, apunté a mi laptop y aquí tenemos lo más esperado:
Así que la cámara, la conexión y todo lo demás funciona. Ya a partir de aquí solo es cuestión de ajustar todos los parámetros para obtener la mejor calidad.
Solo para que quede el registro. Cambié el grab_mode
y el pixel_format
así:
config.frame_size = FRAMESIZE_UXGA;
config.grab_mode = CAMERA_GRAB_LATEST;
Me dio el siguiente error:
E (108) cam_hal: cam_dma_config(301): frame buffer malloc failed
E (108) cam_hal: cam_config(385): cam_dma_config failed
E (108) gdma: gdma_disconnect(299): no peripheral is connected to the channel
E (114) camera: Camera config failed with error 0xffffffff
Es distinto a okNo se pudo obtener el sensor!
Me imagino que es por el tamaño de la foto. Lo cambié a:
config.frame_size = FRAMESIZE_XGA;
Pero me daba fotos mal formadas (aunque eso sí, en mejor calidad), por ejemplo:
Imagino que es el grab_mode
, voy a probar.
Después de cambiarlo la foto sigue igual. Voy a bajar la calidad a FRAMESIZE_SVGA
dejando config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
.
Sigue igual. Voy a bajarla de nuevo a FRAMESIZE_CIF
a ver si así ya respeta.
Correcto, con FRAMESIZE_CIF
ya muestra imágenes bien formadas.
- Probando con HVGA (480x320). También funciona.
- Probando con VGA (640x480). Salen con colores distintos y malformadas
Entonces parece que el máximo es 480x320. Eso sí: nunca bajé la calidad JPG, siempre estuvo en 30 y recordemos que entre más bajo es mayor calidad.
Bajé la calidad a 45 y lo dejé en VGA. El resultado es el mismo: fotos malformadas la mayoría de veces aunque el servidor muestra la foto más rápido.
Stremear cámara
He probado con los siguientes ajustes, no va tan rápido como uno quisiera pero al menos sí se ve decente. No he probado el máximo pero imagino que esto roza el límite.
#include "esp_camera.h"
#include "Arduino.h"
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <WiFi.h>
const char *ssid = "Tu ssid";
const char *password = "Tu contraseña";
AsyncWebServer server(80);
void checkCamera()
{
sensor_t *s = esp_camera_sensor_get();
if (!s)
{
Serial.println("No se pudo obtener el sensor!");
return;
}
Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void setup()
{
Serial.begin(115200);
Serial.setDebugOutput(true);
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 11;
config.pin_d1 = 9;
config.pin_d2 = 8;
config.pin_d3 = 10;
config.pin_d4 = 12;
config.pin_d5 = 18;
config.pin_d6 = 17;
config.pin_d7 = 16;
config.pin_xclk = 15;
config.pin_pclk = 13;
config.pin_vsync = 6;
config.pin_href = 7;
config.pin_sccb_sda = 4;
config.pin_sccb_scl = 5;
config.pin_pwdn = -1;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_VGA;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
config.fb_location = CAMERA_FB_IN_DRAM;
config.jpeg_quality = 45;
config.fb_count = 1;
int res = esp_camera_init(&config);
if (res != ESP_OK)
{
Serial.printf("Es distinto a ok");
}
checkCamera();
WiFi.begin(ssid, password);
Serial.printf("Conectando a %s", ssid);
while (WiFi.status() != WL_CONNECTED)
{
delay(500);
Serial.print(".");
}
Serial.printf("\nConectado! IP: %s\n", WiFi.localIP().toString().c_str());
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/html",
"<h1>ESP32-S3 Cam</h1><img src='/photo'>"); });
server.on("/photo", HTTP_GET, [](AsyncWebServerRequest *request)
{
struct MJPEGState {
camera_fb_t *fb = nullptr;
size_t hdr_pos = 0;
size_t jpg_pos = 0;
bool tail_sent = false;
std::string header;
};
auto state = std::make_shared<MJPEGState>();
auto cb = [state](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
if (!state->fb) {
state->fb = esp_camera_fb_get();
if (!state->fb) return 0;
char h[128];
int hlen = snprintf(h, sizeof(h),
"--frame\r\n"
"Content-Type: image/jpeg\r\n"
"Content-Length: %u\r\n\r\n",
(unsigned)state->fb->len);
state->header.assign(h, hlen);
state->hdr_pos = 0;
state->jpg_pos = 0;
state->tail_sent = false;
}
if (state->hdr_pos < state->header.size()) {
size_t toCopy = std::min(maxLen, state->header.size() - state->hdr_pos);
memcpy(buffer, state->header.data() + state->hdr_pos, toCopy);
state->hdr_pos += toCopy;
return toCopy;
}
if (state->jpg_pos < state->fb->len) {
size_t rem = state->fb->len - state->jpg_pos;
size_t toCopy = std::min(maxLen, rem);
memcpy(buffer, state->fb->buf + state->jpg_pos, toCopy);
state->jpg_pos += toCopy;
return toCopy;
}
if (!state->tail_sent) {
if (maxLen < 2) return 0;
buffer[0] = '\r';
buffer[1] = '\n';
state->tail_sent = true;
esp_camera_fb_return(state->fb);
state->fb = nullptr;
return 2;
}
return 0;
};
AsyncWebServerResponse *response = request->beginChunkedResponse(
"multipart/x-mixed-replace; boundary=frame", cb);
request->send(response); });
server.begin();
}
void loop()
{
}
Enviar foto a Telegram cuando tocan un pin, stremear micrófono y reproducir audio
Este código:
- Envía una foto a Telegram cuando el usuario toca el pin 14
- Stremea el micrófono en ip/stream
- Reproduce MP3 subidos a /upload
Hay que calibrar bien el valor de touchRead
porque a veces dice
que alguien lo está tocando cuando no es cierto.
Primero lo dejé en 20 mil, pero luego decidí dejarlo en 30 mil (30000) y ya no me dio errores. Lo mejor es calibrarlo tú mismo.
#include <driver/i2s.h>
#include <WiFiClientSecure.h>
#include "esp_camera.h"
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
// Pines Micrófono
#define I2S_WS 1
#define I2S_SD 2
#define I2S_SCK 42
// Pines DAC
#define I2S_DOUT 48
#define I2S_BCLK 47
#define I2S_LRC 21
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
const char *telegramHost = "api.telegram.org";
String botToken = "tu token de Telegram";
String chatID = "tu id de chat";
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
AsyncWebServer server(80);
TaskHandle_t TareaReproducirMP3;
volatile bool nuevaCancion = false;
void checkCamera()
{
sensor_t *s = esp_camera_sensor_get();
if (!s)
{
Serial.println("No se pudo obtener el sensor!");
return;
}
Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void sendPhoto()
{
Serial.println("Enviando foto");
/*Descartamos el primero simplemente*/
camera_fb_t *fb = esp_camera_fb_get();
if (fb)
{
Serial.println("Descartado!");
esp_camera_fb_return(fb);
}
// Tomamos el real
Serial.println("Tomando real");
fb = esp_camera_fb_get();
if (!fb)
{
Serial.println("Error al tomar foto");
return;
}
WiFiClientSecure client;
client.setInsecure();
if (!client.connect(telegramHost, 443))
{
Serial.println("Conexión fallida");
esp_camera_fb_return(fb);
return;
}
String boundary = "----ESP32CamBoundary";
// Parte inicial (chat_id y encabezado de archivo)
String head = "--" + boundary + "\r\n"
"Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n" +
chatID + "\r\n"
"--" +
boundary + "\r\n"
"Content-Disposition: form-data; name=\"photo\"; filename=\"esp32s3.jpg\"\r\n"
"Content-Type: image/jpeg\r\n\r\n";
String tail = "\r\n--" + boundary + "--\r\n";
uint32_t totalLen = head.length() + fb->len + tail.length();
// Headers HTTP
client.println("POST /bot" + botToken + "/sendPhoto HTTP/1.1");
client.println("Host: " + String(telegramHost));
client.println("Content-Type: multipart/form-data; boundary=" + boundary);
client.println("Content-Length: " + String(totalLen));
client.println("Connection: close");
client.println();
// Cuerpo
client.print(head);
client.write(fb->buf, fb->len);
client.print(tail);
esp_camera_fb_return(fb);
// Leer respuesta
String response;
while (client.connected())
{
while (client.available())
{
char c = client.read();
response += c;
}
}
Serial.println("Respuesta Telegram:");
Serial.println(response);
}
void Task1code(void *parameter)
{
Serial.println("Ejecutando tarea");
while (1)
{
if (nuevaCancion)
{
if (file)
{
delete file;
file = nullptr;
}
if (mp3)
{
delete mp3;
mp3 = nullptr;
}
file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
mp3 = new AudioGeneratorMP3();
mp3->begin(file, out);
nuevaCancion = false;
}
if (mp3 && mp3->isRunning())
{
if (!mp3->loop())
{
mp3->stop();
delete mp3;
mp3 = nullptr;
delete file;
file = nullptr;
}
else
{
vTaskDelay(10);
}
}
}
}
void setup()
{
Serial.begin(115200);
out = new AudioOutputI2S();
out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
out->SetChannels(2);
out->SetGain(1);
// Configurar I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = 256,
.use_apll = false};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK,
.ws_io_num = I2S_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_SD};
i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_1, &pin_config);
bool ok = SD_MMC.setPins(39, 38, 40);
if (!SD_MMC.begin("/sdcard", true))
{
Serial.println("Error al inicializar la tarjeta SD.");
return;
}
Serial.println("Tarjeta SD inicializada correctamente.");
// Wifi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(300);
Serial.print('.');
}
Serial.println("\nWiFi connected");
Serial.print("Servidor corriendo en: http://");
Serial.println(WiFi.localIP());
checkCamera();
// Cámara
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 11;
config.pin_d1 = 9;
config.pin_d2 = 8;
config.pin_d3 = 10;
config.pin_d4 = 12;
config.pin_d5 = 18;
config.pin_d6 = 17;
config.pin_d7 = 16;
config.pin_xclk = 15;
config.pin_pclk = 13;
config.pin_vsync = 6;
config.pin_href = 7;
config.pin_sccb_sda = 4;
config.pin_sccb_scl = 5;
config.pin_pwdn = -1;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_CIF;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_LATEST;
config.fb_location = CAMERA_FB_IN_DRAM;
config.jpeg_quality = 45;
config.fb_count = 1;
int res = esp_camera_init(&config);
if (res != ESP_OK)
{
Serial.printf("Es distinto a ok");
}
// Termina cámara
server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
static File uploadFile;
if(index == 0){
//Serial.println("Comenzando upload de: " + filename);
uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
}
if(uploadFile){
uploadFile.write(data, len);
}
if(final){
uploadFile.close();
nuevaCancion = true;
//Serial.println("Upload completado, iniciando reproducción...");
/*Aquí reproducimos pero solo quiero probar*/
// iniciar reproducción
//if(file) delete file;
//if(mp3) delete mp3;
//file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
//mp3 = new AudioGeneratorMP3();
//mp3->begin(file, out);
} });
server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Hola mundo" + String(xPortGetCoreID())); });
server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
{
AsyncWebServerResponse *response = request->beginChunkedResponse(
"audio/wav",
[](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
static bool headerSent = false;
//float gain = 2.0f; // 2x volumen = +6dB
if (!headerSent) {
headerSent = true;
// Header WAV "infinito"
uint32_t sampleRate = 16000;
uint16_t bitsPerSample = 16;
uint16_t channels = 1;
uint32_t byteRate = sampleRate * channels * bitsPerSample / 8;
uint16_t blockAlign = channels * bitsPerSample / 8;
uint8_t header[44] = {
'R','I','F','F',
0xFF,0xFF,0xFF,0x7F, // tamaño fake
'W','A','V','E',
'f','m','t',' ',
16,0,0,0,
1,0,
(uint8_t)channels,0,
(uint8_t)(sampleRate & 0xFF),(uint8_t)(sampleRate >> 8),
(uint8_t)(sampleRate >> 16),(uint8_t)(sampleRate >> 24),
(uint8_t)(byteRate & 0xFF),(uint8_t)(byteRate >> 8),
(uint8_t)(byteRate >> 16),(uint8_t)(byteRate >> 24),
(uint8_t)(blockAlign & 0xFF),(uint8_t)(blockAlign >> 8),
(uint8_t)bitsPerSample,0,
'd','a','t','a',
0xFF,0xFF,0xFF,0x7F
};
memcpy(buffer, header, 44);
return 44;
}
// Leer audio del I2S
size_t bytesRead = 0;
i2s_read(I2S_NUM_1, buffer, maxLen, &bytesRead, portMAX_DELAY);
//Serial.printf("Bytes read: %d\n", bytesRead);
// Aplicar ganancia
/*
int16_t *samples = (int16_t *)buffer;
size_t nSamples = bytesRead / 2;
for (size_t i = 0; i < nSamples; i++) {
int32_t s = samples[i] * gain;
if (s > 32767) s = 32767;
if (s < -32768) s = -32768;
samples[i] = (int16_t)s;
}
*/
return bytesRead;
});
request->send(response); });
xTaskCreate(
Task1code,
"ReproducirMp3",
10000,
NULL,
0,
&TareaReproducirMP3);
server.begin();
}
void loop()
{
if (touchRead(14) > 20000)
{
sendPhoto();
}
/*
if (mp3 && mp3->isRunning())
{
if (!mp3->loop())
{
mp3->stop();
delete mp3;
delete file;
mp3 = nullptr;
file = nullptr;
}
}
*/
}
Falta revisar bien el stream del micrófono pues cuando se desconecta es un poco complejo volver a conectar
Optimizando un poco lo anterior
Aumentando el delay de la reproducción de audio. Me parece que 15 es el límite, ya que si vas más abajo es perceptible para el oído.
También aumenté la calidad y resolución de la cámara al máximo, cambié el umbral de activación del pin y no recuerdo si fue aquí o antes que manejo las desconexiones para enviar el header wav siempre que se conecta alguien nuevo
#include <driver/i2s.h>
#include <WiFiClientSecure.h>
#include "esp_camera.h"
#include <Arduino.h>
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <SD_MMC.h>
#include "AudioFileSourceFS.h"
#include "AudioGeneratorMP3.h"
#include "AudioOutputI2S.h"
// Pines Micrófono
// Micrófono funciona bien con esta configuración
#define I2S_WS 1 // LRCK Nuevo: 1
#define I2S_SD 2 // DATA nuevo 2
#define I2S_SCK 42 // BCL nuevo 42
// Pines DAC
#define I2S_DOUT 48 // Nuevo 47
#define I2S_BCLK 47 // Nuevo 21
#define I2S_LRC 21 // Nuevo 20
#define UMBRAL_SENSOR_TOUCH 30000
const int SAMPLE_RATE = 16000;
const int BITS_PER_SAMPLE = 16;
const int CHANNELS = 1;
const char *ssid = "tu ssid";
const char *password = "tu contraseña";
const char *telegramHost = "api.telegram.org";
String botToken = "tu token";
String chatID = "tu id de chat";
AudioFileSourceFS *file;
AudioGeneratorMP3 *mp3;
AudioOutputI2S *out;
AsyncWebServer server(80);
TaskHandle_t TareaReproducirMP3;
volatile bool nuevaCancion = false;
void checkCamera()
{
sensor_t *s = esp_camera_sensor_get();
if (!s)
{
Serial.println("No se pudo obtener el sensor!");
return;
}
Serial.printf("Sensor PID: 0x%x\n", s->id.PID);
Serial.printf("Sensor VER: 0x%x\n", s->id.VER);
Serial.printf("Sensor MIDH: 0x%x, MIDL: 0x%x\n", s->id.MIDH, s->id.MIDL);
}
void sendPhoto()
{
Serial.println("Enviando foto");
/*Descartamos el primero simplemente*/
camera_fb_t *fb = esp_camera_fb_get();
if (fb)
{
Serial.println("Descartado!");
esp_camera_fb_return(fb);
}
// Tomamos el real
Serial.println("Tomando real");
fb = esp_camera_fb_get();
if (!fb)
{
Serial.println("Error al tomar foto");
return;
}
WiFiClientSecure client;
client.setInsecure();
if (!client.connect(telegramHost, 443))
{
Serial.println("Conexión fallida");
esp_camera_fb_return(fb);
return;
}
String boundary = "----ESP32CamBoundary";
// Parte inicial (chat_id y encabezado de archivo)
String head = "--" + boundary + "\r\n"
"Content-Disposition: form-data; name=\"chat_id\"\r\n\r\n" +
chatID + "\r\n"
"--" +
boundary + "\r\n"
"Content-Disposition: form-data; name=\"photo\"; filename=\"esp32s3.jpg\"\r\n"
"Content-Type: image/jpeg\r\n\r\n";
String tail = "\r\n--" + boundary + "--\r\n";
uint32_t totalLen = head.length() + fb->len + tail.length();
// Headers HTTP
client.println("POST /bot" + botToken + "/sendPhoto HTTP/1.1");
client.println("Host: " + String(telegramHost));
client.println("Content-Type: multipart/form-data; boundary=" + boundary);
client.println("Content-Length: " + String(totalLen));
client.println("Connection: close");
client.println();
// Cuerpo
client.print(head);
client.write(fb->buf, fb->len);
client.print(tail);
esp_camera_fb_return(fb);
// Leer respuesta
String response;
while (client.connected())
{
while (client.available())
{
char c = client.read();
response += c;
}
}
Serial.println("Respuesta Telegram:");
Serial.println(response);
}
void Task1code(void *parameter)
{
Serial.println("Ejecutando tarea");
while (1)
{
if (nuevaCancion)
{
if (file)
{
delete file;
file = nullptr;
}
if (mp3)
{
delete mp3;
mp3 = nullptr;
}
file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
mp3 = new AudioGeneratorMP3();
mp3->begin(file, out);
nuevaCancion = false;
}
if (mp3 && mp3->isRunning())
{
if (!mp3->loop())
{
mp3->stop();
delete mp3;
mp3 = nullptr;
delete file;
file = nullptr;
}
else
{
vTaskDelay(15);
}
}
}
}
void setup()
{
Serial.begin(115200);
out = new AudioOutputI2S();
out->SetPinout(I2S_BCLK, I2S_LRC, I2S_DOUT);
out->SetChannels(2);
out->SetGain(1);
// Configurar I2S
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = 256,
.use_apll = false};
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_SCK,
.ws_io_num = I2S_WS,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_SD};
i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
i2s_set_pin(I2S_NUM_1, &pin_config);
bool ok = SD_MMC.setPins(39, 38, 40);
if (!SD_MMC.begin("/sdcard", true))
{
Serial.println("Error al inicializar la tarjeta SD.");
return;
}
Serial.println("Tarjeta SD inicializada correctamente.");
// Wifi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
{
delay(300);
Serial.print('.');
}
Serial.println("\nWiFi connected");
Serial.print("Servidor corriendo en: http://");
Serial.println(WiFi.localIP());
checkCamera();
// Cámara
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 11;
config.pin_d1 = 9;
config.pin_d2 = 8;
config.pin_d3 = 10;
config.pin_d4 = 12;
config.pin_d5 = 18;
config.pin_d6 = 17;
config.pin_d7 = 16;
config.pin_xclk = 15;
config.pin_pclk = 13;
config.pin_vsync = 6;
config.pin_href = 7;
config.pin_sccb_sda = 4;
config.pin_sccb_scl = 5;
config.pin_pwdn = -1;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.frame_size = FRAMESIZE_SVGA;
config.pixel_format = PIXFORMAT_JPEG;
config.grab_mode = CAMERA_GRAB_LATEST;
config.fb_location = CAMERA_FB_IN_DRAM;
config.jpeg_quality = 25; // Originalmente 30, ya vi que con 25 sí aguanta
config.fb_count = 1;
int res = esp_camera_init(&config);
if (res != ESP_OK)
{
Serial.printf("Es distinto a ok");
}
// Termina cámara
server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Archivo recibido"); }, [](AsyncWebServerRequest *request, String filename, size_t index, uint8_t *data, size_t len, bool final)
{
static File uploadFile;
if(index == 0){
//Serial.println("Comenzando upload de: " + filename);
uploadFile = SD_MMC.open("/uploaded.mp3", FILE_WRITE);
}
if(uploadFile){
uploadFile.write(data, len);
}
if(final){
uploadFile.close();
nuevaCancion = true;
//Serial.println("Upload completado, iniciando reproducción...");
/*Aquí reproducimos pero solo quiero probar*/
// iniciar reproducción
//if(file) delete file;
//if(mp3) delete mp3;
//file = new AudioFileSourceFS(SD_MMC, "/uploaded.mp3");
//mp3 = new AudioGeneratorMP3();
//mp3->begin(file, out);
} });
server.on("/hola", HTTP_GET, [](AsyncWebServerRequest *request)
{ request->send(200, "text/plain", "Hola mundo" + String(xPortGetCoreID())); });
server.on("/stream", HTTP_GET, [](AsyncWebServerRequest *request)
{
static bool headerSent = false;
auto *response = request->beginChunkedResponse(
"audio/wav",
[](uint8_t *buffer, size_t maxLen, size_t index) -> size_t {
static const uint8_t header[44] = {
'R','I','F','F',
0xFF,0xFF,0xFF,0x7F,
'W','A','V','E',
'f','m','t',' ',
16,0,0,0,
1,0,
1,0, // canales
0x80,0x3E,0,0, // 16000 Hz
0x00,0x7D,0,0, // byteRate = 32000
2,0, // blockAlign
16,0, // bits/sample
'd','a','t','a',
0xFF,0xFF,0xFF,0x7F
};
if (!headerSent) {
headerSent = true;
memcpy(buffer, header, 44);
return 44;
}
size_t bytesRead = 0;
i2s_read(I2S_NUM_1, buffer, maxLen, &bytesRead, 10 / portTICK_PERIOD_MS);
return bytesRead;
});
request->onDisconnect([&]() {
headerSent = false;
});
request->send(response); });
xTaskCreate(
Task1code,
"ReproducirMp3",
10000,
NULL,
0,
&TareaReproducirMP3);
server.begin();
}
void loop()
{
// Serial.printf("touch read es %d\n", touchRead(14));
if (touchRead(14) > UMBRAL_SENSOR_TOUCH)
{
sendPhoto();
}
/*
if (mp3 && mp3->isRunning())
{
if (!mp3->loop())
{
mp3->stop();
delete mp3;
delete file;
mp3 = nullptr;
file = nullptr;
}
}
*/
}
Conclusión
Me parece que es una tarjeta con muchas características muy buenas, pero sin PSRAM no vamos a poder sacarle mucho partido a la cámara.
Comparado con la ESP32-CAM que tiene 4MB de RAM, esta se queda corta. Una ventaja que le puedo ver es que tiene más pines disponibles que la ESP32-CAM.