# main.py - Código unificado de sistema embebido (Actualizado para Round-Robin)
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 # Añade al inicio de los imports
# Importar las librerías y módulos de tu docente.
import csv # Necesario para Sokoban (niveles y scores)
import dht # Para el sensor DHT22
print("Iniciando script...")
# --- Constantes y Pines ---
WIDTH, HEIGHT = 128, 64 # Dimensiones de la pantalla OLED
# Pines I2C para la pantalla OLED
SCL_PIN = 5
SDA_PIN = 4
# Pines para el Joystick Analógico (Zoom en Calculadora, Cursor)
JOY_X_PIN = 26 # GP26
JOY_Y_PIN = 27 # GP27
# Pines para los Botones (4 Direccionales + 1 Seleccionar + 1 Atrás)
BTN_UP_PIN = 10 # GP10 (Arriba)
BTN_DOWN_PIN = 11 # GP11 (Abajo)
BTN_RIGHT_PIN = 12 # GP12 (Derecha)
BTN_LEFT_PIN = 13 # GP13 (Izquierda)
BTN_ENTER_PIN = 15 # GP15 (Ahora es ENTER/Seleccionar)
BTN_BACK_PIN = 16 # GP16 (NUEVO BOTÓN: ATRÁS)
# Pin para el Buzzer
BUZZER_PIN = 14 # GP14
# Pin para el sensor DHT22
DHT22_PIN = 19 # GP19
# --- Pines para HC-SR04 ---
TRIGGER_PIN = 20 # GPIO20
ECHO_PIN = 21 # GPIO21
DISTANCE_THRESHOLD = 5 # cm para apagar OLED
# --- Definición de Modos del Sistema ---
MODE_PRESENTATION = -1 # Un modo inicial para la pantalla de bienvenida
MODE_MENU = 0
MODE_CLOCK = 1
MODE_CHRONOMETER = 2
MODE_ANALOG_CLOCK = 3
MODE_SOKOBAN = 4
MODE_GRAPH_CALCULATOR = 5
MODE_TEMPERATURE = 6
MODE_CALENDAR = 7
# --- Inicialización de Hardware ---
oled_initialized = False
try:
print("Inicializando I2C y OLED...")
i2c = I2C(0, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN), freq=400000)
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)
oled_initialized = True
print("OLED inicializada exitosamente.")
except Exception as e:
print(f"ERROR: Fallo al inicializar OLED en pines SCL={SCL_PIN}, SDA={SDA_PIN}. Mensaje: {e}")
buzzer = PWM(Pin(BUZZER_PIN))
buzzer.freq(1000) # Frecuencia inicial para el buzzer
buzzer.duty_u16(0) # Apagado inicialmente
print("Buzzer inicializado.")
# Inicialización de pines de botones con pull-up interno
btn_up = Pin(BTN_UP_PIN, Pin.IN, Pin.PULL_UP)
btn_down = Pin(BTN_DOWN_PIN, Pin.IN, Pin.PULL_UP)
btn_right = Pin(BTN_RIGHT_PIN, Pin.IN, Pin.PULL_UP)
btn_left = Pin(BTN_LEFT_PIN, Pin.IN, Pin.PULL_UP)
btn_enter = Pin(BTN_ENTER_PIN, Pin.IN, Pin.PULL_UP)
btn_back = Pin(BTN_BACK_PIN, Pin.IN, Pin.PULL_UP)
print("Botones inicializados.")
# Inicialización de ADCs para el joystick
joy_x = ADC(JOY_X_PIN)
joy_y = ADC(JOY_Y_PIN)
print("Joystick ADCs inicializados.")
# Inicialización del sensor DHT22
dht_sensor = dht.DHT22(Pin(DHT22_PIN))
print("Sensor DHT22 inicializado (o intento).")
# --- Variables Globales del Sistema ---
current_mode = MODE_PRESENTATION # Estado actual del sistema
# Variables para el reloj digital global (se actualiza cada segundo)
# Al arrancar, el reloj siempre empezará en 00:00:00
global_h = 0
global_m = 0
global_s = 0
# Timer para el reloj global (ISR)
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}")
# --- Funciones Auxiliares del Sistema ---
# Control del Buzzer para sonido de clic
def play_click_sound():
if buzzer is not None: # Asegurar que el buzzer esté inicializado
buzzer.duty_u16(5000) # Volumen bajo para clic
buzzer.freq(2000) # Tono agudo
time.sleep_ms(50) # Duración corta
buzzer.duty_u16(0) # Apagar
# Diccionario para almacenar los estados de los botones para debounce.
# Ahora se inicializa globalmente, ya que todas las tareas lo usarán.
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]: # Si se presionó y el estado anterior era alto
last_state_var_list[0] = False # Actualizar estado anterior
play_click_sound() # Reproducir sonido al presionar
return True
elif current_state and not last_state_var_list[0]: # Si se soltó y el estado anterior era bajo
last_state_var_list[0] = True # Actualizar estado anterior
return False
# Muestra el reloj digital global en una esquina de la OLED
def display_global_clock():
if oled_initialized:
oled.text("{:02}:{:02}".format(global_h, global_m), 80, 0) # Posición superior derecha
# --- MODO: Pantalla de Presentación ---
# Ahora la función retorna True cuando debe pasar al siguiente modo
def run_presentation_mode():
global current_mode
if not oled_initialized:
print("OLED no inicializada. Saltando modo presentación.")
current_mode = MODE_MENU # Pasa al siguiente modo si la OLED falla
return True # Indica al scheduler que ha terminado su trabajo
# Estas variables se usarán para controlar el tiempo sin bloquear con time.sleep()
# Se moverán a un contexto global o se pasarán, pero por ahora las dejamos aquí para la lógica.
# En una arquitectura Round-Robin real, no puedes tener un sleep() largo aquí.
# Tendrás que usar un temporizador interno o un contador de ticks.
# Por simplicidad para este ejemplo inicial, el `time.sleep()` se mantiene por ahora,
# pero lo ideal es que esta función simplemente dibuje y luego el scheduler espere.
# Para un Round-Robin puro, esta función solo dibuja y el scheduler gestiona la duración.
# Sin embargo, dado que es un modo "una sola vez", lo convertiremos en una función de "inicio".
# Lógica de sonido inicial, se ejecuta solo una vez al inicio del modo
for _ in range(2):
buzzer.freq(500)
buzzer.duty_u16(30000)
time.sleep_ms(200) # Este sleep bloqueará temporalmente el scheduler
buzzer.duty_u16(0)
time.sleep_ms(50) # Este sleep bloqueará temporalmente el scheduler
buzzer.freq(1000)
buzzer.duty_u16(30000)
time.sleep_ms(200) # Este sleep bloqueará temporalmente el scheduler
buzzer.duty_u16(0)
time.sleep_ms(50) # Este sleep bloqueará temporalmente el scheduler
buzzer.duty_u16(0)
oled.fill(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()
# Para un Round-Robin puro, esta función solo mostraría la pantalla.
# La espera de 3 segundos se gestionaría en el scheduler.
# Por ahora, la mantenemos como un sleep para facilitar la transición,
# pero en el siguiente paso la convertiremos a una lógica no bloqueante.
time.sleep(3) # ESTO BLOQUEA EL SCHEDULER. LO CAMBIAREMOS EN LA SIGUIENTE ITERACIÓN.
oled.fill(0)
oled.show()
# Al finalizar la presentación, indica que debe pasar al siguiente modo
current_mode = MODE_MENU
return True # Indica que la tarea de presentación ha finalizado.
# --- MODO: Menú Principal con Iconos ---
# Iconos (16x16 píxeles, MONO_HLSB)
# Reloj Digital
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)
# Cronómetro
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)
# Reloj Analógico
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)
# Sokoban Game (Caja + Jugador)
icon_sokoban_bytes = bytearray([
0x00, 0x00,
0xfe, 0x7f,
0xf6, 0x7f,
0xea, 0x7f,
0xf6, 0x7f,
0xfe, 0x7f,
0xfe, 0x7f,
0x7e, 0x7e,
0x7e, 0x7e,
0xfe, 0x7f,
0xfe, 0x7f,
0xfe, 0x7f,
0xfe, 0x7f,
0xfe, 0x6f,
0xfe, 0x7f,
0x00, 0x00
])
icon_sokoban_fb = framebuf.FrameBuffer(icon_sokoban_bytes, 16, 16, framebuf.MONO_HLSB)
# Calculadora Gráfica
icon_calc_bytes = bytearray([
0xff, 0xff,
0xf7, 0xff,
0xf7, 0xff,
0xc1, 0xc1,
0xf7, 0xff,
0xf7, 0xff,
0xff, 0xff,
0xff, 0xff,
0xf7, 0xff,
0xc1, 0xdf,
0xf7, 0xef,
0xf7, 0xf7,
0xeb, 0xfb,
0xdd, 0xfd,
0xff, 0xff,
0xff, 0xff
])
icon_calc_fb = framebuf.FrameBuffer(icon_calc_bytes, 16, 16, framebuf.MONO_HLSB)
# Temperatura (Termómetro)
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)
# Calendario
icon_calendar_bytes = bytearray([
0x12, 0x48,
0x12, 0x48,
0x12, 0x48,
0x00, 0x00,
0x00, 0x00,
0xff, 0xff,
0xf7, 0x0f,
0xf7, 0xef,
0xf7, 0xef,
0xf7, 0x0f,
0xf7, 0x7f,
0xf7, 0x7f,
0xf7, 0x7f,
0xf7, 0x0f,
0xff, 0xff,
0xff, 0xff
])
icon_calendar_fb = framebuf.FrameBuffer(icon_calendar_bytes, 16, 16, framebuf.MONO_HLSB)
menu_items = [
("Reloj Digital", icon_clock_fb),
("Cronometro", icon_chrono_fb),
("Reloj Analogico", icon_analog_fb),
("Sokoban Game", icon_sokoban_fb),
("Calculadora Graph", icon_calc_fb),
("Temperatura", icon_temp_fb),
("Calendario", icon_calendar_fb)
]
current_menu_selection = 0
# Ahora la función del menú debe ejecutar una sola iteración y devolver el control
# para que el scheduler pueda correr otras tareas.
def run_menu_mode():
global current_mode, current_menu_selection
if not oled_initialized: return
oled.fill(0)
# Offset para desplazar el menú si hay muchos elementos
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: # Evitar offset negativo
display_offset = 0
# Dibujar los elementos visibles del menú
for i in range(display_offset, min(len(menu_items), display_offset + 3)):
menu_text, menu_icon_fb = menu_items[i]
# Posición para el ícono y el texto
icon_x = 0
text_x = 20 # Texto inicia después del ícono (16px + un pequeño margen)
y_pos = 5 + (i - display_offset) * 20 # Espaciado vertical entre elementos
if i == current_menu_selection:
# Resaltar el elemento seleccionado (invertir colores)
oled.fill_rect(icon_x, y_pos, WIDTH, 18, 1) # Rectángulo blanco de fondo
oled.blit(menu_icon_fb, icon_x + 2, y_pos + 1, 0) # Icono en negro sobre blanco
oled.text(menu_text, text_x, y_pos + 5, 0) # Texto en negro sobre blanco
else:
# Dibujar el elemento normal (negro sobre fondo negro)
oled.blit(menu_icon_fb, icon_x + 2, y_pos + 1, 1) # Icono en blanco sobre negro
oled.text(menu_text, text_x, y_pos + 5, 1) # Texto en blanco sobre negro
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']):
# Aquí la tarea del menú cambia el modo. El scheduler detectará el cambio y llamará a la nueva tarea.
current_mode = current_menu_selection + MODE_CLOCK
# Ya no es necesario un sleep_ms aquí. El scheduler se encargará de los tiempos.
# El botón BACK en el menú no tiene acción, solo se usa para salir de los sub-modos.
# Podrías añadir una opción "Apagar" o "Salir" si quisieras.
# --- Nuevo: El Scheduler Round-Robin ---
# Diccionario que mapea los modos a sus funciones correspondientes
# Cada función debe ser 'cooperativa' (no tener bucles internos while True).
# Se espera que cada función dibuje y procese una iteración, y luego termine,
# devolviendo el control al scheduler.
# Si una función necesita un tiempo prolongado, deberá usar contadores internos
# o un generador para pausar su ejecución y reanudarla.
task_scheduler = {
MODE_PRESENTATION: run_presentation_mode,
MODE_MENU: run_menu_mode,
# Los demás modos se añadirán aquí a medida que los refactoricemos:
# MODE_CLOCK: show_clock_mode,
# MODE_CHRONOMETER: show_chronometer_mode,
# ...
}
# Variable para gestionar el estado de la presentación
presentation_done = False
presentation_start_time = 0
# --- Bucle Principal del Sistema (Scheduler Round-Robin) ---
while True:
# Gestión del modo de presentación (se ejecuta una vez)
if current_mode == MODE_PRESENTATION:
if not presentation_done:
# Esta parte se ejecutará UNA SOLA VEZ para la presentación
run_presentation_mode() # Ejecuta la lógica de presentación
presentation_start_time = time.ticks_ms() # Guarda el tiempo de inicio
presentation_done = True # Marca que la presentación ha empezado
# Espera activa para la duración de la presentación (3 segundos)
# Esto reemplaza el time.sleep(3) dentro de run_presentation_mode()
# Idealmente, 'run_presentation_mode' solo dibuja y el scheduler espera.
if time.ticks_diff(time.ticks_ms(), presentation_start_time) >= 3000:
current_mode = MODE_MENU # Transición al menú después de 3 segundos
presentation_done = False # Reinicia para futuras presentaciones si se vuelve a este modo
elif current_mode in task_scheduler:
# Ejecuta la tarea actual
# Las funciones de tarea (ej. run_menu_mode) ahora deben hacer una iteración de su lógica
# y luego devolver el control.
task_scheduler[current_mode]()
else:
# Manejar un modo desconocido o un modo que aún no tiene una tarea asociada
if oled_initialized:
oled.fill(0)
oled.text("MODO: {}".format(current_mode), 0, 0)
oled.text("NO IMPLEM.", 0, 10)
display_global_clock() # Muestra el reloj incluso en modos no implementados
oled.show()
print(f"Modo desconocido o no implementado: {current_mode}")
time.sleep_ms(100) # Pequeña pausa para no ciclar demasiado rápido
# Pausa para dar tiempo al procesador y evitar un ciclo muy rápido,
# permitiendo que otras operaciones (como la ISR del reloj) se ejecuten.
# El valor aquí es tu 'quantum' de tiempo si no estás usando un Timer para el scheduler.
time.sleep_ms(20)
# --- MODO: Reloj Digital (Visualización y Configuración) ---
# Variable para rastrear qué parte del tiempo se está ajustando
# 0: Horas, 1: Minutos, 2: Segundos
time_setting_part = 0
# Variables temporales para el ajuste, se inicializarán al entrar al modo de configuración.
current_setting_h = 0
current_setting_m = 0
current_setting_s = 0
# Estado del sub-modo: False = solo visualización, True = configuración
clock_setting_active = False
def show_clock_mode():
global global_h, global_m, global_s, current_mode, 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() # Muestra la hora del reloj global en la esquina siempre
if not clock_setting_active:
# --- MODO: VISUALIZACIÓN DEL RELOJ ---
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) # Hora principal del reloj
oled.text("ENTER: Ajustar", 0, 50)
oled.text("BACK: Menu", 0, 40)
oled.show()
if check_button_press(btn_enter, button_last_states['enter']):
# Activar el modo de configuración
clock_setting_active = True
# Inicializar las variables de ajuste con la hora global actual
current_setting_h = global_h
current_setting_m = global_m
current_setting_s = global_s
time_setting_part = 0 # Siempre empezar ajustando horas
time.sleep_ms(300) # Pausa para evitar doble entrada/rebote
if check_button_press(btn_back, button_last_states['back']):
current_mode = MODE_MENU # Volver al menú principal
time.sleep_ms(300)
else:
# --- MODO: CONFIGURACIÓN DEL RELOJ ---
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)
# Indicar qué parte se está ajustando
if time_setting_part == 0: # Horas
oled.rect(20-1, 30+8+1, 2*8+2, 2, 1)
elif time_setting_part == 1: # Minutos
oled.rect(20+3*8-1, 30+8+1, 2*8+2, 2, 1)
elif time_setting_part == 2: # Segundos
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()
# Lógica de ajuste con UP/DOWN/RIGHT
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
time.sleep_ms(150)
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
time.sleep_ms(150)
if check_button_press(btn_right, button_last_states['right']):
time_setting_part = (time_setting_part + 1) % 3
time.sleep_ms(200)
if check_button_press(btn_enter, button_last_states['enter']):
# Aplicar la hora configurada a las variables globales
global_h = current_setting_h
global_m = current_setting_m
global_s = current_setting_s
clock_setting_active = False # Salir del modo de configuración
time.sleep_ms(300)
if check_button_press(btn_back, button_last_states['back']):
clock_setting_active = False # Salir del modo de configuración sin guardar
time.sleep_ms(300)
# --- MODO: Cronómetro ---
ch = cm = cs = cc = 0
cronometro_activo = False
def show_chronometer_mode():
global ch, cm, cs, cc, cronometro_activo, current_mode
if not oled_initialized: return
oled.fill(0)
display_global_clock()
oled.text("Cronometro", 0, 10)
t = "{:02}:{:02}:{:02}.{:02}".format(ch, cm, cs, cc)
oled.text(t[0:5], 10, 30)
oled.text(t[6:], 10, 45)
oled.text("{}".format("ON" if cronometro_activo else "PAUSA"), 85, 20)
# Nuevo mensaje para la opción de reset
oled.text("LEFT: Reset", 0, 56) # Añadido en la parte inferior
oled.show()
if cronometro_activo:
cc += 1
if cc >= 100: # Centisegundos
cc = 0
cs += 1
if cs >= 60:
cs = 0
cm += 1
if cm >= 60:
cm = 0
ch += 1
if check_button_press(btn_enter, button_last_states['enter']):
cronometro_activo = not cronometro_activo
time.sleep_ms(300)
# Esta es la lógica que ya estaba para reset
if check_button_press(btn_left, button_last_states['left']):
ch = cm = cs = cc = 0
cronometro_activo = False # Asegurarse de que el cronómetro se detenga al resetear
time.sleep_ms(300)
if check_button_press(btn_back, button_last_states['back']):
current_mode = MODE_MENU
time.sleep_ms(300)
# --- MODO: Reloj Analógico ---
def show_analog_clock_mode():
global current_mode
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()
if check_button_press(btn_enter, button_last_states['enter']):
pass
if check_button_press(btn_back, button_last_states['back']):
current_mode = MODE_MENU
time.sleep_ms(300)
# --- 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, oled_obj, display_global_clock_func, btn_states, check_btn_func, oled_initialized_status):
self.oled = oled_obj
self.display_global_clock = display_global_clock_func
self.btn_states = btn_states
self.check_button_press = check_btn_func
self.oled_initialized = oled_initialized_status
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
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.")
# --- 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 or level_idx >= len(self.levels):
print("Sokoban: Nivel {} no disponible o fin de niveles.".format(level_idx))
self.game_running = False
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
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)
# Una vez que encontramos al jugador, la celda donde estaba podría ser un espacio o un objetivo
# Esto se manejará al dibujar y al mover. No es necesario cambiar el carácter en current_map aquí.
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
if self.player_pos in self.initial_goals and 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 # Asegurar que la celda es GOAL si el jugador está en ella
elif self.current_map[self.player_pos[0]][self.player_pos[1]] != PLAYER: # Si el jugador no está en un objetivo
self.current_map[self.player_pos[0]][self.player_pos[1]] = EMPTY # Asegurar que la celda es EMPTY
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()
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
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 < 18: # Espacio para "Lvl:", "Mov:", "Push:"
offset_y = 18
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
# --- LLAMADA CORREGIDA AL MÉTODO CIRCLE ---
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
# Primero, restaurar la celda donde estaba el jugador
if (pr, pc) in self.initial_goals:
self.current_map[pr][pc] = GOAL # Si el jugador estaba en un objetivo, restaurar el objetivo
else:
self.current_map[pr][pc] = EMPTY # Si estaba en un espacio vacío, dejarlo vacío
self.player_pos = (new_pr, new_pc) # Actualizar la posición del jugador
# Si la celda a la que se mueve el jugador tiene una caja, la caja ya fue movida
# y ahora la celda se convierte en el lugar del jugador.
# Si no había caja, la celda simplemente se convierte en el jugador.
# Esto es implícito porque player_pos es lo que se dibuja
self.moves += 1 # Contar el movimiento del jugador
play_click_sound()
self.check_win()
def check_win(self):
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:
self.level_completed = False
return
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 not self.load_level(self.current_level_idx): # Intentar cargar el siguiente nivel
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
else:
time.sleep_ms(500) # Pequeña pausa antes de dibujar el siguiente nivel
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}")
def run(self):
if not self.game_running or not self.oled_initialized:
print("Sokoban: No se puede iniciar el juego (no hay niveles o OLED no disponible).")
global current_mode
current_mode = MODE_MENU
return
while self.game_running: # El bucle del juego corre mientras game_running sea True
self.draw_game()
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 # Salir del bucle del juego
break
time.sleep_ms(100) # Pequeño retardo para suavizar el control
global current_mode
current_mode = MODE_MENU # Al salir del bucle (por completar niveles o botón BACK), vuelve al menú
# --- MÓDULO CALCULADORA GRÁFICA (Integrado) ---
# --- Funciones y Clases para la Calculadora Gráfica ---
class Parser:
def __init__(self, expression):
self.expr = expression.replace(" ", "")
self.pos = 0
self.x_val = 0
def parse(self, x_val=0):
self.x_val = x_val
self.pos = 0
try:
result = self.parse_expression()
if self.pos < len(self.expr):
raise ValueError("Caracter inesperado en '{}' en pos {}".format(self.expr, self.pos))
return result
except IndexError:
raise ValueError("Expresión incompleta")
except Exception as e:
raise ValueError(f"Error en el parser: {e}")
def parse_expression(self):
result = self.parse_term()
while self.pos < len(self.expr) and self.expr[self.pos] in "+-":
op = self.expr[self.pos]
self.pos += 1
term = self.parse_term()
result = result + term if op == '+' else result - term
return result
def parse_term(self):
result = self.parse_factor()
while self.pos < len(self.expr) and self.expr[self.pos] in "*/":
op = self.expr[self.pos]
self.pos += 1
factor = self.parse_factor()
result = result * factor if op == '*' else result / factor
return result
def parse_factor(self):
if self.pos >= len(self.expr):
raise ValueError("Expresión incompleta, se esperaba un factor")
if self.expr[self.pos] == '(':
self.pos += 1
result = self.parse_expression()
if self.pos >= len(self.expr) or self.expr[self.pos] != ')':
raise ValueError("Paréntesis de cierre ')' esperado")
self.pos += 1
return result
func_names = ['sin', 'cos', 'tan', 'exp', 'log', 'sqrt']
for fn in func_names:
ln = len(fn)
if self.pos + ln <= len(self.expr) and self.expr[self.pos:self.pos+ln] == fn:
self.pos += ln
if self.pos >= len(self.expr) or self.expr[self.pos] != '(':
raise ValueError("Se esperaba '(' después de función {}".format(fn))
self.pos += 1
val = self.parse_expression()
if self.pos >= len(self.expr) or self.expr[self.pos] != ')':
raise ValueError("Se esperaba ')' después de los argumentos de {}".format(fn))
self.pos += 1
if fn == 'log':
if val <= 0:
raise ValueError("Dominio de logaritmo: valor no positivo")
return math.log(val)
elif fn == 'sqrt':
if val < 0:
raise ValueError("Dominio de sqrt: valor negativo")
return math.sqrt(val)
else:
return getattr(math, fn)(val)
if self.expr[self.pos] == 'x':
self.pos += 1
return self.x_val
start = self.pos
if self.pos < len(self.expr) and self.expr[self.pos] in '+-':
self.pos += 1
num_found = False
while self.pos < len(self.expr) and self.expr[self.pos].isdigit():
self.pos += 1
num_found = True
if self.pos < len(self.expr) and self.expr[self.pos] == '.':
self.pos += 1
while self.pos < len(self.expr) and self.expr[self.pos].isdigit():
self.pos += 1
num_found = True
if num_found or (start < self.pos and self.expr[start:self.pos] in ['+', '-']):
try:
return float(self.expr[start:self.pos])
except ValueError:
raise ValueError("Número inválido o expresión incompleta: {}".format(self.expr[start:self.pos]))
if self.pos < len(self.expr):
raise ValueError("Caracter o expresión inválida: '{}'".format(self.expr[self.pos]))
else:
raise ValueError("Expresión incompleta")
def draw_axes(oled_obj, xmin, xmax, ymin, ymax):
if not oled_initialized: return
# Calcular la posición en píxeles del eje Y (cuando x=0)
# y = ymin + (pixel_y / HEIGHT) * (ymax - ymin) -> pixel_y = (y - ymin) * HEIGHT / (ymax - ymin)
# Si y=0 (el eje X), entonces pixel_y = (-ymin) * HEIGHT / (ymax - ymin)
y0_pixel = int(HEIGHT - (0 - ymin) * HEIGHT / (ymax - ymin)) # Invertido porque la OLED tiene 0,0 arriba-izquierda
if 0 <= y0_pixel < HEIGHT:
oled_obj.hline(0, y0_pixel, WIDTH, 1)
# Calcular la posición en píxeles del eje X (cuando y=0)
# x = xmin + (pixel_x / WIDTH) * (xmax - xmin) -> pixel_x = (x - xmin) * WIDTH / (xmax - xmin)
# Si x=0 (el eje Y), entonces pixel_x = (0 - xmin) * WIDTH / (xmax - xmin)
x0_pixel = int((0 - xmin) * WIDTH / (xmax - xmin))
if 0 <= x0_pixel < WIDTH:
oled_obj.vline(x0_pixel, 0, HEIGHT, 1)
# Función para graficar la expresión y manejar el zoom/cursor
def plot_function(oled_obj, expr_str, joy_x_obj, joy_y_obj, display_global_clock_func, btns_last_states, check_button_func):
if not oled_initialized: return
parser = Parser(expr_str)
x_range_val = 10.0 # Define el rango total de X de -x_range_val a +x_range_val
y_range_val = 5.0 # Define el rango total de Y de -y_range_val a +y_range_val
cursor_x_pixel = WIDTH // 2
cursor_y_pixel = HEIGHT // 2 # Inicializamos en el centro de la pantalla
# False: modo ZOOM (joystick controla rangos X/Y)
# True: modo CURSOR (joystick controla posición del cursor)
is_cursor_mode = False
JOY_DEADZONE = 10000 # Un rango más amplio para evitar "drift" (centro 32768)
ZOOM_FACTOR = 0.05 # Cuánto cambia el rango por tick
MOVE_STEP = 1 # Píxeles por movimiento del cursor para el cursor
while True:
oled_obj.fill(0)
display_global_clock_func()
xmin = -x_range_val
xmax = x_range_val
ymin = -y_range_val
ymax = y_range_val
# --- Lógica de INTERACCIÓN con JOYSTICK y BOTÓN ENTER ---
# El botón ENTER alterna entre modo ZOOM y modo CURSOR
if check_button_func(btn_enter, btns_last_states['enter']):
is_cursor_mode = not is_cursor_mode
if is_cursor_mode:
# Al entrar en modo cursor, intentar posicionar el cursor sobre la función
# Usar la posición actual en píxeles para calcular la X real
cursor_x_func = xmin + cursor_x_pixel * ((xmax - xmin) / WIDTH)
try:
cursor_y_func_on_graph = parser.parse(x_val=cursor_x_func)
except (ValueError, ZeroDivisionError, OverflowError):
cursor_y_func_on_graph = float('nan') # Si hay error de dominio, el punto no existe
if not math.isnan(cursor_y_func_on_graph):
# Convertir el valor Y de la función a píxel Y para el cursor
pixel_y_for_func_val = int(HEIGHT - ((cursor_y_func_on_graph - ymin) / (ymax - ymin)) * HEIGHT)
# Asegurarse de que el cursor esté dentro de los límites de la pantalla
cursor_y_pixel = max(0, min(HEIGHT - 1, pixel_y_for_func_val))
else:
# Si no hay valor de función, dejar el cursor en el centro vertical
cursor_y_pixel = HEIGHT // 2
time.sleep_ms(200) # Debounce para ENTER
if not is_cursor_mode:
# --- MODO ZOOM (Joystick controla rangos X e Y) ---
x_adc_val = joy_x_obj.read_u16()
y_adc_val = joy_y_obj.read_u16()
if x_adc_val < (32768 - JOY_DEADZONE): # Joystick hacia la izquierda (Zoom OUT horizontal)
x_range_val = max(1.0, x_range_val * (1 + ZOOM_FACTOR))
elif x_adc_val > (32768 + JOY_DEADZONE): # Joystick hacia la derecha (Zoom IN horizontal)
x_range_val = max(1.0, x_range_val * (1 - ZOOM_FACTOR))
if y_adc_val < (32768 - JOY_DEADZONE): # Joystick hacia abajo (Zoom OUT vertical)
y_range_val = max(0.5, y_range_val * (1 + ZOOM_FACTOR))
elif y_adc_val > (32768 + JOY_DEADZONE): # Joystick hacia arriba (Zoom IN vertical)
y_range_val = max(0.5, y_range_val * (1 - ZOOM_FACTOR))
oled_obj.text("Zoom (JOY)", 0, 0)
oled_obj.text("X:{:.1f} Y:{:.1f}".format(x_range_val, y_range_val), 0, 10)
else: # is_cursor_mode == True
# --- MODO CURSOR (Joystick controla la posición del cursor) ---
x_adc_val = joy_x_obj.read_u16()
y_adc_val = joy_y_obj.read_u16()
if x_adc_val < (32768 - JOY_DEADZONE): # Mover cursor a la izquierda
cursor_x_pixel = max(0, cursor_x_pixel - MOVE_STEP)
elif x_adc_val > (32768 + JOY_DEADZONE): # Mover cursor a la derecha
cursor_x_pixel = min(WIDTH - 1, cursor_x_pixel + MOVE_STEP)
if y_adc_val < (32768 - JOY_DEADZONE): # Mover cursor hacia abajo (Y aumenta en pantalla hacia abajo)
cursor_y_pixel = min(HEIGHT - 1, cursor_y_pixel + MOVE_STEP)
elif y_adc_val > (32768 + JOY_DEADZONE): # Mover cursor hacia arriba (Y disminuye en pantalla hacia arriba)
cursor_y_pixel = max(0, cursor_y_pixel - MOVE_STEP)
# Mostrar el cursor
oled_obj.rect(cursor_x_pixel - 1, cursor_y_pixel - 1, 3, 3, 1) # Pequeño cuadrado para el cursor
# Calcular y mostrar los valores de X e Y en la posición del cursor
cursor_x_func = xmin + cursor_x_pixel * ((xmax - xmin) / WIDTH)
cursor_y_func = ymax - (cursor_y_pixel * ((ymax - ymin) / HEIGHT)) # Convertir píxel Y a valor real Y
oled_obj.text("Cursor (JOY)", 0, 0)
oled_obj.text("X:{:.2f}".format(cursor_x_func), 0, 10)
oled_obj.text("Y:{:.2f}".format(cursor_y_func), 0, 20) # Mostrar Y un poco más abajo para no superponer
# --- Dibujar ejes y la función ---
draw_axes(oled_obj, xmin, xmax, ymin, ymax)
dx_per_pixel = (xmax - xmin) / WIDTH # Valor real de X por cada pixel
# dy_per_pixel = (ymax - ymin) / HEIGHT # Valor real de Y por cada pixel (negativo porque Y es invertido)
try:
for pixel_x in range(WIDTH):
x = xmin + pixel_x * dx_per_pixel
try:
y = parser.parse(x_val=x)
except (ValueError, ZeroDivisionError, OverflowError):
y = float('nan') # Marca como indefinido si hay un error de dominio
if not math.isnan(y):
# Convertir valor y de la función a coordenada de píxel Y
# pixel_y = HEIGHT - (y - ymin) * HEIGHT / (ymax - ymin)
pixel_y = int(HEIGHT - ((y - ymin) / (ymax - ymin)) * HEIGHT)
if 0 <= pixel_y < HEIGHT:
oled_obj.pixel(pixel_x, pixel_y, 1)
except Exception as e:
oled_obj.fill(0)
oled_obj.text("Expr. Invalida!", 0, 10)
oled_obj.text(str(e)[:18], 0, 25)
oled_obj.show()
time.sleep(2)
return # Salir de la función de ploteo
oled_obj.show()
time.sleep_ms(50) # Pequeño retardo para suavizar animaciones y control
if check_button_func(btn_back, btns_last_states['back']):
return # Salir de la función de ploteo y volver al teclado
# --- MODO: Calculadora Gráfica con Teclado ---
KEYBOARD_CHARS = [
['1', '2', '3', '+', '-', 'sin'],
['4', '5', '6', '*', '/', 'cos'],
['7', '8', '9', '(', ')', 'tan'],
['.', '0', '=', 'x', 'DEL', 'exp'],
['CLR', 'GRAPH', 'sqrt', 'log', 'EXIT'] # QUIT renombrado a EXIT para consistencia
]
KEY_WIDTH_PX = 18 # Ancho máximo de los caracteres (aprox para 'sqrt')
KEY_HEIGHT_PX = 8
KEY_PADDING_X = 1
KEY_PADDING_Y = 1
kb_cursor_x = 0
kb_cursor_y = 0
expression_str = "" # La cadena de expresión que el usuario construye
def run_graph_calculator_mode_internal(oled_obj, btns_last_states, check_button_func, display_global_clock_func, joy_x_obj, joy_y_obj):
global kb_cursor_x, kb_cursor_y, expression_str, current_mode
if not oled_initialized:
print("OLED no inicializada. Saltando modo calculadora.")
return False
oled_obj.fill(0)
# Dibujar la expresión actual en la parte superior
oled_obj.text(expression_str, 0, 0)
# Coordenadas iniciales para dibujar el teclado (ajustado para dejar espacio para la expresión)
current_y_pos = 15 # Empezar un poco más abajo para la expresión
for r_idx, row in enumerate(KEYBOARD_CHARS):
current_x_pos = 0 # Reiniciar X para cada nueva fila
for c_idx, char in enumerate(row):
text_width = len(char) * 8 # Ancho del texto en píxeles (8 píxeles por carácter)
x_start_key = current_x_pos + KEY_PADDING_X
y_start_key = current_y_pos + KEY_PADDING_Y
if r_idx == kb_cursor_y and c_idx == kb_cursor_x:
# Dibujar un rectángulo alrededor del carácter seleccionado
oled_obj.rect(x_start_key - 1, y_start_key - 1, text_width + KEY_PADDING_X * 2, KEY_HEIGHT_PX + 2, 1)
oled_obj.text(char, x_start_key, y_start_key, 0) # Carácter en negro sobre fondo blanco
else:
oled_obj.text(char, x_start_key, y_start_key, 1) # Carácter en blanco sobre fondo negro
# Ajustar la posición X para el siguiente carácter/botón
# Considerar el ancho del texto real para mejor espaciado
current_x_pos += text_width + KEY_PADDING_X * 4 # Un poco más de espacio
current_y_pos += KEY_HEIGHT_PX + KEY_PADDING_Y * 2 # Mover a la siguiente fila del teclado
oled_obj.show()
# --- Manejo de la selección del teclado ---
if check_button_func(btn_up, btns_last_states['up']):
kb_cursor_y = (kb_cursor_y - 1 + len(KEYBOARD_CHARS)) % len(KEYBOARD_CHARS)
# Asegurarse de que el cursor X no exceda el ancho de la nueva fila
kb_cursor_x = min(kb_cursor_x, len(KEYBOARD_CHARS[kb_cursor_y]) - 1)
if check_button_func(btn_down, btns_last_states['down']):
kb_cursor_y = (kb_cursor_y + 1) % len(KEYBOARD_CHARS)
kb_cursor_x = min(kb_cursor_x, len(KEYBOARD_CHARS[kb_cursor_y]) - 1)
if check_button_func(btn_right, btns_last_states['right']):
kb_cursor_x = (kb_cursor_x + 1) % len(KEYBOARD_CHARS[kb_cursor_y])
if check_button_func(btn_left, btns_last_states['left']):
kb_cursor_x = (kb_cursor_x - 1 + len(KEYBOARD_CHARS[kb_cursor_y])) % len(KEYBOARD_CHARS[kb_cursor_y])
if check_button_func(btn_enter, btns_last_states['enter']): # Usar btn_enter para seleccionar
selected_char = KEYBOARD_CHARS[kb_cursor_y][kb_cursor_x]
if selected_char == 'DEL':
expression_str = expression_str[:-1]
elif selected_char == 'CLR':
expression_str = ""
elif selected_char == 'GRAPH':
if expression_str:
# Llamar a la función de ploteo con la expresión actual
plot_function(oled_obj, expression_str, joy_x_obj, joy_y_obj, display_global_clock_func, btns_last_states, check_button_func)
# Cuando se sale de plot_function (con BACK), se vuelve aquí.
else:
oled_obj.fill(0)
oled_obj.text("Ingrese funcion!", 0, 30)
oled_obj.show()
time.sleep(1)
elif selected_char == 'EXIT':
# EXIT desde el teclado de la calculadora.
# Simplemente salimos de la función y main_loop cambiará el modo a MENU.
return True
elif selected_char in ['sin', 'cos', 'tan', 'exp', 'log', 'sqrt']:
expression_str += selected_char + '('
else:
expression_str += selected_char
# Nuevo: Manejo del botón BACK para salir de la calculadora (desde el teclado)
if check_button_func(btn_back, btns_last_states['back']):
return True # Retorna True para indicar que se debe salir del modo
time.sleep_ms(20) # Pequeño retardo para control
return False # Seguir en el modo calculadora (teclado)
# La función wrapper original para el main loop ahora llama a la función interna
def run_graph_calculator_main_wrapper():
global current_mode
# run_graph_calculator_mode_internal devuelve True si se presiona BACK/EXIT
if run_graph_calculator_mode_internal(oled, button_last_states, check_button_press, display_global_clock, joy_x, joy_y):
current_mode = MODE_MENU
time.sleep_ms(50) # Pausa para evitar rebotes o cambios de modo demasiado rápidos
# --- MODO: Temperatura ---
def run_temperature_mode():
global current_mode
if not oled_initialized: return
oled.fill(0)
display_global_clock()
oled.text("Modo: Temperatura", 0, 10)
try:
dht_sensor.measure()
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()
if check_button_press(btn_back, button_last_states['back']): # Usar btn_back para salir
current_mode = MODE_MENU
time.sleep_ms(300)
# --- MODO: Calendario ---
# Variables para la fecha global (se actualiza solo al configurar)
global_day = 1
global_month = 1
global_year = 2025
# Variables temporales para el ajuste del calendario
current_setting_day = 1
current_setting_month = 1
current_setting_year = 2025
# Variable para rastrear qué parte de la fecha se está ajustando
# 0: Día, 1: Mes, 2: Año
date_setting_part = 0
# Estado del sub-modo: False = solo visualización, True = configuración
calendar_setting_active = False
def get_days_in_month(month, year):
if month == 2: # Febrero
if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0): # Año bisiesto
return 29
else:
return 28
elif month in [4, 6, 9, 11]: # Meses con 30 días
return 30
else: # Meses con 31 días
return 31
def show_calendar_mode():
global global_day, global_month, global_year, current_mode
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() # Muestra la hora global en la esquina
if not calendar_setting_active:
# --- MODO: VISUALIZACIÓN DEL CALENDARIO ---
oled.text("Calendario", 0, 10)
full_date_display = "{:02}/{:02}/{}".format(global_day, global_month, global_year)
oled.text(full_date_display, 20, 30) # Fecha principal del calendario
oled.text("ENTER: Ajustar", 0, 50)
oled.text("BACK: Menu", 0, 40)
oled.show()
if check_button_press(btn_enter, button_last_states['enter']):
# Activar el modo de configuración
calendar_setting_active = True
# Inicializar las variables de ajuste con la fecha global actual
current_setting_day = global_day
current_setting_month = global_month
current_setting_year = global_year
date_setting_part = 0 # Siempre empezar ajustando el día
time.sleep_ms(300) # Pausa para evitar doble entrada/rebote
if check_button_press(btn_back, button_last_states['back']):
current_mode = MODE_MENU # Volver al menú principal
time.sleep_ms(300)
else:
# --- MODO: CONFIGURACIÓN DEL CALENDARIO ---
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)
# Indicar qué parte se está ajustando con un subrayado
if date_setting_part == 0: # Día
oled.rect(20-1, 30+8+1, 2*8+2, 2, 1)
elif date_setting_part == 1: # Mes
oled.rect(20+3*8-1, 30+8+1, 2*8+2, 2, 1)
elif date_setting_part == 2: # Año
oled.rect(20+6*8-1, 30+8+1, 4*8+2, 2, 1)
oled.text("UP/DOWN: Ajustar", 0, 40)
oled.text("RIGHT: Siguiente", 0, 50)
oled.show()
# Lógica de ajuste con UP/DOWN/RIGHT
if check_button_press(btn_up, button_last_states['up']):
if date_setting_part == 0: # 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: # Mes
current_setting_month = (current_setting_month % 12) + 1
# Ajustar el día si el mes nuevo tiene menos días
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: # Año
current_setting_year = (current_setting_year % 2100) + 1 if current_setting_year < 2099 else 2000 # Limitar y ciclar el año
# Ajustar el día si el año nuevo (bisiesto/no bisiesto) afecta 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
time.sleep_ms(150)
if check_button_press(btn_down, button_last_states['down']):
if date_setting_part == 0: # 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
elif date_setting_part == 1: # Mes
current_setting_month = (current_setting_month - 2 + 12) % 12 + 1
# Ajustar el día si el mes nuevo tiene menos días
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: # Año
current_setting_year = (current_setting_year - 2001 + 100) % 100 + 2000 if current_setting_year > 2000 else 2099 # Limitar y ciclar el año
# Ajustar el día si el año nuevo (bisiesto/no bisiesto) afecta 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
time.sleep_ms(150)
if check_button_press(btn_right, button_last_states['right']):
date_setting_part = (date_setting_part + 1) % 3
time.sleep_ms(200)
if check_button_press(btn_enter, button_last_states['enter']):
# Aplicar la fecha configurada a las variables globales
global_day = current_setting_day
global_month = current_setting_month
global_year = current_setting_year
calendar_setting_active = False # Salir del modo de configuración
time.sleep_ms(300)
if check_button_press(btn_back, button_last_states['back']):
calendar_setting_active = False # Salir del modo de configuración sin guardar
time.sleep_ms(300)
def read_distance():
"""Mide distancia en cm con HC-SR04"""
trigger = Pin(TRIGGER_PIN, Pin.OUT)
echo = Pin(ECHO_PIN, Pin.IN)
trigger.low()
time.sleep_us(2)
trigger.high()
time.sleep_us(10)
trigger.low()
pulse_time = time_pulse_us(echo, 1, 30000) # Timeout para ~5m max
distance_cm = (pulse_time * 0.0343) / 2
return distance_cm if pulse_time > 0 else -1 # -1 si hay error
def toggle_oled(enable):
"""Activa/desactiva la pantalla según distancia"""
global oled_initialized
if not oled_initialized:
return
if enable:
oled.poweron()
oled.contrast(255) # Brillo máximo
else:
oled.poweroff()
# --- Bucle Principal del Sistema ---
def main_loop():
global current_mode, oled_initialized
# Inicialización del sensor HC-SR04
trigger = Pin(TRIGGER_PIN, Pin.OUT)
echo = Pin(ECHO_PIN, Pin.IN)
trigger.low()
# Variable para rastrear el estado de la OLED
oled_powered = True # Asume que inicia encendida
if oled_initialized:
oled.fill(0)
oled.show()
else:
print("OLED no inicializada. Modo sin pantalla")
run_presentation_mode()
current_mode = MODE_MENU
while True:
try:
# --- Lectura del sensor ultrasónico ---
trigger.high()
time.sleep_us(10)
trigger.low()
pulse_time = time_pulse_us(echo, 1, 30000)
distance = (pulse_time * 0.0343) / 2 if pulse_time > 0 else -1
# --- Control OLED mejorado (sin usar is_on()) ---
if oled_initialized:
if 0 < distance <= DISTANCE_THRESHOLD and oled_powered:
oled.poweroff()
oled_powered = False
elif (distance > DISTANCE_THRESHOLD or distance == -1) and not oled_powered:
oled.poweron()
oled_powered = True
oled.fill(0) # Limpiar pantalla al reactivar
oled.show()
# --- Ejecución de modos ---
if current_mode == MODE_MENU:
run_menu_mode()
elif current_mode == MODE_CLOCK:
show_clock_mode()
# ... (otros modos permanecen igual)
time.sleep_ms(100) # Pausa entre iteraciones
except Exception as e:
print(f"ERROR en main_loop: {e}")
if oled_initialized:
try:
oled.poweron() # Asegurar que la OLED se reactive en errores
oled.fill(0)
oled.text("Error. Reiniciando", 0, 0)
oled.show()
except:
pass
time.sleep(2)
current_mode = MODE_MENU
# --- Ejecución del Programa ---
if __name__ == "__main__":
main_loop()