9 min read

Mostrando imágenes desde internet en una matriz led

Mostrando imágenes desde internet en una matriz led
Emoji dibujado en una matriz LED ws2812b

Hace algunos años conocí un producto de Adafruit llamado "neopixel", un tipo de LED RGB que se controlaba únicamente con un pin, esto me pareció fascinante.
Era algo que tenía que probar, por lo que compré una tira para hacer alguna prueba y quedé enamorado de su funcionamiento y sus colores.

Poco después de terminar el blog Creando una librería para arduino, me planteé hacer una librería desde cero para el W2812b (neopixel).
Para orientarme un poco estuve explorando la librería que escribió Adafruit para este elemento, al hacerlo descubrí que esta librería funciona con el esp8266. Y fué en ese momento que me surgió la duda. "¿Podría usar el módulo wifi esp8266 para descargar imágenes desde internet y mostrarlas en una matriz de led tipo ws2812b. ?" Spoiler alert, ¡Sí es posible.!

¿Como funciona el LED ws2812b?


Este LED de montaje superficial integra un Microcontrolador, un regulador de voltaje y un oscilador de alta precisión. Todo esto es necesario para poder implementar un protocolo de comunicación "serial", lo que nos permite controlar los LEDs conectados en cascada usando un único pin.

El funcionamiento es el siguiente: Cada color necesita 8 bits y al ser RGB cada LED necesitará 24bits de datos. El primer LED recibirá el total de los datos necesarios para todos los "LEDS" en la secuencia (n cantidad de bits). Este conservará los primeros 24bits y pasará lo que le sobré (n - 24) al siguiente LED en la secuencia.

Veamos un ejemplo simplificado para entenderlo mejor:
Para este ejemplp este tipo de "Neopixel" no va a ser RGB, sino blanco y solo recibirá 1 bit. 1 para encendido y 0 para apagado.

La imagen a continuación muestra como es la señal que se debe enviar al "Neopixel" para representar un 1 o un 0

Como podemos ver lo que define si una señal representa un 1 binario o un 0 binario es una señal cuadrada tipo PWM variando su ciclo útil.

Ahora bien podemos agrupar estas señales en una sola que controlaría los LEDs que se encuentren en la secuencia. En el ejemplo anterior enviamos (1,1,0) nuestros LEDS se comportarían de la siguiente manera:

El primer LED captura el primer bit (1), se enciende y envía los bits restantes al segundo, este a su vez captura el primer bit que recibe (1), se enciende y envía el último bit restante (0) al último LED, este al recibir un 0 quedaría apagado.


Generando imágenes desde internet

Una de las limitaciones que tenemos en este proyecto es el tamaño de la imagen, la Matriz de LED que usaremos es de 8x8, por lo que la imagen debe ser del estilo pixel art.
"¿Puede ser más grande?", Sí puede ser más grande pero tendríamos que buscar una matriz de un tamaño superior o juntar varías matrices 8x8, sin embargo para el caso práctico de esta publicación nos quedaremos con el 8x8.

Existe una comunidad llamada pico8 cuyo objetivo es tener una consola de videojuegos de 8bits, en ella he encontrado un aporte increíble. Una colección de diversas imágenes "8x8" que ha compartido el usuario @justin_cyr en la plataforma X.

Colección de imágenes pixel art

Gracias a esta imagen ya tenemos una colección importante de imágenes con la que trabajar, lo primero que haremos será sub dividirla en imágenes más pequeñas que contengan únicamente el "Emoji" que queremos dibujar en la matriz led, una vez que tengamos la imágenes de cada "Emoji" tenemos que extraer sus valores RGB para cada pixel que compone la imagen y finalmente enviaremos esa información al módulo wifi.

Para esta parte que corresponde a la division de la imagen en otras más pequeñas crearé un script usando Javascript. Investigando un poco he descubierto una librería de manipulación de imágenes llamada "Jimp" que nos ofrece herramientas necesarias para poder dividir la imagen y extraer su valor RGB de cada pixel

    
const path = require('node:path');
const { Jimp } = require('jimp');

const yOffset = 24;
const xOffset = 5;
const pixelCount = 32;
const yGap = 16;
const xGap = 20;

(async()=>{
  const imgPath = path.join(__dirname,'assets', '8x8emoji.png')
  const image = await Jimp.read(imgPath)
  for(let i=0; i<10; i++){
    for(let j=0; j<10; j++){
      const miniImgPath = 
        path.join(__dirname,'assets',`8x8emoji_${(i*10)+j}.png`)
      const copy = image.clone().contrast(0.35)    
      copy.crop({
        x:xOffset+((xGap*j) + (pixelCount*j)),
        y:yOffset+((yGap * i)+(pixelCount*i)),
        w: pixelCount,
        h:pixelCount})
      copy.write(miniImgPath)
    }
  }
})()
    
  

El funcionamiento es el siguiente: tenemos dos ciclos for anidados, uno nos permitirá recorrer la imagen pixel por pixel de arriba hacia abajo y el segundo de izquierda a derecha.

