diff --git a/MaselliSimulatorApp.py b/MaselliSimulatorApp.py index 4d5f15c..021d3aa 100644 --- a/MaselliSimulatorApp.py +++ b/MaselliSimulatorApp.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import ttk, scrolledtext, messagebox +from tkinter import ttk, scrolledtext, messagebox, filedialog import serial import socket import threading @@ -7,6 +7,8 @@ import time import math import json import os +import csv +from datetime import datetime from collections import deque import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg @@ -16,56 +18,105 @@ import matplotlib.animation as animation class MaselliSimulatorApp: def __init__(self, root_window): self.root = root_window - self.root.title("Simulador Protocolo Maselli - Serial/Ethernet") + self.root.title("Simulador/Trace Protocolo Maselli") - self.connection = None # Puede ser serial.Serial o socket - self.connection_type = None # 'serial', 'tcp', o 'udp' + self.connection = None + self.connection_type = None self.simulating = False self.simulation_thread = None self.simulation_step = 0 - # Para el gráfico + # Para modo Trace + self.tracing = False + self.trace_thread = None + self.csv_file = None + self.csv_writer = None + + # Para los gráficos 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() + # Datos para simulador + self.sim_time_data = deque(maxlen=self.max_points) + self.sim_brix_data = deque(maxlen=self.max_points) + self.sim_ma_data = deque(maxlen=self.max_points) + self.sim_start_time = time.time() + + # Datos para trace + self.trace_time_data = deque(maxlen=self.max_points) + self.trace_brix_data = deque(maxlen=self.max_points) + self.trace_start_time = time.time() # Archivo de configuración self.config_file = "maselli_simulator_config.json" - # --- Configuration Frame --- - config_frame = ttk.LabelFrame(self.root, text="Configuración") - config_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew", columnspan=2) + # Crear notebook para tabs + self.notebook = ttk.Notebook(self.root) + self.notebook.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + + # Tab Simulador + self.simulator_tab = ttk.Frame(self.notebook) + self.notebook.add(self.simulator_tab, text="Simulador") + + # Tab Trace + self.trace_tab = ttk.Frame(self.notebook) + self.notebook.add(self.trace_tab, text="Trace") + + # --- Frame de Configuración Compartida --- + self.create_shared_config_frame() + + # --- Crear contenido de cada tab --- + self.create_simulator_tab() + self.create_trace_tab() + + # Configurar pesos + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + + # Cargar configuración si existe + self.load_config(silent=True) + + # Inicializar estado de la interfaz + self.on_connection_type_change() + self.on_function_type_change() + + # Iniciar animaciones de gráficos + self.sim_ani = animation.FuncAnimation(self.sim_fig, self.update_sim_graph, interval=100, blit=False) + self.trace_ani = animation.FuncAnimation(self.trace_fig, self.update_trace_graph, interval=100, blit=False) + + def create_shared_config_frame(self): + """Crea el frame de configuración compartida que aparece arriba del notebook""" + shared_config_frame = ttk.LabelFrame(self.root, text="Configuración de Conexión") + shared_config_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew") # Tipo de conexión - ttk.Label(config_frame, text="Tipo de Conexión:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + ttk.Label(shared_config_frame, text="Tipo:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.connection_type_var = tk.StringVar(value="Serial") - self.connection_type_combo = ttk.Combobox(config_frame, textvariable=self.connection_type_var, + self.connection_type_combo = ttk.Combobox(shared_config_frame, textvariable=self.connection_type_var, values=["Serial", "TCP", "UDP"], state="readonly", width=10) self.connection_type_combo.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.connection_type_combo.bind("<>", self.on_connection_type_change) # Frame para configuración Serial - self.serial_frame = ttk.Frame(config_frame) - self.serial_frame.grid(row=1, column=0, columnspan=4, padx=5, pady=5, sticky="ew") + self.serial_frame = ttk.Frame(shared_config_frame) + self.serial_frame.grid(row=0, column=2, columnspan=4, padx=5, pady=5, sticky="ew") - ttk.Label(self.serial_frame, text="Puerto COM:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + ttk.Label(self.serial_frame, text="Puerto:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.com_port_var = tk.StringVar(value="COM3") 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, pady=5, sticky="ew") - ttk.Label(self.serial_frame, text="Baud Rate:").grid(row=0, column=2, padx=5, pady=5, sticky="w") + ttk.Label(self.serial_frame, text="Baud:").grid(row=0, column=2, padx=5, pady=5, sticky="w") self.baud_rate_var = tk.StringVar(value="115200") 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, pady=5, sticky="ew") # Frame para configuración Ethernet - self.ethernet_frame = ttk.Frame(config_frame) - self.ethernet_frame.grid(row=1, column=0, columnspan=4, padx=5, pady=5, sticky="ew") - self.ethernet_frame.grid_remove() # Oculto por defecto + self.ethernet_frame = ttk.Frame(shared_config_frame) + self.ethernet_frame.grid(row=0, column=2, columnspan=4, padx=5, pady=5, sticky="ew") + self.ethernet_frame.grid_remove() - ttk.Label(self.ethernet_frame, text="Dirección IP:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + ttk.Label(self.ethernet_frame, text="IP:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.ip_address_var = tk.StringVar(value="192.168.1.100") 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, pady=5, sticky="ew") @@ -75,46 +126,53 @@ class MaselliSimulatorApp: self.port_entry = ttk.Entry(self.ethernet_frame, textvariable=self.port_var, width=10) self.port_entry.grid(row=0, column=3, padx=5, pady=5, sticky="ew") - # Configuración común - ttk.Label(config_frame, text="ADAM Address (2c):").grid(row=2, column=0, padx=5, pady=5, sticky="w") - self.adam_address_var = tk.StringVar(value="01") - self.adam_address_entry = ttk.Entry(config_frame, textvariable=self.adam_address_var, width=5) - self.adam_address_entry.grid(row=2, column=1, padx=5, pady=5, sticky="ew") - - ttk.Label(config_frame, text="Función:").grid(row=2, column=2, padx=5, pady=5, sticky="w") - self.function_type_var = tk.StringVar(value="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=2, column=3, padx=5, pady=5, sticky="ew") - self.function_type_combo.bind("<>", self.on_function_type_change) - - # Parámetros para mapeo 4-20mA - ttk.Label(config_frame, text="Valor Mínimo (Brix) [p/ 4mA]:").grid(row=3, column=0, padx=5, pady=5, sticky="w") + # Parámetros de mapeo (compartidos) + ttk.Label(shared_config_frame, text="Min Brix [4mA]:").grid(row=1, column=0, padx=5, pady=5, sticky="w") self.min_brix_map_var = tk.StringVar(value="0") - self.min_brix_map_entry = ttk.Entry(config_frame, textvariable=self.min_brix_map_var, width=10) - self.min_brix_map_entry.grid(row=3, column=1, padx=5, pady=5, sticky="ew") + self.min_brix_map_entry = ttk.Entry(shared_config_frame, textvariable=self.min_brix_map_var, width=10) + self.min_brix_map_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew") - ttk.Label(config_frame, text="Valor Máximo (Brix) [p/ 20mA]:").grid(row=3, column=2, padx=5, pady=5, sticky="w") + ttk.Label(shared_config_frame, text="Max Brix [20mA]:").grid(row=1, column=2, padx=5, pady=5, sticky="w") self.max_brix_map_var = tk.StringVar(value="80") - self.max_brix_map_entry = ttk.Entry(config_frame, textvariable=self.max_brix_map_var, width=10) - self.max_brix_map_entry.grid(row=3, column=3, padx=5, pady=5, sticky="ew") - - # Parámetros específicos para simulación continua - ttk.Label(config_frame, text="Periodo Sim. (s):").grid(row=4, column=0, padx=5, pady=5, sticky="w") - self.period_var = tk.StringVar(value="1.0") - self.period_entry = ttk.Entry(config_frame, textvariable=self.period_var, width=5) - self.period_entry.grid(row=4, column=1, padx=5, pady=5, sticky="ew") + self.max_brix_map_entry = ttk.Entry(shared_config_frame, textvariable=self.max_brix_map_var, width=10) + self.max_brix_map_entry.grid(row=1, column=3, padx=5, pady=5, sticky="ew") # Botones de persistencia - self.save_config_button = ttk.Button(config_frame, text="Guardar Config", command=self.save_config) - self.save_config_button.grid(row=4, column=2, padx=5, pady=5, sticky="ew") + self.save_config_button = ttk.Button(shared_config_frame, text="Guardar", command=self.save_config) + self.save_config_button.grid(row=1, column=4, padx=5, pady=5, sticky="ew") - self.load_config_button = ttk.Button(config_frame, text="Cargar Config", command=self.load_config) - self.load_config_button.grid(row=4, column=3, padx=5, pady=5, sticky="ew") + self.load_config_button = ttk.Button(shared_config_frame, text="Cargar", command=self.load_config) + self.load_config_button.grid(row=1, column=5, padx=5, pady=5, sticky="ew") - # Frame para modo Manual con slider - manual_frame = ttk.LabelFrame(config_frame, text="Modo Manual") - manual_frame.grid(row=5, column=0, columnspan=4, padx=5, pady=5, sticky="ew") + def create_simulator_tab(self): + """Crea el contenido del tab Simulador""" + # Frame de configuración del simulador + sim_config_frame = ttk.LabelFrame(self.simulator_tab, text="Configuración Simulador") + sim_config_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew", columnspan=2) + + # Dirección ADAM + ttk.Label(sim_config_frame, text="ADAM Address (2c):").grid(row=0, column=0, padx=5, pady=5, sticky="w") + self.adam_address_var = tk.StringVar(value="01") + self.adam_address_entry = ttk.Entry(sim_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(sim_config_frame, text="Función:").grid(row=0, column=2, padx=5, pady=5, sticky="w") + self.function_type_var = tk.StringVar(value="Lineal") + self.function_type_combo = ttk.Combobox(sim_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) + + # Periodo + ttk.Label(sim_config_frame, text="Periodo (s):").grid(row=0, column=4, padx=5, pady=5, sticky="w") + self.period_var = tk.StringVar(value="1.0") + self.period_entry = ttk.Entry(sim_config_frame, textvariable=self.period_var, width=5) + self.period_entry.grid(row=0, column=5, padx=5, pady=5, sticky="ew") + + # Frame para modo Manual + manual_frame = ttk.LabelFrame(sim_config_frame, text="Modo Manual") + manual_frame.grid(row=1, column=0, columnspan=6, 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="10.0") @@ -123,7 +181,7 @@ class MaselliSimulatorApp: 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 para valor manual + # Slider self.manual_slider_var = tk.DoubleVar(value=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, @@ -135,86 +193,435 @@ class MaselliSimulatorApp: manual_frame.columnconfigure(2, weight=1) - # --- Controls Frame --- - controls_frame = ttk.LabelFrame(self.root, text="Control Simulación Continua") + # Controls Frame + controls_frame = ttk.LabelFrame(self.simulator_tab, 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 Simulación", command=self.start_simulation) + 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 Simulación", command=self.stop_simulation, state=tk.DISABLED) + 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) + self.clear_sim_graph_button = ttk.Button(controls_frame, text="Limpiar Gráfico", command=self.clear_sim_graph) + self.clear_sim_graph_button.pack(side=tk.LEFT, padx=5) - # --- Display Frame --- - display_frame = ttk.LabelFrame(self.root, text="Visualización") - display_frame.grid(row=1, column=1, rowspan=2, padx=10, pady=10, sticky="nsew") + # Display Frame + display_frame = ttk.LabelFrame(self.simulator_tab, text="Valores Actuales") + display_frame.grid(row=1, column=1, padx=10, pady=5, sticky="ew") - ttk.Label(display_frame, text="Valor Brix Actual:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + ttk.Label(display_frame, text="Brix:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.current_brix_display_var = tk.StringVar(value="---") - self.current_brix_label = ttk.Label(display_frame, textvariable=self.current_brix_display_var, font=("Courier", 14, "bold")) + self.current_brix_label = ttk.Label(display_frame, textvariable=self.current_brix_display_var, + font=("Courier", 14, "bold")) self.current_brix_label.grid(row=0, column=1, padx=5, pady=5, sticky="w") - ttk.Label(display_frame, text="Valor mA Correspondiente:").grid(row=1, column=0, 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_display_var = tk.StringVar(value="--.-- mA") - self.current_ma_label = ttk.Label(display_frame, textvariable=self.current_ma_display_var, font=("Courier", 14, "bold")) + self.current_ma_label = ttk.Label(display_frame, textvariable=self.current_ma_display_var, + font=("Courier", 14, "bold")) self.current_ma_label.grid(row=1, column=1, padx=5, pady=5, sticky="w") - # --- Graph Frame --- - graph_frame = ttk.LabelFrame(self.root, text="Gráfico de Valores") - graph_frame.grid(row=2, column=0, padx=10, pady=5, sticky="nsew") + # Graph Frame + sim_graph_frame = ttk.LabelFrame(self.simulator_tab, text="Gráfico Simulador") + sim_graph_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") - # Crear figura de matplotlib - self.fig = Figure(figsize=(6, 3.5), dpi=100) - self.ax1 = self.fig.add_subplot(111) - self.ax2 = self.ax1.twinx() + # Crear figura para simulador + 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() - # Configurar el gráfico - self.ax1.set_xlabel('Tiempo (s)') - self.ax1.set_ylabel('Brix', color='b') - self.ax2.set_ylabel('mA', color='r') - self.ax1.tick_params(axis='y', labelcolor='b') - self.ax2.tick_params(axis='y', labelcolor='r') - self.ax1.grid(True, alpha=0.3) + 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) - # Líneas del gráfico - self.line_brix, = self.ax1.plot([], [], 'b-', label='Brix', linewidth=2) - self.line_ma, = self.ax2.plot([], [], 'r-', label='mA', linewidth=2) + 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) - # Canvas de matplotlib en tkinter - self.canvas = FigureCanvasTkAgg(self.fig, master=graph_frame) - self.canvas.draw() - self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.sim_canvas = FigureCanvasTkAgg(self.sim_fig, master=sim_graph_frame) + self.sim_canvas.draw() + self.sim_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - # --- Log Frame --- - log_frame = ttk.LabelFrame(self.root, text="Log de Comunicación") - log_frame.grid(row=3, column=0, padx=10, pady=10, sticky="nsew", columnspan=2) + # Log Frame + sim_log_frame = ttk.LabelFrame(self.simulator_tab, text="Log de Comunicación") + sim_log_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") - self.com_log_text = scrolledtext.ScrolledText(log_frame, height=10, width=70, wrap=tk.WORD, state=tk.DISABLED) - self.com_log_text.pack(padx=5,pady=5,fill=tk.BOTH, expand=True) + self.sim_log_text = scrolledtext.ScrolledText(sim_log_frame, height=8, width=70, wrap=tk.WORD, state=tk.DISABLED) + self.sim_log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) - # Configurar pesos de las filas y columnas - self.root.columnconfigure(0, weight=1) - self.root.columnconfigure(1, weight=1) - self.root.rowconfigure(2, weight=1) - self.root.rowconfigure(3, weight=1) + # Configurar pesos + self.simulator_tab.columnconfigure(0, weight=1) + self.simulator_tab.columnconfigure(1, weight=1) + self.simulator_tab.rowconfigure(2, weight=1) + self.simulator_tab.rowconfigure(3, weight=1) - self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + def create_trace_tab(self): + """Crea el contenido del tab Trace""" + # Control Frame + trace_control_frame = ttk.LabelFrame(self.trace_tab, text="Control Trace") + trace_control_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky="ew") + + self.start_trace_button = ttk.Button(trace_control_frame, text="Iniciar Trace", command=self.start_trace) + self.start_trace_button.pack(side=tk.LEFT, padx=5) + + self.stop_trace_button = ttk.Button(trace_control_frame, text="Detener Trace", command=self.stop_trace, state=tk.DISABLED) + self.stop_trace_button.pack(side=tk.LEFT, padx=5) + + self.clear_trace_graph_button = ttk.Button(trace_control_frame, text="Limpiar Gráfico", command=self.clear_trace_graph) + self.clear_trace_graph_button.pack(side=tk.LEFT, padx=5) + + ttk.Label(trace_control_frame, text="Archivo CSV:").pack(side=tk.LEFT, padx=(20, 5)) + self.csv_filename_var = tk.StringVar(value="Sin archivo") + self.csv_filename_label = ttk.Label(trace_control_frame, textvariable=self.csv_filename_var) + self.csv_filename_label.pack(side=tk.LEFT, padx=5) + + # Display Frame + trace_display_frame = ttk.LabelFrame(self.trace_tab, text="Último Valor Recibido") + trace_display_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew") + + ttk.Label(trace_display_frame, text="Timestamp:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + self.trace_timestamp_var = tk.StringVar(value="---") + ttk.Label(trace_display_frame, textvariable=self.trace_timestamp_var, font=("Courier", 12)).grid(row=0, column=1, padx=5, pady=5, sticky="w") + + ttk.Label(trace_display_frame, text="Valor mA:").grid(row=0, column=2, padx=5, pady=5, sticky="w") + self.trace_ma_var = tk.StringVar(value="---") + ttk.Label(trace_display_frame, textvariable=self.trace_ma_var, font=("Courier", 12, "bold")).grid(row=0, column=3, padx=5, pady=5, sticky="w") + + ttk.Label(trace_display_frame, text="Valor Brix:").grid(row=0, column=4, padx=5, pady=5, sticky="w") + self.trace_brix_var = tk.StringVar(value="---") + ttk.Label(trace_display_frame, textvariable=self.trace_brix_var, font=("Courier", 12, "bold")).grid(row=0, column=5, padx=5, pady=5, sticky="w") + + # Graph Frame + trace_graph_frame = ttk.LabelFrame(self.trace_tab, text="Gráfico Trace (Brix)") + trace_graph_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") + + # Crear figura para trace + self.trace_fig = Figure(figsize=(8, 4), dpi=100) + self.trace_ax = self.trace_fig.add_subplot(111) - # Cargar configuración si existe - self.load_config(silent=True) + self.trace_ax.set_xlabel('Tiempo (s)') + self.trace_ax.set_ylabel('Brix', color='b') + self.trace_ax.tick_params(axis='y', labelcolor='b') + self.trace_ax.grid(True, alpha=0.3) - # Inicializar estado de la interfaz - self.on_connection_type_change() - self.on_function_type_change() + self.trace_line_brix, = self.trace_ax.plot([], [], 'b-', label='Brix', linewidth=2, marker='o', markersize=4) - # Iniciar animación del gráfico - self.ani = animation.FuncAnimation(self.fig, self.update_graph, interval=100, blit=False) + self.trace_canvas = FigureCanvasTkAgg(self.trace_fig, master=trace_graph_frame) + self.trace_canvas.draw() + self.trace_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # Log Frame + trace_log_frame = ttk.LabelFrame(self.trace_tab, text="Log de Recepción") + trace_log_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") + + self.trace_log_text = scrolledtext.ScrolledText(trace_log_frame, height=10, width=70, wrap=tk.WORD, state=tk.DISABLED) + self.trace_log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) + + # Configurar pesos + self.trace_tab.columnconfigure(0, weight=1) + self.trace_tab.columnconfigure(1, weight=1) + self.trace_tab.rowconfigure(2, weight=1) + self.trace_tab.rowconfigure(3, weight=1) + + def _log_message(self, message, log_widget=None): + """Escribe mensaje en el log especificado o en el log activo""" + if log_widget is None: + # Determinar qué log usar basado en la pestaña activa + current_tab = self.notebook.index(self.notebook.select()) + log_widget = self.sim_log_text if current_tab == 0 else self.trace_log_text + + log_widget.configure(state=tk.NORMAL) + log_widget.insert(tk.END, f"[{datetime.now().strftime('%H:%M:%S')}] {message}\n") + log_widget.see(tk.END) + log_widget.configure(state=tk.DISABLED) + + def parse_adam_message(self, data): + """Parsea un mensaje del protocolo ADAM y retorna el valor en mA""" + try: + # Formato esperado: #AA[valor_mA][checksum]\r + if not data.startswith('#') or not data.endswith('\r'): + return None + + # Remover # y \r + data = data[1:-1] + + # Los primeros 2 caracteres son la dirección + if len(data) < 9: # 2 addr + 6 valor + 2 checksum + return None + + address = data[:2] + value_str = data[2:9] # 6 caracteres para el valor + checksum = data[9:11] # 2 caracteres para checksum + + # Verificar checksum + message_part = f"#{address}{value_str}" + calculated_checksum = self.calculate_checksum(message_part) + + if checksum != calculated_checksum: + self._log_message(f"Checksum incorrecto: recibido={checksum}, calculado={calculated_checksum}", + self.trace_log_text) + return None + + # Convertir valor a float + ma_value = float(value_str) + return {'address': address, 'ma': ma_value} + + except Exception as e: + self._log_message(f"Error parseando mensaje: {e}", self.trace_log_text) + return None + + def ma_to_brix(self, ma_value): + """Convierte valor mA a Brix usando el mapeo configurado""" + try: + min_brix = float(self.min_brix_map_var.get()) + max_brix = float(self.max_brix_map_var.get()) + + if ma_value <= 4.0: + return min_brix + elif ma_value >= 20.0: + return max_brix + else: + # Interpolación lineal + percentage = (ma_value - 4.0) / 16.0 + return min_brix + percentage * (max_brix - min_brix) + except: + return 0.0 + + def start_trace(self): + """Inicia el modo trace para recibir datos""" + if self.tracing: + messagebox.showwarning("Advertencia", "El trace ya está en curso.") + return + + # Obtener parámetros de conexión + try: + conn_type = self.connection_type_var.get() + if conn_type == "Serial": + conn_params = { + 'port': self.com_port_var.get(), + 'baud': int(self.baud_rate_var.get()) + } + else: + conn_params = { + 'ip': self.ip_address_var.get(), + 'port': int(self.port_var.get()) + } + except ValueError as e: + messagebox.showerror("Error", f"Parámetros de conexión inválidos: {e}") + return + + # Crear archivo CSV + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + csv_filename = f"maselli_trace_{timestamp}.csv" + try: + self.csv_file = open(csv_filename, 'w', newline='') + self.csv_writer = csv.writer(self.csv_file) + self.csv_writer.writerow(['Timestamp', 'mA', 'Brix', 'Raw_Message']) + self.csv_filename_var.set(csv_filename) + except Exception as e: + messagebox.showerror("Error", f"No se pudo crear archivo CSV: {e}") + return + + # Abrir conexión + try: + self.connection = self._open_connection(conn_type, conn_params) + self.connection_type = conn_type + self._log_message(f"Conexión {conn_type} abierta para trace.", self.trace_log_text) + except Exception as e: + messagebox.showerror("Error de Conexión", str(e)) + if self.csv_file: + self.csv_file.close() + return + + self.tracing = True + self.trace_start_time = time.time() + self.start_trace_button.config(state=tk.DISABLED) + self.stop_trace_button.config(state=tk.NORMAL) + self._set_trace_entries_state(tk.DISABLED) + + # Iniciar thread de recepción + self.trace_thread = threading.Thread(target=self.run_trace, daemon=True) + self.trace_thread.start() + self._log_message("Trace iniciado.", self.trace_log_text) + + def stop_trace(self): + """Detiene el modo trace""" + if not self.tracing: + return + + self.tracing = False + + # Esperar a que termine el thread + if self.trace_thread and self.trace_thread.is_alive(): + self.trace_thread.join(timeout=2.0) + + # Cerrar conexión + if self.connection: + self._close_connection(self.connection, self.connection_type) + self._log_message("Conexión cerrada.", self.trace_log_text) + self.connection = None + + # Cerrar archivo CSV + if self.csv_file: + self.csv_file.close() + self.csv_file = None + self.csv_writer = None + self._log_message(f"Archivo CSV guardado: {self.csv_filename_var.get()}", self.trace_log_text) + + self.start_trace_button.config(state=tk.NORMAL) + self.stop_trace_button.config(state=tk.DISABLED) + self._set_trace_entries_state(tk.NORMAL) + + self._log_message("Trace detenido.", self.trace_log_text) + + def run_trace(self): + """Thread principal para recepción de datos en modo trace""" + buffer = "" + + while self.tracing: + try: + # Leer datos según el tipo de conexión + data = None + if self.connection_type == "Serial": + if self.connection.in_waiting > 0: + data = self.connection.read(self.connection.in_waiting).decode('ascii', errors='ignore') + elif self.connection_type == "TCP": + self.connection.settimeout(0.1) + try: + data = self.connection.recv(1024).decode('ascii', errors='ignore') + except socket.timeout: + continue + elif self.connection_type == "UDP": + self.connection.settimeout(0.1) + try: + data, addr = self.connection.recvfrom(1024) + data = data.decode('ascii', errors='ignore') + except socket.timeout: + continue + + if data: + buffer += data + + # Buscar mensajes completos (terminan con \r) + while '\r' in buffer: + end_idx = buffer.index('\r') + 1 + message = buffer[:end_idx] + buffer = buffer[end_idx:] + + # Procesar mensaje + self._process_trace_message(message) + + except Exception as e: + self._log_message(f"Error en trace: {e}", self.trace_log_text) + if not self.tracing: + break + time.sleep(0.1) + + def _process_trace_message(self, message): + """Procesa un mensaje recibido en modo trace""" + # Log del mensaje raw + display_msg = message.replace('\r', '').replace('\n', '') + self._log_message(f"Recibido: {display_msg}", self.trace_log_text) + + # Parsear mensaje + parsed = self.parse_adam_message(message) + if parsed: + ma_value = parsed['ma'] + brix_value = self.ma_to_brix(ma_value) + timestamp = datetime.now() + + # Actualizar display + self.trace_timestamp_var.set(timestamp.strftime("%H:%M:%S.%f")[:-3]) + self.trace_ma_var.set(f"{ma_value:.3f} mA") + self.trace_brix_var.set(f"{brix_value:.3f} Brix") + + # Guardar en CSV + if self.csv_writer: + self.csv_writer.writerow([ + timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], + ma_value, + brix_value, + display_msg + ]) + if self.csv_file: + self.csv_file.flush() + + # Agregar al gráfico + current_time = time.time() - self.trace_start_time + self.trace_time_data.append(current_time) + self.trace_brix_data.append(brix_value) + + # Actualizar gráfico + self.root.after(0, self.trace_canvas.draw_idle) + + def _set_trace_entries_state(self, state): + """Habilita/deshabilita controles durante el trace""" + self.connection_type_combo.config(state=state) + self.com_port_entry.config(state=state) + self.baud_rate_entry.config(state=state) + self.ip_address_entry.config(state=state) + self.port_entry.config(state=state) + self.min_brix_map_entry.config(state=state) + self.max_brix_map_entry.config(state=state) + + def update_sim_graph(self, frame): + """Actualiza el gráfico del simulador""" + if len(self.sim_time_data) > 0: + self.sim_line_brix.set_data(list(self.sim_time_data), list(self.sim_brix_data)) + self.sim_line_ma.set_data(list(self.sim_time_data), list(self.sim_ma_data)) + + if len(self.sim_time_data) > 1: + self.sim_ax1.set_xlim(min(self.sim_time_data), max(self.sim_time_data)) + + if len(self.sim_brix_data) > 0: + brix_min = min(self.sim_brix_data) - 1 + brix_max = max(self.sim_brix_data) + 1 + self.sim_ax1.set_ylim(brix_min, brix_max) + + if len(self.sim_ma_data) > 0: + ma_min = min(self.sim_ma_data) - 0.5 + ma_max = max(self.sim_ma_data) + 0.5 + self.sim_ax2.set_ylim(ma_min, ma_max) + + return self.sim_line_brix, self.sim_line_ma + + def update_trace_graph(self, frame): + """Actualiza el gráfico del trace""" + if len(self.trace_time_data) > 0: + self.trace_line_brix.set_data(list(self.trace_time_data), list(self.trace_brix_data)) + + if len(self.trace_time_data) > 1: + self.trace_ax.set_xlim(min(self.trace_time_data), max(self.trace_time_data)) + + if len(self.trace_brix_data) > 0: + brix_min = min(self.trace_brix_data) - 1 + brix_max = max(self.trace_brix_data) + 1 + self.trace_ax.set_ylim(brix_min, brix_max) + + return self.trace_line_brix, + + def clear_sim_graph(self): + """Limpia el gráfico del simulador""" + self.sim_time_data.clear() + self.sim_brix_data.clear() + self.sim_ma_data.clear() + self.sim_start_time = time.time() + self.sim_canvas.draw_idle() + self._log_message("Gráfico del simulador limpiado.", self.sim_log_text) + + def clear_trace_graph(self): + """Limpia el gráfico del trace""" + self.trace_time_data.clear() + self.trace_brix_data.clear() + self.trace_start_time = time.time() + self.trace_canvas.draw_idle() + self._log_message("Gráfico del trace limpiado.", self.trace_log_text) def save_config(self): - """Guarda la configuración actual en un archivo JSON""" + """Guarda la configuración actual""" config = { 'connection_type': self.connection_type_var.get(), 'com_port': self.com_port_var.get(), @@ -239,7 +646,7 @@ class MaselliSimulatorApp: messagebox.showerror("Error", f"No se pudo guardar la configuración: {e}") def load_config(self, silent=False): - """Carga la configuración desde un archivo JSON""" + """Carga la configuración desde archivo""" if not os.path.exists(self.config_file): if not silent: messagebox.showinfo("Información", "No se encontró archivo de configuración.") @@ -249,7 +656,6 @@ class MaselliSimulatorApp: with open(self.config_file, 'r') as f: config = json.load(f) - # Aplicar configuración self.connection_type_var.set(config.get('connection_type', 'Serial')) self.com_port_var.set(config.get('com_port', 'COM3')) self.baud_rate_var.set(config.get('baud_rate', '115200')) @@ -262,13 +668,11 @@ class MaselliSimulatorApp: self.period_var.set(config.get('period', '1.0')) self.manual_brix_var.set(config.get('manual_brix', '10.0')) - # Actualizar slider try: self.manual_slider_var.set(float(config.get('manual_brix', '10.0'))) except: pass - # Actualizar interfaz self.on_connection_type_change() if not silent: @@ -279,68 +683,12 @@ class MaselliSimulatorApp: self._log_message(f"Error al cargar configuración: {e}") messagebox.showerror("Error", f"No se pudo cargar la configuración: {e}") - def on_slider_change(self, value): - """Actualiza el campo de texto cuando cambia el slider""" - self.manual_brix_var.set(f"{float(value):.1f}") - - def update_slider_from_entry(self): - """Actualiza el slider cuando se modifica el campo de texto""" - try: - value = float(self.manual_brix_var.get()) - # Limitar el valor al rango del slider - value = max(0, min(100, value)) - self.manual_slider_var.set(value) - self.manual_brix_var.set(f"{value:.1f}") - except ValueError: - pass - - def update_graph(self, frame): - """Actualiza el gráfico con los datos actuales""" - if len(self.time_data) > 0: - self.line_brix.set_data(list(self.time_data), list(self.brix_data)) - self.line_ma.set_data(list(self.time_data), list(self.ma_data)) - - # Ajustar límites - if len(self.time_data) > 1: - self.ax1.set_xlim(min(self.time_data), max(self.time_data)) - - if len(self.brix_data) > 0: - brix_min = min(self.brix_data) - 1 - brix_max = max(self.brix_data) + 1 - self.ax1.set_ylim(brix_min, brix_max) - - if len(self.ma_data) > 0: - ma_min = min(self.ma_data) - 0.5 - ma_max = max(self.ma_data) + 0.5 - self.ax2.set_ylim(ma_min, ma_max) - - return self.line_brix, self.line_ma - - 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) - - # Redibujar el canvas - self.canvas.draw_idle() - - def clear_graph(self): - """Limpia los datos del gráfico""" - self.time_data.clear() - self.brix_data.clear() - self.ma_data.clear() - self.start_time = time.time() - self.canvas.draw_idle() - self._log_message("Gráfico limpiado.") - def on_connection_type_change(self, event=None): conn_type = self.connection_type_var.get() if conn_type == "Serial": self.ethernet_frame.grid_remove() self.serial_frame.grid() - else: # TCP o UDP + else: self.serial_frame.grid_remove() self.ethernet_frame.grid() @@ -370,11 +718,17 @@ class MaselliSimulatorApp: self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) - def _log_message(self, message): - self.com_log_text.configure(state=tk.NORMAL) - self.com_log_text.insert(tk.END, message + "\n") - self.com_log_text.see(tk.END) - self.com_log_text.configure(state=tk.DISABLED) + def on_slider_change(self, value): + self.manual_brix_var.set(f"{float(value):.1f}") + + def update_slider_from_entry(self): + 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 calculate_checksum(self, message_part): s = sum(ord(c) for c in message_part) @@ -404,7 +758,7 @@ class MaselliSimulatorApp: min_brix_map = float(self.min_brix_map_var.get()) max_brix_map = float(self.max_brix_map_var.get()) if min_brix_map > max_brix_map: - messagebox.showerror("Error", "Valor Mínimo (Brix) para mapeo no puede ser mayor que Valor Máximo (Brix).") + messagebox.showerror("Error", "Valor Mínimo (Brix) no puede ser mayor que Valor Máximo (Brix).") return None conn_type = self.connection_type_var.get() @@ -413,7 +767,7 @@ class MaselliSimulatorApp: com_port = self.com_port_var.get() baud_rate = int(self.baud_rate_var.get()) return conn_type, {'port': com_port, 'baud': baud_rate}, adam_address, min_brix_map, max_brix_map - else: # TCP o UDP + else: ip_address = self.ip_address_var.get() port = int(self.port_var.get()) return conn_type, {'ip': ip_address, 'port': port}, adam_address, min_brix_map, max_brix_map @@ -423,7 +777,6 @@ class MaselliSimulatorApp: return None def _open_connection(self, conn_type, conn_params): - """Abre la conexión según el tipo especificado""" try: if conn_type == "Serial": return serial.Serial(conn_params['port'], conn_params['baud'], timeout=1) @@ -435,14 +788,12 @@ class MaselliSimulatorApp: elif conn_type == "UDP": sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(1.0) - # Para UDP guardamos la dirección destino sock.dest_address = (conn_params['ip'], conn_params['port']) return sock except Exception as e: raise Exception(f"Error al abrir conexión {conn_type}: {e}") def _close_connection(self, connection, conn_type): - """Cierra la conexión según el tipo""" try: if conn_type == "Serial": if connection and connection.is_open: @@ -454,7 +805,6 @@ class MaselliSimulatorApp: self._log_message(f"Error al cerrar conexión: {e}") def _send_data(self, connection, conn_type, data): - """Envía datos según el tipo de conexión""" try: if conn_type == "Serial": connection.write(data.encode('ascii')) @@ -465,6 +815,13 @@ class MaselliSimulatorApp: except Exception as e: raise Exception(f"Error al enviar datos: {e}") + def add_sim_data_point(self, brix_value, ma_value): + current_time = time.time() - self.sim_start_time + self.sim_time_data.append(current_time) + self.sim_brix_data.append(brix_value) + self.sim_ma_data.append(ma_value) + self.sim_canvas.draw_idle() + def send_manual_value(self): common_params = self._get_common_params() if not common_params: @@ -483,8 +840,7 @@ class MaselliSimulatorApp: self.current_brix_display_var.set(f"{manual_brix:.3f} Brix") self.current_ma_display_var.set(f"{mA_str} mA") - # Agregar punto al gráfico - self.add_data_point(manual_brix, mA_val) + self.add_sim_data_point(manual_brix, mA_val) message_part = f"#{adam_address}{mA_str}" checksum = self.calculate_checksum(message_part) @@ -494,16 +850,16 @@ class MaselliSimulatorApp: temp_connection = None try: temp_connection = self._open_connection(conn_type, conn_params) - self._log_message(f"Conexión {conn_type} abierta temporalmente para envío manual.") - self._log_message(f"Enviando Manual: {log_display_string}") + self._log_message(f"Conexión {conn_type} abierta temporalmente para envío manual.", self.sim_log_text) + self._log_message(f"Enviando Manual: {log_display_string}", self.sim_log_text) self._send_data(temp_connection, conn_type, full_string_to_send) except Exception as e: - self._log_message(f"Error al enviar manualmente: {e}") + self._log_message(f"Error al enviar manualmente: {e}", self.sim_log_text) messagebox.showerror("Error de Conexión", str(e)) finally: if temp_connection: self._close_connection(temp_connection, conn_type) - self._log_message(f"Conexión {conn_type} cerrada tras envío manual.") + self._log_message(f"Conexión {conn_type} cerrada tras envío manual.", self.sim_log_text) def start_simulation(self): if self.simulating: @@ -531,7 +887,7 @@ class MaselliSimulatorApp: try: self.connection = self._open_connection(self.connection_type, self.conn_params) - self._log_message(f"Conexión {self.connection_type} abierta para simulación continua.") + self._log_message(f"Conexión {self.connection_type} abierta para simulación continua.", self.sim_log_text) except Exception as e: messagebox.showerror("Error de Conexión", str(e)) self.connection = None @@ -541,13 +897,13 @@ class MaselliSimulatorApp: self.simulation_step = 0 self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) - self._set_config_entries_state(tk.DISABLED) + self._set_sim_config_entries_state(tk.DISABLED) self.simulation_thread = threading.Thread(target=self.run_simulation, daemon=True) self.simulation_thread.start() - self._log_message("Simulación continua iniciada.") + self._log_message("Simulación continua iniciada.", self.sim_log_text) - def _set_config_entries_state(self, state): + def _set_sim_config_entries_state(self, state): self.connection_type_combo.config(state=state) self.com_port_entry.config(state=state) self.baud_rate_entry.config(state=state) @@ -580,11 +936,11 @@ class MaselliSimulatorApp: def stop_simulation(self): if not self.simulating: if self.function_type_var.get() != "Manual": - self._log_message("Simulación continua ya estaba detenida.") + self._log_message("Simulación continua ya estaba detenida.", self.sim_log_text) if self.function_type_var.get() != "Manual": self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) - self._set_config_entries_state(tk.NORMAL) + self._set_sim_config_entries_state(tk.NORMAL) return self.simulating = False @@ -592,19 +948,19 @@ class MaselliSimulatorApp: try: self.simulation_thread.join(timeout=max(0.1, self.simulation_period * 1.5 if hasattr(self, 'simulation_period') else 2.0)) except Exception as e: - self._log_message(f"Error al esperar el hilo de simulación: {e}") + self._log_message(f"Error al esperar el hilo de simulación: {e}", self.sim_log_text) if self.connection: self._close_connection(self.connection, self.connection_type) - self._log_message(f"Conexión {self.connection_type} cerrada (simulación continua).") + self._log_message(f"Conexión {self.connection_type} cerrada (simulación continua).", self.sim_log_text) self.connection = None self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) - self._set_config_entries_state(tk.NORMAL) + self._set_sim_config_entries_state(tk.NORMAL) self.on_function_type_change() - self._log_message("Simulación continua detenida.") + self._log_message("Simulación continua detenida.", self.sim_log_text) self.current_brix_display_var.set("---") self.current_ma_display_var.set("--.-- mA") @@ -631,20 +987,19 @@ class MaselliSimulatorApp: self.current_brix_display_var.set(f"{current_brix_val:.3f} Brix") self.current_ma_display_var.set(f"{mA_str} mA") - # Agregar punto al gráfico - self.root.after(0, lambda b=current_brix_val, m=mA_val: self.add_data_point(b, m)) + self.root.after(0, lambda b=current_brix_val, m=mA_val: self.add_sim_data_point(b, m)) message_part = f"#{self.adam_address}{mA_str}" checksum = self.calculate_checksum(message_part) full_string_to_send = f"{message_part}{checksum}\r" log_display_string = full_string_to_send.replace('\r', '') - self._log_message(f"Enviando Sim: {log_display_string}") + self._log_message(f"Enviando Sim: {log_display_string}", self.sim_log_text) if self.connection: try: self._send_data(self.connection, self.connection_type, full_string_to_send) except Exception as e: - self._log_message(f"Error al escribir en conexión (sim): {e}") + self._log_message(f"Error al escribir en conexión (sim): {e}", self.sim_log_text) self.root.after(0, self.stop_simulation_from_thread_error) break @@ -655,26 +1010,26 @@ class MaselliSimulatorApp: self.root.after(0, self.ensure_gui_stopped_state_sim) def stop_simulation_from_thread_error(self): - """Called from main thread if connection error occurs in sim thread.""" if self.simulating: messagebox.showerror("Error de Simulación", "Error de conexión durante la simulación. Simulación detenida.") self.stop_simulation() def ensure_gui_stopped_state_sim(self): - """Asegura que la GUI refleje el estado detenido si el hilo de simulación continua termina.""" if not self.simulating: self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) - self._set_config_entries_state(tk.NORMAL) + self._set_sim_config_entries_state(tk.NORMAL) if self.connection: self._close_connection(self.connection, self.connection_type) - self._log_message(f"Conexión {self.connection_type} cerrada (auto, sim_end).") + self._log_message(f"Conexión {self.connection_type} cerrada (auto, sim_end).", self.sim_log_text) self.connection = None - self._log_message("Simulación continua terminada (hilo finalizado).") + self._log_message("Simulación continua terminada (hilo finalizado).", self.sim_log_text) def on_closing(self): if self.simulating: self.stop_simulation() + if self.tracing: + self.stop_trace() elif self.connection: self._close_connection(self.connection, self.connection_type) self.root.destroy() diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..7db3ece Binary files /dev/null and b/icon.png differ diff --git a/maselli_simulator_config.json b/maselli_simulator_config.json new file mode 100644 index 0000000..39d73fd --- /dev/null +++ b/maselli_simulator_config.json @@ -0,0 +1,13 @@ +{ + "connection_type": "TCP", + "com_port": "COM8", + "baud_rate": "115200", + "ip_address": "10.1.33.18", + "port": "8899", + "adam_address": "01", + "function_type": "Sinusoidal", + "min_brix_map": "0", + "max_brix_map": "80", + "period": "0.5", + "manual_brix": "10.0" +} \ No newline at end of file diff --git a/maselli_trace_20250522_214450.csv b/maselli_trace_20250522_214450.csv new file mode 100644 index 0000000..4e6d2f3 --- /dev/null +++ b/maselli_trace_20250522_214450.csv @@ -0,0 +1 @@ +Timestamp,mA,Brix,Raw_Message