Project

General

Profile

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
&quot;savenemo_id.txt&quot;
FILE_CONFIG_LOCAL
= None
self.botones_presionados
&quot;config.json&quot;
FILE_CREDENTIALS
= set()

self.lista_botones
&quot;serviceAccountKey.json&quot;

# Configuracion Sensor Temperatura
SENSOR_TEMP_ID
= []
self.contenedor
&quot;28-00000053d2ea&quot;
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&quot;/sys/bus/w1/devices/{SENSOR_TEMP_ID}/w1_slave&quot;

# 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

&quot;nombre_pez&quot;: &quot;Esperando App...&quot;,
&quot;temp_min&quot;: 24.0, &quot;temp_max&quot;: 28.0,
&quot;ph_min&quot;: 6.5, &quot;ph_max&quot;: 7.5,
&quot;nivel_luz&quot;: 50, &quot;nivel_agua&quot;: 80,
&quot;horarios_comida&quot;: [],
&quot;sistema_alimentacion_on&quot;: 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
&quot;&quot;
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
&quot;&quot;
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():
&quot;&quot;&quot;Lee o genera un ID unico para identificar esta pecera.&quot;&quot;&quot;
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, &#39;r&#39;) 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&quot;NEMO-{random.randint(1000, 9999)}&quot;
self.canvas.create_window(275, 170, window=self.label1) with open(FILE_ID, &#39;w&#39;) 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(&#39;https://www.google.com&#39;, 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(&quot;Buscando conexion a internet...&quot;)
while not verificar_internet():

self.label1.destroy()
self.label2.destroy()
self.label_agarrar.destroy()
self.label_soltar.destroy()

time.sleep(3)
print(&quot;Conexion establecida.&quot;)

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(&quot;Firebase conectado exitosamente.&quot;)
}
for k, v in estilos.items():
else:
self.root.option_add(k, v)

