MaselliSimulatorApp/tabs/simulator_tab.py

660 lines
35 KiB
Python

"""
Tab del Simulador - Genera valores de prueba en protocolo ADAM
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import threading
import time
import math
from collections import deque
from protocol_handler import ProtocolHandler
from connection_manager import ConnectionManager
from utils import Utils
class SimulatorTab:
def __init__(self, parent_frame, shared_config):
self.frame = parent_frame
self.shared_config = shared_config
# Estado del simulador
self.simulating = False
self.simulation_thread = None
self.simulation_step = 0
self.connection_manager = ConnectionManager()
# Datos para el gráfico
self.max_points = 100
self.time_data = deque(maxlen=self.max_points)
self.brix_data = deque(maxlen=self.max_points)
self.ma_data = deque(maxlen=self.max_points)
self.start_time = time.time()
# Cargar configuración inicial para obtener valores por defecto para StringVars
# Esto es para asegurar que las StringVars tengan un valor inicial antes de que set_config sea llamado
# por maselli_app.py.
initial_config = self.shared_config['config_manager'].load_config()
self.adam_address_var = tk.StringVar(value=initial_config.get('adam_address', '01'))
self.function_type_var = tk.StringVar(value=initial_config.get('function_type', 'Lineal'))
self.cycle_time_var = tk.StringVar(value=initial_config.get('cycle_time', '10.0'))
self.samples_per_cycle_var = tk.StringVar(value=initial_config.get('samples_per_cycle', '100'))
self.manual_input_type_var = tk.StringVar(value=initial_config.get('manual_input_type', 'Brix'))
self.manual_value_var = tk.StringVar(value=initial_config.get('manual_value', '10.0'))
try:
manual_value_float = float(initial_config.get('manual_value', '10.0'))
except ValueError:
manual_value_float = 10.0 # Fallback
self.manual_slider_var = tk.DoubleVar(value=manual_value_float)
self.current_brix_var = tk.StringVar(value="---")
self.current_ma_var = tk.StringVar(value="--.-- mA")
self.current_voltage_var = tk.StringVar(value="-.-- V") # Nueva para voltaje
self.create_widgets()
def create_widgets(self):
"""Crea los widgets del tab simulador"""
# Frame de configuración del simulador
config_frame = ttk.LabelFrame(self.frame, text="Configuración Simulador")
config_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew", columnspan=2)
# Dirección ADAM
ttk.Label(config_frame, text="ADAM Address (2c):").grid(row=0, column=0, padx=5, pady=5, sticky="w")
# self.adam_address_var inicializada en __init__
self.adam_address_entry = ttk.Entry(config_frame, textvariable=self.adam_address_var, width=5)
self.adam_address_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
# Función
ttk.Label(config_frame, text="Función:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
# self.function_type_var inicializada en __init__
self.function_type_combo = ttk.Combobox(config_frame, textvariable=self.function_type_var,
values=["Lineal", "Sinusoidal", "Manual"],
state="readonly", width=10)
self.function_type_combo.grid(row=0, column=3, padx=5, pady=5, sticky="ew")
self.function_type_combo.bind("<<ComboboxSelected>>", self.on_function_type_change)
# Tiempo de ciclo completo (nueva característica)
ttk.Label(config_frame, text="Tiempo Ciclo (s):").grid(row=0, column=4, padx=5, pady=5, sticky="w")
# self.cycle_time_var inicializada en __init__
self.cycle_time_entry = ttk.Entry(config_frame, textvariable=self.cycle_time_var, width=8)
self.cycle_time_entry.grid(row=0, column=5, padx=5, pady=5, sticky="ew")
# Velocidad de muestreo (calculada automáticamente)
ttk.Label(config_frame, text="Muestras/ciclo:").grid(row=0, column=6, padx=5, pady=5, sticky="w")
# self.samples_per_cycle_var inicializada en __init__
self.samples_per_cycle_entry = ttk.Entry(config_frame, textvariable=self.samples_per_cycle_var, width=8)
self.samples_per_cycle_entry.grid(row=0, column=7, padx=5, pady=5, sticky="ew")
# --- Frame para modo Manual (Modificado) ---
manual_frame = ttk.LabelFrame(config_frame, text="Modo Manual")
manual_frame.grid(row=1, column=0, columnspan=8, padx=5, pady=5, sticky="ew")
ttk.Label(manual_frame, text="Entrada Por:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
# self.manual_input_type_var inicializada en __init__
self.manual_input_type_combo = ttk.Combobox(manual_frame, textvariable=self.manual_input_type_var,
values=["Brix", "mA", "Voltaje"], state="readonly", width=8)
self.manual_input_type_combo.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
self.manual_input_type_combo.bind("<<ComboboxSelected>>", self.on_manual_input_type_change)
self.manual_value_label = ttk.Label(manual_frame, text="Valor Brix:") # Se actualiza dinámicamente
self.manual_value_label.grid(row=1, column=0, padx=5, pady=5, sticky="w")
# self.manual_value_var y self.manual_slider_var inicializadas en __init__
self.manual_value_entry = ttk.Entry(manual_frame, textvariable=self.manual_value_var, width=10, state=tk.DISABLED)
self.manual_value_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
self.manual_value_entry.bind('<Return>', lambda e: self.update_slider_from_entry())
self.manual_value_entry.bind('<FocusOut>', lambda e: self.update_slider_from_entry())
# Slider
self.manual_slider = ttk.Scale(manual_frame, orient=tk.HORIZONTAL, # from_ y to_ se configuran dinámicamente
variable=self.manual_slider_var, command=self.on_slider_change,
state=tk.DISABLED, length=200)
self.manual_slider.grid(row=1, column=2, padx=5, pady=5, sticky="ew")
self.manual_send_button = ttk.Button(manual_frame, text="Enviar Manual",
command=self.send_manual_value, state=tk.DISABLED)
self.manual_send_button.grid(row=1, column=3, padx=5, pady=5, sticky="ew")
manual_frame.columnconfigure(2, weight=1)
# Controls Frame
controls_frame = ttk.LabelFrame(self.frame, text="Control Simulación")
controls_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew")
self.start_button = ttk.Button(controls_frame, text="Iniciar", command=self.start_simulation)
self.start_button.pack(side=tk.LEFT, padx=5)
self.stop_button = ttk.Button(controls_frame, text="Detener", command=self.stop_simulation, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=5)
self.clear_graph_button = ttk.Button(controls_frame, text="Limpiar Gráfico", command=self.clear_graph)
self.clear_graph_button.pack(side=tk.LEFT, padx=5)
# Display Frame
display_frame = ttk.LabelFrame(self.frame, text="Valores Actuales")
display_frame.grid(row=1, column=1, padx=10, pady=5, sticky="ew")
ttk.Label(display_frame, text="Brix:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
# self.current_brix_var inicializada en __init__
ttk.Label(display_frame, textvariable=self.current_brix_var,
font=("Courier", 14, "bold")).grid(row=0, column=1, padx=5, pady=5, sticky="w")
ttk.Label(display_frame, text="mA:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
# self.current_ma_var inicializada en __init__
ttk.Label(display_frame, textvariable=self.current_ma_var,
font=("Courier", 14, "bold")).grid(row=1, column=1, padx=5, pady=5, sticky="w")
ttk.Label(display_frame, text="Voltaje:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
# self.current_voltage_var inicializada en __init__
ttk.Label(display_frame, textvariable=self.current_voltage_var,
font=("Courier", 14, "bold")).grid(row=2, column=1, padx=5, pady=5, sticky="w")
# Log Frame
log_frame = ttk.LabelFrame(self.frame, text="Log de Comunicación")
log_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
self.log_text = scrolledtext.ScrolledText(log_frame, height=8, width=70, wrap=tk.WORD, state=tk.DISABLED)
self.log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
# Configurar pesos
self.frame.columnconfigure(0, weight=1)
self.frame.columnconfigure(1, weight=1)
self.frame.rowconfigure(2, weight=1) # Log frame
# Inicializar estado
self.on_function_type_change()
def get_graph_frame(self):
"""Crea y retorna el frame para el gráfico"""
graph_frame = ttk.LabelFrame(self.frame, text="Gráfico Simulador")
graph_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
self.frame.rowconfigure(3, weight=1) # Graph frame
return graph_frame
def on_function_type_change(self, event=None):
"""Maneja el cambio de tipo de función"""
func_type = self.function_type_var.get()
if func_type == "Manual":
if self.simulating:
self.stop_simulation()
self.manual_input_type_combo.config(state=tk.NORMAL)
self.manual_value_entry.config(state=tk.NORMAL)
self.manual_send_button.config(state=tk.NORMAL)
self.manual_slider.config(state=tk.NORMAL)
self.cycle_time_entry.config(state=tk.DISABLED)
self.samples_per_cycle_entry.config(state=tk.DISABLED)
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.DISABLED)
self.on_manual_input_type_change() # Configurar según el tipo actual
else:
self.manual_input_type_combo.config(state=tk.DISABLED)
self.manual_value_entry.config(state=tk.DISABLED)
self.manual_send_button.config(state=tk.DISABLED)
self.manual_slider.config(state=tk.DISABLED)
self.cycle_time_entry.config(state=tk.NORMAL)
self.samples_per_cycle_entry.config(state=tk.NORMAL)
if not self.simulating:
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
def on_manual_input_type_change(self, event=None):
"""Maneja el cambio de tipo de entrada manual (Brix, mA, Voltaje)"""
input_type = self.manual_input_type_var.get()
min_val, max_val, default_val, label_text, precision = 0, 100, 10.0, "Valor Brix:", 2
if input_type == "Brix":
try:
min_val = float(self.shared_config['min_brix_map_var'].get())
max_val = float(self.shared_config['max_brix_map_var'].get())
if min_val >= max_val: min_val, max_val = 0.0, 80.0 # Fallback
default_val = min_val + (max_val - min_val) / 4
except (ValueError, KeyError, TypeError):
min_val, max_val = 0.0, 80.0
default_val = 10.0
label_text = "Valor Brix:"
precision = 2
elif input_type == "mA":
min_val, max_val = 0.0, 20.0
default_val = 12.0
label_text = "Valor mA:"
precision = 3
elif input_type == "Voltaje":
min_val, max_val = 0.0, 10.0
default_val = 5.0
label_text = "Valor Voltaje:"
precision = 2
self.manual_value_label.config(text=label_text)
self.manual_slider.config(from_=min_val, to=max_val)
try:
current_numeric_val = float(self.manual_value_var.get())
if not (min_val <= current_numeric_val <= max_val):
self.manual_value_var.set(f"{default_val:.{precision}f}")
self.manual_slider_var.set(default_val)
else:
self.manual_slider_var.set(current_numeric_val)
self.manual_value_var.set(f"{current_numeric_val:.{precision}f}")
except ValueError:
self.manual_value_var.set(f"{default_val:.{precision}f}")
self.manual_slider_var.set(default_val)
def on_slider_change(self, value_str):
"""Actualiza el valor del entry cuando cambia el slider"""
value = float(value_str)
input_type = self.manual_input_type_var.get()
precision = 2
if input_type == "Brix": precision = 2
elif input_type == "mA": precision = 3
elif input_type == "Voltaje": precision = 2
self.manual_value_var.set(f"{value:.{precision}f}")
def update_slider_from_entry(self):
"""Actualiza el slider cuando cambia el entry"""
try:
value = float(self.manual_value_var.get())
input_type = self.manual_input_type_var.get()
min_val, max_val, precision = 0,100,2
if input_type == "Brix":
min_val = float(self.shared_config['min_brix_map_var'].get())
max_val = float(self.shared_config['max_brix_map_var'].get())
if min_val >= max_val: min_val, max_val = 0.0, 80.0
precision = 2
elif input_type == "mA": min_val, max_val, precision = 0.0, 20.0, 3
elif input_type == "Voltaje": min_val, max_val, precision = 0.0, 10.0, 2
value = max(min_val, min(max_val, value)) # Clampear al rango
self.manual_slider_var.set(value)
self.manual_value_var.set(f"{value:.{precision}f}")
except (ValueError, KeyError, TypeError):
# Si el valor no es un número o shared_config no está listo, resetear al valor del slider
current_slider_val = self.manual_slider_var.get()
precision_fallback = 2
if self.manual_input_type_var.get() == "mA": precision_fallback = 3
self.manual_value_var.set(f"{current_slider_val:.{precision_fallback}f}")
def send_manual_value(self):
"""Envía un valor manual único"""
try:
# Obtener valores de mapeo
min_brix_map = float(self.shared_config['min_brix_map_var'].get())
max_brix_map = float(self.shared_config['max_brix_map_var'].get())
adam_address = self.adam_address_var.get()
if len(adam_address) != 2:
messagebox.showerror("Error", "La dirección ADAM debe tener 2 caracteres.")
return
if min_brix_map >= max_brix_map:
messagebox.showerror("Error de Configuración", "Min Brix debe ser menor que Max Brix.")
return
input_type = self.manual_input_type_var.get()
manual_numeric_value = float(self.manual_value_var.get())
final_brix, final_ma, final_voltage = 0.0, 0.0, 0.0
if input_type == "Brix":
final_brix = manual_numeric_value
final_ma = ProtocolHandler.scale_to_ma(final_brix, min_brix_map, max_brix_map)
final_voltage = ProtocolHandler.ma_to_voltage(final_ma)
elif input_type == "mA":
final_ma = manual_numeric_value
final_brix = ProtocolHandler.ma_to_brix(final_ma, min_brix_map, max_brix_map)
final_voltage = ProtocolHandler.ma_to_voltage(final_ma)
elif input_type == "Voltaje":
final_voltage = manual_numeric_value
final_ma = ProtocolHandler.voltage_to_ma(final_voltage)
final_brix = ProtocolHandler.ma_to_brix(final_ma, min_brix_map, max_brix_map)
# Usar el nuevo método que toma ma_value directamente para el modo manual
message, returned_ma = ProtocolHandler.create_adam_message_from_ma(adam_address, final_ma)
# Actualizar display
brix_display_text = ""
# Si la entrada fue mA o Voltaje y el resultado es un mA < 4 (estado de error)
if (input_type == "mA" or input_type == "Voltaje") and final_ma < 4.0:
brix_display_text = "Error (Sub 4mA)"
else:
# Para entrada Brix, o mA/Voltaje que resultan en mA >= 4.0
brix_display_text = Utils.format_brix_display(final_brix)
self.current_brix_var.set(brix_display_text)
self.current_ma_var.set(Utils.format_ma_display(returned_ma)) # Usar returned_ma que es igual a final_ma
self.current_voltage_var.set(ProtocolHandler.format_voltage_display(final_voltage))
# Agregar al gráfico
# final_brix aquí es el valor numérico calculado (ej. min_brix_map si mA < 4),
# lo cual es consistente para el gráfico.
self.add_data_point(final_brix, returned_ma)
# Enviar por conexión temporal
current_config_values = {
'connection_type': self.shared_config['connection_type_var'].get(),
'com_port': self.shared_config['com_port_var'].get(),
'baud_rate': self.shared_config['baud_rate_var'].get(),
'ip_address': self.shared_config['ip_address_var'].get(),
'port': self.shared_config['port_var'].get(),
}
conn_type = current_config_values['connection_type']
conn_params = self.shared_config['config_manager'].get_connection_params(current_config_values)
if conn_type == "TCP-Server":
if not self.connection_manager.is_client_connected(): # Verificar el connection_manager de la pestaña
Utils.log_message(self.log_text, "Envío Manual (TCP Server): Ningún cliente conectado.")
messagebox.showinfo("TCP Server", "Ningún cliente conectado para enviar datos manualmente.")
return
# No necesitamos 'listening_details' aquí porque la conexión ya está establecida
# y el log de inicio ya se hizo. Solo usamos la conexión existente.
# La llamada a open_connection no ocurre aquí para TCP-Server en modo manual.
try:
# message ya es bytes
self.connection_manager.send_data(message)
Utils.log_message(self.log_text, f"Enviando Manual (TCP Server): {ProtocolHandler.format_for_display(message)}")
# No se espera respuesta en modo servidor para el simulador
except self.connection_manager.ClientDisconnectedError:
Utils.log_message(self.log_text, "Envío Manual (TCP Server): Cliente desconectado durante el envío.")
messagebox.showerror("TCP Server Error", "El cliente se desconectó durante el envío manual.")
except Exception as e_manual_server:
Utils.log_message(self.log_text, f"Error al enviar manualmente (TCP Server): {e_manual_server}")
messagebox.showerror("Error", str(e_manual_server))
return # Terminar aquí para envío manual en TCP-Server
# Lógica existente para otros tipos de conexión (Serial, TCP Client, UDP)
else:
temp_conn = ConnectionManager()
try:
# open_connection ahora devuelve (connection_object, listening_info)
_, _ = temp_conn.open_connection(conn_type, conn_params) # Ignoramos listening_info para conexión temporal
Utils.log_message(self.log_text, f"Conexión {conn_type} abierta temporalmente.")
Utils.log_message(self.log_text, f"Enviando Manual: {ProtocolHandler.format_for_display(message)}")
temp_conn.send_data(message) # message ya es bytes
response = temp_conn.read_response(timeout=0.5)
if response and response.strip():
Utils.log_message(self.log_text, f"Respuesta: {ProtocolHandler.format_for_display(response)}")
parsed = ProtocolHandler.parse_adam_message(response)
if parsed:
brix_resp = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix_map, max_brix_map)
Utils.log_message(self.log_text,
f" -> Addr: {parsed['address']}, "
f"mA: {parsed['ma']:.3f}, "
f"Brix: {brix_resp:.3f}")
except Exception as e:
Utils.log_message(self.log_text, f"Error al enviar: {e}")
messagebox.showerror("Error", str(e))
finally:
temp_conn.close_connection()
Utils.log_message(self.log_text, "Conexión cerrada.")
except (ValueError, KeyError, TypeError) as e:
messagebox.showerror("Error", f"Valores inválidos en la configuración o entrada: {e}")
def start_simulation(self):
"""Inicia la simulación continua"""
if self.simulating:
messagebox.showwarning("Advertencia", "La simulación ya está en curso.")
return
try:
adam_address = self.adam_address_var.get()
if len(adam_address) != 2:
messagebox.showerror("Error", "La dirección ADAM debe tener 2 caracteres.")
return
cycle_time = float(self.cycle_time_var.get())
if cycle_time <= 0:
messagebox.showerror("Error", "El tiempo de ciclo debe ser mayor que 0.")
return
samples_per_cycle = int(self.samples_per_cycle_var.get())
if samples_per_cycle <= 0:
messagebox.showerror("Error", "Las muestras por ciclo deben ser mayor que 0.")
return
# Validar mapeo Brix
float(self.shared_config['min_brix_map_var'].get())
float(self.shared_config['max_brix_map_var'].get())
except (ValueError, KeyError, TypeError):
messagebox.showerror("Error", "Valores inválidos en la configuración (ADAM, ciclo, muestras o mapeo Brix).")
return
try:
current_config_values = {
'connection_type': self.shared_config['connection_type_var'].get(),
'com_port': self.shared_config['com_port_var'].get(),
'baud_rate': self.shared_config['baud_rate_var'].get(),
'ip_address': self.shared_config['ip_address_var'].get(),
'port': self.shared_config['port_var'].get(),
}
conn_type = current_config_values['connection_type']
conn_params = self.shared_config['config_manager'].get_connection_params(current_config_values)
# open_connection ahora devuelve (connection_object, listening_info)
# El connection_object se guarda internamente en self.connection_manager
_, listening_details = self.connection_manager.open_connection(conn_type, conn_params)
if conn_type == "TCP-Server":
Utils.log_message(self.log_text, f"{listening_details} para simulación.")
elif conn_type != "TCP-Server": # Para otros tipos, el mensaje genérico
Utils.log_message(self.log_text, f"Conexión {conn_type} abierta para simulación.")
except Exception as e:
messagebox.showerror("Error de Conexión", str(e))
return
self.simulating = True
self.simulation_step = 0
self.start_time = time.time() # Reset start time for graph
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self._set_entries_state(tk.DISABLED)
if conn_type == "TCP-Server":
self.shared_config['client_connected_var'].set("Esperando...")
self.simulation_thread = threading.Thread(target=self.run_simulation, daemon=True)
self.simulation_thread.start()
Utils.log_message(self.log_text, "Simulación iniciada.")
def stop_simulation(self):
"""Detiene la simulación"""
if not self.simulating:
return
self.simulating = False
if self.simulation_thread and self.simulation_thread.is_alive():
self.simulation_thread.join(timeout=2.0)
self.connection_manager.close_connection()
Utils.log_message(self.log_text, "Conexión cerrada.")
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self._set_entries_state(tk.NORMAL)
self.on_function_type_change() # Re-evaluar estado de controles manuales
if self.connection_manager.connection_type == "TCP-Server": # Limpiar info del cliente
self.shared_config['client_connected_var'].set("Ninguno")
Utils.log_message(self.log_text, "Simulación detenida.")
self.current_brix_var.set("---")
self.current_ma_var.set("--.-- mA")
self.current_voltage_var.set("-.-- V")
def run_simulation(self):
"""Thread principal de simulación"""
try:
adam_address = self.adam_address_var.get()
min_brix_map = float(self.shared_config['min_brix_map_var'].get())
max_brix_map = float(self.shared_config['max_brix_map_var'].get())
function_type = self.function_type_var.get()
cycle_time = float(self.cycle_time_var.get())
samples_per_cycle = int(self.samples_per_cycle_var.get())
conn_type = self.connection_manager.connection_type # Obtener el tipo de conexión actual
# Obtener la configuración actual para el log del puerto en TCP-Server
current_config_values = {
'connection_type': self.shared_config['connection_type_var'].get(),
'com_port': self.shared_config['com_port_var'].get(),
'baud_rate': self.shared_config['baud_rate_var'].get(),
'ip_address': self.shared_config['ip_address_var'].get(),
'port': self.shared_config['port_var'].get(),
}
sample_period = cycle_time / samples_per_cycle
while self.simulating:
current_brix = 0.0
progress = (self.simulation_step % samples_per_cycle) / samples_per_cycle
if function_type == "Lineal":
cycle_progress = (self.simulation_step % (2 * samples_per_cycle)) / samples_per_cycle
if cycle_progress > 1.0:
cycle_progress = 2.0 - cycle_progress
current_brix = min_brix_map + (max_brix_map - min_brix_map) * cycle_progress
elif function_type == "Sinusoidal":
phase = progress * 2 * math.pi
sin_val = (math.sin(phase) + 1) / 2
current_brix = min_brix_map + (max_brix_map - min_brix_map) * sin_val
message, ma_value = ProtocolHandler.create_adam_message(adam_address, current_brix, min_brix_map, max_brix_map)
voltage_value = ProtocolHandler.ma_to_voltage(ma_value)
self.current_brix_var.set(Utils.format_brix_display(current_brix))
self.current_ma_var.set(Utils.format_ma_display(ma_value))
self.current_voltage_var.set(ProtocolHandler.format_voltage_display(voltage_value))
self.frame.after(0, lambda b=current_brix, m=ma_value: self.add_data_point(b, m))
try:
if conn_type == "TCP-Server":
if not self.connection_manager.is_client_connected():
# Loguear solo si el estado cambia o periódicamente para evitar spam
if not hasattr(self, '_waiting_for_client_logged') or not self._waiting_for_client_logged:
port_to_log = self.shared_config['config_manager'].get_connection_params(current_config_values)['port']
Utils.log_message(self.log_text, f"TCP Server: Esperando cliente en puerto {port_to_log}...")
self._waiting_for_client_logged = True
if self.connection_manager.accept_client(timeout=0.05): # Intento corto no bloqueante
Utils.log_message(self.log_text, f"TCP Server: Cliente conectado desde {self.connection_manager.client_address}")
client_info = f"{self.connection_manager.client_address[0]}:{self.connection_manager.client_address[1]}"
self.shared_config['client_connected_var'].set(client_info)
self._waiting_for_client_logged = False # Resetear flag de log
elif not self.connection_manager.is_client_connected() and \
self.shared_config['client_connected_var'].get() != "Esperando...":
self.shared_config['client_connected_var'].set("Esperando...")
Utils.log_message(self.log_text, f"Enviando: {ProtocolHandler.format_for_display(message)}")
self.connection_manager.send_data(message)
if conn_type != "TCP-Server": # No leer respuesta en modo servidor
response = self.connection_manager.read_response(timeout=0.1)
if response and response.strip():
Utils.log_message(self.log_text, f"Respuesta: {ProtocolHandler.format_for_display(response)}")
parsed = ProtocolHandler.parse_adam_message(response)
if parsed:
brix_resp = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix_map, max_brix_map)
Utils.log_message(self.log_text,
f" -> Addr: {parsed['address']}, "
f"mA: {parsed['ma']:.3f}, "
f"Brix: {brix_resp:.3f}")
except self.connection_manager.ClientDisconnectedError:
Utils.log_message(self.log_text, "TCP Server: Cliente desconectado. Esperando nueva conexión.")
if conn_type == "TCP-Server":
self.shared_config['client_connected_var'].set("Esperando...")
self._waiting_for_client_logged = False # Permitir que se loguee "esperando" de nuevo
except Exception as e:
Utils.log_message(self.log_text, f"Error en comunicación ({conn_type}): {e}")
self.frame.after(0, self.stop_simulation_error) # Schedule GUI update from main thread
break
self.simulation_step += 1
time.sleep(sample_period)
except Exception as e: # Catches errors in parameter fetching or main loop logic
Utils.log_message(self.log_text, f"Error en simulación: {e}")
if self.simulating: # Ensure stop is called only if an error occurs while simulating
self.frame.after(0, self.stop_simulation_error)
def stop_simulation_error(self):
"""Detiene la simulación debido a un error y muestra mensaje"""
if self.simulating: # Solo actuar si la simulación estaba activa
messagebox.showerror("Error de Simulación", "Error durante la simulación. Simulación detenida.")
self.stop_simulation() # Llama al método normal de parada
def add_data_point(self, brix_value, ma_value):
"""Agrega un punto de datos al gráfico"""
current_time = time.time() - self.start_time
self.time_data.append(current_time)
self.brix_data.append(brix_value)
self.ma_data.append(ma_value)
if hasattr(self, 'graph_update_callback'):
self.graph_update_callback()
def clear_graph(self):
"""Limpia los datos del gráfico"""
Utils.clear_graph_data(self.time_data, self.brix_data, self.ma_data)
self.start_time = time.time()
Utils.log_message(self.log_text, "Gráfico limpiado.")
if hasattr(self, 'graph_update_callback'):
self.graph_update_callback()
def _set_entries_state(self, state):
"""Habilita/deshabilita los controles durante la simulación"""
sim_specific_widgets = [
self.adam_address_entry,
self.function_type_combo,
self.cycle_time_entry,
self.samples_per_cycle_entry
]
# No deshabilitar controles de modo manual aquí, se manejan en on_function_type_change
Utils.set_widgets_state(sim_specific_widgets, state)
if 'shared_widgets' in self.shared_config:
Utils.set_widgets_state(self.shared_config['shared_widgets'], state)
def get_config(self):
"""Obtiene la configuración actual del simulador"""
return {
'adam_address': self.adam_address_var.get(),
'function_type': self.function_type_var.get(),
'cycle_time': self.cycle_time_var.get(),
'samples_per_cycle': self.samples_per_cycle_var.get(),
'manual_input_type': self.manual_input_type_var.get(),
'manual_value': self.manual_value_var.get()
}
def set_config(self, config):
"""Establece la configuración del simulador"""
self.adam_address_var.set(config.get('adam_address', '01'))
self.function_type_var.set(config.get('function_type', 'Lineal'))
self.cycle_time_var.set(config.get('cycle_time', '10.0'))
self.samples_per_cycle_var.set(config.get('samples_per_cycle', '100'))
self.manual_input_type_var.set(config.get('manual_input_type', 'Brix'))
self.manual_value_var.set(config.get('manual_value', '10.0'))
try:
self.manual_slider_var.set(float(self.manual_value_var.get()))
except ValueError:
# Si el valor no es un float válido, intentar con un default o el valor del tipo
# Esto se manejará mejor en on_manual_input_type_change
pass
self.on_function_type_change() # Esto llamará a on_manual_input_type_change si es necesario