Saltar al contenido principal

Recetario de Arduino: pantalla de tinta electrónica (reTerminal E Serie)

¿Buscas los periféricos de hardware?

Esta página se centra en controlar la pantalla de tinta electrónica desde Arduino. Si quieres usar el LED integrado, el zumbador, los botones, el sensor SHT4x, el monitor de batería o la ranura para tarjeta microSD, dirígete a Arduino Cookbook: Onboard Peripherals. Para el RTC, los modos de bajo consumo y el micrófono integrado, consulta Arduino Cookbook: RTC, Low Power & Audio.

El código base compartido — configuración del IDE de Arduino, paquete de placas ESP32, instalación de Seeed_GFX, generación de driver.h — también se encuentra en Work with Arduino. Échale un vistazo primero si eres nuevo en Arduino en pantallas de tinta electrónica de Seeed.

Introducción

La reTerminal E Serie es la línea HMI industrial de Seeed Studio, basada en la XIAO ESP32-S3 y que incorpora pantallas de tinta electrónica. Este recetario te guía por todo lo que necesitas para renderizar texto, gráficos e imágenes en la pantalla:

  • Descripción de hardware y enlaces de compra para E1001 / E1002 / E1003 / E1004.
  • Configuración del entorno del IDE de Arduino para los cuatro modelos (placa XIAO_ESP32S3, OPI PSRAM).
  • Un primer Hello World en cada modelo usando la biblioteca Seeed_GFX (con el BOARD_SCREEN_COMBO correspondiente).
  • Ejemplos avanzados específicos de cada panel con Seeed_GFX — escala de grises de 4 niveles en el E1001 y escala de grises de 16 niveles en el E1003.
  • Un Hello World alternativo usando la popular biblioteca GxEPD2.
  • Consejos de resolución de problemas para fallos de refresco de la pantalla de tinta electrónica y errores de carga.

Materiales necesarios

Para completar este tutorial, prepara uno de los siguientes dispositivos reTerminal E Serie:

Preparación del entorno

Para programar la pantalla de tinta electrónica reTerminal E Serie con Arduino, necesitarás configurar el IDE de Arduino con soporte para ESP32.

tip

Si es la primera vez que usas Arduino, te recomendamos encarecidamente que consultes Getting Started with Arduino.

Configuración del IDE de Arduino

Paso 1. Descarga e instala el Arduino IDE y lanza la aplicación de Arduino.


Paso 2. Añade soporte para la placa ESP32 al IDE de Arduino.

En el IDE de Arduino, ve a File > Preferences y añade la siguiente URL en el campo "Additional Boards Manager URLs":

https://raw.githubusercontent.com/espressif/arduino-esp32/gh-pages/package_esp32_index.json

Paso 3. Instala el paquete de placas ESP32.

Navega a Tools > Board > Boards Manager, busca "esp32" e instala el paquete ESP32 de Espressif Systems.

Paso 4. Selecciona la placa correcta.

Ve a Tools > Board > ESP32 Arduino y selecciona XIAO_ESP32S3.

Paso 5. Conecta tu pantalla de tinta electrónica reTerminal E Serie a tu ordenador usando un cable USB-C.

Paso 6. Selecciona el puerto correcto desde Tools > Port.

Programación de la pantalla de tinta electrónica

El reTerminal E1001 incorpora una pantalla de tinta electrónica en blanco y negro de 7,5 pulgadas, mientras que el reTerminal E1002 está equipado con una pantalla de tinta electrónica a todo color de 7,3 pulgadas. Ambas pantallas ofrecen una excelente visibilidad en diversas condiciones de iluminación con un consumo de energía ultrabajo, lo que las hace ideales para aplicaciones industriales que requieren pantallas siempre encendidas con un consumo mínimo de energía.

Uso de la biblioteca Seeed_GFX

Para controlar la pantalla de tinta electrónica, usaremos la biblioteca Seeed_GFX, que proporciona compatibilidad completa con varios dispositivos de visualización de Seeed Studio.

Paso 1. Descarga la biblioteca Seeed_GFX desde GitHub:


Paso 2. Instala la biblioteca añadiendo el archivo ZIP en el IDE de Arduino. Ve a Sketch > Include Library > Add .ZIP Library y selecciona el archivo ZIP descargado.

nota

Si has instalado previamente la biblioteca TFT_eSPI, puede que necesites eliminarla temporalmente o cambiarle el nombre en la carpeta de bibliotecas de Arduino para evitar conflictos, ya que Seeed_GFX es un fork de TFT_eSPI con funciones adicionales para pantallas de Seeed Studio.

Programar reTerminal E1001 (pantalla de tinta electrónica en blanco y negro de 7,5 pulgadas)

Vamos a explorar un ejemplo sencillo que demuestra operaciones básicas de dibujo en la pantalla de tinta electrónica en blanco y negro.

Paso 1. Abre el sketch de ejemplo de la biblioteca Seeed_GFX: File > Examples > Seeed_GFX > ePaper > Basic > HelloWorld

Paso 2. Activa OPI PSRAM en el IDE de Arduino: Tools > PSRAM > OPI PSRAM

Paso 3. Crea un nuevo archivo llamado driver.h en la misma carpeta que tu sketch. Puedes hacerlo haciendo clic en el botón de flecha en el IDE de Arduino y seleccionando "New Tab", luego nombrándolo driver.h.

Paso 4. Copia el código de configuración generado y pégalo en el archivo driver.h. El código debería verse así:

#define BOARD_SCREEN_COMBO 520 // reTerminal E1001 (UC8179)

Paso 5. Carga el sketch en tu reTerminal E1001. Deberías ver en la pantalla varios gráficos, incluidas líneas, texto y formas que demuestran las capacidades básicas de dibujo.

Escala de grises multinivel con Seeed_GFX

