Librería PantallaE213 (versión 2)


Presentación

En esta actualizacion de la librería PantallaE213 se agregan las funciones para el manejo de imagenes con la pantalla de tinta electrónica que trae la placa Vision Master E213 (HT-VME213).

Las funciones agregadas permiten el manejo de las imagenes de forma tal que al cargar la imagen en la pantalla esta ocupe la totalidad de la misma o una parte.

Debido a las limitaciones de hardware y software de la placa Vision Master E213 (HT-VME213) las imagenes para ser procesadas y mostradas en la pantalla deben estar en un archivo binario, tener un formato fijo de 122×250 (ANCHO_FISICO × ALTO_FISICO) y medir exactamente 4000 bytes para que ocupe toda la pantalla y si tiene un formato de menor tamaño ocupara solo parcialmente la misma.


Descripción y funcionamiento de las nuevas funciones

Función:cargar_imagen_binaria(filename, x=0, y=0, ancho_img=0, alto_img=0)

Propósito
Cargar una imagen binaria desde un archivo y:
– Si el archivo tiene exactamente 4000 bytes → reemplazar todo el buffer de la pantalla (imagen completa). En este caso no deben pasarse los valores correspondientes a posición y tamaño de la imagen
– Si el archivo tiene otro tamaño → se procesara como una imagen parcial para dibujarla en la posición (x, y) usando la funcion interna _dibujar_imagen_buffer.
Para ello si es necesarios pasar los valores correspondientes a la posicion y tamaño de la imagen.

Funcionamiento paso a paso

Lectura del archivo:
    • El archivo especificado en filename se abre en modo binario y se carga íntegramente en memoria.
Detección automática de imagen completa:
    • Si no se especifican ancho_img ni alto_img, la función asume que se intenta cargar una imagen de pantalla completa
    • Verifica que el tamaño del archivo coincida exactamente con self.buffer_size (que en tu caso es 4000 bytes, correspondientes a un buffer interno de 250×128 píxeles, aunque solo se visualizan 122 filas).
    • Si el tamaño coincide, copia directamente el contenido al buffer de la pantalla (operación rápida y eficiente), si no coincide da ERROR: Debe especificar ancho_img y alto_img para imágenes no-completas.
Imágenes parciales (requieren dimensiones explícitas):
    • Si se proporcionan ancho_img y alto_img, la función interpreta el archivo como una imagen de tamaño arbitrario.
    • Llama a _dibujar_imagen_buffer() para renderizarla en la posición (x, y).
    • En este caso la imagen se despliega a partir de la posición x, y y si excede el tamaño de la pantalla se muestra parcialmente.
Manejo de errores:
    • Cualquier fallo (archivo inexistente, permisos, etc.) se captura y reporta, devolviendo False.

Función:_dibujar_imagen_buffer(datos, x, y, ancho_imagen, alto_imagen)

Propósito
Dibujar una imagen binaria de dimensiones arbitrarias en una posición específica (x, y) del buffer de la pantalla, respetando los límites físicos visibles (250×122 píxeles). Es una función auxiliar privada, diseñada para renderizar imágenes parciales como íconos, etiquetas o secciones dinámicas.

Funcionamiento técnico

Formato de los datos
– La imagen está codificada en 1 bit por píxel (monocromática).
– Cada byte representa 8 píxeles horizontales consecutivos, con el bit más significativo (MSB) primero:
    • Bit 7 → píxel 0 de la fila
    • Bit 6 → píxel 1
    • …
    • Bit 0 → píxel 7
– Ejemplo: el byte 0b10100000 dibuja negro-blanco-negro-blanco-blanco-blanco-blanco-blanco.

Cálculo del tamaño esperado
– Bytes por fila:
    bytes_por_fila = ⌈ancho_imagen / 8⌉
– Tamaño total esperado:
    tamaño_esperado = bytes_por_fila × alto_imagen
– Si len(datos) no coincide, se imprime una advertencia, pero la función continúa procesando los datos disponibles (sin rellenar con ceros; simplemente se detiene al agotar los bytes).

