You dont have javascript enabled! Please enable it!

Caso práctico: Pan-Tilt WebSocket OV2640 ESP32-CAM y PCA9685

Caso práctico: Pan-Tilt WebSocket OV2640 ESP32-CAM y PCA9685 — hero

Objetivo y caso de uso

Qué construirás: Un sistema de streaming de video utilizando ESP32-CAM y control de pan-tilt mediante PCA9685.

Para qué sirve

  • Monitoreo remoto de espacios utilizando video en tiempo real.
  • Control de cámaras en aplicaciones de robótica y drones.
  • Implementación de sistemas de seguridad con visualización dinámica.
  • Proyectos de domótica para vigilancia y control de áreas específicas.

Resultado esperado

  • Streaming de video a 30 FPS con latencia inferior a 200 ms.
  • Control de posición del servo con precisión de 1 grado.
  • Conexión estable a través de WebSocket con menos de 5% de pérdida de paquetes.
  • Tiempo de respuesta en el control de pan-tilt menor a 100 ms.

Público objetivo: Desarrolladores y entusiastas de IoT; Nivel: Avanzado

Arquitectura/flujo: Comunicación entre ESP32-CAM y servidor WebSocket, control de servos mediante PCA9685.

Nivel: Avanzado

Prerrequisitos

Sistemas operativos soportados y herramientas

  • Windows 10/11 (64-bit)
  • macOS 12/13/14 (Intel o Apple Silicon)
  • Ubuntu 22.04 LTS / Debian 12

Toolchain exacta (versionado cerrado)

  • IDE/CLI:
  • Visual Studio Code 1.93.x (opcional, recomendado para edición)
  • PlatformIO Core 6.1.12 (CLI)
  • Paquetes PlatformIO:
  • Plataforma: espressif32@6.7.0
  • Framework Arduino: framework-arduinoespressif32@3.20014.0 (equivale a Arduino-ESP32 2.0.14)
  • Compilador: toolchain-xtensa32 provisto por espressif32@6.7.0 (instalado automáticamente por PlatformIO)
  • Librerías (resueltas por PlatformIO):
  • me-no-dev/AsyncTCP@1.1.1
  • me-no-dev/ESP Async WebServer@1.2.3
  • adafruit/Adafruit PWM Servo Driver Library@2.4.2
  • adafruit/Adafruit BusIO@1.14.5
  • Python 3.11.x (requerido por PlatformIO Core)
  • Controladores USB-UART (según tu adaptador):
  • CP210x (Silicon Labs) o CH340/CH34x (WCH). Instala el driver oficial correspondiente si tu sistema no lo reconoce de forma nativa.

Instalación rápida de PlatformIO Core (CLI)

  • Windows/macOS/Linux (asumiendo Python 3.11 disponible en PATH):
  • Instalar:
    pip install -U platformio==6.1.12
  • Verificar:
    pio --version
    Debe mostrar: PlatformIO Core, version 6.1.12

Materiales

  • 1x ESP32-CAM (OV2640) — modelo AI-Thinker con cámara OV2640 integrada
  • 1x Módulo PCA9685 16 canales (servo driver I2C)
  • 2x Servos para pan-tilt (p. ej., SG90/MG90S). Si usas un soporte mecánico pan-tilt, típicamente canal 0 = pan, canal 1 = tilt
  • 1x Adaptador USB–UART 3.3 V (FTDI/CP2102/CH340) para programar la ESP32-CAM
  • 1x Fuente 5 V 2 A (para alimentar servos vía V+ del PCA9685)
  • Protoboard o base de conexiones y cables Dupont
  • 2–3 jumpers para modo de programación:
  • GPIO0 a GND (para entrar en bootloader)
  • RST a GND (botón o puente momentáneo para reinicio)
  • Cables para I2C:
  • SDA, SCL entre ESP32-CAM y PCA9685
  • Cables para alimentación:
  • VCC (3.3 V), 5 V, GND
  • Opcional: Estructura mecánica pan-tilt (Smart Car PTZ o similar)