Los sketches de Hello World anteriores son intencionalmente mínimos para que quepan en todos los modelos. Los paneles monocromos en E1001 y E1003 en realidad admiten escala de grises multinivel además del simple blanco y negro: 4 niveles en E1001 y 16 niveles en E1003, y Seeed_GFX expone ambos modos mediante epaper.initGrayMode(...) más un conjunto de constantes de paleta TFT_GRAY_*. Los dos ejemplos siguientes recorren cada uno.

Escala de grises de 4 niveles en reTerminal E1001

El panel monocromo de 7,5" del reTerminal E1001 puede representar 4 niveles de escala de grises en lugar de puro blanco y negro. Seeed_GFX expone esto mediante epaper.initGrayMode(GRAY_LEVEL4) y cuatro constantes de paleta:

ConstanteTono representado
TFT_GRAY_0Negro
TFT_GRAY_1Gris oscuro
TFT_GRAY_2Gris claro
TFT_GRAY_3Blanco

En el siguiente ejemplo primero se pintan cuatro franjas horizontales — una por nivel de gris — para que puedas verificar visualmente la paleta, y luego se copia un mapa de bits en escala de grises de 800×480 en la pantalla. La biblioteca Seeed_GFX ya incluye esto como un ejemplo listo para flashear, incluyendo el image.h preconvertido, por lo que no necesitas generar tú mismo ningún dato de mapa de bits.

Paso 1. Abre el sketch de ejemplo de la biblioteca Seeed_GFX: File > Examples > Seeed_GFX > ePaper > Gray > GrayLevel4. El sketch y su archivo image.h asociado se abrirán en el editor.

Paso 2. Habilita OPI PSRAM en el IDE de Arduino: Tools > PSRAM > OPI PSRAM.

Paso 3. Añade un archivo driver.h junto al ejemplo (mismo flujo de trabajo que el Hello World) y selecciona la combinación de placa y pantalla E1001:

#define BOARD_SCREEN_COMBO 520 // reTerminal E1001 (UC8179)

Paso 4. Sube el sketch. La pantalla primero muestra cuatro franjas en escala de grises — negro en la parte superior, luego gris oscuro, gris claro y blanco en la parte inferior — y luego se limpia y renderiza el mapa de bits desde image.h.

Como referencia, el sketch de ejemplo se ve así:

/*
* 4-Level Grayscale demo for reTerminal E1001
* (Seeed_GFX/examples/ePaper/Gray/Shape/Shape.ino)
*
* The 7.5" monochrome panel supports 4 gray levels:
* TFT_GRAY_0 black
* TFT_GRAY_1 dark gray
* TFT_GRAY_2 light gray
* TFT_GRAY_3 white
*/
#include "TFT_eSPI.h"
#include "image.h"

#ifdef EPAPER_ENABLE // Defined when an ePaper combo is selected in driver.h
EPaper epaper;
#endif

void setup() {
#ifdef EPAPER_ENABLE
epaper.begin();
epaper.fillScreen(TFT_WHITE);
epaper.update();
epaper.initGrayMode(GRAY_LEVEL4);

// Draw four horizontal stripes, one per gray level
epaper.fillRect(0, 0, epaper.width(), epaper.height() / 4, TFT_GRAY_0);
epaper.fillRect(0, epaper.height() * 1 / 4, epaper.width(), epaper.height() / 4, TFT_GRAY_1);
epaper.fillRect(0, epaper.height() * 2 / 4, epaper.width(), epaper.height() / 4, TFT_GRAY_2);
epaper.fillRect(0, epaper.height() * 3 / 4, epaper.width(), epaper.height() / 4, TFT_GRAY_3);
epaper.update();

// Then clear and show a 800x480 grayscale bitmap from image.h
epaper.fillScreen(TFT_GRAY_3);
epaper.pushImage(0, 0, 800, 480, (uint16_t *)L4_GRAY);
epaper.update();
#endif
}

void loop() {
// Nothing to do — ePaper holds the last frame without power
}
¿Quieres usar tu propia imagen?

El arreglo L4_GRAY en image.h es simplemente un mapa de bits en escala de grises de 800×480 preconvertido a un arreglo en C. Para sustituir tu propia imagen, regenera el arreglo a partir de una fuente en escala de grises de 800×480 usando cualquier conversor estándar de "imagen a arreglo C" y reemplaza L4_GRAY en image.h. El sketch en sí no necesita cambiar.

tip

El refresco en escala de grises de 4 niveles es aproximadamente 4 veces más lento que una actualización en blanco y negro de 1 bit porque el controlador lleva cada píxel a través de cuatro voltajes objetivo en lugar de dos. Úsalo para contenido estático como fotos, ilustraciones o paneles con muchos detalles, y mantente en el modo estándar de 1 bit para actualizaciones rápidas de la interfaz de usuario.

Uso de la biblioteca GxEPD2

Además de Seeed_GFX, también puedes usar la biblioteca GxEPD2 para controlar la pantalla de tinta electrónica del reTerminal. Seeed ha hecho un fork de la popular biblioteca GxEPD2 y ha añadido soporte dedicado para la serie reTerminal E10xx, lo que la convierte en la opción recomendada para los usuarios de reTerminal.

Instalación de la biblioteca Seeed_GxEPD2

Para usar esta biblioteca con productos reTerminal, necesitas instalar Seeed_GxEPD2, el fork personalizado de Seeed adaptado específicamente para la serie reTerminal E10xx.

Paso 1. Ve al repositorio de GitHub de Seeed_GxEPD2. Haz clic en el botón "Code" y luego selecciona "Download ZIP" para guardar la biblioteca en tu ordenador.


Paso 2. En el IDE de Arduino, instala la biblioteca desde el archivo descargado. Ve a Sketch > Include Library > Add .ZIP Library... y selecciona el archivo ZIP que acabas de descargar.

Paso 3. La biblioteca Seeed_GxEPD2 requiere la Adafruit GFX Library para funcionar, que también debes instalar. La forma más sencilla de hacerlo es a través del Library Manager: ve a Tools > Manage Libraries..., busca "Adafruit GFX Library" y haz clic en "Install".

nota

