""" Tab del Trace - Escucha datos de un dispositivo Maselli real """ import tkinter as tk from tkinter import ttk, scrolledtext, messagebox import threading import time import csv from collections import deque from datetime import datetime from protocol_handler import ProtocolHandler from connection_manager import ConnectionManager from utils import Utils class TraceTab: def __init__(self, parent_frame, shared_config): self.frame = parent_frame self.shared_config = shared_config # Estado del trace self.tracing = False self.trace_thread = None self.connection_manager = ConnectionManager() # Archivo CSV self.csv_file = None self.csv_writer = None # Datos para el gráfico (ahora con mA también) 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) # Nueva línea para mA self.start_time = time.time() self.create_widgets() def create_widgets(self): """Crea los widgets del tab trace""" # Control Frame control_frame = ttk.LabelFrame(self.frame, text="Control Trace") control_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky="ew") self.start_button = ttk.Button(control_frame, text="Iniciar Trace", command=self.start_trace) self.start_button.pack(side=tk.LEFT, padx=5) self.stop_button = ttk.Button(control_frame, text="Detener Trace", command=self.stop_trace, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=5) self.clear_graph_button = ttk.Button(control_frame, text="Limpiar Gráfico", command=self.clear_graph) self.clear_graph_button.pack(side=tk.LEFT, padx=5) ttk.Label(control_frame, text="Archivo CSV:").pack(side=tk.LEFT, padx=(20, 5)) self.csv_filename_var = tk.StringVar(value="Sin archivo") ttk.Label(control_frame, textvariable=self.csv_filename_var).pack(side=tk.LEFT, padx=5) # Display Frame display_frame = ttk.LabelFrame(self.frame, text="Último Valor Recibido") display_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew") ttk.Label(display_frame, text="Timestamp:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.timestamp_var = tk.StringVar(value="---") ttk.Label(display_frame, textvariable=self.timestamp_var, font=("Courier", 12)).grid(row=0, column=1, padx=5, pady=5, sticky="w") ttk.Label(display_frame, text="Dirección:").grid(row=0, column=2, padx=5, pady=5, sticky="w") self.address_var = tk.StringVar(value="--") ttk.Label(display_frame, textvariable=self.address_var, font=("Courier", 12)).grid(row=0, column=3, padx=5, pady=5, sticky="w") ttk.Label(display_frame, text="Valor mA:").grid(row=1, column=0, padx=5, pady=5, sticky="w") self.ma_var = tk.StringVar(value="---") ttk.Label(display_frame, textvariable=self.ma_var, font=("Courier", 12, "bold"), foreground="red").grid(row=1, column=1, padx=5, pady=5, sticky="w") ttk.Label(display_frame, text="Valor Brix:").grid(row=1, column=2, padx=5, pady=5, sticky="w") self.brix_var = tk.StringVar(value="---") ttk.Label(display_frame, textvariable=self.brix_var, font=("Courier", 12, "bold"), foreground="blue").grid(row=1, column=3, padx=5, pady=5, sticky="w") # Statistics Frame stats_frame = ttk.LabelFrame(self.frame, text="Estadísticas") stats_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="ew") ttk.Label(stats_frame, text="Mensajes recibidos:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.msg_count_var = tk.StringVar(value="0") ttk.Label(stats_frame, textvariable=self.msg_count_var).grid(row=0, column=1, padx=5, pady=5, sticky="w") ttk.Label(stats_frame, text="Errores checksum:").grid(row=0, column=2, padx=5, pady=5, sticky="w") self.checksum_errors_var = tk.StringVar(value="0") ttk.Label(stats_frame, textvariable=self.checksum_errors_var).grid(row=0, column=3, padx=5, pady=5, sticky="w") # Log Frame log_frame = ttk.LabelFrame(self.frame, text="Log de Recepción") log_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") self.log_text = scrolledtext.ScrolledText(log_frame, height=10, 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(3, weight=1) # Contadores self.message_count = 0 self.checksum_error_count = 0 def get_graph_frame(self): """Crea y retorna el frame para el gráfico""" graph_frame = ttk.LabelFrame(self.frame, text="Gráfico Trace (Brix y mA)") graph_frame.grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") self.frame.rowconfigure(4, weight=1) return graph_frame def start_trace(self): """Inicia el modo trace""" if self.tracing: messagebox.showwarning("Advertencia", "El trace ya está en curso.") return # Crear archivo CSV csv_filename = Utils.create_csv_filename("maselli_trace") try: self.csv_file = open(csv_filename, 'w', newline='', encoding='utf-8') self.csv_writer = csv.writer(self.csv_file) self.csv_writer.writerow(['Timestamp', 'Address', 'mA', 'Brix', 'Checksum_Valid', '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: # Construct a dictionary of current config values for get_connection_params current_config_values = { 'connection_type': self.shared_config['connection_type_var'].get(), 'com_port': self.shared_config['com_port_var'].get(), 'baud_rate': self.shared_config['baud_rate_var'].get(), 'ip_address': self.shared_config['ip_address_var'].get(), 'port': self.shared_config['port_var'].get(), } conn_type = current_config_values['connection_type'] conn_params = self.shared_config['config_manager'].get_connection_params(current_config_values) self.connection_manager.open_connection(conn_type, conn_params) Utils.log_message(self.log_text, f"Conexión {conn_type} abierta para trace.") except Exception as e: messagebox.showerror("Error de Conexión", str(e)) if self.csv_file: self.csv_file.close() return # Resetear contadores self.message_count = 0 self.checksum_error_count = 0 self.msg_count_var.set("0") self.checksum_errors_var.set("0") self.tracing = True self.start_time = time.time() self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self._set_entries_state(tk.DISABLED) # Iniciar thread de recepción self.trace_thread = threading.Thread(target=self.run_trace, daemon=True) self.trace_thread.start() Utils.log_message(self.log_text, "Trace iniciado.") 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 self.connection_manager.close_connection() Utils.log_message(self.log_text, "Conexión cerrada.") # Cerrar archivo CSV if self.csv_file: self.csv_file.close() self.csv_file = None self.csv_writer = None Utils.log_message(self.log_text, f"Archivo CSV guardado: {self.csv_filename_var.get()}") self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) self._set_entries_state(tk.NORMAL) Utils.log_message(self.log_text, "Trace detenido.") Utils.log_message(self.log_text, f"Total mensajes: {self.message_count}, Errores checksum: {self.checksum_error_count}") def run_trace(self): """Thread principal para recepción de datos""" buffer = "" while self.tracing: try: # Leer datos disponibles data = self.connection_manager.read_data_non_blocking() if data: buffer += data # Buscar mensajes completos while '\r' in buffer or '\n' in buffer or len(buffer) >= 10: # Encontrar el primer terminador end_idx = -1 for i, char in enumerate(buffer): if char in ['\r', '\n']: end_idx = i + 1 break # Si no hay terminador pero el buffer es largo, buscar mensaje completo if end_idx == -1 and len(buffer) >= 10: # Verificar si hay un mensaje ADAM completo if buffer[0] == '#' or (len(buffer) >= 10 and buffer[2:8].replace('.', '').isdigit()): end_idx = 10 # Longitud mínima de un mensaje ADAM if len(buffer) > 10 and buffer[10] in ['\r', '\n']: end_idx = 11 if end_idx > 0: message = buffer[:end_idx] buffer = buffer[end_idx:] # Procesar mensaje si tiene contenido if message.strip(): self._process_message(message) else: break except Exception as e: if self.tracing: Utils.log_message(self.log_text, f"Error en trace: {e}") break # Pequeña pausa para no consumir demasiado CPU if not data: time.sleep(0.01) def _process_message(self, message): """Procesa un mensaje recibido""" # Log del mensaje raw display_msg = ProtocolHandler.format_for_display(message) Utils.log_message(self.log_text, f"Recibido: {display_msg}") # Parsear mensaje parsed = ProtocolHandler.parse_adam_message(message) if parsed: # Obtener valores de mapeo min_brix = float(self.shared_config['min_brix_map_var'].get()) max_brix = float(self.shared_config['max_brix_map_var'].get()) ma_value = parsed['ma'] brix_value = ProtocolHandler.ma_to_brix(ma_value, min_brix, max_brix) timestamp = datetime.now() # Actualizar contadores self.message_count += 1 self.msg_count_var.set(str(self.message_count)) if not parsed.get('checksum_valid', True): self.checksum_error_count += 1 self.checksum_errors_var.set(str(self.checksum_error_count)) # Actualizar display self.timestamp_var.set(timestamp.strftime("%H:%M:%S.%f")[:-3]) self.address_var.set(parsed['address']) self.ma_var.set(Utils.format_ma_display(ma_value)) self.brix_var.set(Utils.format_brix_display(brix_value)) # Log con detalles checksum_status = "OK" if parsed.get('checksum_valid', True) else "ERROR" Utils.log_message(self.log_text, f" -> Addr: {parsed['address']}, " f"mA: {ma_value:.3f}, " f"Brix: {brix_value:.3f}, " f"Checksum: {checksum_status}") # Guardar en CSV if self.csv_writer: self.csv_writer.writerow([ timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], parsed['address'], f"{ma_value:.3f}", f"{brix_value:.3f}", parsed.get('checksum_valid', True), display_msg ]) if self.csv_file: self.csv_file.flush() # Agregar 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) # Agregar también mA # Actualizar gráfico if hasattr(self, 'graph_update_callback'): self.frame.after(0, self.graph_update_callback) else: # Mensaje no válido Utils.log_message(self.log_text, f" -> Mensaje no válido ADAM") 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 el trace""" # Deshabilitar controles compartidos if 'shared_widgets' in self.shared_config: Utils.set_widgets_state(self.shared_config['shared_widgets'], state) def get_config(self): """Obtiene la configuración actual (no hay configuración específica para trace)""" return {} def set_config(self, config): """Establece la configuración (no hay configuración específica para trace)""" pass