En este artículo te voy a enseñar cómo programar y/o actualizar una ESP32-CAM a través del aire, o mejor dicho, usando el WiFi y la red local en conjunto con PlatformIO y VSCode.

Gracias a esto solo vas a necesitar conectar el programador físicamente una sola vez y a partir de ahí (suponiendo que no olvidas cargar el código de actualización OTA en la ESP32-CAM) todo será a través de la red local.

Esto es muy práctico sobre todo cuando la ESP32-CAM está soldada, pues no se puede conectar fácilmente a su programador

Configurando platformio.ini

Para que podamos habilitar actualizaciones OTA en la ESP32-CAM es muy importante configurar las particiones para que sean como lo dice el archivo min_spiffs.csv:

board_build.partitions = min_spiffs.csv

Mismo que reparte el espacio así:

NameTypeSubTypeOffsetSizeFlags
nvsdatanvs0x90000x5000
otadatadataota0xe0000x2000
app0appota_00x100000x1E0000
app1appota_10x1F00000x1E0000
spiffsdataspiffs0x3D00000x20000
coredumpdatacoredump0x3F00000x10000

Lo he encontrado en: https://github.com/espressif/arduino-esp32/blob/master/tools/partitions/min_spiffs.csv

Entonces para que quede claro, mi platformio.ini se ve así:

; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32cam]
platform = espressif32
board = esp32cam
framework = arduino
monitor_speed = 115200
;build_flags = -DCORE_DEBUG_LEVEL=5 -DDEBUG_ESP_SSL
monitor_rts = 0
monitor_dtr = 0
lib_deps=
    adafruit/Adafruit GFX Library@^1.11.9
    adafruit/Adafruit SSD1306@^2.5.9
  adafruit/DHT sensor library
  adafruit/Adafruit Unified Sensor
    bblanchon/ArduinoJson
upload_protocol=espota
upload_port=192.168.1.72
board_build.partitions = min_spiffs.csv

upload_flags =
    --auth=muchasletrasynumerostalvez

Lo importante es el board_build.partitions, el upload_port, auth y upload_protocol.

Código de gestión de actualizaciones OTA

El código que configura OTA en la ESP32-CAM queda así:

void configurarOTA()
{

  ArduinoOTA.setHostname(host_name);
  ArduinoOTA.onStart([]()
                     { Serial.println("OTA onStart"); });

  ArduinoOTA.onEnd([]()
                   { Serial.println("OTA onEnd"); });

  ArduinoOTA.onError([](ota_error_t error)
                     {
                       Serial.printf("Error [%u]: ", error);
                       if (error == OTA_AUTH_ERROR)
                       {
                         Serial.println("OTA_AUTH_ERROR");
                       }
                       else if (error == OTA_BEGIN_ERROR)
                       {
                         Serial.println("OTA_BEGIN_ERROR");
                       }
                       else if (error == OTA_CONNECT_ERROR)
                       {
                         Serial.println("OTA_CONNECT_ERROR");
                       }
                       else if (error == OTA_RECEIVE_ERROR)
                       {
                         Serial.println("OTA_RECEIVE_ERROR");
                       }
                       else if (error == OTA_END_ERROR)
                       {
                         Serial.println("OTA_END_ERROR");
                       } });
  ArduinoOTA.begin();
}

Lo he encerrado en la función configurarOTA. Basta con invocar a esa función en el setup para que quede configurada, y además de eso debemos estar pendientes de cualquier actualización OTA en el loop (o en un task de FreeRTOS) en donde debemos invocar a ArduinoOTA.handle()

void loop()
{
  ArduinoOTA.handle();
}

Y listo, mientras nunca remuevas la llamada a configurarOTA del setup ni olvides invocar a ArduinoOTA.handle puedes poner cualquier otro código que consideres necesario.

Revirtiendo OTA

Si quieres deshabilitar solo remueve upload_protocol, upload_port y (ya que no necesitamos OTA) board_build.partitions

No olvides incluir las actualizaciones OTA en el nuevo código

Esto puede parecer obvio pero lo dejo claro: cada vez que actualices usando actualizaciones OTA con ESP32 CAM no te olvides de colocar el propio código que gestiona dichas actualizaciones.

Explicado con otras palabras:

La primera vez vas a usar un cable físico y el programador para cargar el código normal de tu tarjeta (el que lee sensores, crea servidores o lo que sea que hagas con tu esp32-cam) pero aparte de eso debes colocar en ese mismo código la gestión de actualizaciones OTA; eso va a permitir que a partir de ahí ya no necesites cable.

