346 lines
17 KiB
Python
346 lines
17 KiB
Python
"""
|
|
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.")
|