La tarjeta de desarrollo Touchdot de UNIT es un producto basado en el diseño de una Lilypad convencional, tomando ventaja de su factor de forma y complementándolo con las capacidades de procesamiento y conectividad de un módulo ESP32-S3, lo que la vuelve incluso más versátil para proyectos de electrónica wearable. En esta ocasión, aprovecharemos varias de las funcionalidades que provée para hacer un visualizador de tarjetas de apoyo comunicativo.
¿Qué son las tarjetas de apoyo comunicativo?
Antes de describir a mayor detalle las características técnicas del proyecto, hace falta adentrarnos un poco en la necesidad del mismo: Las tarjetas de apoyo comunicativo son herramientas para mantener comunicación con una persona que está atravesando un episodio de bloqueo verbal.
Quienes viven con autismo, TDAH, trastorno de ansiedad, dislexia o TEPT pueden llegar a sufrir estos episodios al encontrarse con sobrecargas sensoriales, cognitivas, estrés emocional y situaciones de fatiga extrema. Durante un bloqueo verbal, la corteza prefrontal, las redes temporales de acceso al lenguaje, y su comunicación con los sistemas de estrés subcorticales, se ven afectados, lo que causa una interrupción en la producción de lenguaje, aunque las capacidades de comprensión del lenguaje siguen estando activas.
Entonces, el objetivo de las tarjetas de apoyo es establecer un puente de comunicación de la persona con su entorno y contribuir a la reducción del estrés que podría haber dado lugar al bloqueo. Por esto mismo, deben ir acompañadas de una aceptación y conocimiento por parte del círculo social de la persona que las utiliza, así como estar a la mano y ser fáciles de usar.
¿Qué necesitarás?
- UNIT Touchdot ESP32-S3
- Tarjeta SD
- Modulo TFT Display ST7789V 2.4″
Diagrama de conexiones
- La conexión en pullup de D2 a GND que se muestra abierta, será el circuito que cerraremos con ayuda de un cable caiman , cambiará de tarjeta en nuestro visualizador y, después de 10 segundos se cierre el circuito enviará a un estado “sleep” a la tarjeta.
- Las conexiones de CS, DC y RST, pueden ir a pines distintos de los marcados si así se prefiere, pero las conexiones de SCL y SDA están fijas a los pines que la Touchdot usa para comunicación SPI por defecto (SCK y MOSI/PICO). Se mantienen así por practicidad y para no interferir con la configuración del SPI de la SD.

