# 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
# Importar las librerías y módulos de tu docente.
# Asegúrate de que estos archivos (LibDecTarjeta.py, csv.py, dht.py, ssd1306.py)
# estén en la raíz de tu Raspberry Pi Pico W junto con main.py
from LibDecTarjeta import Board # Se incluye por si necesitas verificar el tipo de placa
import csv # Necesario para Sokoban (niveles y scores)
import dht # Para el sensor DHT22
# --- 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
# --- 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 ---
try:
i2c = I2C(0, scl=Pin(SCL_PIN), sda=Pin(SDA_PIN), freq=400000)
oled = SSD1306_I2C(WIDTH, HEIGHT, i2c)
except Exception as e:
print("Error al inicializar OLED:", e)
# Considerar una forma de notificar al usuario si la pantalla falla
buzzer = PWM(Pin(BUZZER_PIN))
buzzer.freq(1000) # Frecuencia inicial para el buzzer
buzzer.duty_u16(0) # Apagado inicialmente
# 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) # Renombrado a btn_enter
btn_back = Pin(BTN_BACK_PIN, Pin.IN, Pin.PULL_UP) # Nuevo botón btn_back
# Inicialización de ADCs para el joystick
joy_x = ADC(JOY_X_PIN)
joy_y = ADC(JOY_Y_PIN)
# Inicialización del sensor DHT22
dht_sensor = dht.DHT22(Pin(DHT22_PIN))
# --- Variables Globales del Sistema ---
current_mode = MODE_PRESENTATION # Estado actual del sistema
# Variables para el reloj digital global (se actualiza cada segundo)
global_h = 0
global_m = 0
global_s = 0
# Timer para el reloj global
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
timer_global_reloj = Timer()
timer_global_reloj.init(freq=1, mode=Timer.PERIODIC, callback=tick_global_reloj)
# --- Funciones Auxiliares del Sistema ---
# Control del Buzzer para sonido de clic
def play_click_sound():
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.
button_last_states = {
'up': [True],
'down': [True],
'right': [True],
'left': [True],
'enter': [True], # Renombrado a 'enter'
'back': [True] # Nuevo estado para 'back'
}
# 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()
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():
oled.text("{:02}:{:02}".format(global_h, global_m), 80, 0) # Posición superior derecha
# --- MODO: Pantalla de Presentación ---
def run_presentation_mode():
oled.fill(0) # Limpiar pantalla
# Sonido de presentación
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) # Asegurarse de que el buzzer esté apagado
# Agumon Logo
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) # Centrado horizontalmente
oled.text("Edwin Luis", 20, 40)
oled.text("Garavito Omoya", 5, 50)
oled.show()
time.sleep(3) # Mostrar por 3 segundos
oled.fill(0) # Limpiar al finalizar
oled.show()
# --- MODO: Menú Principal ---
menu_items = [
"Reloj Digital",
"Cronometro",
"Reloj Analogico",
"Sokoban Game",
"Calculadora Graph",
"Temperatura",
"Calendario"
]
menu_icons = []
current_menu_selection = 0
def run_menu_mode():
global current_mode, current_menu_selection
oled.fill(0)
display_offset = 0
if len(menu_items) > 3:
if current_menu_selection >= 2:
display_offset = current_menu_selection - 1
if display_offset > len(menu_items) - 3:
display_offset = len(menu_items) - 3
for i in range(display_offset, min(len(menu_items), display_offset + 3)):
y_pos = 10 + (i - display_offset) * 18
if i == current_menu_selection:
oled.text("> " + menu_items[i], 0, y_pos)
else:
oled.text(menu_items[i], 10, y_pos)
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']): # Usar btn_enter para seleccionar
current_mode = current_menu_selection + MODE_CLOCK
time.sleep_ms(300)
# --- MODO: Reloj Digital ---
h_reloj_mode = 0
m_reloj_mode = 0
s_reloj_mode = 0
def show_clock_mode():
global h_reloj_mode, m_reloj_mode, s_reloj_mode, current_mode
oled.fill(0)
oled.text("Modo: Reloj", 0, 0)
oled.text("{:02}:{:02}:{:02}".format(h_reloj_mode, m_reloj_mode, s_reloj_mode), 20, 24)
oled.text("SET: UP/DOWN/RIGHT", 0, 52)
oled.show()
if check_button_press(btn_up, button_last_states['up']):
h_reloj_mode = (h_reloj_mode + 1) % 24
if check_button_press(btn_right, button_last_states['right']):
m_reloj_mode = (m_reloj_mode + 1) % 60
if check_button_press(btn_down, button_last_states['down']):
s_reloj_mode = (s_reloj_mode + 1) % 60
if check_button_press(btn_enter, button_last_states['enter']): # Usar btn_enter para seleccionar
# No se cambia de modo aquí, solo se confirma la configuración
pass
if check_button_press(btn_back, button_last_states['back']): # Botón atrás para volver al menú
current_mode = MODE_MENU
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
oled.fill(0)
display_global_clock()
oled.text("Cronometro", 0, 10)
t = "{:02}:{:02}:{:02}.{:02}".format(ch, cm, cs, cc)
oled.text(t[0:5], 10, 30)
oled.text(t[6:], 10, 45)
oled.text("{}".format("ON" if cronometro_activo else "PAUSA"), 85, 20)
oled.show()
if check_button_press(btn_enter, button_last_states['enter']): # Usar btn_enter para iniciar/pausar
cronometro_activo = not cronometro_activo
time.sleep_ms(300)
if check_button_press(btn_left, button_last_states['left']): # Reiniciar con LEFT
ch = cm = cs = cc = 0
cronometro_activo = False
time.sleep_ms(300)
if check_button_press(btn_back, button_last_states['back']): # Botón atrás para volver al menú
current_mode = MODE_MENU
time.sleep_ms(300)
# --- MODO: Reloj Analógico ---
def show_analog_clock_mode():
global current_mode
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']): # Usar btn_enter como confirmación si fuera necesario
pass # Podría usarse para alguna configuración aquí
if check_button_press(btn_back, button_last_states['back']): # Botón atrás para volver al menú
current_mode = MODE_MENU
time.sleep_ms(300)
# --- MODO: Sokoban ---
WALL = '#'
EMPTY = ' '
PLAYER = '@'
BOX = '$'
GOAL = '.'
BOX_ON_GOAL = '*'
PLAYER_ON_GOAL = '+'
TILE_SIZE = 8
class SokobanGame:
def __init__(self, oled_obj, display_global_clock_func, btn_states, check_btn_func):
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.levels = []
self.current_level_idx = 0
self.player_pos = (0, 0)
self.boxes_pos = []
self.goals_pos = []
self.initial_map = []
self.current_map = []
self.moves = 0
self.pushes = 0
self.game_running = False
self.level_completed = False
self.load_levels("levels.csv")
if self.levels:
self.load_level(self.current_level_idx)
else:
print("No se encontraron niveles en levels.csv")
def load_levels(self, filename="levels.csv"):
self.levels = []
try:
with open(filename, 'r') as f:
reader = csv.reader(f)
for row in reader:
if row:
full_map_string = "".join(row)
self.levels.append([list(line) for line in full_map_string.split(',') if line])
except OSError as e:
print("Error al cargar niveles:", e)
self.levels = []
def load_level(self, level_idx):
if not self.levels or level_idx >= len(self.levels):
print("Nivel {} no disponible.".format(level_idx))
self.game_running = False
return False
self.initial_map = [row[:] for row in self.levels[level_idx]]
self.current_map = [row[:] for row in self.initial_map]
self.moves = 0
self.pushes = 0
self.level_completed = False
self.game_running = True
self.player_pos = (0, 0)
self.boxes_pos = []
self.goals_pos = []
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)
self.current_map[r_idx][c_idx] = EMPTY
elif char == PLAYER_ON_GOAL:
self.player_pos = (r_idx, c_idx)
self.current_map[r_idx][c_idx] = GOAL
self.goals_pos.append((r_idx, c_idx))
elif char == BOX:
self.boxes_pos.append((r_idx, c_idx))
elif char == GOAL:
self.goals_pos.append((r_idx, c_idx))
elif char == BOX_ON_GOAL:
self.boxes_pos.append((r_idx, c_idx))
self.goals_pos.append((r_idx, c_idx))
return True
def draw_game(self):
self.oled.fill(0)
self.display_global_clock()
map_height_pixels = len(self.current_map) * TILE_SIZE
map_width_pixels = max(len(row) for row in self.current_map) * TILE_SIZE
offset_y = (HEIGHT - map_height_pixels) // 2
offset_x = (WIDTH - map_width_pixels) // 2
if offset_y < 15:
offset_y = 15
if offset_x < 0:
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
if char == WALL:
self.oled.rect(x, y, TILE_SIZE, TILE_SIZE, 1)
elif char == GOAL:
self.oled.circle(x + TILE_SIZE // 2, y + TILE_SIZE // 2, TILE_SIZE // 3, 1)
for box_r, box_c in self.boxes_pos:
x = offset_x + box_c * TILE_SIZE
y = offset_y + box_r * TILE_SIZE
self.oled.rect(x+1, y+1, TILE_SIZE-2, TILE_SIZE-2, 1)
px, py = self.player_pos
x = offset_x + py * TILE_SIZE
y = offset_y + px * TILE_SIZE
self.oled.fill_rect(x+2, y+2, TILE_SIZE-4, TILE_SIZE-4, 1)
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):
new_pr, new_pc = self.player_pos[0] + dr, self.player_pos[1] + dc
if not (0 <= new_pr < len(self.current_map) and 0 <= new_pc < len(self.current_map[0])):
return
target_char = self.current_map[new_pr][new_pc]
if target_char == WALL:
return
if (new_pr, new_pc) in self.boxes_pos:
box_target_r, box_target_c = new_pr + dr, new_pc + dc
if not (0 <= box_target_r < len(self.current_map) and 0 <= box_target_c < len(self.current_map[0])):
return
if self.current_map[box_target_r][box_target_c] == WALL or (box_target_r, box_target_c) in self.boxes_pos:
return
old_box_idx = self.boxes_pos.index((new_pr, new_pc))
self.boxes_pos[old_box_idx] = (box_target_r, box_target_c)
self.pushes += 1
self.player_pos = (new_pr, new_pc)
self.moves += 1
self.check_win()
def check_win(self):
for goal_r, goal_c in self.goals_pos:
if (goal_r, goal_c) not in self.boxes_pos:
self.level_completed = False
return
self.level_completed = True
self.game_running = False
self.save_score()
self.oled.fill(0)
self.oled.text("Nivel Completado!", 0, 20)
oled.text("Movs: {}".format(self.moves), 0, 30)
oled.text("Push: {}".format(self.pushes), 0, 40)
oled.show()
time.sleep(3)
self.current_level_idx += 1
if not self.load_level(self.current_level_idx):
self.oled.fill(0)
oled.text("FIN DEL JUEGO!", 0, 30)
oled.show()
time.sleep(2)
global current_mode
current_mode = MODE_MENU
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)
if header:
for row in reader:
scores.append(row)
except OSError:
pass
new_score = [self.current_level_idx + 1, player_name, self.moves, self.pushes]
scores.append(new_score)
with open(filename, 'w') as f:
writer = csv.writer(f)
if not header:
writer.writerow(['Level', 'Player', 'Moves', 'Pushes'])
writer.writerows(scores)
except OSError as e:
print("Error al guardar score:", e)
def run(self):
self.game_running = True
self.level_completed = False
while self.game_running:
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_enter, self.btn_states['enter']): # Usar btn_enter para alguna acción futura si es necesario
pass
elif self.check_button_press(btn_back, self.btn_states['back']): # Botón atrás para salir del juego
self.game_running = False
break
time.sleep_ms(100)
global current_mode
current_mode = MODE_MENU
def run_sokoban_mode():
sokoban_game = SokobanGame(oled, display_global_clock, button_last_states, check_button_press)
sokoban_game.run()
# --- MÓDULO CALCULADORA GRÁFICA (Integrado) ---
# Caracteres para el teclado virtual en pantalla
# Eliminados los números y el punto, dejando solo 'x' y operaciones/funciones.
KEYBOARD_CHARS = [
['sin', 'cos', 'tan', 'sqrt'],
['log', 'exp', '(', ')' ],
['+', '-', '*', '/' ],
['x', 'DEL', 'CLR', 'GRAPH'],
['EXIT'] # Botón de salida en una fila separada para mayor claridad
]
KEY_CHAR_WIDTH = 8
KEY_PADDING_X = 2
KEY_PADDING_Y = 1
KEY_HEIGHT_PX = KEY_CHAR_WIDTH + KEY_PADDING_Y * 2
class Parser:
def __init__(self, expression):
self.expr = expression.replace(" ", "")
self.pos = 0
self.x_val = 0 # Only x_val will be used for function evaluation
def parse(self, x_val=0): # y_val parameter removed from parse method
self.x_val = x_val
self.pos = 0
try:
result = self.parse_expression()
if self.pos < len(self.expr):
# Mejorar el mensaje de error para incluir la expresión completa
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 map_adc_to_range(val, min_val, max_val):
return min_val + (val / 65535) * (max_val - min_val)
def draw_axes(oled_obj, xmin, xmax, ymin, ymax):
y0_pixel = int(HEIGHT * ymax / (ymax - ymin))
if 0 <= y0_pixel < HEIGHT:
oled_obj.hline(0, y0_pixel, WIDTH, 1)
x0_pixel = int(-xmin * WIDTH / (xmax - xmin))
if 0 <= x0_pixel < WIDTH:
oled_obj.vline(x0_pixel, 0, HEIGHT, 1)
def plot_function(oled_obj, expr_str, joy_x_obj, joy_y_obj, display_global_clock_func, btns_last_states, check_button_func):
parser = Parser(expr_str)
x_range_val = 10.0
y_range_val = 5.0
cursor_x_pixel = WIDTH // 2
cursor_y_pixel_on_graph = 0
show_cursor = False
while True:
oled_obj.fill(0)
display_global_clock_func()
if not show_cursor:
x_adc_val = joy_x_obj.read_u16()
y_adc_val = joy_y_obj.read_u16()
x_range_val_new = map_adc_to_range(x_adc_val, 1, 20)
y_range_val_new = map_adc_to_range(y_adc_val, 0.5, 10)
x_range_val = x_range_val * 0.9 + x_range_val_new * 0.1
y_range_val = y_range_val * 0.9 + y_range_val_new * 0.1
xmin = -x_range_val
xmax = x_range_val
ymin = -y_range_val
ymax = y_range_val
draw_axes(oled_obj, xmin, xmax, ymin, ymax)
dx = (xmax - xmin) / WIDTH
try:
for pixel_x in range(WIDTH):
x = xmin + pixel_x * dx
try:
y = parser.parse(x_val=x)
except (ValueError, ZeroDivisionError, OverflowError):
y = float('nan')
if not math.isnan(y):
pixel_y = int(HEIGHT - ((y - ymin) * HEIGHT / (ymax - ymin)))
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
# Lógica del cursor para ver el valor en un punto específico
# Ahora el joystick controla el cursor
joy_x_cursor_val = joy_x_obj.read_u16()
joy_y_cursor_val = joy_y_obj.read_u16()
# Mover el cursor con el joystick
# Si el joystick X se mueve, ajustamos cursor_x_pixel
# Si el joystick Y se mueve, ajustamos show_cursor (alternar o mantener)
# Consideramos un umbral para detectar movimiento del joystick
JOY_THRESHOLD = 5000
# Mover el cursor X con el joystick X
if joy_x_cursor_val < 32768 - JOY_THRESHOLD: # Joystick a la izquierda
cursor_x_pixel = max(0, cursor_x_pixel - 1)
show_cursor = True # Mostrar cursor al moverlo
elif joy_x_cursor_val > 32768 + JOY_THRESHOLD: # Joystick a la derecha
cursor_x_pixel = min(WIDTH - 1, cursor_x_pixel + 1)
show_cursor = True # Mostrar cursor al moverlo
# Activar/desactivar el cursor con el joystick Y (hacia arriba o abajo)
# Podríamos usar un botón para esto, pero si se quiere con joystick, así sería:
if check_button_func(btn_enter, btns_last_states['enter']): # Usar ENTER para alternar cursor
show_cursor = not show_cursor
if show_cursor:
cursor_x_pixel = WIDTH // 2 # Centra el cursor al activarlo
if show_cursor:
cursor_x_func = xmin + cursor_x_pixel * dx
try:
cursor_y_func = parser.parse(x_val=cursor_x_func)
except (ValueError, ZeroDivisionError, OverflowError):
cursor_y_func = float('nan')
if not math.isnan(cursor_y_func):
cursor_y_pixel_on_graph = int(HEIGHT - ((cursor_y_func - ymin) * HEIGHT / (ymax - ymin)))
if 0 <= cursor_y_pixel_on_graph < HEIGHT:
oled_obj.rect(cursor_x_pixel-1, cursor_y_pixel_on_graph-1, 3, 3, 1)
oled_obj.text("X:{:.2f}".format(cursor_x_func), 0, 0)
oled_obj.text("Y:{:.2f}".format(cursor_y_func), 0, 10)
oled_obj.show()
time.sleep_ms(50)
# Usar BTN_BACK para salir del modo de graficación
if check_button_func(btn_back, btns_last_states['back']): # Usar btn_back para salir
return
def run_graph_calculator_mode_internal(oled_obj, btns_last_states, check_button_func, display_global_clock_func, joy_x_obj, joy_y_obj):
expression_str = ""
kb_cursor_x = 0
kb_cursor_y = 0
EXPRESSION_LINE_HEIGHT = 8
EXPRESSION_AREA_START_Y = 0
EXPRESSION_AREA_HEIGHT = EXPRESSION_LINE_HEIGHT * 2 + 2
KEYBOARD_START_Y = EXPRESSION_AREA_HEIGHT + 5
while True:
oled_obj.fill(0)
display_global_clock_func()
oled_obj.text("f(x):", 0, EXPRESSION_AREA_START_Y)
display_expr = expression_str
if len(display_expr) * KEY_CHAR_WIDTH > WIDTH:
display_expr = display_expr[-(WIDTH // KEY_CHAR_WIDTH):]
oled_obj.text(display_expr, 0, EXPRESSION_AREA_START_Y + EXPRESSION_LINE_HEIGHT)
current_y_pos = KEYBOARD_START_Y
for r_idx, row in enumerate(KEYBOARD_CHARS):
current_x_pos = 0
for c_idx, char in enumerate(row):
text_width = len(char) * KEY_CHAR_WIDTH
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:
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)
else:
oled_obj.text(char, x_start_key, y_start_key, 1)
current_x_pos += text_width + KEY_PADDING_X * 4
current_y_pos += KEY_HEIGHT_PX + KEY_PADDING_Y * 2
oled_obj.show()
if check_button_func(btn_up, btns_last_states['up']):
kb_cursor_y = (kb_cursor_y - 1 + len(KEYBOARD_CHARS)) % len(KEYBOARD_CHARS)
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:
plot_function(oled_obj, expression_str, joy_x_obj, joy_y_obj, display_global_clock_func, btns_last_states, check_button_func)
else:
oled_obj.fill(0)
oled_obj.text("Ingrese funcion!", 0, 30)
oled_obj.show()
time.sleep(1)
elif selected_char == 'EXIT':
# El botón EXIT del teclado ya no tiene que devolver True para salir
# sino que simplemente se ignora o se puede eliminar si BACK cumple la función
# Para mantener la flexibilidad, lo dejaremos, pero el BACK físico tiene prioridad.
pass
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
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)
return False
# 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
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)
# --- MODO: Temperatura ---
def run_temperature_mode():
global current_mode
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 ---
dia_calendar_mode = 1
mes_calendar_mode = 1
anio_calendar_mode = 2025
def show_calendar_mode():
global dia_calendar_mode, mes_calendar_mode, anio_calendar_mode, current_mode
oled.fill(0)
display_global_clock()
oled.text("Modo: Calendario", 0, 10)
fecha = "{:02}/{:02}/{}".format(dia_calendar_mode, mes_calendar_mode, anio_calendar_mode)
oled.text("Fecha:", 0, 30)
oled.text(fecha, 10, 45)
oled.text("SET: UP/DOWN/RIGHT", 0, 56)
oled.show()
if check_button_press(btn_up, button_last_states['up']):
dia_calendar_mode = dia_calendar_mode + 1 if dia_calendar_mode < 31 else 1
if check_button_press(btn_right, button_last_states['right']):
mes_calendar_mode = mes_calendar_mode + 1 if mes_calendar_mode < 12 else 1
if check_button_press(btn_down, button_last_states['down']):
anio_calendar_mode = anio_calendar_mode + 1 if anio_calendar_mode < 2099 else 2025
if check_button_press(btn_enter, button_last_states['enter']): # Usar btn_enter como confirmación
pass
if check_button_press(btn_back, button_last_states['back']): # Usar btn_back para salir
current_mode = MODE_MENU
time.sleep_ms(300)
# --- Bucle Principal del Sistema ---
def main_loop():
global current_mode
oled.fill(0)
oled.show()
time.sleep_ms(100)
run_presentation_mode()
current_mode = MODE_MENU
while True:
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:
run_sokoban_mode()
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()
time.sleep_ms(20)
# --- Ejecución del Programa ---
if __name__ == "__main__":
main_loop()