la función crop nos permite recortar la imagen, para ello debemos pasarle la posición x e y que corresponde al origen donde vamos a iniciar el recorte, seguido de los valores w y h que corresponden al ancho (width) y alto (height).

Al analizar la imagen original podemos observar que cada "Emoji" no es una imagen 8x8 sino 32x32, por lo que el ancho y el alto representado por la variable pixelCount tiene asignado este valor.

otras variables importantes para nuestro script son offset y gap que representan un desplazamiento en el eje x e y y un desplazamiento en filas o columnas respectivamente.

Cuando trabajamos con gráficos el punto (0,0) se ubica en la parte superior izquierda de la imagen, de modo que el xOffset = 5 y el yOffset = 24 nos posicionará en la parte superior izquierda del cuadrado de linea punteada blanca.

Análizando las expresiones xOffset+((xGap*j) + (pixelCount*j)) y yOffset+((yGap * i)+(pixelCount*i)) obtenedremos la posicion en x e y para cada imagen. Si i y j son 0 la expresion se reduce a únicamente xOffset y yOffset que como hemos visto anteriormente nos posiciona en la esquina superior izquierda del recuadro blanco. Hemos visto tambien que w y h estan fijos a 32 por lo que la funcion crop nos generaría una imagen comprendida entre la esquina superior izquierda del recuadro blanco y la primera interseccion que se encuentra entre la primera linea verde y la primera linea roja.

Dimensiones primer corte

Cuando j es igual a 1 el valor de x será xOffset+(xGap*(1) + (pixelCount * (1))) lo que es igual a 5 + (20*1 + 32*1).

¿Pero que significa todo esto?. primero xGap es el espacio vertical que hay entre imagenes. y como hemos visto pixelCount puede ser el ancho o el alto de la imagen. por lo que cada incremento en j causará que en valor de x siempre se ubique en un punto de interseccion sobre la linea punteada verde donde inicia una imagen, por otro lado cada incremento en i causara que el valor de y siempre se ubique en una interseccion de la linea punteada roja justo donde comienza una imagen.


¿Como preparar las imágenes para su descarga?


En la sección anterior creamos las imágenes de "Emoji", nuestro objetivo ahora es enviar estas imágenes al esp8266 para que el pueda construir la imagen en la matriz LED.
Esto lo conseguiremos al tomar una imagen de "Emoji" y recorrer cada pixel que la compone, extrayendo su componente RGB. Veamos el código que se encarga de esta parte

    
const express = require('express');
const path = require('node:path');
const { Jimp, intToRGBA } = require('jimp');
const app = express();
const PORT = 3300;

app.use(express.json());

app.get('/', async (req, res) => {
  const index = Math.round(Math.random() * 99)
  const imgPath = path.join(__dirname,'assets', `8x8emoji_${index}.png`)
  const image = await Jimp.read(imgPath)
  image.resize({w:8,h:8})
  let response = []
  image.scan((x,y)=>{
    const {r,g,b,a} = intToRGBA(image.getPixelColor(x, y))
    response.push([r,g,b].join(','))
  })
  res.send(response.join('|'));
});

app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
});
    
  

Link al repositorio del servidor

Cuando el esp8266 realice una petición al servidor este tomará una imagen aleatoria, la re-escalará de 32x32 a 8x8 (que es el tamaño de nuestra matriz LED), luego recorrerá esta imágen con ayuda de la función scan para posteriormente obtener los valores RGB de cada pixel. esto lo hará con la ayuda de la función intToRGBA.

la respuesta será un texto plano que contendrá los valores RGB separados por "coma" y cada grupo de valores RGB sera separado por el carácter "|".

Ejemplo: 0,0,0|255,241,232|255,241,232|0,0,0.

Nota: La decisión de diseño para este servidor recae en el hecho que las librerías y el propio lenguaje de programación disponible para el esp8266. no contienen funciones que permitan transformar datos más complejos. Un ejemplo claro es que para el esp8266 no hay una forma sencilla de trabajar con datos en formato JSON.


Descargando imágenes desde nuestro servidor


Lo primero que haremos será crear un cliente web que se conectará al servidor. Este hará una petición y con la respuesta obtenida hará un tratamiento de los datos para extraer cada grupo de datos RGB y enviarlos a cada LED en la matriz.

    

#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <ESP8266WiFi.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <WiFiClient.h>
  
#define LED_PIN  LED_BUILTIN

// Numero de pixeles presentes en la tarjeta
#define LED_COUNT 64

ESP8266WiFiMulti WiFiMulti;
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_GRB + NEO_KHZ800);

void split(String dest[], String text, char token);

