MaselliSimulatorApp/tabs/netcom_tab.py

454 lines
20 KiB
Python

"""
Tab NetCom - Gateway/Bridge entre puerto COM físico y conexión TCP/UDP
"""
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import threading
import time
from datetime import datetime
from connection_manager import ConnectionManager
from protocol_handler import ProtocolHandler
from utils import Utils
class NetComTab:
def __init__(self, parent_frame, shared_config):
self.frame = parent_frame
self.shared_config = shared_config
# Estado del gateway
self.bridging = False
self.bridge_thread = None
# Conexiones
self.com_connection = ConnectionManager()
self.net_connection = ConnectionManager()
# Estadísticas
self.com_to_net_count = 0
self.net_to_com_count = 0
self.error_count = 0
self.create_widgets()
def create_widgets(self):
"""Crea los widgets del tab NetCom"""
# Frame de configuración COM física
com_config_frame = ttk.LabelFrame(self.frame, text="Configuración Puerto COM Físico")
com_config_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
ttk.Label(com_config_frame, text="Puerto COM:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.com_port_var = tk.StringVar(value=self.shared_config.get('netcom_com_port', 'COM3'))
self.com_port_entry = ttk.Entry(com_config_frame, textvariable=self.com_port_var, width=10)
self.com_port_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
ttk.Label(com_config_frame, text="Baud Rate:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
self.baud_rate_var = tk.StringVar(value=self.shared_config.get('netcom_baud_rate', '115200'))
self.baud_rate_entry = ttk.Entry(com_config_frame, textvariable=self.baud_rate_var, width=10)
self.baud_rate_entry.grid(row=0, column=3, padx=5, pady=5, sticky="ew")
# Info frame
info_frame = ttk.LabelFrame(self.frame, text="Información de Conexión")
info_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
ttk.Label(info_frame, text="Conexión de Red:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.net_info_var = tk.StringVar(value="No configurada")
ttk.Label(info_frame, textvariable=self.net_info_var, font=("Courier", 10)).grid(row=0, column=1, padx=5, pady=5, sticky="w")
ttk.Label(info_frame, text="Estado:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.status_var = tk.StringVar(value="Desconectado")
self.status_label = ttk.Label(info_frame, textvariable=self.status_var, font=("Courier", 10, "bold"))
self.status_label.grid(row=1, column=1, padx=5, pady=5, sticky="w")
# Control Frame
control_frame = ttk.LabelFrame(self.frame, text="Control Gateway")
control_frame.grid(row=2, column=0, padx=10, pady=5, sticky="ew")
self.start_button = ttk.Button(control_frame, text="Iniciar Gateway", command=self.start_bridge)
self.start_button.pack(side=tk.LEFT, padx=5)
self.stop_button = ttk.Button(control_frame, text="Detener Gateway", command=self.stop_bridge, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=5)
self.clear_log_button = ttk.Button(control_frame, text="Limpiar Log", command=self.clear_log)
self.clear_log_button.pack(side=tk.LEFT, padx=5)
# Statistics Frame
stats_frame = ttk.LabelFrame(self.frame, text="Estadísticas")
stats_frame.grid(row=2, column=1, padx=10, pady=5, sticky="ew")
ttk.Label(stats_frame, text="COM → NET:").grid(row=0, column=0, padx=5, pady=2, sticky="w")
self.com_to_net_var = tk.StringVar(value="0")
ttk.Label(stats_frame, textvariable=self.com_to_net_var).grid(row=0, column=1, padx=5, pady=2, sticky="w")
ttk.Label(stats_frame, text="NET → COM:").grid(row=0, column=2, padx=5, pady=2, sticky="w")
self.net_to_com_var = tk.StringVar(value="0")
ttk.Label(stats_frame, textvariable=self.net_to_com_var).grid(row=0, column=3, padx=5, pady=2, sticky="w")
ttk.Label(stats_frame, text="Errores:").grid(row=1, column=0, padx=5, pady=2, sticky="w")
self.errors_var = tk.StringVar(value="0")
ttk.Label(stats_frame, textvariable=self.errors_var, foreground="red").grid(row=1, column=1, padx=5, pady=2, sticky="w")
# Log Frame con filtros
log_control_frame = ttk.Frame(self.frame)
log_control_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=(10,0), sticky="ew")
ttk.Label(log_control_frame, text="Filtros:").pack(side=tk.LEFT, padx=5)
self.show_com_to_net_var = tk.BooleanVar(value=True)
ttk.Checkbutton(log_control_frame, text="COM→NET",
variable=self.show_com_to_net_var).pack(side=tk.LEFT, padx=5)
self.show_net_to_com_var = tk.BooleanVar(value=True)
ttk.Checkbutton(log_control_frame, text="NET→COM",
variable=self.show_net_to_com_var).pack(side=tk.LEFT, padx=5)
self.show_parsed_var = tk.BooleanVar(value=True)
ttk.Checkbutton(log_control_frame, text="Mostrar datos parseados",
variable=self.show_parsed_var).pack(side=tk.LEFT, padx=5)
# Log Frame
log_frame = ttk.LabelFrame(self.frame, text="Log de Gateway (Sniffer)")
log_frame.grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
self.log_text = scrolledtext.ScrolledText(log_frame, height=20, width=80, wrap=tk.WORD, state=tk.DISABLED)
self.log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
# Configurar tags para colores
self.log_text.tag_config("com_to_net", foreground="blue")
self.log_text.tag_config("net_to_com", foreground="green")
self.log_text.tag_config("error", foreground="red")
self.log_text.tag_config("info", foreground="black")
self.log_text.tag_config("parsed", foreground="purple")
# Configurar pesos
self.frame.columnconfigure(0, weight=1)
self.frame.columnconfigure(1, weight=1)
self.frame.rowconfigure(4, weight=1)
# Actualizar info de red
self.update_net_info()
def update_net_info(self):
"""Actualiza la información de la conexión de red configurada"""
conn_type = self.shared_config['connection_type_var'].get()
if conn_type == "Serial":
port = self.shared_config['com_port_var'].get()
baud = self.shared_config['baud_rate_var'].get()
self.net_info_var.set(f"Serial: {port} @ {baud} bps")
elif conn_type == "TCP":
ip = self.shared_config['ip_address_var'].get()
port = self.shared_config['port_var'].get()
self.net_info_var.set(f"TCP: {ip}:{port}")
elif conn_type == "UDP":
ip = self.shared_config['ip_address_var'].get()
port = self.shared_config['port_var'].get()
self.net_info_var.set(f"UDP: {ip}:{port}")
def log_message(self, message, tag="info", force=False):
"""Log con formato especial para el sniffer"""
# Verificar filtros
if not force:
if tag == "com_to_net" and not self.show_com_to_net_var.get():
return
if tag == "net_to_com" and not self.show_net_to_com_var.get():
return
self.log_text.configure(state=tk.NORMAL)
timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
# Agregar prefijo según la dirección
if tag == "com_to_net":
prefix = "[COM→NET]"
elif tag == "net_to_com":
prefix = "[NET→COM]"
elif tag == "error":
prefix = "[ERROR] "
elif tag == "parsed":
prefix = "[PARSED] "
else:
prefix = "[INFO] "
full_message = f"[{timestamp}] {prefix} {message}\n"
# Insertar con color
start_index = self.log_text.index(tk.END)
self.log_text.insert(tk.END, full_message)
end_index = self.log_text.index(tk.END)
self.log_text.tag_add(tag, start_index, end_index)
self.log_text.see(tk.END)
self.log_text.configure(state=tk.DISABLED)
def start_bridge(self):
"""Inicia el gateway/bridge"""
if self.bridging:
messagebox.showwarning("Advertencia", "El gateway ya está activo.")
return
# Actualizar info de red
self.update_net_info()
# Validar configuración
try:
com_port = self.com_port_var.get()
baud_rate = int(self.baud_rate_var.get())
if not com_port.upper().startswith('COM'):
raise ValueError("Puerto COM inválido")
if baud_rate <= 0:
raise ValueError("Baud rate debe ser mayor que 0")
except ValueError as e:
messagebox.showerror("Error", f"Configuración inválida: {e}")
return
# Abrir conexión COM física
try:
self.com_connection.open_connection("Serial", {
'port': com_port,
'baud': baud_rate
})
self.log_message(f"Puerto COM abierto: {com_port} @ {baud_rate} bps")
except Exception as e:
messagebox.showerror("Error", f"No se pudo abrir puerto COM: {e}")
return
# Abrir conexión de red
try:
# For the network side of the bridge, use shared connection settings
net_conn_type_actual = self.shared_config['connection_type_var'].get()
current_shared_config_values = {
'connection_type': self.shared_config['connection_type_var'].get(),
'com_port': self.shared_config['com_port_var'].get(),
'baud_rate': self.shared_config['baud_rate_var'].get(),
'ip_address': self.shared_config['ip_address_var'].get(),
'port': self.shared_config['port_var'].get(),
}
# The first argument to get_connection_params is the dictionary it will read from.
net_conn_params = self.shared_config['config_manager'].get_connection_params(current_shared_config_values)
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()}")
except Exception as e:
self.com_connection.close_connection()
messagebox.showerror("Error", f"No se pudo abrir conexión de red: {e}")
return
# Resetear estadísticas
self.com_to_net_count = 0
self.net_to_com_count = 0
self.error_count = 0
self.update_stats()
# Iniciar bridge
self.bridging = True
self.status_var.set("Conectado")
self.status_label.config(foreground="green")
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self._set_entries_state(tk.DISABLED)
self.bridge_thread = threading.Thread(target=self.run_bridge, daemon=True)
self.bridge_thread.start()
self.log_message("Gateway iniciado - Modo bridge activo")
def stop_bridge(self):
"""Detiene el gateway/bridge"""
if not self.bridging:
return
self.bridging = False
# Esperar a que termine el thread
if self.bridge_thread and self.bridge_thread.is_alive():
self.bridge_thread.join(timeout=2.0)
# Cerrar conexiones
self.com_connection.close_connection()
self.net_connection.close_connection()
self.status_var.set("Desconectado")
self.status_label.config(foreground="black")
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self._set_entries_state(tk.NORMAL)
self.log_message("Gateway detenido")
self.log_message(f"Total transferencias - COM→NET: {self.com_to_net_count}, NET→COM: {self.net_to_com_count}, Errores: {self.error_count}")
def run_bridge(self):
"""Thread principal del bridge"""
com_buffer = ""
net_buffer = ""
while self.bridging:
try:
# Leer del COM físico
com_data = self.com_connection.read_data_non_blocking()
if com_data:
com_buffer += com_data
# Buscar mensajes completos para logging
while '\r' in com_buffer or '\n' in com_buffer or len(com_buffer) >= 10:
end_idx = self._find_message_end(com_buffer)
if end_idx > 0:
message = com_buffer[:end_idx]
com_buffer = com_buffer[end_idx:]
# Log y parseo
display_msg = ProtocolHandler.format_for_display(message)
self.log_message(f"Data: {display_msg}", "com_to_net")
# Intentar parsear si está habilitado
if self.show_parsed_var.get():
parsed = ProtocolHandler.parse_adam_message(message)
if parsed:
# Obtener valores de mapeo
min_brix = float(self.shared_config['min_brix_map_var'].get())
max_brix = float(self.shared_config['max_brix_map_var'].get())
brix_value = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix, max_brix)
self.log_message(
f"ADAM - Addr: {parsed['address']}, "
f"mA: {parsed['ma']:.3f}, "
f"Brix: {brix_value:.3f}, "
f"Checksum: {'OK' if parsed.get('checksum_valid', True) else 'ERROR'}",
"parsed"
)
# Reenviar a la red
try:
self.net_connection.send_data(message)
self.com_to_net_count += 1
self.update_stats()
except Exception as e:
self.log_message(f"Error enviando a red: {e}", "error")
self.error_count += 1
self.update_stats()
else:
break
# Leer de la red
net_data = self.net_connection.read_data_non_blocking()
if net_data:
net_buffer += net_data
# Buscar mensajes completos para logging
while '\r' in net_buffer or '\n' in net_buffer or len(net_buffer) >= 10:
end_idx = self._find_message_end(net_buffer)
if end_idx > 0:
message = net_buffer[:end_idx]
net_buffer = net_buffer[end_idx:]
# Log y parseo
display_msg = ProtocolHandler.format_for_display(message)
self.log_message(f"Data: {display_msg}", "net_to_com")
# Intentar parsear si está habilitado
if self.show_parsed_var.get():
parsed = ProtocolHandler.parse_adam_message(message)
if parsed:
# Obtener valores de mapeo
min_brix = float(self.shared_config['min_brix_map_var'].get())
max_brix = float(self.shared_config['max_brix_map_var'].get())
brix_value = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix, max_brix)
self.log_message(
f"ADAM - Addr: {parsed['address']}, "
f"mA: {parsed['ma']:.3f}, "
f"Brix: {brix_value:.3f}, "
f"Checksum: {'OK' if parsed.get('checksum_valid', True) else 'ERROR'}",
"parsed"
)
# Reenviar al COM
try:
self.com_connection.send_data(message)
self.net_to_com_count += 1
self.update_stats()
except Exception as e:
self.log_message(f"Error enviando a COM: {e}", "error")
self.error_count += 1
self.update_stats()
else:
break
except Exception as e:
if self.bridging:
self.log_message(f"Error en bridge: {e}", "error")
self.error_count += 1
self.update_stats()
break
# Pequeña pausa para no consumir demasiado CPU
if not com_data and not net_data:
time.sleep(0.001)
# Asegurar que el estado se actualice
if not self.bridging:
self.frame.after(0, self._ensure_stopped_state)
def _find_message_end(self, buffer):
"""Encuentra el final de un mensaje en el buffer"""
# Buscar terminadores
for i, char in enumerate(buffer):
if char in ['\r', '\n']:
return i + 1
# Si no hay terminador pero el buffer es largo, buscar mensaje ADAM completo
if len(buffer) >= 10:
if buffer[0] == '#' or (buffer[2:8].replace('.', '').replace(' ', '').replace('-', '').isdigit()):
# Parece un mensaje ADAM
if len(buffer) > 10 and buffer[10] in ['\r', '\n']:
return 11
else:
return 10
return -1
def update_stats(self):
"""Actualiza las estadísticas en la GUI"""
self.com_to_net_var.set(str(self.com_to_net_count))
self.net_to_com_var.set(str(self.net_to_com_count))
self.errors_var.set(str(self.error_count))
def clear_log(self):
"""Limpia el log"""
self.log_text.configure(state=tk.NORMAL)
self.log_text.delete(1.0, tk.END)
self.log_text.configure(state=tk.DISABLED)
self.log_message("Log limpiado", force=True)
def _set_entries_state(self, state):
"""Habilita/deshabilita los controles durante el bridge"""
self.com_port_entry.config(state=state)
self.baud_rate_entry.config(state=state)
# También deshabilitar controles compartidos
if 'shared_widgets' in self.shared_config:
Utils.set_widgets_state(self.shared_config['shared_widgets'], state)
def _ensure_stopped_state(self):
"""Asegura que la GUI refleje el estado detenido"""
self.status_var.set("Desconectado")
self.status_label.config(foreground="black")
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self._set_entries_state(tk.NORMAL)
def get_config(self):
"""Obtiene la configuración actual del NetCom"""
return {
'netcom_com_port': self.com_port_var.get(),
'netcom_baud_rate': self.baud_rate_var.get()
}
def set_config(self, config):
"""Establece la configuración del NetCom"""
self.com_port_var.set(config.get('netcom_com_port', 'COM3'))
self.baud_rate_var.set(config.get('netcom_baud_rate', '115200'))