MaselliSimulatorApp/tabs/netcom_tab.py

555 lines
27 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")
# Data bits, Parity, Stop bits
ttk.Label(com_config_frame, text="Data Bits:").grid(row=0, column=4, padx=5, pady=5, sticky="w")
self.bytesize_var = tk.StringVar(value=str(self.shared_config.get('netcom_bytesize', 8)))
self.bytesize_combo = ttk.Combobox(com_config_frame, textvariable=self.bytesize_var,
values=["5", "6", "7", "8"], state="readonly", width=5)
self.bytesize_combo.grid(row=0, column=5, padx=5, pady=5, sticky="ew")
ttk.Label(com_config_frame, text="Parity:").grid(row=0, column=6, padx=5, pady=5, sticky="w")
self.parity_var = tk.StringVar(value=self.shared_config.get('netcom_parity', 'N'))
self.parity_combo = ttk.Combobox(com_config_frame, textvariable=self.parity_var,
values=["N", "E", "O", "M", "S"], state="readonly", width=5) # N: None, E: Even, O: Odd, M: Mark, S: Space
self.parity_combo.grid(row=0, column=7, padx=5, pady=5, sticky="ew")
ttk.Label(com_config_frame, text="Stop Bits:").grid(row=0, column=8, padx=5, pady=5, sticky="w")
self.stopbits_var = tk.StringVar(value=str(self.shared_config.get('netcom_stopbits', 1)))
self.stopbits_combo = ttk.Combobox(com_config_frame, textvariable=self.stopbits_var,
values=["1", "1.5", "2"], state="readonly", width=5)
self.stopbits_combo.grid(row=0, column=9, padx=5, pady=5, sticky="ew")
# Flow control options
self.rtscts_var = tk.BooleanVar(value=self.shared_config.get('netcom_rtscts', False))
self.rtscts_check = ttk.Checkbutton(com_config_frame, text="RTS/CTS", variable=self.rtscts_var)
self.rtscts_check.grid(row=1, column=0, padx=5, pady=5, sticky="w")
self.dsrdtr_var = tk.BooleanVar(value=self.shared_config.get('netcom_dsrdtr', False))
self.dsrdtr_check = ttk.Checkbutton(com_config_frame, text="DSR/DTR", variable=self.dsrdtr_var)
self.dsrdtr_check.grid(row=1, column=1, padx=5, pady=5, sticky="w")
self.xonxoff_var = tk.BooleanVar(value=self.shared_config.get('netcom_xonxoff', False))
self.xonxoff_check = ttk.Checkbutton(com_config_frame, text="XON/XOFF", variable=self.xonxoff_var)
self.xonxoff_check.grid(row=1, column=2, padx=5, pady=5, sticky="w")
# Bridge delay
ttk.Label(com_config_frame, text="Retardo Bridge (s):").grid(row=1, column=3, padx=5, pady=5, sticky="w")
self.bridge_delay_var = tk.StringVar(value=str(self.shared_config.get('netcom_bridge_delay', 0.001)))
self.bridge_delay_entry = ttk.Entry(com_config_frame, textvariable=self.bridge_delay_var, width=8)
self.bridge_delay_entry.grid(row=1, column=4, 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())
bridge_delay_str = self.bridge_delay_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")
try:
self.current_bridge_delay = float(bridge_delay_str)
if self.current_bridge_delay < 0:
raise ValueError("El retardo del bridge no puede ser negativo.")
except ValueError:
raise ValueError("Retardo del bridge inválido. Debe ser un número (ej: 0.001).")
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,
'baudrate': baud_rate,
'bytesize': int(self.bytesize_var.get()),
'parity': self.parity_var.get(),
'stopbits': float(self.stopbits_var.get()),
'rtscts': self.rtscts_var.get(),
'dsrdtr': self.dsrdtr_var.get(),
'xonxoff': self.xonxoff_var.get()
})
# Log basic serial config
serial_config_log = f"{com_port} @ {baud_rate} bps, {self.bytesize_var.get()}{self.parity_var.get()}{self.stopbits_var.get()}"
fc_log = []
if self.rtscts_var.get(): fc_log.append("RTS/CTS")
if self.dsrdtr_var.get(): fc_log.append("DSR/DTR")
if self.xonxoff_var.get(): fc_log.append("XON/XOFF")
self.log_message(f"Puerto COM abierto: {serial_config_log}. Flow control: {', '.join(fc_log) if fc_log else 'Ninguno'}")
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 = bytearray()
# net_buffer = bytearray() # Ya no se usa para el flujo NET->COM si pasamos los datos directamente
current_delay = self.current_bridge_delay
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
# Adaptar la condición para bytearray
while self._find_message_end_conditions(com_buffer):
end_idx = self._find_message_end(com_buffer)
if end_idx > 0:
message_bytes = bytes(com_buffer[:end_idx])
com_buffer = com_buffer[end_idx:]
# Log y parseo
use_hex_format_for_raw = self.show_parsed_var.get()
display_msg = ProtocolHandler.format_for_display(message_bytes, hex_non_printable=use_hex_format_for_raw)
self.log_message(f"Data: {display_msg}", "com_to_net")
# Intentar parsear si está habilitado
if self.show_parsed_var.get():
# Decodificar solo para el parseo
message_str_for_parse = message_bytes.decode('ascii', errors='ignore')
parsed = ProtocolHandler.parse_adam_message(message_str_for_parse)
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_bytes)
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:
# Los datos de la red (net_data) son bytes.
# Se reenvían directamente al puerto COM.
# Log y parseo (opcional, sobre los datos recibidos directamente)
use_hex_format_for_raw = self.show_parsed_var.get()
display_msg = ProtocolHandler.format_for_display(net_data, hex_non_printable=use_hex_format_for_raw)
self.log_message(f"Data: {display_msg}", "net_to_com")
# Intentar parsear si está habilitado (puede ser sobre fragmentos)
if self.show_parsed_var.get():
# Decodificar solo para el parseo
# Nota: parsear fragmentos puede no ser siempre significativo para protocolos como ADAM.
message_str_for_parse = net_data.decode('ascii', errors='ignore')
parsed = ProtocolHandler.parse_adam_message(message_str_for_parse)
if parsed:
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 (datos red) - 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(net_data) # Enviar los bytes tal cual se recibieron
self.net_to_com_count += len(net_data) # Contar bytes en lugar de "mensajes"
self.update_stats()
except Exception as e:
self.log_message(f"Error enviando a COM: {e}", "error")
self.error_count += 1
self.update_stats()
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(current_delay)
# Asegurar que el estado se actualice
if not self.bridging:
self.frame.after(0, self._ensure_stopped_state)
def _find_message_end_conditions(self, buffer_bytes: bytearray):
"""Verifica si hay condiciones para buscar el final de un mensaje."""
if not buffer_bytes:
return False
has_terminator = any(byte_val in (ord(b'\r'), ord(b'\n')) for byte_val in buffer_bytes)
return has_terminator or len(buffer_bytes) >= 10
def _find_message_end(self, buffer_bytes: bytearray):
"""Encuentra el final de un mensaje en el buffer de bytes."""
# Buscar terminadores
for i, byte_val in enumerate(buffer_bytes):
if byte_val == ord(b'\r') or byte_val == ord(b'\n'):
return i + 1
# Si no hay terminador pero el buffer es largo, buscar mensaje ADAM completo
# Esta parte es una heurística para mensajes tipo ADAM que podrían no tener terminador
# y debe usarse con cuidado para no cortar mensajes prematuramente.
if len(buffer_bytes) >= 10:
starts_with_hash = (buffer_bytes[0] == ord(b'#'))
is_adam_value_like = False
if len(buffer_bytes) >= 8: # Asegurar que el slice buffer_bytes[2:8] sea válido
try:
# Convertir la parte del valor a string para una verificación más sencilla
value_part_str = bytes(buffer_bytes[2:8]).decode('ascii')
# Formato ADAM es XX.XXX (6 caracteres)
if len(value_part_str) == 6 and value_part_str[2] == '.' and \
value_part_str[0:2].isdigit() and value_part_str[3:6].isdigit():
is_adam_value_like = True
except UnicodeDecodeError:
pass # No es ASCII, no es el formato ADAM esperado
if starts_with_hash or is_adam_value_like:
# Heurística: si parece ADAM y tiene al menos 10 bytes.
# Si después de 10 bytes hay un terminador, incluirlo.
if len(buffer_bytes) > 10 and \
(buffer_bytes[10] == ord(b'\r') or buffer_bytes[10] == ord(b'\n')):
return 11
else:
# Asumir un mensaje de 10 bytes (ej: #AAXX.XXXCC)
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))
# Si net_to_com_count ahora cuenta bytes, el label "NET → COM:" seguido de un número
# podría interpretarse como mensajes. Para mayor claridad, se podría cambiar el label
# o el formato del valor (ej. self.net_to_com_var.set(f"{self.net_to_com_count} bytes")).
# Por ahora, solo actualizamos el valor; el label no cambia.
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)
self.bytesize_combo.config(state=state)
self.parity_combo.config(state=state)
self.stopbits_combo.config(state=state)
self.rtscts_check.config(state=state)
self.dsrdtr_check.config(state=state)
self.xonxoff_check.config(state=state)
self.bridge_delay_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(),
'netcom_rtscts': self.rtscts_var.get(),
'netcom_dsrdtr': self.dsrdtr_var.get(),
'netcom_xonxoff': self.xonxoff_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'))
self.rtscts_var.set(config.get('netcom_rtscts', False))
self.dsrdtr_var.set(config.get('netcom_dsrdtr', False))
self.xonxoff_var.set(config.get('netcom_xonxoff', False))