""" 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("<>", 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("<>", 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('', lambda e: self.update_slider_from_entry()) self.manual_value_entry.bind('', 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) message, calculated_ma_from_brix = ProtocolHandler.create_adam_message(adam_address, final_brix, min_brix_map, max_brix_map) # Actualizar display self.current_brix_var.set(Utils.format_brix_display(final_brix)) self.current_ma_var.set(Utils.format_ma_display(final_ma)) self.current_voltage_var.set(ProtocolHandler.format_voltage_display(final_voltage)) # Agregar al gráfico self.add_data_point(final_brix, final_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) 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) 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) 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_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) 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 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()) 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)) Utils.log_message(self.log_text, f"Enviando: {ProtocolHandler.format_for_display(message)}") try: self.connection_manager.send_data(message) 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 Exception as e: Utils.log_message(self.log_text, f"Error en comunicación: {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