MaselliSimulatorApp/tabs/trace_tab.py

361 lines
16 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
import sys # Add sys import
import os # Add os import
# If this script is run directly, add the parent directory to sys.path
# to allow imports of modules like protocol_handler, connection_manager, utils
if __name__ == "__main__" and __package__ is None:
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
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
# Verificar si el tipo de conexión global es compatible
global_conn_type = self.shared_config['connection_type_var'].get()
if global_conn_type == "TCP-Server":
messagebox.showerror("Modo No Compatible", "El modo Trace no es compatible cuando el tipo de conexión global es TCP-Server.")
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 = bytearray() # Cambiar buffer a bytearray
while self.tracing:
try:
# Leer datos disponibles
data = self.connection_manager.read_data_non_blocking()
if data:
buffer.extend(data) # Usar extend para bytearray
# Las condiciones de búsqueda ahora deben usar bytes
while b'\r' in buffer or b'\n' in buffer or len(buffer) >= 10: # Encontrar el primer terminador
end_idx = -1
# Iterar sobre los valores de byte
for i, byte_val in enumerate(buffer):
if byte_val == ord(b'\r') or byte_val == ord(b'\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
# Heurística: si empieza con '#' o parece un valor ADAM
# Decodificar solo la parte necesaria para la heurística
is_adam_like = False
try:
temp_str_for_check = buffer[:10].decode('ascii', errors='ignore')
if temp_str_for_check.startswith('#') or \
(len(temp_str_for_check) >= 8 and temp_str_for_check[2:8].replace('.', '').isdigit()):
is_adam_like = True
except: pass
if is_adam_like:
end_idx = 10 # Longitud de un mensaje ADAM sin terminador explícito
if len(buffer) > 10 and (buffer[10] == ord(b'\r') or buffer[10] == ord(b'\n')):
end_idx = 11
if end_idx > 0:
message_bytes = bytes(buffer[:end_idx]) # Extraer como bytes
buffer = buffer[end_idx:]
# Procesar mensaje si tiene contenido
message_str = message_bytes.decode('ascii', errors='ignore') # Decodificar a string
if message_str.strip(): # Procesar si la cadena decodificada tiene contenido
self._process_message(message_str)
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