Separado de eventos en el log de los eventos ciclicos

This commit is contained in:
Miguel 2025-05-31 00:07:10 +02:00
parent d7d52b2565
commit fe0abb9965
1 changed files with 109 additions and 244 deletions

View File

@ -33,8 +33,6 @@ class SimulatorTab:
self.start_time = time.time()
# Cargar configuración inicial para obtener valores por defecto para StringVars
# Esto es para asegurar que las StringVars tengan un valor inicial antes de que set_config sea llamado
# por maselli_app.py.
initial_config = self.shared_config['config_manager'].load_config()
self.adam_address_var = tk.StringVar(value=initial_config.get('adam_address', '01'))
@ -54,7 +52,7 @@ class SimulatorTab:
self.current_brix_var = tk.StringVar(value="---")
self.current_ma_var = tk.StringVar(value="--.-- mA")
self.current_voltage_var = tk.StringVar(value="-.-- V") # Nueva para voltaje
self.current_voltage_var = tk.StringVar(value="-.-- V")
# Para simulación de errores
self.random_error_timer = None
@ -66,69 +64,54 @@ class SimulatorTab:
self.actual_graph_frame_container = None # Inicializar ANTES de create_widgets
self.create_widgets()
# La línea anterior que asignaba None aquí ha sido eliminada.
def create_widgets(self):
"""Crea los widgets del tab simulador"""
# Frame de configuración del simulador
config_frame = ttk.LabelFrame(self.frame, text="Configuración Simulador")
config_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew", columnspan=2)
# Dirección ADAM
ttk.Label(config_frame, text="ADAM Address (2c):").grid(row=0, column=0, padx=5, pady=5, sticky="w")
# self.adam_address_var inicializada en __init__
self.adam_address_entry = ttk.Entry(config_frame, textvariable=self.adam_address_var, width=5)
self.adam_address_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
# Función
ttk.Label(config_frame, text="Función:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
# self.function_type_var inicializada en __init__
self.function_type_combo = ttk.Combobox(config_frame, textvariable=self.function_type_var,
values=["Lineal", "Sinusoidal", "Manual"],
state="readonly", width=10)
self.function_type_combo.grid(row=0, column=3, padx=5, pady=5, sticky="ew")
self.function_type_combo.bind("<<ComboboxSelected>>", self.on_function_type_change)
# Tiempo de ciclo completo (nueva característica)
ttk.Label(config_frame, text="Tiempo Ciclo (s):").grid(row=0, column=4, padx=5, pady=5, sticky="w")
# self.cycle_time_var inicializada en __init__
self.cycle_time_entry = ttk.Entry(config_frame, textvariable=self.cycle_time_var, width=8)
self.cycle_time_entry.grid(row=0, column=5, padx=5, pady=5, sticky="ew")
# Velocidad de muestreo (calculada automáticamente)
ttk.Label(config_frame, text="Muestras/ciclo:").grid(row=0, column=6, padx=5, pady=5, sticky="w")
# self.samples_per_cycle_var inicializada en __init__
self.samples_per_cycle_entry = ttk.Entry(config_frame, textvariable=self.samples_per_cycle_var, width=8)
self.samples_per_cycle_entry.grid(row=0, column=7, padx=5, pady=5, sticky="ew")
# --- Frame para modo Manual (Modificado) ---
manual_frame = ttk.LabelFrame(config_frame, text="Modo Manual")
manual_frame.grid(row=1, column=0, columnspan=8, padx=5, pady=5, sticky="ew")
ttk.Label(manual_frame, text="Entrada Por:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
# self.manual_input_type_var inicializada en __init__
self.manual_input_type_combo = ttk.Combobox(manual_frame, textvariable=self.manual_input_type_var,
values=["Brix", "mA", "Voltaje"], state="readonly", width=8)
self.manual_input_type_combo.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
self.manual_input_type_combo.bind("<<ComboboxSelected>>", self.on_manual_input_type_change)
self.manual_value_label = ttk.Label(manual_frame, text="Valor Brix:") # Se actualiza dinámicamente
self.manual_value_label = ttk.Label(manual_frame, text="Valor Brix:")
self.manual_value_label.grid(row=1, column=0, padx=5, pady=5, sticky="w")
# self.manual_value_var y self.manual_slider_var inicializadas en __init__
self.manual_value_entry = ttk.Entry(manual_frame, textvariable=self.manual_value_var, width=10, state=tk.DISABLED)
self.manual_value_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
self.manual_value_entry.bind('<Return>', lambda e: self.update_slider_from_entry())
self.manual_value_entry.bind('<FocusOut>', lambda e: self.update_slider_from_entry())
# Slider
self.manual_slider = ttk.Scale(manual_frame, orient=tk.HORIZONTAL, # from_ y to_ se configuran dinámicamente
self.manual_slider = ttk.Scale(manual_frame, orient=tk.HORIZONTAL,
variable=self.manual_slider_var, command=self.on_slider_change,
state=tk.DISABLED, length=200)
self.manual_slider.grid(row=1, column=2, padx=5, pady=5, sticky="ew")
manual_frame.columnconfigure(2, weight=1)
# Controls Frame
controls_frame = ttk.LabelFrame(self.frame, text="Control Simulación")
controls_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew")
@ -138,90 +121,75 @@ class SimulatorTab:
self.stop_button = ttk.Button(controls_frame, text="Detener", command=self.stop_simulation, state=tk.DISABLED)
self.stop_button.pack(side=tk.LEFT, padx=5)
self.clear_comm_log_button = ttk.Button(controls_frame, text="Limpiar Log Com.", command=self.clear_comm_log)
self.clear_comm_log_button.pack(side=tk.LEFT, padx=5)
self.clear_cyclic_log_button = ttk.Button(controls_frame, text="Limpiar Log Cíclico", command=self.clear_cyclic_log)
self.clear_cyclic_log_button.pack(side=tk.LEFT, padx=5)
self.clear_graph_button = ttk.Button(controls_frame, text="Limpiar Gráfico", command=self.clear_graph)
self.clear_graph_button.pack(side=tk.LEFT, padx=5)
# Display Frame
display_frame = ttk.LabelFrame(self.frame, text="Valores Actuales")
display_frame.grid(row=1, column=1, padx=10, pady=5, sticky="nsew")
ttk.Label(display_frame, text="Brix:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
# self.current_brix_var inicializada en __init__
ttk.Label(display_frame, textvariable=self.current_brix_var,
font=("Courier", 14, "bold")).grid(row=0, column=1, padx=5, pady=5, sticky="w")
ttk.Label(display_frame, text="mA:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
# self.current_ma_var inicializada en __init__
ttk.Label(display_frame, textvariable=self.current_ma_var,
font=("Courier", 14, "bold")).grid(row=1, column=1, padx=5, pady=5, sticky="w")
ttk.Label(display_frame, text="Voltaje:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
# self.current_voltage_var inicializada en __init__
ttk.Label(display_frame, textvariable=self.current_voltage_var,
font=("Courier", 14, "bold")).grid(row=2, column=1, padx=5, pady=5, sticky="w")
# Event Log Frame (antes era el log principal)
event_log_frame = ttk.LabelFrame(self.frame, text="Log de Eventos")
event_log_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
self.event_log_text = scrolledtext.ScrolledText(event_log_frame, height=8, width=70, wrap=tk.WORD, state=tk.DISABLED)
self.event_log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
# --- Frame para Comunicación y Gráfico ---
comm_and_graph_parent_frame = ttk.Frame(self.frame)
comm_and_graph_parent_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
comm_and_graph_parent_frame.columnconfigure(0, weight=1) # Comm log
comm_and_graph_parent_frame.columnconfigure(1, weight=1) # Graph
comm_and_graph_parent_frame.rowconfigure(0, weight=1) # Ambos toman la altura completa de esta fila
comm_and_graph_parent_frame.columnconfigure(0, weight=1)
comm_and_graph_parent_frame.columnconfigure(1, weight=1)
comm_and_graph_parent_frame.rowconfigure(0, weight=1)
# Comm Log Frame (Nuevo)
comm_log_frame = ttk.LabelFrame(comm_and_graph_parent_frame, text="Log de Comunicación (Simulador)")
comm_log_frame.grid(row=0, column=0, padx=(0, 5), pady=0, sticky="nsew")
comm_log_frame.rowconfigure(0, weight=1)
comm_log_frame.columnconfigure(0, weight=1)
cyclic_log_frame = ttk.LabelFrame(comm_and_graph_parent_frame, text="Log Cíclico (Simulador)")
cyclic_log_frame.grid(row=0, column=0, padx=(0, 5), pady=0, sticky="nsew")
cyclic_log_frame.rowconfigure(0, weight=1)
cyclic_log_frame.columnconfigure(0, weight=1)
self.comm_log_text = scrolledtext.ScrolledText(comm_log_frame, height=10, width=70, wrap=tk.WORD, state=tk.DISABLED)
self.comm_log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
self.cyclic_log_text = scrolledtext.ScrolledText(cyclic_log_frame, height=10, width=70, wrap=tk.WORD, state=tk.DISABLED)
self.cyclic_log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
# Graph Frame Container (el gráfico se insertará aquí por MaselliApp)
# self.get_graph_frame() ahora devuelve este contenedor.
self.actual_graph_frame_container = ttk.LabelFrame(comm_and_graph_parent_frame, text="Gráfico Simulador")
self.actual_graph_frame_container.grid(row=0, column=1, padx=(5, 0), pady=0, sticky="nsew")
# --- Frame para Simulación de Errores ---
self._setup_error_simulation_ui() # Se añade al final de create_widgets
self._setup_error_simulation_ui()
# Configurar pesos
self.frame.columnconfigure(0, weight=1)
self.frame.columnconfigure(1, weight=1)
self.frame.rowconfigure(2, weight=1) # Event log frame
self.frame.rowconfigure(3, weight=3) # Comm and Graph parent frame (más altura)
self.frame.rowconfigure(4, weight=0) # Error frame no se expande tanto
self.frame.rowconfigure(2, weight=1)
self.frame.rowconfigure(3, weight=3)
self.frame.rowconfigure(4, weight=0)
# Inicializar estado
self.on_function_type_change()
def get_graph_frame(self):
"""Retorna el frame contenedor donde se debe dibujar el gráfico del simulador."""
return self.actual_graph_frame_container
def _setup_error_simulation_ui(self):
"""Crea los controles para la simulación de errores."""
error_frame = ttk.LabelFrame(self.frame, text="Simulación de Errores (Modo TCP Server)")
error_frame.grid(row=4, column=0, columnspan=2, padx=10, pady=10, sticky="ew")
self.frame.rowconfigure(4, weight=0) # Error frame no se expande tanto como el log o gráfico
ttk.Label(error_frame, text="Tipo de Error:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
self.error_type_var = tk.StringVar(value="Ninguno")
self.error_type_combo = ttk.Combobox(
error_frame,
textvariable=self.error_type_var,
state="disabled", # Se habilita/deshabilita dinámicamente
state="disabled",
values=[
"Ninguno", # Para enviar una trama normal desde este control
"Ninguno",
"ID Erróneo",
"Valor Fuera de Escala (mA)",
"Checksum Erróneo",
@ -239,74 +207,58 @@ class SimulatorTab:
self.random_error_var = tk.BooleanVar(value=False)
self.random_error_check = ttk.Checkbutton(
error_frame,
text="Errores Aleatorios (cada ~10s)",
text="Errores Aleatorios",
variable=self.random_error_var,
command=self.toggle_random_errors,
state="disabled" # Se habilita/deshabilita dinámicamente
state="disabled"
)
self.random_error_check.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w")
self.random_error_check.grid(row=1, column=0, columnspan=1, padx=5, pady=5, sticky="w")
# Checkbox para reemplazar trama normal con error (ahora en su propia fila para claridad)
self.replace_with_error_check = ttk.Checkbutton(
error_frame,
text="Reemplazar trama normal con error",
variable=self.replace_normal_with_error_var,
state="disabled" # Se habilita/deshabilita dinámicamente
state="disabled"
)
self.replace_with_error_check.grid(row=1, column=2, padx=(10,5), pady=5, sticky="w")
ttk.Label(error_frame, text="Intervalo Errores Aleatorios (s):").grid(row=2, column=0, padx=5, pady=5, sticky="w")
ttk.Label(error_frame, text="Intervalo Err. Aleat. (s):").grid(row=2, column=0, padx=5, pady=5, sticky="w")
self.random_error_interval_entry = ttk.Entry(
error_frame,
textvariable=self.random_error_interval_var,
width=8, # El Entry solo necesita el parent, textvariable, width y state.
state="disabled" # Se habilita/deshabilita dinámicamente
width=8,
state="disabled"
)
self.random_error_interval_entry.grid(row=2, column=1, padx=5, pady=5, sticky="ew") # Añadir el grid para el Entry
# El grid para self.replace_with_error_check ya está definido donde se crea ese widget.
self.random_error_interval_entry.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
error_frame.columnconfigure(1, weight=1)
self.update_error_controls_state() # Establecer estado inicial
self.update_error_controls_state()
def update_error_controls_state(self):
"""Habilita o deshabilita los controles de error según el modo de conexión."""
# Asegurarse de que los widgets de error existan antes de intentar configurarlos
if not hasattr(self, 'error_type_combo'):
return
is_tcp_server_mode = self.shared_config['connection_type_var'].get() == "TCP-Server"
# Considerar si la simulación (conexión) está activa para habilitar el envío
# is_connection_active = self.simulating # O una propiedad más directa de ConnectionManager
# Los controles de error solo tienen sentido si estamos en modo TCP-Server
# y la conexión está activa (es decir, la simulación principal está corriendo o
# el servidor está escuchando de alguna forma).
# Por ahora, lo basaremos en is_tcp_server_mode y self.simulating
enable_controls = is_tcp_server_mode and self.simulating
new_state_tk = tk.NORMAL if enable_controls else tk.DISABLED
new_state_str = "normal" if enable_controls else "disabled" # Para Checkbutton
new_state_str = "normal" if enable_controls else "disabled"
self.error_type_combo.config(state=new_state_tk if is_tcp_server_mode else tk.DISABLED) # Combo siempre según modo
self.error_type_combo.config(state=new_state_tk if is_tcp_server_mode else tk.DISABLED)
self.send_error_button.config(state=new_state_tk)
self.random_error_check.config(state=new_state_str)
# El entry del intervalo de errores aleatorios depende de que el check de errores aleatorios esté activo
interval_entry_state_tk = tk.NORMAL if enable_controls and self.random_error_var.get() else tk.DISABLED
self.random_error_interval_entry.config(state=interval_entry_state_tk)
# El check de "Reemplazar trama normal" se habilita si los controles de error están habilitados
self.replace_with_error_check.config(state=new_state_str)
if not enable_controls and self.random_error_var.get():
self.random_error_var.set(False)
self.toggle_random_errors() # Detiene el timer si estaba activo y se deshabilitan controles
self.toggle_random_errors()
def get_current_error_sim_parameters(self):
"""Obtiene parámetros para la simulación de errores (dirección ADAM, valor mA base)."""
adam_address = self.adam_address_var.get()
base_ma_value = 12.345 # Valor por defecto
base_ma_value = 12.345
if self.function_type_var.get() == "Manual":
try:
@ -315,57 +267,42 @@ class SimulatorTab:
if input_type == "Brix":
min_b = float(self.shared_config['min_brix_map_var'].get())
max_b = float(self.shared_config['max_brix_map_var'].get())
base_ma_value = ProtocolHandler.scale_to_ma(manual_val, min_b, max_b)
elif input_type == "mA": # noqa: E721
base_ma_value = ProtocolHandler.scale_to_ma(manual_val, min_b, max_b) # noqa: E701
elif input_type == "mA":
base_ma_value = manual_val
elif input_type == "Voltaje":
base_ma_value = ProtocolHandler.voltage_to_ma(manual_val)
except (ValueError, KeyError, TypeError):
Utils.log_message(self.log_text, "Error Sim: Usando valor mA base por defecto para error.")
else: # Si no es manual, o para tener un valor si la simulación principal no corre
# Podríamos tomar el self.current_ma_var si la simulación está corriendo
# pero para simplicidad, un valor fijo si no es manual.
pass # Mantiene 12.345
Utils.log_message(self.event_log_text, "Error Sim: Usando valor mA base por defecto para error.")
return adam_address, base_ma_value
def on_function_type_change(self, event=None):
"""Maneja el cambio de tipo de función"""
func_type = self.function_type_var.get()
is_manual_mode = (func_type == "Manual")
# Si la simulación está corriendo y el tipo de función cambia, detenerla.
if self.simulating:
self.stop_simulation()
# Configurar controles de entrada manual
manual_specific_state = tk.NORMAL if is_manual_mode else tk.DISABLED
self.manual_input_type_combo.config(state=manual_specific_state)
self.manual_value_entry.config(state=manual_specific_state)
self.manual_slider.config(state=manual_specific_state)
# Tiempo de ciclo y muestras por ciclo ahora están habilitados para todos los modos continuos
self.cycle_time_entry.config(state=tk.NORMAL)
self.samples_per_cycle_entry.config(state=tk.NORMAL)
if is_manual_mode:
self.on_manual_input_type_change() # Actualizar rangos de slider/entry y valor actual
self.on_manual_input_type_change()
# El estado de los botones Start/Stop depende de si la simulación está (o estaba) corriendo.
# Como stop_simulation() se llama arriba si estaba corriendo, self.simulating debería ser False aquí.
if not self.simulating:
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
else:
# Este estado idealmente no se alcanzaría si stop_simulation()
# establece correctamente self.simulating a False y actualiza los botones.
# Sin embargo, como salvaguarda:
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.update_error_controls_state() # Actualizar estado de controles de error
self.update_error_controls_state()
def on_manual_input_type_change(self, event=None):
"""Maneja el cambio de tipo de entrada manual (Brix, mA, Voltaje)"""
input_type = self.manual_input_type_var.get()
min_val, max_val, default_val, label_text, precision = 0, 100, 10.0, "Valor Brix:", 2
@ -373,14 +310,14 @@ class SimulatorTab:
try:
min_val = float(self.shared_config['min_brix_map_var'].get())
max_val = float(self.shared_config['max_brix_map_var'].get())
if min_val >= max_val: min_val, max_val = 0.0, 80.0 # Fallback
if min_val >= max_val: min_val, max_val = 0.0, 80.0
default_val = min_val + (max_val - min_val) / 4
except (ValueError, KeyError, TypeError):
min_val, max_val = 0.0, 80.0
default_val = 10.0 # noqa: F841
default_val = 10.0
label_text = "Valor Brix:"
precision = 2
elif input_type == "mA": # noqa: E721
elif input_type == "mA":
min_val, max_val = 0.0, 20.0
default_val = 12.0
label_text = "Valor mA:"
@ -407,17 +344,15 @@ class SimulatorTab:
self.manual_slider_var.set(default_val)
def on_slider_change(self, value_str):
"""Actualiza el valor del entry cuando cambia el slider"""
value = float(value_str)
input_type = self.manual_input_type_var.get()
precision = 2
if input_type == "Brix": precision = 2 # noqa: E701
if input_type == "Brix": precision = 2
elif input_type == "mA": precision = 3
elif input_type == "Voltaje": precision = 2
self.manual_value_var.set(f"{value:.{precision}f}")
def update_slider_from_entry(self):
"""Actualiza el slider cuando cambia el entry"""
try:
value = float(self.manual_value_var.get())
input_type = self.manual_input_type_var.get()
@ -428,21 +363,19 @@ class SimulatorTab:
max_val = float(self.shared_config['max_brix_map_var'].get())
if min_val >= max_val: min_val, max_val = 0.0, 80.0
precision = 2
elif input_type == "mA": min_val, max_val, precision = 0.0, 20.0, 3 # noqa: E701
elif input_type == "mA": min_val, max_val, precision = 0.0, 20.0, 3
elif input_type == "Voltaje": min_val, max_val, precision = 0.0, 10.0, 2
value = max(min_val, min(max_val, value)) # Clampear al rango
value = max(min_val, min(max_val, value))
self.manual_slider_var.set(value)
self.manual_value_var.set(f"{value:.{precision}f}")
except (ValueError, KeyError, TypeError):
# Si el valor no es un número o shared_config no está listo, resetear al valor del slider
current_slider_val = self.manual_slider_var.get()
precision_fallback = 2
if self.manual_input_type_var.get() == "mA": precision_fallback = 3
self.manual_value_var.set(f"{current_slider_val:.{precision_fallback}f}")
def start_simulation(self):
"""Inicia la simulación continua"""
if self.simulating:
messagebox.showwarning("Advertencia", "La simulación ya está en curso.")
return
@ -452,21 +385,16 @@ class SimulatorTab:
if len(adam_address) != 2:
messagebox.showerror("Error", "La dirección ADAM debe tener 2 caracteres.")
return
cycle_time = float(self.cycle_time_var.get())
if cycle_time <= 0:
messagebox.showerror("Error", "El tiempo de ciclo debe ser mayor que 0.")
return
samples_per_cycle = int(self.samples_per_cycle_var.get())
if samples_per_cycle <= 0:
messagebox.showerror("Error", "Las muestras por ciclo deben ser mayor que 0.")
return
# Validar mapeo Brix
float(self.shared_config['min_brix_map_var'].get())
float(self.shared_config['max_brix_map_var'].get())
except (ValueError, KeyError, TypeError):
messagebox.showerror("Error", "Valores inválidos en la configuración (ADAM, ciclo, muestras o mapeo Brix).")
return
@ -481,44 +409,39 @@ class SimulatorTab:
}
conn_type = current_config_values['connection_type']
conn_params = self.shared_config['config_manager'].get_connection_params(current_config_values)
# 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.comm_log_text, f"{listening_details} para simulación.")
elif conn_type != "TCP-Server": # Para otros tipos, el mensaje genérico
Utils.log_message(self.comm_log_text, f"Conexión {conn_type} abierta para simulación.")
Utils.log_message(self.event_log_text, f"{listening_details} para simulación.")
elif conn_type != "TCP-Server":
Utils.log_message(self.event_log_text, f"Conexión {conn_type} abierta para simulación.")
except Exception as e:
messagebox.showerror("Error de Conexión", str(e))
return
self.simulating = True
self.simulation_step = 0
self.start_time = time.time() # Reset start time for graph
self.start_time = time.time()
self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self._set_entries_state(tk.DISABLED)
self.update_error_controls_state() # Habilitar controles de error si es TCP Server
self.update_error_controls_state()
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() # noqa: E701
self.simulation_thread.start()
Utils.log_message(self.event_log_text, "Simulación iniciada.")
def stop_simulation(self):
"""Detiene la simulación"""
if not self.simulating:
return
self.simulating = False
# Detener el timer de errores aleatorios primero
if self.random_error_timer and self.random_error_timer.is_alive():
self.random_error_timer_stop_event.set()
self.random_error_timer.join(timeout=1.0) # Esperar un poco
self.random_error_timer.join(timeout=1.0)
self.random_error_timer = None
self.next_frame_is_error_event.clear()
self.error_details_for_replacement = None
@ -527,11 +450,11 @@ class SimulatorTab:
self.simulation_thread.join(timeout=2.0)
self.connection_manager.close_connection()
Utils.log_message(self.comm_log_text, "Conexión cerrada.")
Utils.log_message(self.event_log_text, "Conexión cerrada.")
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.on_function_type_change()
if self.connection_manager.connection_type == "TCP-Server":
self.shared_config['client_connected_var'].set("Ninguno")
Utils.log_message(self.event_log_text, "Simulación detenida.")
@ -539,12 +462,11 @@ class SimulatorTab:
self.current_ma_var.set("--.-- mA")
self.current_voltage_var.set("-.-- V")
self.start_button.config(state=tk.NORMAL) # Mover después de _set_entries_state y on_function_type_change
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self.update_error_controls_state() # Deshabilitar controles de error
self.update_error_controls_state()
def run_simulation(self):
"""Thread principal de simulación"""
try:
adam_address = self.adam_address_var.get()
min_brix_map = float(self.shared_config['min_brix_map_var'].get())
@ -552,9 +474,8 @@ 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
conn_type = self.connection_manager.connection_type
# 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(),
@ -562,23 +483,15 @@ class SimulatorTab:
'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:
message_to_send = None
ma_value_for_message_generation = 0.0 # mA que se usaría para generar la trama (normal o base para error)
# --- Determinar valores base de la simulación para este ciclo (Brix, mA) ---
# Esta lógica calcula los valores que se mostrarían y graficarían,
# y que se usarían para generar una trama normal.
target_brix = 0.0 # Brix consistente con target_ma para display/graph
# target_ma es el valor de mA que se usaría para generar el mensaje ADAM si fuera normal
# o el valor base si un error lo reemplaza.
current_manual_input_type = self.manual_input_type_var.get() # Cache para este ciclo
ma_value_for_message_generation = 0.0
target_brix = 0.0
current_manual_input_type = self.manual_input_type_var.get()
if function_type == "Manual": # Lógica para modo Manual
if function_type == "Manual":
manual_input_type = self.manual_input_type_var.get()
manual_numeric_value = 0.0
try:
@ -599,14 +512,12 @@ class SimulatorTab:
voltage_input = manual_numeric_value
ma_value_for_message_generation = ProtocolHandler.voltage_to_ma(voltage_input)
target_brix = ProtocolHandler.ma_to_brix(ma_value_for_message_generation, min_brix_map, max_brix_map)
elif function_type == "Lineal":
cycle_progress = (self.simulation_step % (2 * samples_per_cycle)) / samples_per_cycle
if cycle_progress > 1.0:
cycle_progress = 2.0 - cycle_progress
target_brix = min_brix_map + (max_brix_map - min_brix_map) * cycle_progress
ma_value_for_message_generation = ProtocolHandler.scale_to_ma(target_brix, min_brix_map, max_brix_map)
elif function_type == "Sinusoidal":
progress = (self.simulation_step % samples_per_cycle) / samples_per_cycle
phase = progress * 2 * math.pi
@ -614,34 +525,26 @@ class SimulatorTab:
target_brix = min_brix_map + (max_brix_map - min_brix_map) * sin_val
ma_value_for_message_generation = ProtocolHandler.scale_to_ma(target_brix, min_brix_map, max_brix_map)
# ma_value_in_message es el valor de mA que realmente se usaría en la trama o que se mostraría
# Si la trama es reemplazada por un error, este valor sigue siendo el de la simulación normal
# para la UI, pero la trama enviada será diferente.
ma_value_for_ui_display = ma_value_for_message_generation
voltage_value_display = ProtocolHandler.ma_to_voltage(ma_value_for_ui_display)
# --- Preparar la trama a enviar (normal o error de reemplazo) ---
log_prefix_for_send = "Enviando"
log_suffix_for_send = ""
actual_error_type_sent = "Normal" # Para el log
actual_error_type_sent = "Normal"
if self.next_frame_is_error_event.is_set() and \
self.error_details_for_replacement is not None and \
self.replace_normal_with_error_var.get():
error_msg_bytes, error_log_suffix, error_type_str = self.error_details_for_replacement
message_to_send = error_msg_bytes
log_prefix_for_send = "Error Sim (Reemplazo Programado)"
log_suffix_for_send = error_log_suffix
actual_error_type_sent = error_type_str
self.next_frame_is_error_event.clear()
self.error_details_for_replacement = None
else:
# Generar trama normal
message_to_send, _ = ProtocolHandler.create_adam_message_from_ma(adam_address, ma_value_for_message_generation)
# Preparar texto para display
brix_display_text = ""
if ma_value_for_ui_display < 4.0 and function_type == "Manual" and \
(current_manual_input_type == "mA" or current_manual_input_type == "Voltaje"):
@ -649,25 +552,21 @@ class SimulatorTab:
else:
brix_display_text = Utils.format_brix_display(target_brix)
# Actualizar GUI (StringVars son thread-safe para .set())
self.current_brix_var.set(brix_display_text)
self.current_ma_var.set(Utils.format_ma_display(ma_value_for_ui_display))
self.current_voltage_var.set(ProtocolHandler.format_voltage_display(voltage_value_display))
# Agregar punto de datos al gráfico (desde el thread GUI)
self.frame.after(0, lambda b=target_brix, m=ma_value_for_ui_display: self.add_data_point(b, m))
# --- Enviar la trama (normal o de error) ---
if message_to_send: # Si hay algo que enviar (no es "Trama Faltante" de reemplazo)
if message_to_send:
try:
if conn_type == "TCP-Server":
if not self.connection_manager.is_client_connected():
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.comm_log_text, f"TCP Server: Esperando cliente en puerto {port_to_log}...")
Utils.log_message(self.event_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):
Utils.log_message(self.comm_log_text, f"TCP Server: Cliente conectado desde {self.connection_manager.client_address}")
Utils.log_message(self.event_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
@ -677,52 +576,49 @@ class SimulatorTab:
log_content = ProtocolHandler.format_for_display(message_to_send, hex_non_printable=True)
if actual_error_type_sent != "Normal" and log_prefix_for_send.startswith("Error Sim (Reemplazo Programado)"):
Utils.log_message(self.comm_log_text, f"{log_prefix_for_send}: Trama '{actual_error_type_sent}'{log_suffix_for_send} -> {log_content}")
Utils.log_message(self.cyclic_log_text, f"{log_prefix_for_send}: Trama '{actual_error_type_sent}'{log_suffix_for_send} -> {log_content}")
else:
Utils.log_message(self.comm_log_text, f"{log_prefix_for_send}: {log_content}")
Utils.log_message(self.cyclic_log_text, f"{log_prefix_for_send}: {log_content}")
self.connection_manager.send_data(message_to_send)
if conn_type != "TCP-Server": # No leer respuesta en modo servidor
if conn_type != "TCP-Server":
response = self.connection_manager.read_response(timeout=0.1)
if response and response.strip():
Utils.log_message(self.comm_log_text, f"Respuesta: {ProtocolHandler.format_for_display(response)}")
Utils.log_message(self.cyclic_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.comm_log_text,
Utils.log_message(self.cyclic_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.comm_log_text, "TCP Server: Cliente desconectado. Esperando nueva conexión.")
Utils.log_message(self.event_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
except Exception as e:
Utils.log_message(self.comm_log_text, f"Error en comunicación ({conn_type}): {e}")
Utils.log_message(self.event_log_text, f"Error en comunicación ({conn_type}): {e}")
self.frame.after(0, self.stop_simulation_error)
break
elif actual_error_type_sent == "Trama Faltante (Omitir Envío)" and log_prefix_for_send.startswith("Error Sim (Reemplazo Programado)"):
# Loguear que se omitió una trama debido al reemplazo por "Trama Faltante"
Utils.log_message(self.comm_log_text, f"{log_prefix_for_send}: Simulación de '{actual_error_type_sent}'{log_suffix_for_send}. No se envió trama.")
Utils.log_message(self.cyclic_log_text, f"{log_prefix_for_send}: Simulación de '{actual_error_type_sent}'{log_suffix_for_send}. No se envió trama.")
self.simulation_step += 1
time.sleep(sample_period)
except Exception as e: # Catches errors in parameter fetching or main loop logic
except Exception as e:
Utils.log_message(self.event_log_text, f"Error en simulación: {e}")
if self.simulating: # Ensure stop is called only if an error occurs while simulating
if self.simulating:
self.frame.after(0, self.stop_simulation_error)
def stop_simulation_error(self):
"""Detiene la simulación debido a un error y muestra mensaje"""
if self.simulating: # Solo actuar si la simulación estaba activa
if self.simulating:
messagebox.showerror("Error de Simulación", "Error durante la simulación. Simulación detenida.")
self.stop_simulation() # Llama al método normal de parada
self.stop_simulation()
def generate_erroneous_message_logic(self, error_type, adam_address, base_ma_value):
"""Genera la trama (bytes) según el tipo de error."""
message_bytes = None
log_message_suffix = ""
@ -740,14 +636,14 @@ class SimulatorTab:
elif error_type == "Longitud Errónea (Aleatoria)":
base_msg_bytes, _ = ProtocolHandler.create_adam_message_from_ma(adam_address, base_ma_value)
if len(base_msg_bytes) > 1:
if random.choice([True, False]): # Acortar
if random.choice([True, False]):
cut_len = random.randint(1, max(1, len(base_msg_bytes) // 2))
message_bytes = base_msg_bytes[:-cut_len]
log_message_suffix = f" (longitud acortada en {cut_len} bytes)"
else: # Alargar
add_len = random.randint(1, 5) # Aumentado un poco el largo posible
else:
add_len = random.randint(1, 5)
garbage = bytes([random.randint(32, 126) for _ in range(add_len)])
message_bytes = base_msg_bytes + garbage # Podría ser al final o en medio
message_bytes = base_msg_bytes + garbage
log_message_suffix = f" (longitud aumentada en {add_len} bytes)"
else:
message_bytes, _ = ProtocolHandler.create_adam_message_with_bad_checksum(adam_address, base_ma_value)
@ -755,65 +651,54 @@ class SimulatorTab:
elif error_type == "Trama Faltante (Omitir Envío)":
log_message_suffix = " (trama omitida)"
return None, log_message_suffix
elif error_type == "Ninguno": # Enviar trama normal
elif error_type == "Ninguno":
message_bytes, _ = ProtocolHandler.create_adam_message_from_ma(adam_address, base_ma_value)
log_message_suffix = " (trama normal)"
else:
Utils.log_message(self.comm_log_text, f"Error Sim: Tipo de error '{error_type}' desconocido.")
Utils.log_message(self.event_log_text, f"Error Sim: Tipo de error '{error_type}' desconocido.")
return None, f" (tipo de error '{error_type}' desconocido)"
return message_bytes, log_message_suffix
def send_selected_error_manually(self):
"""Manejador del botón 'Enviar Trama Errónea'."""
if not (self.shared_config['connection_type_var'].get() == "TCP-Server" and self.simulating):
messagebox.showwarning("No Activo", "La simulación de errores manuales requiere modo TCP-Server y simulación activa.")
return
if not self.connection_manager.is_client_connected():
Utils.log_message(self.comm_log_text, "Error Sim: No hay cliente conectado para enviar trama errónea.")
# messagebox.showinfo("Sin Cliente", "No hay cliente conectado para enviar la trama errónea.")
# return # Permitir enviar aunque no haya cliente, el log lo indicará
Utils.log_message(self.event_log_text, "Error Sim: No hay cliente conectado para enviar trama errónea.")
error_type = self.error_type_var.get()
adam_address, base_ma_value = self.get_current_error_sim_parameters()
message_bytes, log_suffix_from_gen = self.generate_erroneous_message_logic(error_type, adam_address, base_ma_value)
if self.replace_normal_with_error_var.get():
# Programar para reemplazo en el siguiente ciclo de simulación
self.error_details_for_replacement = (message_bytes, log_suffix_from_gen, error_type)
self.next_frame_is_error_event.set()
if error_type == "Trama Faltante (Omitir Envío)":
Utils.log_message(self.comm_log_text, f"Error Sim Manual: Programada OMISIÓN de trama '{error_type}'{log_suffix_from_gen} para reemplazo.")
Utils.log_message(self.event_log_text, f"Error Sim Manual: Programada OMISIÓN de trama '{error_type}'{log_suffix_from_gen} para reemplazo.")
elif message_bytes:
Utils.log_message(self.comm_log_text, f"Error Sim Manual: Programada trama '{error_type}'{log_suffix_from_gen} para reemplazo.")
else: # Error en generación o tipo desconocido
Utils.log_message(self.comm_log_text, f"Error Sim Manual: No se pudo programar trama '{error_type}'{log_suffix_from_gen} para reemplazo.")
Utils.log_message(self.event_log_text, f"Error Sim Manual: Programada trama '{error_type}'{log_suffix_from_gen} para reemplazo.")
else:
Utils.log_message(self.event_log_text, f"Error Sim Manual: No se pudo programar trama '{error_type}'{log_suffix_from_gen} para reemplazo.")
else:
# Enviar inmediatamente como trama adicional
if message_bytes:
try:
self.connection_manager.send_data(message_bytes)
Utils.log_message(self.comm_log_text, f"Error Sim Manual (Adicional): Trama '{error_type}'{log_suffix_from_gen} -> {ProtocolHandler.format_for_display(message_bytes, hex_non_printable=True)}")
Utils.log_message(self.event_log_text, f"Error Sim Manual (Adicional): Trama '{error_type}'{log_suffix_from_gen} -> {ProtocolHandler.format_for_display(message_bytes, hex_non_printable=True)}")
except Exception as e:
Utils.log_message(self.comm_log_text, f"Error Sim Manual (Adicional): Fallo al enviar trama: {e}")
Utils.log_message(self.event_log_text, f"Error Sim Manual (Adicional): Fallo al enviar trama: {e}")
elif error_type == "Trama Faltante (Omitir Envío)":
Utils.log_message(self.comm_log_text, f"Error Sim Manual (Adicional): Simulación de '{error_type}'{log_suffix_from_gen}. No se envió trama adicional.")
# else: Ya logueado por generate_erroneous_message_logic si message_bytes es None y no es "Trama Faltante"
Utils.log_message(self.event_log_text, f"Error Sim Manual (Adicional): Simulación de '{error_type}'{log_suffix_from_gen}. No se envió trama adicional.")
def toggle_random_errors(self):
"""Activa o desactiva el envío de errores aleatorios."""
# self.random_error_var.get() refleja el nuevo estado del checkbox debido al clic del usuario
can_actually_start_random_errors = (self.shared_config['connection_type_var'].get() == "TCP-Server" and self.simulating)
if self.random_error_var.get(): # Si el usuario intenta activar los errores aleatorios
if self.random_error_var.get():
if not can_actually_start_random_errors:
Utils.log_message(self.event_log_text, "Error Sim: Errores aleatorios solo en TCP-Server con simulación activa.")
self.random_error_var.set(False) # Forzar a False ya que las condiciones no se cumplen
# El timer no se iniciará. update_error_controls_state() al final se encargará.
else: # Las condiciones se cumplen, iniciar el timer si no está ya activo
self.random_error_var.set(False)
else:
try:
interval_val = float(self.random_error_interval_var.get())
if interval_val <= 0:
@ -827,22 +712,18 @@ class SimulatorTab:
self.update_error_controls_state()
return
# Las condiciones se cumplen, iniciar el timer si no está ya activo
if self.random_error_timer is None or not self.random_error_timer.is_alive():
self.random_error_timer_stop_event.clear()
self.random_error_timer = threading.Thread(target=self._random_error_loop, args=(interval_val,), daemon=True)
self.random_error_timer.start()
else: # Si el usuario intenta desactivar los errores aleatorios (el checkbox ahora está desmarcado)
else:
if self.random_error_timer and self.random_error_timer.is_alive():
Utils.log_message(self.event_log_text, "Error Sim: Deteniendo envío de errores aleatorios.")
self.random_error_timer_stop_event.set()
# No es necesario join aquí, se hará en stop_simulation o al cerrar.
# Actualizar siempre el estado de los controles al final, basado en el estado final de self.random_error_var
self.update_error_controls_state()
def _random_error_loop(self, initial_interval_s):
"""Bucle del hilo que envía errores aleatorios."""
possible_error_types = [val for val in self.error_type_combo['values'] if val != "Ninguno"]
if not possible_error_types: return
@ -851,7 +732,7 @@ class SimulatorTab:
while not self.random_error_timer_stop_event.is_set():
if not (self.shared_config['connection_type_var'].get() == "TCP-Server" and self.simulating and self.connection_manager.is_client_connected()):
self.random_error_timer_stop_event.wait(1.0) # Esperar si no hay cliente o no está activo
self.random_error_timer_stop_event.wait(1.0)
continue
selected_random_error = random.choice(possible_error_types)
@ -859,36 +740,31 @@ class SimulatorTab:
message_bytes, log_suffix = self.generate_erroneous_message_logic(selected_random_error, adam_address, base_ma_value)
if self.replace_normal_with_error_var.get():
# Programar el error para que reemplace la siguiente trama normal
self.error_details_for_replacement = (message_bytes, log_suffix, selected_random_error)
self.next_frame_is_error_event.set()
# El log de este envío se hará en run_simulation cuando efectivamente se envíe/omita
Utils.log_message(self.comm_log_text, f"Error Sim Aleatorio: Programada trama '{selected_random_error}'{log_suffix} para reemplazo.")
Utils.log_message(self.cyclic_log_text, f"Error Sim Aleatorio: Programada trama '{selected_random_error}'{log_suffix} para reemplazo.")
else:
# Enviar el error inmediatamente, además de las tramas normales
if message_bytes:
try:
self.connection_manager.send_data(message_bytes)
Utils.log_message(self.comm_log_text, f"Error Sim Aleatorio (Adicional): Trama '{selected_random_error}'{log_suffix} -> {ProtocolHandler.format_for_display(message_bytes, hex_non_printable=True)}")
Utils.log_message(self.cyclic_log_text, f"Error Sim Aleatorio (Adicional): Trama '{selected_random_error}'{log_suffix} -> {ProtocolHandler.format_for_display(message_bytes, hex_non_printable=True)}")
except Exception as e:
Utils.log_message(self.comm_log_text, f"Error Sim Aleatorio (Adicional): Fallo al enviar: {e}")
Utils.log_message(self.cyclic_log_text, f"Error Sim Aleatorio (Adicional): Fallo al enviar: {e}")
elif selected_random_error == "Trama Faltante (Omitir Envío)":
Utils.log_message(self.comm_log_text, f"Error Sim Aleatorio (Adicional): Simulación de '{selected_random_error}'{log_suffix}. No se envió trama adicional.")
Utils.log_message(self.cyclic_log_text, f"Error Sim Aleatorio (Adicional): Simulación de '{selected_random_error}'{log_suffix}. No se envió trama adicional.")
# Permitir que el intervalo se actualice dinámicamente
try:
new_interval = float(self.random_error_interval_var.get())
if new_interval > 0 and new_interval != current_interval:
current_interval = new_interval
Utils.log_message(self.event_log_text, f"Error Sim: Intervalo de errores aleatorios actualizado a {current_interval:.2f}s.")
except ValueError:
pass # Mantener el intervalo actual si el nuevo valor es inválido
pass
self.random_error_timer_stop_event.wait(timeout=current_interval)
Utils.log_message(self.event_log_text, "Error Sim: Hilo de errores aleatorios detenido.")
def add_data_point(self, brix_value, ma_value):
"""Agrega un punto de datos al gráfico"""
current_time = time.time() - self.start_time
self.time_data.append(current_time)
self.brix_data.append(brix_value)
@ -898,37 +774,30 @@ class SimulatorTab:
self.graph_update_callback()
def clear_graph(self):
"""Limpia los datos del gráfico"""
Utils.clear_graph_data(self.time_data, self.brix_data, self.ma_data)
self.start_time = time.time() # noqa: F841
self.start_time = time.time()
Utils.log_message(self.event_log_text, "Gráfico del simulador limpiado.")
if hasattr(self, 'graph_update_callback'):
self.graph_update_callback()
def clear_comm_log(self):
"""Limpia el log de comunicación del simulador."""
Utils.clear_log_widget(self.comm_log_text)
Utils.log_message(self.event_log_text, "Log de comunicación del simulador limpiado.")
def clear_cyclic_log(self):
Utils.clear_log_widget(self.cyclic_log_text)
Utils.log_message(self.event_log_text, "Log cíclico del simulador limpiado.")
def _set_entries_state(self, state):
"""Habilita/deshabilita los controles durante la simulación"""
sim_specific_widgets = [
self.adam_address_entry,
self.function_type_combo,
self.cycle_time_entry,
self.samples_per_cycle_entry
]
# No deshabilitar controles de modo manual aquí, se manejan en on_function_type_change
Utils.set_widgets_state(sim_specific_widgets, state)
if 'shared_widgets' in self.shared_config:
Utils.set_widgets_state(self.shared_config['shared_widgets'], state)
# self.update_error_controls_state() # El estado de los controles de error depende también de self.simulating
def get_config(self):
"""Obtiene la configuración actual del simulador"""
return {
'adam_address': self.adam_address_var.get(),
'function_type': self.function_type_var.get(),
@ -940,7 +809,6 @@ class SimulatorTab:
}
def set_config(self, config):
"""Establece la configuración del simulador"""
self.adam_address_var.set(config.get('adam_address', '01'))
self.function_type_var.set(config.get('function_type', 'Lineal'))
self.cycle_time_var.set(config.get('cycle_time', '10.0'))
@ -953,14 +821,11 @@ class SimulatorTab:
try:
self.manual_slider_var.set(float(self.manual_value_var.get()))
except ValueError:
# Si el valor no es un float válido, intentar con un default o el valor del tipo
# Esto se manejará mejor en on_manual_input_type_change
pass
self.on_function_type_change() # Esto llamará a on_manual_input_type_change si es necesario
self.update_error_controls_state() # Actualizar estado de controles de error al cargar config
self.on_function_type_change()
self.update_error_controls_state()
def on_app_close(self):
"""Llamado cuando la aplicación se está cerrando para limpiar recursos."""
if self.simulating:
self.stop_simulation() # Asegura que todo se detenga y limpie correctamente
self.stop_simulation()