void setup() {

  Serial.begin(115200);

  Serial.println();
  Serial.println();
  Serial.println();

  for (uint8_t t = 4; t > 0; t--) {
    Serial.printf("[SETUP] WAIT %d...\n", t);
    Serial.flush();
    delay(1000);
  }

  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP("wifi_ssid", "wifi_pass");
  strip.begin();           // INITIALIZE NeoPixel strip object (REQUIRED)
  strip.show();            // Turn OFF all pixels ASAP
  strip.setBrightness(50); // Set BRIGHTNESS to about 1/5 (max = 255)
}

void loop() {
  // wait for WiFi connection
  if ((WiFiMulti.run() == WL_CONNECTED)) {

    WiFiClient client;

    HTTPClient http;

    Serial.print("[HTTP] begin...\n");
    if (http.begin(client, "http://192.168.1.101:3300/")) {  // HTTP


      Serial.print("[HTTP] GET...\n");
      // start connection and send HTTP header
      int httpCode = http.GET();

      // httpCode will be negative on error
      if (httpCode > 0) {
        // HTTP header has been send and Server response header has been handled
        Serial.printf("[HTTP] GET... code: %d\n", httpCode);

        // file found at server
        if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
          String payload = http.getString();
          Serial.println(payload);
          String pixels[LED_COUNT];
          String rgbPixels[3];
          int delim_cnt = 0;
          int prevI=0;
          split(pixels, payload, '|');
          for(int i=0; i < LED_COUNT; i++ ){
            split(rgbPixels, pixels[i], ',');
            int r = rgbPixels[0].toInt();
            int g = rgbPixels[1].toInt();
            int b = rgbPixels[2].toInt();
            uint32_t color = strip.Color(r,g,b);
            strip.setPixelColor(i, color); 
          };
          strip.show()
        }
      } else {
        Serial.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
      }

      http.end();
    } else {
      Serial.println("[HTTP] Unable to connect");
    }
  }

  delay(10000);
}

void split(String dest[], String text, char token){
  int delim_cnt = 0;
  int prevI=0;
  for(int i=0; i < text.length(); i++){
    char delim = text[i];
    if(delim == token){
      dest[delim_cnt]= text.substring(prevI,i);
      prevI= i +1;
      delim_cnt++;
    }
    if( i== text.length() -1){
      dest[delim_cnt]= text.substring(prevI);
    }
  }
}

    
  

Este código ha sido creado a partir de los ejemplos BasicHttpClient de la librería del "esp8266" y el strandTest de la librería "Adafruit Neopixel". El ejemplo BasicHttpClient lo que hace es configurar el módulo wifi como cliente. Establece una conexión a un servidor y hacer una peticion "GET".

La parte del código que establece la conexión al servidor es http.begin(client, "http://192.168.1.101:3300/"). Para mi caso el servidor lo ejecuto desde mi ordenador y me conecto a el usando su dirección ip.

El comando para obtener la direccion ip de tu máquina varia según tu sistema operativo.

Windows - Powershell

Get-NetIPAddress -AddressFamily IPv4

Mac os / Linux

ifconfig

Lo que buscamos es una direccion del tipo 192.168.x.x.

Nota: Puede que te encuentres una dirección ip tipo 192.168.x.1. esta no es la de tu máquina si no la del router, la de tu máquina debe tener en el último grupo de números un valor que va del 2 al 254. Además debes dejar la parte :3300 este es el puerto que usará el servidor para transmitir los datos.

Una vez que se han recibido los datos es necesario transformarlos. eEl servidor nos envía un texto y de el debemos obtener los grupos RGB para cada pixel y transformalos a un valor numérico. Para ello usamos la funcion split, esta función lo que hace es recibe un string, lo separa según un carácter definido y devuelve los datos separados en un array.

Ejemplo:
para el dato recibido "0,0,0|255,241,232|41,173,255" podemos llamar al a función split con el carácter | y nos retornará un array así ["0,0,0","255,241,232","41,173,255"].

Ahora que tenemos los grupos RGB debemos llamar nuevamente la función split pero esta vez dentro del ciclo for para poder obtener los valores RGB de cada LED. Si le pasamos a split un string así "255,241,232" y un carácter separador , este nos regresará un array así ["255","241","232"] aquí ya tenemos el valor RGB pero aún no es un valor númerico por lo que debemos convertirlo, esto lo podemos hacer con el método toInt() de la clase String.

Aquí lo único que nos hace falta es hacer una última transformación ya que la librería de Adafruit necesita un entero de 32bit. Eso lo hacemos con esta función uint32_t color = strip.Color(r,g,b); que toma 3 números enteros de 8 bit y los transforma a un entero de 32 bit. Finalmente usamos el método setPixelColor de la clase Adafruit_NeoPixel para indicar de que color queremos que se vaya a encender un pixel en específico de la matriz.

Finalmente tenemos el proyecto completamente ensamblado y podemos ver su resultado final a continuación.

0:00
/0:27

Video mostrando las imágenes tomadas aleatoreamente desde internet

Diagrama de conexión

Diagrama de conexión

Muchas gracias por haber llegado hasta el final de esta publicación, nos vemos en una próxima entrega.

¿Te resultó útil este artículo? ☕ Invítame a un café