MaselliSimulatorApp/maselli_app.py

432 lines
20 KiB
Python

"""
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("<<ComboboxSelected>>", 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()