""" Aplicación principal del Simulador/Trace Maselli Une todos los módulos y maneja la interfaz principal """ import tkinter as tk from tkinter import ttk, messagebox import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure import matplotlib.animation as animation from config_manager import ConfigManager from utils import Utils from tabs.simulator_tab import SimulatorTab from tabs.trace_tab import TraceTab from tabs.netcom_tab import NetComTab class MaselliApp: def __init__(self, root): self.root = root self.root.title("Simulador/Trace/NetCom Protocolo Maselli") self.root.geometry("1000x800") # Cargar icono Utils.load_icon(self.root) # Gestor de configuración self.config_manager = ConfigManager() self.config = self.config_manager.load_config() # Diccionario para compartir configuración entre tabs self.shared_config = { 'config_manager': self.config_manager } # Crear interfaz self.create_widgets() # Cargar configuración inicial self.load_config_to_gui() # Configurar eventos self.root.protocol("WM_DELETE_WINDOW", self.on_closing) # Inicializar animaciones de gráficos self.sim_ani = animation.FuncAnimation( self.sim_fig, self.update_sim_graph, interval=100, blit=False, cache_frame_data=False ) self.trace_ani = animation.FuncAnimation( self.trace_fig, self.update_trace_graph, interval=100, blit=False, cache_frame_data=False ) def create_widgets(self): """Crea todos los widgets de la aplicación""" # Frame principal main_frame = ttk.Frame(self.root) main_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) # Configurar pesos self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) main_frame.columnconfigure(0, weight=1) main_frame.rowconfigure(1, weight=1) # Notebook # Frame de configuración compartida self.create_shared_config_frame(main_frame) # Notebook para tabs self.notebook = ttk.Notebook(main_frame) self.notebook.grid(row=1, column=0, sticky="nsew") # Tab Simulador sim_frame = ttk.Frame(self.notebook) self.notebook.add(sim_frame, text="Simulador") self.simulator_tab = SimulatorTab(sim_frame, self.shared_config) # Tab Trace trace_frame = ttk.Frame(self.notebook) self.notebook.add(trace_frame, text="Trace") self.trace_tab = TraceTab(trace_frame, self.shared_config) # Tab NetCom netcom_frame = ttk.Frame(self.notebook) self.notebook.add(netcom_frame, text="NetCom (Gateway)") self.netcom_tab = NetComTab(netcom_frame, self.shared_config) # Crear gráficos self.create_graphs() # Establecer callbacks para actualización de gráficos self.simulator_tab.graph_update_callback = self.update_sim_graph self.trace_tab.graph_update_callback = self.update_trace_graph def create_shared_config_frame(self, parent): """Crea el frame de configuración compartida""" config_frame = ttk.LabelFrame(parent, text="Configuración de Conexión") config_frame.grid(row=0, column=0, sticky="ew", pady=(0, 5)) # Tipo de conexión ttk.Label(config_frame, text="Tipo:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.connection_type_var = tk.StringVar() self.connection_type_combo = ttk.Combobox( config_frame, textvariable=self.connection_type_var, values=["Serial", "TCP", "UDP", "TCP-Server"], state="readonly", width=10 ) self.connection_type_combo.grid(row=0, column=1, padx=5, pady=5) self.connection_type_combo.bind("<>", self.on_connection_type_change) # Frame para Serial self.serial_frame = ttk.Frame(config_frame) self.serial_frame.grid(row=0, column=2, columnspan=6, padx=5, pady=5, sticky="ew") # Ajustado columnspan ttk.Label(self.serial_frame, text="Puerto:").grid(row=0, column=0, padx=5, sticky="w") self.com_port_var = tk.StringVar() self.com_port_entry = ttk.Entry(self.serial_frame, textvariable=self.com_port_var, width=10) self.com_port_entry.grid(row=0, column=1, padx=5) ttk.Label(self.serial_frame, text="Baud:").grid(row=0, column=2, padx=5, sticky="w") self.baud_rate_var = tk.StringVar() self.baud_rate_entry = ttk.Entry(self.serial_frame, textvariable=self.baud_rate_var, width=10) self.baud_rate_entry.grid(row=0, column=3, padx=5) # Frame para Ethernet self.ethernet_frame = ttk.Frame(config_frame) self.ethernet_frame.grid(row=0, column=2, columnspan=6, padx=5, pady=5, sticky="ew") # Aumentado columnspan self.ethernet_frame.grid_remove() self.ip_address_label_widget = ttk.Label(self.ethernet_frame, text="IP:") self.ip_address_label_widget.grid(row=0, column=0, padx=5, sticky="w") self.ip_address_var = tk.StringVar() self.ip_address_entry = ttk.Entry(self.ethernet_frame, textvariable=self.ip_address_var, width=15) self.ip_address_entry.grid(row=0, column=1, padx=5) self.port_label_widget = ttk.Label(self.ethernet_frame, text="Puerto:") self.port_label_widget.grid(row=0, column=2, padx=5, sticky="w") self.port_var = tk.StringVar() self.port_entry = ttk.Entry(self.ethernet_frame, textvariable=self.port_var, width=8) self.port_entry.grid(row=0, column=3, padx=(0,5), pady=5) # Ajustado padx # Label para mostrar el cliente conectado en modo TCP-Server self.client_connected_label_widget = ttk.Label(self.ethernet_frame, text="Cliente Conectado:") # Se mostrará/ocultará en on_connection_type_change self.client_connected_var = tk.StringVar(value="Ninguno") self.client_connected_display = ttk.Label(self.ethernet_frame, textvariable=self.client_connected_var, width=25) # Parámetros de mapeo ttk.Label(config_frame, text="Min Brix [4mA]:").grid(row=1, column=0, padx=5, pady=5, sticky="w") self.min_brix_map_var = tk.StringVar() self.min_brix_map_entry = ttk.Entry(config_frame, textvariable=self.min_brix_map_var, width=10) self.min_brix_map_entry.grid(row=1, column=1, padx=5, pady=5) ttk.Label(config_frame, text="Max Brix [20mA]:").grid(row=1, column=2, padx=5, pady=5, sticky="w") self.max_brix_map_var = tk.StringVar() self.max_brix_map_entry = ttk.Entry(config_frame, textvariable=self.max_brix_map_var, width=10) self.max_brix_map_entry.grid(row=1, column=3, padx=5, pady=5) # Botones ttk.Button(config_frame, text="Guardar Config", command=self.save_config).grid(row=1, column=4, padx=5, pady=5) ttk.Button(config_frame, text="Cargar Config", command=self.load_config).grid(row=1, column=5, padx=5, pady=5) # Guardar referencias para compartir self.shared_config.update({ 'connection_type_var': self.connection_type_var, 'com_port_var': self.com_port_var, 'baud_rate_var': self.baud_rate_var, 'ip_address_var': self.ip_address_var, 'port_var': self.port_var, 'min_brix_map_var': self.min_brix_map_var, 'max_brix_map_var': self.max_brix_map_var, 'client_connected_var': self.client_connected_var, # Para actualizar desde el simulador 'shared_widgets': [ self.connection_type_combo, self.com_port_entry, self.baud_rate_entry, self.ip_address_entry, self.port_entry, self.min_brix_map_entry, self.max_brix_map_entry, # self.client_connected_display # No deshabilitar el display, solo su contenido ] }) def create_graphs(self): """Crea los gráficos para simulador y trace""" # Gráfico del simulador # get_graph_frame() ahora devuelve el contenedor LabelFrame donde debe ir el canvas. # Este contenedor ya está posicionado por SimulatorTab.create_widgets(). sim_graph_canvas_parent = self.simulator_tab.get_graph_frame() self.sim_fig = Figure(figsize=(8, 3.5), dpi=100) self.sim_ax1 = self.sim_fig.add_subplot(111) self.sim_ax2 = self.sim_ax1.twinx() self.sim_ax1.set_xlabel('Tiempo (s)') self.sim_ax1.set_ylabel('Brix', color='b') self.sim_ax2.set_ylabel('mA', color='r') self.sim_ax1.tick_params(axis='y', labelcolor='b') self.sim_ax2.tick_params(axis='y', labelcolor='r') self.sim_ax1.grid(True, alpha=0.3) self.sim_line_brix, = self.sim_ax1.plot([], [], 'b-', label='Brix', linewidth=2) self.sim_line_ma, = self.sim_ax2.plot([], [], 'r-', label='mA', linewidth=2) self.sim_canvas = FigureCanvasTkAgg(self.sim_fig, master=sim_graph_canvas_parent) # sim_graph_canvas_parent es un LabelFrame self.sim_canvas.draw() sim_canvas_widget = self.sim_canvas.get_tk_widget() sim_canvas_widget.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) # Configurar el LabelFrame (sim_graph_canvas_parent) para que el canvas (su hijo) se expanda sim_graph_canvas_parent.rowconfigure(0, weight=1) sim_graph_canvas_parent.columnconfigure(0, weight=1) # Gráfico del trace (ahora con doble eje Y) trace_graph_frame = self.trace_tab.get_graph_frame() self.trace_fig = Figure(figsize=(8, 4), dpi=100) self.trace_ax1 = self.trace_fig.add_subplot(111) self.trace_ax2 = self.trace_ax1.twinx() self.trace_ax1.set_xlabel('Tiempo (s)') self.trace_ax1.set_ylabel('Brix', color='b') self.trace_ax2.set_ylabel('mA', color='r') self.trace_ax1.tick_params(axis='y', labelcolor='b') self.trace_ax2.tick_params(axis='y', labelcolor='r') self.trace_ax1.grid(True, alpha=0.3) self.trace_line_brix, = self.trace_ax1.plot([], [], 'b-', label='Brix', linewidth=2, marker='o', markersize=4) self.trace_line_ma, = self.trace_ax2.plot([], [], 'r-', label='mA', linewidth=2, marker='s', markersize=3) self.trace_canvas = FigureCanvasTkAgg(self.trace_fig, master=trace_graph_frame) # trace_graph_frame es un LabelFrame self.trace_canvas.draw() trace_canvas_widget = self.trace_canvas.get_tk_widget() trace_canvas_widget.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) # Configurar el LabelFrame (trace_graph_frame) para que el canvas (su hijo) se expanda trace_graph_frame.rowconfigure(0, weight=1) trace_graph_frame.columnconfigure(0, weight=1) def update_sim_graph(self, frame=None): """Actualiza el gráfico del simulador""" time_data = list(self.simulator_tab.time_data) brix_data = list(self.simulator_tab.brix_data) ma_data = list(self.simulator_tab.ma_data) if len(time_data) > 0: self.sim_line_brix.set_data(time_data, brix_data) self.sim_line_ma.set_data(time_data, ma_data) if len(time_data) > 1: self.sim_ax1.set_xlim(min(time_data), max(time_data)) if brix_data: brix_min = min(brix_data) - 1 brix_max = max(brix_data) + 1 self.sim_ax1.set_ylim(brix_min, brix_max) if ma_data: ma_min = min(ma_data) - 0.5 ma_max = max(ma_data) + 0.5 self.sim_ax2.set_ylim(ma_min, ma_max) self.sim_canvas.draw_idle() return self.sim_line_brix, self.sim_line_ma def update_trace_graph(self, frame=None): """Actualiza el gráfico del trace""" time_data = list(self.trace_tab.time_data) brix_data = list(self.trace_tab.brix_data) ma_data = list(self.trace_tab.ma_data) if len(time_data) > 0: self.trace_line_brix.set_data(time_data, brix_data) self.trace_line_ma.set_data(time_data, ma_data) if len(time_data) > 1: self.trace_ax1.set_xlim(min(time_data), max(time_data)) if brix_data: brix_min = min(brix_data) - 1 brix_max = max(brix_data) + 1 self.trace_ax1.set_ylim(brix_min, brix_max) if ma_data: ma_min = min(ma_data) - 0.5 ma_max = max(ma_data) + 0.5 self.trace_ax2.set_ylim(ma_min, ma_max) self.trace_canvas.draw_idle() return self.trace_line_brix, self.trace_line_ma def on_connection_type_change(self, event=None): """Maneja el cambio de tipo de conexión""" conn_type = self.connection_type_var.get() is_server_mode = (conn_type == "TCP-Server") if conn_type == "Serial": self.ethernet_frame.grid_remove() self.serial_frame.grid() # No es necesario manipular los widgets dentro de ethernet_frame si está oculto self.client_connected_label_widget.grid_remove() self.client_connected_display.grid_remove() self.client_connected_var.set("Ninguno") elif conn_type == "TCP-Server": self.serial_frame.grid_remove() self.ethernet_frame.grid() self.ip_address_label_widget.grid_remove() # Ocultar etiqueta IP self.ip_address_entry.config(state=tk.DISABLED) # IP no se usa para el servidor self.ip_address_entry.grid_remove() # Ocultar campo IP self.port_label_widget.grid(row=0, column=2, padx=5, sticky="w") # Asegurar que la etiqueta Puerto esté visible self.port_entry.config(state=tk.NORMAL) # Puerto es para escuchar self.port_entry.grid(row=0, column=3, padx=(0,5), pady=5) # Asegurar que el campo Puerto esté visible self.client_connected_label_widget.grid(row=0, column=4, padx=(10,2), pady=5, sticky="w") self.client_connected_display.grid(row=0, column=5, padx=(0,5), pady=5, sticky="w") else: # TCP, UDP self.serial_frame.grid_remove() self.ethernet_frame.grid() self.ip_address_label_widget.grid(row=0, column=0, padx=5, sticky="w") # Asegurar que la etiqueta IP esté visible self.ip_address_entry.config(state=tk.NORMAL) self.ip_address_entry.grid(row=0, column=1, padx=5) # Asegurar que el campo IP esté visible self.port_label_widget.grid(row=0, column=2, padx=5, sticky="w") # Asegurar que la etiqueta Puerto esté visible self.port_entry.config(state=tk.NORMAL) self.port_entry.grid(row=0, column=3, padx=(0,5), pady=5) # Asegurar que el campo Puerto esté visible self.client_connected_label_widget.grid_remove() self.client_connected_display.grid_remove() self.client_connected_var.set("Ninguno") # Actualizar info en NetCom if hasattr(self, 'netcom_tab'): self.netcom_tab.update_net_info() # Habilitar/deshabilitar botones Start en otras pestañas según compatibilidad if hasattr(self, 'simulator_tab'): # El simulador maneja TCP-Server, su lógica de botón es interna pass if hasattr(self, 'trace_tab'): if is_server_mode: self.trace_tab.start_button.config(state=tk.DISABLED) if self.trace_tab.tracing: # Si estaba trazando y el modo cambió self.trace_tab.stop_trace() messagebox.showinfo("Trace Detenido", "El modo Trace se detuvo porque el tipo de conexión cambió a TCP-Server, que no es compatible.") elif not self.trace_tab.tracing : # Habilitar solo si no está trazando self.trace_tab.start_button.config(state=tk.NORMAL) if hasattr(self, 'netcom_tab'): if is_server_mode: self.netcom_tab.start_button.config(state=tk.DISABLED) if self.netcom_tab.bridging: # Si estaba en modo bridge self.netcom_tab.stop_bridge() messagebox.showinfo("NetCom Detenido", "El modo NetCom se detuvo porque el tipo de conexión cambió a TCP-Server, que no es compatible.") elif not self.netcom_tab.bridging: # Habilitar solo si no está en modo bridge self.netcom_tab.start_button.config(state=tk.NORMAL) def save_config(self): """Guarda la configuración actual""" # Recopilar configuración de todos los componentes config = { 'connection_type': self.connection_type_var.get(), 'com_port': self.com_port_var.get(), 'baud_rate': self.baud_rate_var.get(), 'ip_address': self.ip_address_var.get(), 'port': self.port_var.get(), 'min_brix_map': self.min_brix_map_var.get(), 'max_brix_map': self.max_brix_map_var.get() } # Agregar configuración de cada tab config.update(self.simulator_tab.get_config()) config.update(self.netcom_tab.get_config()) # Validar configuración errors = self.config_manager.validate_config(config) if errors: messagebox.showerror("Error de Configuración", "\n".join(errors)) return # Guardar if self.config_manager.save_config(config): messagebox.showinfo("Éxito", "Configuración guardada correctamente.") else: messagebox.showerror("Error", "No se pudo guardar la configuración.") def load_config(self): """Carga la configuración desde archivo""" self.config = self.config_manager.load_config() self.load_config_to_gui() messagebox.showinfo("Éxito", "Configuración cargada correctamente.") def load_config_to_gui(self): """Carga la configuración en los widgets de la GUI""" # Configuración compartida self.connection_type_var.set(self.config.get('connection_type', 'Serial')) self.com_port_var.set(self.config.get('com_port', 'COM3')) self.baud_rate_var.set(self.config.get('baud_rate', '115200')) self.ip_address_var.set(self.config.get('ip_address', '192.168.1.100')) self.port_var.set(self.config.get('port', '502')) self.min_brix_map_var.set(self.config.get('min_brix_map', '0')) self.max_brix_map_var.set(self.config.get('max_brix_map', '80')) # Configuración específica de cada tab self.simulator_tab.set_config(self.config) self.netcom_tab.set_config(self.config) # Actualizar vista self.on_connection_type_change() if self.connection_type_var.get() != "TCP-Server": self.client_connected_var.set("Ninguno") def on_closing(self): """Maneja el cierre de la aplicación""" # Detener cualquier operación activa if hasattr(self.simulator_tab, 'simulating') and self.simulator_tab.simulating: self.simulator_tab.stop_simulation() if hasattr(self.trace_tab, 'tracing') and self.trace_tab.tracing: self.trace_tab.stop_trace() if hasattr(self.netcom_tab, 'bridging') and self.netcom_tab.bridging: self.netcom_tab.stop_bridge() # Cerrar ventana self.root.destroy()