""" Tab NetCom - Gateway/Bridge entre puerto COM físico y conexión TCP/UDP """ import tkinter as tk from tkinter import ttk, scrolledtext, messagebox import threading import time from datetime import datetime from connection_manager import ConnectionManager from protocol_handler import ProtocolHandler from utils import Utils class NetComTab: def __init__(self, parent_frame, shared_config): self.frame = parent_frame self.shared_config = shared_config # Estado del gateway self.bridging = False self.bridge_thread = None # Conexiones self.com_connection = ConnectionManager() self.net_connection = ConnectionManager() # Estadísticas self.com_to_net_count = 0 self.net_to_com_count = 0 self.error_count = 0 self.create_widgets() def create_widgets(self): """Crea los widgets del tab NetCom""" # Frame de configuración COM física com_config_frame = ttk.LabelFrame(self.frame, text="Configuración Puerto COM Físico") com_config_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky="ew") ttk.Label(com_config_frame, text="Puerto COM:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.com_port_var = tk.StringVar(value=self.shared_config.get('netcom_com_port', 'COM3')) self.com_port_entry = ttk.Entry(com_config_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(com_config_frame, text="Baud Rate:").grid(row=0, column=2, padx=5, pady=5, sticky="w") self.baud_rate_var = tk.StringVar(value=self.shared_config.get('netcom_baud_rate', '115200')) self.baud_rate_entry = ttk.Entry(com_config_frame, textvariable=self.baud_rate_var, width=10) self.baud_rate_entry.grid(row=0, column=3, padx=5, pady=5, sticky="ew") # Info frame info_frame = ttk.LabelFrame(self.frame, text="Información de Conexión") info_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew") ttk.Label(info_frame, text="Conexión de Red:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.net_info_var = tk.StringVar(value="No configurada") ttk.Label(info_frame, textvariable=self.net_info_var, font=("Courier", 10)).grid(row=0, column=1, padx=5, pady=5, sticky="w") ttk.Label(info_frame, text="Estado:").grid(row=1, column=0, padx=5, pady=5, sticky="w") self.status_var = tk.StringVar(value="Desconectado") self.status_label = ttk.Label(info_frame, textvariable=self.status_var, font=("Courier", 10, "bold")) self.status_label.grid(row=1, column=1, padx=5, pady=5, sticky="w") # Control Frame control_frame = ttk.LabelFrame(self.frame, text="Control Gateway") control_frame.grid(row=2, column=0, padx=10, pady=5, sticky="ew") self.start_button = ttk.Button(control_frame, text="Iniciar Gateway", command=self.start_bridge) self.start_button.pack(side=tk.LEFT, padx=5) self.stop_button = ttk.Button(control_frame, text="Detener Gateway", command=self.stop_bridge, state=tk.DISABLED) self.stop_button.pack(side=tk.LEFT, padx=5) self.clear_log_button = ttk.Button(control_frame, text="Limpiar Log", command=self.clear_log) self.clear_log_button.pack(side=tk.LEFT, padx=5) # Statistics Frame stats_frame = ttk.LabelFrame(self.frame, text="Estadísticas") stats_frame.grid(row=2, column=1, padx=10, pady=5, sticky="ew") ttk.Label(stats_frame, text="COM → NET:").grid(row=0, column=0, padx=5, pady=2, sticky="w") self.com_to_net_var = tk.StringVar(value="0") ttk.Label(stats_frame, textvariable=self.com_to_net_var).grid(row=0, column=1, padx=5, pady=2, sticky="w") ttk.Label(stats_frame, text="NET → COM:").grid(row=0, column=2, padx=5, pady=2, sticky="w") self.net_to_com_var = tk.StringVar(value="0") ttk.Label(stats_frame, textvariable=self.net_to_com_var).grid(row=0, column=3, padx=5, pady=2, sticky="w") ttk.Label(stats_frame, text="Errores:").grid(row=1, column=0, padx=5, pady=2, sticky="w") self.errors_var = tk.StringVar(value="0") ttk.Label(stats_frame, textvariable=self.errors_var, foreground="red").grid(row=1, column=1, padx=5, pady=2, sticky="w") # Log Frame con filtros log_control_frame = ttk.Frame(self.frame) log_control_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=(10,0), sticky="ew") ttk.Label(log_control_frame, text="Filtros:").pack(side=tk.LEFT, padx=5) self.show_com_to_net_var = tk.BooleanVar(value=True) ttk.Checkbutton(log_control_frame, text="COM→NET", variable=self.show_com_to_net_var).pack(side=tk.LEFT, padx=5) self.show_net_to_com_var = tk.BooleanVar(value=True) ttk.Checkbutton(log_control_frame, text="NET→COM", variable=self.show_net_to_com_var).pack(side=tk.LEFT, padx=5) self.show_parsed_var = tk.BooleanVar(value=True) ttk.Checkbutton(log_control_frame, text="Mostrar datos parseados", variable=self.show_parsed_var).pack(side=tk.LEFT, padx=5) # Log Frame log_frame = ttk.LabelFrame(self.frame, text="Log de Gateway (Sniffer)") log_frame.grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") self.log_text = scrolledtext.ScrolledText(log_frame, height=20, width=80, wrap=tk.WORD, state=tk.DISABLED) self.log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) # Configurar tags para colores self.log_text.tag_config("com_to_net", foreground="blue") self.log_text.tag_config("net_to_com", foreground="green") self.log_text.tag_config("error", foreground="red") self.log_text.tag_config("info", foreground="black") self.log_text.tag_config("parsed", foreground="purple") # Configurar pesos self.frame.columnconfigure(0, weight=1) self.frame.columnconfigure(1, weight=1) self.frame.rowconfigure(4, weight=1) # Actualizar info de red self.update_net_info() def update_net_info(self): """Actualiza la información de la conexión de red configurada""" conn_type = self.shared_config['connection_type_var'].get() if conn_type == "Serial": port = self.shared_config['com_port_var'].get() baud = self.shared_config['baud_rate_var'].get() self.net_info_var.set(f"Serial: {port} @ {baud} bps") elif conn_type == "TCP": ip = self.shared_config['ip_address_var'].get() port = self.shared_config['port_var'].get() self.net_info_var.set(f"TCP: {ip}:{port}") elif conn_type == "UDP": ip = self.shared_config['ip_address_var'].get() port = self.shared_config['port_var'].get() self.net_info_var.set(f"UDP: {ip}:{port}") def log_message(self, message, tag="info", force=False): """Log con formato especial para el sniffer""" # Verificar filtros if not force: if tag == "com_to_net" and not self.show_com_to_net_var.get(): return if tag == "net_to_com" and not self.show_net_to_com_var.get(): return self.log_text.configure(state=tk.NORMAL) timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3] # Agregar prefijo según la dirección if tag == "com_to_net": prefix = "[COM→NET]" elif tag == "net_to_com": prefix = "[NET→COM]" elif tag == "error": prefix = "[ERROR] " elif tag == "parsed": prefix = "[PARSED] " else: prefix = "[INFO] " full_message = f"[{timestamp}] {prefix} {message}\n" # Insertar con color start_index = self.log_text.index(tk.END) self.log_text.insert(tk.END, full_message) end_index = self.log_text.index(tk.END) self.log_text.tag_add(tag, start_index, end_index) self.log_text.see(tk.END) self.log_text.configure(state=tk.DISABLED) def start_bridge(self): """Inicia el gateway/bridge""" if self.bridging: messagebox.showwarning("Advertencia", "El gateway ya está activo.") return # Actualizar info de red self.update_net_info() # Validar configuración try: com_port = self.com_port_var.get() baud_rate = int(self.baud_rate_var.get()) if not com_port.upper().startswith('COM'): raise ValueError("Puerto COM inválido") if baud_rate <= 0: raise ValueError("Baud rate debe ser mayor que 0") except ValueError as e: messagebox.showerror("Error", f"Configuración inválida: {e}") return # Abrir conexión COM física try: self.com_connection.open_connection("Serial", { 'port': com_port, 'baud': baud_rate }) self.log_message(f"Puerto COM abierto: {com_port} @ {baud_rate} bps") except Exception as e: messagebox.showerror("Error", f"No se pudo abrir puerto COM: {e}") return # Abrir conexión de red try: # For the network side of the bridge, use shared connection settings net_conn_type_actual = self.shared_config['connection_type_var'].get() current_shared_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(), } # The first argument to get_connection_params is the dictionary it will read from. net_conn_params = self.shared_config['config_manager'].get_connection_params(current_shared_config_values) self.net_connection.open_connection(net_conn_type_actual, net_conn_params) self.log_message(f"Conexión {net_conn_type_actual} abierta: {self.net_info_var.get()}") except Exception as e: self.com_connection.close_connection() messagebox.showerror("Error", f"No se pudo abrir conexión de red: {e}") return # Resetear estadísticas self.com_to_net_count = 0 self.net_to_com_count = 0 self.error_count = 0 self.update_stats() # Iniciar bridge self.bridging = True self.status_var.set("Conectado") self.status_label.config(foreground="green") self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) self._set_entries_state(tk.DISABLED) self.bridge_thread = threading.Thread(target=self.run_bridge, daemon=True) self.bridge_thread.start() self.log_message("Gateway iniciado - Modo bridge activo") def stop_bridge(self): """Detiene el gateway/bridge""" if not self.bridging: return self.bridging = False # Esperar a que termine el thread if self.bridge_thread and self.bridge_thread.is_alive(): self.bridge_thread.join(timeout=2.0) # Cerrar conexiones self.com_connection.close_connection() self.net_connection.close_connection() self.status_var.set("Desconectado") self.status_label.config(foreground="black") self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) self._set_entries_state(tk.NORMAL) self.log_message("Gateway detenido") self.log_message(f"Total transferencias - COM→NET: {self.com_to_net_count}, NET→COM: {self.net_to_com_count}, Errores: {self.error_count}") def run_bridge(self): """Thread principal del bridge""" com_buffer = "" net_buffer = "" while self.bridging: try: # Leer del COM físico com_data = self.com_connection.read_data_non_blocking() if com_data: com_buffer += com_data # Buscar mensajes completos para logging while '\r' in com_buffer or '\n' in com_buffer or len(com_buffer) >= 10: end_idx = self._find_message_end(com_buffer) if end_idx > 0: message = com_buffer[:end_idx] com_buffer = com_buffer[end_idx:] # Log y parseo display_msg = ProtocolHandler.format_for_display(message) self.log_message(f"Data: {display_msg}", "com_to_net") # Intentar parsear si está habilitado if self.show_parsed_var.get(): 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()) brix_value = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix, max_brix) self.log_message( f"ADAM - Addr: {parsed['address']}, " f"mA: {parsed['ma']:.3f}, " f"Brix: {brix_value:.3f}, " f"Checksum: {'OK' if parsed.get('checksum_valid', True) else 'ERROR'}", "parsed" ) # Reenviar a la red try: self.net_connection.send_data(message) self.com_to_net_count += 1 self.update_stats() except Exception as e: self.log_message(f"Error enviando a red: {e}", "error") self.error_count += 1 self.update_stats() else: break # Leer de la red net_data = self.net_connection.read_data_non_blocking() if net_data: net_buffer += net_data # Buscar mensajes completos para logging while '\r' in net_buffer or '\n' in net_buffer or len(net_buffer) >= 10: end_idx = self._find_message_end(net_buffer) if end_idx > 0: message = net_buffer[:end_idx] net_buffer = net_buffer[end_idx:] # Log y parseo display_msg = ProtocolHandler.format_for_display(message) self.log_message(f"Data: {display_msg}", "net_to_com") # Intentar parsear si está habilitado if self.show_parsed_var.get(): 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()) brix_value = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix, max_brix) self.log_message( f"ADAM - Addr: {parsed['address']}, " f"mA: {parsed['ma']:.3f}, " f"Brix: {brix_value:.3f}, " f"Checksum: {'OK' if parsed.get('checksum_valid', True) else 'ERROR'}", "parsed" ) # Reenviar al COM try: self.com_connection.send_data(message) self.net_to_com_count += 1 self.update_stats() except Exception as e: self.log_message(f"Error enviando a COM: {e}", "error") self.error_count += 1 self.update_stats() else: break except Exception as e: if self.bridging: self.log_message(f"Error en bridge: {e}", "error") self.error_count += 1 self.update_stats() break # Pequeña pausa para no consumir demasiado CPU if not com_data and not net_data: time.sleep(0.001) # Asegurar que el estado se actualice if not self.bridging: self.frame.after(0, self._ensure_stopped_state) def _find_message_end(self, buffer): """Encuentra el final de un mensaje en el buffer""" # Buscar terminadores for i, char in enumerate(buffer): if char in ['\r', '\n']: return i + 1 # Si no hay terminador pero el buffer es largo, buscar mensaje ADAM completo if len(buffer) >= 10: if buffer[0] == '#' or (buffer[2:8].replace('.', '').replace(' ', '').replace('-', '').isdigit()): # Parece un mensaje ADAM if len(buffer) > 10 and buffer[10] in ['\r', '\n']: return 11 else: return 10 return -1 def update_stats(self): """Actualiza las estadísticas en la GUI""" self.com_to_net_var.set(str(self.com_to_net_count)) self.net_to_com_var.set(str(self.net_to_com_count)) self.errors_var.set(str(self.error_count)) def clear_log(self): """Limpia el log""" self.log_text.configure(state=tk.NORMAL) self.log_text.delete(1.0, tk.END) self.log_text.configure(state=tk.DISABLED) self.log_message("Log limpiado", force=True) def _set_entries_state(self, state): """Habilita/deshabilita los controles durante el bridge""" self.com_port_entry.config(state=state) self.baud_rate_entry.config(state=state) # También deshabilitar controles compartidos if 'shared_widgets' in self.shared_config: Utils.set_widgets_state(self.shared_config['shared_widgets'], state) def _ensure_stopped_state(self): """Asegura que la GUI refleje el estado detenido""" self.status_var.set("Desconectado") self.status_label.config(foreground="black") self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) self._set_entries_state(tk.NORMAL) def get_config(self): """Obtiene la configuración actual del NetCom""" return { 'netcom_com_port': self.com_port_var.get(), 'netcom_baud_rate': self.baud_rate_var.get() } def set_config(self, config): """Establece la configuración del NetCom""" self.com_port_var.set(config.get('netcom_com_port', 'COM3')) self.baud_rate_var.set(config.get('netcom_baud_rate', '115200'))