Pero cada vez que actualices (o sea, cuando cambies tu código normal) no debes olvidar colocar también el código que gestiona actualizaciones OTA

Básicamente nunca remuevas el código de gestión OTA por más que hagas actualizaciones en otros módulos.

Tampoco te olvides de configurar las credenciales de tu red WiFi si es que han cambiado. Cuando a mí me pasa eso simplemente hago un punto de acceso con mi teléfono con las credenciales de la red antigua.

Protegiendo con contraseña

En mi caso específico no uso contraseña (aunque mi platformio.ini tiene una configurada), pero si tú quieres ponerle una configura la opción --auth en platformio.ini y también con ArduinoOTA.setPassword

Buscar IP de la ESP32-CAM para OTA

Debemos colocar la IP local de la ESP32-CAM en upload_port. Esa IP puede cambiar pero podemos localizarla usando ping y su hostname.

Definimos el hostname de la ESP32-CAM en el código. Yo le he puesto esp32cam-ota:

const char *host_name = "esp32cam-ota";

Ahora hacemos lo siguiente con ping:

C:\Users\parzibyte>ping esp32cam-ota.local
Haciendo ping a esp32cam-ota.local [192.168.1.72] con 32 bytes de datos:
Respuesta desde 192.168.1.72: bytes=32 tiempo=60ms TTL=64
Respuesta desde 192.168.1.72: bytes=32 tiempo=5ms TTL=64

Estadísticas de ping para 192.168.1.72:
    Paquetes: enviados = 2, recibidos = 2, perdidos = 0
    (0% perdidos),
Tiempos aproximados de ida y vuelta en milisegundos:
    Mínimo = 5ms, Máximo = 60ms, Media = 32ms

Básicamente es ping hostname_esp.local (no olvides el .local) y con eso me ha dicho que la IP que tiene en mi caso es la 192.168.1.72 así que esa es la que he configurado en upload_port de platformio.ini

Ejemplo real

Para poner un ejemplo real y muy obvio vamos a usar un código muy simple que enciende y apaga el LED flash de la ESP32-CAM.

Recuerda que aunque el código sea muy corto siempre será necesario conectar al WiFi para poder cargar código “a través del aire”.


bool conectarWifi()
{
  Serial.println("Conectando wifi...");
  WiFi.mode(WIFI_STA);

  WiFi.begin("Nombre de tu red", "Contraseña aquí");

  WiFi.setTxPower(WIFI_POWER_19_5dBm);

  unsigned long inicio = millis();
  const unsigned long TIEMPO_LIMITE = 10000;

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");

    if (millis() - inicio > TIEMPO_LIMITE)
    {
      Serial.println("\nError: Tiempo de espera agotado.");
      return false;
    }
  }

  Serial.println("\nWiFi Conectado!");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
  if (!MDNS.begin(host_name))
  {
    Serial.println("Error al iniciar mDNS");
  }
  else
  {
    Serial.print("mDNS iniciado. Acceder via: http://");
    Serial.print(host_name);
    Serial.println(".local");
    MDNS.addService("http", "tcp", 80);
  }

  return true;
}

También necesitamos mDNS para configurar y anunciarle a la red local nuestro hostname y así se nos pueda encontrar usando ping.

Nota importante hasta este punto no recuerdo si mDNS es estrictamente necesario, ya que con ArduinoOTA.setHostname podría bastar, pero tengo miedo de quitarlo en este momento. No hace daño dejarlo, pero lo aclaro.

Luego tenemos el parpadeo del LED:

void parpadear()
{
  unsigned long ahora = millis();
  if (ahora - ultimosMilisegundosFlash >= intervaloFlash)
  {
    ultimosMilisegundosFlash = ahora;

    if (estadoFlash == LOW)
    {
      estadoFlash = HIGH;
    }
    else
    {
      estadoFlash = LOW;
    }

    digitalWrite(LED_FLASH, estadoFlash);
  }
}

El código completo queda así:

