Una matriz de 8×8 LEDs RGB tiene 64 pixeles y se controla con un solo cable de datos. El problema de siempre es otro: para dibujar algo en ella hay que editar un arreglo de 64 valores dentro del código, recompilar y volver a subir el sketch cada vez que quieres cambiar la figura. Este proyecto rompe ese ciclo. El ESP32 levanta su propio servidor web, abres una grilla de 8×8 en el navegador del celular o del computador, pintas pixel por pixel con el color que quieras y la figura aparece al instante en la matriz física.

Al terminar vas a tener un editor de figuras con 10 ranuras de memoria que sobreviven al reinicio, una galería para mostrarlas con un clic, y un modo presentación que las va rotando solo. Y, de paso, vas a entender dos cosas que casi nunca se explican: cómo se calcula la alimentación real de una matriz WS2812B y por qué tu primer dibujo casi seguro va a salir espejado.

Qué vas a tener funcionando

Página web del editor de figuras para la matriz 8×8, con la grilla de dibujo a la izquierda y la galería de figuras guardadas a la derecha

  • Editor de grilla 8×8 en el navegador, con color independiente para cada pixel.
  • Control de brillo por deslizador, aplicado a la figura completa.
  • 10 ranuras de memoria: las figuras se guardan como JSON en LittleFS, así que siguen ahí después de desenchufar el ESP32.
  • Galería con miniaturas: cada figura guardada se previsualiza tal como se va a ver.
  • Cargar para editar: al hacer clic en una figura de la galería se muestra en la matriz y además vuelve al editor para que la modifiques.
  • Vista previa instantánea: pruebas el dibujo en los LEDs reales antes de ocupar una ranura.
  • Modo presentación: recorre todas las figuras guardadas con el intervalo que tú definas.

Cómo funciona el panel WS2812B por dentro

Panel de matriz LED RGB de 64 LEDs WS2812B de 8×8

Cada uno de los 64 LEDs del panel es un WS2812B, también conocido como neopixel. Adentro de cada encapsulado hay tres LEDs (rojo, verde y azul) y un pequeño controlador integrado. Ese controlador es la clave: los 64 LEDs están encadenados en serie y el primero recibe la trama completa de datos por el pin DIN. Se queda con los primeros 24 bits (8 por color), y el resto lo retransmite al siguiente LED por su salida. Por eso se controlan 64 pixeles con un solo pin digital del ESP32, y por eso el orden físico del cableado interno del panel importa tanto: el LED número 9 no es necesariamente el que está justo debajo del número 1.

Ese detalle es el que vamos a resolver por software más abajo, con tres opciones de configuración del sketch.

Alimentación: la parte que casi nadie calcula bien

Esta es la sección que más problemas evita. A 5 V, cada LED WS2812B consume cerca de 50 mA cuando está al máximo de brillo en blanco (los tres colores encendidos a fondo). Haz la multiplicación completa antes de conectar nada:

  • 64 LEDs × 50 mA = 3,2 A en el peor caso, con toda la matriz en blanco al 100 %.
  • Un ESP32 alimentado por USB entrega, en el mejor de los casos, unos 500 mA por su regulador de 3,3 V y el pin de 5 V queda colgando del puerto USB del computador.

O sea: alimentar la matriz completa desde el pin 5 V del ESP32 no funciona si vas a usar blanco o brillos altos. Vas a ver colores que se apagan, parpadeos, o directamente reinicios del ESP32 por caída de tensión (el clásico brownout).

Para este proyecto usamos una fuente externa de 5 V 2 A, que alcanza de sobra porque el editor dibuja figuras, no ilumina los 64 pixeles en blanco a la vez. Si tu caso de uso sí enciende la matriz completa, sube a 5 V 4 A. Y hay una regla que no puedes saltarte:

La tierra de la fuente externa y el GND del ESP32 tienen que estar unidos. Sin esa referencia común, la señal de datos no tiene contra qué compararse y el panel muestra basura o no enciende.

Dato práctico: el sketch trae el brillo limitado a 200 de 255 y el editor parte en 128. Bajar el brillo es la forma más barata de bajar el consumo, porque el WS2812B regula corriente por PWM.

Conexiones

Detalle de los pines del panel de matriz LED 8×8 WS2812B

El panel trae tres conexiones: V+, V- y DIN. El cableado queda así:

Matriz 8×8 Va conectado a
DIN GPIO 2 del ESP32 (o cualquier otro GPIO de salida)
V+ 5 V de la fuente externa
V- GND de la fuente externa y GND del ESP32