Nota: Este caso práctico está centrado estrictamente en el modelo “ESP32-CAM (OV2640) + PCA9685 servo driver” y el objetivo “ov2640-websocket-pan-tilt”.

Preparación y conexión

En la ESP32-CAM (AI-Thinker), la cámara utiliza una serie de GPIOs internos; evitaremos usar la ranura microSD para poder disponer de GPIO14 y GPIO15 como bus I2C. No usaremos la microSD en este proyecto.

  • Bus I2C elegido:
  • SDA → GPIO15 de la ESP32-CAM
  • SCL → GPIO14 de la ESP32-CAM

  • Conexiones USB–UART para programación:

  • U0R (GPIO3) ← TX del adaptador USB–UART
  • U0T (GPIO1) → RX del adaptador USB–UART
  • 5V (ESP32-CAM) ← 5V del adaptador (si entrega suficiente) o fuente externa
  • GND (ESP32-CAM) ← GND del adaptador
  • GPIO0 ↔ GND (para entrar en modo de programación)
  • RST ↔ GND (pulsar momentáneamente para reiniciar)

  • PCA9685:

  • VCC (lógica) ← 3.3 V de la ESP32-CAM (acepta 3.3–5 V; usar 3.3 V para lógica segura)
  • V+ (potencia servos) ← 5 V de la fuente externa
  • GND ← GND común (ESP32-CAM, adaptador USB–UART, fuente 5 V)
  • SDA ← GPIO15 (ESP32-CAM)
  • SCL ← GPIO14 (ESP32-CAM)
  • Canales PWM:
    • CH0 (PCA9685) → Señal servo PAN
    • CH1 (PCA9685) → Señal servo TILT
  • Alimentación de servos:
    • Rojo (Vcc) → V+ 5 V del PCA9685
    • Marrón/Negro (GND) → GND del PCA9685
    • Amarillo/Blanco (Señal) → CH0/CH1 del PCA9685

Tabla de conexiones detallada:

Componente Pin/Señal Conecta a Notas
ESP32-CAM U0R (GPIO3) TXD (USB–UART) Programación/monitor serie
ESP32-CAM U0T (GPIO1) RXD (USB–UART) Programación/monitor serie
ESP32-CAM 5V 5V (USB–UART o fuente) Puede alimentar módulo, NO a los servos
ESP32-CAM GND GND común Referencia común
ESP32-CAM GPIO0 GND (solo al programar) Bootloader
ESP32-CAM RST Pulsador a GND Reset
ESP32-CAM GPIO15 (SDA) SDA (PCA9685) Bus I2C, 3.3 V
ESP32-CAM GPIO14 (SCL) SCL (PCA9685) Bus I2C, 3.3 V
PCA9685 VCC (lógica) 3.3 V (ESP32-CAM) Lógica 3.3 V
PCA9685 V+ (potencia) 5 V (fuente externa) Potencia de servos
PCA9685 GND GND común Referencia común
PCA9685 CH0 PWM Señal servo PAN Servo 1 (pan)
PCA9685 CH1 PWM Señal servo TILT Servo 2 (tilt)

Advertencias:
– No alimentes los servos desde el pin 5V de la ESP32-CAM. Usa el terminal V+ del PCA9685 con una fuente 5 V adecuada y GND común.
– Evita usar GPIO12 (MTDI) para señales externas; puede causar problemas de arranque.
– Si usas una fuente 5 V separada para V+, une las masas (GND) de fuente, PCA9685 y ESP32-CAM.

Código completo

A continuación se proporciona el proyecto completo con PlatformIO. El objetivo “ov2640-websocket-pan-tilt” crea un servidor WebSocket que:
– Transmite imágenes JPEG de la cámara OV2640 en binario a los clientes conectados.
– Recibe comandos WebSocket de texto para posicionar PAN/TILT mediante el PCA9685.
– Sirve una página HTML con un visor y controles para pan/tilt.

platformio.ini

Incluye versiones fijas y librerías necesarias.