Recorrido para la escritura del buffer
– Itera por cada fila (0 a alto_imagen - 1) y dentro de cada fila, por cada byte.
– Para cada bit del byte:
    • Calcula la posición absoluta:
        x_actual = x + byte_idx * 8 + bit
        y_actual = y + fila
    • Ignora píxeles fuera del área visible:
        0 ≤ x_actual < 250
        0 ≤ y_actual < 122
        (esto evita accesos inválidos al buffer o dibujos fuera de la pantalla).
    • Asigna color:
        – Bit = 1 → dibujar_pixel(..., NEGRO)
        – Bit = 0 → dibujar_pixel(..., BLANCO)

Compatibilidad con el hardware
– Aunque el controlador del display usa un buffer interno de 250×128 píxeles (4000 bytes), esta función solo opera dentro del área visible de 250×122.
– Esto garantiza que las actualizaciones parciales nunca afecten filas no visibles ni causen desbordamientos.

Código de la Librería

En virtud que estoy presentando una actualizacion del codigo de la Libreria: Pantalla E213 primero pongo el codigo correspondiente las funciones que se agregan.

Funciones agregadas


    # ===============================================================
    #                CARGAR IMAGEN BINARIA DESDE ARCHIVO
    # ===============================================================
    def cargar_imagen_binaria(self, filename, x=0, y=0, ancho_img=None, alto_img=None):
        try:
            with open(filename, 'rb') as f:
                datos = f.read()
            
            # Si no se especifican dimensiones, intentar calcularlas o asumir pantalla completa
            if ancho_img is None or alto_img is None:
                # Verificar si es imagen de pantalla completa
                if len(datos) == self.buffer_size:
                    self.buffer[:] = datos
                    return True
                else:
                    print(f"ERROR: Debe especificar ancho_img y alto_img para imágenes no-completas")
                    return False
            
            # Dibujar en posición específica con dimensiones conocidas
            self._dibujar_imagen_buffer(datos, x, y, ancho_img, alto_img)
            return True
            
        except Exception as e:
            print(f"Error cargando imagen binaria: {e}")
            return False

    def _dibujar_imagen_buffer(self, datos, x, y, ancho_imagen, alto_imagen):
        bytes_por_fila = (ancho_imagen + 7) // 8
        tamaño_esperado = bytes_por_fila * alto_imagen
        
        
        if len(datos) != tamaño_esperado:
            print(f"ADVERTENCIA: Tamaño de datos no coincide con dimensiones especificadas")
            print(f"Esperado: {tamaño_esperado} bytes, Recibido: {len(datos)} bytes")
        
        # Procesar fila por fila
        for fila in range(alto_imagen):
            y_actual = y + fila
            
            # Verificar límites verticales
            if y_actual < 0 or y_actual >= ALTO_FISICO:
                continue
            
            # Procesar cada byte de la fila
            for byte_idx in range(bytes_por_fila):
                pos = fila * bytes_por_fila + byte_idx
                
                # Verificar que no excedamos los datos
                if pos >= len(datos):
                    break
                
                byte_actual = datos[pos]
                
                # Desempaquetar los 8 bits del byte
                for bit in range(8):
                    x_pixel = byte_idx * 8 + bit
                    
                    # Solo procesar píxeles dentro del ancho de la imagen
                    if x_pixel >= ancho_imagen:
                        break
                    
                    x_actual = x + x_pixel
                    
                    # Verificar límites horizontales
                    if x_actual < 0 or x_actual >= ANCHO_FISICO:
                        continue
                    
                    # Bit activo (1) = píxel negro, Bit inactivo (0) = píxel blanco
                    if byte_actual & (0x80 >> bit):
                        self.dibujar_pixel(x_actual, y_actual, NEGRO)
                    else:
                        self.dibujar_pixel(x_actual, y_actual, BLANCO)


A continuacion coloco el archivo pantalla_e213_v2.py que tiene la version actualizada de la libreria y que al igual que la version 1 debe complementarse con los archivos fuente_8x8.py y fuente_12x16.py, que contiene las fuentes para textos.

Archivo: pantalla_e213_v2.py

# pantalla_e213_v2.py - Controlador para display E-ink Heltec Vision Master E213

from machine import Pin, SPI
import time

