diff --git a/config_manager.py b/config_manager.py index 9c1a18f..3e3aa87 100644 --- a/config_manager.py +++ b/config_manager.py @@ -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')) diff --git a/connection_manager.py b/connection_manager.py index 64e633b..d17a134 100644 --- a/connection_manager.py +++ b/connection_manager.py @@ -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.") diff --git a/maselli_app.py b/maselli_app.py index e1abaa3..8626434 100644 --- a/maselli_app.py +++ b/maselli_app.py @@ -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("<>", 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() + diff --git a/maselli_simulator_config.json b/maselli_simulator_config.json index e9539d8..261ca2f 100644 --- a/maselli_simulator_config.json +++ b/maselli_simulator_config.json @@ -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", diff --git a/tabs/netcom_tab.py b/tabs/netcom_tab.py index d85745b..3487a11 100644 --- a/tabs/netcom_tab.py +++ b/tabs/netcom_tab.py @@ -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}") diff --git a/tabs/simulator_tab.py b/tabs/simulator_tab.py index a3b6922..d4d9474 100644 --- a/tabs/simulator_tab.py +++ b/tabs/simulator_tab.py @@ -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 diff --git a/tabs/trace_tab.py b/tabs/trace_tab.py index 094b09c..a1c4289 100644 --- a/tabs/trace_tab.py +++ b/tabs/trace_tab.py @@ -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 diff --git a/utils.py b/utils.py index 10294b5..1857eef 100644 --- a/utils.py +++ b/utils.py @@ -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)