; ov2640-websocket-pan-tilt/platformio.ini
[env:esp32cam]
platform = espressif32@6.7.0
board = esp32cam
framework = arduino
platform_packages =
  framework-arduinoespressif32@3.20014.0
board_build.partitions = huge_app.csv
monitor_speed = 115200
upload_speed = 921600
build_flags =
  -DCORE_DEBUG_LEVEL=1
lib_deps =
  me-no-dev/AsyncTCP@1.1.1
  me-no-dev/ESP Async WebServer@1.2.3
  adafruit/Adafruit PWM Servo Driver Library@2.4.2
  adafruit/Adafruit BusIO@1.14.5

src/main.cpp

Explicación rápida de las partes clave:
– Configuración de cámara para AI-Thinker (OV2640).
– Inicialización de WiFi STA con credenciales.
– Servidor HTTP (AsyncWebServer) y WebSocket en /ws.
– Tarea FreeRTOS que captura frames y los empuja a los clientes vía WebSocket usando buffers seguros.
– Control de servos con PCA9685 a 50 Hz, mapeando ángulos a microsegundos.
– Página HTML embebida con JS para visualizar el stream y emitir comandos PAN/TILT.

// ov2640-websocket-pan-tilt/src/main.cpp
#include <Arduino.h>
#include <WiFi.h>
#include <esp_camera.h>
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

// ========= Configuración WiFi =========
static const char* WIFI_SSID = "TU_SSID";
static const char* WIFI_PASS = "TU_PASSWORD";

// ========= Pines cámara AI-Thinker (OV2640) =========
#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

// ========= I2C PCA9685 =========
// Usamos GPIO15 (SDA) y GPIO14 (SCL) en la ESP32-CAM
#define I2C_SDA 15
#define I2C_SCL 14

// ========= PCA9685 y servo =========
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40); // Dirección I2C por defecto

// Frecuencia típica servos
const float SERVO_FREQ_HZ = 50.0f;

// Canales
const uint8_t PAN_CH  = 0;
const uint8_t TILT_CH = 1;

// Calibración µs (ajústalo a tus servos/mecánica)
const int SERVO_MIN_US = 500;   // pulso mínimo
const int SERVO_MAX_US = 2500;  // pulso máximo

// Almacenamos ángulos actuales/objetivo para suavizado
volatile int pan_target_deg  = 90;
volatile int tilt_target_deg = 90;

volatile int pan_current_deg  = 90;
volatile int tilt_current_deg = 90;

// ========= Web =========
AsyncWebServer server(80);
AsyncWebSocket ws("/ws");