#include <Arduino.h>
#include <WiFi.h>
#include <ArduinoOTA.h>
#include <WiFi.h>
#include <ESPmDNS.h>
#define LED_FLASH 4
const char *host_name = "esp32cam-ota";
// Ajustes propios del LED
unsigned long ultimosMilisegundosFlash = 0;
const long intervaloFlash = 500;
int estadoFlash = LOW;
void parpadear()
{
  unsigned long ahora = millis();
  if (ahora - ultimosMilisegundosFlash >= intervaloFlash)
  {
    ultimosMilisegundosFlash = ahora;

    if (estadoFlash == LOW)
    {
      estadoFlash = HIGH;
    }
    else
    {
      estadoFlash = LOW;
    }

    digitalWrite(LED_FLASH, estadoFlash);
  }
}
bool conectarWifi()
{
  Serial.println("Conectando wifi...");
  WiFi.mode(WIFI_STA);

  WiFi.begin("Red", "Contraseña");

  WiFi.setTxPower(WIFI_POWER_19_5dBm);

  unsigned long inicio = millis();
  const unsigned long TIEMPO_LIMITE = 10000;

  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");

    if (millis() - inicio > TIEMPO_LIMITE)
    {
      Serial.println("\nError: Tiempo de espera agotado.");
      return false;
    }
  }

  Serial.println("\nWiFi Conectado!");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
  if (!MDNS.begin(host_name))
  {
    Serial.println("Error al iniciar mDNS");
  }
  else
  {
    Serial.print("mDNS iniciado. Acceder via: http://");
    Serial.print(host_name);
    Serial.println(".local");
    MDNS.addService("http", "tcp", 80);
  }

  return true;
}

void configurarOTA()
{

  ArduinoOTA.setHostname(host_name);
  ArduinoOTA.onStart([]()
                     { Serial.println("OTA onStart"); });

  ArduinoOTA.onEnd([]()
                   { Serial.println("OTA onEnd"); });

  ArduinoOTA.onError([](ota_error_t error)
                     {
                       Serial.printf("Error [%u]: ", error);
                       if (error == OTA_AUTH_ERROR)
                       {
                         Serial.println("OTA_AUTH_ERROR");
                       }
                       else if (error == OTA_BEGIN_ERROR)
                       {
                         Serial.println("OTA_BEGIN_ERROR");
                       }
                       else if (error == OTA_CONNECT_ERROR)
                       {
                         Serial.println("OTA_CONNECT_ERROR");
                       }
                       else if (error == OTA_RECEIVE_ERROR)
                       {
                         Serial.println("OTA_RECEIVE_ERROR");
                       }
                       else if (error == OTA_END_ERROR)
                       {
                         Serial.println("OTA_END_ERROR");
                       } });
  ArduinoOTA.begin();
}

void setup()
{
  pinMode(LED_FLASH, OUTPUT);
  Serial.begin(115200);
  if (!conectarWifi())
  {
    ESP.restart();
  }
  configurarOTA();
}

void loop()
{
  ArduinoOTA.handle();
  parpadear();
}

Lo cargamos una vez con el programador. Y la siguiente vez si, por ejemplo, queremos cambiar el intervalo del parpadeo basta con volver a subir el código.

Otro ejemplo

Para mi proyecto de la OLED SSD1306 + DHT22 + PIR + Telegram + ESP32-CAM + Relevador he usado actualizaciones OTA. Puse lo siguiente en el onProgress:

  ArduinoOTA.onProgress([](unsigned int bytesTransferidos, unsigned int bytesTotales)
                        {
                          /*
                          100 % --> bytes totales
                          X --> bytesTransferidos

                          X =100*bytesTransferidos/bytesTotales

                          */
                          double progresoComoPorcentaje = 100 * bytesTransferidos / bytesTotales;
                          display.clearDisplay();
                          display.setTextColor(SSD1306_WHITE);
                          // La barra completa vacía
                          // display.drawRect(0, 16, ANCHO_OLED, ALTO_OLED / 2, SSD1306_WHITE);
                          /*
                          100 % -> ANCHO_OLED
                          porcentaje -> x

                          barraRellena = porcentaje/ANCHO_OLED*100;
                           */
                          int barraRellena = progresoComoPorcentaje * ANCHO_OLED / 100;
                          display.drawRect(0, 18, ANCHO_OLED, ALTO_OLED / 2, SSD1306_WHITE);
                          display.fillRect(0, 18, barraRellena, ALTO_OLED / 2, SSD1306_WHITE);
                          display.setTextSize(2);
                          display.setCursor((ANCHO_OLED / 2) - (6 * 12 / 2), 0);
                          display.printf("%6.2f%%", progresoComoPorcentaje);
                          display.display(); });

Y mira qué guapo se ve cuando recibe una actualización:

(Recuerda que en un vídeo se ven unas bandas de refresco de la OLED, por eso se ve esa diagonal negra).

Puedes ver el proyecto completo en: https://parzibyte.me/blog/posts/esp32-cam-pir-dht22-oled/

Si el post ha sido de tu agrado te invito a que me sigas para saber cuando haya escrito un nuevo post, haya actualizado algún sistema o publicado un nuevo software. Facebook | X | Instagram | Telegram | También estoy a tus órdenes para cualquier contratación en mi página de contacto