Add TCP-Server support and enhance connection management

- Implement TCP-Server connection type in ConfigManager and ConnectionManager.
- Update MaselliApp to handle TCP-Server mode, including UI adjustments.
- Modify NetComTab to prevent operation in TCP-Server mode.
- Enhance SimulatorTab to manage client connections and data sending for TCP-Server.
- Update trace_tab.py to ensure compatibility with TCP-Server.
- Improve logging functionality in utils.py to limit log lines.
This commit is contained in:
Miguel 2025-05-26 16:12:57 +02:00
parent 86669fc94c
commit a28f02b4c9
8 changed files with 431 additions and 79 deletions

View File

@ -90,6 +90,12 @@ class ConfigManager:
'port': config.get('com_port', 'COM3'),
'baud': int(config.get('baud_rate', '115200'))
}
elif conn_type == "TCP-Server":
# Para TCP-Server, la IP es implícitamente '0.0.0.0' (escuchar en todas las interfaces)
# Solo necesitamos el puerto para el bind.
return {
'port': int(config.get('port', '8899')) # Usa el mismo campo de puerto
}
else:
return {
'ip': config.get('ip_address', '192.168.1.100'),
@ -142,7 +148,7 @@ class ConfigManager:
except ValueError:
errors.append("La velocidad de baudios debe ser un número entero")
# Validar configuración TCP/UDP
# Validar configuración TCP/UDP/TCP-Server
else:
try:
port = int(config.get('port', '502'))

View File

@ -6,15 +6,34 @@ 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
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'],
@ -42,12 +61,49 @@ class ConnectionManager:
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}"
return self.connection
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:
@ -57,11 +113,24 @@ class ConnectionManager:
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):
@ -94,8 +163,25 @@ class ConnectionManager:
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:
raise Exception(f"Error al enviar datos: {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"""
@ -140,7 +226,25 @@ class ConnectionManager:
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:
@ -174,7 +278,21 @@ class ConnectionManager:
# 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:
@ -185,5 +303,43 @@ class ConnectionManager:
"""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.")

View File

@ -61,7 +61,7 @@ class MaselliApp:
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(0, weight=1)
main_frame.columnconfigure(0, weight=1)
main_frame.rowconfigure(1, weight=1)
main_frame.rowconfigure(1, weight=1) # Notebook
# Frame de configuración compartida
self.create_shared_config_frame(main_frame)
@ -102,14 +102,14 @@ class MaselliApp:
self.connection_type_var = tk.StringVar()
self.connection_type_combo = ttk.Combobox(
config_frame, textvariable=self.connection_type_var,
values=["Serial", "TCP", "UDP"], state="readonly", width=10
values=["Serial", "TCP", "UDP", "TCP-Server"], state="readonly", width=10
)
self.connection_type_combo.grid(row=0, column=1, padx=5, pady=5)
self.connection_type_combo.bind("<<ComboboxSelected>>", self.on_connection_type_change)
# Frame para Serial
self.serial_frame = ttk.Frame(config_frame)
self.serial_frame.grid(row=0, column=2, columnspan=4, padx=5, pady=5, sticky="ew")
self.serial_frame.grid(row=0, column=2, columnspan=6, padx=5, pady=5, sticky="ew") # Ajustado columnspan
ttk.Label(self.serial_frame, text="Puerto:").grid(row=0, column=0, padx=5, sticky="w")
self.com_port_var = tk.StringVar()
@ -123,18 +123,26 @@ class MaselliApp:
# Frame para Ethernet
self.ethernet_frame = ttk.Frame(config_frame)
self.ethernet_frame.grid(row=0, column=2, columnspan=4, padx=5, pady=5, sticky="ew")
self.ethernet_frame.grid(row=0, column=2, columnspan=6, padx=5, pady=5, sticky="ew") # Aumentado columnspan
self.ethernet_frame.grid_remove()
ttk.Label(self.ethernet_frame, text="IP:").grid(row=0, column=0, padx=5, sticky="w")
self.ip_address_label_widget = ttk.Label(self.ethernet_frame, text="IP:")
self.ip_address_label_widget.grid(row=0, column=0, padx=5, sticky="w")
self.ip_address_var = tk.StringVar()
self.ip_address_entry = ttk.Entry(self.ethernet_frame, textvariable=self.ip_address_var, width=15)
self.ip_address_entry.grid(row=0, column=1, padx=5)
ttk.Label(self.ethernet_frame, text="Puerto:").grid(row=0, column=2, padx=5, sticky="w")
self.port_label_widget = ttk.Label(self.ethernet_frame, text="Puerto:")
self.port_label_widget.grid(row=0, column=2, padx=5, sticky="w")
self.port_var = tk.StringVar()
self.port_entry = ttk.Entry(self.ethernet_frame, textvariable=self.port_var, width=8)
self.port_entry.grid(row=0, column=3, padx=5)
self.port_entry.grid(row=0, column=3, padx=(0,5), pady=5) # Ajustado padx
# Label para mostrar el cliente conectado en modo TCP-Server
self.client_connected_label_widget = ttk.Label(self.ethernet_frame, text="Cliente Conectado:")
# Se mostrará/ocultará en on_connection_type_change
self.client_connected_var = tk.StringVar(value="Ninguno")
self.client_connected_display = ttk.Label(self.ethernet_frame, textvariable=self.client_connected_var, width=25)
# Parámetros de mapeo
ttk.Label(config_frame, text="Min Brix [4mA]:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
@ -162,6 +170,7 @@ class MaselliApp:
'port_var': self.port_var,
'min_brix_map_var': self.min_brix_map_var,
'max_brix_map_var': self.max_brix_map_var,
'client_connected_var': self.client_connected_var, # Para actualizar desde el simulador
'shared_widgets': [
self.connection_type_combo,
self.com_port_entry,
@ -169,7 +178,8 @@ class MaselliApp:
self.ip_address_entry,
self.port_entry,
self.min_brix_map_entry,
self.max_brix_map_entry
self.max_brix_map_entry,
# self.client_connected_display # No deshabilitar el display, solo su contenido
]
})
@ -274,16 +284,71 @@ class MaselliApp:
def on_connection_type_change(self, event=None):
"""Maneja el cambio de tipo de conexión"""
conn_type = self.connection_type_var.get()
is_server_mode = (conn_type == "TCP-Server")
if conn_type == "Serial":
self.ethernet_frame.grid_remove()
self.serial_frame.grid()
else:
# No es necesario manipular los widgets dentro de ethernet_frame si está oculto
self.client_connected_label_widget.grid_remove()
self.client_connected_display.grid_remove()
self.client_connected_var.set("Ninguno")
elif conn_type == "TCP-Server":
self.serial_frame.grid_remove()
self.ethernet_frame.grid()
self.ip_address_label_widget.grid_remove() # Ocultar etiqueta IP
self.ip_address_entry.config(state=tk.DISABLED) # IP no se usa para el servidor
self.ip_address_entry.grid_remove() # Ocultar campo IP
self.port_label_widget.grid(row=0, column=2, padx=5, sticky="w") # Asegurar que la etiqueta Puerto esté visible
self.port_entry.config(state=tk.NORMAL) # Puerto es para escuchar
self.port_entry.grid(row=0, column=3, padx=(0,5), pady=5) # Asegurar que el campo Puerto esté visible
self.client_connected_label_widget.grid(row=0, column=4, padx=(10,2), pady=5, sticky="w")
self.client_connected_display.grid(row=0, column=5, padx=(0,5), pady=5, sticky="w")
else: # TCP, UDP
self.serial_frame.grid_remove()
self.ethernet_frame.grid()
self.ip_address_label_widget.grid(row=0, column=0, padx=5, sticky="w") # Asegurar que la etiqueta IP esté visible
self.ip_address_entry.config(state=tk.NORMAL)
self.ip_address_entry.grid(row=0, column=1, padx=5) # Asegurar que el campo IP esté visible
self.port_label_widget.grid(row=0, column=2, padx=5, sticky="w") # Asegurar que la etiqueta Puerto esté visible
self.port_entry.config(state=tk.NORMAL)
self.port_entry.grid(row=0, column=3, padx=(0,5), pady=5) # Asegurar que el campo Puerto esté visible
self.client_connected_label_widget.grid_remove()
self.client_connected_display.grid_remove()
self.client_connected_var.set("Ninguno")
# Actualizar info en NetCom
if hasattr(self, 'netcom_tab'):
self.netcom_tab.update_net_info()
# Habilitar/deshabilitar botones Start en otras pestañas según compatibilidad
if hasattr(self, 'simulator_tab'):
# El simulador maneja TCP-Server, su lógica de botón es interna
pass
if hasattr(self, 'trace_tab'):
if is_server_mode:
self.trace_tab.start_button.config(state=tk.DISABLED)
if self.trace_tab.tracing: # Si estaba trazando y el modo cambió
self.trace_tab.stop_trace()
messagebox.showinfo("Trace Detenido", "El modo Trace se detuvo porque el tipo de conexión cambió a TCP-Server, que no es compatible.")
elif not self.trace_tab.tracing : # Habilitar solo si no está trazando
self.trace_tab.start_button.config(state=tk.NORMAL)
if hasattr(self, 'netcom_tab'):
if is_server_mode:
self.netcom_tab.start_button.config(state=tk.DISABLED)
if self.netcom_tab.bridging: # Si estaba en modo bridge
self.netcom_tab.stop_bridge()
messagebox.showinfo("NetCom Detenido", "El modo NetCom se detuvo porque el tipo de conexión cambió a TCP-Server, que no es compatible.")
elif not self.netcom_tab.bridging: # Habilitar solo si no está en modo bridge
self.netcom_tab.start_button.config(state=tk.NORMAL)
def save_config(self):
"""Guarda la configuración actual"""
@ -337,6 +402,8 @@ class MaselliApp:
# Actualizar vista
self.on_connection_type_change()
if self.connection_type_var.get() != "TCP-Server":
self.client_connected_var.set("Ninguno")
def on_closing(self):
"""Maneja el cierre de la aplicación"""
@ -352,3 +419,4 @@ class MaselliApp:
# Cerrar ventana
self.root.destroy()

View File

@ -1,14 +1,14 @@
{
"connection_type": "TCP",
"connection_type": "TCP-Server",
"com_port": "COM8",
"baud_rate": "115200",
"ip_address": "10.1.33.18",
"port": "8899",
"min_brix_map": "0",
"max_brix_map": "80",
"max_brix_map": "60",
"adam_address": "01",
"function_type": "Manual",
"cycle_time": "3",
"function_type": "Sinusoidal",
"cycle_time": "15",
"samples_per_cycle": "100",
"manual_input_type": "Brix",
"manual_value": "0.00",

View File

@ -224,6 +224,12 @@ class NetComTab:
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()
@ -251,7 +257,8 @@ class NetComTab:
# Abrir conexión COM física
try:
self.com_connection.open_connection("Serial", {
# 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()),
@ -288,8 +295,12 @@ class NetComTab:
# 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()}")
# 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}")

View File

@ -347,36 +347,61 @@ class SimulatorTab:
conn_type = current_config_values['connection_type']
conn_params = self.shared_config['config_manager'].get_connection_params(current_config_values)
temp_conn = ConnectionManager()
try:
temp_conn.open_connection(conn_type, conn_params)
Utils.log_message(self.log_text, f"Conexión {conn_type} abierta temporalmente.")
Utils.log_message(self.log_text, f"Enviando Manual: {ProtocolHandler.format_for_display(message)}")
if conn_type == "TCP-Server":
if not self.connection_manager.is_client_connected(): # Verificar el connection_manager de la pestaña
Utils.log_message(self.log_text, "Envío Manual (TCP Server): Ningún cliente conectado.")
messagebox.showinfo("TCP Server", "Ningún cliente conectado para enviar datos manualmente.")
return
temp_conn.send_data(message)
# No necesitamos 'listening_details' aquí porque la conexión ya está establecida
# y el log de inicio ya se hizo. Solo usamos la conexión existente.
# La llamada a open_connection no ocurre aquí para TCP-Server en modo manual.
response = temp_conn.read_response(timeout=0.5)
if response and response.strip():
Utils.log_message(self.log_text, f"Respuesta: {ProtocolHandler.format_for_display(response)}")
try:
# message ya es bytes
self.connection_manager.send_data(message)
Utils.log_message(self.log_text, f"Enviando Manual (TCP Server): {ProtocolHandler.format_for_display(message)}")
# No se espera respuesta en modo servidor para el simulador
except self.connection_manager.ClientDisconnectedError:
Utils.log_message(self.log_text, "Envío Manual (TCP Server): Cliente desconectado durante el envío.")
messagebox.showerror("TCP Server Error", "El cliente se desconectó durante el envío manual.")
except Exception as e_manual_server:
Utils.log_message(self.log_text, f"Error al enviar manualmente (TCP Server): {e_manual_server}")
messagebox.showerror("Error", str(e_manual_server))
return # Terminar aquí para envío manual en TCP-Server
# Lógica existente para otros tipos de conexión (Serial, TCP Client, UDP)
else:
temp_conn = ConnectionManager()
try:
# open_connection ahora devuelve (connection_object, listening_info)
_, _ = temp_conn.open_connection(conn_type, conn_params) # Ignoramos listening_info para conexión temporal
Utils.log_message(self.log_text, f"Conexión {conn_type} abierta temporalmente.")
Utils.log_message(self.log_text, f"Enviando Manual: {ProtocolHandler.format_for_display(message)}")
parsed = ProtocolHandler.parse_adam_message(response)
if parsed:
brix_resp = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix_map, max_brix_map)
Utils.log_message(self.log_text,
f" -> Addr: {parsed['address']}, "
f"mA: {parsed['ma']:.3f}, "
f"Brix: {brix_resp:.3f}")
except Exception as e:
Utils.log_message(self.log_text, f"Error al enviar: {e}")
messagebox.showerror("Error", str(e))
finally:
temp_conn.close_connection()
Utils.log_message(self.log_text, "Conexión cerrada.")
temp_conn.send_data(message) # message ya es bytes
response = temp_conn.read_response(timeout=0.5)
if response and response.strip():
Utils.log_message(self.log_text, f"Respuesta: {ProtocolHandler.format_for_display(response)}")
parsed = ProtocolHandler.parse_adam_message(response)
if parsed:
brix_resp = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix_map, max_brix_map)
Utils.log_message(self.log_text,
f" -> Addr: {parsed['address']}, "
f"mA: {parsed['ma']:.3f}, "
f"Brix: {brix_resp:.3f}")
except Exception as e:
Utils.log_message(self.log_text, f"Error al enviar: {e}")
messagebox.showerror("Error", str(e))
finally:
temp_conn.close_connection()
Utils.log_message(self.log_text, "Conexión cerrada.")
except (ValueError, KeyError, TypeError) as e:
messagebox.showerror("Error", f"Valores inválidos en la configuración o entrada: {e}")
def start_simulation(self):
"""Inicia la simulación continua"""
if self.simulating:
@ -418,8 +443,14 @@ class SimulatorTab:
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 simulación.")
# open_connection ahora devuelve (connection_object, listening_info)
# El connection_object se guarda internamente en self.connection_manager
_, listening_details = self.connection_manager.open_connection(conn_type, conn_params)
if conn_type == "TCP-Server":
Utils.log_message(self.log_text, f"{listening_details} para simulación.")
elif conn_type != "TCP-Server": # Para otros tipos, el mensaje genérico
Utils.log_message(self.log_text, f"Conexión {conn_type} abierta para simulación.")
except Exception as e:
messagebox.showerror("Error de Conexión", str(e))
return
@ -430,6 +461,8 @@ class SimulatorTab:
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self._set_entries_state(tk.DISABLED)
if conn_type == "TCP-Server":
self.shared_config['client_connected_var'].set("Esperando...")
self.simulation_thread = threading.Thread(target=self.run_simulation, daemon=True)
self.simulation_thread.start()
@ -452,6 +485,8 @@ class SimulatorTab:
self.stop_button.config(state=tk.DISABLED)
self._set_entries_state(tk.NORMAL)
self.on_function_type_change() # Re-evaluar estado de controles manuales
if self.connection_manager.connection_type == "TCP-Server": # Limpiar info del cliente
self.shared_config['client_connected_var'].set("Ninguno")
Utils.log_message(self.log_text, "Simulación detenida.")
self.current_brix_var.set("---")
@ -467,7 +502,17 @@ class SimulatorTab:
function_type = self.function_type_var.get()
cycle_time = float(self.cycle_time_var.get())
samples_per_cycle = int(self.samples_per_cycle_var.get())
conn_type = self.connection_manager.connection_type # Obtener el tipo de conexión actual
# Obtener la configuración actual para el log del puerto en TCP-Server
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(),
}
sample_period = cycle_time / samples_per_cycle
while self.simulating:
@ -494,23 +539,45 @@ class SimulatorTab:
self.frame.after(0, lambda b=current_brix, m=ma_value: self.add_data_point(b, m))
Utils.log_message(self.log_text, f"Enviando: {ProtocolHandler.format_for_display(message)}")
try:
self.connection_manager.send_data(message)
response = self.connection_manager.read_response(timeout=0.1)
if response and response.strip():
Utils.log_message(self.log_text, f"Respuesta: {ProtocolHandler.format_for_display(response)}")
parsed = ProtocolHandler.parse_adam_message(response)
if parsed:
brix_resp = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix_map, max_brix_map)
Utils.log_message(self.log_text,
f" -> Addr: {parsed['address']}, "
f"mA: {parsed['ma']:.3f}, "
f"Brix: {brix_resp:.3f}")
if conn_type == "TCP-Server":
if not self.connection_manager.is_client_connected():
# Loguear solo si el estado cambia o periódicamente para evitar spam
if not hasattr(self, '_waiting_for_client_logged') or not self._waiting_for_client_logged:
port_to_log = self.shared_config['config_manager'].get_connection_params(current_config_values)['port']
Utils.log_message(self.log_text, f"TCP Server: Esperando cliente en puerto {port_to_log}...")
self._waiting_for_client_logged = True
if self.connection_manager.accept_client(timeout=0.05): # Intento corto no bloqueante
Utils.log_message(self.log_text, f"TCP Server: Cliente conectado desde {self.connection_manager.client_address}")
client_info = f"{self.connection_manager.client_address[0]}:{self.connection_manager.client_address[1]}"
self.shared_config['client_connected_var'].set(client_info)
self._waiting_for_client_logged = False # Resetear flag de log
elif not self.connection_manager.is_client_connected() and \
self.shared_config['client_connected_var'].get() != "Esperando...":
self.shared_config['client_connected_var'].set("Esperando...")
Utils.log_message(self.log_text, f"Enviando: {ProtocolHandler.format_for_display(message)}")
self.connection_manager.send_data(message)
if conn_type != "TCP-Server": # No leer respuesta en modo servidor
response = self.connection_manager.read_response(timeout=0.1)
if response and response.strip():
Utils.log_message(self.log_text, f"Respuesta: {ProtocolHandler.format_for_display(response)}")
parsed = ProtocolHandler.parse_adam_message(response)
if parsed:
brix_resp = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix_map, max_brix_map)
Utils.log_message(self.log_text,
f" -> Addr: {parsed['address']}, "
f"mA: {parsed['ma']:.3f}, "
f"Brix: {brix_resp:.3f}")
except self.connection_manager.ClientDisconnectedError:
Utils.log_message(self.log_text, "TCP Server: Cliente desconectado. Esperando nueva conexión.")
if conn_type == "TCP-Server":
self.shared_config['client_connected_var'].set("Esperando...")
self._waiting_for_client_logged = False # Permitir que se loguee "esperando" de nuevo
except Exception as e:
Utils.log_message(self.log_text, f"Error en comunicación: {e}")
Utils.log_message(self.log_text, f"Error en comunicación ({conn_type}): {e}")
self.frame.after(0, self.stop_simulation_error) # Schedule GUI update from main thread
break

View File

@ -9,6 +9,13 @@ 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
@ -120,6 +127,12 @@ class TraceTab:
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")
@ -201,7 +214,7 @@ class TraceTab:
def run_trace(self):
"""Thread principal para recepción de datos"""
buffer = ""
buffer = bytearray() # Cambiar buffer a bytearray
while self.tracing:
try:
@ -209,32 +222,43 @@ class TraceTab:
data = self.connection_manager.read_data_non_blocking()
if data:
buffer += data
buffer.extend(data) # Usar extend para bytearray
# Buscar mensajes completos
while '\r' in buffer or '\n' in buffer or len(buffer) >= 10:
# Encontrar el primer terminador
# 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
for i, char in enumerate(buffer):
if char in ['\r', '\n']:
# 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
if buffer[0] == '#' or (len(buffer) >= 10 and buffer[2:8].replace('.', '').isdigit()):
end_idx = 10 # Longitud mínima de un mensaje ADAM
if len(buffer) > 10 and buffer[10] in ['\r', '\n']:
end_idx = 11
# 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 = buffer[:end_idx]
message_bytes = bytes(buffer[:end_idx]) # Extraer como bytes
buffer = buffer[end_idx:]
# Procesar mensaje si tiene contenido
if message.strip():
self._process_message(message)
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

View File

@ -6,16 +6,28 @@ import tkinter as tk
from datetime import datetime
import os
MAX_LOG_LINES = 100 # Número máximo de líneas en el log
class Utils:
@staticmethod
def log_message(log_widget, message):
"""Escribe un mensaje con timestamp en el widget de log especificado"""
"""Escribe un mensaje con timestamp en el widget de log especificado, limitando el número de líneas."""
if log_widget:
log_widget.configure(state=tk.NORMAL)
timestamp = datetime.now().strftime('%H:%M:%S')
log_widget.insert(tk.END, f"[{timestamp}] {message}\n")
# Limitar el número de líneas
num_lines = int(log_widget.index('end-1c').split('.')[0])
if num_lines > MAX_LOG_LINES:
lines_to_delete = num_lines - MAX_LOG_LINES
# Sumamos 1.0 porque delete va hasta el índice anterior al segundo parámetro
log_widget.delete('1.0', f"{lines_to_delete + 1}.0")
log_widget.see(tk.END)
log_widget.configure(state=tk.DISABLED)
@staticmethod
def load_icon(root):
@ -81,3 +93,11 @@ class Utils:
for container in data_containers:
if hasattr(container, 'clear'):
container.clear()
@staticmethod
def clear_log_widget(log_widget):
"""Limpia el contenido del widget de log especificado."""
if log_widget:
log_widget.configure(state=tk.NORMAL)
log_widget.delete('1.0', tk.END)
log_widget.configure(state=tk.DISABLED)