static const char INDEX_HTML[] PROGMEM = R"HTML(
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>ov2640-websocket-pan-tilt</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
 body { font-family: system-ui, sans-serif; margin: 1rem; color: #222; }
 .row { display: flex; gap: 1.5rem; flex-wrap: wrap; }
 .panel { border: 1px solid #ccc; border-radius: 8px; padding: 1rem; }
 #img { width: 100%; max-width: 480px; background: #000; }
 .ctrl label { display:block; margin-top: .5rem; }
 .ctrl input[type=range] { width: 100%; }
 .status { font-size: .9rem; color: #555; }
 button { padding: .5rem 1rem; margin-top: .5rem; }
</style>
</head>
<body>
<h1>ov2640-websocket-pan-tilt</h1>
<div class="row">
  <div class="panel">
    <img id="img" alt="stream" />
    <div class="status" id="st">Conectando…</div>
  </div>
  <div class="panel ctrl">
    <label>Pan: <span id="panv">90</span>°
      <input id="pan" type="range" min="0" max="180" value="90" />
    </label>
    <label>Tilt: <span id="tiltv">90</span>°
      <input id="tilt" type="range" min="0" max="180" value="90" />
    </label>
    <button id="center">Centrar (90°, 90°)</button>
  </div>
</div>
<script>
(() => {
  const img = document.getElementById('img');
  const st  = document.getElementById('st');
  const pan = document.getElementById('pan');
  const tilt= document.getElementById('tilt');
  const panv= document.getElementById('panv');
  const tiltv= document.getElementById('tiltv');
  const center = document.getElementById('center');

  let ws;
  function connect() {
    const proto = location.protocol === 'https:' ? 'wss' : 'ws';
    ws = new WebSocket(`${proto}://${location.host}/ws`);
    ws.binaryType = 'arraybuffer';
    ws.onopen = () => st.textContent = 'Conectado';
    ws.onclose = () => { st.textContent = 'Desconectado. Reintentando…'; setTimeout(connect, 2000); };
    ws.onerror = () => { st.textContent = 'Error WS'; };

    ws.onmessage = (ev) => {
      if (typeof ev.data !== 'string') {
        const blob = new Blob([ev.data], { type: 'image/jpeg' });
        const url = URL.createObjectURL(blob);
        img.onload = () => URL.revokeObjectURL(url);
        img.src = url;
      } else {
        // Mensajes de texto del servidor (estado)
        st.textContent = ev.data;
      }
    };
  }
  connect();

  function sendCmd(cmd) {
    if (ws && ws.readyState === 1) ws.send(cmd);
  }

  pan.addEventListener('input', () => {
    panv.textContent = pan.value;
    sendCmd(`PAN:${pan.value}`);
  });
  tilt.addEventListener('input', () => {
    tiltv.textContent = tilt.value;
    sendCmd(`TILT:${tilt.value}`);
  });
  center.addEventListener('click', () => {
    pan.value = 90; tilt.value = 90;
    panv.textContent = '90'; tiltv.textContent = '90';
    sendCmd('PAN:90'); sendCmd('TILT:90');
  });
})();
</script>
</body>
</html>
)HTML";

// ========= Utilidades =========
static inline int clampi(int v, int lo, int hi) { return v < lo ? lo : (v > hi ? hi : v); }

void setServoAngle(uint8_t ch, int angleDeg) {
  angleDeg = clampi(angleDeg, 0, 180);
  const int us = SERVO_MIN_US + (int)((SERVO_MAX_US - SERVO_MIN_US) * (angleDeg / 180.0f));
  pwm.writeMicroseconds(ch, us);
}

void servoSmootherTask(void* arg) {
  const TickType_t interval = pdMS_TO_TICKS(15); // ~66 actualizaciones/s
  for (;;) {
    int p = pan_current_deg, pt = pan_target_deg;
    int t = tilt_current_deg, tt = tilt_target_deg;

    if (p != pt) {
      p += (pt > p) ? 1 : -1;
      pan_current_deg = p;
      setServoAngle(PAN_CH, p);
    }
    if (t != tt) {
      t += (tt > t) ? 1 : -1;
      tilt_current_deg = t;
      setServoAngle(TILT_CH, t);
    }
    vTaskDelay(interval);
  }
}

void wsOnEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, 
               AwsEventType type, void * arg, uint8_t * data, size_t len) {
  if (type == WS_EVT_CONNECT) {
    client->text("WS conectado. Envíe PAN:<0..180> / TILT:<0..180>");
  } else if (type == WS_EVT_DATA) {
    AwsFrameInfo* info = (AwsFrameInfo*)arg;
    if (info->opcode == WS_TEXT) {
      String msg;
      msg.reserve(len);
      for (size_t i = 0; i < len; i++) msg += (char)data[i];

      // Formato esperado: "PAN:123" o "TILT:45"
      msg.trim();
      int sep = msg.indexOf(':');
      if (sep > 0) {
        String key = msg.substring(0, sep);
        String val = msg.substring(sep + 1);
        int angle = clampi(val.toInt(), 0, 180);
        if (key.equalsIgnoreCase("PAN")) {
          pan_target_deg = angle;
          client->text("PAN->" + String(angle));
        } else if (key.equalsIgnoreCase("TILT")) {
          tilt_target_deg = angle;
          client->text("TILT->" + String(angle));
        } else {
          client->text("CMD desconocido");
        }
      } else {
        client->text("Formato inválido");
      }
    }
  }
}

TaskHandle_t frameTaskHandle = nullptr;

void frameSenderTask(void* arg) {
  const TickType_t period = pdMS_TO_TICKS(100); // ~10 fps
  for (;;) {
    if (ws.count() > 0) {
      camera_fb_t* fb = esp_camera_fb_get();
      if (!fb) {
        // Notificar error eventual
        ws.textAll("Error: no frame");
      } else {
        // Crear buffer gestionado por AsyncWebSocket
        auto buf = ws.makeBuffer(fb->len);
        if (buf) {
          memcpy(buf->get(), fb->buf, fb->len);
          ws.binaryAll(buf);
        }
        esp_camera_fb_return(fb);
      }
    }
    vTaskDelay(period);
  }
}

bool initCamera() {
  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; // 20 MHz típico
  config.pixel_format = PIXFORMAT_JPEG;

  if (psramFound()) {
    config.frame_size   = FRAMESIZE_QVGA; // 320x240
    config.jpeg_quality = 12;             // 10–20 es razonable
    config.fb_count     = 2;
    config.grab_mode    = CAMERA_GRAB_LATEST;
  } else {
    config.frame_size   = FRAMESIZE_QQVGA; // por si no hay PSRAM
    config.jpeg_quality = 15;
    config.fb_count     = 1;
    config.grab_mode    = CAMERA_GRAB_WHEN_EMPTY;
  }

  esp_err_t err = esp_camera_init(&config);
  if (err != ESP_OK) {
    Serial.printf("camera_init failed: 0x%x\n", err);
    return false;
  }
  sensor_t* s = esp_camera_sensor_get();
  // Ajustes opcionales del sensor
  s->set_brightness(s, 0);
  s->set_contrast(s, 0);
  s->set_saturation(s, 0);
  s->set_framesize(s, config.frame_size);

  return true;
}

void setup() {
  Serial.begin(115200);
  delay(200);

  // I2C para PCA9685
  Wire.begin(I2C_SDA, I2C_SCL, 400000); // 400 kHz
  if (!pwm.begin()) {
    Serial.println("ERROR: PCA9685 no responde en 0x40");
  }
  pwm.setOscillatorFrequency(27000000); // 27 MHz nominal
  pwm.setPWMFreq(SERVO_FREQ_HZ);
  delay(10);
  setServoAngle(PAN_CH, pan_current_deg);
  setServoAngle(TILT_CH, tilt_current_deg);

  // Cámara OV2640
  if (!initCamera()) {
    Serial.println("Fallo al iniciar cámara. Revise cableado y PSRAM.");
    // no return; permitimos que el sistema arranque para debug
  }

  // WiFi
  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(WIFI_SSID, WIFI_PASS);
  Serial.printf("Conectando a WiFi SSID='%s'\n", WIFI_SSID);
  unsigned long t0 = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - t0 < 20000) {
    delay(300);
    Serial.print(".");
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.printf("WiFi OK. IP: %s\n", WiFi.localIP().toString().c_str());
  } else {
    Serial.println("WiFi no conectado (timeout). Continúa en AP del router o revise credenciales.");
  }

  // Servidor HTTP y WebSocket
  ws.onEvent(wsOnEvent);
  server.addHandler(&ws);

  server.on("/", HTTP_GET, [](AsyncWebServerRequest *req){
    AsyncWebServerResponse* res = req->beginResponse_P(200, "text/html; charset=utf-8", INDEX_HTML, strlen(INDEX_HTML));
    res->addHeader("Cache-Control", "no-store");
    req->send(res);
  });

  server.on("/favicon.ico", HTTP_GET, [](AsyncWebServerRequest* req){ req->send(204); });

  server.begin();
  Serial.println("Servidor HTTP/WS listo.");

  // Tarea de suavizado servo
  xTaskCreatePinnedToCore(servoSmootherTask, "servoSmoother", 2048, nullptr, 1, nullptr, 1);

  // Tarea de envío de frames
  xTaskCreatePinnedToCore(frameSenderTask, "frameSender", 4096, nullptr, 1, &frameTaskHandle, 1);
}