# ===== CARGA DE FUENTES =====
from fuente_8x8 import PATRONES as PATRONES_8x8
from fuente_12x16 import PATRONES_12x16

# ===== CONFIGURACIÓN DE HARDWARE =====
PIN_DC = 2
PIN_CS = 5
PIN_BUSY = 1
PIN_RST = 3
PIN_VEXT = 18

SPI_ID = 1
PIN_SCK = 4
PIN_MOSI = 6

ANCHO_FISICO = 122
ALTO_FISICO = 250

NEGRO = 1
BLANCO = 0

# ===============================================================
#               CLASE DISPLAY E213
# ===============================================================
class PantallaE213:
    def __init__(self, orientacion=0):

        self.orientacion = orientacion

        self.pin_dc = Pin(PIN_DC, Pin.OUT)
        self.pin_cs = Pin(PIN_CS, Pin.OUT)
        self.pin_busy = Pin(PIN_BUSY, Pin.IN)
        self.pin_rst = Pin(PIN_RST, Pin.OUT)
        self.pin_vext = Pin(PIN_VEXT, Pin.OUT)

        # SPI más rápido y estable
        self.spi = SPI(
            SPI_ID,
            baudrate=4000000,  # DOBLADO (funciona perfecto en ESP32S3)
            polarity=0,
            phase=0,
            bits=8,
            firstbit=SPI.MSB,
            sck=Pin(PIN_SCK),
            mosi=Pin(PIN_MOSI)
        )

        # Tamaño buffer
        self.bytes_por_fila = (ANCHO_FISICO + 7) // 8
        self.buffer_size = self.bytes_por_fila * ALTO_FISICO
        self.buffer = bytearray(self.buffer_size)

        # Dimensiones lógicas
        if orientacion == 0:
            self.ancho = ANCHO_FISICO
            self.alto = ALTO_FISICO
        elif orientacion == 90:
            self.ancho = ALTO_FISICO
            self.alto = ANCHO_FISICO
        else:
            raise ValueError("Orientación debe ser 0 o 90")

        # buffer para envío rápido de comando/dato
        self._buf1 = bytearray(1)

        self.inicializado = False

    # ===============================================================
    #                     FUNCIONES BAJO NIVEL
    # ===============================================================
    def enviar_comando(self, cmd):
        """Envío rápido de comando"""
        self._buf1[0] = cmd
        self.pin_dc.value(0)
        self.pin_cs.value(0)
        self.spi.write(self._buf1)
        self.pin_cs.value(1)

    def enviar_dato(self, dato):
        """Envío rápido de dato"""
        self._buf1[0] = dato
        self.pin_dc.value(1)
        self.pin_cs.value(0)
        self.spi.write(self._buf1)
        self.pin_cs.value(1)

    # ===============================================================
    def _esperar_listo(self, timeout=5000):
        start = time.ticks_ms()

        while True:
            if self.pin_busy.value() == 1:
                return True
            if time.ticks_diff(time.ticks_ms(), start) > timeout:
                return False
            time.sleep_ms(25)

    # ===============================================================
    #                     INICIALIZACIÓN
    # ===============================================================
    def inicializar(self):
        if self.inicializado:
            return

        self.pin_vext.value(1)
        time.sleep_ms(100)

        # Reset rápido
        self.pin_rst.value(0)
        time.sleep_ms(10)
        self.pin_rst.value(1)
        time.sleep_ms(100)

        self._esperar_listo(2000)

        self.enviar_comando(0x00) 
        self.enviar_dato(0x0F)

        self.enviar_comando(0x50)
        self.enviar_dato(0x97)

        self._cargar_luts()

        self.inicializado = True

    # ===============================================================

    def _cargar_luts(self):
        """Carga los LUTs reales del display LCMEN2R13EFC1 (parcial)."""

        LUT_VCOM = [
            0x01, 0x06, 0x03, 0x02, 0x01, 0x01, 0x01,
            0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        LUT_WW = [
            0x01, 0x06, 0x03, 0x02, 0x81, 0x01, 0x01,
            0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        LUT_BW = [
            0x01, 0x86, 0x83, 0x82, 0x81, 0x01, 0x01,
            0x01, 0x86, 0x82, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        LUT_WB = [
            0x01, 0x46, 0x43, 0x02, 0x01, 0x01, 0x01,
            0x01, 0x46, 0x42, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        LUT_BB = [
            0x01, 0x06, 0x03, 0x42, 0x41, 0x01, 0x01,
            0x01, 0x06, 0x02, 0x01, 0x01, 0x01, 0x01,
        ] + [0x00] * 42

        luts = {
            0x20: LUT_VCOM,
            0x21: LUT_WW,
            0x22: LUT_BW,
            0x23: LUT_WB,
            0x24: LUT_BB,
        }

        for cmd, tabla in luts.items():
            self.enviar_comando(cmd)
            for v in tabla:
                self.enviar_dato(v)

    # ===============================================================
    #                            DIBUJO
    # ===============================================================
    def dibujar_pixel(self, x, y, color=NEGRO):

        if x < 0 or y < 0 or x >= self.ancho or y >= self.alto:
            return

        # Transformación más rápida
        if self.orientacion == 0:
            xf, yf = x, y
        else:
            xf = y
            yf = ALTO_FISICO - 1 - x

        pos = yf * self.bytes_por_fila + (xf >> 3)
        mask = 1 << (7 - (xf & 7))

        if color:
            self.buffer[pos] |= mask
        else:
            self.buffer[pos] &= ~mask

    # ===============================================================
    def limpiar(self, color=BLANCO):
        v = 0xFF if color else 0x00
        self.buffer[:] = bytes([v]) * self.buffer_size

    # ===============================================================
    #                           TEXTO
    # ===============================================================
    def dibujar_texto(self, txt, x, y, color=NEGRO, tamaño=8):
        if tamaño == 8:
            for c in txt:
                self._char8(c, x, y, color)
                x += 8
        elif tamaño == 16:
            for c in txt:
                self._char16(c, x, y, color)
                x += 12

    def _char8(self, c, x, y, color):
        p = PATRONES_8x8.get(c)
        if not p:
            for i in range(8):
                self.dibujar_pixel(x, y+i, color)
                self.dibujar_pixel(x+7, y+i, color)
            for i in range(8):
                self.dibujar_pixel(x+i, y, color)
                self.dibujar_pixel(x+i, y+7, color)
            return

        for fy, fila in enumerate(p):
            for cx in range(8):
                if fila & (1 << (7 - cx)):
                    self.dibujar_pixel(x + cx, y + fy, color)

    def _char16(self, c, x, y, color):
        p = PATRONES_12x16.get(c)
        if not p:
            for i in range(12):
                self.dibujar_pixel(x+i, y, color)
                self.dibujar_pixel(x+i, y+15, color)
            for i in range(16):
                self.dibujar_pixel(x, y+i, color)
                self.dibujar_pixel(x+11, y+i, color)
            return

        for fy, fila in enumerate(p):
            for cx in range(12):
                if fila & (1 << (15 - cx)):
                    self.dibujar_pixel(x + cx, y + fy, color)

    # ===============================================================
    #                       LINEAS Y RECTANGULOS
    # ===============================================================

    #         Linea horizontal

    def dibujar_linea_horizontal(self, x, y, longitud, color=NEGRO):
        """Dibuja una línea horizontal optimizada (usa escritura por bytes)."""
        if y < 0 or y >= self.alto:
            return

        if x < 0:
            longitud += x
            x = 0
        if x + longitud > self.ancho:
            longitud = self.ancho - x
        if longitud <= 0:
            return

        # Recalculamos posiciones físicas según la orientación
        for i in range(longitud):
            self.dibujar_pixel(x + i, y, color)

    #               Linea vertical

    def dibujar_linea_vertical(self, x, y, longitud, color=NEGRO):
        """Dibuja una línea vertical optimizada."""
        if x < 0 or x >= self.ancho:
            return

        if y < 0:
            longitud += y
            y = 0
        if y + longitud > self.alto:
            longitud = self.alto - y
        if longitud <= 0:
            return

        for i in range(longitud):
            self.dibujar_pixel(x, y + i, color)

    #              Rectangulo

    def dibujar_rectangulo(self, x, y, ancho, alto, color=NEGRO, relleno=False):
        """Dibuja un rectángulo (con o sin relleno)."""
        if ancho <= 0 or alto <= 0:
            return

        if relleno:
            # Relleno por líneas horizontales (más rápido)
            for dy in range(alto):
                self.dibujar_linea_horizontal(x, y + dy, ancho, color)
        else:
            # Bordes (4 líneas)
            self.dibujar_linea_horizontal(x, y, ancho, color)
            self.dibujar_linea_horizontal(x, y + alto - 1, ancho, color)
            self.dibujar_linea_vertical(x, y, alto, color)
            self.dibujar_linea_vertical(x + ancho - 1, y, alto, color)


    # ===============================================================
    #                CARGAR IMAGEN BINARIA DESDE ARCHIVO
    # ===============================================================
    def cargar_imagen_binaria(self, filename, x=0, y=0, ancho_img=None, alto_img=None):
        try:
            with open(filename, 'rb') as f:
                datos = f.read()
            
            # Si no se especifican dimensiones, intentar calcularlas o asumir pantalla completa
            if ancho_img is None or alto_img is None:
                # Verificar si es imagen de pantalla completa
                if len(datos) == self.buffer_size:
                    self.buffer[:] = datos
                    return True
                else:
                    print(f"ERROR: Debe especificar ancho_img y alto_img para imágenes no-completas")
                    return False
            
            # Dibujar en posición específica con dimensiones conocidas
            self._dibujar_imagen_buffer(datos, x, y, ancho_img, alto_img)
            return True
            
        except Exception as e:
            print(f"Error cargando imagen binaria: {e}")
            return False

    def _dibujar_imagen_buffer(self, datos, x, y, ancho_imagen, alto_imagen):
        bytes_por_fila = (ancho_imagen + 7) // 8
        tamaño_esperado = bytes_por_fila * alto_imagen
        
        
        if len(datos) != tamaño_esperado:
            print(f"ADVERTENCIA: Tamaño de datos no coincide con dimensiones especificadas")
            print(f"Esperado: {tamaño_esperado} bytes, Recibido: {len(datos)} bytes")
        
        # Procesar fila por fila
        for fila in range(alto_imagen):
            y_actual = y + fila
            
            # Verificar límites verticales
            if y_actual < 0 or y_actual >= ALTO_FISICO:
                continue
            
            # Procesar cada byte de la fila
            for byte_idx in range(bytes_por_fila):
                pos = fila * bytes_por_fila + byte_idx
                
                # Verificar que no excedamos los datos
                if pos >= len(datos):
                    break
                
                byte_actual = datos[pos]
                
                # Desempaquetar los 8 bits del byte
                for bit in range(8):
                    x_pixel = byte_idx * 8 + bit
                    
                    # Solo procesar píxeles dentro del ancho de la imagen
                    if x_pixel >= ancho_imagen:
                        break
                    
                    x_actual = x + x_pixel
                    
                    # Verificar límites horizontales
                    if x_actual < 0 or x_actual >= ANCHO_FISICO:
                        continue
                    
                    # Bit activo (1) = píxel negro, Bit inactivo (0) = píxel blanco
                    if byte_actual & (0x80 >> bit):
                        self.dibujar_pixel(x_actual, y_actual, NEGRO)
                    else:
                        self.dibujar_pixel(x_actual, y_actual, BLANCO)




    # ===============================================================
    #                             ACTUALIZACIÓN
    # ===============================================================
    def actualizar(self):
        self.inicializar()

        # Power on
        self.enviar_comando(0x04)
        self._esperar_listo(3000)

        # OLD DATA (pantalla anterior)
        self.enviar_comando(0x10)
        self.pin_dc.value(1)
        self.pin_cs.value(0)
        self.spi.write(bytes([0xFF]) * len(self.buffer))  # SIEMPRE BLANCO
        self.pin_cs.value(1)

        # NEW DATA (imagen actual)
        self.enviar_comando(0x13)
        self.pin_dc.value(1)
        self.pin_cs.value(0)
        self.spi.write(self.buffer)
        self.pin_cs.value(1)

        # REFRESH
        self.enviar_comando(0x12)
        self._esperar_listo(10000)

        self.inicializado = False

    # ===============================================================
    def dormir(self):
        self.enviar_comando(0x02)
        self._esperar_listo(2000)
        self.enviar_comando(0x07)
        self.enviar_dato(0xA5)
        self.pin_vext.value(0)
        self.inicializado = False
📥 Descargar pantalla_e213_v2.py

Implementación basica

Funcionamiento:
– Si ya tiene instalado y funcionando el archivo pantalla_e213_v1.py en la placa solo debe reemplazar ese archivo por el archivo pantalla_e213_v2.py y ya queda listo para su uso. En caso contrario recomiendo revisar la instalacion prevista en Librería PantallaE213
– El siguiente archivo prueba_imagen_total.py es un ejemplo de un programa para mostrar una imagen que ocupa toda la pantalla

Archivo: prueba_imagen_total.py


from pantalla_e213_v2 import PantallaE213, NEGRO, BLANCO


pantalla = PantallaE213(orientacion=90)
pantalla.limpiar(BLANCO)



# Cargar y mostrar imagen
pantalla.cargar_imagen_binaria("portada.bin")
#"portada.bin" debe cambiarse por el nombre del archivo binario de imagen que se utilice

pantalla.actualizar()

pantalla.dormir()
📥 Descargar prueba_imagen_total.py

Si no dispone de un archivo de imagen binario de 122×250 (ANCHO_FISICO × ALTO_FISICO) y 4000 bytes puede descargar el siguiente archivo: portada.bin


📥 Descargar portada.bin

– A continuacion el archivo prueba_imagen_parcial.py es un ejemplo de un programa para mostrar una imagen que ocupa parte la pantalla a partir de las coordenadas x, y que se le proporcionan:

Archivo: prueba_imagen_parcial.py


from pantalla_e213_v2 import PantallaE213, NEGRO, BLANCO


pantalla = PantallaE213(orientacion=90)
pantalla.limpiar(BLANCO)



# Cargar y mostrar imagen

pantalla.cargar_imagen_binaria("flor.bin", 50, 10, 75, 75)




pantalla.dibujar_texto("Esta es una flor", 40, 90, NEGRO, 16)

pantalla.actualizar()

pantalla.dormir()
📥 Descargar prueba_imagen_parcial.py

De no disponer de un archivo de imagen binario para este ejemplo se puede descargar el siguiente archivo: flor.bin


📥 Descargar flor.bin

Observacion Final:

Los archivos de imagen utilizados por la librería Librería PantallaE213 siguen un formato binario crudo sin encabezado, optimizado para pantallas e-Ink monocromáticas como la LCMEN2R13EFC1 (250×122 píxeles).

Características generales:
1 bit por píxel: cada bit representa un píxel.
    • 1 → píxel negro
    • 0 → píxel blanco
Sin compresión ni metadatos: el archivo es un volcado directo del buffer de imagen.
Orden de bits: dentro de cada byte, los bits se interpretan de más significativo a menos significativo (MSB primero).
    • Ejemplo: en el byte 0b10110000, los primeros tres píxeles son: negro, blanco, negro, y el resto blanco.
Almacenamiento por filas: los datos se organizan fila a fila, de arriba hacia abajo.

Tamaño del archivo
El tamaño depende del tipo de imagen:

Tipo de imagen: Pantalla completa
Dimensiones esperadas: 250 × 128 (buffer interno)
Tamaño en bytes: 4000 bytes
Uso en el sistema: Se carga sin ancho_img/alto_img. Aunque solo se ven 122 filas, el controlador requiere las 128 (6 filas adicionales de padding).

Tipo de imagen: Imagen parcial
Dimensiones esperadas: ancho_img × alto_img (arbitrario)
Tamaño en bytes: ⌈ancho_img / 8⌉ × alto_img
Uso en el sistema: Se debe especificar ancho_img y alto_img al cargar. Solo se dibujan los píxeles dentro del área visible (0–249 en X, 0–121 en Y).


En breve subire los programa necesarios para convertir imagenes png en archivos binarios utilizados por esta librería

Cerrar