# Módulo M8 – Programa principal de integración (control de asistencia con ESP32)
# Código generado por IA (Claude). Revisor: PENDIENTE (falta el nombre del revisor).
# Proyecto: control de asistencia con ESP32 DevKit V1 y MicroPython (Wokwi)
import time
import salidas
import lcd
import teclado
import pulsador
import validacion
import reloj
import registro
# --- Tiempos (milisegundos) ---
TIEMPO_MENSAJE_MS = 1500 # cuánto se muestra un mensaje de resultado
TIEMPO_FIN_SESION_MS = 3000 # cuánto se muestra el resumen al finalizar la sesión
PAUSA_BUCLE_MS = 5 # pausa corta al final de cada vuelta
PAUSA_ERROR_MS = 100 # pausa tras un error inesperado, para no saturar la consola
# --- Textos del LCD (máximo 16 caracteres por fila, sin tildes) ---
TXT_CONECTANDO = "Conectando..."
TXT_SINCRONIZANDO = "Sincronizando"
TXT_HORA_PUNTOS = "hora..."
TXT_SIN_WIFI = "Sin WiFi"
TXT_SIN_HORA = "Sin hora"
TXT_PULSE_BOTON = "Pulse boton"
TXT_SESION_INACTIVA = "Sesion inactiva"
TXT_PULSE_EL_BOTON = "Pulse el boton"
TXT_INGRESE_CODIGO = "Ingrese codigo:"
TXT_SESION_TERMINADA = "Sesion terminada" # "Sesion finalizada" no cabe en 16 caracteres
TXT_INCOMPLETO = "Incompleto"
TXT_FALTAN_DIGITOS = "Faltan digitos"
TXT_REGISTRADO = "Registrado"
TXT_YA_REGISTRADO = "Ya registrado"
TXT_CODIGO_INVALIDO = "Codigo invalido"
TXT_ERROR_ARCHIVO = "Error archivo"
TXT_ERROR_GUARDAR = "Error al guardar"
TXT_NO_SE_REGISTRA = "No se registra"
TXT_PULSE_PARA_SALIR = "Pulse para salir"
# --- Estado del programa ---
lcd_listo = False # True si el LCD se inicializó bien
motivo_sin_hora = TXT_SIN_HORA # texto a mostrar mientras no haya hora
mensaje_hasta = None # momento (ticks_ms) en que vence el mensaje temporal
archivo_sesion_ok = False # True si el archivo CSV de la sesión actual se creó bien
vuelta_max_us = 0 # vuelta más lenta entre las vueltas sin eventos (microsegundos)
vueltas_sin_evento = 0 # cuántas vueltas sin eventos se midieron
def mostrar_lcd(linea1, linea2=""):
"""Muestra texto en el LCD solo si se inicializó correctamente."""
if lcd_listo:
lcd.mostrar(linea1, linea2)
def mostrar_mensaje(linea1, linea2="", duracion_ms=TIEMPO_MENSAJE_MS):
"""Muestra un mensaje temporal; el bucle restaura la pantalla cuando vence."""
global mensaje_hasta
mostrar_lcd(linea1, linea2)
mensaje_hasta = time.ticks_add(time.ticks_ms(), duracion_ms)
def mostrar_pantalla_actual():
"""Muestra la pantalla que corresponde al estado actual del sistema."""
if not reloj.hora_sincronizada():
mostrar_lcd(motivo_sin_hora, TXT_PULSE_BOTON)
elif pulsador.sesion_activa():
mostrar_lcd(TXT_INGRESE_CODIGO, teclado.obtener_codigo())
else:
mostrar_lcd(TXT_SESION_INACTIVA, TXT_PULSE_EL_BOTON)
def intentar_conexion():
"""Conecta el WiFi y sincroniza la hora. Devuelve True si ambas cosas salieron bien."""
global motivo_sin_hora
mostrar_lcd(TXT_CONECTANDO)
if not reloj.conectar_wifi():
motivo_sin_hora = TXT_SIN_WIFI
return False
mostrar_lcd(TXT_SINCRONIZANDO, TXT_HORA_PUNTOS)
if not reloj.sincronizar_hora():
motivo_sin_hora = TXT_SIN_HORA
return False
return True
def arrancar():
"""Inicializa los módulos y obtiene la hora. Sin hora, el sistema queda bloqueado."""
global lcd_listo
lcd_listo = lcd.iniciar_lcd()
if not lcd_listo:
print("AVISO: el LCD no está disponible; se informará solo por consola.")
teclado.iniciar_teclado()
pulsador.iniciar_pulsador()
if intentar_conexion():
print("Sistema listo. Presione el pulsador para iniciar la sesión.")
else:
print("Sin hora: no se puede iniciar sesión. Presione el pulsador para reintentar.")
mostrar_pantalla_actual()
def iniciar_sesion():
"""Prepara una sesión nueva: registros en cero y un archivo CSV nuevo."""
global mensaje_hasta, archivo_sesion_ok
mensaje_hasta = None # se cancela cualquier mensaje pendiente
fecha_hora = reloj.obtener_fecha_hora()
if fecha_hora is None: # no debería ocurrir con la hora sincronizada
archivo_sesion_ok = False
mostrar_mensaje(TXT_SIN_HORA, TXT_PULSE_PARA_SALIR)
salidas.senal_error()
return
fecha, hora = fecha_hora
validacion.reiniciar_registros()
teclado.reiniciar_codigo()
nombre_archivo = registro.iniciar_archivo(fecha, hora)
archivo_sesion_ok = nombre_archivo is not None
if not archivo_sesion_ok:
mostrar_mensaje(TXT_ERROR_ARCHIVO, TXT_PULSE_PARA_SALIR)
salidas.senal_error()
return
print("Sesión iniciada. Archivo:", nombre_archivo)
mostrar_pantalla_actual()
def finalizar_sesion():
"""Muestra el total de registros y el contenido completo del CSV en la consola."""
total = validacion.cantidad_registrados()
teclado.reiniciar_codigo()
mostrar_mensaje(TXT_SESION_TERMINADA, "Registros: " + str(total), TIEMPO_FIN_SESION_MS)
print("=" * 32)
if archivo_sesion_ok:
print("SESIÓN FINALIZADA. Archivo:", registro.archivo_actual())
for linea in registro.consultar_registros():
print(linea)
else:
print("SESIÓN FINALIZADA sin archivo: no se pudo crear al iniciarla.")
print("Total de registros:", total)
if vueltas_sin_evento > 0:
print("Vuelta más lenta sin eventos:", vuelta_max_us, "us (", vueltas_sin_evento, "vueltas medidas)")
print("=" * 32)
def manejar_pulsador():
"""Atiende el pulsador. Devuelve True si hubo una pulsación."""
if not reloj.hora_sincronizada():
# Sin hora no se cambia el estado de la sesión: la pulsación solo reintenta la conexión
if pulsador.pulsador_presionado():
print("Sin hora: se reintenta la conexión...")
intentar_conexion()
mostrar_pantalla_actual()
return True
return False
cambio = pulsador.actualizar_sesion()
if cambio == "INICIADA":
iniciar_sesion()
elif cambio == "FINALIZADA":
finalizar_sesion()
return cambio is not None
def procesar_confirmacion():
"""Valida el código confirmado con # y lo registra. Primero el mensaje, después la señal."""
inicio_ms = time.ticks_ms()
codigo = teclado.obtener_codigo()
resultado, nombre = validacion.validar_codigo(codigo)
exito = False
if resultado == "VALIDO":
fecha_hora = reloj.obtener_fecha_hora()
if fecha_hora is None:
linea1, linea2, detalle = TXT_SIN_HORA, TXT_NO_SE_REGISTRA, "SIN_HORA"
elif not archivo_sesion_ok:
linea1, linea2, detalle = TXT_ERROR_ARCHIVO, TXT_NO_SE_REGISTRA, "SIN_ARCHIVO"
else:
fecha, hora = fecha_hora
guardado = registro.guardar_registro(codigo, nombre, fecha, hora)
if guardado == "OK":
validacion.marcar_registrado(codigo) # solo si el CSV se guardó bien
linea1, linea2, detalle, exito = nombre, TXT_REGISTRADO, "REGISTRADO", True
elif guardado == "DUPLICADO":
linea1, linea2, detalle = TXT_YA_REGISTRADO, nombre, "DUPLICADO"
else:
linea1, linea2, detalle = TXT_ERROR_GUARDAR, codigo, "ERROR_GUARDAR"
elif resultado == "REPETIDO":
linea1, linea2, detalle = TXT_YA_REGISTRADO, nombre, "REPETIDO"
else:
linea1, linea2, detalle = TXT_CODIGO_INVALIDO, codigo, "INVALIDO"
mostrar_mensaje(linea1, linea2) # 1.º el mensaje en el LCD
transcurrido_ms = time.ticks_diff(time.ticks_ms(), inicio_ms)
teclado.reiniciar_codigo()
print("Código:", codigo, "| Resultado:", detalle, "|", transcurrido_ms, "ms hasta la señal")
if exito: # 2.º la señal (bloquea 0.6 a 0.7 s)
salidas.senal_exito()
else:
salidas.senal_error()
def manejar_teclado():
"""Atiende el teclado. Devuelve True si se presionó una tecla."""
global mensaje_hasta
tecla = teclado.leer_tecla()
if tecla is None:
return False
if not reloj.hora_sincronizada():
return True # sin hora, las teclas se descartan
resultado = teclado.procesar_tecla(tecla)
if not pulsador.sesion_activa():
# Sin sesión activa solo se rechaza al confirmar; los dígitos se ignoran sin mostrar nada
if resultado == "CONFIRMADO" or resultado == "INCOMPLETO":
mostrar_mensaje(TXT_SESION_INACTIVA, TXT_PULSE_EL_BOTON)
teclado.reiniciar_codigo()
salidas.senal_error()
return True
if resultado == "DIGITO" or resultado == "BORRADO":
mensaje_hasta = None # se cancela el mensaje pendiente y se muestra lo escrito
mostrar_pantalla_actual()
elif resultado == "INCOMPLETO":
mostrar_mensaje(TXT_INCOMPLETO, TXT_FALTAN_DIGITOS) # conserva los dígitos escritos
elif resultado == "CONFIRMADO":
procesar_confirmacion()
return True
def revisar_mensaje_temporal():
"""Cuando vence un mensaje temporal, restaura la pantalla. Devuelve True si lo hizo."""
global mensaje_hasta
if mensaje_hasta is not None and time.ticks_diff(time.ticks_ms(), mensaje_hasta) >= 0:
mensaje_hasta = None
mostrar_pantalla_actual()
return True
return False
def principal():
"""Arranca el sistema y ejecuta el bucle principal."""
global vuelta_max_us, vueltas_sin_evento
arrancar()
while True:
inicio_us = time.ticks_us()
hubo_evento = False
try:
if manejar_pulsador():
hubo_evento = True
if manejar_teclado():
hubo_evento = True
if revisar_mensaje_temporal():
hubo_evento = True
except Exception as error: # un error inesperado no debe detener el programa
print("ERROR inesperado en el bucle principal:", error)
hubo_evento = True
time.sleep_ms(PAUSA_ERROR_MS)
# Solo se mide la duración de las vueltas sin eventos (las demás incluyen escrituras al LCD y señales)
if not hubo_evento:
duracion_us = time.ticks_diff(time.ticks_us(), inicio_us)
vueltas_sin_evento += 1
if duracion_us > vuelta_max_us:
vuelta_max_us = duracion_us
time.sleep_ms(PAUSA_BUCLE_MS)
if __name__ == "__main__":
principal()