void loop() {
  // AsyncWebServer no requiere loop pesado; el WS se atiende en callbacks
  delay(50);
}

Puntos clave:
– Se usan GPIO14 (SCL) y GPIO15 (SDA) para I2C, liberados al no usar la microSD.
– La tarea frameSender copia cada JPEG a un buffer administrado por AsyncWebSocket (ws.makeBuffer) antes de enviar, evitando usar el buffer de la cámara tras esp_camera_fb_return.
– Los comandos de control PAN/TILT son texto simple “PAN:<0..180>” y “TILT:<0..180>”.
– Un “servoSmootherTask” evita saltos bruscos en mecánicas pan-tilt.

Compilación, flash y ejecución

Asegúrate de poner GPIO0 a GND y reiniciar (RST→GND momentáneo) para entrar en bootloader antes de cargar el firmware.

1) Crear el proyecto PlatformIO:
– Con PlatformIO Core CLI:
mkdir ov2640-websocket-pan-tilt
cd ov2640-websocket-pan-tilt
pio project init --board esp32cam

– Sustituye el contenido del platformio.ini generado por el provisto en este caso.
– Crea el árbol de fuentes:
mkdir -p src
# Coloca el main.cpp provisto en src/main.cpp

2) Compilar:
pio run

3) Detectar el puerto serie (opcional):
pio device list