Seeed_GxEPD2 es el fork personalizado de Seeed de la biblioteca original GxEPD2, con controladores y optimizaciones dedicados para la serie reTerminal E10xx. Recomendamos encarecidamente usar este fork en lugar de la biblioteca original para garantizar la compatibilidad total con tu dispositivo reTerminal.

Programación del reTerminal E1001 (pantalla en blanco y negro de 7,5")

El reTerminal E1001 incorpora una pantalla de tinta electrónica en blanco y negro de 7,5" (800×480, panel GDEY075T7, controlador UC8179). El siguiente ejemplo muestra múltiples pantallas, incluyendo una de inicio, información del sistema, tipografía, geometría, patrones y un diseño de panel de control.

Después de instalar la biblioteca Seeed_GxEPD2, puedes encontrar este ejemplo en el IDE de Arduino mediante File > Examples > Seeed_GxEPD2 > GxEPD2_reTerminal_E1001, o localizarlo manualmente en Seeed_GxEPD2/examples/GxEPD2_reTerminal_E1001/GxEPD2_reTerminal_E1001.ino.

Haz clic aquí para ver el código completo
#include <SPI.h>
#include <GxEPD2_BW.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <Fonts/FreeSansBold24pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans9pt7b.h>
#include <Fonts/FreeMono9pt7b.h>

// ===== Pin mapping =====
#define EPD_SCK_PIN 7
#define EPD_MOSI_PIN 9
#define EPD_CS_PIN 10
#define EPD_DC_PIN 11
#define EPD_RES_PIN 12
#define EPD_BUSY_PIN 13

SPIClass hspi(HSPI);

// ===== Display: 7.5" B&W 800x480 =====
#define MAX_DISPLAY_BUFFER_SIZE 16000u
#define MAX_HEIGHT(EPD) \
(EPD::HEIGHT <= MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8) \
? EPD::HEIGHT \
: MAX_DISPLAY_BUFFER_SIZE / (EPD::WIDTH / 8))

GxEPD2_BW<GxEPD2_750_GDEY075T7, MAX_HEIGHT(GxEPD2_750_GDEY075T7)>
display(GxEPD2_750_GDEY075T7(EPD_CS_PIN, EPD_DC_PIN, EPD_RES_PIN, EPD_BUSY_PIN));

void setup()
{
Serial.begin(115200);
delay(200);
Serial.println(F("[E1001] GxEPD2 reTerminal E1001 Demo (7.5\" B&W)"));

pinMode(EPD_RES_PIN, OUTPUT);
pinMode(EPD_DC_PIN, OUTPUT);
pinMode(EPD_CS_PIN, OUTPUT);

hspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);
display.epd2.selectSPI(hspi, SPISettings(2000000, MSBFIRST, SPI_MODE0));
display.init(0);

Serial.println(F("[E1001] Screen 1: Splash"));
showSplashScreen();
delay(3000);

Serial.println(F("[E1001] Screen 2: System Info"));
showSystemInfo();
delay(3000);

Serial.println(F("[E1001] Screen 3: Typography"));
showTypographyDemo();
delay(3000);

Serial.println(F("[E1001] Screen 4: Geometry"));
showGeometryDemo();
delay(3000);

Serial.println(F("[E1001] Screen 5: Patterns"));
showPatternDemo();
delay(3000);

Serial.println(F("[E1001] Screen 6: Dashboard"));
showDashboardDemo();

Serial.println(F("[E1001] Demo complete. Hibernating."));
delay(2000);
display.hibernate();
}

void loop() {}