Código
Funcionamiento, cierre de circuito y modo”sleep” para la Touchdot
Bibliotecas y variables globales:
#include <Arduino.h>
//Pin para el pin de cierre de circuito, que funcionara como botón para los cambios de estado
#define LID 11 // D2
// Variables para el control de los estados
bool last = 1, justClosed = 0;
unsigned long t = 0;
Configuración (A1 como pulldown):
void setup() {
Serial.begin(9600);
while (!Serial) ;
// Interruptor por via pin
pinMode(LID, INPUT_PULLUP);
// Declarar condición para activar modo "sleep"(GPIO11 en HIGH).
esp_sleep_enable_ext0_wakeup(GPIO_NUM_11, 1);
}
Implementación:
void loop() {
// Lectura del pin
bool r = digitalRead(LID);
// Cierre del circuito via pin,condición de debounce.
if (r != last && millis() - t > 200) {
t = millis();
Serial.println(t);
if (!r) justClosed = true; // Flanco de bajada (LOW).
else if (justClosed) { // Flanco de subida (HIGH).
justClosed = false;
showNextPic();
}
last = r;
}
// Modo "sleep" activado por 10s al mantener el pin LID cerrando el circuto
if (justClosed && millis() - t > 10000) {
Serial.println("SHUTTING DOWN.");
digitalWrite(TFT_BL, LOW); // Apagar la backlight.
esp_deep_sleep_start(); // Sueño profundo
}
}
1. Almacenamos una lectura del pin.
Primer condicional:
2. Si dicha lectura difiere de la última lectura (r) (i. e., el pin ha cambiado de estado) y han pasado 200 milisegundos desde la última marca de tiempo (t):
3. Actualizamos la marca de tiempo. Esta marca evita que se tomen lecturas demasiado seguidas como para ser acción humana; aquellas que pudieran ser ruido. Se le llama “debounce”.
4. Si el pin regresa LOW, marcamos una bandera como verdadera.
5. Si, por otro lado, regresa HIGH y la bandera es verdadera, sabemos que el circuito fue cerrado y después abierto, entonces gestionamos nuestra carga de imágenes con una función (más sobre esto a continuación). Esta condición solo es ingresada la segunda vez que hay un cambio en la lectura, lo que la vuelve siempre equivalente al estado “LOW seguido de HIGH”.
6. Actualizamos la última lectura.
Segundo condicional:
7. Si no hay cambio del estado del pin (r), pero tenemos activada la bandera de cubierta cerrada, y además han pasado 10 segundos:
8. Apagamos la backlight de la pantalla (más sobre esto a continuación).
9. Iniciamos Sueño Profundo("sleep").
Iteración de las imágenes y muestra en pantalla
Bibliotecas y variables globales:
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <SdFat_Adafruit_Fork.h>
#include <Adafruit_ImageReader.h>
// Pin para la tarjeta SD.
#define SD_CS 21
// Pines para la pantalla TFT.
#define TFT_DC 33 // D15.
#define TFT_RST 34 // D16
#define TFT_BL 35 // D17.
#define TFT_CS 36 // D18.
// Declaraciones para la SD.
SdFat SD; // Sistema de archivos.
FatFile dir; // Para manejar un directorio.
FatFile file; // Para manejar un archivo.
char name[64]; // Nombre de la imagen a mostrar.
// Declaraciones de ImageReader.
Adafruit_ImageReader reader(SD); // Lector de imágenes de la SD.
ImageReturnCode status; // Estatus regresado por las funciones.
Adafruit_Image img; // Lienzo en RAM para imágenes.
// Declaración de la pantalla TFT.
Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST);
Las bibliotecas Adafruit_GFX y Adafruit_ST7789 nos permitirán mostrar cualquier tipo de gráficos en nuestra pantalla. Por su parte, SdFat_Adafruit_Fork nos permitirá navegar el contenido del sistema de archivos de la SD eficientemente, y por último, Adafruit_ImageReader nos permitirá visualizar archivos BMP fácilmente en la pantalla.
Nota importante: Las imágenes en la SD deberán tener nombre en formato 8.3 (8 carácteres, punto, y extensión de 3 carácteres) y tener color de 24 bits de profundidad para que ImageReader las pueda leer.
Configuración:
void setup() {
// Aquí va el código de la sección anterior
// Control de backlight.
pinMode(TFT_BL, OUTPUT);
digitalWrite(TFT_BL, HIGH);
// Iniciar la pantalla manualmente (previene glitches ocasionales con la SD).
pinMode(TFT_CS, OUTPUT);
digitalWrite(TFT_CS, HIGH);
if (!SD.begin(SD_CS)) {
Serial.println("Error al inicializar la tarjeta SD.");
for(;;);
}
Serial.println("Tarjeta SD inicializada correctamente.");
// Apertura del directorio donde están las imágenes.
if (!dir.open("/")) {
Serial.println("Error al abrir '/'.");
for(;;);
}
Serial.println("Directorio raíz abierto.");
// Inicialización de la pantalla TFT.
tft.init(240, 320);
// Posicionar al inicio del directorio y preparar las primeras imágenes (show & preload).
dir.rewind();
if (! showNextPic()) {
Serial.println("No se pudo inicializar imágenes.");
for(;;);
}
}
Aquí, primero encendemos la backlight y el CS de la pantalla manualmente, y verificamos la correcta inicialización de nuestra tarjeta SD y su sistema de archivos. Si cualquiera de las anteriores cosas falla, nuestro programa no puede continuar, entonces lo mandamos a un ciclo infinito. Si todo está en orden, inicializamos nuestra pantalla con la resolución correcta, luego nos aseguramos que se empiece a leer en el directorio desde el inicio (dir.rewind()), y llamamos por primera vez a la función que se encargará de mostrar nuestras imágenes siempre.
Implementación:
bool showNextPic() {
// Función lambda auxiliar.
auto getNextName = [&]() -> bool {
for (int attempts = 0; attempts < 1000; ++attempts) {
// Cerrar el archivo anterior.
if (file.isOpen()) file.close();
// Ciclo en el EOF.
if (!file.openNext(&dir, O_RDONLY)) {
dir.rewind();
// EOF constante (directorio vacío).
if (!file.openNext(&dir, O_RDONLY)) return false;
}
// Ignorar directorios.
if (file.isDir()) {
file.close();
continue;
}
// Verificar que sea un BMP por su nombre.
file.getName(name, sizeof(name));
String s = name;
s.toLowerCase();
if (!s.endsWith(".bmp")) {
file.close();
continue;
}
// Aquí tenemos un BMP válido y el file está abierto sobre él.
return true;
}
return false;
};
if (!getNextName()) return false;
if (!img.getCanvas()) {
// Muestra la primera imagen.
Serial.print("Mostrando primera imagen: ");
Serial.println(name);
status = reader.drawBMP(name, tft, 0, 0);
reader.printStatus(status);
// Avanza en los archivos para que la imagen cargada a la RAM sea la siguiente.
getNextName();
} else {
// Muestra la imagen guardada en RAM.
Serial.println("Mostrando la imagen guardada en RAM.");
img.draw(tft, 0, 0);
}
// Carga la imagen a la RAM.
Serial.print("Cargando a RAM la imagen: ");
Serial.println(name);
status = reader.loadBMP(name, img);
reader.printStatus(status);
return true;
}
Aquí tenemos primero una función auxiliar getNextName que iterará sobre todo el directorio y almacenará el nombre de un archivo válido BMP en la variable global name. Sus criterios básicos son los siguientes: El directorio no puede estar vacío (regresa falso si, llegando al EOF (End of File) del directorio, y regresando al inicio, de todas formas no encuentra un archivo siguiente), y omite abrir directorios o archivos distintos de BMP. Una vez cubiertos esos criterios, el algoritmo principal es muy simple:
1. Si getNextName() falla, regresa falso. 2. Si el lienzo en RAM está vacío, muestra la primera imagen directamente desde la SD y avanza un paso el iterador de archivos (getNextName()). 4. De otra forma, dibuja en la pantalla la imagen almacenada en la RAM. 5. Después, carga al lienzo en RAM la imagen que estamos iterando (que desde el paso 2, debe ser la siguiente a la primera y seguirá iterando con normalidad). 6. Termina. Una vez que hayamos cargado nuestras imágenes a la tarjeta SD, nuestro prototipo está listo. A continuación se muestra un video de su funcionamiento:
Conclusión
Este proyecto, por su naturaleza de baja complejidad, puede servir de introducción al mundo del desarrollo, mientras que cumple una función social para con una población en relativa desventaja.
Este proyecto tiene el potencial para poder sustituir el pin LIN por algún otro tipo de conector en algún peluche, botón o cualquier instrumento de wearable para hacerlo más amable con el usuario. Cualquier duda o comentario, siéntanse libres de dejarla en la sección de comentarios.