4) Poner la ESP32-CAM en modo programación:
– Conectar GPIO0 a GND.
– Reiniciar pulsando RST a GND y soltando.
– Verifica que el adaptador USB–UART está a 3.3 V (nivel lógico), y que TX/RX están cruzados correctamente.

5) Subir firmware:
pio run -t upload

6) Desconectar el modo programación:
– Desconecta GPIO0 de GND.
– Reinicia la placa (RST a GND momentáneo).

7) Monitor serie:
pio device monitor -b 115200
Observa el IP asignado por tu router (p. ej., 192.168.1.123).

8) Abrir la aplicación:
– En tu navegador, accede a:
– http:///
– Debes ver el visor con el streaming y sliders para PAN/TILT.

Notas:
– Si la subida falla, repite el paso 4 y 5. Algunos adaptadores requieren bajar la velocidad de upload:
pio run -t upload --upload-port COMx
o ajusta upload_speed en platformio.ini (p. ej., 460800).

Validación paso a paso

1) Alimentación y conexiones:
– La ESP32-CAM enciende y el LED rojo está estable.
– El PCA9685 recibe 3.3 V en VCC y 5 V en V+, con GND común.
– Los servos están conectados a CH0 y CH1 del PCA9685, con V+ a 5 V.

2) Arranque y logs:
– En el monitor serie, tras un reset sin GPIO0 a GND, verás:
– Mensajes de “Conectando a WiFi…” seguido de “WiFi OK. IP: x.x.x.x”.
– “Servidor HTTP/WS listo.”
– Si la cámara se inicializa: no aparece “camera_init failed”.

3) Acceso web:
– En la URL http:/// se carga la página.
– El estado pasa de “Conectando…” a “Conectado”.
– Debes ver refrescarse la imagen JPEG constantemente (≈10 fps).

4) Control pan-tilt:
– Al mover el slider de “Pan”, el servo correspondiente debe responder con un pequeño retardo (suavizado 1°/15 ms).
– Al mover el slider de “Tilt”, el segundo servo debe responder.
– El botón “Centrar” debe posicionar ambos a 90°.

5) WebSocket:
– Abre las herramientas de desarrollador del navegador (F12 → Network → WS):
– Observa que /ws está en estado abierto.
– Debe recibir frames binarios (tipo ArrayBuffer).
– Los mensajes de texto del servidor confirman cambios (“PAN->X”, “TILT->Y”).

6) Calidad de imagen:
– La imagen debe ser nítida en QVGA (320×240). Si necesitas más, puedes aumentar a VGA, pero observarás mayor latencia/uso de RAM.

