MaselliSimulatorApp/tabs/trace_tab.py

337 lines
14 KiB
Python

"""
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