Diagrama de conexión entre el ESP32, el panel de matriz LED 8×8 WS2812B y una fuente externa de 5 V

El GPIO 2 es solo una sugerencia. Puedes usar casi cualquier pin de salida, pero evita los que el ESP32 usa al arrancar (GPIO 6 a 11 van al flash, GPIO 0 y 15 son pines de arranque). Si el panel queda lejos del ESP32, conviene agregar dos cosas que el montaje mínimo no trae: una resistencia de 330 Ω a 470 Ω en serie con la línea de datos, pegada al panel, para amortiguar los rebotes de la señal, y un condensador electrolítico de 1000 µF entre V+ y V-, para absorber los golpes de corriente cuando muchos LEDs encienden de golpe.

Un punto que aparece seguido en los foros: el WS2812B espera datos de 5 V y el ESP32 entrega 3,3 V. En la mayoría de los paneles funciona igual, pero si ves pixeles que cambian de color solos o el primer LED se porta raro, el culpable es ese desfase de niveles. La solución es un conversor de nivel de 3,3 V a 5 V en la línea DIN.

Librerías que necesitas

Antes de compilar, instala estas cuatro desde el Gestor de Librerías del IDE de Arduino:

Instalación de la librería FastLED desde el Gestor de Librerías del IDE de Arduino

  1. FastLED, de Daniel Garcia. maneja el protocolo del WS2812B.
  2. ESPAsyncWebServer, de ESP32Async. el servidor web asíncrono.
  3. AsyncTCP, de ESP32Async. dependencia obligatoria de la anterior.
  4. ArduinoJson, de Benoit Blanchon, versión 7.X. serializa las figuras que se guardan en LittleFS.

Instalación de la librería ArduinoJSON desde el Gestor de Librerías del IDE de Arduino

Instala también el soporte de placas ESP32 en el IDE si todavía no lo tienes. Ojo con ArduinoJson: la versión 6 y la 7 tienen una API distinta, y el código usa la sintaxis de la 7 (JsonDocument doc; sin tamaño fijo). Si compilas con la 6 vas a recibir errores de tipos.

El código, pieza por pieza

El sketch completo está en el tutorial original enlazado al final. Acá vamos a mirar los cuatro bloques que de verdad tienes que entender para adaptarlo a tu panel.

1. Configuración

Todo lo que vas a tocar está en las primeras líneas: credenciales de WiFi, pin de datos, tamaño de la matriz y, sobre todo, las tres banderas de orientación.

C++
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>
#include <LittleFS.h>
#include <ArduinoJson.h>
#include <FastLED.h>

// REPLACE WITH YOUR NETWORK CREDENTIALS
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

#define DIN_PIN       2
#define MATRIX_WIDTH  8
#define MATRIX_HEIGHT 8
#define NUM_LEDS      (MATRIX_WIDTH * MATRIX_HEIGHT)
#define BRIGHTNESS    200        // Brightness (0-255)
#define LED_TYPE      WS2812B
#define COLOR_ORDER   GRB
#define ZIGZAG        false      // Set false if your panel is wired in straight rows instead of zigzag
#define FLIP_H        true       // Set true if the image displays mirrored left to right
#define FLIP_V        true       // Set true if the image displays upside down

#define NUM_SLOTS     10         // Number of stored shape slots
#define SHAPES_DIR    "/shapes"

CRGB leds[NUM_LEDS];
AsyncWebServer server(80);

CRGB    currentPixelColors[MATRIX_HEIGHT][MATRIX_WIDTH]; // Colors per pixel for the shape currently loaded into memory
uint8_t currentBrightness = 128;
bool    haveShapeLoaded   = false;
int     currentSlot       = -1;    // Which saved slot is on screen (-1 = preview/none)

// Slideshow state
bool     slideshowActive   = false;
uint32_t slideshowInterval = 2000;   // Delay in ms between shapes
uint32_t slideshowLastTick = 0;
int      slideshowIndex    = -1;     // Last slot shown (-1 = none)
int      slideshowOrder[NUM_SLOTS];  // Slots that currently hold a shape
int      slideshowCount    = 0;

2. La traducción de coordenadas (acá está el 90 % de los problemas)

Esta función es la que convierte un par de coordenadas (x, y) del dibujo en el índice real del LED dentro de la cadena. Las tres constantes que configuraste arriba entran en juego justo acá:

C++
// Convert (x,y) coordinates to the corresponding index in the matrix
uint16_t coordToIndex(uint8_t x, uint8_t y) {
  if (FLIP_H) x = (MATRIX_WIDTH - 1) - x;
  if (FLIP_V) y = (MATRIX_HEIGHT - 1) - y;
  if (ZIGZAG && (y & 0x01)) {
    x = (MATRIX_WIDTH - 1) - x;
  }
  return (y * MATRIX_WIDTH) + x;
}

// Set the color of a pixel at (x,y)
void setPixel(uint8_t x, uint8_t y, CRGB color) {
  if (x >= MATRIX_WIDTH || y >= MATRIX_HEIGHT) return;
  leds[coordToIndex(x, y)] = color;
}
  • ZIGZAG: muchos paneles cablean las filas en serpentina, o sea la fila 0 va de izquierda a derecha y la fila 1 de vuelta de derecha a izquierda. Si dibujas una línea vertical y te sale una escalera, tu panel es en zigzag: pon true.
  • FLIP_H y FLIP_V: corrigen el panel montado al revés. Si dibujas una "L" y aparece espejada, cambia FLIP_H; si aparece de cabeza, cambia FLIP_V.

La forma rápida de calibrar: dibuja un solo pixel en la esquina superior izquierda del editor y mira en qué esquina física se enciende. Con eso deduces las tres banderas en un intento.

3. El renderizado

Acá se ve por qué el brillo del editor no pelea con el brillo global de FastLED: cada color se escala con nscale8_video(), que baja la intensidad sin apagar del todo los pixeles muy tenues.

C++
// Renders currentPixelColors (scaled by currentBrightness) to the physical LEDs
void renderCurrentShape() {
  FastLED.clear();
  if (haveShapeLoaded) {
    for (uint8_t y = 0; y < MATRIX_HEIGHT; y++) {
      for (uint8_t x = 0; x < MATRIX_WIDTH; x++) {
        CRGB c = currentPixelColors[y][x];
        if (c == CRGB::Black) continue;
        c.nscale8_video(currentBrightness);
        setPixel(x, y, c);
      }
    }
  }
  FastLED.show();
}

Fíjate también en el continue cuando el color es negro. Es una micro optimización que evita escribir los pixeles apagados y mantiene el refresco rápido.

4. El guardado en LittleFS

Cada figura se guarda como un archivo JSON independiente en /shapes/N.json, con los 64 pixeles y el brillo. Usar LittleFS en vez de EEPROM tiene una ventaja concreta: la EEPROM emulada del ESP32 son 512 bytes por defecto y los 64 pixeles a tres bytes ya se comen 192 bytes por figura. Con archivos no tienes ese techo, y además cada ranura se borra por separado.

C++
// Saves a shape to a slot, validating the input first
bool saveSlot(int slot, const char* name, JsonArray pixels, uint8_t brightness) {
  char path[32];
  if (!slotPath(slot, path, sizeof(path))) return false;
  if (pixels.size() != NUM_LEDS) return false;

  for (JsonVariant v : pixels) {
    JsonObject o = v.as<JsonObject>();
    if (o.isNull() || !o["r"].is<int>() || !o["g"].is<int>() || !o["b"].is<int>()) {
      return false;
    }
  }

  JsonDocument doc;
  doc["name"] = name;
  JsonArray px = doc["pixels"].to<JsonArray>();
  for (JsonVariant v : pixels) {
    JsonObject src = v.as<JsonObject>();
    JsonObject dst = px.add<JsonObject>();
    dst["r"] = constrain(src["r"].as<int>(), 0, 255);
    dst["g"] = constrain(src["g"].as<int>(), 0, 255);
    dst["b"] = constrain(src["b"].as<int>(), 0, 255);
  }
  doc["brightness"] = brightness;

  File f = LittleFS.open(path, "w");
  if (!f) return false;
  bool ok = serializeJson(doc, f) > 0;
  f.close();
  return ok;
}

Nota lo que hace constrain() con cada canal antes de escribir: el servidor nunca confía en el valor que viene del navegador. Es una validación chica pero es la que evita que un JSON mal formado deje la matriz en un estado imposible.

Primer arranque

Sube el sketch con tus credenciales de WiFi ya editadas y abre el Monitor Serial a 115200 baudios. El ESP32 imprime la dirección IP que le asignó tu router:

Monitor Serial del IDE de Arduino mostrando la dirección IP del ESP32

Escribe esa IP en el navegador de cualquier dispositivo conectado a la misma red. Te va a cargar el editor con la grilla vacía:

Editor web con la grilla de 8×8 vacía, el selector de color y el control de brillo

