from machine import Pin, I2C, PWM, Timer
import time
import math
import framebuf
import csv
from dht import DHT11 # Make sure you have this library (dht.py) on your Pico if you use DHT22
# (Your existing SSD1306_I2C class should be here, not included for brevity)
# If you don't have it, you'll need to include the ssd1306.py library or its contents.
class SSD1306_I2C(framebuf.FrameBuffer):
def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):
self.width = width
self.height = height
self.i2c = i2c
self.addr = addr
self.external_vcc = external_vcc
self.buffer = bytearray(self.height * self.width // 8)
super().__init__(self.buffer, self.width, self.height, framebuf.MONO_VLSB)
self.init_display()
def init_display(self):
for cmd in (
0xAE, # Display off
0x20, 0x00, # Set Memory Addressing Mode
0x21, 0x00, 0x7F, # Column Address Start/End
0x22, 0x00, 0x3F, # Page Address Start/End
0xDC, 0x00, # Display Start Line
0xAE, # Set display start line to 0
0x81, 0xcf, # Set contrast control
0xa1, # Set segment re-map 0 to 127
0xa6, # Normal display
0xa8, 0x3f, # Set multiplex ratio(1 to 64)
0xa4, # Output scan direction
0xd3, 0x00, # Set display offset
0xd5, 0x80, # Set display clock divide ratio/oscillator frequency
0xd9, 0xf1, # Set pre-charge period
0xda, 0x12, # Set COM pins hardware configuration
0xdb, 0x40, # Set VCOMH deselect level
0x8d, 0x14, # Charge pump setting
0xaf, # Display ON
):
self.write_cmd(cmd)
self.clear()
self.show()
def poweroff(self):
self.write_cmd(0xAE)
def poweron(self):
self.write_cmd(0xAF)
def write_cmd(self, cmd):
self.i2c.writeto(self.addr, bytearray([0x80, cmd]))
def write_data(self, buf):
self.i2c.writeto(self.addr, bytearray([0x40]) + buf)
def clear(self):
self.fill(0)
self.show()
def show(self):
self.i2c.writeto(self.addr, bytearray([0x40]) + self.buffer)
# --- Constantes Globales ---
WIDTH = 128
HEIGHT = 64
SCL_PIN = 5 # GPIO5 para SCL de la OLED
SDA_PIN = 4 # GPIO4 para SDA de la OLED
BUZZER_PIN = 14 # GPIO14 para el buzzer
# Pins para el sensor DHT11/DHT22 (ej. DHT22 en GP21)
DHT_PIN = 21
dht_sensor = DHT11(Pin(DHT_PIN)) # Cambia a DHT22 si usas ese sensor
# --- Modos del Sistema ---
MODE_PRESENTATION = 0
MODE_MENU = 1
MODE_CLOCK = 2
MODE_CHRONOMETER = 3
MODE_ANALOG_CLOCK = 4
MODE_TEMPERATURE = 5
MODE_CALENDAR = 6
MODE_SOKOBAN = 7
# --- Variables Globales de Estado ---
global_current_mode = MODE_PRESENTATION # Inicia en modo presentación
oled_initialized = False
oled_powered = True # Asumimos que la OLED estará encendida si se inicializa
# Variables para el reloj digital global
global_h = 10
global_m = 30
global_s = 0
global_day = 1
global_month = 1
global_year = 2024
# Variables para el cronómetro
ch = 0 # Cronómetro horas
cm = 0 # Cronómetro minutos
cs = 0 # Cronómetro segundos
cc = 0 # Cronómetro centésimas
cronometro_activo = False
# Variables para la configuración del reloj
clock_setting_active = False
current_setting_h = 0
current_setting_m = 0
current_setting_s = 0
time_setting_part = 0 # 0=hora, 1=minuto, 2=segundo
# Variables para la configuración del calendario
calendar_setting_active = False
current_setting_day = 1
current_setting_month = 1
current_setting_year = 2024
date_setting_part = 0 # 0=dia, 1=mes, 2=año
# --- Inicialización de Hardware ---
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.")
# Asegurar que la OLED esté encendida y limpia al inicio
oled.poweron()
oled.fill(0)
oled.show()
except Exception as e:
print(f"ERROR: Fallo al inicializar OLED en pines SCL={SCL_PIN}, SDA={SDA_PIN}. Mensaje: {e}")
oled_initialized = False # Asegurarse de que el flag sea False si hay error
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 = 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)
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.")
button_last_states = {
'up': [True],
'down': [True],
'right': [True],
'left': [True],
'enter': [True],
'back': [True]
}
# --- Funciones Auxiliares del Sistema ---
def play_click_sound():
if buzzer is not None:
buzzer.duty_u16(5000)
buzzer.freq(2000)
time.sleep_ms(50)
buzzer.duty_u16(0)
def check_button_press(button_pin, last_state_var_list):
current_state = button_pin.value()
# Si el botón está presionado (LOW) y estaba suelto (HIGH en el last_state)
if not current_state and last_state_var_list[0]:
last_state_var_list[0] = False # Actualiza el estado a presionado
play_click_sound()
return True
# Si el botón está suelto (HIGH) y estaba presionado (LOW en el el last_state)
elif current_state and not last_state_var_list[0]:
last_state_var_list[0] = True # Actualiza el estado a suelto
return False
def display_global_clock():
if oled_initialized and oled_powered:
oled.text("{:02}:{:02}".format(global_h, global_m), 80, 0)
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
# --- MODO: Pantalla de Presentación ---
def run_presentation_mode_once():
global global_current_mode
if not oled_initialized or not oled_powered:
print("OLED no inicializada o no encendida. Saltando modo presentación.")
global_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()
global_current_mode = MODE_MENU # Pasar al menú después de la presentación
# --- MODO: Sokoban ---
# 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):
# Referencias a objetos globales
self.oled = oled
self.display_global_clock = display_global_clock
self.btn_states = button_last_states
self.check_button_press = check_button_press
self.oled_initialized = oled_initialized # Se mantiene como un flag
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 # Controlará si el juego está activo o en menú
self.level_completed = False
print("Sokoban: Cargando niveles...")
self.load_levels("levels.csv")
if self.levels:
print(f"Sokoban: {len(self.levels)} niveles cargados. Cargando nivel 1.")
self.load_level(self.current_level_idx)
else:
print("Sokoban: No se encontraron niveles en levels.csv. El juego no podrá iniciar.")
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 or level_idx >= len(self.levels):
print("Sokoban: Nivel {} no disponible o fin de niveles.".format(level_idx))
self.game_running = False # No hay más niveles, el juego termina
return False
# Copiar el mapa del nivel para poder modificarlo durante el juego
self.current_map = [row[:] for row in self.levels[level_idx]]
self.moves = 0
self.pushes = 0
self.level_completed = False
self.game_running = True # Se marca como activo si se carga un nivel
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)
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
# Después de obtener todos los objetivos, si el jugador inició sobre un objetivo,
# su posición en el mapa "original" debe ser el objetivo
# Y si no inició en un objetivo, asegurar que su celda es EMPTY
# NOTA: En Sokoban, PLAYER o PLAYER_ON_GOAL son solo el estado inicial.
# La celda subyacente (EMPTY o GOAL) es lo que debe permanecer en el mapa.
# El jugador es un 'sprite' que se dibuja encima.
if self.current_map[self.player_pos[0]][self.player_pos[1]] == PLAYER:
self.current_map[self.player_pos[0]][self.player_pos[1]] = EMPTY
elif self.current_map[self.player_pos[0]][self.player_pos[1]] == PLAYER_ON_GOAL:
self.current_map[self.player_pos[0]][self.player_pos[1]] = GOAL
print(f"Sokoban: Nivel {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()
# Calcular dimensiones del mapa actual para centrado
# Esto asume que todas las filas tienen la misma longitud para max_map_width
# Si no, se necesita un manejo de mapas irregulares
if not self.current_map: # Manejar el caso de un mapa vacío
self.oled.text("No map data.", 0, 20)
self.oled.show()
return
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 superior
offset_y = (HEIGHT - map_height_pixels) // 2
offset_x = (WIDTH - map_width_pixels) // 2
# Asegurar que el mapa no se dibuje sobre la información del juego
if offset_y < 24: # Espacio para "Lvl:", "Mov:", "Push:", y un pequeño margen
offset_y = 24
if offset_x < 0: # Evitar que el mapa se salga por la izquierda
offset_x = 0
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
# Determinar si la celda actual es un objetivo inicial
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: # Si es un objetivo, dibujar el objetivo primero
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)
# Dibujar el jugador si está en esta posición
if (r_idx, c_idx) == self.player_pos:
self.oled.fill_rect(x+2, y+2, TILE_SIZE-4, TILE_SIZE-4, 1) # Hombrecito (cuadrado central)
self.oled.text("Lvl: {}".format(self.current_level_idx + 1), 0, 0)
self.oled.text("Mov: {}".format(self.moves), 0, 8)
self.oled.text("Push: {}".format(self.pushes), 0, 16)
self.oled.show()
def move_player(self, dr, dc):
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
self.current_map[box_target_r][box_target_c] = BOX
self.pushes += 1 # Contar el empuje
# Mover el jugador
self.player_pos = (new_pr, new_pc) # Actualizar la posición del jugador
self.moves += 1 # Contar el movimiento del jugador
play_click_sound()
self.check_win()
def check_win(self):
all_boxes_on_goals = True
for goal_r, goal_c in self.initial_goals:
# Si un objetivo no tiene una caja encima
if self.current_map[goal_r][goal_c] != BOX:
all_boxes_on_goals = False
break
if all_boxes_on_goals:
self.level_completed = True
self.game_running = False # Detener el bucle del juego para mostrar mensaje de victoria
if not self.oled_initialized: return
self.oled.fill(0)
self.oled.text("Nivel Completado!", 0, 20)
self.oled.text("Movs: {}".format(self.moves), 0, 30)
self.oled.text("Push: {}".format(self.pushes), 0, 40)
self.oled.show()
time.sleep(2) # Mostrar el mensaje por un tiempo
self.save_score() # Guardar el score del nivel actual
self.current_level_idx += 1
if self.current_level_idx < len(self.levels):
if self.load_level(self.current_level_idx): # Intentar cargar el siguiente nivel
time.sleep_ms(500) # Pequeña pausa antes de dibujar el siguiente nivel
else:
print("Sokoban: No se pudo cargar el siguiente nivel.")
self.game_running = False # Asegurar que el juego se detiene
else: # No hay más 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 global_current_mode # <--- Usar la variable global correcta
global_current_mode = MODE_MENU # Volver al menú principal al terminar todos los niveles
def save_score(self, filename="scores.csv", player_name="Player1"):
try:
scores = []
try:
with open(filename, 'r') as f:
reader = csv.reader(f)
header = next(reader, None) # Lee la cabecera si existe
for row in reader:
scores.append(row)
except OSError: # El archivo no existe, no hay problema
pass
new_score = [self.current_level_idx + 1, player_name, self.moves, self.pushes]
scores.append(new_score)
with open(filename, 'w', newline='') as f: # Añadir newline='' para evitar líneas en blanco en Windows
writer = csv.writer(f)
writer.writerow(['Level', 'Player', 'Moves', 'Pushes']) # Escribir siempre la cabecera
writer.writerows(scores)
print(f"Score guardado para Nivel {new_score[0]}: Movs {new_score[2]}, Pushes {new_score[3]}")
except Exception as e:
print(f"ERROR: No se pudo guardar score en {filename}. Mensaje: {e}")
# Este método 'run' se convierte en el "frame" de una iteración del juego
# Es lo que el scheduler llamará repetidamente. NO DEBE TENER UN BUCLE INTERNO.
def run_sokoban_iteration(self):
global global_current_mode
if not self.game_running or not self.oled_initialized:
# Si el juego no está activo (ej. se acaba de completar un nivel o se salió)
# o si la OLED no está lista, volvemos al menú.
global_current_mode = MODE_MENU
return
self.draw_game()
# Procesar entradas de botones
if self.check_button_press(btn_up, self.btn_states['up']):
self.move_player(-1, 0)
elif self.check_button_press(btn_down, self.btn_states['down']):
self.move_player(1, 0)
elif self.check_button_press(btn_left, self.btn_states['left']):
self.move_player(0, -1)
elif self.check_button_press(btn_right, self.btn_states['right']):
self.move_player(0, 1)
elif self.check_button_press(btn_back, self.btn_states['back']):
self.game_running = False # Marca el juego como no activo
global_current_mode = MODE_MENU # Volver al menú inmediatamente
return # Salir para que el scheduler pueda cambiar el modo
# --- Funciones de "Tareas" para el Planificador Round Robin ---
def task_update_global_clock():
# Esta función ya no hace nada, el reloj se actualiza por su propio Timer ISR
pass
# --- Creación de la instancia de SokobanGame (después de inicializar OLED y botones) ---
sokoban_game = SokobanGame()
# --- MODO: Menú Principal con Iconos ---
# Iconos (16x16 píxeles, MONO_HLSB)
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, 0xff, 0xff,
0xfe, 0x7f,
0xfd, 0xbf,
0xfd, 0x3f,
0xfd, 0xbf,
0xfd, 0x3f,
0xfd, 0xbf,
0xfd, 0x3f,
0xfb, 0xdf,
0xf7, 0xef,
0xf7, 0xef,
0xf7, 0xef,
0xf8, 0x1f,
0xff, 0xff,
0x00, 0x00,
0x00, 0x00
])
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 para Sokoban (ejemplo simple)
icon_sokoban_bytes = bytearray([
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x3C, 0x3C, 0x3C, 0x3C,
0x3C, 0x3C, 0x3C, 0x3C,
0x3C, 0x3C, 0x3C, 0x3C,
0x3C, 0x3C, 0x3C, 0x3C,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 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)
]
current_menu_selection = 0
def run_menu_mode():
global global_current_mode, current_menu_selection
if not oled_initialized or not oled_powered: 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)
display_global_clock()
oled.show()
if check_button_press(btn_up, button_last_states['up']):
current_menu_selection = (current_menu_selection - 1 + len(menu_items)) % len(menu_items)
if check_button_press(btn_down, button_last_states['down']):
current_menu_selection = (current_menu_selection + 1) % len(menu_items)
if check_button_press(btn_enter, button_last_states['enter']):
global_current_mode = menu_items[current_menu_selection][2]
# Si entramos al modo Sokoban, inicializar el juego si no está activo
if global_current_mode == MODE_SOKOBAN:
if not sokoban_game.game_running: # Solo cargar si no está ya en un nivel
sokoban_game.current_level_idx = 0 # Reiniciar al primer nivel si entras
sokoban_game.load_level(sokoban_game.current_level_idx)
# --- MODO: Reloj Digital (Visualización y Configuración) ---
def show_clock_mode():
global global_current_mode, time_setting_part
global current_setting_h, current_setting_m, current_setting_s, clock_setting_active
global global_h, global_m, global_s
if not oled_initialized or not oled_powered: return
oled.fill(0)
display_global_clock()
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)
oled.show()
if check_button_press(btn_enter, button_last_states['enter']):
clock_setting_active = True
current_setting_h = global_h
current_setting_m = global_m
current_setting_s = global_s
time_setting_part = 0
if check_button_press(btn_back, button_last_states['back']):
global_current_mode = MODE_MENU
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()
if check_button_press(btn_up, button_last_states['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
if check_button_press(btn_down, button_last_states['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
if check_button_press(btn_right, button_last_states['right']):
time_setting_part = (time_setting_part + 1) % 3
if check_button_press(btn_enter, button_last_states['enter']):
global_h = current_setting_h
global_m = current_setting_m
global_s = current_setting_s
clock_setting_active = False
if check_button_press(btn_back, button_last_states['back']):
clock_setting_active = False
# --- MODO: Cronómetro ---
def show_chronometer_mode():
global ch, cm, cs, cc, cronometro_activo, global_current_mode
if not oled_initialized or not oled_powered: 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()
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
if check_button_press(btn_enter, button_last_states['enter']):
cronometro_activo = not cronometro_activo
if check_button_press(btn_left, button_last_states['left']):
ch = cm = cs = cc = 0
cronometro_activo = False
if check_button_press(btn_back, button_last_states['back']):
global_current_mode = MODE_MENU
# --- MODO: Reloj Analógico ---
def show_analog_clock_mode():
global global_current_mode
if not oled_initialized or not oled_powered: return
oled.fill(0)
cx, cy, r = 64, 32, 28 # Centro y radio del círculo
# Dibujar marcas de las horas (simuladas cada 30 grados)
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) # Dibuja un píxel para cada marca
# Calcular ángulos de las manecillas
# Segundos (cada segundo es 6 grados, 0 es a las 3 en punto)
ang_s = (global_s / 60) * 360 - 90
# Minutos (cada minuto es 6 grados, más el avance fraccional de segundos)
ang_m = (global_m / 60) * 360 - 90
# Horas (cada hora es 30 grados, más el avance fraccional de minutos)
ang_h = ((global_h % 12 + global_m / 60) / 12) * 360 - 90
# Dibujar manecillas (ajusta la longitud con (r - offset))
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)
display_global_clock() # Muestra también el reloj digital en una esquina
oled.show()
if check_button_press(btn_back, button_last_states['back']):
global_current_mode = MODE_MENU
# --- MODO: Temperatura ---
temp_last_read_time = 0
TEMP_READ_INTERVAL_MS = 2000 # Leer sensor cada 2 segundos
def run_temperature_mode():
global global_current_mode, temp_last_read_time
if not oled_initialized or not oled_powered: return
oled.fill(0)
display_global_clock()
oled.text("Modo: Temperatura", 0, 10)
current_time_ms = time.ticks_ms()
# Solo leer el sensor si ha pasado suficiente tiempo desde la última lectura
if time.ticks_diff(current_time_ms, temp_last_read_time) >= TEMP_READ_INTERVAL_MS:
temp_last_read_time = current_time_ms
try:
dht_sensor.measure() # Realiza la lectura del sensor
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 los primeros 15 caracteres del error
else:
oled.text("Esperando datos...", 0, 30)
oled.show()
if check_button_press(btn_back, button_last_states['back']):
global_current_mode = MODE_MENU
# --- MODO: Calendario ---
def show_calendar_mode():
global global_current_mode
global current_setting_day, current_setting_month, current_setting_year
global date_setting_part, calendar_setting_active
global global_day, global_month, global_year
if not oled_initialized or not oled_powered: 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)
oled.show()
if check_button_press(btn_enter, button_last_states['enter']):
calendar_setting_active = True
current_setting_day = global_day
current_setting_month = global_month
current_setting_year = global_year
date_setting_part = 0 # Iniciar ajuste en el día
if check_button_press(btn_back, button_last_states['back']):
global_current_mode = MODE_MENU
else: # Modo de ajuste de fecha
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)
# Resaltar la parte que se está ajustando
if date_setting_part == 0: # Día
oled.rect(20-1, 30+8+1, 2*8+2, 2, 1) # Subrayado bajo el día
elif date_setting_part == 1: # Mes
oled.rect(20+3*8-1, 30+8+1, 2*8+2, 2, 1) # Subrayado bajo el mes
elif date_setting_part == 2: # Año
oled.rect(20+6*8-1, 30+8+1, 4*8+2, 2, 1) # Subrayado bajo el año
oled.text("UP/DOWN: Ajustar", 0, 40)
oled.text("RIGHT: Siguiente", 0, 50)
oled.show()
if check_button_press(btn_up, button_last_states['up']):
if date_setting_part == 0: # Ajustar Día
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: # Ajustar Mes
current_setting_month = (current_setting_month % 12) + 1
# Asegurar que el día sea válido para el nuevo mes
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: # Ajustar Año
current_setting_year = (current_setting_year % 2100) + 1 if current_setting_year < 2099 else 2000 # Rango de años
# Asegurar que el día sea válido si cambiamos a un año no bisiesto en febrero
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
if check_button_press(btn_down, button_last_states['down']):
if date_setting_part == 0: # Ajustar Día
max_days = get_days_in_month(current_setting_month, current_setting_year)
current_setting_day = (current_setting_day - 2 + max_days) % max_days + 1 # +1 para asegurar 1-max_days
elif date_setting_part == 1: # Ajustar Mes
current_setting_month = (current_setting_month - 2 + 12) % 12 + 1 # +1 para asegurar 1-12
# Asegurar que el día sea válido para el nuevo mes
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: # Ajustar Año
current_setting_year = (current_setting_year - 2001 + 100) % 100 + 2000 if current_setting_year > 2000 else 2099 # Rango de años
# Asegurar que el día sea válido si cambiamos a un año no bisiesto en febrero
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
if check_button_press(btn_right, button_last_states['right']):
date_setting_part = (date_setting_part + 1) % 3 # Mover a la siguiente parte (día, mes, año)
if check_button_press(btn_enter, button_last_states['enter']):
global_day = current_setting_day
global_month = current_setting_month
global_year = current_setting_year
calendar_setting_active = False # Salir del modo de ajuste
if check_button_press(btn_back, button_last_states['back']):
calendar_setting_active = False # Salir del modo de ajuste sin guardar
# --- Función para ejecutar el modo Sokoban ---
def run_sokoban_mode():
"""
Esta función es la que el scheduler llamará para el modo SOKOBAN.
Simplemente llama al método de la instancia SokobanGame que maneja una iteración.
"""
sokoban_game.run_sokoban_iteration()
# --- Planificador Round Robin con Interrupciones ---
system_tasks = [
task_update_global_clock # Esta tarea actualmente no hace nada, ya que el reloj se actualiza por ISR
]
mode_to_task_map = {
MODE_MENU: run_menu_mode,
MODE_CLOCK: show_clock_mode,
MODE_CHRONOMETER: show_chronometer_mode,
MODE_ANALOG_CLOCK: show_analog_clock_mode,
MODE_TEMPERATURE: run_temperature_mode,
MODE_CALENDAR: show_calendar_mode,
MODE_SOKOBAN: run_sokoban_mode # Mapeamos la función de Sokoban
}
TIME_SLICE_MS = 50 # Periodo de ejecución del planificador (50 ms)
# Configuración del Timer para el reloj global (se dispara cada segundo)
timer_global_reloj = Timer()
# La función tick_global_reloj_ISR ahora también maneja el avance de días/meses/años
timer_global_reloj.init(freq=1, mode=Timer.PERIODIC, callback=lambda t: tick_global_reloj_ISR())
def tick_global_reloj_ISR():
global global_h, global_m, global_s, global_day, global_month, global_year
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
global_day += 1
# Lógica para avanzar el día, mes, año
max_days_in_month = get_days_in_month(global_month, global_year)
if global_day > max_days_in_month:
global_day = 1
global_month += 1
if global_month > 12:
global_month = 1
global_year += 1
def scheduler_callback(timer):
"""
Esta función es la rutina de servicio de interrupción (ISR) del planificador.
Se ejecuta periódicamente cada TIME_SLICE_MS.
"""
global global_current_mode
# Ejecuta la tarea del modo actual
if global_current_mode == MODE_PRESENTATION:
run_presentation_mode_once() # Se llama directamente para que se ejecute una vez
else:
if global_current_mode in mode_to_task_map:
mode_to_task_map[global_current_mode]()
else:
# Si el modo actual no es válido, vuelve al menú
global_current_mode = MODE_MENU
print(f"AVISO: Modo desconocido '{global_current_mode}', volviendo a menú.")
# Ejecuta otras tareas del sistema (si las hay)
for sys_task in system_tasks:
sys_task()
# Configuración del Timer para el planificador Round Robin
rr_timer = Timer()
try:
print(f"Inicializando Timer Round Robin con periodo de {TIME_SLICE_MS}ms...")
rr_timer.init(mode=Timer.PERIODIC, period=TIME_SLICE_MS, callback=scheduler_callback)
print("Timer Round Robin inicializado.")
except Exception as e:
print(f"ERROR: Fallo al inicializar el Timer Round Robin. Mensaje: {e}")
# --- Bucle Principal del Sistema ---
def main_loop_scheduler():
"""
Este es el bucle principal del programa. En un sistema basado en interrupciones,
este bucle simplemente mantiene el microcontrolador activo, mientras las tareas
reales se ejecutan dentro de las ISRs de los Timers.
"""
print("Entrando al bucle principal del scheduler. Las tareas se ejecutan por interrupciones.")
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("Detectado Ctrl+C. Deteniendo timers...")
rr_timer.deinit()
timer_global_reloj.deinit()
print("Timers detenidos. Programa finalizado.")
# --- Ejecución del Programa ---
if __name__ == "__main__":
main_loop_scheduler()