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
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()
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
– 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()
De no disponer de un archivo de imagen binario para este ejemplo se puede descargar el siguiente archivo: 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