# main.py - Código unificado de sistema embebido con arquitectura FQS
import time
import math
import random
from machine import Pin, I2C, ADC, Timer, PWM
from ssd1306 import SSD1306_I2C
import framebuf
from machine import time_pulse_us
import csv # Necesario para Sokoban (aunque lo hemos omitido, el import queda por si acaso)
import dht # Para el sensor DHT22 -- DESCOMENTADO
print("Iniciando script...")
# --- Constantes y Pines ---
WIDTH, HEIGHT = 128, 64 # Dimensiones de la pantalla OLED
# Pines I2C para la pantalla OLED
SCL_PIN = 5
SDA_PIN = 4
# Pines para el Joystick Analógico (Zoom en Calculadora, Cursor)
JOY_X_PIN = 26 # GP26
JOY_Y_PIN = 27 # GP27
# Pines para los Botones (4 Direccionales + 1 Seleccionar + 1 Atrás)
BTN_UP_PIN = 10 # GP10 (Arriba)
BTN_DOWN_PIN = 11 # GP11 (Abajo)
BTN_RIGHT_PIN = 12 # GP12 (Derecha)
BTN_LEFT_PIN = 13 # GP13 (Izquierda)
BTN_ENTER_PIN = 15 # GP15 (Ahora es ENTER/Seleccionar)
BTN_BACK_PIN = 16 # GP16 (NUEVO BOTÓN: ATRÁS)
# Pin para el Buzzer
BUZZER_PIN = 14 # GP14
# Pin para el sensor DHT22
DHT22_PIN = 19 # GP19 -- DESCOMENTADO
# Pines para HC-SR04 -- AÚN COMENTADO PARA DIAGNÓSTICO
# TRIGGER_PIN = 20 # GPIO20
# ECHO_PIN = 21 # GPIO21
# DISTANCE_THRESHOLD = 5 # cm para apagar OLED
# --- Definición de Modos del Sistema ---
MODE_PRESENTATION = -1 # Un modo inicial para la pantalla de bienvenida
MODE_MENU = 0
MODE_CLOCK = 1
MODE_CHRONOMETER = 2
MODE_ANALOG_CLOCK = 3
# MODE_SOKOBAN = 4 # Eliminado
# MODE_GRAPH_CALCULATOR = 5 # Eliminado
MODE_TEMPERATURE = 6
MODE_CALENDAR = 7
# --- Inicialización de Hardware ---
oled_initialized = False
try:
print("Inicializando I2C y OLED...")
i2c = I2C(0, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN), freq=400000)
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)
oled_initialized = True
print("OLED inicializada exitosamente.")
except Exception as e:
print(f"ERROR: Fallo al inicializar OLED en pines SCL={SCL_PIN}, SDA={SDA_PIN}. Mensaje: {e}")
buzzer = PWM(Pin(BUZZER_PIN))
buzzer.freq(1000) # Frecuencia inicial para el buzzer
buzzer.duty_u16(0) # Apagado inicialmente
print("Buzzer inicializado.")
# Inicialización de pines de botones con pull-up interno
btn_up = Pin(BTN_UP_PIN, Pin.IN, Pin.PULL_UP)
btn_down = Pin(BTN_DOWN_PIN, Pin.IN, Pin.PULL_UP)
btn_right = Pin(BTN_RIGHT_PIN, Pin.IN, Pin.PULL_UP)
btn_left = Pin(BTN_LEFT_PIN, Pin.IN, Pin.PULL_UP)
btn_enter = Pin(BTN_ENTER_PIN, Pin.IN, Pin.PULL_UP)
btn_back = Pin(BTN_BACK_PIN, Pin.IN, Pin.PULL_UP)
print("Botones inicializados.")
# Inicialización de ADCs para el joystick
joy_x = ADC(JOY_X_PIN)
joy_y = ADC(JOY_Y_PIN)
print("Joystick ADCs inicializados.")
# Inicialización del sensor DHT22
dht_sensor = dht.DHT22(Pin(DHT22_PIN)) # DESCOMENTADO
print("Sensor DHT22 inicializado (o intento).")
# Inicialización del sensor HC-SR04 -- AÚN COMENTADO PARA DIAGNÓSTICO
# trigger_pin = Pin(TRIGGER_PIN, Pin.OUT)
# echo_pin = Pin(ECHO_PIN, Pin.IN)
# trigger_pin.low()
# print("HC-SR04 inicializado.")
# --- Variables Globales del Sistema ---
current_mode = MODE_PRESENTATION # Estado actual del sistema
# Variables para el reloj digital global (se actualiza cada segundo)
global_h = 0
global_m = 0
global_s = 0
# Timer para el reloj global (actualiza variables globales, no la UI)
def tick_global_reloj(t):
global global_h, global_m, global_s
global_s += 1
if global_s >= 60:
global_s = 0
global_m += 1
if global_m >= 60:
global_m = 0
global_h += 1
if global_h >= 24:
global_h = 0
try:
print("Inicializando Timer global para el reloj...")
timer_global_reloj = Timer()
timer_global_reloj.init(freq=1, mode=Timer.PERIODIC, callback=tick_global_reloj)
print("Timer global inicializado.")
except Exception as e:
print(f"ERROR: Fallo al inicializar el Timer global. Mensaje: {e}")
# Variables para el cronómetro
ch = cm = cs = cc = 0
cronometro_activo = False
# Variables para el calendario
global_day = 1
global_month = 1
global_year = 2025
# Variables de estado para los sub-modos de ajuste
clock_setting_active = False
time_setting_part = 0 # 0: Horas, 1: Minutos, 2: Segundos
current_setting_h = 0
current_setting_m = 0
current_setting_s = 0
calendar_setting_active = False
date_setting_part = 0 # 0: Día, 1: Mes, 2: Año
current_setting_day = 1
current_setting_month = 1
current_setting_year = 2025
# Variable para rastrear el estado de la OLED (power)
oled_powered = True
# --- Funciones Auxiliares del Sistema ---
# Control del Buzzer para sonido de clic
def play_click_sound():
if buzzer is not None:
buzzer.duty_u16(5000)
buzzer.freq(2000)
time.sleep_ms(50)
buzzer.duty_u16(0)
# Diccionario para almacenar los estados de los botones para debounce.
button_last_states = {
'up': [True], 'down': [True], 'right': [True],
'left': [True], 'enter': [True], 'back': [True]
}
# Función para debouncing de botones. Retorna True si el botón fue presionado (transición de alto a bajo)
def check_button_press(button_pin, last_state_var_list):
current_state = button_pin.value()
if not current_state and last_state_var_list[0]:
last_state_var_list[0] = False
play_click_sound() # Reproducir sonido al presionar
return True
elif current_state and not last_state_var_list[0]:
last_state_var_list[0] = True
return False
# Muestra el reloj digital global en una esquina de la OLED
def display_global_clock():
if oled_initialized:
oled.text("{:02}:{:02}".format(global_h, global_m), 80, 0) # Posición superior derecha
# --- Clases base para la arquitectura FQS ---
class Task:
"""Clase base para todas las tareas que serán encoladas y ejecutadas."""
def __init__(self, name="GenericTask", priority=0, requires_oled=False):
self.name = name
self.priority = priority
self.requires_oled = requires_oled # Indica si la tarea necesita la OLED encendida
def execute(self):
"""Método que debe ser implementado por las subclases para realizar la acción de la tarea."""
raise NotImplementedError("El método execute() debe ser implementado por la subclase.")
def __lt__(self, other):
"""Define el comportamiento 'menor que' para comparación de prioridades (para PriorityQueue)."""
return self.priority < other.priority
def __repr__(self):
return f"<Task: {self.name}, Prio: {self.priority}, OLED: {self.requires_oled}>"
class UITask(Task):
"""Tarea para actualizar la interfaz de usuario de un modo específico."""
def __init__(self, mode, name="UITask", priority=5): # UI es de prioridad media
super().__init__(name, priority, requires_oled=True)
self.mode = mode
def execute(self):
# La ejecución real la maneja el scheduler/main loop a través de mode_handlers
pass
class ButtonPressTask(Task):
"""Tarea que representa una pulsación de botón con su acción asociada."""
def __init__(self, button_type, current_mode, extra_params=None, priority=10, name="ButtonTask"):
super().__init__(name, priority) # Las pulsaciones de botón suelen ser de alta prioridad
self.button_type = button_type
self.current_mode = current_mode
self.extra_params = extra_params if extra_params is not None else {}
def execute(self):
# La ejecución real la maneja el scheduler a través de button_action_handlers
pass
class ClockTickTask(Task):
"""Tarea para actualizar la lógica de tiempo del cronómetro o reloj. Alta prioridad para precisión."""
def __init__(self, target_clock, priority=8, name="ClockTickTask"):
super().__init__(name, priority)
self.target_clock = target_clock
def execute(self):
if self.target_clock == 'global_clock':
tick_global_reloj(None) # Llama al callback del timer directamente
elif self.target_clock == 'chronometer':
global ch, cm, cs, cc, cronometro_activo
if cronometro_activo:
cc += 1
if cc >= 100: # Centisegundos
cc = 0
cs += 1
if cs >= 60:
cs = 0
cm += 1
if cm >= 60:
cm = 0
ch += 1
class SensorReadTask(Task):
"""Tarea para leer un sensor (DHT22 o HC-SR04)."""
def __init__(self, sensor_type, priority=7, name="SensorReadTask"):
super().__init__(name, priority)
self.sensor_type = sensor_type
def execute(self):
global oled_powered # Necesario para la lógica del HC-SR04
if self.sensor_type == 'dht22':
try:
# Los valores se leerán en el draw_temperature_mode, solo medimos aquí
dht_sensor.measure() # DESCOMENTADO
# print(f"DHT22: Temp={dht_sensor.temperature()}C, Hum={dht_sensor.humidity()}%") # Debug
except OSError as e:
print(f"Error reading DHT22: {e}")
# Lógica del HC-SR04 -- AÚN COMENTADA PARA DIAGNÓSTICO
# elif self.sensor_type == 'hcsr04':
# distance = read_distance_hcsr04()
# # print(f"HC-SR04: Distance={distance}cm") # Debug
# if oled_initialized:
# # La lógica de encender/apagar la OLED es parte de la tarea del sensor
# if 0 < distance <= DISTANCE_THRESHOLD and oled_powered:
# oled.poweroff()
# oled_powered = False
# oled.fill(0) # Limpiar al apagar
# oled.show()
# elif (distance > DISTANCE_THRESHOLD or distance == -1) and not oled_powered:
# oled.poweron()
# oled_powered = True
# oled.fill(0) # Limpiar al reactivar
# oled.show()
class BuzzerTask(Task):
"""Tarea para controlar el buzzer."""
def __init__(self, freq, duty_u16, duration_ms, priority=9, name="BuzzerTask"):
super().__init__(name, priority)
self.freq = freq
self.duty_u16 = duty_u16
self.duration_ms = duration_ms
def execute(self):
if buzzer is not None:
buzzer.duty_u16(self.duty_u16)
buzzer.freq(self.freq)
time.sleep_ms(self.duration_ms)
buzzer.duty_u16(0)
# Reemplazamos play_click_sound para que encolle una BuzzerTask
def enqueue_click_sound():
task_queue.add_task(BuzzerTask(freq=2000, duty_u16=5000, duration_ms=50, name="ClickSound"))
# --- Funciones de Modos (solo dibujan) ---
# MODO: Pantalla de Presentación
# Esta es una función especial que se ejecuta al inicio y bloquea por un tiempo.
# No la convertiremos en una tarea regular para la cola para simplificar la inicialización.
def run_presentation_mode():
global current_mode # Para poder cambiar el modo al finalizar
if not oled_initialized:
print("OLED no inicializada. Saltando modo presentación.")
current_mode = MODE_MENU
return
oled.fill(0)
# Para la presentación, permitiremos un bloqueo temporal para que los sonidos se reproduzcan antes de mostrar el menú
for _ in range(2):
buzzer.freq(500)
buzzer.duty_u16(30000)
time.sleep_ms(200)
buzzer.duty_u16(0)
time.sleep_ms(50)
buzzer.freq(1000)
buzzer.duty_u16(30000)
time.sleep_ms(200)
buzzer.duty_u16(0)
time.sleep_ms(50)
buzzer.duty_u16(0)
agumon_bytes = bytearray([
0xff, 0xc0, 0x03, 0xff, 0xff, 0xc0, 0x03, 0xff,
0xff, 0x3f, 0xfc, 0xff, 0xff, 0x3f, 0xfc, 0xff,
0xc0, 0xff, 0x0f, 0x3f, 0xc0, 0xff, 0x0f, 0x3f,
0x3f, 0xff, 0xc3, 0x3f, 0x3f, 0xff, 0xc3, 0x3f,
0x3f, 0xff, 0x03, 0x3f, 0x3f, 0xff, 0x03, 0x3f,
0x3f, 0xc3, 0xff, 0x3f, 0x3f, 0xc3, 0xff, 0x3f,
0xc0, 0x3f, 0xff, 0x3f, 0xc0, 0x3f, 0xff, 0x3f,
0xcf, 0xff, 0x0c, 0xff, 0xcf, 0xff, 0x0c, 0xff,
0xf0, 0x00, 0xfc, 0xff, 0xf0, 0x00, 0xfc, 0xff,
0xfc, 0x3f, 0x0f, 0x3f, 0xfc, 0x3f, 0x0f, 0x3f,
0xf3, 0x3c, 0xfc, 0x3f, 0xf3, 0x3c, 0xfc, 0x3f,
0xf0, 0x3c, 0x0f, 0xc0, 0xf0, 0x3c, 0x0f, 0xc0,
0xff, 0x0f, 0xcc, 0x0c, 0xff, 0x0f, 0xcc, 0x0c,
0xf0, 0xf0, 0x0f, 0xc3, 0xf0, 0xf0, 0x0f, 0xc3,
0xcc, 0xf3, 0x33, 0x33, 0xcc, 0xf3, 0x33, 0x33,
0xc0, 0x03, 0x00, 0x03, 0xc0, 0x03, 0x00, 0x03
])
agumon_fb = framebuf.FrameBuffer(agumon_bytes, 32, 32, framebuf.MONO_HLSB)
oled.blit(agumon_fb, (WIDTH // 2) - 16, 0)
oled.text("Edwin Luis", 20, 40)
oled.text("Garavito Omoya", 5, 50)
oled.show()
time.sleep(3)
oled.fill(0)
oled.show()
current_mode = MODE_MENU
# --- MODO: Menú Principal con Iconos ---
# ... (iconos de la Parte 1 - se mantienen igual) ...
icon_clock_bytes = bytearray([0xff, 0xff, 0xff, 0xff, 0x85, 0xb7, 0xbd, 0xb7, 0xbd, 0xb7, 0xbd, 0xb4, 0x85, 0xb4, 0xf5, 0xb7, 0xf5, 0xb7, 0xf5, 0xb4, 0xf5, 0xb4, 0xf5, 0xb7, 0x85, 0xb7, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])
icon_clock_fb = framebuf.FrameBuffer(icon_clock_bytes, 16, 16, framebuf.MONO_HLSB)
icon_chrono_bytes = bytearray([0xff, 0xff, 0x7f, 0xfc, 0xff, 0xfe, 0x1f, 0xf0, 0xef, 0xef, 0xf7, 0xdf, 0xfb, 0xbf, 0x1b, 0xb1, 0x5b, 0xb5, 0x5b, 0xb5, 0x1b, 0xb1, 0xfb, 0xbf, 0xf7, 0xdf, 0xef, 0xef, 0x1f, 0xf0, 0xff, 0xff])
icon_chrono_fb = framebuf.FrameBuffer(icon_chrono_bytes, 16, 16, framebuf.MONO_HLSB)
icon_analog_bytes = bytearray([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf7, 0xff, 0xfb, 0xff, 0xfd, 0x7f, 0xfe, 0x7f, 0xfe, 0xbf, 0xff, 0xdf, 0xff, 0xef, 0xff, 0xf7, 0xff, 0xfb, 0xff, 0xfd, 0xff, 0xff, 0xff])
icon_analog_fb = framebuf.FrameBuffer(icon_analog_bytes, 16, 16, framebuf.MONO_HLSB)
icon_temp_bytes = bytearray([0xff, 0xff, 0xfe, 0x7f, 0xfd, 0xbf, 0xfd, 0x3f, 0xfd, 0xbf, 0xfd, 0x3f, 0xfd, 0xbf, 0xfd, 0x3f, 0xfd, 0xbf, 0xfd, 0x3f, 0xfb, 0xdf, 0xf7, 0xef, 0xf7, 0xef, 0xf7, 0xef, 0xf8, 0x1f, 0xff, 0xff])
icon_temp_fb = framebuf.FrameBuffer(icon_temp_bytes, 16, 16, framebuf.MONO_HLSB)
icon_calendar_bytes = bytearray([0x12, 0x48, 0x12, 0x48, 0x12, 0x48, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0xf7, 0x0f, 0xf7, 0xef, 0xf7, 0xef, 0xf7, 0x0f, 0xf7, 0x7f, 0xf7, 0x7f, 0xf7, 0x7f, 0xf7, 0x0f, 0xff, 0xff, 0xff, 0xff])
icon_calendar_fb = framebuf.FrameBuffer(icon_calendar_bytes, 16, 16, framebuf.MONO_HLSB)
menu_items = [
("Reloj Digital", icon_clock_fb, MODE_CLOCK),
("Cronometro", icon_chrono_fb, MODE_CHRONOMETER),
("Reloj Analogico", icon_analog_fb, MODE_ANALOG_CLOCK),
("Temperatura", icon_temp_fb, MODE_TEMPERATURE),
("Calendario", icon_calendar_fb, MODE_CALENDAR)
]
current_menu_selection = 0
# `draw_menu_mode` para dibujar el menú, sin lógica de botones.
def draw_menu_mode():
global current_menu_selection
if not oled_initialized: return
oled.fill(0)
display_offset = 0
if len(menu_items) > 3:
if current_menu_selection >= 2:
display_offset = current_menu_selection - 2
if display_offset > len(menu_items) - 3:
display_offset = len(menu_items) - 3
if display_offset < 0:
display_offset = 0
for i in range(display_offset, min(len(menu_items), display_offset + 3)):
menu_text, menu_icon_fb, _ = menu_items[i]
icon_x = 0
text_x = 20
y_pos = 5 + (i - display_offset) * 20
if i == current_menu_selection:
oled.fill_rect(icon_x, y_pos, WIDTH, 18, 1)
oled.blit(menu_icon_fb, icon_x + 2, y_pos + 1, 0)
oled.text(menu_text, text_x, y_pos + 5, 0)
else:
oled.blit(menu_icon_fb, icon_x + 2, y_pos + 1, 1)
oled.text(menu_text, text_x, y_pos + 5, 1)
oled.show()
# --- MODO: Reloj Digital (solo dibujo) ---
def draw_clock_mode():
global global_h, global_m, global_s, time_setting_part
global current_setting_h, current_setting_m, current_setting_s, clock_setting_active
if not oled_initialized: return
oled.fill(0)
display_global_clock()
if not clock_setting_active: # Modo de visualización
oled.text("Reloj Digital", 0, 10)
full_time_display = "{:02}:{:02}:{:02}".format(global_h, global_m, global_s)
oled.text(full_time_display, 20, 30)
oled.text("ENTER: Ajustar", 0, 50)
oled.text("BACK: Menu", 0, 40)
else:
oled.text("Ajuste de Hora", 0, 10)
hora_display = "{:02}:{:02}:{:02}".format(current_setting_h, current_setting_m, current_setting_s)
oled.text(hora_display, 20, 30)
if time_setting_part == 0:
oled.rect(20-1, 30+8+1, 2*8+2, 2, 1)
elif time_setting_part == 1:
oled.rect(20+3*8-1, 30+8+1, 2*8+2, 2, 1)
elif time_setting_part == 2:
oled.rect(20+6*8-1, 30+8+1, 2*8+2, 2, 1)
oled.text("UP/DOWN: Ajustar", 0, 40)
oled.text("RIGHT: Siguiente", 0, 50)
oled.show()
# --- MODO: Cronómetro (solo dibujo) ---
def draw_chronometer_mode():
global ch, cm, cs, cc, cronometro_activo
if not oled_initialized: return
oled.fill(0)
display_global_clock()
oled.text("Cronometro", 0, 10)
t = "{:02}:{:02}:{:02}.{:02}".format(ch, cm, cs, cc)
oled.text(t[0:5], 10, 30)
oled.text(t[6:], 10, 45)
oled.text("{}".format("ON" if cronometro_activo else "PAUSA"), 85, 20)
oled.text("LEFT: Reset", 0, 56)
oled.show()
# --- MODO: Reloj Analógico (solo dibujo) ---
def draw_analog_clock_mode():
global global_h, global_m, global_s
if not oled_initialized: return
oled.fill(0)
cx, cy, r = 64, 32, 28
for a in range(0, 360, 30):
x = int(cx + math.cos(math.radians(a)) * r)
y = int(cy + math.sin(math.radians(a)) * r)
oled.pixel(x, y, 1)
ang_s = (global_s / 60) * 360 - 90
ang_m = (global_m / 60) * 360 - 90
ang_h = ((global_h % 12 + global_m / 60) / 12) * 360 - 90
oled.line(cx, cy, int(cx + math.cos(math.radians(ang_s)) * r), int(cy + math.sin(math.radians(ang_s)) * r), 1)
oled.line(cx, cy, int(cx + math.cos(math.radians(ang_m)) * (r - 6)), int(cy + math.sin(math.radians(ang_m)) * (r - 6)), 1)
oled.line(cx, cy, int(cx + math.cos(math.radians(ang_h)) * (r - 12)), int(cy + math.sin(math.radians(ang_h)) * (r - 12)), 1)
oled.text("Analogico", 30, 0)
oled.show()
# --- MODO: Temperatura (solo dibujo) ---
def draw_temperature_mode():
global current_mode
if not oled_initialized: return
oled.fill(0)
display_global_clock()
oled.text("Modo: Temperatura", 0, 10)
try:
# Los valores ya deberían estar actualizados por SensorReadTask('dht22')
temperatura = dht_sensor.temperature()
humedad = dht_sensor.humidity()
oled.text("Temp: {:.1f}C".format(temperatura), 0, 30)
oled.text("Hum: {:.1f}%".format(humedad), 0, 45)
except OSError as e:
oled.text("Error DHT22", 0, 30)
oled.text(str(e)[:15], 0, 45) # Muestra el error si no se pudo leer
oled.show()
# --- MODO: Calendario (solo dibujo y funciones auxiliares) ---
def get_days_in_month(month, year):
if month == 2: # Febrero
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0): # Año bisiesto
return 29
else:
return 28
elif month in [4, 6, 9, 11]: # Meses con 30 días
return 30
else: # Meses con 31 días
return 31
def draw_calendar_mode():
global global_day, global_month, global_year
global current_setting_day, current_setting_month, current_setting_year
global date_setting_part, calendar_setting_active
if not oled_initialized: return
oled.fill(0)
display_global_clock()
if not calendar_setting_active:
oled.text("Calendario", 0, 10)
full_date_display = "{:02}/{:02}/{}".format(global_day, global_month, global_year)
oled.text(full_date_display, 20, 30)
oled.text("ENTER: Ajustar", 0, 50)
oled.text("BACK: Menu", 0, 40)
else:
oled.text("Ajuste de Fecha", 0, 10)
date_display = "{:02}/{:02}/{}".format(current_setting_day, current_setting_month, current_setting_year)
oled.text(date_display, 20, 30)
if date_setting_part == 0: # Día
oled.rect(20-1, 30+8+1, 2*8+2, 2, 1)
elif date_setting_part == 1: # Mes
oled.rect(20+3*8-1, 30+8+1, 2*8+2, 2, 1)
elif date_setting_part == 2: # Año
oled.rect(20+6*8-1, 30+8+1, 4*8+2, 2, 1)
oled.text("UP/DOWN: Ajustar", 0, 40)
oled.text("RIGHT: Siguiente", 0, 50)
oled.show()
# --- Funciones de Manejo de Lógica (llamadas por ButtonPressTask o el Scheduler) ---
def handle_button_menu_mode(button_type):
global current_mode, current_menu_selection
if button_type == 'up':
current_menu_selection = (current_menu_selection - 1 + len(menu_items)) % len(menu_items)
elif button_type == 'down':
current_menu_selection = (current_menu_selection + 1) % len(menu_items)
elif button_type == 'enter':
_ , _ , target_mode = menu_items[current_menu_selection]
current_mode = target_mode
def handle_button_clock_mode(button_type):
global current_mode, clock_setting_active, time_setting_part
global global_h, global_m, global_s, current_setting_h, current_setting_m, current_setting_s
if not clock_setting_active: # Modo de visualización
if button_type == 'enter':
clock_setting_active = True
current_setting_h = global_h
current_setting_m = global_m
current_setting_s = global_s
time_setting_part = 0
elif button_type == 'back':
current_mode = MODE_MENU
else: # Modo de configuración
if button_type == 'up':
if time_setting_part == 0:
current_setting_h = (current_setting_h + 1) % 24
elif time_setting_part == 1:
current_setting_m = (current_setting_m + 1) % 60
elif time_setting_part == 2:
current_setting_s = (current_setting_s + 1) % 60
elif button_type == 'down':
if time_setting_part == 0:
current_setting_h = (current_setting_h - 1 + 24) % 24
elif time_setting_part == 1:
current_setting_m = (current_setting_m - 1 + 60) % 60
elif time_setting_part == 2:
current_setting_s = (current_setting_s - 1 + 60) % 60
elif button_type == 'right':
time_setting_part = (time_setting_part + 1) % 3
elif button_type == 'enter':
global_h = current_setting_h
global_m = current_setting_m
global_s = current_setting_s
clock_setting_active = False
elif button_type == 'back':
clock_setting_active = False
def handle_button_chronometer_mode(button_type):
global cronometro_activo, ch, cm, cs, cc, current_mode
if button_type == 'enter':
cronometro_activo = not cronometro_activo
elif button_type == 'left':
ch = cm = cs = cc = 0
cronometro_activo = False
elif button_type == 'back':
current_mode = MODE_MENU
def handle_button_analog_clock_mode(button_type):
global current_mode
if button_type == 'back':
current_mode = MODE_MENU
def handle_button_temperature_mode(button_type):
global current_mode
if button_type == 'back':
current_mode = MODE_MENU
def handle_button_calendar_mode(button_type):
global current_mode, calendar_setting_active, date_setting_part
global global_day, global_month, global_year
global current_setting_day, current_setting_month, current_setting_year
if not calendar_setting_active:
if button_type == 'enter':
calendar_setting_active = True
current_setting_day = global_day
current_setting_month = global_month
current_setting_year = global_year
date_setting_part = 0
elif button_type == 'back':
current_mode = MODE_MENU
else:
if button_type == 'up':
if date_setting_part == 0:
max_days = get_days_in_month(current_setting_month, current_setting_year)
current_setting_day = (current_setting_day % max_days) + 1
elif date_setting_part == 1:
current_setting_month = (current_setting_month % 12) + 1
max_days = get_days_in_month(current_setting_month, current_setting_year)
if current_setting_day > max_days:
current_setting_day = max_days
elif date_setting_part == 2:
current_setting_year = (current_setting_year % 2100) + 1 if current_setting_year < 2099 else 2000
if current_setting_month == 2:
max_days = get_days_in_month(current_setting_month, current_setting_year)
if current_setting_day > max_days:
current_setting_day = max_days
elif button_type == 'down':
if date_setting_part == 0:
max_days = get_days_in_month(current_setting_month, current_setting_year)
current_setting_day = (current_setting_day - 2 + max_days) % max_days + 1
elif date_setting_part == 1:
current_setting_month = (current_setting_month - 2 + 12) % 12 + 1
max_days = get_days_in_month(current_setting_month, current_setting_year)
if current_setting_day > max_days:
current_setting_day = max_days
elif date_setting_part == 2:
current_setting_year = (current_setting_year - 2001 + 100) % 100 + 2000 if current_setting_year > 2000 else 2099
if current_setting_month == 2:
max_days = get_days_in_month(current_setting_month, current_setting_year)
if current_setting_day > max_days:
current_setting_day = max_days
elif button_type == 'right':
date_setting_part = (date_setting_part + 1) % 3
elif button_type == 'enter':
global_day = current_setting_day
global_month = current_setting_month
global_year = current_setting_year
calendar_setting_active = False
elif button_type == 'back':
calendar_setting_active = False
# --- Sensor HC-SR04 -- AÚN COMENTADO PARA DIAGNÓSTICO
# def read_distance_hcsr04():
# """Mide distancia en cm con HC-SR04"""
# trigger_pin.low()
# time.sleep_us(2)
# trigger_pin.high()
# time.sleep_us(10)
# trigger_pin.low()
# # Timeout para ~5m max (500cm / (0.0343 cm/us * 2)) = ~29154 us
# pulse_time = time_pulse_us(echo_pin, 1, 30000)
# distance_cm = (pulse_time * 0.0343) / 2
# return distance_cm if pulse_time > 0 else -1
# --- Mapeo de Funciones de Dibujo y Manejo de Botones ---
mode_draw_functions = {
MODE_MENU: draw_menu_mode,
MODE_CLOCK: draw_clock_mode,
MODE_CHRONOMETER: draw_chronometer_mode,
MODE_ANALOG_CLOCK: draw_analog_clock_mode,
MODE_TEMPERATURE: draw_temperature_mode,
MODE_CALENDAR: draw_calendar_mode
}
button_action_handlers = {
MODE_MENU: handle_button_menu_mode,
MODE_CLOCK: handle_button_clock_mode,
MODE_CHRONOMETER: handle_button_chronometer_mode,
MODE_ANALOG_CLOCK: handle_button_analog_clock_mode,
MODE_TEMPERATURE: handle_button_temperature_mode,
MODE_CALENDAR: handle_button_calendar_mode
}
# --- Implementación de Colas (Queues) ---
class BaseQueue:
def __init__(self):
self._tasks = []
def add_task(self, task):
raise NotImplementedError
def get_task(self):
raise NotImplementedError
def is_empty(self):
return len(self._tasks) == 0
def size(self):
return len(self._tasks)
class FIFOQueue(BaseQueue):
def __init__(self):
super().__init__()
print("FIFOQueue initialized.")
def add_task(self, task):
self._tasks.append(task)
def get_task(self):
if not self.is_empty():
return self._tasks.pop(0)
return None
class LIFOQueue(BaseQueue):
def __init__(self):
super().__init__()
print("LIFOQueue initialized.")
def add_task(self, task):
self._tasks.append(task)
def get_task(self):
if not self.is_empty():
return self._tasks.pop()
return None
class PriorityQueue(BaseQueue):
def __init__(self):
super().__init__()
print("PriorityQueue initialized.")
def add_task(self, task):
# Insertar manteniendo la lista ordenada por prioridad (menor número = mayor prioridad)
# O (más simple para MicroPython): añadir y reordenar al sacar.
self._tasks.append(task)
self._tasks.sort(key=lambda x: x.priority, reverse=True) # Mayor prioridad al final para pop()
def get_task(self):
if not self.is_empty():
return self._tasks.pop() # Sacar el último, que será el de mayor prioridad
return None
# --- Implementación de Schedulers ---
class Scheduler:
def __init__(self, task_queue, name="GenericScheduler"):
self.task_queue = task_queue
self.name = name
self.last_ui_update_time = time.ticks_ms()
self.ui_update_interval_ms = 100 # Frecuencia de actualización de UI (10 FPS)
self.last_sensor_read_time = time.ticks_ms()
self.sensor_read_interval_ms = 1000 # Leer sensores cada 1 segundo
def enqueue_periodic_tasks(self):
# Tarea de actualización de UI
if time.ticks_diff(time.ticks_ms(), self.last_ui_update_time) >= self.ui_update_interval_ms:
self.task_queue.add_task(UITask(current_mode, name=f"DrawMode_{current_mode}"))
self.last_ui_update_time = time.ticks_ms()
# Tarea de lectura de sensores (DHT22 y HC-SR04)
if time.ticks_diff(time.ticks_ms(), self.last_sensor_read_time) >= self.sensor_read_interval_ms:
self.task_queue.add_task(SensorReadTask('dht22', name="ReadDHT22")) # DESCOMENTADO
# self.task_queue.add_task(SensorReadTask('hcsr04', name="ReadHCSR04")) # AÚN COMENTADO
self.last_sensor_read_time = time.ticks_ms()
# Tarea de tick del cronómetro (más frecuente que el sensor)
if current_mode == MODE_CHRONOMETER and cronometro_activo:
# Añadir con una prioridad que permita su ejecución frecuente
self.task_queue.add_task(ClockTickTask('chronometer', priority=12, name="ChronoTick"))
def check_buttons_and_enqueue_tasks(self):
# Mapeo de tipos de botón (string) a sus objetos Pin correspondientes
button_pins_by_type = {
'up': btn_up,
'down': btn_down,
'right': btn_right,
'left': btn_left,
'enter': btn_enter,
'back': btn_back
}
for button_type, pin_obj in button_pins_by_type.items(): # Iteramos sobre clave (string) y valor (Pin)
if check_button_press(pin_obj, button_last_states[button_type]): # Pasamos el objeto Pin y el estado
self.task_queue.add_task(ButtonPressTask(button_type, current_mode, name=f"Btn_{button_type}"))
def run(self):
raise NotImplementedError
class RoundRobinScheduler(Scheduler):
def __init__(self, task_queue):
super().__init__(task_queue, name="RoundRobinScheduler")
print("Scheduler: Round Robin")
def run(self):
# 1. Encolar tareas periódicas
self.enqueue_periodic_tasks()
# 2. Encolar tareas de botones
self.check_buttons_and_enqueue_tasks()
# 3. Procesar una tarea de la cola (Round Robin)
task = self.task_queue.get_task()
if task:
# Control de OLED basado en la tarea
global oled_powered
if task.requires_oled and not oled_powered:
# Si una tarea necesita OLED pero está apagada, la reencendemos
oled.poweron()
oled_powered = True
oled.fill(0)
oled.show()
# Lógica de apagar OLED por sensor HC-SR04 está comentada, así que no afectará
# elif not task.requires_oled and oled_powered and current_mode not in mode_draw_functions:
# pass
# Ejecutar la tarea
if isinstance(task, UITask):
if oled_powered and task.mode in mode_draw_functions:
mode_draw_functions[task.mode]() # Llamar a la función de dibujo
elif isinstance(task, ButtonPressTask):
if task.current_mode in button_action_handlers:
button_action_handlers[task.current_mode](task.button_type)
# Tras un botón, siempre encolamos una tarea de UI para refrescar la pantalla
self.task_queue.add_task(UITask(current_mode, name=f"DrawMode_{current_mode}"))
else:
# Cualquier otra tarea genérica (sensor, buzzer, etc.)
task.execute()
time.sleep_ms(10) # Pequeña pausa para evitar un bucle 100% CPU
class PriorityBasedScheduler(Scheduler):
def __init__(self, task_queue):
if not isinstance(task_queue, PriorityQueue):
raise TypeError("PriorityBasedScheduler requiere una PriorityQueue.")
super().__init__(task_queue, name="PriorityBasedScheduler")
print("Scheduler: Priority Based")
def run(self):
# 1. Encolar tareas periódicas
self.enqueue_periodic_tasks()
# 2. Encolar tareas de botones
self.check_buttons_and_enqueue_tasks()
# 3. Procesar la tarea de mayor prioridad
task = self.task_queue.get_task()
if task:
global oled_powered
if task.requires_oled and not oled_powered:
oled.poweron()
oled_powered = True
oled.fill(0)
oled.show()
if isinstance(task, UITask):
if oled_powered and task.mode in mode_draw_functions:
mode_draw_functions[task.mode]()
elif isinstance(task, ButtonPressTask):
if task.current_mode in button_action_handlers:
button_action_handlers[task.current_mode](task.button_type)
self.task_queue.add_task(UITask(current_mode, name=f"DrawMode_{current_mode}", priority=task.priority + 1)) # Refrescar UI con mayor prioridad
else:
task.execute()
time.sleep_ms(10)
class EventDrivenLIFOScheduler(Scheduler):
def __init__(self, task_queue):
if not isinstance(task_queue, LIFOQueue):
raise TypeError("EventDrivenLIFOScheduler requiere una LIFOQueue.")
super().__init__(task_queue, name="EventDrivenLIFOScheduler")
# Ajustamos los intervalos para que las lecturas de sensores y UI sean menos frecuentes
# a menos que haya un evento. O mantenerlas constantes y que los eventos tengan más peso.
self.ui_update_interval_ms = 200 # UI menos frecuente por default
self.sensor_read_interval_ms = 2000 # Sensores menos frecuentes por default
print("Scheduler: Event Driven (LIFO)")
def run(self):
# En este scheduler, los eventos de usuario (botones) son lo principal.
# Las tareas periódicas se encolan, pero se procesan LIFO, lo que significa
# que las tareas de evento recientes (botones) se procesarán primero.
# 1. Encolar tareas de botones (alta prioridad LIFO)
self.check_buttons_and_enqueue_tasks()
# 2. Encolar tareas periódicas (se añadirán al final, pero LIFO significa que
# si un botón se presiona, esa tarea se procesa antes que la periódica vieja)
self.enqueue_periodic_tasks()
# 3. Procesar la última tarea añadida (LIFO)
task = self.task_queue.get_task()
if task:
global oled_powered
if task.requires_oled and not oled_powered:
oled.poweron()
oled_powered = True
oled.fill(0)
oled.show()
if isinstance(task, UITask):
if oled_powered and task.mode in mode_draw_functions:
mode_draw_functions[task.mode]()
elif isinstance(task, ButtonPressTask):
if task.current_mode in button_action_handlers:
button_action_handlers[task.current_mode](task.button_type)
# Tras un botón, es vital refrescar la UI, así que la encolamos como nueva tarea LIFO
self.task_queue.add_task(UITask(current_mode, name=f"DrawMode_{current_mode}"))
else:
task.execute()
time.sleep_ms(10) # Pequeña pausa
# --- Bucle Principal del Sistema (refactorizado para FQS) ---
def main_loop():
global current_mode, oled_powered
if oled_initialized:
oled.fill(0)
oled.show()
else:
print("OLED no inicializada. Modo sin pantalla")
# Ejecutar el modo de presentación una vez al inicio, ya que bloquea el sistema.
# No es una tarea encolable de forma regular.
run_presentation_mode()
current_mode = MODE_MENU
# --- Configuración de Schedulers y Colas ---
# Opción 1: FIFO Queue + Round Robin Scheduler (DEFAULT)
global task_queue, active_scheduler
task_queue = FIFOQueue()
active_scheduler = RoundRobinScheduler(task_queue)
print("\n--- USANDO FIFO QUEUE + ROUND ROBIN SCHEDULER (DEFAULT) ---")
# Opción 2: Priority Queue + Priority-Based Scheduler
# global task_queue, active_scheduler
# task_queue = PriorityQueue()
# active_scheduler = PriorityBasedScheduler(task_queue)
# print("\n--- USANDO PRIORITY QUEUE + PRIORITY-BASED SCHEDULER ---")
# Opción 3: LIFO Queue + Event-Driven LIFO Scheduler
# global task_queue, active_scheduler
# task_queue = LIFOQueue()
# active_scheduler = EventDrivenLIFOScheduler(task_queue)
# print("\n--- USANDO LIFO QUEUE + EVENT-DRIVEN LIFO SCHEDULER ---")
# Bucle principal del sistema, ahora impulsado por el scheduler
while True:
try:
active_scheduler.run()
except Exception as e:
print(f"ERROR FATAL en main_loop del scheduler: {e}")
if oled_initialized:
try:
oled.poweron()
oled.fill(0)
oled.text("Error! Reinic", 0, 0)
oled.show()
except:
pass
time.sleep(2)
current_mode = MODE_MENU # Intenta volver al menú
# --- Ejecución del Programa ---
if __name__ == "__main__":
main_loop()