7) Temperatura y consumo:
– La ESP32-CAM puede calentarse ligeramente; es normal.
– Los servos no deben zumbar en exceso en reposo. Si lo hacen, reduce el rango o calibra µs.

8) Persistencia:
– Desconecta y reconecta la alimentación. El sistema debe reanudar conexión al WiFi y servir la interfaz sin intervención adicional.

Troubleshooting

1) Error de alimentación/“Brownout detector”
– Síntomas: reinicios, mensajes “Brownout detector was triggered”.
– Causa: Caídas en 5 V cuando los servos arrancan o se bloquean mecánicamente.
– Solución:
– Alimenta los servos desde una fuente 5 V 2 A (mínimo) separada del 5 V del USB.
– Asegura GND común entre ESP32-CAM, PCA9685 y fuente 5 V.
– Añade condensadores cerca del PCA9685 y servos (p. ej., 470–1000 µF en V+ a GND).

2) “camera_init failed: 0x…” o imagen negra
– Revisa el mapeo de pines (este caso usa AI-Thinker por defecto).
– Asegúrate de no usar la ranura microSD en paralelo (hemos tomado GPIO14/15 para I2C).
– Reduce frame_size a QQVGA y/o baja jpeg_quality si hay poca RAM.
– Confirma que la cámara está bien insertada en su conector.

3) El PCA9685 no responde (“ERROR: PCA9685 no responde en 0x40”)
– Comprueba SDA=SDA y SCL=SCL (GPIO15 y GPIO14 respectivamente).
– Verifica VCC=3.3 V y GND.
– Si tu módulo PCA9685 tiene jumpers de dirección A0–A5, confirma que la dirección es 0x40.
– Usa un escáner I2C si es necesario (temporalmente) para listar direcciones.

4) El stream WebSocket se congela o es muy lento
– Baja la resolución a QQVGA o sube jpeg_quality (número mayor reduce tamaño).
– Asegúrate de tener buena señal WiFi; aproxima el router o usa otro canal menos congestionado.
– Controla el periodo de envío (frameSenderTask) para evitar saturación (p. ej., 100–150 ms).

5) Fallo al subir firmware (“A fatal error occurred: Failed to connect to ESP32”)
– Asegúrate de:
– GPIO0 a GND durante la carga.
– Pulsar RST (a GND) justo antes de “upload”.
– RX/TX están cruzados (TX del adaptador a U0R/GPIO3; RX a U0T/GPIO1).
– Baja upload_speed a 460800 o 115200 en platformio.ini si el adaptador es inestable.

6) Servos vibran, zumban o no se mueven correctamente
– Revisa SERVO_MIN_US y SERVO_MAX_US para tu modelo de servo; prueba 600–2400 µs.
– Elimina topes mecánicos: no fuerces el mecanismo más allá de su rango físico.
– Evita enviar comandos a 1000 Hz: el suavizado ya limita la cadencia.
– Asegúrate de V+ estable a 5 V y GND común.

7) La página web carga, pero no hay imagen
– Revisa que el browser no bloquea contenido mixto si accedes vía HTTPS en otra pestaña.
– Verifica en DevTools → Network → WS que /ws está abierto.
– Si hay errores CORS u otros, recarga y limpia cache (Ctrl+F5).

8) Reinicios aleatorios (WDT) al mover sliders
– Asegúrate de no bloquear callbacks WS con lógica pesada.
– El envío de frames usa buffers gestionados; si cambiaste a otra API, cuida la vida de los punteros.
– Verifica que la fuente 5 V no cae con el movimiento de los servos.