# Botones
print(&quot;Falta archivo serviceAccountKey.json&quot;)
except Exception as e:
print(f&quot;Error Firebase: {e}&quot;)
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(&#39;acuarios&#39;).document(nemo_id).collection(&#39;data&#39;).document(&#39;config&#39;).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(&quot;Configuracion actualizada desde la nube.&quot;)
config_actual.update(datos)
with open(FILE_CONFIG_LOCAL, &#39;w&#39;) as f: json.dump(config_actual, f)
except: pass

def eliminar_botones(self): subir_estado(temp, ph, luz, agua, alertas):
&quot;&quot;&quot;Sube los datos de sensores a Firestore.&quot;&quot;&quot;
if not firebase_activo: return
try:

for boton in self.lista_botones: db.collection(&#39;acuarios&#39;).document(nemo_id).collection(&#39;data&#39;).document(&#39;estado&#39;).set({
boton.destroy() &#39;temp_actual&#39;: temp, &#39;ph_actual&#39;: ph, &#39;luz_actual&#39;: luz, &#39;agua_nivel&#39;: agua,
&#39;alertas&#39;: alertas, &#39;ultima_actualizacion&#39;: 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(&#39;acuarios&#39;).document(nemo_id).collection(&#39;data&#39;).document(&#39;tokens&#39;)
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(&#39;token_celular&#39;) 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&quot;Push enviado: {titulo}&quot;)
except Exception as e:
print(f&quot;Error push: {e}&quot;)

def activar_teclas_teclado(self): guardar_historial_evento(titulo, mensaje):
if not firebase_activo: return
try:

for tecla in self.teclas_teclado: db.collection(&#39;acuarios&#39;).document(nemo_id).collection(&#39;historial&#39;).add({
self.root.bind(f"<KeyPress-{tecla}>", self.crear_callback(tecla, self.pressed)) &#39;titulo&#39;: titulo,
self.root.bind(f"<KeyRelease-{tecla}>", self.crear_callback(tecla, self.released)) &#39;mensaje&#39;: mensaje,
self.root.bind(f"<KeyPress-{tecla.upper()}>", self.crear_callback(tecla, self.pressed)) &#39;fecha&#39;: datetime.now().strftime(&quot;%Y-%m-%d %H:%M:%S&quot;),
self.root.bind(f"<KeyRelease-{tecla.upper()}>", self.crear_callback(tecla, self.released))

&#39;timestamp&#39;: 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, &quot;OUTPUT&quot;)
digitalWrite(PORT_RELAY_CAL, 0)

# LCD
try:

self.root.unbind(f"<KeyPress-{tecla}>") setText(&quot;INICIANDO\nSISTEMA...&quot;)
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(&quot;LCD no detectado (continuando...)&quot;)

# 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(&quot;Hardware inicializado correctamente.&quot;)
except Exception as e:
print(f&quot;Error Hardware: {e}&quot;)
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(&quot;Sensor de temperatura no encontrado (Cable desconectado?)&quot;)
return 25.0


with open(SENSOR_TEMP_PATH, &#39;r&#39;) as f:
lineas = f.readlines()

if tecla lineas[0].strip()[-3:] == self.tecla_pressed: &#39;YES&#39;:
self.canvas.itemconfig(self.contenedor[tecla], fill="#0F2B6A")
self.tecla_pressed
posicion_t = None lineas[1].find(&#39;t=&#39;)
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&quot;Error leyendo temp: {e}&quot;)

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 &gt; 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(&#39;temp_min&#39;, 24.0))
if temp_actual &lt; target:
digitalWrite(PORT_RELAY_CAL, 1) # Encender
return
True
elif temp_actual &gt; 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(&quot;Dispensando comida...&quot;)
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&quot;Error Servo: {e}&quot;)
return False

def gestionar_comida():
global ultimo_minuto_alimentacion
if not config_actual.get(&#39;sistema_alimentacion_on&#39;, True):
return False


def conectar_servidor(self, ip_entry, port_entry, scene): hora_actual = datetime.now().strftime(&quot;%H:%M&quot;)
horarios = config_actual.get(&quot;horarios_comida&quot;, [])

if (hora_actual in horarios) and (hora_actual != ultimo_minuto_alimentacion):

ip print(f&quot;Intentando alimentar ({hora_actual})...&quot;)
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(&quot;Hora de comer!&quot;, f&quot;Tu pez ha sido alimentado a las {hora_actual}&quot;)
guardar_historial_evento(&quot;Comida Servida&quot;, &quot;Dispensador automatico activado con exito.&quot;)
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(&quot; DISPENSANDO\n COMIDA...&quot;)
time.sleep(3)
return

port


if calefactor_on: setRGB(255, 60, 0) # Naranja
else: setRGB(0, 255, 0) # Verde

setText(f&quot;ID:{nemo_id}\nT:{temp}C pH:{ph}&quot;)
time.sleep(4)
setText(f&quot;ID:{nemo_id}\nLuz:{luz}% Agua:{agua}%&quot;)
time.sleep(4)
except: pass

def main():
global nemo_id, ya_se_envio_notificacion

print(&quot;\n--- INICIANDO SAVENEMO ---&quot;)

inicializar_hardware()
nemo_id
= int(port)

obtener_id_unico()
print(f&quot;ID DISPOSITIVO: {nemo_id}&quot;)

esperar_internet()
iniciar_firebase()

print(&quot;Loop principal iniciado.\n&quot;)

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 &lt; float(cfg.get(&#39;temp_min&#39;, 20)): alertas.append(&quot;Temp Baja&quot;)
messagebox.showerror("Error", f"Ha ocurrido un error desconocido")


def ventana_conexion_servidor(self):
self.secondary_window
if temp &gt; float(cfg.get(&#39;temp_max&#39;, 35)): alertas.append(&quot;Temp Alta&quot;)
if agua &lt; int(cfg.get(&#39;nivel_agua&#39;, 80)): alertas.append(&quot;Nivel Bajo&quot;)
if ph &lt; int(cfg.get(&#39;ph_min&#39;, 80)): alertas.append(&quot;PH Bajo&quot;)
if ph &gt; int(cfg.get(&#39;ph_max&#39;, 80)): alertas.append(&quot;PH Alto&quot;)

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&quot;Alertas: {alertas}&quot;)
# EMOJI REMOVIDO AQUI
enviar_notificacion_push(&quot;ALERTA ACUARIO
", 9, "bold")).pack(pady=5)
self.port_entry
f&quot;Atencion: {&#39;, &#39;.join(alertas)}&quot;)
guardar_historial_evento(&quot;Alerta Detectada&quot;, &#39;, &#39;.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):
&#39;ON&#39; if messagebox.askyesno("Confirmación", "¿Está seguro que quiere desconectar del servidor?"):
self.client_socket.close()
calefactor_on else &#39;OFF&#39;

self.current_funcion = self.ventana_conexion_servidor # Solo para ver por terminal
self.current_image = self.imagen_desconexion print(f&quot;[{datetime.now().strftime(&#39;%H:%M&#39;)}] T:{temp}C pH:{ph} L:{luz}% A:{agua}% Cal:{cal_status}&quot;)

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(&quot;\nApagando sistema...&quot;)
self.canvas.image = self.current_image try:
digitalWrite(PORT_RELAY_CAL, 0)
setRGB(0, 0, 0)
setText(&quot;&quot;)
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&quot;Error en Loop: {e}&quot;)
time.sleep(5)


if __name__ == "__main__":
root = tk.Tk()
app = Aplicacion(root)
root.mainloop()
main()

</code></pre>