Ahora el flujo de trabajo: elige un color, haz clic en las celdas para pintar, ajusta el brillo y usa Preview on Matrix para ver el resultado en los LEDs reales. Cuando te guste, ponle nombre, elige una ranura y guarda.

Panel del editor con una figura dibujada, el nombre asignado y el selector de ranura

Las figuras guardadas aparecen en la galería con su miniatura. Un clic la muestra en la matriz y la devuelve al editor; la cruz roja la borra:

Galería del editor web con las miniaturas de las figuras guardadas

Y el resultado en el panel físico:

Matriz LED 8×8 WS2812B mostrando figuras de colores creadas desde el editor web

Errores comunes y cómo depurarlos

  • Los colores salen cambiados (el rojo se ve verde): tu panel no usa el orden GRB. Cambia COLOR_ORDER a RGB y vuelve a probar.
  • La figura sale espejada o de cabeza: son FLIP_H y FLIP_V, con el truco del pixel en la esquina que vimos arriba.
  • Las líneas verticales salen en escalera: ZIGZAG mal configurado.
  • El ESP32 se reinicia al encender muchos LEDs: caída de tensión. Revisa la fuente, baja BRIGHTNESS y agrega el condensador de 1000 µF.
  • Solo enciende el primer LED: casi siempre es el DIN conectado al pin equivocado, o el V- del panel sin unir al GND del ESP32.
  • La página carga pero las figuras no se guardan: LittleFS sin formatear o sin el directorio /shapes. Revisa en el Monitor Serial si el montaje del sistema de archivos falló al arrancar.
  • La página no carga: confirma que el dispositivo desde el que abres el navegador esté en la misma red WiFi, no en la red de invitados ni en datos móviles.

Variantes y mejoras

Tres extensiones concretas que este proyecto deja servidas:

  1. Presets de iconos listos para usar. En vez de dibujar todo a mano, agrega un arreglo de figuras predefinidas en el firmware (corazón, flecha, check, carita) y un botón que las cargue al editor. Como el formato de guardado ya es JSON de 64 pixeles, basta con dejar esos arreglos en PROGMEM y servirlos por un endpoint nuevo tipo /api/presets.

  2. Disparo remoto por MQTT desde Home Assistant. Agrega la librería PubSubClient y suscribe el ESP32 a un tópico como casa/matriz/figura. Publicando el número de ranura desde una automatización, la matriz puede mostrar un icono según el evento: una gota cuando el sensor de humedad se dispara, un sobre cuando llega una notificación, una carita cuando alguien llega a la casa. La lógica de mostrar ya existe en loadSlotToCurrent() más renderCurrentShape(), así que es conectar el callback de MQTT a esas dos funciones.

  3. Texto desplazándose y matrices más grandes. El mismo código escala a un panel de 16×16 cambiando MATRIX_WIDTH y MATRIX_HEIGHT (recuerda recalcular la alimentación: 256 LEDs en blanco son casi 13 A). Con la librería FastLED y una fuente de caracteres de 8 pixeles puedes agregar un modo de texto que desplace mensajes, reutilizando la misma página web para escribirlos.

Personalización para Chile

Este montaje se arma completo con componentes que hay en catálogo local en MechatronicStore. La lista de materiales real es corta:

  • Placa ESP32 DevKit. cualquier variante de 30 o 38 pines sirve, no necesitas un modelo específico.
  • Matriz LED 8×8 WS2812B. el panel de 64 neopixels. Es la pieza central y no tiene reemplazo: un panel de LEDs comunes o un display OLED no hacen lo mismo.
  • Fuente 5V 2A switching. con conector jack DC. Si vas a usar la matriz al máximo de brillo en blanco, sube a 4 A.
  • Jumpers macho hembra 20cm. tres bastan para el cableado mínimo.
  • Protoboard 400 puntos. opcional, pero deja el nodo de tierra común mucho más ordenado que un empalme al aire.

Si el tutorial original menciona un adaptador de MakerAdvisor o una placa de otra marca, la equivalencia es directa: cualquier ESP32 DevKit del catálogo cumple exactamente la misma función, porque el sketch no usa nada específico de un fabricante. El presupuesto total del proyecto queda en torno a los $35.000 CLP aproximados; revisa precios y stock actualizados directamente en la tienda antes de comprar.

Recursos

Versión chilena con componentes en stock local, basada en el proyecto de Random Nerd Tutorials y ampliada con el cálculo de alimentación, la guía de calibración de orientación y la sección de depuración.