// =====================================================================
void drawCenteredText(const char* text, int16_t y, const GFXfont* font)
{
display.setFont(font);
int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds(text, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor((display.width() - tbw) / 2 - tbx, y);
display.print(text);
}

// =====================================================================
// Screen 1: Splash
// =====================================================================
void showSplashScreen()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.drawRect(10, 10, W - 20, H - 20, GxEPD_BLACK);
display.drawRect(14, 14, W - 28, H - 28, GxEPD_BLACK);

display.setTextColor(GxEPD_BLACK);
drawCenteredText("reTerminal E1001", H / 2 - 60, &FreeSansBold24pt7b);
drawCenteredText("7.5\" e-Paper Display", H / 2 - 10, &FreeSansBold12pt7b);
display.drawFastHLine(W / 4, H / 2 + 10, W / 2, GxEPD_BLACK);
drawCenteredText("GxEPD2 + UC8179 Driver Demo", H / 2 + 45, &FreeSansBold12pt7b);
drawCenteredText("800 x 480 pixels | Black & White", H / 2 + 75, &FreeSans9pt7b);
drawCenteredText("Seeed Studio x GxEPD2", H - 40, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 2: System Info
// =====================================================================
void showSystemInfo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("System Information", 30, &FreeSansBold12pt7b);

display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeMonoBold9pt7b);

const char* labels[] = {"MCU", "Display", "Panel", "Controller", "Interface", "Color Depth"};
char chipBuf[48];
snprintf(chipBuf, sizeof(chipBuf), "ESP32-S3 @ %lu MHz", (unsigned long)ESP.getCpuFreqMHz());
const char* values[] = {chipBuf, "800 x 480", "GDEY075T7", "UC8179", "SPI (HSPI) @ 2MHz", "B&W (1-bit)"};

int y = 95;
for (int i = 0; i < 6; i++) {
display.setCursor(50, y);
display.print(labels[i]);
display.setCursor(250, y);
display.print(": ");
display.print(values[i]);
y += 48;
if (i < 5) display.drawFastHLine(50, y - 18, W - 100, GxEPD_BLACK);
}

display.drawFastHLine(30, H - 40, W - 60, GxEPD_BLACK);
drawCenteredText("reTerminal E1001 | GxEPD2 Demo", H - 20, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 3: Typography
// =====================================================================
void showTypographyDemo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("Typography Demo", 30, &FreeSansBold12pt7b);

display.setTextColor(GxEPD_BLACK);
int y = 85, x = 40;

display.setFont(&FreeSansBold24pt7b);
display.setCursor(x, y); display.print("Sans Bold 24pt");
y += 65;

display.setFont(&FreeSansBold18pt7b);
display.setCursor(x, y); display.print("Sans Bold 18pt");
y += 50;

display.setFont(&FreeSansBold12pt7b);
display.setCursor(x, y); display.print("Sans Bold 12pt - Clean and Modern");
y += 42;

display.setFont(&FreeSans9pt7b);
display.setCursor(x, y); display.print("Sans 9pt - Body text for dense info display.");
y += 40;

display.drawFastHLine(x, y, W - 80, GxEPD_BLACK);
y += 25;

display.setFont(&FreeMonoBold12pt7b);
display.setCursor(x, y); display.print("Mono Bold 12pt");
y += 38;

display.setFont(&FreeMono9pt7b);
display.setCursor(x, y); display.print("Mono 9pt: 0123456789 ABCDEF");
y += 38;

display.setFont(&FreeSansBold18pt7b);
display.setCursor(x, y); display.print("0 1 2 3 4 5 6 7 8 9");

drawCenteredText("Multiple fonts supported", H - 20, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 4: Geometry
// =====================================================================
void showGeometryDemo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("Geometry Demo", 30, &FreeSansBold12pt7b);
display.setTextColor(GxEPD_BLACK);

// Rectangles
for (int i = 0; i < 4; i++)
display.drawRect(40 + i * 80, 60, 60, 50, GxEPD_BLACK);
display.fillRect(40 + 4 * 80, 60, 60, 50, GxEPD_BLACK);

// Circles
for (int i = 0; i < 4; i++)
display.drawCircle(70 + i * 90, 170, 25 + i * 3, GxEPD_BLACK);
for (int i = 0; i < 3; i++)
display.fillCircle(70 + (i + 4) * 90, 170, 25 + i * 3, GxEPD_BLACK);

// Triangles
for (int i = 0; i < 5; i++) {
int tx = 50 + i * 120, sz = 40 + i * 5;
display.drawTriangle(tx, 270 + sz, tx + sz / 2, 270, tx + sz, 270 + sz, GxEPD_BLACK);
}

// Fan of lines
int fcx = 150, fcy = 410;
for (int a = 0; a < 180; a += 12) {
float rad = a * 3.14159f / 180.0f;
display.drawLine(fcx, fcy, fcx + (int)(60 * cosf(rad)), fcy - (int)(60 * sinf(rad)), GxEPD_BLACK);
}

// Concentric circles
for (int r = 8; r <= 56; r += 8)
display.drawCircle(400, 400, r, GxEPD_BLACK);

// Rounded rects
for (int i = 0; i < 3; i++)
display.drawRoundRect(550 + i * 8, 340 + i * 8, 120 - i * 16, 100 - i * 16, 8 + i * 4, GxEPD_BLACK);

drawCenteredText("Adafruit GFX primitives on 7.5\" e-Paper", H - 15, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 5: Patterns
// =====================================================================
void showPatternDemo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("Pattern Demo", 30, &FreeSansBold12pt7b);
display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeSans9pt7b);

int bx = 30, by = 55, bw = 150, bh = 150, gap = 30;

// Checkerboard
display.setCursor(bx + 25, by + 15); display.print("Checker");
by += 20;
for (int py = 0; py < bh / 12; py++)
for (int px = 0; px < bw / 12; px++)
if ((px + py) & 1)
display.fillRect(bx + px * 12, by + py * 12, 12, 12, GxEPD_BLACK);

// H-stripes
int bx2 = bx + bw + gap; int by2 = by - 20;
display.setCursor(bx2 + 25, by2 + 15); display.print("H-Stripes");
by2 += 20;
for (int py = 0; py < bh; py += 8)
if ((py / 8) & 1)
display.fillRect(bx2, by2 + py, bw, 8, GxEPD_BLACK);

// V-stripes
int bx3 = bx2 + bw + gap; int by3 = by2 - 20;
display.setCursor(bx3 + 25, by3 + 15); display.print("V-Stripes");
by3 += 20;
for (int px = 0; px < bw; px += 8)
if ((px / 8) & 1)
display.fillRect(bx3 + px, by3, 8, bh, GxEPD_BLACK);

// Dot grid
int bx4 = bx3 + bw + gap; int by4 = by3 - 20;
display.setCursor(bx4 + 30, by4 + 15); display.print("Dot Grid");
by4 += 20;
for (int py = 0; py < bh; py += 12)
for (int px = 0; px < bw; px += 12)
display.fillCircle(bx4 + px + 6, by4 + py + 6, 3, GxEPD_BLACK);

// Dither gradient (bottom)
int gx = 30, gy = 310, gw = W - 60, gh = 120;
display.setFont(&FreeSans9pt7b);
display.setCursor(gx, gy - 5); display.print("Dither Gradient:");
display.drawRect(gx, gy, gw, gh, GxEPD_BLACK);
for (int py = 0; py < gh; py++)
for (int px = 0; px < gw; px++) {
int density = (px * 255) / gw;
if ((((px * 7 + py * 13) ^ (px * py)) & 0xFF) < density)
display.drawPixel(gx + px, gy + py, GxEPD_BLACK);
}

drawCenteredText("Fill patterns and dithering", H - 15, &FreeSans9pt7b);
} while (display.nextPage());
}

// =====================================================================
// Screen 6: Dashboard
// =====================================================================
void drawCard(int x, int y, int w, int h, const char* title, const char* value, const char* unit)
{
display.drawRoundRect(x, y, w, h, 6, GxEPD_BLACK);
display.fillRoundRect(x + 2, y + 2, w - 4, 26, 4, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setFont(&FreeSansBold12pt7b);
int16_t tbx, tby; uint16_t tbw, tbh;
display.getTextBounds(title, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(x + (w - tbw) / 2 - tbx, y + 22);
display.print(title);

display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeSansBold18pt7b);
display.getTextBounds(value, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(x + (w - tbw) / 2 - tbx, y + h / 2 + 12);
display.print(value);

display.setFont(&FreeSans9pt7b);
display.getTextBounds(unit, 0, 0, &tbx, &tby, &tbw, &tbh);
display.setCursor(x + (w - tbw) / 2 - tbx, y + h - 10);
display.print(unit);
}

void showDashboardDemo()
{
const uint16_t W = display.width();
const uint16_t H = display.height();
display.setRotation(0);
display.setFullWindow();
display.firstPage();
do {
display.fillScreen(GxEPD_WHITE);
display.fillRect(0, 0, W, 40, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
drawCenteredText("Dashboard Demo", 30, &FreeSansBold12pt7b);
display.setTextColor(GxEPD_BLACK);

int cw = 170, ch = 130, gap = 20;
int sx = (W - 4 * cw - 3 * gap) / 2;
int row1Y = 60;

char uptBuf[16]; snprintf(uptBuf, sizeof(uptBuf), "%lu", millis() / 1000);
char heapBuf[16]; snprintf(heapBuf, sizeof(heapBuf), "%lu", (unsigned long)(ESP.getFreeHeap() / 1024));

drawCard(sx, row1Y, cw, ch, "Temp", "23.5", "Celsius");
drawCard(sx + cw + gap, row1Y, cw, ch, "Humidity", "65", "% RH");
drawCard(sx + 2 * (cw + gap), row1Y, cw, ch, "Heap", heapBuf, "kB free");
drawCard(sx + 3 * (cw + gap), row1Y, cw, ch, "Uptime", uptBuf, "seconds");

// Log area
int logY = row1Y + ch + 20;
display.drawRoundRect(sx, logY, W - 2 * sx, 200, 6, GxEPD_BLACK);
display.fillRoundRect(sx + 2, logY + 2, W - 2 * sx - 4, 26, 4, GxEPD_BLACK);
display.setTextColor(GxEPD_WHITE);
display.setFont(&FreeSansBold12pt7b);
display.setCursor(sx + 15, logY + 22);
display.print("Activity Log");
display.setTextColor(GxEPD_BLACK);
display.setFont(&FreeMono9pt7b);

const char* logs[] = {
"[00:01] System boot - ESP32-S3",
"[00:02] Panel: GDEY075T7 800x480",
"[00:03] UC8179 controller ready",
"[00:04] SPI @ 2MHz (HSPI)",
"[00:05] Demo sequence started",
};
int ly = logY + 50;
for (int i = 0; i < 5; i++) {
display.setCursor(sx + 15, ly);
display.print(logs[i]);
ly += 28;
}

// Progress bar
int barY = logY + 210;
display.setFont(&FreeSansBold12pt7b);
display.setCursor(sx, barY + 15);
display.print("Progress:");
int barX = sx + 170, barW = W - 2 * sx - 180, barH = 20;
display.drawRect(barX, barY, barW, barH, GxEPD_BLACK);
display.fillRect(barX + 2, barY + 2, barW - 4, barH - 4, GxEPD_BLACK);
display.setFont(&FreeSans9pt7b);
display.setCursor(barX + barW + 8, barY + 14);
display.print("100%");

drawCenteredText("E-paper: zero power to maintain image", H - 15, &FreeSans9pt7b);
} while (display.nextPage());
}

La siguiente figura muestra el efecto de visualización real del ejemplo E1001:

Uso de la biblioteca GxEPD2 para visualización en escala de grises multinivel

Además de los modos estándar en blanco y negro y de 6 colores, algunas pantallas reTerminal admiten renderizado en escala de grises multinivel. La biblioteca Seeed_GxEPD2 incluye ejemplos dedicados de escala de grises que omiten los controladores normales GxEPD2_BW / GxEPD2_7C y, en su lugar, controlan directamente el controlador de pantalla utilizando formas de onda LUT personalizadas, aprovechando al mismo tiempo Adafruit_GFX para el dibujo.

  • reTerminal E1001 (7.5" B&W): El controlador UC8179 admite un modo de escala de grises de 4 niveles mediante tablas LUT especializadas VCOM/WW/KW/WK/KK. Se utiliza un framebuffer de 2 bpp (96 KB), y cada píxel puede ser negro, gris oscuro, gris claro o blanco.
  • reTerminal E1003 (10.3" Monochrome): El controlador IT8951 admite de forma nativa escala de grises de 16 niveles mediante su modo de forma de onda GC16. Se asigna un framebuffer de 4 bpp (~1,25 MB) en la PSRAM, y cada píxel puede representar uno de 16 niveles de gris.

Programación de reTerminal E1001 — escala de grises de 4 niveles

El controlador UC8179 del E1001 se puede cambiar de su modo normal de 1 bit a un modo de escala de grises de 4 niveles cargando tablas LUT personalizadas (VCOM, LUTWW, LUTKW, LUTWK, LUTKK). Este ejemplo crea un Gray4Canvas (2 bpp, 96 KB) y utiliza Adafruit_GFX para dibujar, luego carga dos planos de bits al controlador para el renderizado en escala de grises.

Después de instalar la biblioteca Seeed_GxEPD2, puedes encontrar este ejemplo en el IDE de Arduino a través de File > Examples > Seeed_GxEPD2 > GxEPD2_reTerminal_E1001_Gray4, o localizarlo manualmente en Seeed_GxEPD2/examples/GxEPD2_reTerminal_E1001_Gray4/GxEPD2_reTerminal_E1001_Gray4.ino.

Haz clic aquí para ver el código completo
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Fonts/FreeMonoBold9pt7b.h>
#include <Fonts/FreeMonoBold12pt7b.h>
#include <Fonts/FreeSansBold18pt7b.h>
#include <Fonts/FreeSansBold12pt7b.h>
#include <Fonts/FreeSans9pt7b.h>

// ===== Pin mapping (E1001) =====
#define EPD_SCK_PIN 7
#define EPD_MOSI_PIN 9
#define EPD_CS_PIN 10
#define EPD_DC_PIN 11
#define EPD_RES_PIN 12
#define EPD_BUSY_PIN 13

#define EPD_W 800
#define EPD_H 480

SPIClass hspi(HSPI);
SPISettings spiSet(2000000, MSBFIRST, SPI_MODE0);

// Gray levels
#define G_BLACK 0
#define G_DARK_GRAY 1
#define G_LIGHT_GRAY 2
#define G_WHITE 3

// UC8179 gray LUT tables (verbatim from Seeed_GFX UC8179_Defines.h)
// Each LUT is 7 phases x 6 bytes = 42 bytes.
static const uint8_t LUT_VCOM_GRAY[] = {
0x00,0x00,0x06,0x08,0x07,0x01,
0x00,0x06,0x0A,0x0B,0x0A,0x01,
0x00,0x03,0x03,0x00,0x00,0x03,
0x00,0x05,0x09,0x06,0x06,0x01,
0x00,0x02,0x02,0x0A,0x0A,0x01,
0x00,0x0A,0x11,0x06,0x07,0x01,
0x00,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t LUT_WW_GRAY[] = {
0x15,0x00,0x06,0x08,0x07,0x01,
0x54,0x06,0x0A,0x0B,0x0A,0x01,
0x90,0x03,0x03,0x00,0x00,0x03,
0x2A,0x05,0x09,0x06,0x06,0x01,
0xAA,0x02,0x02,0x0A,0x0A,0x01,
0x00,0x0A,0x11,0x06,0x07,0x01,
0x28,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t LUT_KW_GRAY[] = {
0x2A,0x00,0x06,0x08,0x07,0x01,
0x59,0x06,0x0A,0x0B,0x0A,0x01,
0x90,0x03,0x03,0x00,0x00,0x03,
0x5A,0x05,0x09,0x06,0x06,0x01,
0xA8,0x02,0x02,0x0A,0x0A,0x01,
0x45,0x0A,0x11,0x06,0x07,0x01,
0xA8,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t LUT_WK_GRAY[] = {
0x16,0x00,0x06,0x08,0x07,0x01,
0xA0,0x06,0x0A,0x0B,0x0A,0x01,
0x90,0x03,0x03,0x00,0x00,0x03,
0x99,0x05,0x09,0x06,0x06,0x01,
0xA0,0x02,0x02,0x0A,0x0A,0x01,
0x40,0x0A,0x11,0x06,0x07,0x01,
0x20,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t LUT_KK_GRAY[] = {
0x26,0x00,0x06,0x08,0x07,0x01,
0x6A,0x06,0x0A,0x0B,0x0A,0x01,
0x90,0x03,0x03,0x00,0x00,0x03,
0x65,0x05,0x09,0x06,0x06,0x01,
0x50,0x02,0x02,0x0A,0x0A,0x01,
0x10,0x0A,0x11,0x06,0x07,0x01,
0x10,0x02,0x01,0x02,0x01,0x01,
};

static const uint8_t CMD_USER_GRAY[] = {
0x17, 0x3F, 0x3F, 0x07, 0x06, 0x12,
};

// ============================================================
// 4-level grayscale canvas (2bpp)
// ============================================================
class Gray4Canvas : public Adafruit_GFX
{
public:
Gray4Canvas(uint16_t w, uint16_t h) : Adafruit_GFX(w, h), _buf(nullptr) {}

bool begin()
{
uint32_t sz = uint32_t(_width) * _height / 4;
_buf = (uint8_t*)malloc(sz);
if (_buf) memset(_buf, 0xFF, sz); // fill white (0b11 = 3 = white)
return _buf != nullptr;
}

void drawPixel(int16_t x, int16_t y, uint16_t color) override
{
if (!_buf) return;
if (x < 0 || x >= width() || y < 0 || y >= height()) return;

int16_t rx = x, ry = y;
switch (getRotation()) {
case 1: rx = _width - 1 - y; ry = x; break;
case 2: rx = _width - 1 - x; ry = _height - 1 - y; break;
case 3: rx = y; ry = _height - 1 - x; break;
}

uint8_t g = color & 0x03;
uint32_t idx = uint32_t(ry) * (_width / 4) + rx / 4;
uint8_t shift = (3 - (rx & 3)) * 2;
_buf[idx] = (_buf[idx] & ~(0x03 << shift)) | (g << shift);
}

void fillScreen(uint16_t color) override
{
if (!_buf) return;
uint8_t g = color & 0x03;
uint8_t fill = (g << 6) | (g << 4) | (g << 2) | g;
memset(_buf, fill, uint32_t(_width) * _height / 4);
}

uint8_t getPixel(int16_t x, int16_t y) const
{
if (!_buf || x < 0 || x >= _width || y < 0 || y >= _height) return 0;
uint32_t idx = uint32_t(y) * (_width / 4) + x / 4;
uint8_t shift = (3 - (x & 3)) * 2;
return (_buf[idx] >> shift) & 0x03;
}

uint8_t* buffer() { return _buf; }

private:
uint8_t* _buf;
};

Gray4Canvas canvas(EPD_W, EPD_H);

// ============================================================
// UC8179 SPI helpers
// ============================================================
void checkBusy() {
delay(10);
while (!digitalRead(EPD_BUSY_PIN)) delay(10);
}

void writeCommand(uint8_t cmd) {
hspi.beginTransaction(spiSet);
digitalWrite(EPD_DC_PIN, LOW);
digitalWrite(EPD_CS_PIN, LOW);
hspi.transfer(cmd);
digitalWrite(EPD_CS_PIN, HIGH);
digitalWrite(EPD_DC_PIN, HIGH);
hspi.endTransaction();
}

void writeData(uint8_t data) {
hspi.beginTransaction(spiSet);
digitalWrite(EPD_CS_PIN, LOW);
hspi.transfer(data);
digitalWrite(EPD_CS_PIN, HIGH);
hspi.endTransaction();
}

void writeLUT(uint8_t cmd, const uint8_t* lut, uint16_t len) {
writeCommand(cmd);
for (uint16_t i = 0; i < len; i++) writeData(lut[i]);
}

// ============================================================
// UC8179 gray mode initialization
// ============================================================
void initGrayMode()
{
// Hardware reset
digitalWrite(EPD_RES_PIN, LOW); delay(10);
digitalWrite(EPD_RES_PIN, HIGH); delay(10);
checkBusy();

// Power setting (0x01)
writeCommand(0x01);
writeData(0x07);
writeData(CMD_USER_GRAY[0]);
writeData(CMD_USER_GRAY[1]);
writeData(CMD_USER_GRAY[2]);
writeData(CMD_USER_GRAY[3]);

// PLL (0x30)
writeCommand(0x30);
writeData(CMD_USER_GRAY[4]);

// VCOM DC (0x82)
writeCommand(0x82);
writeData(CMD_USER_GRAY[5]);

// Booster (0x06)
writeCommand(0x06);
writeData(0x27);
writeData(0x27);
writeData(0x28);
writeData(0x17);

// Power ON (0x04)
writeCommand(0x04);
delay(100);
checkBusy();

// Panel Setting (0x00)
writeCommand(0x00);
writeData(0x3F);

// Power saving (0xE3)
writeCommand(0xE3);
writeData(0x88);

// VCOM and Data interval (0x50)
writeCommand(0x50);
writeData(0x10);
writeData(0x07);

// PLL setting (0x52)
writeCommand(0x52);
writeData(0x00);

// Resolution (0x61)
writeCommand(0x61);
writeData(EPD_W >> 8);
writeData(EPD_W & 0xFF);
writeData(EPD_H >> 8);
writeData(EPD_H & 0xFF);

// Write LUTs for gray mode. CRITICAL ordering — UC8179:
// 0x20 = LUTC (VCOM) ← must be present
// 0x21 = LUTWW (W -> W)
// 0x22 = LUTKW (K -> W)
// 0x23 = LUTWK (W -> K)
// 0x24 = LUTKK (K -> K)
writeLUT(0x20, LUT_VCOM_GRAY, sizeof(LUT_VCOM_GRAY));
checkBusy();
writeLUT(0x21, LUT_WW_GRAY, sizeof(LUT_WW_GRAY));
checkBusy();
writeLUT(0x22, LUT_KW_GRAY, sizeof(LUT_KW_GRAY));
checkBusy();
writeLUT(0x23, LUT_WK_GRAY, sizeof(LUT_WK_GRAY));
writeLUT(0x24, LUT_KK_GRAY, sizeof(LUT_KK_GRAY));

Serial.println(F("[Gray4] UC8179 gray mode init done"));
}

// ============================================================
// Upload 2bpp canvas to UC8179 as two bit-planes.
// ============================================================
void uploadGray4Frame()
{
const uint32_t bytesPerRow = EPD_W / 4; // 200 bytes (2bpp, 4 pixels/byte)

// ---- Bit plane → DTM1 (command 0x10) ----
writeCommand(0x10);
hspi.beginTransaction(spiSet);
digitalWrite(EPD_CS_PIN, LOW);
for (uint16_t row = 0; row < EPD_H; row++) {
const uint8_t* rp = canvas.buffer() + uint32_t(row) * bytesPerRow;
for (uint16_t col8 = 0; col8 < EPD_W / 8; col8++) {
uint8_t out = 0;
for (uint8_t bit = 0; bit < 8; bit++) {
uint16_t px = col8 * 8 + bit;
uint32_t idx = px / 4;
uint8_t shift = (3 - (px & 3)) * 2;
uint8_t gray = 3 - ((rp[idx] >> shift) & 0x03); // INVERT
if (gray & 0x01) out |= (0x80 >> bit);
}
hspi.transfer(out);
}
}
digitalWrite(EPD_CS_PIN, HIGH);
hspi.endTransaction();

// ---- Bit plane → DTM2 (command 0x13) ----
writeCommand(0x13);
hspi.beginTransaction(spiSet);
digitalWrite(EPD_CS_PIN, LOW);
for (uint16_t row = 0; row < EPD_H; row++) {
const uint8_t* rp = canvas.buffer() + uint32_t(row) * bytesPerRow;
for (uint16_t col8 = 0; col8 < EPD_W / 8; col8++) {
uint8_t out = 0;
for (uint8_t bit = 0; bit < 8; bit++) {
uint16_t px = col8 * 8 + bit;
uint32_t idx = px / 4;
uint8_t shift = (3 - (px & 3)) * 2;
uint8_t gray = 3 - ((rp[idx] >> shift) & 0x03); // INVERT
if (gray & 0x02) out |= (0x80 >> bit);
}
hspi.transfer(out);
}
}
digitalWrite(EPD_CS_PIN, HIGH);
hspi.endTransaction();

Serial.println(F("[Gray4] Frame uploaded (2 bit planes)"));
}

void refreshDisplay()
{
unsigned long t0 = millis();
writeCommand(0x12); // Display Refresh
delay(100);
checkBusy();
Serial.printf("[Gray4] Refresh %lu ms\n", millis() - t0);
}

void sleepDisplay()
{
writeCommand(0x02); // Power OFF
checkBusy();
writeCommand(0x07); // Deep sleep
writeData(0xA5);
}

// ============================================================
// Demo drawing
// ============================================================
void drawCenteredText(const char* text, int16_t y, const GFXfont* font, uint8_t gray)
{
canvas.setFont(font);
canvas.setTextColor(gray);
int16_t tbx, tby; uint16_t tbw, tbh;
canvas.getTextBounds(text, 0, 0, &tbx, &tby, &tbw, &tbh);
canvas.setCursor((EPD_W - tbw) / 2 - tbx, y);
canvas.print(text);
}

void showGrayscaleDemo()
{
Serial.println(F("[Gray4] Drawing demo..."));
canvas.fillScreen(G_WHITE);

// Title bar
canvas.fillRect(0, 0, EPD_W, 40, G_BLACK);
drawCenteredText("4-Level Grayscale Demo - E1001", 30, &FreeMonoBold12pt7b, G_WHITE);

// 4 large gray bands
int bandH = 70, startY = 55;
const char* labels[] = {"Gray 0: Black", "Gray 1: Dark Gray", "Gray 2: Light Gray", "Gray 3: White"};
for (int i = 0; i < 4; i++) {
int y = startY + i * (bandH + 8);
canvas.fillRect(30, y, EPD_W - 60, bandH, i);
uint8_t textGray = (i < 2) ? G_WHITE : G_BLACK;
canvas.setFont(&FreeSansBold12pt7b);
canvas.setTextColor(textGray);
canvas.setCursor(60, y + bandH / 2 + 8);
canvas.print(labels[i]);
}

// Concentric gray circles
int areaTop = startY + 4 * (bandH + 8);
int areaBot = EPD_H - 30;
int cy = (areaTop + areaBot) / 2;
int cx = EPD_W - 80;
canvas.setFont(&FreeMonoBold9pt7b);
canvas.setTextColor(G_BLACK);
canvas.setCursor(30, cy - 6);
canvas.print("Concentric gray circles");
canvas.setCursor(30, cy + 14);
canvas.print("(black / dark / light / white)");
canvas.fillCircle(cx, cy, 38, G_BLACK);
canvas.fillCircle(cx, cy, 28, G_DARK_GRAY);
canvas.fillCircle(cx, cy, 18, G_LIGHT_GRAY);
canvas.fillCircle(cx, cy, 9, G_WHITE);

// Footer
canvas.fillRect(0, EPD_H - 30, EPD_W, 30, G_BLACK);
drawCenteredText("UC8179 4-gray LUT mode | E1001", EPD_H - 8, &FreeMonoBold9pt7b, G_WHITE);

Serial.println(F("[Gray4] Uploading..."));
uploadGray4Frame();
refreshDisplay();
Serial.println(F("[Gray4] Done."));
}

// ============================================================
void setup()
{
Serial.begin(115200);
delay(200);
Serial.println(F("[E1001 Gray4] Starting..."));

pinMode(EPD_CS_PIN, OUTPUT); digitalWrite(EPD_CS_PIN, HIGH);
pinMode(EPD_DC_PIN, OUTPUT); digitalWrite(EPD_DC_PIN, HIGH);
pinMode(EPD_RES_PIN, OUTPUT); digitalWrite(EPD_RES_PIN, HIGH);
pinMode(EPD_BUSY_PIN, INPUT);

hspi.begin(EPD_SCK_PIN, -1, EPD_MOSI_PIN, -1);

if (!canvas.begin()) {
Serial.println(F("[Gray4] FATAL: alloc failed (96 KB)"));
while (true) delay(1000);
}
Serial.printf("[Gray4] Canvas OK (%lu bytes)\n", (unsigned long)(EPD_W * EPD_H / 4));

initGrayMode();
showGrayscaleDemo();
sleepDisplay();
}

void loop() {}

La siguiente figura muestra el efecto de visualización real del ejemplo de escala de grises de 4 niveles del E1001:

nota

Las pantallas de papel electrónico tienen una velocidad de actualización relativamente lenta. Las pantallas en blanco y negro (E1001, E1003) suelen actualizarse en 1-3 segundos, mientras que las pantallas de 6 colores (E1002, E1004) pueden tardar entre 25 y 40 segundos en una actualización completa. Este comportamiento es normal y es una compensación por el consumo de energía ultrabajo y la excelente visibilidad sin retroiluminación.

Solución de problemas

P1: ¿Por qué la pantalla de papel electrónico del reTerminal no muestra nada ni se actualiza al ejecutar el código anterior?

Este problema puede ocurrir si has insertado una tarjeta MicroSD en el reTerminal. La razón es que la tarjeta MicroSD y la pantalla de papel electrónico comparten el mismo bus SPI en el reTerminal. Si se inserta una tarjeta MicroSD pero su pin de habilitación (chip select) no se gestiona correctamente, puede provocar un conflicto en el bus SPI. En concreto, la tarjeta MicroSD puede mantener la línea BUSY en alto, lo que impide que la pantalla de papel electrónico funcione correctamente, dando como resultado que no haya actualizaciones ni refrescos de la pantalla.

// Initialize SD Card
pinMode(SD_EN_PIN, OUTPUT);
digitalWrite(SD_EN_PIN, HIGH);
pinMode(SD_DET_PIN, INPUT_PULLUP);

Para resolver esto, debes asegurarte de que la tarjeta MicroSD esté correctamente habilitada utilizando el código proporcionado arriba. El código inicializa y habilita la tarjeta MicroSD configurando los estados correctos de los pines, lo que evita conflictos en el bus SPI y permite que tanto la tarjeta SD como la pantalla de papel electrónico funcionen juntas. Utiliza siempre el código de inicialización recomendado cuando uses una tarjeta MicroSD con el reTerminal para evitar este tipo de problemas.

Si la tarjeta MicroSD no se utiliza en tu proyecto, recomendamos apagar el dispositivo y retirar la tarjeta antes de ejecutar el programa de visualización. Si la tarjeta se ha insertado en el reTerminal, tendrás que añadir el código anterior para asegurarte de que la pantalla pueda mostrarse correctamente, independientemente de si estás utilizando una tarjeta MicroSD o no.

P2: ¿Por qué no puedo cargar programas en el reTerminal?

Si encuentras el siguiente error al cargar un programa en el reTerminal.

Entonces, es probable que tu Arduino IDE esté configurado con una velocidad de carga excesivamente alta. Cámbiala a 115200 baudios para resolver este problema.

Soporte técnico y debate sobre el producto

Gracias por elegir nuestros productos. Estamos aquí para ofrecerte diferentes tipos de soporte y garantizar que tu experiencia con nuestros productos sea lo más fluida posible. Ofrecemos varios canales de comunicación para adaptarnos a diferentes preferencias y necesidades.

Loading Comments...