""" Gestor de conexiones para Serial, TCP y UDP """ import serial import socket import time class ConnectionManagerError(Exception): """Clase base para excepciones de ConnectionManager.""" pass class ClientDisconnectedError(ConnectionManagerError): """Excepción personalizada para cuando un cliente TCP se desconecta.""" pass class NoClientConnectedError(ConnectionManagerError): """Excepción personalizada para cuando se intenta enviar datos sin un cliente TCP conectado.""" pass class ConnectionManager: def __init__(self): self.connection = None # Para Serial (Serial obj), TCP Client (socket), UDP (socket) self.server_socket = None # Para TCP Server (listening socket) self.client_socket = None # Para TCP Server (accepted client connection) self.client_address = None # Para TCP Server (address of accepted client) self.connection_type = None self.dest_address = None # Para UDP self.ClientDisconnectedError = ClientDisconnectedError self.NoClientConnectedError = NoClientConnectedError def open_connection(self, conn_type, conn_params): """Abre una conexión según el tipo especificado""" try: listening_info = None # Información sobre dónde está escuchando el servidor if conn_type == "Serial": self.connection = serial.Serial( port=conn_params['port'], baudrate=conn_params['baudrate'], # Standard pyserial parameter name timeout=conn_params.get('timeout', 1), bytesize=conn_params.get('bytesize', serial.EIGHTBITS), # Use provided or default parity=conn_params.get('parity', serial.PARITY_NONE), # Use provided or default stopbits=conn_params.get('stopbits', serial.STOPBITS_ONE), # Use provided or default xonxoff=conn_params.get('xonxoff', False), rtscts=conn_params.get('rtscts', False), dsrdtr=conn_params.get('dsrdtr', False) ) self.connection_type = "Serial" elif conn_type == "TCP": sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(5.0) sock.connect((conn_params['ip'], conn_params['port'])) self.connection = sock self.connection_type = "TCP" elif conn_type == "UDP": sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(1.0) self.dest_address = (conn_params['ip'], conn_params['port']) self.connection = sock self.connection_type = "UDP" elif conn_type == "TCP-Server": self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # conn_params para TCP-Server solo debería tener 'port' # listen_ip_param es la IP que se usará para bind (e.g., '0.0.0.0' o una IP específica) listen_ip_param = conn_params.get('ip', '0.0.0.0') listen_port = conn_params['port'] actual_listen_description = "" if listen_ip_param == '0.0.0.0': try: hostname = socket.gethostname() ip_list = socket.gethostbyname_ex(hostname)[2] # Direcciones IPv4 non_loopback_ips = [ip for ip in ip_list if not ip.startswith("127.")] display_ips = non_loopback_ips if non_loopback_ips else ip_list if display_ips: actual_listen_description = f"en IPs: {', '.join(display_ips)} en puerto {listen_port}" else: actual_listen_description = f"en todas las interfaces (0.0.0.0) puerto {listen_port} (no se pudieron determinar IPs específicas)" except socket.gaierror: actual_listen_description = f"en todas las interfaces (0.0.0.0) puerto {listen_port} (error al resolver hostname para IPs específicas)" except Exception as e_get_ip: print(f"Advertencia: Error obteniendo IPs locales: {e_get_ip}") actual_listen_description = f"en todas las interfaces (0.0.0.0) puerto {listen_port} (error obteniendo IPs locales)" else: # Se especificó una IP para escuchar actual_listen_description = f"en IP: {listen_ip_param}:{listen_port}" self.server_socket.bind((listen_ip_param, listen_port)) self.server_socket.listen(1) # Escuchar hasta 1 conexión en cola self.connection_type = "TCP-Server" self.connection = self.server_socket # self.connection apunta al socket principal listening_info = f"TCP Server escuchando {actual_listen_description}" print(listening_info) # Log para la consola return self.connection, listening_info except Exception as e: raise Exception(f"Error al abrir conexión {conn_type}: {e}") def close_connection(self): """Cierra la conexión actual""" try: if self.connection_type == "Serial": if self.connection and self.connection.is_open: self.connection.close() elif self.connection_type in ["TCP", "UDP"]: if self.connection: self.connection.close() elif self.connection_type == "TCP-Server": if self.client_socket: try: self.client_socket.close() except Exception as e_client: print(f"Error cerrando client_socket: {e_client}") self.client_socket = None self.client_address = None if self.server_socket: # server_socket es self.connection en este modo try: self.server_socket.close() except Exception as e_server: print(f"Error cerrando server_socket: {e_server}") self.server_socket = None self.connection = None # Asegurar que self.connection también se limpie except Exception as e: print(f"Error al cerrar conexión: {e}") finally: self.connection = None self.connection_type = None # No limpiar server_socket, client_socket aquí, ya se hizo arriba si era TCP-Server self.dest_address = None def send_data(self, data_bytes): """Envía datos por la conexión actual""" if not self.connection: raise Exception("No hay conexión activa") data_to_send = None if isinstance(data_bytes, str): # Esto no debería suceder si el llamador (NetComTab, SimulatorTab) funciona como se espera. # Loguear una advertencia e intentar codificar como último recurso. print(f"ADVERTENCIA: ConnectionManager.send_data recibió str, se esperaba bytes. Intentando codificar a ASCII. Datos: {data_bytes!r}") try: data_to_send = data_bytes.encode('ascii') except UnicodeEncodeError as uee: print(f"ERROR CRÍTICO: No se pudo codificar la cadena (str) a ASCII antes de enviar: {uee}. Datos: {data_bytes!r}") # Elevar una excepción clara porque no se puede continuar si la codificación falla. raise Exception(f"Error al enviar datos: la cadena no pudo ser codificada a ASCII: {uee}") from uee elif isinstance(data_bytes, (bytes, bytearray)): data_to_send = data_bytes # Ya es bytes o bytearray (que .write/.send aceptan) else: # Si no es ni str ni bytes/bytearray, es un error de tipo fundamental. print(f"ERROR CRÍTICO: ConnectionManager.send_data recibió un tipo inesperado: {type(data_bytes)}. Se esperaba bytes. Datos: {data_bytes!r}") raise TypeError(f"Error al enviar datos: se esperaba un objeto tipo bytes, pero se recibió {type(data_bytes)}") try: if self.connection_type == "Serial": self.connection.write(data_to_send) elif self.connection_type == "TCP": self.connection.send(data_to_send) elif self.connection_type == "UDP": self.connection.sendto(data_to_send, self.dest_address) elif self.connection_type == "TCP-Server": if self.client_socket: try: self.client_socket.sendall(data_to_send) # sendall es más robusto except (socket.error, BrokenPipeError, ConnectionResetError) as e_send: print(f"TCP Server: Cliente desconectado durante el envío: {e_send}") self.reset_client_connection() # Limpiar el socket del cliente raise self.ClientDisconnectedError(f"Cliente desconectado: {e_send}") from e_send else: # Opción: ser silencioso si no hay cliente, o lanzar NoClientConnectedError # print(f"TCP Server: No hay cliente conectado, datos no enviados: {data_to_send!r}") pass # No enviar si no hay cliente, la simulación puede continuar except self.ClientDisconnectedError: # Permitir que esta pase tal cual raise except self.NoClientConnectedError: # Permitir que esta pase tal cual raise except Exception as e: # Solo envolver otras excepciones no manejadas específicamente raise Exception(f"Error al enviar datos ({self.connection_type}): {e}") from e def read_response(self, timeout=0.5): """Intenta leer una respuesta del dispositivo""" if not self.connection: return None try: response = None if self.connection_type == "Serial": # Guardar timeout original original_timeout = self.connection.timeout self.connection.timeout = timeout # Esperar un poco para que llegue la respuesta time.sleep(0.05) # Leer todos los bytes disponibles response_bytes = b"" start_time = time.time() while (time.time() - start_time) < timeout: if self.connection.in_waiting > 0: response_bytes += self.connection.read(self.connection.in_waiting) # Si encontramos un terminador, salir if b'\r' in response_bytes or b'\n' in response_bytes: break else: time.sleep(0.01) if response_bytes: response = response_bytes.decode('ascii', errors='ignore') self.connection.timeout = original_timeout elif self.connection_type == "TCP": self.connection.settimeout(timeout) try: response = self.connection.recv(1024).decode('ascii', errors='ignore') except socket.timeout: pass elif self.connection_type == "UDP": self.connection.settimeout(timeout) try: response, addr = self.connection.recvfrom(1024) response = response.decode('ascii', errors='ignore') except socket.timeout: pass elif self.connection_type == "TCP-Server": if self.client_socket: self.client_socket.settimeout(timeout) try: response_bytes = self.client_socket.recv(1024) if response_bytes: response = response_bytes.decode('ascii', errors='ignore') else: # Cliente cerró conexión self.reset_client_connection() print("TCP Server: Cliente cerró conexión durante lectura.") except socket.timeout: pass # Sin datos en timeout except (socket.error, ConnectionResetError) as e_read: print(f"TCP Server: Error leyendo de cliente o cliente desconectado: {e_read}") self.reset_client_connection() else: # Sin cliente conectado, no hay respuesta posible pass return response except Exception as e: print(f"Error al leer respuesta: {e}") return None def read_data_non_blocking(self): """Lee datos disponibles sin bloquear (para modo trace y netcom)""" if not self.connection: return None try: data = None if self.connection_type == "Serial": if self.connection.in_waiting > 0: data = self.connection.read(self.connection.in_waiting) # Returns bytes elif self.connection_type == "TCP": self.connection.settimeout(0.1) try: data = self.connection.recv(1024) # Returns bytes if not data: # Conexión cerrada return None except socket.timeout: pass elif self.connection_type == "UDP": self.connection.settimeout(0.1) try: data, addr = self.connection.recvfrom(1024) # data is already bytes except socket.timeout: pass elif self.connection_type == "TCP-Server": if self.client_socket: self.client_socket.settimeout(0.01) # Timeout muy corto para no bloquear try: data = self.client_socket.recv(1024) # Retorna bytes if not data: # Cliente cerró conexión self.reset_client_connection() print("TCP Server: Cliente cerró conexión (lectura no bloqueante).") return None # Indicar que la conexión se cerró except socket.timeout: pass # Sin datos disponibles except (socket.error, ConnectionResetError) as e_read_nb: print(f"TCP Server: Error leyendo (no bloqueante) o cliente desconectado: {e_read_nb}") self.reset_client_connection() data = None # Error return data except Exception as e: print(f"Error al leer datos: {e}") return None def is_connected(self): """Verifica si hay una conexión activa""" if self.connection_type == "Serial": return self.connection and self.connection.is_open elif self.connection_type == "TCP-Server": # "Conectado" significa que el servidor está escuchando. # Para enviar datos, is_client_connected() es más relevante. return self.server_socket is not None else: return self.connection is not None def accept_client(self, timeout=None): """Acepta una conexión de cliente (solo para modo TCP-Server).""" if self.connection_type != "TCP-Server" or not self.server_socket: return False if self.client_socket: # Ya hay un cliente conectado return True original_timeout = self.server_socket.gettimeout() self.server_socket.settimeout(timeout) try: self.client_socket, self.client_address = self.server_socket.accept() self.client_socket.settimeout(None) # Volver a modo bloqueante para send/recv en client_socket print(f"TCP Server: Cliente conectado desde {self.client_address}") self.server_socket.settimeout(original_timeout) return True except socket.timeout: self.server_socket.settimeout(original_timeout) return False except Exception as e: print(f"TCP Server: Error aceptando cliente: {e}") self.server_socket.settimeout(original_timeout) return False def is_client_connected(self): # Específico para TCP-Server return self.connection_type == "TCP-Server" and self.client_socket is not None def reset_client_connection(self): # Específico para TCP-Server if self.client_socket: try: self.client_socket.close() except Exception: pass self.client_socket = None self.client_address = None print("TCP Server: Conexión con cliente reseteada.")