from machine import Pin, I2C, PWM, Timer
import time
import math
import framebuf
import csv
from dht import DHT22 # Importar 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 (corregido)
# Pins para el sensor DHT11/DHT22
DHT_PIN = 19 # GPIO19 para el sensor DHT22
dht_sensor = DHT22(Pin(DHT_PIN))
# --- Modos del Sistema ---
MODE_CLOCK = 0 # Ahora es el primer modo al iniciar
MODE_CHRONOMETER = 1
MODE_ANALOG_CLOCK = 2
MODE_TEMPERATURE = 3
MODE_CALENDAR = 4
MODE_SOKOBAN = 5
MODE_MENU = 99 # Mantener MENU como un destino específico para Sokoban
# Define el orden de los modos para la navegación con los botones Izquierda/Derecha
MODE_CYCLE_ORDER = [
MODE_CLOCK,
MODE_CHRONOMETER,
MODE_ANALOG_CLOCK,
MODE_TEMPERATURE,
MODE_CALENDAR,
MODE_SOKOBAN
]
# --- Variables Globales de Estado ---
global_current_mode = MODE_CLOCK # Inicia directamente en modo reloj digital
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(30000)
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 (Solo se ejecuta una vez al inicio del script) ---
# Se mantiene aquí por si se quiere reactivar en algún momento, pero no se llama automáticamente.
def play_startup_sequence():
if not oled_initialized or not oled_powered:
print("OLED no inicializada o no encendida. Saltando secuencia de inicio.")
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()
# --- 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.")
# No cargar el nivel aquí, solo en run_sokoban_iteration si el modo cambia a Sokoban
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
# --- INICIO DE LA MODIFICACIÓN ---
# Borrar la caja de la posición anterior y restaurar la celda a su estado original
if (new_pr, new_pc) in self.initial_goals:
self.current_map[new_pr][new_pc] = GOAL # Si la caja estaba en un objetivo, restaurar el objetivo
else:
self.current_map[new_pr][new_pc] = EMPTY # Si la caja estaba en un espacio vacío, restaurar el espacio vacío
# --- FIN DE LA MODIFICACIÓN ---
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
global_current_mode = MODE_CLOCK # Volver al primer modo si el juego termina
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
# Solo cargar el nivel si Sokoban acaba de activarse como modo
if not self.game_running:
self.current_level_idx = 0 # Reiniciar al primer nivel
self.load_level(self.current_level_idx)
if not self.game_running: # Si load_level falla, volver al primer modo
global_current_mode = MODE_CLOCK
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_enter, self.btn_states['enter']):
self.game_running = False # Marca el juego como no activo
global_current_mode = MODE_MENU # Volver al menú principal o un modo predeterminado
return # Salir para que el scheduler pueda cambiar el modo
# --- Creación de la instancia de SokobanGame (después de inicializar OLED y botones) ---
sokoban_game = SokobanGame()
# Llamar a la secuencia de inicio una vez al arrancar el script
play_startup_sequence()
# --- 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
# --- 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("RIGHT: Siguiente", 0, 40) # Changed navigation
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_right, button_last_states['right']): # Changed navigation
current_index = MODE_CYCLE_ORDER.index(global_current_mode)
global_current_mode = MODE_CYCLE_ORDER[(current_index + 1) % len(MODE_CYCLE_ORDER)]
else: # Clock setting active
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, 0) # Título más arriba
t = "{:02}:{:02}:{:02}.{:02}".format(ch, cm, cs, cc)
oled.text(t[0:5], 10, 20) # Subir primera parte del cronómetro
oled.text(t[6:], 10, 35) # Subir segunda parte del cronómetro
oled.text("{}".format("ON" if cronometro_activo else "PAUSA"), 85, 10) # Subir estado ON/PAUSA
oled.text("LEFT: Reset", 0, 48) # Subir mensaje de botón
oled.text("RIGHT: Siguiente", 0, 56) # Subir mensaje de botón
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_right, button_last_states['right']): # Changed navigation
current_index = MODE_CYCLE_ORDER.index(global_current_mode)
global_current_mode = MODE_CYCLE_ORDER[(current_index + 1) % len(MODE_CYCLE_ORDER)]
# --- 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.text("RIGHT: Siguiente", 0, 56) # Changed navigation
oled.show()
if check_button_press(btn_right, button_last_states['right']): # Changed navigation
current_index = MODE_CYCLE_ORDER.index(global_current_mode)
global_current_mode = MODE_CYCLE_ORDER[(current_index + 1) % len(MODE_CYCLE_ORDER)]
# --- 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
except Exception as e: # Captura cualquier otro error durante la lectura
oled.text("Error Gral.", 0, 30)
oled.text(str(e)[:15], 0, 45)
else:
oled.text("Esperando datos...", 0, 30)
oled.text("RIGHT: Siguiente", 0, 56) # Changed navigation
oled.show()
if check_button_press(btn_right, button_last_states['right']): # Changed navigation
current_index = MODE_CYCLE_ORDER.index(global_current_mode)
global_current_mode = MODE_CYCLE_ORDER[(current_index + 1) % len(MODE_CYCLE_ORDER)]
# --- 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("RIGHT: Siguiente", 0, 40) # Changed navigation
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_right, button_last_states['right']): # Changed navigation
current_index = MODE_CYCLE_ORDER.index(global_current_mode)
global_current_mode = MODE_CYCLE_ORDER[(current_index + 1) % len(MODE_CYCLE_ORDER)]
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.
"""
global global_current_mode
sokoban_game.run_sokoban_iteration()
# No hay navegación LEFT/RIGHT aquí; ENTER para salir al menú.
# --- MODO: Menú Principal (Ahora solo es un destino para Sokoban) ---
# No hay navegación cíclica hacia o desde este modo con LEFT/RIGHT
def run_menu_mode():
global global_current_mode
if not oled_initialized or not oled_powered: return
oled.fill(0)
oled.text("Menu Principal", 0, 10)
oled.text("Presiona BACK", 0, 30)
oled.text("para ir al RELOJ", 0, 40)
display_global_clock()
oled.show()
# Si estamos en el modo MENU, BACK nos devuelve al primer modo ciclable
if check_button_press(btn_back, button_last_states['back']):
global_current_mode = MODE_CLOCK
# --- 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_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,
MODE_MENU: run_menu_mode # Incluirlo si es un posible destino
}
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 in mode_to_task_map:
mode_to_task_map[global_current_mode]()
else:
# Si el modo actual no es válido, vuelve al primer modo ciclable
global_current_mode = MODE_CLOCK
print(f"AVISO: Modo desconocido '{global_current_mode}', volviendo a Reloj Digital.")
# 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()