""" 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() 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 = tk.StringVar(value=self.shared_config.get('adam_address', '01')) 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 = tk.StringVar(value=self.shared_config.get('function_type', 'Lineal')) 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("<>", 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 = tk.StringVar(value=self.shared_config.get('cycle_time', '10.0')) 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 = tk.StringVar(value="100") 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 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="Valor Brix:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.manual_brix_var = tk.StringVar(value=self.shared_config.get('manual_brix', '10.0')) self.manual_brix_entry = ttk.Entry(manual_frame, textvariable=self.manual_brix_var, width=10, state=tk.DISABLED) self.manual_brix_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.manual_brix_entry.bind('', lambda e: self.update_slider_from_entry()) self.manual_brix_entry.bind('', lambda e: self.update_slider_from_entry()) # Slider self.manual_slider_var = tk.DoubleVar(value=float(self.shared_config.get('manual_brix', '10.0'))) self.manual_slider = ttk.Scale(manual_frame, from_=0, to=100, orient=tk.HORIZONTAL, variable=self.manual_slider_var, command=self.on_slider_change, state=tk.DISABLED, length=200) self.manual_slider.grid(row=0, 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=0, 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 = tk.StringVar(value="---") 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 = tk.StringVar(value="--.-- mA") ttk.Label(display_frame, textvariable=self.current_ma_var, font=("Courier", 14, "bold")).grid(row=1, 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) # 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) 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_brix_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) else: self.manual_brix_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_slider_change(self, value): """Actualiza el valor del entry cuando cambia el slider""" self.manual_brix_var.set(f"{float(value):.1f}") def update_slider_from_entry(self): """Actualiza el slider cuando cambia el entry""" try: value = float(self.manual_brix_var.get()) value = max(0, min(100, value)) self.manual_slider_var.set(value) self.manual_brix_var.set(f"{value:.1f}") except ValueError: pass def send_manual_value(self): """Envía un valor manual único""" try: # Obtener valores de mapeo min_brix = float(self.shared_config['min_brix_map_var'].get()) max_brix = 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 manual_brix = float(self.manual_brix_var.get()) # Crear mensaje message, ma_value = ProtocolHandler.create_adam_message(adam_address, manual_brix, min_brix, max_brix) # Actualizar display self.current_brix_var.set(Utils.format_brix_display(manual_brix)) self.current_ma_var.set(Utils.format_ma_display(ma_value)) # Agregar al gráfico self.add_data_point(manual_brix, ma_value) # Enviar por conexión temporal # Construct a dictionary of current config values for get_connection_params 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) temp_conn = ConnectionManager() try: temp_conn.open_connection(conn_type, conn_params) 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) # Intentar leer respuesta 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, max_brix) 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 as e: messagebox.showerror("Error", "Valores inválidos en la configuración.") def start_simulation(self): """Inicia la simulación continua""" if self.simulating: messagebox.showwarning("Advertencia", "La simulación ya está en curso.") return # Validar configuración 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 except ValueError: messagebox.showerror("Error", "Valores inválidos en la configuración.") return # Abrir conexión try: # Construct a dictionary of current config values for get_connection_params 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) self.connection_manager.open_connection(conn_type, conn_params) 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_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self._set_entries_state(tk.DISABLED) 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() Utils.log_message(self.log_text, "Simulación detenida.") self.current_brix_var.set("---") self.current_ma_var.set("--.-- mA") def run_simulation(self): """Thread principal de simulación""" try: # Obtener parámetros adam_address = self.adam_address_var.get() min_brix = float(self.shared_config['min_brix_map_var'].get()) max_brix = 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()) # Calcular período entre muestras sample_period = cycle_time / samples_per_cycle while self.simulating: # Calcular valor actual según la función progress = (self.simulation_step % samples_per_cycle) / samples_per_cycle if function_type == "Lineal": # Onda triangular 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 + (max_brix - min_brix) * cycle_progress elif function_type == "Sinusoidal": phase = progress * 2 * math.pi sin_val = (math.sin(phase) + 1) / 2 current_brix = min_brix + (max_brix - min_brix) * sin_val # Crear y enviar mensaje message, ma_value = ProtocolHandler.create_adam_message(adam_address, current_brix, min_brix, max_brix) # Actualizar display self.current_brix_var.set(Utils.format_brix_display(current_brix)) self.current_ma_var.set(Utils.format_ma_display(ma_value)) # Agregar al gráfico self.frame.after(0, lambda b=current_brix, m=ma_value: self.add_data_point(b, m)) # Log y envío Utils.log_message(self.log_text, f"Enviando: {ProtocolHandler.format_for_display(message)}") try: self.connection_manager.send_data(message) # Leer respuesta sin bloquear demasiado 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, max_brix) 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 en comunicación: {e}") self.frame.after(0, self.stop_simulation_error) break self.simulation_step += 1 time.sleep(sample_period) except Exception as e: Utils.log_message(self.log_text, f"Error en simulación: {e}") self.frame.after(0, self.stop_simulation_error) def stop_simulation_error(self): """Detiene la simulación debido a un error""" if self.simulating: messagebox.showerror("Error", "Error durante la simulación. Simulación detenida.") self.stop_simulation() 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) # Notificar a la aplicación principal para actualizar el gráfico 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""" widgets = [ self.adam_address_entry, self.function_type_combo, self.cycle_time_entry, self.samples_per_cycle_entry ] Utils.set_widgets_state(widgets, state) # También deshabilitar controles compartidos 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_brix': self.manual_brix_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_brix_var.set(config.get('manual_brix', '10.0')) try: self.manual_slider_var.set(float(config.get('manual_brix', '10.0'))) except: pass self.on_function_type_change()