/*
Consideraciones en uso de botones: INPUT_PULLUP vs INPUT_PULLDOWN
Presionado Sin presionar Resistencia
INPUT_PULLUP HIGH == 0 LOW == 1 no requiere, usa la interna
INPUT_PULLDOWN HIGH == 1 LOW == 0 requiere resistencia 10k en tierra
(Para este proyecto se usa INPUT_PULLUP)
Notas: Menu configurable (no muy intuitivo por el momento) de
multiples niveles, las funciones son invocadas dinamicamente.
Cambiando la variable menuFilas se pueden utilizar otras pantallas
con mas filas dinamicamente, ya esta contemplado.
Intente utlizar una libreria de menu, pero fue complicado desde el
simulador porque requieren modificacion de archivos en la libreria
que no son modificables en el simulador,
ver https://github.com/Jomelo/LCDMenuLib2
*/
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
// #include <Arduino.h>
// Variables del menú
const int menuColumnas = 16, menuFilas = 2;
const int menuFlechaDerechaCaracter = 126, posMenuFlecha = 0, posMenuOpcion = 1;
int posMenuActual = 0, posFlechaActual = 0; // Posicion inicial del menu
#define maxPilaMenu 20 // Hasta diez niveles de menu (2 valores por nivel)
int pilaMenu[maxPilaMenu], pilaMenuTope = -1;
// Inicializa el LCD 1602 con dirección I2C (usualmente 0x27 o 0x3F)
LiquidCrystal_I2C lcd(0x27, menuColumnas, menuFilas); // Dirección 0x27, 16 columnas y 2 filas
// Definir los pines de los botones
const int botArriba = 4, botAbajo = 5, botSeleccion = 6;
const int pinLedRojo = 9, pinLedAzul = 8, pinLedVerde = 10;
// Para controlar reimpresion luego de detectar una presion de boton
bool indArriba = false, indAbajo = false, indSeleccion = false;
//
// Funciones que se usan en el menu
//
void estadoLed(int pin, bool estado){
if (estado) digitalWrite(pin, HIGH);
else digitalWrite(pin, LOW);
}
void f_led_rojo_on() { estadoLed(pinLedRojo, true); }
void f_led_rojo_off() { estadoLed(pinLedRojo, false); }
void f_led_azul_on() { estadoLed(pinLedAzul, true); }
void f_led_azul_off() { estadoLed(pinLedAzul, false); }
void f_led_verde_on() { estadoLed(pinLedVerde, true); }
void f_led_verde_off() { estadoLed(pinLedVerde, false); }
// Si el los leds estan encendidos los apaga y viceversa
void f_navidad() {
bool estado;
estado = (digitalRead(pinLedRojo) == HIGH);
estadoLed(pinLedRojo, !estado);
estado = (digitalRead(pinLedAzul) == HIGH);
estadoLed(pinLedAzul, !estado);
estado = (digitalRead(pinLedVerde) == HIGH);
estadoLed(pinLedVerde, !estado);
}
// Creamos una estructura para el menu que asocia nombres
// de funciones con punteros a función, de manera que no hay
// que hacer nada mas definirlas y automaticamente se invocan
// al presionar el bonton de seleccion
struct strMenu {
const char* opcion; // Descripcion de la opcion de menu
int nivel; // Solo de adorno por el momento
void (*func)(); // Puntero a función
strMenu* subMenu; // Puntero a otros menu (submenu)
strMenu* menuAnterior; // Puntero al menu anterior
};
strMenu* menuActual; // Para manejo del menu
//*************************************************************
// IMPORTANTE: TODOS LOS MENUS DEBEN TERMINAR CON UN REGISTRO
// EN NULO y el orden de los jerarquico de los menus
// se debe respetar, no se deben poner atajos al menu
// principal por ejemplo. Si ese fuera el caso debe
// revisarse la logica de la pila de menu
//*************************************************************
strMenu subsubMenuRojo[] = {
{"Atras", 2, nullptr, nullptr, nullptr},
{nullptr, 0, nullptr, nullptr, nullptr}
};
strMenu subMenuRojo[] = {
{"ON", 2, f_led_rojo_on, nullptr, nullptr},
{"OFF", 2, f_led_rojo_off, nullptr, nullptr},
{"SMR", 2, nullptr, subsubMenuRojo, nullptr},
{"Atras", 2, nullptr, nullptr, nullptr},
{nullptr, 0, nullptr, nullptr, nullptr}
};
strMenu subMenuAzul[] = {
{"ON", 2, f_led_azul_on, nullptr, nullptr},
{"OFF", 2, f_led_azul_off, nullptr, nullptr},
{"Atras", 2, nullptr, nullptr, nullptr},
{nullptr, 0, nullptr, nullptr, nullptr}
};
strMenu subMenuVerde[] = {
{"ON", 2, f_led_verde_on, nullptr, nullptr},
{"OFF", 2, f_led_verde_off, nullptr, nullptr},
{"Atras", 2, nullptr, nullptr, nullptr},
{nullptr, 0, nullptr, nullptr, nullptr}
};
strMenu subMenuSoloUno[] = {
{"Atras", 2, nullptr, nullptr, nullptr},
{nullptr, 0, nullptr, nullptr, nullptr}
};
//
// Estructura del menu principal
strMenu menuPrincipal[] = {
{"Led Rojo", 1, nullptr, subMenuRojo, nullptr},
{"Led Azul", 1, nullptr, subMenuAzul, nullptr},
{"Led Verde", 1, nullptr, subMenuVerde, nullptr},
{"Navidad!", 1, f_navidad, nullptr, nullptr},
{"Solo uno", 1, nullptr, subMenuSoloUno, nullptr},
{nullptr, 0, nullptr, nullptr, nullptr}
};
// Se definen caracteres personalizados
const int flechaArribaCaracter = 0, flechaAbajoCaracter = 1;
const int corazonCaracter = 2;
byte flechaArriba[8] = {
0b00100,
0b01110,
0b10101,
0b00100,
0b00100,
0b00000,
0b00000,
0b00000
};
byte flechaAbajo[8] = {
0b00000,
0b00000,
0b00100,
0b00100,
0b10101,
0b01110,
0b00100,
0b00000
};
byte corazon[8] = {
0b00000,
0b01010,
0b11111,
0b11111,
0b11111,
0b01110,
0b00100,
0b00000
};
void setup() {
Serial.begin(115200);
// Configura los pines de los botones
pinMode(botArriba, INPUT_PULLUP);
pinMode(botAbajo, INPUT_PULLUP);
pinMode(botSeleccion, INPUT_PULLUP);
pinMode(pinLedRojo, OUTPUT);
pinMode(pinLedAzul, OUTPUT);
pinMode(pinLedVerde, OUTPUT);
// Configuracion de menu
// Necesario para navegacion de menu, el indice del vector
// coincide con la opcion "Atras" del submenu correspondiente
menuActual = menuPrincipal;
subMenuRojo[3].menuAnterior = menuPrincipal;
subMenuAzul[2].menuAnterior = menuPrincipal;
subMenuVerde[2].menuAnterior = menuPrincipal;
subMenuSoloUno[0].menuAnterior = menuPrincipal;
subsubMenuRojo[0].menuAnterior = subMenuRojo;
lcd.init(); // Inicializa la pantalla
lcd.backlight(); // Enciende la luz de fondo
// Crea y almacena caracteres especiales (maxinmo 8)
lcd.createChar(flechaArribaCaracter, flechaArriba);
lcd.createChar(flechaAbajoCaracter, flechaAbajo);
lcd.createChar(corazonCaracter, corazon);
// Mensaje de bienvenida
lcd.setCursor(0, 0);
lcd.write(byte(corazonCaracter));
lcd.setCursor(0, menuFilas-1);
lcd.write(byte(corazonCaracter));
lcd.setCursor(menuColumnas-1, 0);
lcd.write(byte(corazonCaracter));
lcd.setCursor(menuColumnas-1, menuFilas-1);
lcd.write(byte(corazonCaracter));
lcd.setCursor(1, 0);
lcd.print("Bienvenido");
lcd.setCursor(1, 1);
lcd.print("Don Gato!");
delay(3000);
menuLCDMostrar();
}
void loop() {
// Leer los botones
if (digitalRead(botArriba) == LOW) {
indArriba = true;
if (posMenuActual > 0) posMenuActual--;
if (posFlechaActual > 0) posFlechaActual--;
menuLCDMostrar();
}
if (digitalRead(botAbajo) == LOW) {
indAbajo = true;
// Si existen mas opciones de menu hacia abajo
if (menuActual[posMenuActual + 1].opcion != nullptr) {
posMenuActual++;
// menuFilas es la cantidad maxima de filas del menu
if (posFlechaActual < (menuFilas-1) ) posFlechaActual++;
}
menuLCDMostrar();
}
if (digitalRead(botSeleccion) == LOW) {
indSeleccion = true;
// Si hay funcion asociada invocarla
if (menuActual[posMenuActual].func != nullptr) menuActual[posMenuActual].func();
// Si hay submenu asociado, cambiar menu actual
if (menuActual[posMenuActual].subMenu != nullptr) {
// Almacena los valores de menu actuales para cuando
// regrese mostrar el menu en la misma posicion
push(posMenuActual);
push(posFlechaActual);
// Cambia el menu actual por el submenu en la opcion seleccionada
menuActual = menuActual[posMenuActual].subMenu;
posMenuActual = 0;
posFlechaActual = 0;
}
// Si hay menu anterior, actualizar el menu actual
else if (menuActual[posMenuActual].menuAnterior != nullptr) {
// Cambia el menu actual por el menuAnterior, basicamente el menu
// que invocó este submenu, dado que solamente los submenus
// deberian tener asociado un menu anterior
menuActual = menuActual[posMenuActual].menuAnterior;
// El orden debe ser inverso al que se uso en la carga del valor (push)
posFlechaActual = pop();
posMenuActual = pop();
// Si se detecta error mostrar menu desde la posicion 0 (inicio)
if (posMenuActual < 0 || posFlechaActual < 0) {
posFlechaActual = 0;
posMenuActual = 0;
}
}
menuLCDMostrar();
}
}
void menuLCDMostrar(){
// Muestra los valores el menu en la pantalla LCD
lcd.clear();
// Se imprimen en pantalla las opciones dependiendo del
// tamano (filas) de la pantalla. Las posiciones van de 0 a 'x'
//Serial.println(String(posFlechaActual) + " - " + String(posMenuActual));
lcd.setCursor(posMenuFlecha, posFlechaActual);
lcd.print(char(menuFlechaDerechaCaracter));
int posRelativa, posUltimaOpcionEscrita = -1;
//
// PARA ENTENDER LA LOGICA DE LA IMPRESION EN PANTALLA ES
// INDISPENSABLE ENTENDER EL CALCULO DE LA POSICION RELATIVA
//
// Primero recordar que la logica esta hecha para trabajar con un
// LCD de cualquier tamaño, vamos a poner por ejemplo uno de 4 lineas
// y 50 columnas (se configura en las constantes al inicio del programa).
// Ahara imagina que tenemos un menu principal o submenu de 10 opciones
// (tanto menu como submenu usan la misma estructura "strMenu"
// asi que da igual si es uno u otro).
// Si la posición en el menu es 5 y la flecha en pantalla es 2,
// la posición relativa de reimpresion sera igual a 5-2 = 3, y
// se mostrarán en pantalla los registro 3, 4, 5, y 6
// (ya que son cuatro lineas en pantala),
// donde 5 esta en la tercera posicion (la 2 en el ldc)
posRelativa = posMenuActual - posFlechaActual;
if (posRelativa >= 0 ) {
int posLcd = 0; // Para impresion en LCD desde la prima linea
for (int i = posRelativa; i < (menuFilas + posRelativa); i++){
// Si el campo actual no es nulo
if (menuActual[i].opcion != nullptr){
lcd.setCursor(posMenuOpcion, posLcd);
lcd.print(menuActual[i].opcion);
posLcd ++;
// Ultima opcion de menu que se mostro en pantalla
posUltimaOpcionEscrita = i;
}
else break; // Si se encuentra registro nulo salir del ciclo, ya no hay mas opciones de menu hacia abajo
}
// Si la posicion relativa calculada es mayor a cero,
// entonces hay opciones de menu no visibles hacia arriba
if (posRelativa > 0){
lcd.setCursor(menuColumnas-1, 0);
lcd.write(byte(flechaArribaCaracter));
}
// Si se escribio al menos una linea del menu...
if (posUltimaOpcionEscrita > 0){
// Si existe al menos una opcion de menu valida para mostrar,
// pero que no se ha mostrado todavia
if (menuActual[posUltimaOpcionEscrita+1].opcion != nullptr){
lcd.setCursor(menuColumnas-1, menuFilas-1);
lcd.write(byte(flechaAbajoCaracter));
}
}
}
delay(200); // Para evitar el rebote de los botones
}
// Carga valores en la pila del menu
void push(int valor) {
if (pilaMenuTope < maxPilaMenu - 1) {
pilaMenu[++pilaMenuTope] = valor; // Agregar un valor a la pila
}
else {
Serial.println("Pila llena");
}
}
// Descarga valores de la pila del menu
int pop() {
if (pilaMenuTope >= 0) {
return pilaMenu[pilaMenuTope--]; // Eliminar y devolver el último valor
}
else {
Serial.println("Pila vacía");
return -1; // Indicador de error
}
}
Arriba
Abajo
Seleccion