Servidor » History » Version 4
Version 3 (cristobal hernandez, 12/18/2025 01:17 AM) → Version 4/5 (cristobal hernandez, 12/18/2025 01:21 AM)
h1. Interfaz
Servidor
<pre><code class="python">
import pygame time
import tkinter as tk glob
import json
import requests
import os
import random
from tkinter datetime import ttk datetime
import grovepi
from tkinter grovepi import messagebox *
from grove_rgb_lcd import socket *
from gpiozero import AngularServo
class Aplicacion:
def __init__(self, root):
self.key_pressed import firebase_admin
from firebase_admin import credentials, firestore, messaging
# Archivos Locales
FILE_ID = False
self.tecla_pressed "savenemo_id.txt"
FILE_CONFIG_LOCAL = None
self.botones_presionados "config.json"
FILE_CREDENTIALS = set()
self.lista_botones "serviceAccountKey.json"
# Configuracion Sensor Temperatura
SENSOR_TEMP_ID = []
self.contenedor "28-00000053d2ea"
SENSOR_TEMP_PATH = {}
self.labels = {}
self.imagenes_botones = {
"up": tk.PhotoImage(file="resources2/up.png"),
"left": tk.PhotoImage(file="resources2/left.png"),
"down": tk.PhotoImage(file="resources2/down.png"),
"right": tk.PhotoImage(file="resources2/right.png"),
"up_claw": tk.PhotoImage(file="resources2/up_claw.png"),
"down_claw": tk.PhotoImage(file="resources2/down_claw.png"),
"center1": tk.PhotoImage(file="resources2/center1.png"),
"center2": tk.PhotoImage(file="resources2/center2.png"),
"off": tk.PhotoImage(file="resources2/off1.png")
}
self.botones_config = [
(self.imagenes_botones["up"], 275, 250, self.moveUp),
(self.imagenes_botones["left"], 225, 300, self.moveLeft),
(self.imagenes_botones["down"], 275, 300, self.moveDown),
(self.imagenes_botones["right"], 325, 300, self.moveRight),
(self.imagenes_botones["up_claw"], 475, 250, self.upCraw),
(self.imagenes_botones["down_claw"], 475, 300, self.downCraw),
(self.imagenes_botones["center1"], 555, 275, self.grab),
(self.imagenes_botones["center2"], 625, 275, self.drop)
]
self.teclas_teclado = {
"w": {"coords": (250, 225, 300, 275), "label_pos": (275, 250), "etiqueta": "W", "funcion": self.moveUp}, f"/sys/bus/w1/devices/{SENSOR_TEMP_ID}/w1_slave"
# Arriba
"a": {"coords": (200, 275, 250, 325), "label_pos": (225, 300), "etiqueta": "A", "funcion": self.moveLeft}, Puertos GrovePi
PORT_ULTRASONIC = 8 # Izquierda
"s": {"coords": (250, 275, 300, 325), "label_pos": (275, 300), "etiqueta": "S", "funcion": self.moveDown}, D8
PORT_RELAY_CAL = 3 # Abajo
"d": {"coords": (300, 275, 350, 325), "label_pos": (325, 300), "etiqueta": "D", "funcion": self.moveRight}, D3 (Calefactor)
PORT_PH = 0 # Derecha
"l": {"coords": (450, 225, 500, 275), "label_pos": (475, 250), "etiqueta": "L", "funcion": self.upCraw}, A0
PORT_SENSOR_LUZ = 1 # Arriba Garra
"k": {"coords": (450, 275, 500, 325), "label_pos": (475, 300), "etiqueta": "K", "funcion": self.downCraw}, A1
SERVO_PIN = 18 # Abajo Garra
"x": {"coords": (530, 250, 580, 300), "label_pos": (555, 275), "etiqueta": "X", "funcion": self.grab}, Pin GPIO (Servomotor)
# Agarrar
"c": {"coords": (600, 250, 650, 300), "label_pos": (625, 275), "etiqueta": "C", "funcion": self.drop} Variables Globales de Hardware
alimentador_servo = None
# Soltar
}
self.teclas_joystick Configuracion Inicial por Defecto
DEFAULT_CONFIG = {
(0, 1): {"coords": (175, 200, 225, 250), "label_pos": (200, 225), "etiqueta": "w"}, # Arriba
(0, -1): {"coords": (175, 300, 225, 350), "label_pos": (200, 325), "etiqueta": "s"}, # Abajo
(-1, 0): {"coords": (125, 250, 175, 300), "label_pos": (150, 275), "etiqueta": "a"}, # Izquierda
(1, 0): {"coords": (225, 250, 275, 300), "label_pos": (250, 275), "etiqueta": "d"}, # Derecha
8: {"coords": (450, 200, 500, 250), "label_pos": (475, 225), "etiqueta": "L2"}, # Arriba Garra
9: {"coords": (450, 250, 500, 300), "label_pos": (475, 275), "etiqueta": "R2"}, # Abajo Garra
0: {"coords": (530, 225, 580, 275), "label_pos": (555, 250), "etiqueta": "A"}, # Agarrar
1: {"coords": (600, 225, 650, 275), "label_pos": (625, 250), "etiqueta": "B"}, # Soltar
"C": {"coords": (300, 225, 400, 325), "label_pos": (625, 250), "etiqueta": ""}, # Círculo Grande
"c": {"coords": (340, 265, 360, 285), "label_pos": (625, 250), "etiqueta": ""}, # Círculo Chico
"nombre_pez": "Esperando App...",
"temp_min": 24.0, "temp_max": 28.0,
"ph_min": 6.5, "ph_max": 7.5,
"nivel_luz": 50, "nivel_agua": 80,
"horarios_comida": [],
"sistema_alimentacion_on": True
}
self.root = root
self.root.title("Blitz")
self.root.geometry("800x450")
self.root.resizable(0, 0)
self.root.iconbitmap("resources2/logoBlitz.png")
self._configurar_estilos()
self.imagen_fondo = tk.PhotoImage(file="resources2/fondoBlitz.png")
self.canvas = tk.Canvas(self.root, width=800, height=400)
self.canvas.pack(fill="both", expand=True)
self.canvas.create_image(0, 0, image=self.imagen_fondo, anchor="nw")
self.crear_labels()
self.combobox_controles = ttk.Combobox(self.root, values=["Botones", "Teclado", "Joystick"],
state="disabled", font=("Comic Sans MS", 10), width=7,
style="TCombobox")
self.combobox_controles.bind("<<ComboboxSelected>>", self.on_select)
self.canvas.create_window(180, 135, window=self.combobox_controles)
self.combobox_controles.current(0)
# Crear botón "off"
self.x0, self.y0, self.x1, self.y1 Estado del Sistema
config_actual = 375, 327, 425, 377
self.circle DEFAULT_CONFIG.copy()
nemo_id = self.canvas.create_oval(self.x0, self.y0, self.x1, self.y1, fill="blue", outline="black")
self.imagen_conexion ""
db = tk.PhotoImage(file="resources2/off1.png")
self.imagen_desconexion None
firebase_activo = tk.PhotoImage(file="resources2/off2.png")
self.current_image False
ultimo_minuto_alimentacion = self.imagen_desconexion
self.canvas_image ""
ya_se_envio_notificacion = self.canvas.create_image((self.x0 + self.x1) / 2, (self.y0 + self.y1) / 2, image=self.current_image)
self.canvas.image = self.current_image
self.current_funcion = self.ventana_conexion_servidor
self.canvas.bind("<Button-1>", self.check_click)
False
# Funciones BD
def check_click(self, event): obtener_id_unico():
"""Lee o genera un ID unico para identificar esta pecera."""
if os.path.exists(FILE_ID):
x, y = event.x, event.y
if self.x0 <= x <= self.x1 and self.y0 <= y <= self.y1: with open(FILE_ID, 'r') as f:
self.current_funcion()
def crear_labels(self): return f.read().strip()
else:
self.label1 nuevo_id = tk.Label(self.root, text="Robot", font=("Comic Sans MS", 16, "bold")) f"NEMO-{random.randint(1000, 9999)}"
self.canvas.create_window(275, 170, window=self.label1) with open(FILE_ID, 'w') as f:
f.write(nuevo_id)
self.label2 = tk.Label(self.root, text="Garra", font=("Comic Sans MS", 16, "bold")) return nuevo_id
def verificar_internet():
try:
self.canvas.create_window(555, 170, window=self.label2) requests.get('https://www.google.com', timeout=3)
self.label_agarrar = tk.Label(self.root, text="Agarrar", font=("Comic Sans MS", 9, "bold"))
self.canvas.create_window(555, 325, window=self.label_agarrar)
self.label_soltar = tk.Label(self.root, text="Soltar", font=("Comic Sans MS", 9, "bold"))
self.canvas.create_window(625, 325, window=self.label_soltar)
return True
except: return False
def borrar_labels(self): esperar_internet():
print("Buscando conexion a internet...")
while not verificar_internet():
self.label1.destroy()
self.label2.destroy()
self.label_agarrar.destroy()
self.label_soltar.destroy()
time.sleep(3)
print("Conexion establecida.")
def _configurar_estilos(self): iniciar_firebase():
global db, firebase_activo
try:
estilos if os.path.exists(FILE_CREDENTIALS):
cred = { credentials.Certificate(FILE_CREDENTIALS)
'*TCombobox*Listbox.font': ("Comic Sans MS", 10), if not firebase_admin._apps:
firebase_admin.initialize_app(cred)
'*TCombobox*Listbox.background': "#0F2B6A", db = firestore.client()
'*TCombobox*Listbox.foreground': "#ffffff", firebase_activo = True
'*TCombobox*Listbox.selectBackground': '#08FBF9' print("Firebase conectado exitosamente.")
}
for k, v in estilos.items(): else:
self.root.option_add(k, v)
# Botones print("Falta archivo serviceAccountKey.json")
except Exception as e:
print(f"Error Firebase: {e}")
firebase_activo = False
def crear_botones(self): sincronizar_configuracion():
global config_actual
if not firebase_activo: return
try:
for imagen, x, y, funcion in self.botones_config[:4]:
boton doc = tk.Button(self.root, image=imagen, borderwidth=0, highlightthickness=0, cursor="hand2", bg="#0F2B6A")
boton.bind("<ButtonPress>", lambda event, f=funcion: f())
boton.bind("<ButtonRelease>", lambda event: self.stop())
self.canvas.create_window(x, y, window=boton)
self.lista_botones.append(boton) db.collection('acuarios').document(nemo_id).collection('data').document('config').get()
for imagen, x, y, funcion in self.botones_config[4:]: if doc.exists:
boton datos = tk.Button(self.root, image=imagen, borderwidth=0, highlightthickness=0, cursor="hand2", bg="#0F2B6A") doc.to_dict()
boton.bind("<ButtonPress>", lambda event, f=funcion: f())
self.canvas.create_window(x, y, window=boton)
self.lista_botones.append(boton)
if datos != config_actual: # Solo actualizar si hay cambios
print("Configuracion actualizada desde la nube.")
config_actual.update(datos)
with open(FILE_CONFIG_LOCAL, 'w') as f: json.dump(config_actual, f)
except: pass
def eliminar_botones(self): subir_estado(temp, ph, luz, agua, alertas):
"""Sube los datos de sensores a Firestore."""
if not firebase_activo: return
try:
for boton in self.lista_botones: db.collection('acuarios').document(nemo_id).collection('data').document('estado').set({
boton.destroy() 'temp_actual': temp, 'ph_actual': ph, 'luz_actual': luz, 'agua_nivel': agua,
'alertas': alertas, 'ultima_actualizacion': datetime.now().isoformat()
self.lista_botones.clear()
# Crear y eliminar Teclas })
except: pass
def crear_teclas_teclado(self): enviar_notificacion_push(titulo, mensaje):
if not firebase_activo: return
try:
for tecla, datos in self.teclas_teclado.items():
coords, label_pos, etiqueta tokens_ref = datos["coords"], datos["label_pos"], datos["etiqueta"]
self.contenedor[tecla] db.collection('acuarios').document(nemo_id).collection('data').document('tokens')
doc = self.canvas.create_rectangle(*coords, fill="#0F2B6A", outline="black")
label = self.canvas.create_text(*label_pos, text=etiqueta, font=("Comic Sans MS", 20), fill="white")
self.labels[tecla] = label
def crear_teclas_joystick(self): tokens_ref.get()
for tecla, datos in self.teclas_joystick.items():
coords, label_pos, etiqueta token = datos["coords"], datos["label_pos"], datos["etiqueta"]
self.contenedor[tecla] = self.canvas.create_oval(*coords, fill="#0F2B6A", outline="black")
label = self.canvas.create_text(*label_pos, text=etiqueta, font=("Comic Sans MS", 20), fill="white")
self.labels[tecla] = label doc.to_dict().get('token_celular') if doc.exists else None
self.canvas.itemconfig(self.contenedor["C"], fill="")
self.canvas.itemconfig(self.contenedor["c"], fill="white")
def eliminar_teclas_y_botones(self):
if self.contenedor: token:
for tecla in self.contenedor: msg = messaging.Message(
self.canvas.delete(self.contenedor[tecla]) notification=messaging.Notification(title=titulo, body=mensaje),
token=token,
self.contenedor.clear()
for tecla in self.labels: )
self.canvas.delete(self.labels[tecla])
for boton in self.lista_botones: messaging.send(msg)
boton.destroy()
self.labels.clear()
self.lista_botones.clear()
#Teclado print(f"Push enviado: {titulo}")
except Exception as e:
print(f"Error push: {e}")
def activar_teclas_teclado(self): guardar_historial_evento(titulo, mensaje):
if not firebase_activo: return
try:
for tecla in self.teclas_teclado: db.collection('acuarios').document(nemo_id).collection('historial').add({
self.root.bind(f"<KeyPress-{tecla}>", self.crear_callback(tecla, self.pressed)) 'titulo': titulo,
self.root.bind(f"<KeyRelease-{tecla}>", self.crear_callback(tecla, self.released)) 'mensaje': mensaje,
self.root.bind(f"<KeyPress-{tecla.upper()}>", self.crear_callback(tecla, self.pressed)) 'fecha': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
self.root.bind(f"<KeyRelease-{tecla.upper()}>", self.crear_callback(tecla, self.released))
'timestamp': datetime.now().timestamp()
})
except: pass
# Hardware
def desactivar_teclas_teclado(self): inicializar_hardware():
global alimentador_servo
try:
for tecla in self.teclas_teclado: # Relay
pinMode(PORT_RELAY_CAL, "OUTPUT")
digitalWrite(PORT_RELAY_CAL, 0)
# LCD
try:
self.root.unbind(f"<KeyPress-{tecla}>") setText("INICIANDO\nSISTEMA...")
self.root.unbind(f"<KeyRelease-{tecla}>")
self.root.unbind(f"<KeyPress-{tecla.upper()}>")
self.root.unbind(f"<KeyRelease-{tecla.upper()}>")
def crear_callback(self, tecla, funcion): setRGB(0, 0, 255) # Azul
return lambda event: funcion(tecla)
def pressed(self, tecla): except: print("LCD no detectado (continuando...)")
# Servo Motor
if not self.key_pressed and self.tecla_pressed is None:
self.canvas.itemconfig(self.contenedor[tecla], fill="#08FBF9")
self.tecla_pressed alimentador_servo = tecla
self.key_pressed AngularServo(SERVO_PIN, min_angle=0, max_angle=180,
min_pulse_width=0.0006, max_pulse_width=0.0024)
print("Hardware inicializado correctamente.")
except Exception as e:
print(f"Error Hardware: {e}")
alimentador_servo = True
self.teclas_teclado[tecla]["funcion"]()
None
# Sensores
def released(self, tecla): leer_temperatura():
try:
teclas_sin_stop = ["L", "K", "X", "C"]
if tecla.upper() not in teclas_sin_stop: os.path.exists(SENSOR_TEMP_PATH):
self.stop() print("Sensor de temperatura no encontrado (Cable desconectado?)")
return 25.0
with open(SENSOR_TEMP_PATH, 'r') as f:
lineas = f.readlines()
if tecla lineas[0].strip()[-3:] == self.tecla_pressed: 'YES':
self.canvas.itemconfig(self.contenedor[tecla], fill="#0F2B6A")
self.tecla_pressed posicion_t = None lineas[1].find('t=')
self.key_pressed if posicion_t != -1:
temp_string = False lineas[1][posicion_t+2:]
temp_c = float(temp_string) / 1000.0
return round(temp_c, 2)
except Exception as e:
print(f"Error leyendo temp: {e}")
return 25.0
def on_select(self, event): leer_ph():
VOLTAJE_NEUTRO = 2.50
FACTOR_CONVERSION = 4.17
try:
seleccion valor_raw = self.combobox_controles.get() grovepi.analogRead(PORT_PH)
self.eliminar_teclas_y_botones() voltaje = float(valor_raw) * 5.0 / 1023
if seleccion == "Botones":
self.crear_botones() ph_actual = 7.0 + ((voltaje - VOLTAJE_NEUTRO) * FACTOR_CONVERSION)
elif seleccion == "Teclado":
self.crear_teclas_teclado()
self.activar_teclas_teclado() return round(ph_actual, 1)
except: return 7.0
def leer_nivel_agua():
try:
else:
self.crear_teclas_joystick()
self.inicializar_pygame() altura_total = 40
self.root.focus() dist = ultrasonicRead(PORT_ULTRASONIC)
self.tecla_pressed if dist > altura_total: dist = None 40
self.key_pressed porc = False
# Servidor int(100 - ((dist / altura_total) * 100))
return max(0, min(100, porc))
except: return 80
def validar_ip(self, ip): leer_luz():
try:
try:
socket.inet_aton(ip)
lectura = analogRead(PORT_SENSOR_LUZ)
return int((lectura / 800.0) * 100)
except: return 50
# Actuadores
def gestionar_calefactor(temp_actual):
target = float(config_actual.get('temp_min', 24.0))
if temp_actual < target:
digitalWrite(PORT_RELAY_CAL, 1) # Encender
return True
elif temp_actual > target + 0.5:
except socket.error:
digitalWrite(PORT_RELAY_CAL, 0) # Apagar
return False
return False
def validar_puerto(self, port): mover_servo_alimentar():
if alimentador_servo is None: return False
try:
try:
port print("Dispensando comida...")
alimentador_servo.angle = int(port)
90
time.sleep(0.8)
alimentador_servo.angle = 110
time.sleep(0.3)
alimentador_servo.angle = 90
time.sleep(0.3)
alimentador_servo.angle = 0
time.sleep(1)
alimentador_servo.value = None
return 1 <= port <= 65535 True
except Exception as e:
except ValueError:
print(f"Error Servo: {e}")
return False
def gestionar_comida():
global ultimo_minuto_alimentacion
if not config_actual.get('sistema_alimentacion_on', True):
return False
def conectar_servidor(self, ip_entry, port_entry, scene): hora_actual = datetime.now().strftime("%H:%M")
horarios = config_actual.get("horarios_comida", [])
if (hora_actual in horarios) and (hora_actual != ultimo_minuto_alimentacion):
ip print(f"Intentando alimentar ({hora_actual})...")
exito = ip_entry.get() mover_servo_alimentar()
port = port_entry.get()
if not self.validar_ip(ip): exito:
messagebox.showerror("Error", "Dirección IP no válida.") ultimo_minuto_alimentacion = hora_actual
# EMOJI REMOVIDO AQUI
enviar_notificacion_push("Hora de comer!", f"Tu pez ha sido alimentado a las {hora_actual}")
guardar_historial_evento("Comida Servida", "Dispensador automatico activado con exito.")
return True
return False
def actualizar_lcd(nemo_id, temp, ph, luz, agua, calefactor_on, comida_servida):
try:
if not self.validar_puerto(port): comida_servida:
messagebox.showerror("Error", "Puerto no válido. Debe estar entre 1 y 65535.") setText(" DISPENSANDO\n COMIDA...")
time.sleep(3)
return
port
if calefactor_on: setRGB(255, 60, 0) # Naranja
else: setRGB(0, 255, 0) # Verde
setText(f"ID:{nemo_id}\nT:{temp}C pH:{ph}")
time.sleep(4)
setText(f"ID:{nemo_id}\nLuz:{luz}% Agua:{agua}%")
time.sleep(4)
except: pass
def main():
global nemo_id, ya_se_envio_notificacion
print("\n--- INICIANDO SAVENEMO ---")
inicializar_hardware()
nemo_id = int(port)
obtener_id_unico()
print(f"ID DISPOSITIVO: {nemo_id}")
esperar_internet()
iniciar_firebase()
print("Loop principal iniciado.\n")
while True:
try:
self.client_socket sincronizar_configuracion()
temp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) leer_temperatura()
self.client_socket.connect((ip, port))
self.combobox_controles.config(state="reondly")
self.combobox_controles.set("Botones")
self.crear_botones()
scene.destroy()
self.current_funcion ph = self.ventana_desconectar_servidor leer_ph()
self.current_image agua = self.imagen_conexion leer_nivel_agua()
self.canvas_image luz = self.canvas.create_image((self.x0 + self.x1) / 2, (self.y0 + self.y1) / 2, image=self.current_image) leer_luz()
alertas = []
self.canvas.image cfg = self.current_image
messagebox.showinfo("Éxito", "Conexión exitosa al servidor.")
except socket.error as e: config_actual
messagebox.showerror("Error", f"Error al conectar al servidor")
except Exception as e: if temp < float(cfg.get('temp_min', 20)): alertas.append("Temp Baja")
messagebox.showerror("Error", f"Ha ocurrido un error desconocido")
def ventana_conexion_servidor(self):
self.secondary_window if temp > float(cfg.get('temp_max', 35)): alertas.append("Temp Alta")
if agua < int(cfg.get('nivel_agua', 80)): alertas.append("Nivel Bajo")
if ph < int(cfg.get('ph_min', 80)): alertas.append("PH Bajo")
if ph > int(cfg.get('ph_max', 80)): alertas.append("PH Alto")
calefactor_on = tk.Toplevel(self.root)
self.secondary_window.title("Ingresar IP y Puerto")
self.secondary_window.geometry("400x225")
self.secondary_window.config(bg="#0F2B6A")
self.secondary_window.resizable(0, 0)
self.secondary_window.iconbitmap("resources2/logoBlitz.png")
tk.Label(self.secondary_window, text="IP del Servidor:", font=("Helvetica ", 9, "bold")).pack(pady=(20,5))
self.ip_entry gestionar_calefactor(temp)
comida_servida = tk.Entry(self.secondary_window)
self.ip_entry.pack(pady=5)
tk.Label(self.secondary_window, text="Puerto del Servidor:", font=("Helvetica gestionar_comida()
if alertas:
if not ya_se_envio_notificacion:
print(f"Alertas: {alertas}")
# EMOJI REMOVIDO AQUI
enviar_notificacion_push("ALERTA ACUARIO ", 9, "bold")).pack(pady=5)
self.port_entry f"Atencion: {', '.join(alertas)}")
guardar_historial_evento("Alerta Detectada", ', '.join(alertas))
ya_se_envio_notificacion = tk.Entry(self.secondary_window)
self.port_entry.pack(pady=5)
confirm_button True
else:
if ya_se_envio_notificacion: ya_se_envio_notificacion = tk.Button(self.secondary_window, text="Conectar", bg="#22c8c9", command=lambda: self.conectar_servidor(self.ip_entry, self.port_entry, self.secondary_window))
confirm_button.pack(pady=5)
ip_save_button False
subir_estado(temp, ph, luz, agua, alertas)
cal_status = tk.Button(self.secondary_window, text="Ingresar IP guardada", bg="#22c8c9", command=lambda: self.ingresar_ip_guardada(self.ip_entry, self.port_entry))
ip_save_button.pack()
def ingresar_ip_guardada(self, ip_entry, port_entry):
ip_entry.delete(0, tk.END)
ip_entry.insert(0, "192.168.144.84")
port_entry.delete(0, tk.END)
port_entry.insert(0, "8080")
def ventana_desconectar_servidor(self):
'ON' if messagebox.askyesno("Confirmación", "¿Está seguro que quiere desconectar del servidor?"):
self.client_socket.close() calefactor_on else 'OFF'
self.current_funcion = self.ventana_conexion_servidor # Solo para ver por terminal
self.current_image = self.imagen_desconexion print(f"[{datetime.now().strftime('%H:%M')}] T:{temp}C pH:{ph} L:{luz}% A:{agua}% Cal:{cal_status}")
actualizar_lcd(nemo_id, temp, ph, luz, agua, calefactor_on, comida_servida)
except KeyboardInterrupt:
self.canvas_image = self.canvas.create_image((self.x0 + self.x1) / 2, (self.y0 + self.y1) / 2, image=self.current_image) print("\nApagando sistema...")
self.canvas.image = self.current_image try:
digitalWrite(PORT_RELAY_CAL, 0)
setRGB(0, 0, 0)
setText("")
if alimentador_servo: alimentador_servo.detach()
self.eliminar_teclas_y_botones()
def moveUp(self): except: pass
break
self.client_socket.send(bytes([ord('w')]))
def moveDown(self):
self.client_socket.send(bytes([ord('s')]))
def moveRight(self):
self.client_socket.send(bytes([ord('d')]))
def moveLeft(self):
self.client_socket.send(bytes([ord('a')]))
def upCraw(self):
self.client_socket.send(bytes([ord('l')]))
def downCraw(self):
self.client_socket.send(bytes([ord('k')]))
def grab(self):
self.client_socket.send(bytes([ord('x')]))
def drop(self):
self.client_socket.send(bytes([ord('c')]))
def stop(self):
self.client_socket.send(bytes([ord(' ')])) except Exception as e:
print(f"Error en Loop: {e}")
time.sleep(5)
if __name__ == "__main__":
root = tk.Tk()
app = Aplicacion(root)
root.mainloop()
main()
</code></pre>
Servidor
<pre><code class="python">
import pygame time
import tkinter as tk glob
import json
import requests
import os
import random
from tkinter datetime import ttk datetime
import grovepi
from tkinter grovepi import messagebox *
from grove_rgb_lcd import socket *
from gpiozero import AngularServo
class Aplicacion:
def __init__(self, root):
self.key_pressed import firebase_admin
from firebase_admin import credentials, firestore, messaging
# Archivos Locales
FILE_ID = False
self.tecla_pressed "savenemo_id.txt"
FILE_CONFIG_LOCAL = None
self.botones_presionados "config.json"
FILE_CREDENTIALS = set()
self.lista_botones "serviceAccountKey.json"
# Configuracion Sensor Temperatura
SENSOR_TEMP_ID = []
self.contenedor "28-00000053d2ea"
SENSOR_TEMP_PATH = {}
self.labels = {}
self.imagenes_botones = {
"up": tk.PhotoImage(file="resources2/up.png"),
"left": tk.PhotoImage(file="resources2/left.png"),
"down": tk.PhotoImage(file="resources2/down.png"),
"right": tk.PhotoImage(file="resources2/right.png"),
"up_claw": tk.PhotoImage(file="resources2/up_claw.png"),
"down_claw": tk.PhotoImage(file="resources2/down_claw.png"),
"center1": tk.PhotoImage(file="resources2/center1.png"),
"center2": tk.PhotoImage(file="resources2/center2.png"),
"off": tk.PhotoImage(file="resources2/off1.png")
}
self.botones_config = [
(self.imagenes_botones["up"], 275, 250, self.moveUp),
(self.imagenes_botones["left"], 225, 300, self.moveLeft),
(self.imagenes_botones["down"], 275, 300, self.moveDown),
(self.imagenes_botones["right"], 325, 300, self.moveRight),
(self.imagenes_botones["up_claw"], 475, 250, self.upCraw),
(self.imagenes_botones["down_claw"], 475, 300, self.downCraw),
(self.imagenes_botones["center1"], 555, 275, self.grab),
(self.imagenes_botones["center2"], 625, 275, self.drop)
]
self.teclas_teclado = {
"w": {"coords": (250, 225, 300, 275), "label_pos": (275, 250), "etiqueta": "W", "funcion": self.moveUp}, f"/sys/bus/w1/devices/{SENSOR_TEMP_ID}/w1_slave"
# Arriba
"a": {"coords": (200, 275, 250, 325), "label_pos": (225, 300), "etiqueta": "A", "funcion": self.moveLeft}, Puertos GrovePi
PORT_ULTRASONIC = 8 # Izquierda
"s": {"coords": (250, 275, 300, 325), "label_pos": (275, 300), "etiqueta": "S", "funcion": self.moveDown}, D8
PORT_RELAY_CAL = 3 # Abajo
"d": {"coords": (300, 275, 350, 325), "label_pos": (325, 300), "etiqueta": "D", "funcion": self.moveRight}, D3 (Calefactor)
PORT_PH = 0 # Derecha
"l": {"coords": (450, 225, 500, 275), "label_pos": (475, 250), "etiqueta": "L", "funcion": self.upCraw}, A0
PORT_SENSOR_LUZ = 1 # Arriba Garra
"k": {"coords": (450, 275, 500, 325), "label_pos": (475, 300), "etiqueta": "K", "funcion": self.downCraw}, A1
SERVO_PIN = 18 # Abajo Garra
"x": {"coords": (530, 250, 580, 300), "label_pos": (555, 275), "etiqueta": "X", "funcion": self.grab}, Pin GPIO (Servomotor)
# Agarrar
"c": {"coords": (600, 250, 650, 300), "label_pos": (625, 275), "etiqueta": "C", "funcion": self.drop} Variables Globales de Hardware
alimentador_servo = None
# Soltar
}
self.teclas_joystick Configuracion Inicial por Defecto
DEFAULT_CONFIG = {
(0, 1): {"coords": (175, 200, 225, 250), "label_pos": (200, 225), "etiqueta": "w"}, # Arriba
(0, -1): {"coords": (175, 300, 225, 350), "label_pos": (200, 325), "etiqueta": "s"}, # Abajo
(-1, 0): {"coords": (125, 250, 175, 300), "label_pos": (150, 275), "etiqueta": "a"}, # Izquierda
(1, 0): {"coords": (225, 250, 275, 300), "label_pos": (250, 275), "etiqueta": "d"}, # Derecha
8: {"coords": (450, 200, 500, 250), "label_pos": (475, 225), "etiqueta": "L2"}, # Arriba Garra
9: {"coords": (450, 250, 500, 300), "label_pos": (475, 275), "etiqueta": "R2"}, # Abajo Garra
0: {"coords": (530, 225, 580, 275), "label_pos": (555, 250), "etiqueta": "A"}, # Agarrar
1: {"coords": (600, 225, 650, 275), "label_pos": (625, 250), "etiqueta": "B"}, # Soltar
"C": {"coords": (300, 225, 400, 325), "label_pos": (625, 250), "etiqueta": ""}, # Círculo Grande
"c": {"coords": (340, 265, 360, 285), "label_pos": (625, 250), "etiqueta": ""}, # Círculo Chico
"nombre_pez": "Esperando App...",
"temp_min": 24.0, "temp_max": 28.0,
"ph_min": 6.5, "ph_max": 7.5,
"nivel_luz": 50, "nivel_agua": 80,
"horarios_comida": [],
"sistema_alimentacion_on": True
}
self.root = root
self.root.title("Blitz")
self.root.geometry("800x450")
self.root.resizable(0, 0)
self.root.iconbitmap("resources2/logoBlitz.png")
self._configurar_estilos()
self.imagen_fondo = tk.PhotoImage(file="resources2/fondoBlitz.png")
self.canvas = tk.Canvas(self.root, width=800, height=400)
self.canvas.pack(fill="both", expand=True)
self.canvas.create_image(0, 0, image=self.imagen_fondo, anchor="nw")
self.crear_labels()
self.combobox_controles = ttk.Combobox(self.root, values=["Botones", "Teclado", "Joystick"],
state="disabled", font=("Comic Sans MS", 10), width=7,
style="TCombobox")
self.combobox_controles.bind("<<ComboboxSelected>>", self.on_select)
self.canvas.create_window(180, 135, window=self.combobox_controles)
self.combobox_controles.current(0)
# Crear botón "off"
self.x0, self.y0, self.x1, self.y1 Estado del Sistema
config_actual = 375, 327, 425, 377
self.circle DEFAULT_CONFIG.copy()
nemo_id = self.canvas.create_oval(self.x0, self.y0, self.x1, self.y1, fill="blue", outline="black")
self.imagen_conexion ""
db = tk.PhotoImage(file="resources2/off1.png")
self.imagen_desconexion None
firebase_activo = tk.PhotoImage(file="resources2/off2.png")
self.current_image False
ultimo_minuto_alimentacion = self.imagen_desconexion
self.canvas_image ""
ya_se_envio_notificacion = self.canvas.create_image((self.x0 + self.x1) / 2, (self.y0 + self.y1) / 2, image=self.current_image)
self.canvas.image = self.current_image
self.current_funcion = self.ventana_conexion_servidor
self.canvas.bind("<Button-1>", self.check_click)
False
# Funciones BD
def check_click(self, event): obtener_id_unico():
"""Lee o genera un ID unico para identificar esta pecera."""
if os.path.exists(FILE_ID):
x, y = event.x, event.y
if self.x0 <= x <= self.x1 and self.y0 <= y <= self.y1: with open(FILE_ID, 'r') as f:
self.current_funcion()
def crear_labels(self): return f.read().strip()
else:
self.label1 nuevo_id = tk.Label(self.root, text="Robot", font=("Comic Sans MS", 16, "bold")) f"NEMO-{random.randint(1000, 9999)}"
self.canvas.create_window(275, 170, window=self.label1) with open(FILE_ID, 'w') as f:
f.write(nuevo_id)
self.label2 = tk.Label(self.root, text="Garra", font=("Comic Sans MS", 16, "bold")) return nuevo_id
def verificar_internet():
try:
self.canvas.create_window(555, 170, window=self.label2) requests.get('https://www.google.com', timeout=3)
self.label_agarrar = tk.Label(self.root, text="Agarrar", font=("Comic Sans MS", 9, "bold"))
self.canvas.create_window(555, 325, window=self.label_agarrar)
self.label_soltar = tk.Label(self.root, text="Soltar", font=("Comic Sans MS", 9, "bold"))
self.canvas.create_window(625, 325, window=self.label_soltar)
return True
except: return False
def borrar_labels(self): esperar_internet():
print("Buscando conexion a internet...")
while not verificar_internet():
self.label1.destroy()
self.label2.destroy()
self.label_agarrar.destroy()
self.label_soltar.destroy()
time.sleep(3)
print("Conexion establecida.")
def _configurar_estilos(self): iniciar_firebase():
global db, firebase_activo
try:
estilos if os.path.exists(FILE_CREDENTIALS):
cred = { credentials.Certificate(FILE_CREDENTIALS)
'*TCombobox*Listbox.font': ("Comic Sans MS", 10), if not firebase_admin._apps:
firebase_admin.initialize_app(cred)
'*TCombobox*Listbox.background': "#0F2B6A", db = firestore.client()
'*TCombobox*Listbox.foreground': "#ffffff", firebase_activo = True
'*TCombobox*Listbox.selectBackground': '#08FBF9' print("Firebase conectado exitosamente.")
}
for k, v in estilos.items(): else:
self.root.option_add(k, v)
# Botones print("Falta archivo serviceAccountKey.json")
except Exception as e:
print(f"Error Firebase: {e}")
firebase_activo = False
def crear_botones(self): sincronizar_configuracion():
global config_actual
if not firebase_activo: return
try:
for imagen, x, y, funcion in self.botones_config[:4]:
boton doc = tk.Button(self.root, image=imagen, borderwidth=0, highlightthickness=0, cursor="hand2", bg="#0F2B6A")
boton.bind("<ButtonPress>", lambda event, f=funcion: f())
boton.bind("<ButtonRelease>", lambda event: self.stop())
self.canvas.create_window(x, y, window=boton)
self.lista_botones.append(boton) db.collection('acuarios').document(nemo_id).collection('data').document('config').get()
for imagen, x, y, funcion in self.botones_config[4:]: if doc.exists:
boton datos = tk.Button(self.root, image=imagen, borderwidth=0, highlightthickness=0, cursor="hand2", bg="#0F2B6A") doc.to_dict()
boton.bind("<ButtonPress>", lambda event, f=funcion: f())
self.canvas.create_window(x, y, window=boton)
self.lista_botones.append(boton)
if datos != config_actual: # Solo actualizar si hay cambios
print("Configuracion actualizada desde la nube.")
config_actual.update(datos)
with open(FILE_CONFIG_LOCAL, 'w') as f: json.dump(config_actual, f)
except: pass
def eliminar_botones(self): subir_estado(temp, ph, luz, agua, alertas):
"""Sube los datos de sensores a Firestore."""
if not firebase_activo: return
try:
for boton in self.lista_botones: db.collection('acuarios').document(nemo_id).collection('data').document('estado').set({
boton.destroy() 'temp_actual': temp, 'ph_actual': ph, 'luz_actual': luz, 'agua_nivel': agua,
'alertas': alertas, 'ultima_actualizacion': datetime.now().isoformat()
self.lista_botones.clear()
# Crear y eliminar Teclas })
except: pass
def crear_teclas_teclado(self): enviar_notificacion_push(titulo, mensaje):
if not firebase_activo: return
try:
for tecla, datos in self.teclas_teclado.items():
coords, label_pos, etiqueta tokens_ref = datos["coords"], datos["label_pos"], datos["etiqueta"]
self.contenedor[tecla] db.collection('acuarios').document(nemo_id).collection('data').document('tokens')
doc = self.canvas.create_rectangle(*coords, fill="#0F2B6A", outline="black")
label = self.canvas.create_text(*label_pos, text=etiqueta, font=("Comic Sans MS", 20), fill="white")
self.labels[tecla] = label
def crear_teclas_joystick(self): tokens_ref.get()
for tecla, datos in self.teclas_joystick.items():
coords, label_pos, etiqueta token = datos["coords"], datos["label_pos"], datos["etiqueta"]
self.contenedor[tecla] = self.canvas.create_oval(*coords, fill="#0F2B6A", outline="black")
label = self.canvas.create_text(*label_pos, text=etiqueta, font=("Comic Sans MS", 20), fill="white")
self.labels[tecla] = label doc.to_dict().get('token_celular') if doc.exists else None
self.canvas.itemconfig(self.contenedor["C"], fill="")
self.canvas.itemconfig(self.contenedor["c"], fill="white")
def eliminar_teclas_y_botones(self):
if self.contenedor: token:
for tecla in self.contenedor: msg = messaging.Message(
self.canvas.delete(self.contenedor[tecla]) notification=messaging.Notification(title=titulo, body=mensaje),
token=token,
self.contenedor.clear()
for tecla in self.labels: )
self.canvas.delete(self.labels[tecla])
for boton in self.lista_botones: messaging.send(msg)
boton.destroy()
self.labels.clear()
self.lista_botones.clear()
#Teclado print(f"Push enviado: {titulo}")
except Exception as e:
print(f"Error push: {e}")
def activar_teclas_teclado(self): guardar_historial_evento(titulo, mensaje):
if not firebase_activo: return
try:
for tecla in self.teclas_teclado: db.collection('acuarios').document(nemo_id).collection('historial').add({
self.root.bind(f"<KeyPress-{tecla}>", self.crear_callback(tecla, self.pressed)) 'titulo': titulo,
self.root.bind(f"<KeyRelease-{tecla}>", self.crear_callback(tecla, self.released)) 'mensaje': mensaje,
self.root.bind(f"<KeyPress-{tecla.upper()}>", self.crear_callback(tecla, self.pressed)) 'fecha': datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
self.root.bind(f"<KeyRelease-{tecla.upper()}>", self.crear_callback(tecla, self.released))
'timestamp': datetime.now().timestamp()
})
except: pass
# Hardware
def desactivar_teclas_teclado(self): inicializar_hardware():
global alimentador_servo
try:
for tecla in self.teclas_teclado: # Relay
pinMode(PORT_RELAY_CAL, "OUTPUT")
digitalWrite(PORT_RELAY_CAL, 0)
# LCD
try:
self.root.unbind(f"<KeyPress-{tecla}>") setText("INICIANDO\nSISTEMA...")
self.root.unbind(f"<KeyRelease-{tecla}>")
self.root.unbind(f"<KeyPress-{tecla.upper()}>")
self.root.unbind(f"<KeyRelease-{tecla.upper()}>")
def crear_callback(self, tecla, funcion): setRGB(0, 0, 255) # Azul
return lambda event: funcion(tecla)
def pressed(self, tecla): except: print("LCD no detectado (continuando...)")
# Servo Motor
if not self.key_pressed and self.tecla_pressed is None:
self.canvas.itemconfig(self.contenedor[tecla], fill="#08FBF9")
self.tecla_pressed alimentador_servo = tecla
self.key_pressed AngularServo(SERVO_PIN, min_angle=0, max_angle=180,
min_pulse_width=0.0006, max_pulse_width=0.0024)
print("Hardware inicializado correctamente.")
except Exception as e:
print(f"Error Hardware: {e}")
alimentador_servo = True
self.teclas_teclado[tecla]["funcion"]()
None
# Sensores
def released(self, tecla): leer_temperatura():
try:
teclas_sin_stop = ["L", "K", "X", "C"]
if tecla.upper() not in teclas_sin_stop: os.path.exists(SENSOR_TEMP_PATH):
self.stop() print("Sensor de temperatura no encontrado (Cable desconectado?)")
return 25.0
with open(SENSOR_TEMP_PATH, 'r') as f:
lineas = f.readlines()
if tecla lineas[0].strip()[-3:] == self.tecla_pressed: 'YES':
self.canvas.itemconfig(self.contenedor[tecla], fill="#0F2B6A")
self.tecla_pressed posicion_t = None lineas[1].find('t=')
self.key_pressed if posicion_t != -1:
temp_string = False lineas[1][posicion_t+2:]
temp_c = float(temp_string) / 1000.0
return round(temp_c, 2)
except Exception as e:
print(f"Error leyendo temp: {e}")
return 25.0
def on_select(self, event): leer_ph():
VOLTAJE_NEUTRO = 2.50
FACTOR_CONVERSION = 4.17
try:
seleccion valor_raw = self.combobox_controles.get() grovepi.analogRead(PORT_PH)
self.eliminar_teclas_y_botones() voltaje = float(valor_raw) * 5.0 / 1023
if seleccion == "Botones":
self.crear_botones() ph_actual = 7.0 + ((voltaje - VOLTAJE_NEUTRO) * FACTOR_CONVERSION)
elif seleccion == "Teclado":
self.crear_teclas_teclado()
self.activar_teclas_teclado() return round(ph_actual, 1)
except: return 7.0
def leer_nivel_agua():
try:
else:
self.crear_teclas_joystick()
self.inicializar_pygame() altura_total = 40
self.root.focus() dist = ultrasonicRead(PORT_ULTRASONIC)
self.tecla_pressed if dist > altura_total: dist = None 40
self.key_pressed porc = False
# Servidor int(100 - ((dist / altura_total) * 100))
return max(0, min(100, porc))
except: return 80
def validar_ip(self, ip): leer_luz():
try:
try:
socket.inet_aton(ip)
lectura = analogRead(PORT_SENSOR_LUZ)
return int((lectura / 800.0) * 100)
except: return 50
# Actuadores
def gestionar_calefactor(temp_actual):
target = float(config_actual.get('temp_min', 24.0))
if temp_actual < target:
digitalWrite(PORT_RELAY_CAL, 1) # Encender
return True
elif temp_actual > target + 0.5:
except socket.error:
digitalWrite(PORT_RELAY_CAL, 0) # Apagar
return False
return False
def validar_puerto(self, port): mover_servo_alimentar():
if alimentador_servo is None: return False
try:
try:
port print("Dispensando comida...")
alimentador_servo.angle = int(port)
90
time.sleep(0.8)
alimentador_servo.angle = 110
time.sleep(0.3)
alimentador_servo.angle = 90
time.sleep(0.3)
alimentador_servo.angle = 0
time.sleep(1)
alimentador_servo.value = None
return 1 <= port <= 65535 True
except Exception as e:
except ValueError:
print(f"Error Servo: {e}")
return False
def gestionar_comida():
global ultimo_minuto_alimentacion
if not config_actual.get('sistema_alimentacion_on', True):
return False
def conectar_servidor(self, ip_entry, port_entry, scene): hora_actual = datetime.now().strftime("%H:%M")
horarios = config_actual.get("horarios_comida", [])
if (hora_actual in horarios) and (hora_actual != ultimo_minuto_alimentacion):
ip print(f"Intentando alimentar ({hora_actual})...")
exito = ip_entry.get() mover_servo_alimentar()
port = port_entry.get()
if not self.validar_ip(ip): exito:
messagebox.showerror("Error", "Dirección IP no válida.") ultimo_minuto_alimentacion = hora_actual
# EMOJI REMOVIDO AQUI
enviar_notificacion_push("Hora de comer!", f"Tu pez ha sido alimentado a las {hora_actual}")
guardar_historial_evento("Comida Servida", "Dispensador automatico activado con exito.")
return True
return False
def actualizar_lcd(nemo_id, temp, ph, luz, agua, calefactor_on, comida_servida):
try:
if not self.validar_puerto(port): comida_servida:
messagebox.showerror("Error", "Puerto no válido. Debe estar entre 1 y 65535.") setText(" DISPENSANDO\n COMIDA...")
time.sleep(3)
return
port
if calefactor_on: setRGB(255, 60, 0) # Naranja
else: setRGB(0, 255, 0) # Verde
setText(f"ID:{nemo_id}\nT:{temp}C pH:{ph}")
time.sleep(4)
setText(f"ID:{nemo_id}\nLuz:{luz}% Agua:{agua}%")
time.sleep(4)
except: pass
def main():
global nemo_id, ya_se_envio_notificacion
print("\n--- INICIANDO SAVENEMO ---")
inicializar_hardware()
nemo_id = int(port)
obtener_id_unico()
print(f"ID DISPOSITIVO: {nemo_id}")
esperar_internet()
iniciar_firebase()
print("Loop principal iniciado.\n")
while True:
try:
self.client_socket sincronizar_configuracion()
temp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) leer_temperatura()
self.client_socket.connect((ip, port))
self.combobox_controles.config(state="reondly")
self.combobox_controles.set("Botones")
self.crear_botones()
scene.destroy()
self.current_funcion ph = self.ventana_desconectar_servidor leer_ph()
self.current_image agua = self.imagen_conexion leer_nivel_agua()
self.canvas_image luz = self.canvas.create_image((self.x0 + self.x1) / 2, (self.y0 + self.y1) / 2, image=self.current_image) leer_luz()
alertas = []
self.canvas.image cfg = self.current_image
messagebox.showinfo("Éxito", "Conexión exitosa al servidor.")
except socket.error as e: config_actual
messagebox.showerror("Error", f"Error al conectar al servidor")
except Exception as e: if temp < float(cfg.get('temp_min', 20)): alertas.append("Temp Baja")
messagebox.showerror("Error", f"Ha ocurrido un error desconocido")
def ventana_conexion_servidor(self):
self.secondary_window if temp > float(cfg.get('temp_max', 35)): alertas.append("Temp Alta")
if agua < int(cfg.get('nivel_agua', 80)): alertas.append("Nivel Bajo")
if ph < int(cfg.get('ph_min', 80)): alertas.append("PH Bajo")
if ph > int(cfg.get('ph_max', 80)): alertas.append("PH Alto")
calefactor_on = tk.Toplevel(self.root)
self.secondary_window.title("Ingresar IP y Puerto")
self.secondary_window.geometry("400x225")
self.secondary_window.config(bg="#0F2B6A")
self.secondary_window.resizable(0, 0)
self.secondary_window.iconbitmap("resources2/logoBlitz.png")
tk.Label(self.secondary_window, text="IP del Servidor:", font=("Helvetica ", 9, "bold")).pack(pady=(20,5))
self.ip_entry gestionar_calefactor(temp)
comida_servida = tk.Entry(self.secondary_window)
self.ip_entry.pack(pady=5)
tk.Label(self.secondary_window, text="Puerto del Servidor:", font=("Helvetica gestionar_comida()
if alertas:
if not ya_se_envio_notificacion:
print(f"Alertas: {alertas}")
# EMOJI REMOVIDO AQUI
enviar_notificacion_push("ALERTA ACUARIO ", 9, "bold")).pack(pady=5)
self.port_entry f"Atencion: {', '.join(alertas)}")
guardar_historial_evento("Alerta Detectada", ', '.join(alertas))
ya_se_envio_notificacion = tk.Entry(self.secondary_window)
self.port_entry.pack(pady=5)
confirm_button True
else:
if ya_se_envio_notificacion: ya_se_envio_notificacion = tk.Button(self.secondary_window, text="Conectar", bg="#22c8c9", command=lambda: self.conectar_servidor(self.ip_entry, self.port_entry, self.secondary_window))
confirm_button.pack(pady=5)
ip_save_button False
subir_estado(temp, ph, luz, agua, alertas)
cal_status = tk.Button(self.secondary_window, text="Ingresar IP guardada", bg="#22c8c9", command=lambda: self.ingresar_ip_guardada(self.ip_entry, self.port_entry))
ip_save_button.pack()
def ingresar_ip_guardada(self, ip_entry, port_entry):
ip_entry.delete(0, tk.END)
ip_entry.insert(0, "192.168.144.84")
port_entry.delete(0, tk.END)
port_entry.insert(0, "8080")
def ventana_desconectar_servidor(self):
'ON' if messagebox.askyesno("Confirmación", "¿Está seguro que quiere desconectar del servidor?"):
self.client_socket.close() calefactor_on else 'OFF'
self.current_funcion = self.ventana_conexion_servidor # Solo para ver por terminal
self.current_image = self.imagen_desconexion print(f"[{datetime.now().strftime('%H:%M')}] T:{temp}C pH:{ph} L:{luz}% A:{agua}% Cal:{cal_status}")
actualizar_lcd(nemo_id, temp, ph, luz, agua, calefactor_on, comida_servida)
except KeyboardInterrupt:
self.canvas_image = self.canvas.create_image((self.x0 + self.x1) / 2, (self.y0 + self.y1) / 2, image=self.current_image) print("\nApagando sistema...")
self.canvas.image = self.current_image try:
digitalWrite(PORT_RELAY_CAL, 0)
setRGB(0, 0, 0)
setText("")
if alimentador_servo: alimentador_servo.detach()
self.eliminar_teclas_y_botones()
def moveUp(self): except: pass
break
self.client_socket.send(bytes([ord('w')]))
def moveDown(self):
self.client_socket.send(bytes([ord('s')]))
def moveRight(self):
self.client_socket.send(bytes([ord('d')]))
def moveLeft(self):
self.client_socket.send(bytes([ord('a')]))
def upCraw(self):
self.client_socket.send(bytes([ord('l')]))
def downCraw(self):
self.client_socket.send(bytes([ord('k')]))
def grab(self):
self.client_socket.send(bytes([ord('x')]))
def drop(self):
self.client_socket.send(bytes([ord('c')]))
def stop(self):
self.client_socket.send(bytes([ord(' ')])) except Exception as e:
print(f"Error en Loop: {e}")
time.sleep(5)
if __name__ == "__main__":
root = tk.Tk()
app = Aplicacion(root)
root.mainloop()
main()
</code></pre>