# main.py - Código unificado de sistema embebido
import time
import math
import random
from machine import Pin, I2C, ADC, Timer, PWM
from ssd1306 import SSD1306_I2C
import framebuf
import network
import urequests
import ujson
import os
import sys
import gc
# from LibDecTarjeta import Board
import csv
import dht
print("Iniciando script...")
# --- CONFIGURACIÓN DE RED ---
SSID = 'HUAWEI P30 lite'
PASSWORD = '78312beb'
PHP_UPLOAD_URL = 'http://192.168.43.47:8012/upload_dht_data.php'
PHP_GET_TIME_URL = 'http://192.168.43.47:8012/get_time.php'
# --- 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 (ENTER/Seleccionar)
BTN_BACK_PIN = 16 # GP16 (ATRÁS)
# Pin para el Buzzer
BUZZER_PIN = 14 # GP14
# Pin para el sensor DHT22
DHT22_PIN = 19 # GP19
# --- 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
MODE_NODO_ID_CONFIG = 8
# --- 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}")
def connect_test():
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
oled.text("Conectando WiFi...", 0, 0)
oled.show()
wlan.connect(SSID, PASSWORD)
max_attempts = 20
attempts = 0
while not wlan.isconnected() and attempts < max_attempts:
oled.text(".", 0 + attempts * 5, 10) # Mostrar puntos
oled.show()
time.sleep(1)
attempts += 1
if wlan.isconnected():
ip_address = wlan.ifconfig()[0]
oled.fill(0)
oled.text("Conectado!", 0, 0)
oled.text(f"IP: {ip_address}", 0, 10)
oled.show()
print(f"Conectado! IP: {ip_address}")
return True
else:
oled.fill(0)
oled.text("Fallo de WiFi!", 0, 0)
oled.text("Check credenciales", 0, 10)
oled.show()
print("Fallo de conexión WiFi.")
return False
connect_test()
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 global (ahora se sincronizarán con el servidor) ---
global_h = 0
global_m = 0
global_s = 0
global_day = 1
global_month = 1
global_year = 2000
# Variable para saber si estamos conectados a internet
wifi_connected = False
# --- Variables Globales para la Configuración del NODO_ID ---
NODO_ID = "PicoW" # Valor por defecto. Se cargará desde flash o se configurará
NODO_ID_setting_active = False # Bandera para saber si estamos configurando el NODO_ID
current_input_char_index = 0 # Índice del carácter que estamos editando
current_input_string = bytearray([ord('_')] * 8) # Buffer inicial para 8 caracteres con '_'
# Usamos bytearray para que sea modificable y eficiente
max_nodo_id_length = 8 # Longitud máxima del NODO_ID
# Caracteres permitidos para la entrada (puedes expandir esta lista)
# El espacio al inicio permite un "borrado" visual al navegar
ALLOWED_CHARS = " ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_."
# --- Archivo de Configuración ---
CONFIG_FILE = "config.json" # Nombre del archivo para guardar la configuración
# --- 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(50000) # 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.
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
# --- Función para avanzar el reloj global localmente ---
def tick_global_reloj(timer):
global global_s, global_m, global_h, 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 (considerando días en el mes, años bisiestos)
# Lo mejor es que la sincronización de red maneje la fecha principal.
days_in_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if global_month == 2 and (global_year % 4 == 0 and (global_year % 100 != 0 or global_year % 400 == 0)):
days_in_month[2] = 29 # Año bisiesto
if global_day > days_in_month[global_month]:
global_day = 1
global_month += 1
if global_month > 12:
global_month = 1
global_year += 1
# print(f"Tick local: {global_h:02}:{global_m:02}:{global_s:02}") # Para depuración
# --- Inicialización del Timer para el reloj global ---
timer_reloj = Timer(-1) # Usamos un hardware timer
timer_reloj.init(period=1000, mode=Timer.PERIODIC, callback=tick_global_reloj)
print("Timer del reloj global inicializado.")
# --- FUNCIONES DE RED ---
# --- Funciones para Guardar/Cargar Configuración ---
def load_config_from_flash():
global NODO_ID, current_input_string
try:
with open(CONFIG_FILE, "r") as f:
config = ujson.load(f)
if "NODO_ID" in config:
NODO_ID = config["NODO_ID"]
# Cargar en el buffer de edición, rellenando con '_' si es más corto
temp_bytes = NODO_ID.encode('utf-8')
current_input_string = bytearray(temp_bytes)
if len(current_input_string) < max_nodo_id_length:
current_input_string.extend(bytearray([ord('_')] * (max_nodo_id_length - len(current_input_string))))
print(f"NODO_ID cargado desde flash: {NODO_ID}")
else:
# Si el archivo existe pero no tiene NODO_ID, usar valor por defecto
NODO_ID = "PicoW"
current_input_string = bytearray("PicoW___".encode('utf-8'))[:max_nodo_id_length] # Rellenar con guiones
print(f"config.json existe pero no tiene NODO_ID. Usando por defecto: {NODO_ID}")
except (OSError, ValueError) as e:
print(f"No se pudo cargar {CONFIG_FILE} o está vacío/corrupto ({e}). Usando NODO_ID por defecto: {NODO_ID}")
# Asegurarse de que current_input_string siempre tenga el tamaño correcto y esté inicializado
current_input_string = bytearray("PicoW___".encode('utf-8'))[:max_nodo_id_length] # Inicializar con el valor por defecto
# Ajustar si el default NODO_ID es más corto que max_nodo_id_length
if len(current_input_string) < max_nodo_id_length:
current_input_string.extend(bytearray([ord('_')] * (max_nodo_id_length - len(current_input_string))))
def save_config_to_flash(nodo_id_value):
config = {"NODO_ID": nodo_id_value}
try:
with open(CONFIG_FILE, "w") as f:
ujson.dump(config, f)
print(f"NODO_ID '{nodo_id_value}' guardado en flash.")
except Exception as e:
print(f"Error al guardar {CONFIG_FILE}: {e}")
def connect_wifi():
global wifi_connected
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
if not wlan.isconnected():
print('Conectando al WiFi...')
if oled_initialized:
oled.fill(0)
oled.text("Conectando WiFi", 0, 0) # Feedback visual
oled.show()
wlan.connect(SSID, PASSWORD)
retries = 0
while not wlan.isconnected() and retries < 20: # Intentar 20 veces
time.sleep(0.5)
if oled_initialized:
oled.text(".", 80 + retries * 5, 0) # Puntos de progreso
oled.show()
retries += 1
if wlan.isconnected():
print('Conectado, IP:', wlan.ifconfig()[0])
wifi_connected = True
return wlan.ifconfig()[0]
else:
print('No se pudo conectar al WiFi.')
if oled_initialized:
oled.fill(0)
oled.text("WiFi Fallo!", 0, 0)
oled.show()
wifi_connected = False
return None
wifi_connected = True # Si ya estaba conectado
return wlan.ifconfig()[0] # Si ya estaba conectado, devuelve la IP
def sync_time_from_server():
global global_h, global_m, global_s, global_day, global_month, global_year
if not wifi_connected:
print("No hay WiFi para sincronizar hora.")
return False
print("\n--- Sincronizando hora del servidor ---")
print(f"URL: {PHP_GET_TIME_URL}")
if oled_initialized:
oled.fill(0)
oled.text("Sincronizando...", 0, 0)
oled.show()
try:
response = urequests.get(PHP_GET_TIME_URL)
print(f"Respuesta recibida. Estado HTTP: {response.status_code}")
if response.status_code == 200: # Código 200 significa OK
data = ujson.loads(response.text)
print(f"JSON recibido: {data}")
response.close() # Siempre cerrar la respuesta
if data.get("estado") == "ok":
global_year = data.get("year")
global_month = data.get("month")
global_day = data.get("day")
global_h = data.get("hour")
global_m = data.get("minute")
global_s = data.get("second")
print(f"Hora sincronizada OK: {global_day:02}/{global_month:02}/{global_year} {global_h:02}:{global_m:02}:{global_s:02}")
if oled_initialized:
oled.text("Sinc OK!", 0, 10)
oled.show()
time.sleep_ms(500)
return True
else:
print("Error del servidor (JSON):", data.get("mensaje", "Desconocido"))
if oled_initialized:
oled.text("Sinc FALLO (JSON)!", 0, 10)
oled.show()
time.sleep_ms(500)
return False
else:
# Si el estado no es 200, algo salió mal a nivel HTTP
print(f"Error HTTP. Código de estado: {response.status_code}")
print(f"Contenido de la respuesta: {response.text}")
response.close()
if oled_initialized:
oled.text(f"Sinc FALLO HTTP {response.status_code}!", 0, 10)
oled.show()
time.sleep_ms(500)
return False
except OSError as e:
# Error de red, DNS, conexión rechazada, etc.
print(f"Error de red (OSError) al sincronizar hora: {e}")
if oled_initialized:
oled.text("RED ERROR (OS)!", 0, 10)
oled.show()
time.sleep_ms(500)
return False
except ValueError as e:
# Error al parsear JSON
print(f"Error al parsear JSON de respuesta (ValueError): {e}")
if oled_initialized:
oled.text("JSON ERROR!", 0, 10)
oled.show()
time.sleep_ms(500)
return False
except Exception as e:
# Cualquier otro error inesperado
print(f"Error inesperado al sincronizar hora: {e}")
if oled_initialized:
oled.text("Sinc ERROR!", 0, 10)
oled.show()
time.sleep_ms(500)
return False
def upload_dht_data(temp, hum):
global wifi_connected, NODO_ID
if not wifi_connected:
print("No hay WiFi para subir datos DHT22.")
return False
data = {
"Nodo_ID": NODO_ID,
"temperatura": temp,
"humedad": hum
}
json_data = ujson.dumps(data)
print("\n--- Subiendo datos DHT22 ---")
print(f"URL: {PHP_UPLOAD_URL}")
print(f"Datos a enviar: {json_data}")
try:
response = urequests.post(PHP_UPLOAD_URL, data=json_data, headers={"Content-Type": "application/json"})
print(f"Respuesta recibida. Estado HTTP: {response.status_code}")
if response.status_code == 200:
server_response = ujson.loads(response.text)
print(f"JSON recibido: {server_response}")
response.close()
if server_response.get("estado") == "ok":
print("Datos DHT22 subidos OK.")
return True
else:
print("Error del servidor (JSON) al subir DHT22:", server_response.get("mensaje", "Desconocido"))
return False
else:
print(f"Error HTTP. Código de estado: {response.status_code}")
print(f"Contenido de la respuesta: {response.text}")
response.close()
return False
except OSError as e:
print(f"Error de red (OSError) al subir datos DHT22: {e}")
return False
except ValueError as e:
print(f"Error al parsear JSON de respuesta (ValueError) al subir DHT22: {e}")
return False
except Exception as e:
print(f"Error inesperado al subir datos DHT22: {e}")
return False
# --- MODO: Pantalla de Presentación ---
def run_presentation_mode():
if not oled_initialized:
print("OLED no inicializada. Saltando modo presentación.")
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: Menú Principal con Iconos ---
# --- Íconos de Menú (32x32 píxeles) ---
# Icono de Reloj Digital
icon_clock_bytes = bytearray([
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xf0, 0x0f,
0xff, 0xff, 0xf7, 0xef, 0xff, 0xff, 0xf7, 0xef,
0xff, 0xff, 0xf7, 0xef, 0xc0, 0x00, 0x00, 0x03,
0x9f, 0xff, 0xff, 0xf9, 0x3f, 0xff, 0xff, 0xfc,
0x7f, 0xff, 0xff, 0xfe, 0x76, 0x07, 0xe0, 0x82,
0x77, 0xf7, 0xee, 0xba, 0x77, 0xf6, 0x6e, 0xba,
0x77, 0xf6, 0x6e, 0xba, 0x77, 0xf7, 0xee, 0xba,
0x77, 0xf7, 0xee, 0xba, 0x77, 0xf7, 0xee, 0xba,
0x76, 0x07, 0xee, 0xba, 0x76, 0xff, 0xee, 0xba,
0x76, 0xff, 0xee, 0xba, 0x76, 0xff, 0xee, 0xba,
0x76, 0xfe, 0x6e, 0xba, 0x76, 0xfe, 0x6e, 0xba,
0x76, 0xff, 0xee, 0xba, 0x76, 0x07, 0xe0, 0x82,
0x7f, 0xff, 0xff, 0xfe, 0x7f, 0xff, 0xff, 0xfe,
0x3f, 0xff, 0xff, 0xfc, 0x9f, 0xff, 0xff, 0xf9,
0xc0, 0x00, 0x00, 0x03, 0xef, 0xbf, 0xfd, 0xf7,
0xef, 0xbf, 0xfd, 0xf7, 0xe0, 0x3f, 0xfc, 0x07
])
icon_clock_fb = framebuf.FrameBuffer(icon_clock_bytes, 32, 32, framebuf.MONO_HLSB)
# Icono de Cronómetro
icon_chronometer_bytes = bytearray([
0xff, 0xf8, 0x3f, 0xff, 0xff, 0xf8, 0x3f, 0xff,
0xff, 0xf8, 0x3f, 0xff, 0xff, 0xf8, 0x3f, 0xff,
0xf8, 0x7e, 0xfe, 0x1f, 0xf8, 0x7e, 0xfe, 0x1f,
0xe0, 0x00, 0x00, 0x07, 0xc0, 0x00, 0x00, 0x03,
0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01,
0x0f, 0xff, 0xff, 0xf0, 0x0f, 0xff, 0xfd, 0xf0,
0x0f, 0xff, 0xfb, 0xf0, 0x0f, 0xff, 0xf7, 0xf0,
0x0f, 0xff, 0xef, 0xf0, 0x0f, 0xff, 0xdf, 0xf0,
0x0f, 0xff, 0xbf, 0xf0, 0x0f, 0xfc, 0x7f, 0xf0,
0x0f, 0xfc, 0x7f, 0xf0, 0x0f, 0xfc, 0x7f, 0xf0,
0x0f, 0xfb, 0xff, 0xf0, 0x0f, 0xf7, 0xff, 0xf0,
0x0f, 0xef, 0xff, 0xf0, 0x0f, 0xdf, 0xff, 0xf0,
0x0f, 0xff, 0xff, 0xf0, 0x0f, 0xff, 0xff, 0xf0,
0x0f, 0xff, 0xff, 0xf0, 0x8f, 0xff, 0xff, 0xf1,
0x80, 0x00, 0x00, 0x01, 0xc0, 0x00, 0x00, 0x03,
0xe0, 0x00, 0x00, 0x07, 0xff, 0xff, 0xff, 0xff
])
icon_chronometer_fb = framebuf.FrameBuffer(icon_chronometer_bytes, 32, 32, framebuf.MONO_HLSB)
# Icono de Reloj Analógico
icon_analog_clock_bytes = bytearray([
0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xfd,
0xff, 0xff, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xf7,
0xff, 0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xdf,
0xff, 0xff, 0xff, 0xbf, 0xff, 0xff, 0xff, 0x7f,
0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xfd, 0xff,
0xff, 0xff, 0xfb, 0xff, 0xff, 0xff, 0xf7, 0xff,
0xff, 0xff, 0xef, 0xff, 0xff, 0xff, 0xdf, 0xff,
0xff, 0xfc, 0x3f, 0xff, 0xff, 0xfc, 0x3f, 0xff,
0xff, 0xfc, 0x3f, 0xff, 0xff, 0xfc, 0x3f, 0xff,
0xff, 0xfb, 0xff, 0xff, 0xff, 0xf7, 0xff, 0xff,
0xff, 0xef, 0xff, 0xff, 0xff, 0xdf, 0xff, 0xff,
0xff, 0xbf, 0xff, 0xff, 0xff, 0x7f, 0xff, 0xff,
0xfe, 0xff, 0xff, 0xff, 0xfd, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
])
icon_analog_clock_fb = framebuf.FrameBuffer(icon_analog_clock_bytes, 32, 32, framebuf.MONO_HLSB)
# Icono de Sokoban
icon_sokoban_bytes = bytearray([
0x00, 0x00, 0x00, 0x00, 0x7f, 0xff, 0xff, 0xfe,
0x5f, 0xff, 0xff, 0xfa, 0x7f, 0xff, 0xff, 0xfe,
0x00, 0x00, 0x00, 0x00, 0xbf, 0xff, 0xff, 0xfd,
0xbf, 0xff, 0xff, 0xfd, 0x80, 0x00, 0x00, 0x01,
0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01,
0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01,
0x80, 0x00, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xfd,
0xbf, 0xff, 0xff, 0xfd, 0xbf, 0xff, 0xff, 0xfd,
0xbf, 0xff, 0xff, 0xfd, 0xbf, 0xff, 0xff, 0xfd,
0xbf, 0xff, 0xff, 0xfd, 0x80, 0x00, 0x00, 0x01,
0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01,
0x80, 0x00, 0x00, 0x01, 0x80, 0x00, 0x00, 0x01,
0x80, 0x00, 0x00, 0x01, 0xbf, 0xff, 0xff, 0xfd,
0xbf, 0xff, 0xff, 0xfd, 0x00, 0x00, 0x00, 0x00,
0x7f, 0xff, 0xff, 0xfe, 0x5f, 0xff, 0xff, 0xfa,
0x7f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00
])
icon_sokoban_fb = framebuf.FrameBuffer(icon_sokoban_bytes, 32, 32, framebuf.MONO_HLSB)
# Icono de Calculadora
icon_calculator_bytes = bytearray([
0x80, 0x00, 0x00, 0x01, 0x3f, 0xff, 0xff, 0xfc,
0x60, 0x00, 0x00, 0x06, 0x5f, 0xff, 0xff, 0xfa,
0x5f, 0xff, 0xff, 0xfa, 0x5f, 0xff, 0xff, 0xfa,
0x5f, 0xff, 0xff, 0xfa, 0x5f, 0xff, 0xff, 0xfa,
0x5f, 0xff, 0xff, 0xfa, 0x60, 0x00, 0x00, 0x06,
0x7f, 0xff, 0xff, 0xfe, 0x63, 0x18, 0xc6, 0x06,
0x63, 0x18, 0xc6, 0x06, 0x63, 0x18, 0xc6, 0x06,
0x7f, 0xff, 0xff, 0xfe, 0x63, 0x18, 0xc6, 0x06,
0x63, 0x18, 0xc6, 0x06, 0x63, 0x18, 0xc6, 0x06,
0x7f, 0xff, 0xff, 0xfe, 0x63, 0x18, 0xc6, 0x06,
0x63, 0x18, 0xc6, 0x06, 0x63, 0x18, 0xc6, 0x06,
0x7f, 0xff, 0xff, 0xfe, 0x63, 0x18, 0xc6, 0x06,
0x63, 0x18, 0xc6, 0x06, 0x63, 0x18, 0xc6, 0x06,
0x3f, 0xff, 0xff, 0xfc, 0x80, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
])
icon_calculator_fb = framebuf.FrameBuffer(icon_calculator_bytes, 32, 32, framebuf.MONO_HLSB)
# Icono de Temperatura
icon_temperature_bytes = bytearray([
0xff, 0xfc, 0x3f, 0xf6, 0xff, 0xf9, 0x9f, 0xf6,
0xff, 0xfb, 0xdf, 0xf0, 0xff, 0xfb, 0x9f, 0xf6,
0xff, 0xfb, 0xdf, 0xf6, 0xff, 0xfb, 0x9f, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xff, 0xfb, 0x9f, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xff, 0xfb, 0x9f, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xff, 0xfb, 0x9f, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xff, 0xfb, 0x9f, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xff, 0xfb, 0x9f, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xff, 0xfb, 0x9f, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xff, 0xfb, 0x9f, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xff, 0xfb, 0x9f, 0xff,
0xff, 0xf3, 0xcf, 0xff, 0xff, 0xf7, 0xef, 0xff,
0xff, 0xec, 0x37, 0xff, 0xff, 0xd8, 0x1b, 0xff,
0xf9, 0xd0, 0x0b, 0xff, 0x89, 0xd0, 0x0b, 0xff,
0x7f, 0xd8, 0x1b, 0xff, 0x7f, 0xec, 0x37, 0xff,
0x7f, 0xf7, 0xef, 0xff, 0x8f, 0xf0, 0x0f, 0xff
])
icon_temperature_fb = framebuf.FrameBuffer(icon_temperature_bytes, 32, 32, framebuf.MONO_HLSB)
# Icono de Calendario
icon_calendar_bytes = bytearray([
0xf9, 0xff, 0xff, 0x9f, 0xf9, 0xff, 0xff, 0x9f,
0x00, 0x00, 0x00, 0x00, 0x79, 0xff, 0xff, 0x9e,
0x79, 0xff, 0xff, 0x9e, 0x79, 0xff, 0xff, 0x9e,
0x7f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00,
0x7f, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x00,
0xbf, 0xff, 0xff, 0xfd, 0xbf, 0xff, 0xff, 0xfd,
0xb0, 0x07, 0x9f, 0x3d, 0xbf, 0xe7, 0x9f, 0x3d,
0xbf, 0xe7, 0x9f, 0x3d, 0xbf, 0xe7, 0x9f, 0x3d,
0xbf, 0xe7, 0x9f, 0x3d, 0xbf, 0xe7, 0x9f, 0x3d,
0xbf, 0xe7, 0x9f, 0x3d, 0xb0, 0x07, 0x80, 0x1d,
0xb3, 0xff, 0xff, 0x3d, 0xb3, 0xff, 0xff, 0x3d,
0xb3, 0xff, 0xff, 0x3d, 0xb3, 0xff, 0xff, 0x3d,
0xb3, 0xff, 0xff, 0x3d, 0xb3, 0xff, 0xff, 0x3d,
0xb3, 0xff, 0xff, 0x3d, 0xb0, 0x07, 0xff, 0x3d,
0xbf, 0xff, 0xff, 0xfd, 0xbf, 0xff, 0xff, 0xfd,
0xbf, 0xff, 0xff, 0xfd, 0x80, 0x00, 0x00, 0x01
])
icon_calendar_fb = framebuf.FrameBuffer(icon_calendar_bytes, 32, 32, framebuf.MONO_HLSB)
# Icono de Configuración
icon_settings_bytes = bytearray([
0xff, 0xfc, 0x3f, 0xff, 0xff, 0xfb, 0xdf, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xfe, 0xfb, 0xdf, 0x7f,
0xfd, 0x7b, 0xde, 0xbf, 0xfb, 0xb7, 0xed, 0xdf,
0xf7, 0xcf, 0xf3, 0xef, 0xef, 0xff, 0xff, 0xf7,
0xf7, 0xff, 0xff, 0xef, 0xfb, 0xff, 0xff, 0xdf,
0xfd, 0xf0, 0x0f, 0xbf, 0xfd, 0xf7, 0xef, 0xbf,
0xfb, 0xcf, 0xf3, 0xdf, 0x87, 0xdf, 0xfb, 0xe1,
0x7f, 0xdf, 0xfb, 0xfe, 0x7f, 0xdf, 0xfb, 0xfe,
0x7f, 0xdf, 0xfb, 0xfe, 0x7f, 0xdf, 0xfb, 0xfe,
0x87, 0xdf, 0xfb, 0xe1, 0xfb, 0xcf, 0xf3, 0xdf,
0xfd, 0xf7, 0xef, 0xbf, 0xfd, 0xf0, 0x0f, 0xbf,
0xfb, 0xff, 0xff, 0xdf, 0xf7, 0xff, 0xff, 0xef,
0xef, 0xff, 0xff, 0xf7, 0xf7, 0xcf, 0xf3, 0xef,
0xfb, 0xb7, 0xed, 0xdf, 0xfd, 0x7b, 0xde, 0xbf,
0xfe, 0xfb, 0xdf, 0x7f, 0xff, 0xfb, 0xdf, 0xff,
0xff, 0xfb, 0xdf, 0xff, 0xff, 0xfc, 0x3f, 0xff
])
icon_settings_fb = framebuf.FrameBuffer(icon_settings_bytes, 32, 32, framebuf.MONO_HLSB)
# --- Definición de los Elementos del Menú ---
menu_items = [
("Reloj Digital", icon_clock_fb),
("Cronometro", icon_chronometer_fb),
("Reloj Analogico", icon_analog_clock_fb),
("Sokoban Game", icon_sokoban_fb),
("Calculadora", icon_calculator_fb),
("Temperatura", icon_temperature_fb),
("Calendario", icon_calendar_fb),
("Config. Nodo", icon_settings_fb)
]
current_menu_selection = 0 # Asegúrate de que esta variable sea global o esté accesible
def run_menu_mode():
global current_mode, current_menu_selection
# Asumiendo que oled_initialized, oled, y las funciones de botón están definidas
if not oled_initialized:
print("OLED no inicializada. Saltando modo menú.")
return
oled.fill(0) # Limpiar la pantalla
menu_text, menu_icon_fb = menu_items[current_menu_selection]
# Calcular la posición para centrar el ícono y el texto (ADAPTADO A 32x32)
ICON_SIZE = 32 # Definimos el tamaño de los iconos
icon_x = (WIDTH // 2) - (ICON_SIZE // 2) # Centrar icono horizontalmente
icon_y = (HEIGHT // 2) - ICON_SIZE - 5 # Posicionar icono ligeramente arriba del centro
# El texto ahora siempre se dibuja en una sola línea
text_width_pixels = len(menu_text) * 8 # Ancho aproximado del texto (8 píxeles por caracter)
text_x = (WIDTH // 2) - (text_width_pixels // 2) # Centrar texto horizontalmente
text_y = (HEIGHT // 2) + 5 # Posicionar texto ligeramente debajo del centro
# Dibujar el ícono y el texto
oled.blit(menu_icon_fb, icon_x, icon_y, 0) # Ícono en blanco
oled.text(menu_text, text_x, text_y, 1) # Texto en blanco
oled.show()
# Lógica de navegación con botones
if check_button_press(btn_up, button_last_states['up']):
current_menu_selection = (current_menu_selection - 1 + len(menu_items)) % len(menu_items)
time.sleep_ms(150)
if check_button_press(btn_down, button_last_states['down']):
current_menu_selection = (current_menu_selection + 1) % len(menu_items)
time.sleep_ms(150)
if check_button_press(btn_enter, button_last_states['enter']):
target_mode = current_menu_selection + MODE_CLOCK
current_mode = target_mode
time.sleep_ms(300)
# El botón BACK en el menú no tiene acción, solo se usa para salir de los sub-modos.
# --- 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
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, wifi_connected
if not oled_initialized: return
oled.fill(0)
display_global_clock() # Muestra la hora global sincronizada
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)
# *** AQUI SE SUBEN LOS DATOS ***
if wifi_connected:
oled.text("Subiendo...", 0, 55) # Feedback visual
oled.show()
if upload_dht_data(temperatura, humedad):
oled.text("Subido OK!", 0, 55) # Feedback visual
else:
oled.text("Subida FALLO!", 0, 55)
else:
oled.text("No hay WiFi", 0, 55)
except OSError as e:
oled.text("Error DHT22", 0, 30)
oled.text(str(e)[:15], 0, 45)
oled.text("No subido", 0, 55) # Si falla la lectura, no se sube
oled.show() # Actualizar OLED después de los posibles mensajes de subida
# Permitir una pequeña pausa para ver el mensaje de subida/error antes de leer botones
time.sleep_ms(200)
if check_button_press(btn_back, button_last_states['back']):
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)
# --- MODO: Configuración de NODO_ID ---
# --- MODO: Configuración de NODO_ID (VERSIÓN CON MÁS DEPURACIÓN) ---
def run_nodo_id_config_mode():
global current_mode, current_input_char_index, current_input_string, NODO_ID
if not oled_initialized:
print("run_nodo_id_config_mode: OLED no inicializada. Saliendo.")
current_mode = MODE_MENU # Volver al menú si no hay OLED
return
# Limpiar y dibujar elementos estáticos del OLED
oled.fill(0)
display_global_clock() # Mostrar la hora global (ahora que sabemos que la red funciona)
oled.text("Config. Nodo ID", 0, 10)
# --- Manejo y Visualización del String de Entrada ---
try:
# Decodifica el bytearray a string, reemplazando nulos con guiones bajos para visualización
display_string = current_input_string.decode('utf-8').replace('\x00', '_')
oled.text(f"ID: {display_string}", 10, 30)
print(f"Modo NODO ID: Mostrando string: '{display_string}'")
# Resaltar el carácter actual (subrayado)
char_start_x = 10 + (len("ID: ") * 8) # Posición X de donde empieza el texto del ID
char_x = char_start_x + (current_input_char_index * 8) # Posición X del carácter actual
oled.rect(char_x, 30 + 8 + 1, 8, 2, 1) # Dibujar el subrayado
print(f"Modo NODO ID: Caracter editando en indice {current_input_char_index}")
except Exception as e:
# Captura errores al dibujar o manipular el string
print(f"Modo NODO ID ERROR VISUALIZACION: {e}")
sys.print_exception(e) # Imprime el traceback completo
oled.text("ERROR DISPLAY", 0, 40)
oled.show()
time.sleep(1)
current_mode = MODE_MENU # Vuelve al menú en caso de error crítico de visualización
return
oled.text("UP/DWN: Char", 0, 40) # Ahora en dos líneas para mayor claridad
oled.text("RGT: Siguiente", 0, 50)
oled.text("ENTER: Guardar", 0, 58) # Ahora en una línea separada
oled.text("BACK: Salir", 0, 58) # Cambiado a "Salir" para evitar ambigüedad con borrar
oled.show()
# --- Lógica de los botones para la entrada de texto ---
if check_button_press(btn_up, button_last_states['up']):
print("Modo NODO ID: Boton UP presionado")
# Ciclar a través de los caracteres permitidos para el carácter actual
current_char_val = current_input_string[current_input_char_index]
try:
# Encuentra el índice del carácter actual en ALLOWED_CHARS
char_idx = ALLOWED_CHARS.index(chr(current_char_val))
except ValueError:
# Si el carácter actual no está en la lista (ej. '_'), empezar desde el principio
char_idx = 0
next_char_idx = (char_idx + 1) % len(ALLOWED_CHARS)
current_input_string[current_input_char_index] = ord(ALLOWED_CHARS[next_char_idx])
print(f"Modo NODO ID: Caracter en pos {current_input_char_index} cambiado a '{ALLOWED_CHARS[next_char_idx]}'")
time.sleep_ms(150) # Pequeño retardo anti-rebote y para ver el cambio
if check_button_press(btn_down, button_last_states['down']):
print("Modo NODO ID: Boton DOWN presionado")
current_char_val = current_input_string[current_input_char_index]
try:
char_idx = ALLOWED_CHARS.index(chr(current_char_val))
except ValueError:
char_idx = 0
prev_char_idx = (char_idx - 1 + len(ALLOWED_CHARS)) % len(ALLOWED_CHARS)
current_input_string[current_input_char_index] = ord(ALLOWED_CHARS[prev_char_idx])
print(f"Modo NODO ID: Caracter en pos {current_input_char_index} cambiado a '{ALLOWED_CHARS[prev_char_idx]}'")
time.sleep_ms(150)
if check_button_press(btn_right, button_last_states['right']):
print("Modo NODO ID: Boton RIGHT presionado")
current_input_char_index = (current_input_char_index + 1) % max_nodo_id_length
print(f"Modo NODO ID: Cursor movido a indice {current_input_char_index}")
time.sleep_ms(200) # Retardo un poco más largo para la navegación
if check_button_press(btn_back, button_last_states['back']):
print("Modo NODO ID: Boton BACK presionado. Saliendo al menu.")
# Ahora el botón BACK siempre sale del modo, no borra caracteres
current_mode = MODE_MENU
time.sleep_ms(300) # Retardo para volver al menú
if check_button_press(btn_enter, button_last_states['enter']):
print("Modo NODO ID: Boton ENTER presionado. Intentando guardar...")
temp_nodo_id = current_input_string.decode('utf-8').replace('_', '').strip()
if len(temp_nodo_id) > 0:
NODO_ID = temp_nodo_id
print(f"Modo NODO ID: Nuevo NODO_ID a guardar: '{NODO_ID}'")
try:
save_config_to_flash(NODO_ID) # Guarda en la flash
if oled_initialized:
oled.fill(0)
oled.text("ID GUARDADO!", 0, 0) # Pequeño feedback
oled.show()
print("Modo NODO ID: NODO_ID guardado con exito.")
time.sleep_ms(500)
except Exception as e:
print(f"Modo NODO ID ERROR AL GUARDAR: {e}")
sys.print_exception(e)
if oled_initialized:
oled.fill(0)
oled.text("ERROR GUARDAR!", 0, 0)
oled.text(str(e)[:15], 0, 10)
oled.show()
time.sleep_ms(1000)
else:
print("Modo NODO ID: NODO_ID vacio, no se guarda.")
if oled_initialized:
oled.fill(0)
oled.text("ID VACIO!", 0, 0)
oled.show()
time.sleep_ms(500)
current_mode = MODE_MENU # Volver al menú después de intentar guardar
time.sleep_ms(300)
# --- LISTA DE ITEMS DEL MENÚ ---
# Asegúrate de que icon_settings_fb esté definido antes de esta lista
menu_items = [
("Reloj Digital", icon_clock_fb),
("Cronometro", icon_chronometer_fb),
("Reloj Analogico", icon_analog_clock_fb),
("Sokoban Game", icon_sokoban_fb),
("Calculadora", icon_calculator_fb),
("Temperatura", icon_temperature_fb),
("Calendario", icon_calendar_fb),
("Config. Nodo", icon_settings_fb) # <-- ¡NUEVA ENTRADA!
]
# --- Bucle Principal del Sistema ---
last_sync_time = 0 # Para controlar la frecuencia de sincronización de la hora
SYNC_INTERVAL_MS = 300000 # Sincronizar cada 5 minutos (300,000 ms)
def main_loop():
global current_mode, last_sync_time, wifi_connected, NODO_ID
if oled_initialized:
oled.fill(0)
oled.show()
else:
print("Advertencia: OLED no inicializada. El sistema puede no mostrar nada.")
time.sleep_ms(100)
# Cargar configuración (NODO_ID) al inicio
load_config_from_flash() # <-- ¡NUEVO!
# Conectar al WiFi al inicio
wifi_connected = connect_wifi()
if wifi_connected:
# Sincronizar hora inicial
sync_time_from_server()
last_sync_time = time.ticks_ms()
else:
print("No se pudo conectar al WiFi. Usando hora local (no sincronizada).")
if oled_initialized:
run_presentation_mode() # Tu modo de presentación si lo tienes
current_mode = MODE_MENU
print("Iniciando bucle principal...")
while True:
# Sincronizar la hora periódicamente si hay WiFi y ha pasado el tiempo
if wifi_connected and (time.ticks_ms() - last_sync_time > SYNC_INTERVAL_MS):
sync_time_from_server()
last_sync_time = time.ticks_ms()
try:
if current_mode == MODE_MENU:
run_menu_mode()
elif current_mode == MODE_CLOCK:
show_clock_mode()
elif current_mode == MODE_CHRONOMETER:
show_chronometer_mode()
elif current_mode == MODE_ANALOG_CLOCK:
show_analog_clock_mode()
elif current_mode == MODE_SOKOBAN:
sokoban_game = SokobanGame(oled, display_global_clock, button_last_states, check_button_press, oled_initialized)
sokoban_game.run()
elif current_mode == MODE_GRAPH_CALCULATOR:
run_graph_calculator_main_wrapper()
elif current_mode == MODE_TEMPERATURE:
run_temperature_mode()
elif current_mode == MODE_CALENDAR:
show_calendar_mode()
elif current_mode == MODE_NODO_ID_CONFIG:
run_nodo_id_config_mode()
except Exception as e:
print(f"ERROR: Fallo en el bucle del modo {current_mode}. Mensaje: {e}")
if oled_initialized:
oled.fill(0)
oled.text("ERROR EN MODO", 0, 10)
oled.text(str(current_mode), 0, 20)
oled.text(str(e)[:15], 0, 30)
oled.text("Volviendo a Menu", 0, 45)
oled.show()
time.sleep(3)
current_mode = MODE_MENU
time.sleep_ms(20) # Pequeña pausa para el bucle principal
# --- Bucle Principal del Sistema ---
last_sync_time = 0 # Para controlar la frecuencia de sincronización de la hora
SYNC_INTERVAL_MS = 300000 # Sincronizar cada 5 minutos (300,000 ms)
def main_loop():
global current_mode, last_sync_time, wifi_connected, NODO_ID
if not oled_initialized:
print("Advertencia: OLED no inicializada. Intentando inicializar de nuevo...")
initialize_oled() # Intenta inicializar de nuevo por si acaso
if not oled_initialized:
print("ERROR FATAL: OLED no pudo ser inicializada. Deteniendo la ejecución del bucle principal.")
# Si el OLED no se puede inicializar, no tiene sentido continuar
while True:
time.sleep(1) # Bucle infinito para evitar reinicios constantes si hay un fallo de hardware
oled.fill(0)
oled.show()
time.sleep_ms(100) # Pequeña pausa para asegurar la inicialización visual
print("Memoria libre antes de cargar config:", gc.mem_free())
# 1. Cargar configuración (NODO_ID) al inicio
load_config_from_flash()
print(f"NODO_ID inicial: {NODO_ID}") # Para confirmar que se carga
print("Memoria libre después de cargar config:", gc.mem_free())
print("Memoria libre antes de conectar WiFi:", gc.mem_free())
# 2. Conectar al WiFi al inicio
wifi_connected = connect_wifi()
if wifi_connected:
print("Conexión WiFi exitosa. Intentando sincronizar hora...")
# 3. Sincronizar hora inicial si hay WiFi
sync_time_from_server()
last_sync_time = time.ticks_ms() # Establecer el tiempo de la última sincronización
else:
print("No se pudo conectar al WiFi. Usando hora local (no sincronizada).")
# Considera mostrar un mensaje persistente en la OLED si no hay WiFi
if oled_initialized:
oled.fill(0)
oled.text("NO WIFI", 0, 0)
oled.text("Hora no sinc.", 0, 10)
oled.show()
time.sleep(2) # Dar tiempo para que el usuario vea el mensaje
print("Memoria libre después de intentar WiFi/sincro:", gc.mem_free())
# Ejecutar el modo de presentación (si lo tienes) o ir directo al menú
if oled_initialized:
run_presentation_mode() # Tu modo de presentación si lo tienes
current_mode = MODE_MENU # Iniciar en el modo menú
print("Iniciando bucle principal...")
while True:
gc.collect() # <--- ¡Liberar memoria en cada iteración del bucle principal!
print(f"Memoria libre en inicio de bucle: {gc.mem_free()}")
# Sincronizar la hora periódicamente si hay WiFi y ha pasado el tiempo
if wifi_connected and (time.ticks_ms() - last_sync_time > SYNC_INTERVAL_MS):
print("Tiempo para resincronizar hora...")
sync_time_from_server()
last_sync_time = time.ticks_ms()
try:
# Tu lógica existente para manejar los diferentes modos
# Añado prints para saber en qué modo se cuelga
if current_mode == MODE_MENU:
print("Modo: MENU - Ejecutando run_menu_mode()")
run_menu_mode()
elif current_mode == MODE_CLOCK:
print("Modo: RELOJ - Ejecutando show_clock_mode()")
show_clock_mode()
elif current_mode == MODE_CHRONOMETER:
print("Modo: CRONOMETRO - Ejecutando show_chronometer_mode()")
show_chronometer_mode()
elif current_mode == MODE_ANALOG_CLOCK:
print("Modo: RELOJ ANALOGICO - Ejecutando show_analog_clock_mode()")
show_analog_clock_mode()
elif current_mode == MODE_SOKOBAN:
print("Modo: SOKOBAN - Inicializando y ejecutando SokobanGame()")
sokoban_game = SokobanGame(oled, display_global_clock, button_last_states, check_button_press, oled_initialized)
sokoban_game.run()
elif current_mode == MODE_GRAPH_CALCULATOR:
print("Modo: CALCULADORA - Ejecutando run_graph_calculator_main_wrapper()")
run_graph_calculator_main_wrapper()
elif current_mode == MODE_TEMPERATURE:
print("Modo: TEMPERATURA - Ejecutando run_temperature_mode()")
run_temperature_mode()
elif current_mode == MODE_CALENDAR:
print("Modo: CALENDARIO - Ejecutando show_calendar_mode()")
show_calendar_mode()
elif current_mode == MODE_NODO_ID_CONFIG:
print("Modo: NODO ID CONFIG - Ejecutando run_nodo_id_config_mode()")
run_nodo_id_config_mode()
except Exception as e:
# Captura cualquier error que ocurra dentro de los modos
print("\n--- ¡ERROR CAPTURADO EN EL MODO! ---")
sys.print_exception(e) # Esto imprimirá el traceback completo del error
print(f"Mensaje de error: {e}")
print(f"Modo actual al momento del error: {current_mode}")
if oled_initialized:
oled.fill(0)
oled.text("ERROR EN MODO", 0, 0)
oled.text(f"Modo: {current_mode}", 0, 10)
oled.text(f"Error: {str(e)[:20]}...", 0, 20) # Muestra una parte del mensaje de error
oled.text("Volviendo a Menu", 0, 45)
oled.show()
time.sleep(3) # Dar tiempo para leer el error
current_mode = MODE_MENU # Vuelve al menú para intentar recuperar el sistema
time.sleep_ms(20)
# --- Ejecución del Programa ---
if __name__ == "__main__":
main_loop()