Mejoras/variantes

  • Streaming MJPEG por HTTP:
  • Alternativa a WebSocket binario para compatibilidad con visores MJPEG nativos de navegadores y herramientas. Aumenta la compatibilidad a costa de menos control bidireccional.
  • WSS y autenticación:
  • Añade TLS con un proxy inverso (Nginx/Caddy) y credenciales para restringir el acceso.
  • Controles avanzados:
  • Implementar trayectorias suaves (easing) y límites de velocidad/ aceleración para no castigar la mecánica.
  • Resoluciones y ROI:
  • Cambiar a FRAMESIZE_VGA con PSRAM y reducir fps si tu red lo permite. O implementar región de interés (ROI) si solo interesa una zona.
  • Calibración interactiva:
  • Permite guardar en NVS los parámetros SERVO_MIN_US/SERVO_MAX_US y ángulos máximos seguros por cada servo.
  • OTA (Over-The-Air):
  • Agregar ArduinoOTA o AsyncElegantOTA para actualizar sin cable una vez desplegado.
  • Telemetría:
  • Añade un canal WS de estado (temperatura, fps efectivo, latencia medida por marca de tiempo) para depuración avanzada.
  • Node-RED / Control externo:
  • Publicar un endpoint WS o HTTP con API para integrar con dashboards externos.

Checklist de verificación

  • [ ] Toolchain instalado:
  • [ ] PlatformIO Core 6.1.12 (pio –version)
  • [ ] Plataforma espressif32@6.7.0 y Arduino-ESP32 2.0.14 (via platformio.ini)
  • [ ] Drivers USB–UART instalados (CP210x/CH34x) y puerto visible en “pio device list”
  • [ ] Cableado correcto:
  • [ ] TX (USB–UART) → U0R (GPIO3), RX (USB–UART) ← U0T (GPIO1)
  • [ ] GPIO0 a GND al programar; suelto para ejecutar
  • [ ] PCA9685: VCC=3.3 V, V+=5 V, GND común
  • [ ] I2C: SDA=GPIO15, SCL=GPIO14
  • [ ] Servos: señal en CH0/CH1, V+ a 5 V, GND común
  • [ ] Compilación OK: “pio run” sin errores
  • [ ] Carga OK: “pio run -t upload” con GPIO0 a GND y reset
  • [ ] Monitor serie (115200) muestra IP tras conectar al WiFi
  • [ ] Página accesible en http:///
  • [ ] WebSocket conectado (“Conectado” en UI)
  • [ ] Imagen de la cámara visible y actualizándose
  • [ ] Deslizadores mueven PAN/TILT como se espera
  • [ ] Sin “Brownout” ni reinicios al mover servos
  • [ ] Fuente 5 V disipando adecuadamente y sin calentamiento excesivo

Con este caso práctico has construido un sistema “ov2640-websocket-pan-tilt” completamente reproducible sobre el modelo exacto “ESP32-CAM (OV2640) + PCA9685 servo driver”, usando PlatformIO con versiones fijadas. Has cubierto desde la preparación del entorno, cableado y programación hasta la validación, resolución de problemas y posibles mejoras.

Encuentra este producto y/o libros sobre este tema en Amazon

Ir a Amazon

Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.

Quiz rápido

Pregunta 1: ¿Cuál es el sistema operativo soportado por PlatformIO Core?




Pregunta 2: ¿Qué versión de Python es requerida por PlatformIO Core?




Pregunta 3: ¿Qué herramienta se recomienda para la edición del código?




Pregunta 4: ¿Cuál es la versión del compilador que se instala automáticamente por PlatformIO?




Pregunta 5: ¿Qué módulo se necesita para controlar servos mediante I2C?




Pregunta 6: ¿Cuál es la versión de la plataforma espressif32 mencionada?




Pregunta 7: ¿Qué tipo de adaptador USB se menciona para programar la ESP32-CAM?




Pregunta 8: ¿Qué librería se necesita para utilizar el ESP Async WebServer?




Pregunta 9: ¿Qué tipo de servos se mencionan para el pan-tilt?




Pregunta 10: ¿Qué es necesario instalar para utilizar PlatformIO Core en Windows/macOS/Linux?




Carlos Núñez Zorrilla
Carlos Núñez Zorrilla
Electronics & Computer Engineer

Ingeniero Superior en Electrónica de Telecomunicaciones e Ingeniero en Informática (titulaciones oficiales en España).

Sígueme:
Scroll al inicio