# main.py - Código unificado de sistema embebido con arquitectura FQS y Sokoban
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
import dht
print("Iniciando script...")
# --- Constantes y Pines ---
WIDTH, HEIGHT = 128, 64
# 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
JOY_Y_PIN = 27
# Pines para los Botones (4 Direccionales + 1 Seleccionar + 1 Atrás)
BTN_UP_PIN = 10
BTN_DOWN_PIN = 11
BTN_RIGHT_PIN = 12
BTN_LEFT_PIN = 13
BTN_ENTER_PIN = 15
BTN_BACK_PIN = 16
# Pin para el Buzzer
BUZZER_PIN = 14
# Pin para el sensor DHT22
DHT22_PIN = 19
# Pines para HC-SR04 -- Mantenemos COMENTADO para evitar el error anterior
# TRIGGER_PIN = 20
# ECHO_PIN = 21
# DISTANCE_THRESHOLD = 5
# --- Definición de Modos del Sistema ---
MODE_PRESENTATION = -1
MODE_MENU = 0
MODE_CLOCK = 1
MODE_CHRONOMETER = 2
MODE_ANALOG_CLOCK = 3
MODE_TEMPERATURE = 6
MODE_CALENDAR = 7
MODE_SOKOBAN = 8 # Nuevo modo para Sokoban
# --- 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)
buzzer.duty_u16(0)
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))
print("Sensor DHT22 inicializado (o intento).")
# --- Variables Globales del Sistema ---
current_mode = MODE_PRESENTATION
# 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
current_setting_h = 0
current_setting_m = 0
current_setting_s = 0
calendar_setting_active = False
date_setting_part = 0
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()
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_on_oled(): # Renombrado para evitar conflicto si se pasa a Sokoban
if oled_initialized:
oled.text("{:02}:{:02}".format(global_h, global_m), 80, 0)
# --- 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
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):
super().__init__(name, priority, requires_oled=True)
self.mode = mode
def execute(self):
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)
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):
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)
elif self.target_clock == 'chronometer':
global ch, cm, cs, cc, cronometro_activo
if cronometro_activo:
cc += 1
if cc >= 100:
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
if self.sensor_type == 'dht22':
try:
dht_sensor.measure()
except OSError as e:
print(f"Error reading DHT22: {e}")
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)
def enqueue_click_sound():
task_queue.add_task(BuzzerTask(freq=2000, duty_u16=5000, duration_ms=50, name="ClickSound"))
# --- Funciones de Modos (solo dibujan) ---
def run_presentation_mode():
global current_mode
if not oled_initialized:
print("OLED no inicializada. Saltando modo presentación.")
current_mode = MODE_MENU
return
oled.fill(0)
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 ---
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)
# Icono Sokoban (ejemplo simple, puedes crear uno mejor)
icon_sokoban_bytes = bytearray([0x00, 0x00, 0x3C, 0x7E, 0x3C, 0x7E, 0x3C, 0x7E, 0x00, 0x00, 0x3C, 0x7E, 0x3C, 0x7E, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x7E, 0x3C, 0x7E, 0x3C, 0x7E, 0x00, 0x00, 0x3C, 0x7E, 0x3C, 0x7E, 0x00, 0x00])
icon_sokoban_fb = framebuf.FrameBuffer(icon_sokoban_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),
("Sokoban", icon_sokoban_fb, MODE_SOKOBAN) # Añadir Sokoban al menú
]
current_menu_selection = 0
def draw_menu_mode():
global current_menu_selection
if not oled_initialized: return
oled.fill(0)
display_offset = 0
# Ajusta para mostrar hasta 3 elementos visibles en pantalla
if len(menu_items) > 3:
if current_menu_selection >= 2:
display_offset = current_menu_selection - 2
# Asegurarse de no ir más allá del final de la lista
if display_offset > len(menu_items) - 3:
display_offset = len(menu_items) - 3
if display_offset < 0: # Para evitar offsets negativos si la lista es corta
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) # Resaltar el elemento seleccionado
oled.blit(menu_icon_fb, icon_x + 2, y_pos + 1, 0) # Icono en color invertido
oled.text(menu_text, text_x, y_pos + 5, 0) # Texto en color invertido
else:
oled.blit(menu_icon_fb, icon_x + 2, y_pos + 1, 1) # Icono normal
oled.text(menu_text, text_x, y_pos + 5, 1) # Texto normal
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_on_oled()
if not clock_setting_active:
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_on_oled()
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_on_oled()
oled.text("Modo: Temperatura", 0, 10)
try:
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)
oled.show()
# --- MODO: Calendario (solo dibujo y funciones auxiliares) ---
def get_days_in_month(month, year):
if month == 2:
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0):
return 29
else:
return 28
elif month in [4, 6, 9, 11]:
return 30
else:
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_on_oled()
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:
oled.rect(20-1, 30+8+1, 2*8+2, 2, 1)
elif date_setting_part == 1:
oled.rect(20+3*8-1, 30+8+1, 2*8+2, 2, 1)
elif date_setting_part == 2:
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()
# --- MODO: Sokoban (lógica y dibujo) ---
# Caracteres de la representación del mapa
WALL = '#'
EMPTY = ' '
PLAYER = '@'
BOX = '$'
GOAL = '.'
BOX_ON_GOAL = '*'
PLAYER_ON_GOAL = '+'
TILE_SIZE = 8 # Tamaño de cada "tile" en píxeles
class SokobanGame:
def __init__(self, oled_obj, display_global_clock_func, btn_states, check_btn_func, oled_initialized_status, buzzer_obj):
self.oled = oled_obj
self.display_global_clock = display_global_clock_func
self.btn_states = btn_states # Se pasa para la clase, aunque la función global check_button_press ya la usa
self.check_button_press = check_btn_func
self.oled_initialized = oled_initialized_status
self.buzzer = buzzer_obj
self.levels = [] # Lista de matrices de caracteres para cada nivel
self.current_level_idx = 0
self.player_pos = (0, 0) # (row, col)
self.current_map = [] # Matriz actual del juego (lista de listas de caracteres)
self.initial_goals = [] # Posiciones fijas de los objetivos
self.moves = 0
self.pushes = 0
self.game_running = False # Controlado por el scheduler, no por un bucle interno
self.level_completed = False
print("Sokoban: Cargando niveles...")
self.load_levels("levels.csv")
if self.levels:
print(f"Sokoban: {len(self.levels)} niveles cargados. Listo para cargar nivel.")
else:
print("Sokoban: No se encontraron niveles en levels.csv. El juego no podrá iniciar.")
# --- NUEVA FUNCIÓN PARA DIBUJAR CÍRCULOS ---
def _draw_circle(self, x_center, y_center, radius, color):
x = 0
y = radius
p = 1 - radius # Decision parameter
# Dibuja los 8 puntos iniciales simétricos
self.oled.pixel(x_center + x, y_center + y, color)
self.oled.pixel(x_center + x, y_center - y, color)
self.oled.pixel(x_center - x, y_center + y, color)
self.oled.pixel(x_center - x, y_center - y, color)
self.oled.pixel(x_center + y, y_center + x, color)
self.oled.pixel(x_center + y, y_center - x, color)
self.oled.pixel(x_center - y, y_center + x, color)
self.oled.pixel(x_center - y, y_center - x, color)
# Loop para calcular los puntos restantes en un octante y replicarlos
while x < y:
x += 1
if p < 0:
p += 2 * x + 1
else:
y -= 1
p += 2 * (x - y) + 1
self.oled.pixel(x_center + x, y_center + y, color)
self.oled.pixel(x_center + x, y_center - y, color)
self.oled.pixel(x_center - x, y_center + y, color)
self.oled.pixel(x_center - x, y_center - y, color)
self.oled.pixel(x_center + y, y_center + x, color)
self.oled.pixel(x_center + y, y_center - x, color)
self.oled.pixel(x_center - y, y_center + x, color)
self.oled.pixel(x_center - y, y_center - x, color)
def load_levels(self, filename="levels.csv"):
self.levels = []
try:
with open(filename, 'r') as f:
current_level_map = []
for line in f:
stripped_line = line.strip()
if stripped_line: # Si la línea no está vacía
current_level_map.append(list(stripped_line))
else: # Si la línea está vacía, es el final de un nivel
if current_level_map: # Si hay un mapa acumulado
self.levels.append(current_level_map)
current_level_map = [] # Reiniciar para el siguiente nivel
# Añadir el último nivel si el archivo no termina en una línea vacía
if current_level_map:
self.levels.append(current_level_map)
print(f"Sokoban: {len(self.levels)} niveles cargados desde {filename}.")
except OSError as e:
print(f"ERROR: No se pudo cargar levels.csv. Mensaje: {e}")
self.levels = []
except Exception as e:
print(f"ERROR: Formato de levels.csv inválido. Mensaje: {e}")
self.levels = []
def load_level(self, level_idx):
if not self.levels:
print("Sokoban: No hay niveles cargados.")
self.game_running = False
return False
if level_idx >= len(self.levels):
print("Sokoban: Fin de todos los niveles.")
self.oled.fill(0)
self.oled.text("FIN DEL JUEGO!", 0, 20)
self.oled.text("Gracias por jugar!", 0, 35)
self.oled.show()
time.sleep(3)
global current_mode
current_mode = MODE_MENU # Volver al menú principal al terminar todos los niveles
self.game_running = False # Asegurar que el juego se detiene
self.current_level_idx = 0 # Resetear para empezar de nuevo
return False
self.current_level_idx = level_idx # Actualizar índice del nivel
# Copiar el mapa del nivel para poder modificarlo durante el juego
self.current_map = [row[:] for row in self.levels[self.current_level_idx]]
self.moves = 0
self.pushes = 0
self.level_completed = False
self.game_running = True # El juego está activo
self.player_pos = (0, 0)
self.initial_goals = [] # Objetivos fijos del nivel
for r_idx, row in enumerate(self.current_map):
for c_idx, char in enumerate(row):
if char == PLAYER:
self.player_pos = (r_idx, c_idx)
# El jugador sobre un espacio vacío, lo marcamos como EMPTY en el mapa interno
self.current_map[r_idx][c_idx] = EMPTY
elif char == GOAL:
self.initial_goals.append((r_idx, c_idx))
elif char == BOX_ON_GOAL:
self.initial_goals.append((r_idx, c_idx))
self.current_map[r_idx][c_idx] = BOX # Internamente, es una caja que puede moverse
elif char == PLAYER_ON_GOAL:
self.player_pos = (r_idx, c_idx)
self.initial_goals.append((r_idx, c_idx))
self.current_map[r_idx][c_idx] = GOAL # Internamente, es un objetivo
print(f"Sokoban: Nivel {self.current_level_idx+1} cargado. Player: {self.player_pos}, Initial Goals: {len(self.initial_goals)}")
return True
def draw_game(self):
if not self.oled_initialized: return
self.oled.fill(0)
self.display_global_clock() # Muestra el reloj global en la esquina
max_map_width = max(len(row) for row in self.current_map)
map_height_pixels = len(self.current_map) * TILE_SIZE
map_width_pixels = max_map_width * TILE_SIZE
# Centrar el mapa en la pantalla, dejando espacio para el texto arriba
offset_y = 20 # Dejar espacio para Lvl, Mov, Push
offset_x = (WIDTH - map_width_pixels) // 2
if offset_x < 0: offset_x = 0 # Asegurar que no se salga por la izquierda
for r_idx, row in enumerate(self.current_map):
for c_idx, char in enumerate(row):
x = offset_x + c_idx * TILE_SIZE
y = offset_y + r_idx * TILE_SIZE
is_initial_goal = (r_idx, c_idx) in self.initial_goals
# Dibujar el fondo del tile (vacío, pared, objetivo, etc.)
if is_initial_goal:
self._draw_circle(x + TILE_SIZE // 2, y + TILE_SIZE // 2, TILE_SIZE // 3, 1) # Círculo para objetivo
if char == WALL: # Pared
self.oled.fill_rect(x, y, TILE_SIZE, TILE_SIZE, 1)
elif char == BOX: # Caja
self.oled.rect(x+1, y+1, TILE_SIZE-2, TILE_SIZE-2, 1) # Caja (rectángulo con borde)
if is_initial_goal: # Si es una caja sobre objetivo, pintar el centro
self.oled.fill_rect(x+3, y+3, TILE_SIZE-6, TILE_SIZE-6, 1)
# EMPTY y GOAL se dibujan por la ausencia de otros elementos, y el círculo para GOAL
# Dibujar el jugador
player_x = offset_x + self.player_pos[1] * TILE_SIZE
player_y = offset_y + self.player_pos[0] * TILE_SIZE
self.oled.fill_rect(player_x + 2, player_y + 2, TILE_SIZE - 4, TILE_SIZE - 4, 1) # Hombrecito (cuadrado central)
self.oled.text(f"Lvl: {self.current_level_idx + 1}", 0, 0)
self.oled.text(f"Mov: {self.moves}", 0, 8)
self.oled.text(f"Push: {self.pushes}", 0, 16)
if self.level_completed:
self.oled.text("¡GANASTE! Pulsa ENTER", 0, HEIGHT - 8)
elif not self.game_running: # Esto podría ser si no hay niveles o se salió
self.oled.text("Juego Terminado", 0, HEIGHT - 8)
self.oled.show()
def move_player(self, dr, dc):
if self.level_completed or not self.game_running:
return
pr, pc = self.player_pos # Posición actual del jugador
new_pr, new_pc = pr + dr, pc + dc # Próxima posición del jugador
# Validar límites del mapa
if not (0 <= new_pr < len(self.current_map) and 0 <= new_pc < len(self.current_map[0])):
return
target_cell_content = self.current_map[new_pr][new_pc]
# Si el destino es una pared, no se mueve
if target_cell_content == WALL:
return
# Si el destino es una caja
if target_cell_content == BOX:
box_target_r, box_target_c = new_pr + dr, new_pc + dc # Próxima posición de la caja
# Validar límites del mapa para la caja
if not (0 <= box_target_r < len(self.current_map) and 0 <= box_target_c < len(self.current_map[0])):
return
box_next_cell_content = self.current_map[box_target_r][box_target_c]
# Si la caja no se puede mover (choca con pared o con otra caja)
if box_next_cell_content == WALL or box_next_cell_content == BOX:
return
# Mover la caja: la celda destino de la caja se convierte en BOX
self.current_map[box_target_r][box_target_c] = BOX
self.pushes += 1 # Contar el empuje
# Mover el jugador
# La celda actual del jugador (pr, pc) debe volver a su estado original (EMPTY o GOAL)
if (pr, pc) in self.initial_goals:
self.current_map[pr][pc] = GOAL
else:
self.current_map[pr][pc] = EMPTY
self.player_pos = (new_pr, new_pc) # Actualizar la posición del jugador
self.moves += 1 # Contar el movimiento del jugador
enqueue_click_sound() # Mover esto a la función global para usar la cola del scheduler
self.check_win()
def check_win(self):
# Un nivel se gana si todas las cajas están en una posición de objetivo
# y todas las posiciones de objetivo tienen una caja.
# Esto asume que el número de cajas es igual al número de objetivos.
all_boxes_on_goals = True
# Verificar que cada objetivo tenga una caja
for goal_r, goal_c in self.initial_goals:
if self.current_map[goal_r][goal_c] != BOX:
all_boxes_on_goals = False
break
# Verificar que no haya cajas fuera de objetivos (si el número de cajas es mayor que objetivos)
# Esto es implícito si la condición anterior es True y el número de cajas == número de objetivos
if all_boxes_on_goals:
self.level_completed = True
self.game_running = False # Señal para el scheduler de que el juego pausará para el win screen
print(f"Sokoban: Nivel {self.current_level_idx + 1} completado!")
self.play_win_sound()
# self.save_score() # <--- ¡ELIMINADA ESTA LÍNEA!
# El avance de nivel o regreso al menú se manejará en handle_button_sokoban_mode
# cuando el usuario presione ENTER, o automáticamente aquí si el siguiente nivel existe.
# Forzamos la carga del siguiente nivel aquí para un flujo automático
if not self.load_level(self.current_level_idx + 1):
# Si load_level regresa False, significa que no hay más niveles
# y ya se ha manejado el mensaje de "FIN DEL JUEGO" y el cambio de modo a MENU.
pass # No hacer nada más aquí, load_level ya hizo todo.
else:
# Si se cargó un nuevo nivel, el `game_running` ya estará en True por `load_level`
time.sleep_ms(500) # Pequeña pausa antes de dibujar el siguiente nivel
else:
self.level_completed = False
def play_win_sound(self):
if self.buzzer is not None:
self.buzzer.freq(1500)
self.buzzer.duty_u16(5000)
time.sleep_ms(200)
self.buzzer.freq(2000)
time.sleep_ms(200)
self.buzzer.freq(2500)
time.sleep_ms(400)
self.buzzer.duty_u16(0)
# --- MÉTODO save_score ELIMINADO ---
# def save_score(self, filename="scores.csv", player_name="Player1"):
# # ... todo el contenido de este método ha sido eliminado.
# pass # Este pass es solo un marcador, la función entera se borra.
# Este método ya no se usará como un bucle principal, se elimina o refactoriza.
# El scheduler llamará a draw_game y handle_button_sokoban_mode.
# def run(self):
# pass # La lógica del juego será impulsada por el scheduler
# --- 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
# Si entramos al modo Sokoban, inicializar el primer nivel si la instancia existe
if current_mode == MODE_SOKOBAN and sokoban_game_instance:
sokoban_game_instance.load_level(sokoban_game_instance.current_level_idx) # Cargar el nivel actual o el primero
sokoban_game_instance.game_running = True # Asegurarse de que el juego esté activo al entrar
print("Entrando al modo Sokoban.")
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:
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:
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
def handle_button_sokoban_mode(button_type):
global current_mode, sokoban_game_instance
if not sokoban_game_instance:
print("SokobanGame instance not initialized.")
current_mode = MODE_MENU
return
if sokoban_game_instance.level_completed:
if button_type == 'enter':
# Intentar cargar el siguiente nivel. Si no hay más, load_level lo maneja.
if sokoban_game_instance.load_level(sokoban_game_instance.current_level_idx + 1):
sokoban_game_instance.game_running = True # Asegurar que el juego esté activo de nuevo
print(f"Sokoban: Cargando nivel {sokoban_game_instance.current_level_idx + 1}")
else:
# load_level ya habrá puesto current_mode a MENU y game_running a False
pass
elif button_type == 'back':
current_mode = MODE_MENU
sokoban_game_instance.game_running = False
sokoban_game_instance.current_level_idx = 0 # Resetear al salir
return
elif not sokoban_game_instance.game_running: # Si el juego no está corriendo por alguna razón (ej. fin de niveles)
if button_type == 'back':
current_mode = MODE_MENU
sokoban_game_instance.current_level_idx = 0 # Resetear al salir
return
elif button_type == 'enter':
# Si el juego ha terminado (no hay más niveles), ENTER puede reiniciar desde el primer nivel
if not sokoban_game_instance.levels:
print("Sokoban: No hay niveles para reiniciar.")
current_mode = MODE_MENU
return
sokoban_game_instance.load_level(0)
sokoban_game_instance.game_running = True
print("Sokoban: Reiniciando juego desde el nivel 1.")
else: # El juego está en progreso y no se ha ganado
if button_type == 'up':
sokoban_game_instance.move_player(-1, 0)
elif button_type == 'down':
sokoban_game_instance.move_player(1, 0)
elif button_type == 'left':
sokoban_game_instance.move_player(0, -1)
elif button_type == 'right':
sokoban_game_instance.move_player(0, 1)
elif button_type == 'back':
current_mode = MODE_MENU
sokoban_game_instance.game_running = False # Detener el juego al salir
sokoban_game_instance.current_level_idx = 0 # Resetear al salir
elif button_type == 'enter': # Reiniciar nivel actual
sokoban_game_instance.load_level(sokoban_game_instance.current_level_idx)
sokoban_game_instance.game_running = True # Asegurarse de que el juego esté activo de nuevo
# --- 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,
MODE_SOKOBAN: lambda: sokoban_game_instance.draw_game() if sokoban_game_instance else None # Delega al objeto SokobanGame
}
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,
MODE_SOKOBAN: handle_button_sokoban_mode # Añadir manejador de botones para Sokoban
}
# --- 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):
self._tasks.append(task)
# Ordenar por prioridad, el número más bajo es la prioridad más alta
self._tasks.sort(key=lambda x: x.priority, reverse=False)
def get_task(self):
if not self.is_empty():
return self._tasks.pop(0) # pop(0) para la prioridad más alta (número más bajo)
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
self.last_sensor_read_time = time.ticks_ms()
self.sensor_read_interval_ms = 1000
def enqueue_periodic_tasks(self):
# Tarea de actualización de UI
# Prioridad alta para UI ya que es lo que el usuario ve
# Solo encolar UI si el modo actual requiere una actualización continua
# y si no estamos en un estado de "victoria" que bloquea la UI temporalmente
if current_mode == MODE_SOKOBAN and sokoban_game_instance and sokoban_game_instance.level_completed:
# Si se ganó un nivel, la UI se actualizó en check_win y esperamos input
pass
else:
self.task_queue.add_task(UITask(current_mode, name=f"DrawMode_{current_mode}", priority=5))
self.last_ui_update_time = time.ticks_ms()
# Tarea de lectura de sensores (DHT22)
if time.ticks_diff(time.ticks_ms(), self.last_sensor_read_time) >= self.sensor_read_interval_ms:
# Prioridad media para lectura de sensores
self.task_queue.add_task(SensorReadTask('dht22', name="ReadDHT22", priority=7))
self.last_sensor_read_time = time.ticks_ms()
# Tarea de tick del cronómetro
if current_mode == MODE_CHRONOMETER and cronometro_activo:
# Prioridad muy alta para el cronómetro para precisión
self.task_queue.add_task(ClockTickTask('chronometer', priority=2, name="ChronoTick"))
def check_buttons_and_enqueue_tasks(self):
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():
if check_button_press(pin_obj, button_last_states[button_type]):
# Prioridad más alta para las interacciones del usuario
self.task_queue.add_task(ButtonPressTask(button_type, current_mode, name=f"Btn_{button_type}", priority=1))
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):
self.enqueue_periodic_tasks() # Encola tareas periódicas
self.check_buttons_and_enqueue_tasks() # Encola tareas de botones
task = self.task_queue.get_task() # Obtiene la siguiente tarea (FIFO)
if task:
global oled_powered
# Si la tarea requiere OLED y está apagada, encenderla
if task.requires_oled and not oled_powered:
oled.poweron()
oled_powered = True
oled.fill(0)
oled.show()
# Ejecutar la lógica de la tarea
if isinstance(task, UITask):
if oled_powered and task.mode in mode_draw_functions:
# Solo dibuja la UI del modo actual si el juego Sokoban no está en estado de victoria pendiente de input
if not (task.mode == MODE_SOKOBAN and sokoban_game_instance and sokoban_game_instance.level_completed):
mode_draw_functions[task.mode]() # Dibuja la UI del modo actual
elif isinstance(task, ButtonPressTask):
if task.current_mode in button_action_handlers:
button_action_handlers[task.current_mode](task.button_type) # Maneja la acción del botón
# Después de un botón, generalmente se quiere actualizar la UI
self.task_queue.add_task(UITask(current_mode, name=f"DrawMode_{current_mode}_PostBtn", priority=5))
else:
task.execute() # Ejecuta cualquier otra tarea (sensor, buzzer, etc.)
time.sleep_ms(10) # Pequeño retardo para no saturar la CPU y permitir otros procesos
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):
self.enqueue_periodic_tasks() # Encola tareas periódicas
self.check_buttons_and_enqueue_tasks() # Encola tareas de botones
task = self.task_queue.get_task() # Obtiene la tarea de mayor prioridad
if task:
global oled_powered
# Si la tarea requiere OLED y está apagada, encenderla
if task.requires_oled and not oled_powered:
oled.poweron()
oled_powered = True
oled.fill(0)
oled.show()
# Ejecutar la lógica de la tarea
if isinstance(task, UITask):
if oled_powered and task.mode in mode_draw_functions:
# Solo dibuja la UI del modo actual si el juego Sokoban no está en estado de victoria pendiente de input
if not (task.mode == MODE_SOKOBAN and sokoban_game_instance and sokoban_game_instance.level_completed):
mode_draw_functions[task.mode]() # Dibuja la UI del modo actual
elif isinstance(task, ButtonPressTask):
if task.current_mode in button_action_handlers:
button_action_handlers[task.current_mode](task.button_type) # Maneja la acción del botón
# Después de un botón, encola una tarea de UI con mayor prioridad
self.task_queue.add_task(UITask(current_mode, name=f"DrawMode_{current_mode}_PostBtn", priority=task.priority + 1))
else:
task.execute() # Ejecuta cualquier otra tarea
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")
self.ui_update_interval_ms = 200 # Podemos hacer la UI menos frecuente para LIFO
self.sensor_read_interval_ms = 2000 # Sensores también menos frecuentes
print("Scheduler: Event Driven (LIFO)")
def run(self):
self.check_buttons_and_enqueue_tasks() # Las pulsaciones de botones deben encolarse primero
self.enqueue_periodic_tasks() # Luego las periódicas, para que las de botón se ejecuten antes
task = self.task_queue.get_task() # Obtiene la tarea más reciente (LIFO)
if task:
global oled_powered
# Si la tarea requiere OLED y está apagada, encenderla
if task.requires_oled and not oled_powered:
oled.poweron()
oled_powered = True
oled.fill(0)
oled.show()
# Ejecutar la lógica de la tarea
if isinstance(task, UITask):
if oled_powered and task.mode in mode_draw_functions:
# Solo dibuja la UI del modo actual si el juego Sokoban no está en estado de victoria pendiente de input
if not (task.mode == MODE_SOKOBAN and sokoban_game_instance and sokoban_game_instance.level_completed):
mode_draw_functions[task.mode]() # Dibuja la UI del modo actual
elif isinstance(task, ButtonPressTask):
if task.current_mode in button_action_handlers:
button_action_handlers[task.current_mode](task.button_type) # Maneja la acción del botón
# Después de un botón, encola una tarea de UI para que sea lo siguiente en dibujarse
self.task_queue.add_task(UITask(current_mode, name=f"DrawMode_{current_mode}_PostBtn", priority=5))
else:
task.execute() # Ejecuta cualquier otra tarea
time.sleep_ms(10)
# --- Bucle Principal del Sistema (refactorizado para FQS) ---
def main_loop():
global current_mode, oled_powered, sokoban_game_instance
if oled_initialized:
oled.fill(0)
oled.show()
else:
print("OLED no inicializada. Modo sin pantalla")
run_presentation_mode()
current_mode = MODE_MENU
# Inicializar la instancia del juego Sokoban DESPUÉS de que OLED y otros estén listos
sokoban_game_instance = SokobanGame(oled, display_global_clock_on_oled, button_last_states, check_button_press, oled_initialized, buzzer)
# --- Configuración de Schedulers y Colas ---
# Opción 1: FIFO Queue + Round Robin Scheduler (DEFAULT)
# Descomenta la siguiente sección si quieres usar Round Robin
# 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
# Comenta la Opción 1 y la Opción 3, y DESCOMENTA esta sección para usarla
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
# Comenta la Opción 1 y la Opción 2, y DESCOMENTA esta sección para usarla
# 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
# --- Ejecución del Programa ---
if __name__ == "__main__":
main_loop()
# En nuestro sistema embebido, hemos implementado y configurado tres combinaciones
# diferentes de colas (Queues) y planificadores (Schedulers) para la gestión de tareas.
# Cada combinación ofrece un enfoque distinto para decidir qué tarea se ejecuta y cuándo,
# impactando directamente la 'sensación' y el rendimiento del sistema.
#
# Un sistema embebido como el nuestro (basado en MicroPython en una Pico W) es
# mono-hilo, lo que significa que solo puede ejecutar una cosa a la vez. Las colas y
# los schedulers nos ayudan a simular concurrencia y a gestionar múltiples tareas
# de manera eficiente, decidiendo el orden en que estas tareas "compiten" por el tiempo
# de procesamiento de la CPU.
#
# -----------------------------------------------------------------------------------
#
# 1. FIFO Queue + Round Robin Scheduler (Fairness y Simpleza)
#
# - FIFO (First-In, First-Out) Queue: Como una fila en un banco o un tubo.
# La primera tarea que se añade a la cola es la primera en ser procesada.
# Es el método más directo y predecible para la gestión de datos.
#
# - Round Robin Scheduler: Este planificador toma la tarea más antigua de la cola,
# la ejecuta (en nuestro caso, la ejecuta por completo), y luego pasa a la siguiente.
# Todas las tareas reciben una oportunidad equitativa de ejecución en un ciclo
# repetitivo. No hay prioridad; el orden es estrictamente por llegada.
#
# Ventajas:
# - Simpleza y Facilidad de Implementación: Es muy fácil de entender y codificar.
# - Equidad (Fairness): Todas las tareas, sin importar su importancia,
# eventualmente se ejecutarán. Ninguna tarea se queda sin ser procesada
# indefinidamente ('starvation').
# - Predecibilidad: El orden de ejecución es claro y secuencial.
#
# Desventajas:
# - Baja Latencia de Respuesta a Eventos Críticos: Si una pulsación de botón
# (una tarea que requiere respuesta inmediata) se encola detrás de muchas
# tareas de baja prioridad, el usuario podría percibir un retraso.
# - Ineficiencia para Tareas de Alta Prioridad: Una tarea urgente
# (como una actualización crítica de la pantalla o un tick de cronómetro)
# debe esperar su turno como cualquier otra.
#
# -----------------------------------------------------------------------------------
#
# 2. Priority Queue + Priority-Based Scheduler (Responsividad y Priorización)
#
# Esta combinación introduce el concepto de importancia relativa de las tareas.
#
# - Priority Queue: Las tareas se almacenan y se recuperan según su nivel de prioridad.
# En nuestro código, hemos asignado prioridades a las tareas (ej: pulsaciones de
# botones tienen prioridad alta, actualizaciones de UI prioridad media, etc.).
# Cuando se añade una tarea, la cola se reordena (o la tarea se inserta en el lugar
# correcto) para que las tareas de mayor prioridad (número más bajo) estén siempre
# al frente, listas para ser seleccionadas.
#
# - Priority-Based Scheduler: El planificador siempre selecciona y ejecuta la tarea
# con la **mayor prioridad** (el número de prioridad más bajo) que esté disponible
# en la cola.
#
# Ventajas:
# - Excelente Responsividad para Tareas Críticas: Tareas importantes como
# pulsaciones de botones o ticks de cronómetro se ejecutarán casi de inmediato,
# sin importar cuántas tareas de menor prioridad estén esperando. Esto mejora
# significativamente la experiencia del usuario para interacciones críticas.
# - Uso Eficiente de Recursos: El sistema puede concentrar sus esfuerzos en lo más
# importante en un momento dado.
#
# Desventajas:
# - Riesgo de "Hambruna" (Starvation): Si hay un flujo constante de tareas de
# alta prioridad, las tareas de baja prioridad podrían nunca ejecutarse,
# o ejecutarse con muy poca frecuencia. Aunque en nuestro diseño se mitiga un poco.
# - Mayor Complejidad: Requiere una asignación cuidadosa y un entendimiento
# claro de las prioridades de cada tipo de tarea.
#
# -----------------------------------------------------------------------------------
#
# 3. LIFO Queue + Event-Driven LIFO Scheduler (Inmediatez del Evento Reciente)
#
# Esta combinación se enfoca en la inmediatez de los eventos más recientes.
#
# - LIFO (Last-In, First-Out) Queue: Como una pila de platos. El último elemento
# que se añade a la pila es el primero en ser retirado. Las tareas más
# recientemente encoladas son las primeras en ser ejecutadas.
#
# - Event-Driven LIFO Scheduler: Este planificador es ideal para sistemas donde
# la "frescura" del evento es clave. Las tareas generadas por los eventos más
# recientes (como las interacciones del usuario) se priorizan sobre las tareas
# antiguas o periódicas. El sistema se siente muy reactivo a las últimas acciones.
#
# Ventajas:
# - Buena Respuesta a Eventos Recientes: Si el usuario presiona un botón
# varias veces seguidas, la última pulsación (y su efecto en la UI) se procesará
# muy rápidamente, lo que puede dar una sensación de gran inmediatez.
# - Sentido de Inmediatez: La interfaz de usuario reacciona rápidamente a la
# interacción más reciente del usuario.
#
# Desventajas:
# - Menos Predecibilidad para Tareas Antiguas/Periódicas: Si hay muchos eventos
# o tareas nuevas que se añaden constantemente, las tareas periódicas (como la
# actualización constante de la hora o la lectura de sensores) podrían retrasarse
# o "saltarse" ciclos, ya que las tareas más recientes tienen preferencia.
# - Posible "Hambruna" de Tareas Antiguas: Similar al scheduler de prioridad,
# pero en este caso por ser desplazado por nuevas entradas en la pila.
#
# -----------------------------------------------------------------------------------
#
# En resumen, al experimentar con estas tres configuraciones, puedes observar y comprender
# de primera mano cómo las decisiones de diseño en la gestión de tareas impactan
# directamente la **experiencia de usuario** y el **rendimiento general** de tu sistema
# embebido. Cada una tiene su lugar y es adecuada para diferentes tipos de aplicaciones.
#
# # Para cambiar entre los schedulers:
# # 1. Ve a la función 'main_loop()' al final del archivo.
# # 2. Comenta los dos bloques de código de 'global task_queue, active_scheduler' que NO quieres usar.
# # 3. Descomenta el bloque que SÍ quieres usar.
# # 4. Guarda y reinicia tu dispositivo o simulación.
#