// ================================================================
// ENCRIPTADOR ALGEBRAICO v3.0
// Raspberry Pi Pico | Arduino IDE | Wokwi
// Autor: YOLTZIN HERNANDEZ CANSECO
//
// DESCRIPCION:
// Cifrador matricial Hill Cipher 2×2 con interfaz TFT y teclado.
// El texto se divide en bloques de 4 chars, cada bloque se
// convierte en una matriz 2×2 de valores ASCII y se multiplica
// por la inversa de la clave Z para encriptar.
// Para desencriptar se multiplica por Z directamente.
//
// HARDWARE (pines):
// TFT ILI9341: RST=GP14, DC=GP15, SCK=GP2, MOSI=GP3, MISO=GP4, CS=GP5
// Keypad filas: GP6,GP7,GP8,GP9 columnas: GP16,GP17,GP18,GP19
// LEDs: Verde=GP11, Rojo=GP12
//
// LIBRERIAS REQUERIDAS:
// - BasicLinearAlgebra (tomstewart89)
// - Adafruit GFX Library
// - Adafruit ILI9341
// - Keypad (Mark Stanley / Alexander Brevig)
//
// ORGANIZACION:
// [1] Includes y tipos globales
// [2] Pines y constantes de hardware
// [3] TFT: instancia y paleta de colores
// [4] Keypad: instancia y sistema T9 multi-tap
// [5] LEDs
// [6] Algebra matricial – funciones de encr.ino
// [7] Utilidades de dibujo TFT
// [8] Pantalla: Menu principal
// [9] Pantalla: Encriptar
// [10] Pantalla: Desencriptar
// [11] Pantalla: Configuraciones
// [12] Enrutador de eventos
// [13] Setup y Loop
// ================================================================
// ================================================================
// [1] INCLUDES Y TIPOS GLOBALES
// ================================================================
#include <BasicLinearAlgebra.h> // Algebra matricial: Inverse, Determinant
#include <SPI.h> // Comunicacion SPI para TFT
#include <Adafruit_GFX.h> // Graficos base Adafruit
#include <Adafruit_ILI9341.h> // Driver pantalla ILI9341
#include <Keypad.h> // Teclado matricial 4x4
using namespace BLA; // Espacio de nombres BasicLinearAlgebra
// Modos de interpretacion del valor ASCII del caracter
// (heredado de encr.ino, solo se usa ASCII_RAW en este proyecto)
enum ConversionMode {
ASCII_RAW, // valor directo 0-255
ASCII_NORM, // normalizado 0.0-1.0
ASCII_CENTERED // centrado en cero, -128 a 127
};
// Constantes del cifrador (identicas a encr.ino)
const int rPAQ = 2; // dimension de la matriz (2x2)
const int lPAQ = rPAQ * rPAQ; // caracteres por bloque (4)
const int cPAQ = 10; // maximo de bloques (matrices)
// Estructura que agrupa las matrices de un texto completo.
// Identica a TXTtoMTRX de encr.ino para reutilizacion directa.
struct TXTtoMTRX {
Matrix<2,2> matrices[cPAQ]; // array de matrices (max cPAQ=10)
int nMTRX; // cuantas matrices estan ocupadas
ConversionMode modo; // modo de conversion usado
};
// Tamano del buffer de texto en pantalla
#define MAX_BUF 256
// ================================================================
// [2] PINES Y CONSTANTES DE HARDWARE
// ================================================================
// -- TFT ILI9341 -------------------------------------------------
#define TFT_RST 14
#define TFT_DC 15
#define TFT_MOSI 3
#define TFT_SCK 2
#define TFT_MISO 4
#define TFT_CS 5
// ── LEDs indicadores ────────────────────────────────────────────
#define LED_G 11 // Verde : operación correcta
#define LED_R 12 // Rojo : error
// -- Teclado 4x4 -------------------------------------------------
const byte ROWS = 4, COLS = 4;
// Mapa de caracteres del teclado fisico
char hexKeys[ROWS][COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
// GP de filas y columnas
byte rowPins[ROWS] = {6, 7, 8, 9 };
byte colPins[COLS] = {16, 17, 18, 19};
// Instancia global del keypad
Keypad keypad = Keypad(makeKeymap(hexKeys), rowPins, colPins, ROWS, COLS);
// ── Anti-rebote (debounce) ───────────────────────────────────────
// La libreria Keypad tiene debounce interno, pero en la Pico el
// loop es tan rapido que igual se cuelan lecturas fantasma.
// Doble filtro por software:
// DEBOUNCE_MS : silencio minimo entre dos pulsaciones validas
// KEY_HOLD : lecturas PRESSED consecutivas iguales requeridas
// para aceptar la tecla (~1ms por iteracion = 5ms)
#define DEBOUNCE_MS 80 // ms de silencio entre teclas
#define KEY_HOLD 5 // lecturas estables para confirmar
static char db_candidate = '\0';
static int db_count = 0;
static unsigned long db_lastMs = 0;
/**
* Wrapper de keypad con doble filtro anti-rebote.
*
* Logica:
* 1. Lee el estado raw del keypad.
* 2. Descarta cualquier lectura dentro del silencio DEBOUNCE_MS.
* 3. Exige KEY_HOLD lecturas consecutivas iguales antes de aceptar.
*
* Esto elimina:
* - Teclas fantasma por ruido electrico.
* - Pulsaciones dobles por rebote mecanico.
* - Caracteres espureos (#, p, etc.) al arrancar.
*
* @return caracter confirmado, o '\0' si no hay ninguno aun.
*/
char getKeyFiltered() {
unsigned long now = millis();
// Usar getKeys() para acceder al estado continuo de cada tecla
keypad.getKeys();
// Buscar la primera tecla en estado PRESSED (flanco de subida)
char raw = '\0';
for (int i = 0; i < LIST_MAX; i++) {
if (keypad.key[i].kstate == PRESSED) {
raw = keypad.key[i].kchar;
break;
}
}
if (raw == '\0') {
// Sin tecla: resetear candidato
db_candidate = '\0';
db_count = 0;
return '\0';
}
// Dentro del silencio post-pulsacion: ignorar
if ((now - db_lastMs) < (unsigned long)DEBOUNCE_MS) return '\0';
if (raw == db_candidate) {
db_count++; // misma tecla: acumular
} else {
db_candidate = raw; // tecla nueva: reiniciar
db_count = 1;
}
if (db_count >= KEY_HOLD) {
db_count = 0;
db_candidate = '\0';
db_lastMs = now;
return raw; // tecla confirmada
}
return '\0';
}
// ================================================================
// [3] TFT: INSTANCIA Y PALETA DE COLORES
// ================================================================
Adafruit_ILI9341 tft = Adafruit_ILI9341(
TFT_CS, TFT_DC, TFT_MOSI, TFT_SCK, TFT_RST, TFT_MISO
);
// Dimensiones en landscape (rotation=3): 320x240
#define W 320
#define H 240
#define SPLIT_Y 120 // linea divisoria en pantallas split (entrada/salida)
// Paleta de colores RGB565
#define C_BG ILI9341_BLACK
#define C_WHITE ILI9341_WHITE
#define C_CYAN ILI9341_CYAN
#define C_YELLOW ILI9341_YELLOW
#define C_GREEN ILI9341_GREEN
#define C_RED ILI9341_RED
#define C_ORANGE 0xFD20u // naranja
#define C_GREY 0x4208u // gris oscuro (bordes)
#define C_LGREY 0xC618u // gris claro (textos secundarios)
// ================================================================
// [4] KEYPAD: SISTEMA T9 MULTI-TAP
// ================================================================
// -- Mapa T9 -----------------------------------------------------
// Cada entrada asocia una tecla con la lista de caracteres que cicla.
// Presionar la misma tecla varias veces antes del timeout avanza el indice.
// Presionar una tecla diferente (o esperar el timeout) confirma el caracter.
struct T9Entry { char key; const char* chars; };
const T9Entry t9map[] = {
{'1', "1.,?!-'"},
{'2', "2ABC"},
{'3', "3DEF"},
{'4', "4GHI"},
{'5', "5JKL"},
{'6', "6MNO"},
{'7', "7OPQRS"},
{'8', "8TUV"},
{'9', "9WXYZ"},
{'0', " 0"},
{'*', "*"},
{'#', "#"}
};
const int T9_SIZE = 12; // entradas en el mapa
const int T9_TIMEOUT = 800; // ms de espera para confirmar caracter
// Estado interno del T9 (persiste entre llamadas a t9Feed)
static char t9LastKey = '\0';
static int t9Idx = 0;
static unsigned long t9Ms = 0;
/**
* Resetea el estado T9.
* Llamar antes de procesar teclas de comando (A, B, C, D)
* para que el siguiente caracter no sea reemplazo del anterior.
*/
void t9Reset() {
t9LastKey = '\0';
t9Idx = 0;
}
/**
* Procesa una pulsacion de tecla en el sistema T9.
*
* Logica:
* - Si la tecla es la misma que la anterior y no ha expirado el timeout:
* avanza al siguiente caracter del grupo (*repl = true → reemplazar).
* - Si es diferente o ha expirado el timeout:
* guarda el caracter anterior (ya esta en buffer), inicia nuevo grupo.
*
* @param k Tecla presionada
* @param out [salida] caracter resultante
* @param repl [salida] true si debe reemplazar el ultimo caracter del buffer
* @return true si se genero un caracter, false si la tecla no tiene mapa
*/
bool t9Feed(char k, char* out, bool* repl) {
*repl = false;
unsigned long now = millis();
// Buscar la tecla en el mapa
const char* opts = nullptr;
for (int i = 0; i < T9_SIZE; i++) {
if (t9map[i].key == k) { opts = t9map[i].chars; break; }
}
if (!opts) return false; // tecla sin mapa T9 (A, B, C, D)
if (k == t9LastKey && (now - t9Ms) < (unsigned long)T9_TIMEOUT) {
// Misma tecla dentro del timeout: avanzar ciclo de caracteres
t9Idx = (t9Idx + 1) % (int)strlen(opts);
*out = opts[t9Idx];
*repl = true; // indica que el ultimo char del buffer debe reemplazarse
} else {
// Tecla diferente o timeout expirado: iniciar nuevo grupo
t9LastKey = k;
t9Idx = 0;
*out = opts[0];
*repl = false;
}
t9Ms = now; // actualizar timestamp siempre
return true;
}
// ================================================================
// [5] LEDs
// ================================================================
/** Establece el estado de los dos LEDs. */
inline void ledSet(int g, int r) {
digitalWrite(LED_G, g);
digitalWrite(LED_R, r);
}
void ledOK() { ledSet(1, 0); } // Verde: operacion exitosa
void ledError() { ledSet(0, 1); } // Rojo: error
void ledOff() { ledSet(0, 0); } // Apagado
// ================================================================
// [6] ALGEBRA MATRICIAL – HILL CIPHER
// Funciones tomadas directamente de encr.ino
// ================================================================
// Clave Z almacenada como array fila-mayor: [a,b,c,d] → [[a,b],[c,d]]
// Defaults del codigo original
float Zdata[4] = {12.0f, 14.0f, 9.0f, 2.0f};
// Configuracion mutable del cifrador
int cfg_dim = 2; // dimension de la matriz (fijo 2 para este HW)
int cfg_paq = cPAQ; // max paquetes (igual a cPAQ = 10)
bool cfg_lineal = true; // formato de salida: true=lineal, false=natural
int cfg_tsize = 1; // tamano de texto TFT (1-2)
/** Construye la matriz clave 2x2 desde Zdata[]. */
inline Matrix<2,2> getZ22() {
Matrix<2,2> m;
m(0,0) = Zdata[0]; m(0,1) = Zdata[1];
m(1,0) = Zdata[2]; m(1,1) = Zdata[3];
return m;
}
/** Verifica que la clave actual es invertible (det != 0). */
bool claveValida() {
return fabs(Determinant(getZ22())) > 1e-6;
}
// ----------------------------------------------------------------
// textToMatrixAdvanced (encr.ino)
// Convierte 4 caracteres del texto (desde 'startIndex') en una
// matriz 2x2. Si el indice cae fuera del texto, usa 0 (padding).
// ----------------------------------------------------------------
Matrix<2,2> textToMatrixAdvanced(const String& text, int startIndex, ConversionMode modo) {
Matrix<2,2> m;
for (int i = 0; i < rPAQ; i++) {
for (int j = 0; j < rPAQ; j++) {
int charIndex = startIndex + i * rPAQ + j;
if (charIndex < (int)text.length()) {
char c = text.charAt(charIndex);
switch (modo) {
case ASCII_RAW:
m(i,j) = (float)(unsigned char)c;
break;
case ASCII_NORM:
m(i,j) = (float)(unsigned char)c / 255.0f;
break;
case ASCII_CENTERED:
m(i,j) = (float)(unsigned char)c - 128.0f;
break;
}
} else {
m(i,j) = 0.0f; // padding si el bloque no esta completo
}
}
}
return m;
}
// ----------------------------------------------------------------
// con_TXTtoMTRX (encr.ino)
// Divide el texto en bloques de lPAQ=4 caracteres y convierte
// cada bloque en una matriz 2x2.
// ----------------------------------------------------------------
TXTtoMTRX con_TXTtoMTRX(const String& TXT, ConversionMode modo) {
TXTtoMTRX conv;
conv.modo = modo;
int nMTRX = ((int)TXT.length() + lPAQ - 1) / lPAQ; // ceil(len / 4)
conv.nMTRX = min(nMTRX, cPAQ);
for (int i = 0; i < conv.nMTRX; i++) {
conv.matrices[i] = textToMatrixAdvanced(TXT, i * lPAQ, modo);
}
return conv;
}
// ----------------------------------------------------------------
// cryptoMTRX (encr.ino)
// Encripta: C_i = Z^-1 * M_i
// Calcula la inversa una sola vez (operacion costosa).
// ----------------------------------------------------------------
TXTtoMTRX cryptoMTRX(const TXTtoMTRX& txtMatrices, const Matrix<2,2>& clave) {
TXTtoMTRX crypto;
crypto.modo = txtMatrices.modo;
crypto.nMTRX = txtMatrices.nMTRX;
double det = Determinant(clave);
if (fabs(det) < 1e-6) {
// Clave no invertible: imposible encriptar
crypto.nMTRX = 0;
return crypto;
}
Matrix<2,2> invClave = Inverse(clave); // calcular Z^-1 una sola vez
for (int i = 0; i < crypto.nMTRX; i++) {
crypto.matrices[i] = invClave * txtMatrices.matrices[i];
}
return crypto;
}
// ----------------------------------------------------------------
// descryptoMTRX (encr.ino)
// Desencripta: M_i = Z * C_i
// (inversa de la encriptacion, ya que (Z^-1)^-1 = Z)
// ----------------------------------------------------------------
TXTtoMTRX descryptoMTRX(const TXTtoMTRX& txtMatrices, const Matrix<2,2>& clave) {
TXTtoMTRX descrypto;
descrypto.modo = txtMatrices.modo;
descrypto.nMTRX = txtMatrices.nMTRX;
double det = Determinant(clave);
if (fabs(det) < 1e-6) {
descrypto.nMTRX = 0;
return descrypto;
}
for (int i = 0; i < descrypto.nMTRX; i++) {
descrypto.matrices[i] = clave * txtMatrices.matrices[i]; // Z * C_i
}
return descrypto;
}
// ----------------------------------------------------------------
// MTRXtoTXTSimple (encr.ino)
// Convierte matrices de vuelta a String ASCII.
// Redondea cada float al entero mas cercano y lo interpreta como char.
// 'longitudOriginal' evita incluir caracteres de padding.
// ----------------------------------------------------------------
String MTRXtoTXTSimple(const TXTtoMTRX& matrices, int longitudOriginal) {
String resultado = "";
int charsAgregados = 0;
for (int i = 0; i < matrices.nMTRX && charsAgregados < longitudOriginal; i++) {
for (int row = 0; row < rPAQ && charsAgregados < longitudOriginal; row++) {
for (int col = 0; col < rPAQ && charsAgregados < longitudOriginal; col++) {
// Redondeo critico: sin el redondeo el ASCII no se recupera correctamente
int asciiCode = (int)round(matrices.matrices[i](row, col));
if (asciiCode >= 32 && asciiCode <= 126) {
resultado += (char)asciiCode;
charsAgregados++;
} else if (asciiCode == 0) {
resultado += ' '; // cero de padding → espacio
charsAgregados++;
}
// valores fuera de rango se ignoran (no deberian ocurrir con clave valida)
}
}
}
return resultado;
}
// ----------------------------------------------------------------
// mtrxToStr (basado en printMatrixInline de encr.ino)
// Convierte las matrices cifradas a un String para mostrar en TFT.
// Formato lineal: [a,b,c,d],[e,f,g,h]
// Formato natural: [a,b]\n[c,d]\n\n[e,f]\n[g,h]
// ----------------------------------------------------------------
String mtrxToStr(const TXTtoMTRX& src) {
String res = "";
for (int i = 0; i < src.nMTRX; i++) {
if (cfg_lineal) {
// Una linea por matriz: [a,b,c,d]
res += "[";
for (int r = 0; r < rPAQ; r++) {
for (int c = 0; c < rPAQ; c++) {
float v = src.matrices[i](r, c);
// Mostrar sin decimales si el valor es entero
if (fabs(v - round(v)) < 0.01f) {
res += (int)round(v);
} else {
char tmp[16];
snprintf(tmp, sizeof(tmp), "%.2f", v);
res += tmp;
}
if (!(r == rPAQ-1 && c == rPAQ-1)) res += ",";
}
}
res += "]";
} else {
// Dos lineas por matriz: una fila por renglon
for (int r = 0; r < rPAQ; r++) {
res += "[";
for (int c = 0; c < rPAQ; c++) {
float v = src.matrices[i](r, c);
if (fabs(v - round(v)) < 0.01f) {
res += (int)round(v);
} else {
char tmp[16];
snprintf(tmp, sizeof(tmp), "%.2f", v);
res += tmp;
}
if (c < rPAQ-1) res += ",";
}
res += "]\n";
}
}
// Separador entre matrices
if (i < src.nMTRX - 1) res += (cfg_lineal ? "," : "\n");
}
return res;
}
// ----------------------------------------------------------------
// parseMtrxStr
// Parsea un String "v1#v2#v3#v4#v5#..." en TXTtoMTRX.
// Usado en la pantalla de desencriptar para leer la entrada numerica.
// ----------------------------------------------------------------
TXTtoMTRX parseMtrxStr(const char* s) {
TXTtoMTRX r;
r.modo = ASCII_RAW;
r.nMTRX = 0;
float vals[cPAQ * lPAQ]; // buffer para todos los valores posibles
int cnt = 0;
String tok = "";
for (int i = 0; s[i] && cnt < cPAQ * lPAQ; i++) {
if (s[i] == '#') {
if (tok.length()) { vals[cnt++] = tok.toFloat(); tok = ""; }
} else {
tok += s[i];
}
}
if (tok.length()) vals[cnt++] = tok.toFloat(); // ultimo token
r.nMTRX = cnt / lPAQ; // cuantas matrices completas hay
for (int i = 0; i < r.nMTRX; i++) {
for (int row = 0; row < rPAQ; row++) {
for (int col = 0; col < rPAQ; col++) {
r.matrices[i](row, col) = vals[i * lPAQ + row * rPAQ + col];
}
}
}
return r;
}
// ================================================================
// [7] UTILIDADES DE DIBUJO TFT
// ================================================================
// ----------------------------------------------------------------
// tftHeader
// Dibuja una barra de titulo en la parte superior de la pantalla.
// color: color de fondo de la barra (por defecto cian)
// ----------------------------------------------------------------
void tftHeader(const char* txt, uint16_t color = C_CYAN) {
tft.fillRect(0, 0, W, 18, color); // barra de fondo
tft.setTextColor(C_BG); // texto en color del fondo (negro)
tft.setTextSize(1);
tft.setCursor(4, 5);
tft.print(txt);
}
// ----------------------------------------------------------------
// tftFooter
// Dibuja una barra de ayuda en la parte inferior con los atajos.
// ----------------------------------------------------------------
void tftFooter(const char* txt) {
tft.fillRect(0, H-16, W, 16, C_GREY); // barra gris oscuro
tft.setTextColor(C_LGREY);
tft.setTextSize(1);
tft.setCursor(2, H-11);
tft.print(txt);
}
// ----------------------------------------------------------------
// tftWrapped
// Imprime texto con salto de linea automatico dentro de un rectangulo.
// Parametros:
// x,y : esquina superior izquierda del area
// maxW : ancho maximo del area en pixeles
// maxH : alto maximo del area en pixeles
// txt : texto a imprimir
// color : color del texto
// sz : tamano de fuente (1=6px ancho, 2=12px, 3=18px)
// ----------------------------------------------------------------
void tftWrapped(int x, int y, int maxW, int maxH,
const char* txt, uint16_t color, uint8_t sz) {
tft.setTextColor(color);
tft.setTextSize(sz);
int cw = 6 * sz; // ancho de un caracter en pixeles
int ch = 9 * sz; // alto de un caracter en pixeles (fuente 5x7 + margen)
int cx = x, cy = y;
for (int i = 0; txt[i] != '\0'; i++) {
// Salto de linea explicito o desbordamiento horizontal
if (txt[i] == '\n' || cx + cw > x + maxW) {
cx = x;
cy += ch;
}
if (cy + ch > y + maxH) break; // sin espacio vertical: cortar
tft.setCursor(cx, cy);
tft.print(txt[i]);
cx += cw;
}
}
// ----------------------------------------------------------------
// refreshInputArea
// Refresco PARCIAL del area de entrada de texto.
// Solo borra y redibuja el area interior (no toda la pantalla).
// Dibuja un cursor subrayado al final del texto.
// ----------------------------------------------------------------
void refreshInputArea(int y0, int h, const char* buf, uint8_t sz = 1) {
// Borrar solo el interior del area (2px de margen con el borde)
tft.fillRect(2, y0+14, W-4, h-16, C_BG);
// Redibujar el texto
tftWrapped(4, y0+14, W-8, h-16, buf, C_WHITE, sz);
// Cursor parpadeante: subrayado cian al final del texto
int cw = 6 * sz;
int ch = 9 * sz;
int lineW = (W - 8) / cw; // caracteres por linea
int n = (int)strlen(buf);
int cx = 4 + (n % lineW) * cw;
int cy = y0 + 14 + (n / lineW) * ch;
if (cy + ch <= y0 + h - 2) {
tft.fillRect(cx, cy + ch - 2, cw - 1, 2, C_CYAN); // linea de 2px
}
}
// ----------------------------------------------------------------
// refreshOutputArea
// Refresco PARCIAL del area de resultado (texto verde, sin cursor).
// ----------------------------------------------------------------
void refreshOutputArea(int y0, int h, const char* buf, uint8_t sz = 1) {
tft.fillRect(2, y0+14, W-4, h-16, C_BG);
tftWrapped(4, y0+14, W-8, h-16, buf, C_GREEN, sz);
}
// ----------------------------------------------------------------
// drawSplitScreen
// Dibuja la plantilla base con dos areas: entrada (arriba) y
// salida (abajo), separadas por SPLIT_Y.
// La llaman drawEncriptar y drawDesencriptar.
// ----------------------------------------------------------------
void drawSplitScreen(const char* titulo, uint16_t headerColor,
const char* labelTop, const char* labelBot,
const char* footer) {
tft.fillScreen(C_BG);
tftHeader(titulo, headerColor);
// Caja superior: area de entrada
tft.drawRect(0, 20, W, SPLIT_Y-20, C_GREY);
tft.setTextColor(C_CYAN);
tft.setTextSize(1);
tft.setCursor(4, 23);
tft.print(labelTop);
// Caja inferior: area de resultado
tft.drawRect(0, SPLIT_Y, W, H-SPLIT_Y-16, C_GREY);
tft.setTextColor(C_CYAN);
tft.setCursor(4, SPLIT_Y+3);
tft.print(labelBot);
tftFooter(footer);
}
// ================================================================
// [8] PANTALLA: MENU PRINCIPAL
// ================================================================
// Maquina de estados de pantallas
enum Pantalla {
P_MENU,
P_ENCRIPTAR,
P_DESENCRIPTAR,
P_CFG,
P_CFG_LLAVE,
P_CFG_FORMATO,
P_CFG_TSIZE
};
Pantalla pantallaActual = P_MENU;
// Items del menu
#define N_MENU 3
const char* menuTextos[N_MENU] = {
"1. Encriptar Mensaje",
"2. Desencriptar Mensaje",
"3. Configuraciones"
};
int menuCursor = 0; // item seleccionado (0 a N_MENU-1)
// Buffers de texto compartidos entre pantallas
char inputBuf[MAX_BUF];
int inputLen = 0;
char outputBuf[MAX_BUF];
/**
* Dibuja el menu principal completo.
* El cursor ">" indica el item activo.
* La clave actual se muestra en la parte inferior.
*/
void drawMenu() {
tft.fillScreen(C_BG);
tftHeader(" ENCRIPTADOR ALGEBRAICO v3", C_CYAN);
// Dibujar cada item del menu
for (int i = 0; i < N_MENU; i++) {
int y = 30 + i * 55; // separacion vertical entre items
// Fondo del item seleccionado
if (menuCursor == i) {
tft.fillRoundRect(4, y, W-8, 40, 4, C_GREY);
}
// Cursor ">" a la izquierda
tft.setTextColor(C_YELLOW);
tft.setTextSize(1);
tft.setCursor(8, y+15);
tft.print(menuCursor == i ? ">" : " ");
// Texto del item
tft.setTextColor(menuCursor == i ? C_CYAN : C_WHITE);
tft.setCursor(18, y+15);
tft.print(menuTextos[i]);
}
// Mostrar clave actual en la parte inferior
tft.setTextColor(C_LGREY);
tft.setTextSize(1);
tft.setCursor(4, H-30);
char tmp[48];
snprintf(tmp, sizeof(tmp), "Clave Z: [%.0f,%.0f,%.0f,%.0f]",
Zdata[0], Zdata[1], Zdata[2], Zdata[3]);
tft.print(tmp);
// Indicador de validez de la clave
if (claveValida()) {
tft.setTextColor(C_GREEN);
tft.setCursor(W-55, H-30);
tft.print("[OK]");
ledOK();
} else {
tft.setTextColor(C_RED);
tft.setCursor(W-60, H-30);
tft.print("[ERR]");
ledError();
}
tftFooter("2:arriba 8:abajo A:entrar 1-3:directo");
}
// ================================================================
// [9] PANTALLA: ENCRIPTAR MENSAJE
// ================================================================
/**
* Dibuja la pantalla de encriptacion (split: entrada arriba, resultado abajo).
* El usuario escribe con T9. A para encriptar, B borra, C limpia, D vuelve.
*/
void drawEncriptar() {
drawSplitScreen(
" ENCRIPTAR MENSAJE", C_CYAN,
"Texto (T9 multi-tap):", "Resultado cifrado:",
"A:encriptar B:borrar C:limpiar D:menu"
);
refreshInputArea(20, SPLIT_Y-20, inputBuf, 1);
refreshOutputArea(SPLIT_Y, H-SPLIT_Y-16, outputBuf, 1);
ledOK();
}
/**
* Maneja pulsaciones de teclado en la pantalla de encriptacion.
*
* A: ejecutar encriptacion con la clave actual
* B: borrar ultimo caracter
* C: limpiar toda la entrada y la salida
* D: volver al menu
* 0-9,*,#: entrada de texto por T9
*/
void handleEncriptar(char k) {
int maxC = lPAQ * cPAQ; // maximo de caracteres admitidos (40)
if (k == 'D') {
t9Reset();
pantallaActual = P_MENU;
menuCursor = 0;
drawMenu();
return;
}
if (k == 'C') {
t9Reset();
inputLen = 0; inputBuf[0] = '\0';
outputBuf[0] = '\0';
drawEncriptar();
return;
}
if (k == 'B') {
t9Reset();
if (inputLen > 0) { inputLen--; inputBuf[inputLen] = '\0'; }
outputBuf[0] = '\0';
refreshInputArea(20, SPLIT_Y-20, inputBuf, 1);
refreshOutputArea(SPLIT_Y, H-SPLIT_Y-16, outputBuf, 1);
return;
}
if (k == 'A') {
t9Reset();
if (inputLen == 0) return;
if (!claveValida()) {
// Mostrar error en el area de salida
tft.fillRect(2, SPLIT_Y+14, W-4, H-SPLIT_Y-30, C_BG);
tft.setTextColor(C_RED);
tft.setCursor(4, SPLIT_Y+14);
tft.print("! Clave no invertible");
ledError();
delay(1200);
drawEncriptar();
return;
}
// Encriptar usando las funciones de encr.ino
Matrix<2,2> Z = getZ22();
TXTtoMTRX tm = con_TXTtoMTRX(String(inputBuf), ASCII_RAW);
TXTtoMTRX enc = cryptoMTRX(tm, Z);
String res = mtrxToStr(enc);
strncpy(outputBuf, res.c_str(), MAX_BUF-1);
outputBuf[MAX_BUF-1] = '\0';
refreshOutputArea(SPLIT_Y, H-SPLIT_Y-16, outputBuf, 1);
ledOK();
return;
}
// Cualquier otra tecla: entrada T9
char ch;
bool repl;
if (t9Feed(k, &ch, &repl) && inputLen < maxC) {
if (repl && inputLen > 0) {
// Reemplazar el ultimo caracter del buffer
inputLen--;
inputBuf[inputLen] = '\0';
}
inputBuf[inputLen++] = ch;
inputBuf[inputLen] = '\0';
outputBuf[0] = '\0'; // limpiar resultado al modificar entrada
refreshInputArea(20, SPLIT_Y-20, inputBuf, 1);
refreshOutputArea(SPLIT_Y, H-SPLIT_Y-16, outputBuf, 1);
}
}
// ================================================================
// [10] PANTALLA: DESENCRIPTAR MENSAJE
// ================================================================
/**
* Dibuja la pantalla de desencriptacion.
* El usuario introduce los valores numericos separados por '#'.
* Ejemplo: 5#-7#3#12#... (un '#' por elemento de la matriz)
*/
void drawDesencriptar() {
drawSplitScreen(
" DESENCRIPTAR", C_CYAN,
"Valores (sep. con #):", "Texto recuperado:",
"A:descifrar B:borrar C:limpiar D:menu"
);
refreshInputArea(20, SPLIT_Y-20, inputBuf, 1);
refreshOutputArea(SPLIT_Y, H-SPLIT_Y-16, outputBuf, 1);
ledOK();
}
/**
* Maneja pulsaciones en la pantalla de desencriptacion.
* Solo acepta digitos, '#' como separador y '*' como signo negativo.
* A: ejecutar desencriptacion
*/
void handleDesencriptar(char k) {
if (k == 'D') {
pantallaActual = P_MENU;
menuCursor = 0;
drawMenu();
return;
}
if (k == 'C') {
inputLen = 0; inputBuf[0] = '\0';
outputBuf[0] = '\0';
drawDesencriptar();
return;
}
if (k == 'B') {
if (inputLen > 0) { inputLen--; inputBuf[inputLen] = '\0'; }
outputBuf[0] = '\0';
refreshInputArea(20, SPLIT_Y-20, inputBuf, 1);
refreshOutputArea(SPLIT_Y, H-SPLIT_Y-16, outputBuf, 1);
return;
}
if (k == 'A') {
if (inputLen == 0) return;
if (!claveValida()) {
tft.fillRect(2, SPLIT_Y+14, W-4, H-SPLIT_Y-30, C_BG);
tft.setTextColor(C_RED);
tft.setCursor(4, SPLIT_Y+14);
tft.print("! Clave no invertible");
ledError();
delay(1200);
drawDesencriptar();
return;
}
// Parsear los valores de la matriz, desencriptar y recuperar texto
TXTtoMTRX enc = parseMtrxStr(inputBuf);
Matrix<2,2> Z = getZ22();
TXTtoMTRX dec = descryptoMTRX(enc, Z);
// La longitud original no se conoce: usar nMTRX * lPAQ como maximo
String txt = MTRXtoTXTSimple(dec, dec.nMTRX * lPAQ);
txt.trim();
strncpy(outputBuf, txt.c_str(), MAX_BUF-1);
outputBuf[MAX_BUF-1] = '\0';
refreshOutputArea(SPLIT_Y, H-SPLIT_Y-16, outputBuf, 1);
ledOK();
return;
}
// Solo caracteres validos para la entrada numerica de la matriz
char c = '\0';
if (k >= '0' && k <= '9') c = k; // digito
else if (k == '#') c = '#'; // separador de elementos
else if (k == '*') c = '-'; // signo negativo (valores negativos cifrados)
if (c && inputLen < MAX_BUF - 2) {
inputBuf[inputLen++] = c;
inputBuf[inputLen] = '\0';
outputBuf[0] = '\0';
refreshInputArea(20, SPLIT_Y-20, inputBuf, 1);
refreshOutputArea(SPLIT_Y, H-SPLIT_Y-16, outputBuf, 1);
}
}
// ================================================================
// [11] PANTALLA: CONFIGURACIONES
// ================================================================
// Items del submenu de configuracion
#define N_CFG 3
const char* cfgTextos[N_CFG] = {
"3.1 Llave Z",
"3.2 Formato salida",
"3.3 Tamano de texto"
};
int cfgCursor = 0;
/**
* Dibuja el submenu de configuracion con los valores actuales.
*/
void drawCfg() {
tft.fillScreen(C_BG);
tftHeader(" CONFIGURACIONES", C_ORANGE);
for (int i = 0; i < N_CFG; i++) {
int y = 26 + i * 55;
if (cfgCursor == i) {
tft.fillRoundRect(4, y, W-8, 44, 4, C_GREY);
}
// Cursor
tft.setTextColor(C_YELLOW); tft.setTextSize(1);
tft.setCursor(8, y+8);
tft.print(cfgCursor == i ? ">" : " ");
// Nombre de la opcion
tft.setTextColor(cfgCursor == i ? C_CYAN : C_WHITE);
tft.setCursor(18, y+8);
tft.print(cfgTextos[i]);
// Valor actual (debajo del nombre)
tft.setTextColor(C_LGREY);
tft.setCursor(18, y+22);
char tmp[48];
switch (i) {
case 0:
snprintf(tmp, sizeof(tmp), "[%.0f,%.0f,%.0f,%.0f]",
Zdata[0], Zdata[1], Zdata[2], Zdata[3]);
tft.print(tmp);
break;
case 1:
tft.print(cfg_lineal ? "Lineal [a,b,c,d]" : "Natural [a,b]/[c,d]");
break;
case 2:
snprintf(tmp, sizeof(tmp), "Tamano: %d", cfg_tsize);
tft.print(tmp);
break;
}
}
tftFooter("2:arriba 8:abajo A:entrar D:volver");
}
// ----------------------------------------------------------------
// 3.1 CONFIGURAR LLAVE Z
// Formato de entrada: "a#b#c#d" (4 numeros separados por #)
// Valida que det(Z) != 0 antes de guardar.
// ----------------------------------------------------------------
char keyBuf[MAX_BUF]; // buffer separado para no pisar inputBuf
int keyLen = 0;
void drawCfgLlave() {
tft.fillScreen(C_BG);
tftHeader(" CFG: LLAVE Z", C_ORANGE);
// Clave actual
tft.setTextColor(C_LGREY); tft.setTextSize(1);
tft.setCursor(4, 22);
char tmp[48];
snprintf(tmp, sizeof(tmp), "Actual: [%.0f,%.0f,%.0f,%.0f]",
Zdata[0], Zdata[1], Zdata[2], Zdata[3]);
tft.print(tmp);
// Instruccion
tft.setTextColor(C_CYAN); tft.setCursor(4, 36);
tft.print("Nueva clave (a#b#c#d):");
// Cuadro de entrada
tft.drawRect(0, 48, W, H-64, C_GREY);
tft.fillRect(2, 50, W-4, H-66, C_BG);
tftWrapped(4, 52, W-8, H-70, keyBuf, C_WHITE, 1);
// Cursor al final del texto ingresado
int n = keyLen, cw = 6, lw = (W-8)/cw;
tft.fillRect(4 + (n % lw) * cw, 52 + (n / lw) * 9 + 7, cw-1, 2, C_CYAN);
tftFooter("A:guardar B:borrar C:limpiar D:volver");
}
void handleCfgLlave(char k) {
if (k == 'D') { pantallaActual = P_CFG; drawCfg(); return; }
if (k == 'C') { keyLen = 0; keyBuf[0] = '\0'; drawCfgLlave(); return; }
if (k == 'B') {
if (keyLen > 0) { keyLen--; keyBuf[keyLen] = '\0'; }
drawCfgLlave();
return;
}
if (k == 'A') {
if (keyLen == 0) { pantallaActual = P_CFG; drawCfg(); return; }
// Parsear "a#b#c#d" en cuatro floats
float v[4] = {0,0,0,0};
int cnt = 0;
String tok = "";
for (int i = 0; keyBuf[i] && cnt < 4; i++) {
if (keyBuf[i] == '#') {
if (tok.length()) { v[cnt++] = tok.toFloat(); tok = ""; }
} else {
tok += keyBuf[i];
}
}
if (tok.length()) v[cnt++] = tok.toFloat();
if (cnt < 4) {
// Faltan elementos: mostrar error y volver a la pantalla de llave
tft.fillRect(0, H-32, W, 16, C_RED);
tft.setTextColor(C_WHITE); tft.setCursor(4, H-28);
tft.print("! Necesito 4 valores (a#b#c#d)");
ledError(); delay(1500); drawCfgLlave(); return;
}
// Verificar que el determinante no sea cero
float det = v[0]*v[3] - v[1]*v[2];
if (fabs(det) < 1e-6) {
tft.fillRect(0, H-32, W, 16, C_RED);
tft.setTextColor(C_WHITE); tft.setCursor(4, H-28);
tft.print("! Clave no invertible (det=0)");
ledError(); delay(1500); drawCfgLlave(); return;
}
// Guardar y confirmar
for (int i = 0; i < 4; i++) Zdata[i] = v[i];
tft.fillRect(0, H-32, W, 16, C_GREEN);
tft.setTextColor(C_BG); tft.setCursor(4, H-28);
tft.print("Guardado!"); ledOK(); delay(900);
pantallaActual = P_CFG; drawCfg();
return;
}
// Solo digitos, '#' y '*' (negativo)
char c = '\0';
if (k >= '0' && k <= '9') c = k;
else if (k == '#') c = '#';
else if (k == '*') c = '-';
if (c && keyLen < MAX_BUF-2) {
keyBuf[keyLen++] = c;
keyBuf[keyLen] = '\0';
drawCfgLlave();
}
}
// ----------------------------------------------------------------
// 3.2 CONFIGURAR FORMATO DE SALIDA
// Lineal: [a,b,c,d],[e,f,g,h]
// Natural: [a,b]\n[c,d]\n\n[e,f]\n[g,h]
// ----------------------------------------------------------------
void drawCfgFormato() {
tft.fillScreen(C_BG);
tftHeader(" CFG: FORMATO SALIDA", C_ORANGE);
// Opcion 1: Lineal
if (cfg_lineal) tft.fillRoundRect(10, 36, W-20, 52, 4, C_YELLOW);
else tft.drawRoundRect(10, 36, W-20, 52, 4, C_GREY);
tft.setTextColor(cfg_lineal ? C_BG : C_WHITE);
tft.setTextSize(1);
tft.setCursor(20, 48); tft.print("1. LINEAL");
tft.setTextColor(cfg_lineal ? C_BG : C_LGREY);
tft.setCursor(20, 62); tft.print("Ej: [a,b,c,d],[e,f,g,h]");
// Opcion 2: Natural
if (!cfg_lineal) tft.fillRoundRect(10, 108, W-20, 68, 4, C_YELLOW);
else tft.drawRoundRect(10, 108, W-20, 68, 4, C_GREY);
tft.setTextColor(!cfg_lineal ? C_BG : C_WHITE);
tft.setCursor(20, 118); tft.print("2. NATURAL");
tft.setTextColor(!cfg_lineal ? C_BG : C_LGREY);
tft.setCursor(20, 132); tft.print("Ej: [a,b]");
tft.setCursor(20, 144); tft.print(" [c,d]");
tftFooter("1:Lineal 2:Natural D:volver");
}
void handleCfgFormato(char k) {
if (k == 'D') { pantallaActual = P_CFG; drawCfg(); return; }
if (k == '1') { cfg_lineal = true; drawCfgFormato(); }
if (k == '2') { cfg_lineal = false; drawCfgFormato(); }
}
// ----------------------------------------------------------------
// 3.3 CONFIGURAR TAMANO DE TEXTO (1 o 2)
// ----------------------------------------------------------------
void drawCfgTsize() {
tft.fillScreen(C_BG);
tftHeader(" CFG: TAMANO DE TEXTO", C_ORANGE);
tft.setTextColor(C_LGREY); tft.setTextSize(1);
tft.setCursor(4, 24);
tft.print("Tamano actual: "); tft.print(cfg_tsize);
tft.setTextColor(C_WHITE); tft.setCursor(4, 42);
tft.print("Presiona 1 o 2:");
// Botones de seleccion
for (int d = 1; d <= 2; d++) {
bool sel = (cfg_tsize == d);
if (sel) tft.fillRoundRect(20 + (d-1)*130, 70, 110, 50, 6, C_YELLOW);
else tft.drawRoundRect(20 + (d-1)*130, 70, 110, 50, 6, C_GREY);
tft.setTextColor(sel ? C_BG : C_WHITE);
tft.setTextSize(2);
tft.setCursor(55 + (d-1)*130, 86);
tft.print("T"); tft.print(d);
}
// Vista previa
tft.setTextSize(1); tft.setTextColor(C_LGREY);
tft.setCursor(4, 140); tft.print("Vista previa:");
tft.setTextSize(cfg_tsize); tft.setTextColor(C_WHITE);
tft.setCursor(4, 156); tft.print("Hola mundo");
tftFooter("1/2:elegir A:guardar D:volver");
tft.setTextSize(1);
}
void handleCfgTsize(char k) {
if (k == 'D') { pantallaActual = P_CFG; drawCfg(); return; }
if (k == '1') { cfg_tsize = 1; drawCfgTsize(); }
if (k == '2') { cfg_tsize = 2; drawCfgTsize(); }
if (k == 'A') { pantallaActual = P_CFG; drawCfg(); }
}
// ================================================================
// [12] ENRUTADOR DE EVENTOS
// ================================================================
/**
* Despacha la tecla presionada al manejador de la pantalla activa.
* Centraliza toda la logica de navegacion.
*
* @param k Tecla del keypad
*/
void handleKey(char k) {
switch (pantallaActual) {
// -- Menu principal ------------------------------------------
case P_MENU:
if (k == '2') {
menuCursor = (menuCursor - 1 + N_MENU) % N_MENU;
drawMenu();
} else if (k == '8') {
menuCursor = (menuCursor + 1) % N_MENU;
drawMenu();
} else if (k == 'A') {
inputLen = 0; inputBuf[0] = '\0'; outputBuf[0] = '\0';
switch (menuCursor) {
case 0: pantallaActual = P_ENCRIPTAR; drawEncriptar(); break;
case 1: pantallaActual = P_DESENCRIPTAR; drawDesencriptar(); break;
case 2: pantallaActual = P_CFG; cfgCursor = 0; drawCfg(); break;
}
} else if (k >= '1' && k <= '3') {
// Acceso directo por numero
menuCursor = k - '1';
handleKey('A');
}
break;
// -- Pantallas activas ---------------------------------------
case P_ENCRIPTAR: handleEncriptar(k); break;
case P_DESENCRIPTAR: handleDesencriptar(k); break;
// -- Submenu configuracion -----------------------------------
case P_CFG:
if (k == '2') { cfgCursor = (cfgCursor - 1 + N_CFG) % N_CFG; drawCfg(); }
else if (k == '8') { cfgCursor = (cfgCursor + 1) % N_CFG; drawCfg(); }
else if (k == 'A') {
inputLen = 0; inputBuf[0] = '\0';
keyLen = 0; keyBuf[0] = '\0';
switch (cfgCursor) {
case 0: pantallaActual = P_CFG_LLAVE; drawCfgLlave(); break;
case 1: pantallaActual = P_CFG_FORMATO; drawCfgFormato(); break;
case 2: pantallaActual = P_CFG_TSIZE; drawCfgTsize(); break;
}
} else if (k == 'D') {
pantallaActual = P_MENU;
menuCursor = N_MENU - 1; // volver con cursor en "Configuraciones"
drawMenu();
} else if (k >= '1' && k <= '3') {
cfgCursor = k - '1';
handleKey('A');
}
break;
// -- Subpantallas de configuracion ---------------------------
case P_CFG_LLAVE: handleCfgLlave(k); break;
case P_CFG_FORMATO: handleCfgFormato(k); break;
case P_CFG_TSIZE: handleCfgTsize(k); break;
}
}
// ================================================================
// [13] SETUP Y LOOP
// ================================================================
// Variables para el temporizador T9 (confirmar caracter por timeout)
static unsigned long lastKeyMs = 0;
static bool t9Pending = false;
/**
* Inicializacion del hardware y pantalla de bienvenida.
*/
void setup() {
Serial.begin(115200);
// Inicializar LEDs
pinMode(LED_G, OUTPUT);
pinMode(LED_R, OUTPUT);
ledOff();
// Inicializar TFT
// Los pines SPI0 (GP2/GP3/GP4) son configurados internamente por tft.begin().
tft.begin();
tft.setRotation(3); // landscape: 320x240
tft.fillScreen(C_BG);
// Configurar debounce de la libreria Keypad.
// setDebounceTime: tiempo minimo en ms que una tecla debe estar
// presionada para registrarse (por defecto 10ms en la lib).
// setHoldTime: tiempo para que el estado pase a HOLD (no usado aqui).
keypad.setDebounceTime(50); // 50ms de debounce en la libreria
keypad.setHoldTime(500);
// Pantalla de bienvenida
tft.setTextColor(C_CYAN); tft.setTextSize(2);
tft.setCursor(20, 60); tft.print("Encriptador");
tft.setCursor(20, 86); tft.print("Algebraico v3");
tft.setTextColor(C_LGREY); tft.setTextSize(1);
tft.setCursor(20, 120); tft.print("Yoltzin Hernandez Canseco");
tft.setCursor(20, 136); tft.print("Raspberry Pi Pico - sin OneWire");
// Parpadeo de inicio en los LEDs
for (int i = 0; i < 3; i++) {
ledSet(1, 1); delay(120);
ledOff(); delay(120);
}
delay(800);
// Mostrar menu principal
drawMenu();
}
/**
* Bucle principal.
*
* Estrategia no bloqueante para Pico:
* 1. Verificar timeout T9 con millis() (sin delay).
* 2. Leer keypad; si no hay tecla, salir inmediatamente.
* 3. Despachar tecla al manejador de pantalla activa.
*
* El refresco es PARCIAL: solo se redibuja el area que cambio,
* no toda la pantalla, lo que elimina el parpadeo y reduce la carga.
*/
void loop() {
// ── Timeout T9: confirmar caracter pendiente ─────────────────
// Si el usuario no presiona nada en T9_TIMEOUT ms, el caracter
// actual queda fijo en el buffer. Solo refrescamos el cursor.
if (t9Pending && (millis() - lastKeyMs) > (unsigned long)(T9_TIMEOUT + 50)) {
t9Pending = false;
if (pantallaActual == P_ENCRIPTAR) {
refreshInputArea(20, SPLIT_Y-20, inputBuf, 1);
}
}
// ── Leer teclado con filtro anti-rebote ─────────────────────
// getKeyFiltered() exige KEY_HOLD lecturas estables + silencio
// DEBOUNCE_MS para evitar teclas fantasma y rebotes mecanicos.
char k = getKeyFiltered();
if (k == '\0') return;
// Registrar el momento de la pulsacion para el timeout T9
lastKeyMs = millis();
// Marcar como pendiente T9 solo si estamos en pantalla de texto
// y la tecla es numerica (genera caracter T9)
bool esTeclaNumerica = (k >= '0' && k <= '9') || k == '*';
t9Pending = esTeclaNumerica && (pantallaActual == P_ENCRIPTAR);
// Despachar al manejador de la pantalla activa
handleKey(k);
}