""" 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") # Data bits, Parity, Stop bits ttk.Label(com_config_frame, text="Data Bits:").grid(row=0, column=4, padx=5, pady=5, sticky="w") self.bytesize_var = tk.StringVar(value=str(self.shared_config.get('netcom_bytesize', 8))) self.bytesize_combo = ttk.Combobox(com_config_frame, textvariable=self.bytesize_var, values=["5", "6", "7", "8"], state="readonly", width=5) self.bytesize_combo.grid(row=0, column=5, padx=5, pady=5, sticky="ew") ttk.Label(com_config_frame, text="Parity:").grid(row=0, column=6, padx=5, pady=5, sticky="w") self.parity_var = tk.StringVar(value=self.shared_config.get('netcom_parity', 'N')) self.parity_combo = ttk.Combobox(com_config_frame, textvariable=self.parity_var, values=["N", "E", "O", "M", "S"], state="readonly", width=5) # N: None, E: Even, O: Odd, M: Mark, S: Space self.parity_combo.grid(row=0, column=7, padx=5, pady=5, sticky="ew") ttk.Label(com_config_frame, text="Stop Bits:").grid(row=0, column=8, padx=5, pady=5, sticky="w") self.stopbits_var = tk.StringVar(value=str(self.shared_config.get('netcom_stopbits', 1))) self.stopbits_combo = ttk.Combobox(com_config_frame, textvariable=self.stopbits_var, values=["1", "1.5", "2"], state="readonly", width=5) self.stopbits_combo.grid(row=0, column=9, padx=5, pady=5, sticky="ew") # Flow control options self.rtscts_var = tk.BooleanVar(value=self.shared_config.get('netcom_rtscts', False)) self.rtscts_check = ttk.Checkbutton(com_config_frame, text="RTS/CTS", variable=self.rtscts_var) self.rtscts_check.grid(row=1, column=0, padx=5, pady=5, sticky="w") self.dsrdtr_var = tk.BooleanVar(value=self.shared_config.get('netcom_dsrdtr', False)) self.dsrdtr_check = ttk.Checkbutton(com_config_frame, text="DSR/DTR", variable=self.dsrdtr_var) self.dsrdtr_check.grid(row=1, column=1, padx=5, pady=5, sticky="w") self.xonxoff_var = tk.BooleanVar(value=self.shared_config.get('netcom_xonxoff', False)) self.xonxoff_check = ttk.Checkbutton(com_config_frame, text="XON/XOFF", variable=self.xonxoff_var) self.xonxoff_check.grid(row=1, column=2, padx=5, pady=5, sticky="w") # Bridge delay ttk.Label(com_config_frame, text="Retardo Bridge (s):").grid(row=1, column=3, padx=5, pady=5, sticky="w") self.bridge_delay_var = tk.StringVar(value=str(self.shared_config.get('netcom_bridge_delay', 0.001))) self.bridge_delay_entry = ttk.Entry(com_config_frame, textvariable=self.bridge_delay_var, width=8) self.bridge_delay_entry.grid(row=1, column=4, 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 # Verificar si el tipo de conexión global es compatible con el lado de red de NetCom global_conn_type_for_network_side = self.shared_config['connection_type_var'].get() if global_conn_type_for_network_side == "TCP-Server": messagebox.showerror("Modo No Compatible", "El lado de red de NetCom no puede operar en modo TCP-Server (configuración global). Seleccione TCP, UDP o Serial para la conexión de red.") 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()) bridge_delay_str = self.bridge_delay_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") try: self.current_bridge_delay = float(bridge_delay_str) if self.current_bridge_delay < 0: raise ValueError("El retardo del bridge no puede ser negativo.") except ValueError: raise ValueError("Retardo del bridge inválido. Debe ser un número (ej: 0.001).") except ValueError as e: messagebox.showerror("Error", f"Configuración inválida: {e}") return # Abrir conexión COM física try: # open_connection ahora devuelve (connection_object, listening_info) _, _ = self.com_connection.open_connection("Serial", { # Ignoramos listening_info para Serial 'port': com_port, 'baudrate': baud_rate, 'bytesize': int(self.bytesize_var.get()), 'parity': self.parity_var.get(), 'stopbits': float(self.stopbits_var.get()), 'rtscts': self.rtscts_var.get(), 'dsrdtr': self.dsrdtr_var.get(), 'xonxoff': self.xonxoff_var.get() }) # Log basic serial config serial_config_log = f"{com_port} @ {baud_rate} bps, {self.bytesize_var.get()}{self.parity_var.get()}{self.stopbits_var.get()}" fc_log = [] if self.rtscts_var.get(): fc_log.append("RTS/CTS") if self.dsrdtr_var.get(): fc_log.append("DSR/DTR") if self.xonxoff_var.get(): fc_log.append("XON/XOFF") self.log_message(f"Puerto COM abierto: {serial_config_log}. Flow control: {', '.join(fc_log) if fc_log else 'Ninguno'}") 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) # open_connection ahora devuelve (connection_object, listening_info) _, net_listening_details = self.net_connection.open_connection(net_conn_type_actual, net_conn_params) if net_conn_type_actual == "TCP-Server" and net_listening_details: # Aunque NetCom no usa TCP-Server globalmente self.log_message(f"{net_listening_details}") else: self.log_message(f"Conexión de red ({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 = bytearray() # net_buffer = bytearray() # Ya no se usa para el flujo NET->COM si pasamos los datos directamente current_delay = self.current_bridge_delay 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 # Adaptar la condición para bytearray while self._find_message_end_conditions(com_buffer): end_idx = self._find_message_end(com_buffer) if end_idx > 0: message_bytes = bytes(com_buffer[:end_idx]) com_buffer = com_buffer[end_idx:] # Log y parseo use_hex_format_for_raw = self.show_parsed_var.get() display_msg = ProtocolHandler.format_for_display(message_bytes, hex_non_printable=use_hex_format_for_raw) self.log_message(f"Data: {display_msg}", "com_to_net") # Intentar parsear si está habilitado if self.show_parsed_var.get(): # Decodificar solo para el parseo message_str_for_parse = message_bytes.decode('ascii', errors='ignore') parsed = ProtocolHandler.parse_adam_message(message_str_for_parse) 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_bytes) 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: # Los datos de la red (net_data) son bytes. # Se reenvían directamente al puerto COM. # Log y parseo (opcional, sobre los datos recibidos directamente) use_hex_format_for_raw = self.show_parsed_var.get() display_msg = ProtocolHandler.format_for_display(net_data, hex_non_printable=use_hex_format_for_raw) self.log_message(f"Data: {display_msg}", "net_to_com") # Intentar parsear si está habilitado (puede ser sobre fragmentos) if self.show_parsed_var.get(): # Decodificar solo para el parseo # Nota: parsear fragmentos puede no ser siempre significativo para protocolos como ADAM. message_str_for_parse = net_data.decode('ascii', errors='ignore') parsed = ProtocolHandler.parse_adam_message(message_str_for_parse) if parsed: 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 (datos red) - 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(net_data) # Enviar los bytes tal cual se recibieron self.net_to_com_count += len(net_data) # Contar bytes en lugar de "mensajes" self.update_stats() except Exception as e: self.log_message(f"Error enviando a COM: {e}", "error") self.error_count += 1 self.update_stats() 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(current_delay) # Asegurar que el estado se actualice if not self.bridging: self.frame.after(0, self._ensure_stopped_state) def _find_message_end_conditions(self, buffer_bytes: bytearray): """Verifica si hay condiciones para buscar el final de un mensaje.""" if not buffer_bytes: return False has_terminator = any(byte_val in (ord(b'\r'), ord(b'\n')) for byte_val in buffer_bytes) return has_terminator or len(buffer_bytes) >= 10 def _find_message_end(self, buffer_bytes: bytearray): """Encuentra el final de un mensaje en el buffer de bytes.""" # Buscar terminadores for i, byte_val in enumerate(buffer_bytes): if byte_val == ord(b'\r') or byte_val == ord(b'\n'): return i + 1 # Si no hay terminador pero el buffer es largo, buscar mensaje ADAM completo # Esta parte es una heurística para mensajes tipo ADAM que podrían no tener terminador # y debe usarse con cuidado para no cortar mensajes prematuramente. if len(buffer_bytes) >= 10: starts_with_hash = (buffer_bytes[0] == ord(b'#')) is_adam_value_like = False if len(buffer_bytes) >= 8: # Asegurar que el slice buffer_bytes[2:8] sea válido try: # Convertir la parte del valor a string para una verificación más sencilla value_part_str = bytes(buffer_bytes[2:8]).decode('ascii') # Formato ADAM es XX.XXX (6 caracteres) if len(value_part_str) == 6 and value_part_str[2] == '.' and \ value_part_str[0:2].isdigit() and value_part_str[3:6].isdigit(): is_adam_value_like = True except UnicodeDecodeError: pass # No es ASCII, no es el formato ADAM esperado if starts_with_hash or is_adam_value_like: # Heurística: si parece ADAM y tiene al menos 10 bytes. # Si después de 10 bytes hay un terminador, incluirlo. if len(buffer_bytes) > 10 and \ (buffer_bytes[10] == ord(b'\r') or buffer_bytes[10] == ord(b'\n')): return 11 else: # Asumir un mensaje de 10 bytes (ej: #AAXX.XXXCC) 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)) # Si net_to_com_count ahora cuenta bytes, el label "NET → COM:" seguido de un número # podría interpretarse como mensajes. Para mayor claridad, se podría cambiar el label # o el formato del valor (ej. self.net_to_com_var.set(f"{self.net_to_com_count} bytes")). # Por ahora, solo actualizamos el valor; el label no cambia. 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) self.bytesize_combo.config(state=state) self.parity_combo.config(state=state) self.stopbits_combo.config(state=state) self.rtscts_check.config(state=state) self.dsrdtr_check.config(state=state) self.xonxoff_check.config(state=state) self.bridge_delay_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(), 'netcom_rtscts': self.rtscts_var.get(), 'netcom_dsrdtr': self.dsrdtr_var.get(), 'netcom_xonxoff': self.xonxoff_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')) self.rtscts_var.set(config.get('netcom_rtscts', False)) self.dsrdtr_var.set(config.get('netcom_dsrdtr', False)) self.xonxoff_var.set(config.get('netcom_xonxoff', False))