Mejorado de la simulacion de eventos con error

This commit is contained in:
Miguel 2025-05-30 23:01:16 +02:00
parent a28f02b4c9
commit 835bfc0383
7 changed files with 529 additions and 1426 deletions

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ class ConfigManager:
'cycle_time': '0.5', 'cycle_time': '0.5',
'manual_input_type': 'Brix', # Nuevo: 'Brix', 'mA', 'Voltaje' 'manual_input_type': 'Brix', # Nuevo: 'Brix', 'mA', 'Voltaje'
'manual_value': '10.0', # Nuevo: valor correspondiente al manual_input_type 'manual_value': '10.0', # Nuevo: valor correspondiente al manual_input_type
'random_error_interval': '10.0', # Intervalo para errores aleatorios en el simulador
# Configuración para NetCom # Configuración para NetCom
'netcom_com_port': 'COM3', 'netcom_com_port': 'COM3',
'netcom_baud_rate': '115200', 'netcom_baud_rate': '115200',
@ -135,6 +136,14 @@ class ConfigManager:
except ValueError: except ValueError:
errors.append("El valor manual debe ser un número válido") errors.append("El valor manual debe ser un número válido")
# Validar intervalo de errores aleatorios del simulador
try:
random_error_interval = float(config.get('random_error_interval', '10.0'))
if random_error_interval <= 0:
errors.append("El intervalo para errores aleatorios debe ser un número positivo.")
except ValueError:
errors.append("El intervalo para errores aleatorios debe ser un número válido.")
# Validar puerto serie # Validar puerto serie
if config.get('connection_type') == 'Serial': if config.get('connection_type') == 'Serial':
com_port = config.get('com_port', '') com_port = config.get('com_port', '')

View File

@ -45,10 +45,10 @@ class MaselliApp:
# Inicializar animaciones de gráficos # Inicializar animaciones de gráficos
self.sim_ani = animation.FuncAnimation( self.sim_ani = animation.FuncAnimation(
self.sim_fig, self.update_sim_graph, interval=100, blit=False self.sim_fig, self.update_sim_graph, interval=100, blit=False, cache_frame_data=False
) )
self.trace_ani = animation.FuncAnimation( self.trace_ani = animation.FuncAnimation(
self.trace_fig, self.update_trace_graph, interval=100, blit=False self.trace_fig, self.update_trace_graph, interval=100, blit=False, cache_frame_data=False
) )
def create_widgets(self): def create_widgets(self):
@ -419,4 +419,3 @@ class MaselliApp:
# Cerrar ventana # Cerrar ventana
self.root.destroy() self.root.destroy()

View File

@ -9,9 +9,10 @@
"adam_address": "01", "adam_address": "01",
"function_type": "Sinusoidal", "function_type": "Sinusoidal",
"cycle_time": "15", "cycle_time": "15",
"samples_per_cycle": "100", "samples_per_cycle": "70",
"manual_input_type": "Brix", "manual_input_type": "Brix",
"manual_value": "0.00", "manual_value": "0.00",
"random_error_interval": "2.0",
"netcom_com_port": "COM11", "netcom_com_port": "COM11",
"netcom_baud_rate": "9600", "netcom_baud_rate": "9600",
"netcom_rtscts": false, "netcom_rtscts": false,

View File

@ -1 +0,0 @@
Timestamp,mA,Brix,Raw_Message
1 Timestamp mA Brix Raw_Message

View File

@ -66,6 +66,32 @@ class ProtocolHandler:
checksum = ProtocolHandler.calculate_checksum(message_part) checksum = ProtocolHandler.calculate_checksum(message_part)
full_message_str = f"{message_part}{checksum}\r" full_message_str = f"{message_part}{checksum}\r"
return full_message_str.encode('ascii'), ma_value return full_message_str.encode('ascii'), ma_value
@staticmethod
def create_adam_message_with_bad_checksum(adam_address, ma_value):
"""
Crea un mensaje completo ADAM (como bytes) directamente desde un valor mA,
pero con un checksum deliberadamente incorrecto.
"""
ma_str = ProtocolHandler.format_ma_value(ma_value) # Formato XX.XXX
message_part = f"#{adam_address}{ma_str}"
correct_checksum = ProtocolHandler.calculate_checksum(message_part)
# Generar un checksum incorrecto.
bad_checksum_str = "XX" # Valor por defecto si algo falla
try:
correct_sum_val = int(correct_checksum, 16)
# Sumar 1 (o cualquier otro valor) y tomar módulo 256 para que siga siendo un byte.
# Asegurarse de que sea diferente al original.
bad_sum_val = (correct_sum_val + 1) % 256
if f"{bad_sum_val:02X}" == correct_checksum: # En caso de que correct_checksum fuera FF
bad_sum_val = (correct_sum_val + 2) % 256
bad_checksum_str = f"{bad_sum_val:02X}"
except ValueError: # Si correct_checksum no es un hexadecimal válido (no debería pasar)
pass # bad_checksum_str se queda como "XX"
full_message_str = f"{message_part}{bad_checksum_str}\r"
return full_message_str.encode('ascii'), ma_value
@staticmethod @staticmethod
def ma_to_voltage(ma_value): def ma_to_voltage(ma_value):

View File

@ -5,6 +5,7 @@ Tab del Simulador - Genera valores de prueba en protocolo ADAM
import tkinter as tk import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox from tkinter import ttk, scrolledtext, messagebox
import threading import threading
import random
import time import time
import math import math
from collections import deque from collections import deque
@ -41,6 +42,7 @@ class SimulatorTab:
self.cycle_time_var = tk.StringVar(value=initial_config.get('cycle_time', '10.0')) self.cycle_time_var = tk.StringVar(value=initial_config.get('cycle_time', '10.0'))
self.samples_per_cycle_var = tk.StringVar(value=initial_config.get('samples_per_cycle', '100')) self.samples_per_cycle_var = tk.StringVar(value=initial_config.get('samples_per_cycle', '100'))
# Configuración para modo manual y errores
self.manual_input_type_var = tk.StringVar(value=initial_config.get('manual_input_type', 'Brix')) self.manual_input_type_var = tk.StringVar(value=initial_config.get('manual_input_type', 'Brix'))
self.manual_value_var = tk.StringVar(value=initial_config.get('manual_value', '10.0')) self.manual_value_var = tk.StringVar(value=initial_config.get('manual_value', '10.0'))
@ -53,6 +55,14 @@ class SimulatorTab:
self.current_brix_var = tk.StringVar(value="---") self.current_brix_var = tk.StringVar(value="---")
self.current_ma_var = tk.StringVar(value="--.-- mA") 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") # Nueva para voltaje
# Para simulación de errores
self.random_error_timer = None
self.random_error_timer_stop_event = threading.Event()
self.replace_normal_with_error_var = tk.BooleanVar(value=False)
self.next_frame_is_error_event = threading.Event()
self.random_error_interval_var = tk.StringVar(value=initial_config.get('random_error_interval', '10.0'))
self.error_details_for_replacement = None # (message_bytes, log_suffix, error_type_str)
self.create_widgets() self.create_widgets()
@ -115,10 +125,6 @@ class SimulatorTab:
state=tk.DISABLED, length=200) state=tk.DISABLED, length=200)
self.manual_slider.grid(row=1, column=2, padx=5, pady=5, sticky="ew") self.manual_slider.grid(row=1, column=2, padx=5, pady=5, sticky="ew")
self.manual_send_button = ttk.Button(manual_frame, text="Enviar Manual",
command=self.send_manual_value, state=tk.DISABLED)
self.manual_send_button.grid(row=1, column=3, padx=5, pady=5, sticky="ew")
manual_frame.columnconfigure(2, weight=1) manual_frame.columnconfigure(2, weight=1)
# Controls Frame # Controls Frame
@ -160,11 +166,14 @@ class SimulatorTab:
self.log_text = scrolledtext.ScrolledText(log_frame, height=8, width=70, wrap=tk.WORD, state=tk.DISABLED) self.log_text = scrolledtext.ScrolledText(log_frame, height=8, width=70, wrap=tk.WORD, state=tk.DISABLED)
self.log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) self.log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
# --- Frame para Simulación de Errores ---
self._setup_error_simulation_ui() # Se añade al final de create_widgets
# Configurar pesos # Configurar pesos
self.frame.columnconfigure(0, weight=1) self.frame.columnconfigure(0, weight=1)
self.frame.columnconfigure(1, weight=1) self.frame.columnconfigure(1, weight=1)
self.frame.rowconfigure(2, weight=1) # Log frame self.frame.rowconfigure(2, weight=1) # Log frame
# Inicializar estado # Inicializar estado
self.on_function_type_change() self.on_function_type_change()
@ -172,38 +181,166 @@ class SimulatorTab:
"""Crea y retorna el frame para el gráfico""" """Crea y retorna el frame para el gráfico"""
graph_frame = ttk.LabelFrame(self.frame, text="Gráfico Simulador") graph_frame = ttk.LabelFrame(self.frame, text="Gráfico Simulador")
graph_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") graph_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
self.frame.rowconfigure(3, weight=1) # Graph frame # El rowconfigure para el gráfico se hace aquí, y el de errores abajo
self.frame.rowconfigure(3, weight=1) # Graph frame (se mueve una fila abajo)
return graph_frame return graph_frame
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
values=[
"Ninguno", # Para enviar una trama normal desde este control
"ID Erróneo",
"Valor Fuera de Escala (mA)",
"Checksum Erróneo",
"Longitud Errónea (Aleatoria)",
"Trama Faltante (Omitir Envío)"
]
)
self.error_type_combo.current(0)
self.error_type_combo.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
self.send_error_button = ttk.Button(error_frame, text="Enviar Trama Errónea",
command=self.send_selected_error_manually, state=tk.DISABLED)
self.send_error_button.grid(row=0, column=2, padx=5, pady=5)
self.random_error_var = tk.BooleanVar(value=False)
self.random_error_check = ttk.Checkbutton(
error_frame,
text="Errores Aleatorios (cada ~10s)",
variable=self.random_error_var,
command=self.toggle_random_errors,
state="disabled" # Se habilita/deshabilita dinámicamente
)
self.random_error_check.grid(row=1, column=0, columnspan=2, 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
)
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")
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
)
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.
error_frame.columnconfigure(1, weight=1)
self.update_error_controls_state() # Establecer estado inicial
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
self.error_type_combo.config(state=new_state_tk if is_tcp_server_mode else tk.DISABLED) # Combo siempre según modo
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
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
if self.function_type_var.get() == "Manual":
try:
manual_val = float(self.manual_value_var.get())
input_type = self.manual_input_type_var.get()
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":
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
return adam_address, base_ma_value
def on_function_type_change(self, event=None): def on_function_type_change(self, event=None):
"""Maneja el cambio de tipo de función""" """Maneja el cambio de tipo de función"""
func_type = self.function_type_var.get() func_type = self.function_type_var.get()
if func_type == "Manual": is_manual_mode = (func_type == "Manual")
if self.simulating:
self.stop_simulation() # Si la simulación está corriendo y el tipo de función cambia, detenerla.
if self.simulating:
self.manual_input_type_combo.config(state=tk.NORMAL) self.stop_simulation()
self.manual_value_entry.config(state=tk.NORMAL)
self.manual_send_button.config(state=tk.NORMAL) # Configurar controles de entrada manual
self.manual_slider.config(state=tk.NORMAL) manual_specific_state = tk.NORMAL if is_manual_mode else tk.DISABLED
self.manual_input_type_combo.config(state=manual_specific_state)
self.cycle_time_entry.config(state=tk.DISABLED) self.manual_value_entry.config(state=manual_specific_state)
self.samples_per_cycle_entry.config(state=tk.DISABLED) self.manual_slider.config(state=manual_specific_state)
self.start_button.config(state=tk.DISABLED)
# 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
# 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) self.stop_button.config(state=tk.DISABLED)
self.on_manual_input_type_change() # Configurar según el tipo actual
else: else:
self.manual_input_type_combo.config(state=tk.DISABLED) # Este estado idealmente no se alcanzaría si stop_simulation()
self.manual_value_entry.config(state=tk.DISABLED) # establece correctamente self.simulating a False y actualiza los botones.
self.manual_send_button.config(state=tk.DISABLED) # Sin embargo, como salvaguarda:
self.manual_slider.config(state=tk.DISABLED) self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL)
self.cycle_time_entry.config(state=tk.NORMAL) self.update_error_controls_state() # Actualizar estado de controles de error
self.samples_per_cycle_entry.config(state=tk.NORMAL)
if not self.simulating:
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
def on_manual_input_type_change(self, event=None): def on_manual_input_type_change(self, event=None):
"""Maneja el cambio de tipo de entrada manual (Brix, mA, Voltaje)""" """Maneja el cambio de tipo de entrada manual (Brix, mA, Voltaje)"""
input_type = self.manual_input_type_var.get() input_type = self.manual_input_type_var.get()
@ -280,127 +417,6 @@ class SimulatorTab:
precision_fallback = 2 precision_fallback = 2
if self.manual_input_type_var.get() == "mA": precision_fallback = 3 if self.manual_input_type_var.get() == "mA": precision_fallback = 3
self.manual_value_var.set(f"{current_slider_val:.{precision_fallback}f}") self.manual_value_var.set(f"{current_slider_val:.{precision_fallback}f}")
def send_manual_value(self):
"""Envía un valor manual único"""
try:
# Obtener valores de mapeo
min_brix_map = float(self.shared_config['min_brix_map_var'].get())
max_brix_map = float(self.shared_config['max_brix_map_var'].get())
adam_address = self.adam_address_var.get()
if len(adam_address) != 2:
messagebox.showerror("Error", "La dirección ADAM debe tener 2 caracteres.")
return
if min_brix_map >= max_brix_map:
messagebox.showerror("Error de Configuración", "Min Brix debe ser menor que Max Brix.")
return
input_type = self.manual_input_type_var.get()
manual_numeric_value = float(self.manual_value_var.get())
final_brix, final_ma, final_voltage = 0.0, 0.0, 0.0
if input_type == "Brix":
final_brix = manual_numeric_value
final_ma = ProtocolHandler.scale_to_ma(final_brix, min_brix_map, max_brix_map)
final_voltage = ProtocolHandler.ma_to_voltage(final_ma)
elif input_type == "mA":
final_ma = manual_numeric_value
final_brix = ProtocolHandler.ma_to_brix(final_ma, min_brix_map, max_brix_map)
final_voltage = ProtocolHandler.ma_to_voltage(final_ma)
elif input_type == "Voltaje":
final_voltage = manual_numeric_value
final_ma = ProtocolHandler.voltage_to_ma(final_voltage)
final_brix = ProtocolHandler.ma_to_brix(final_ma, min_brix_map, max_brix_map)
# Usar el nuevo método que toma ma_value directamente para el modo manual
message, returned_ma = ProtocolHandler.create_adam_message_from_ma(adam_address, final_ma)
# Actualizar display
brix_display_text = ""
# Si la entrada fue mA o Voltaje y el resultado es un mA < 4 (estado de error)
if (input_type == "mA" or input_type == "Voltaje") and final_ma < 4.0:
brix_display_text = "Error (Sub 4mA)"
else:
# Para entrada Brix, o mA/Voltaje que resultan en mA >= 4.0
brix_display_text = Utils.format_brix_display(final_brix)
self.current_brix_var.set(brix_display_text)
self.current_ma_var.set(Utils.format_ma_display(returned_ma)) # Usar returned_ma que es igual a final_ma
self.current_voltage_var.set(ProtocolHandler.format_voltage_display(final_voltage))
# Agregar al gráfico
# final_brix aquí es el valor numérico calculado (ej. min_brix_map si mA < 4),
# lo cual es consistente para el gráfico.
self.add_data_point(final_brix, returned_ma)
# Enviar por conexión temporal
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(),
}
conn_type = current_config_values['connection_type']
conn_params = self.shared_config['config_manager'].get_connection_params(current_config_values)
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
# 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.
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)}")
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): def start_simulation(self):
"""Inicia la simulación continua""" """Inicia la simulación continua"""
@ -461,6 +477,7 @@ class SimulatorTab:
self.start_button.config(state=tk.DISABLED) self.start_button.config(state=tk.DISABLED)
self.stop_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.NORMAL)
self._set_entries_state(tk.DISABLED) self._set_entries_state(tk.DISABLED)
self.update_error_controls_state() # Habilitar controles de error si es TCP Server
if conn_type == "TCP-Server": if conn_type == "TCP-Server":
self.shared_config['client_connected_var'].set("Esperando...") self.shared_config['client_connected_var'].set("Esperando...")
@ -475,14 +492,20 @@ class SimulatorTab:
self.simulating = False 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 = None
self.next_frame_is_error_event.clear()
self.error_details_for_replacement = None
if self.simulation_thread and self.simulation_thread.is_alive(): if self.simulation_thread and self.simulation_thread.is_alive():
self.simulation_thread.join(timeout=2.0) self.simulation_thread.join(timeout=2.0)
self.connection_manager.close_connection() self.connection_manager.close_connection()
Utils.log_message(self.log_text, "Conexión cerrada.") Utils.log_message(self.log_text, "Conexión cerrada.")
self.start_button.config(state=tk.NORMAL)
self.stop_button.config(state=tk.DISABLED)
self._set_entries_state(tk.NORMAL) self._set_entries_state(tk.NORMAL)
self.on_function_type_change() # Re-evaluar estado de controles manuales self.on_function_type_change() # Re-evaluar estado de controles manuales
if self.connection_manager.connection_type == "TCP-Server": # Limpiar info del cliente if self.connection_manager.connection_type == "TCP-Server": # Limpiar info del cliente
@ -492,6 +515,10 @@ class SimulatorTab:
self.current_brix_var.set("---") self.current_brix_var.set("---")
self.current_ma_var.set("--.-- mA") self.current_ma_var.set("--.-- mA")
self.current_voltage_var.set("-.-- V") 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.stop_button.config(state=tk.DISABLED)
self.update_error_controls_state() # Deshabilitar controles de error
def run_simulation(self): def run_simulation(self):
"""Thread principal de simulación""" """Thread principal de simulación"""
@ -516,70 +543,146 @@ class SimulatorTab:
sample_period = cycle_time / samples_per_cycle sample_period = cycle_time / samples_per_cycle
while self.simulating: while self.simulating:
current_brix = 0.0 message_to_send = None
progress = (self.simulation_step % samples_per_cycle) / samples_per_cycle ma_value_for_message_generation = 0.0 # mA que se usaría para generar la trama (normal o base para error)
if function_type == "Lineal": # --- 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
if function_type == "Manual": # Lógica para modo Manual
manual_input_type = self.manual_input_type_var.get()
manual_numeric_value = 0.0
try:
manual_numeric_value = float(self.manual_value_var.get())
except ValueError:
Utils.log_message(self.log_text, f"Valor manual inválido: '{self.manual_value_var.get()}'. Usando valor por defecto.")
if manual_input_type == "Brix": manual_numeric_value = min_brix_map
elif manual_input_type == "mA": manual_numeric_value = 4.0
elif manual_input_type == "Voltaje": manual_numeric_value = ProtocolHandler.ma_to_voltage(4.0)
if manual_input_type == "Brix":
target_brix = manual_numeric_value
ma_value_for_message_generation = ProtocolHandler.scale_to_ma(target_brix, min_brix_map, max_brix_map)
elif manual_input_type == "mA":
ma_value_for_message_generation = manual_numeric_value
target_brix = ProtocolHandler.ma_to_brix(ma_value_for_message_generation, min_brix_map, max_brix_map)
elif manual_input_type == "Voltaje":
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 cycle_progress = (self.simulation_step % (2 * samples_per_cycle)) / samples_per_cycle
if cycle_progress > 1.0: if cycle_progress > 1.0:
cycle_progress = 2.0 - cycle_progress cycle_progress = 2.0 - cycle_progress
current_brix = min_brix_map + (max_brix_map - min_brix_map) * 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": elif function_type == "Sinusoidal":
progress = (self.simulation_step % samples_per_cycle) / samples_per_cycle
phase = progress * 2 * math.pi phase = progress * 2 * math.pi
sin_val = (math.sin(phase) + 1) / 2 sin_val = (math.sin(phase) + 1) / 2
current_brix = min_brix_map + (max_brix_map - min_brix_map) * sin_val 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)
message, ma_value = ProtocolHandler.create_adam_message(adam_address, current_brix, min_brix_map, max_brix_map)
voltage_value = ProtocolHandler.ma_to_voltage(ma_value) # 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
self.current_brix_var.set(Utils.format_brix_display(current_brix)) # para la UI, pero la trama enviada será diferente.
self.current_ma_var.set(Utils.format_ma_display(ma_value)) ma_value_for_ui_display = ma_value_for_message_generation
self.current_voltage_var.set(ProtocolHandler.format_voltage_display(voltage_value)) voltage_value_display = ProtocolHandler.ma_to_voltage(ma_value_for_ui_display)
self.frame.after(0, lambda b=current_brix, m=ma_value: self.add_data_point(b, m)) # --- Preparar la trama a enviar (normal o error de reemplazo) ---
log_prefix_for_send = "Enviando"
try: log_suffix_for_send = ""
if conn_type == "TCP-Server": actual_error_type_sent = "Normal" # Para el log
if not self.connection_manager.is_client_connected():
# Loguear solo si el estado cambia o periódicamente para evitar spam if self.next_frame_is_error_event.is_set() and \
if not hasattr(self, '_waiting_for_client_logged') or not self._waiting_for_client_logged: self.error_details_for_replacement is not None and \
port_to_log = self.shared_config['config_manager'].get_connection_params(current_config_values)['port'] self.replace_normal_with_error_var.get():
Utils.log_message(self.log_text, f"TCP Server: Esperando cliente en puerto {port_to_log}...")
self._waiting_for_client_logged = True error_msg_bytes, error_log_suffix, error_type_str = self.error_details_for_replacement
message_to_send = error_msg_bytes
if self.connection_manager.accept_client(timeout=0.05): # Intento corto no bloqueante log_prefix_for_send = "Error Sim (Reemplazo Programado)"
Utils.log_message(self.log_text, f"TCP Server: Cliente conectado desde {self.connection_manager.client_address}") log_suffix_for_send = error_log_suffix
client_info = f"{self.connection_manager.client_address[0]}:{self.connection_manager.client_address[1]}" actual_error_type_sent = error_type_str
self.shared_config['client_connected_var'].set(client_info)
self._waiting_for_client_logged = False # Resetear flag de log self.next_frame_is_error_event.clear()
elif not self.connection_manager.is_client_connected() and \ self.error_details_for_replacement = None
self.shared_config['client_connected_var'].get() != "Esperando...": 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"):
brix_display_text = "Error (Sub 4mA)"
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)
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.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.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
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...")
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.log_text, f"{log_prefix_for_send}: Trama '{actual_error_type_sent}'{log_suffix_for_send} -> {log_content}")
else:
Utils.log_message(self.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
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.shared_config['client_connected_var'].set("Esperando...")
self._waiting_for_client_logged = False
Utils.log_message(self.log_text, f"Enviando: {ProtocolHandler.format_for_display(message)}") except Exception as e:
self.connection_manager.send_data(message) Utils.log_message(self.log_text, f"Error en comunicación ({conn_type}): {e}")
self.frame.after(0, self.stop_simulation_error)
if conn_type != "TCP-Server": # No leer respuesta en modo servidor break
response = self.connection_manager.read_response(timeout=0.1) elif actual_error_type_sent == "Trama Faltante (Omitir Envío)" and log_prefix_for_send.startswith("Error Sim (Reemplazo Programado)"):
if response and response.strip(): # Loguear que se omitió una trama debido al reemplazo por "Trama Faltante"
Utils.log_message(self.log_text, f"Respuesta: {ProtocolHandler.format_for_display(response)}") Utils.log_message(self.log_text, f"{log_prefix_for_send}: Simulación de '{actual_error_type_sent}'{log_suffix_for_send}. No se envió trama.")
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 ({conn_type}): {e}")
self.frame.after(0, self.stop_simulation_error) # Schedule GUI update from main thread
break
self.simulation_step += 1 self.simulation_step += 1
time.sleep(sample_period) time.sleep(sample_period)
@ -594,6 +697,172 @@ class SimulatorTab:
if self.simulating: # Solo actuar si la simulación estaba activa if self.simulating: # Solo actuar si la simulación estaba activa
messagebox.showerror("Error de Simulación", "Error durante la simulación. Simulación detenida.") 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() # Llama al método normal de parada
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 = ""
if error_type == "ID Erróneo":
wrong_adam_address = "99" if adam_address != "99" else "98"
message_bytes, _ = ProtocolHandler.create_adam_message_from_ma(wrong_adam_address, base_ma_value)
log_message_suffix = f" (ID cambiado a {wrong_adam_address})"
elif error_type == "Valor Fuera de Escala (mA)":
out_of_scale_ma = 2.500 if random.random() < 0.5 else 22.500
message_bytes, _ = ProtocolHandler.create_adam_message_from_ma(adam_address, out_of_scale_ma)
log_message_suffix = f" (valor mA: {out_of_scale_ma:.3f})"
elif error_type == "Checksum Erróneo":
message_bytes, _ = ProtocolHandler.create_adam_message_with_bad_checksum(adam_address, base_ma_value)
log_message_suffix = " (checksum incorrecto)"
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
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
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
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)
log_message_suffix = " (longitud errónea -> fallback a checksum incorrecto)"
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
message_bytes, _ = ProtocolHandler.create_adam_message_from_ma(adam_address, base_ma_value)
log_message_suffix = " (trama normal)"
else:
Utils.log_message(self.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.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á
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.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.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.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.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.log_text, f"Error Sim Manual (Adicional): Fallo al enviar trama: {e}")
elif error_type == "Trama Faltante (Omitir Envío)":
Utils.log_message(self.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"
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 not can_actually_start_random_errors:
Utils.log_message(self.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
try:
interval_val = float(self.random_error_interval_var.get())
if interval_val <= 0:
messagebox.showerror("Error de Intervalo", "El intervalo para errores aleatorios debe ser un número positivo.")
self.random_error_var.set(False)
self.update_error_controls_state()
return
except ValueError:
messagebox.showerror("Error de Intervalo", "Valor inválido para el intervalo de errores aleatorios.")
self.random_error_var.set(False)
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)
if self.random_error_timer and self.random_error_timer.is_alive():
Utils.log_message(self.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
current_interval = initial_interval_s
Utils.log_message(self.log_text, f"Error Sim: Hilo de errores aleatorios iniciado con intervalo {current_interval:.2f}s.")
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
continue
selected_random_error = random.choice(possible_error_types)
adam_address, base_ma_value = self.get_current_error_sim_parameters()
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.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.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.log_text, f"Error Sim Aleatorio (Adicional): Fallo al enviar: {e}")
elif selected_random_error == "Trama Faltante (Omitir Envío)":
Utils.log_message(self.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.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
self.random_error_timer_stop_event.wait(timeout=current_interval)
Utils.log_message(self.log_text, "Error Sim: Hilo de errores aleatorios detenido.")
def add_data_point(self, brix_value, ma_value): def add_data_point(self, brix_value, ma_value):
"""Agrega un punto de datos al gráfico""" """Agrega un punto de datos al gráfico"""
@ -627,6 +896,8 @@ class SimulatorTab:
if 'shared_widgets' in self.shared_config: if 'shared_widgets' in self.shared_config:
Utils.set_widgets_state(self.shared_config['shared_widgets'], state) 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): def get_config(self):
"""Obtiene la configuración actual del simulador""" """Obtiene la configuración actual del simulador"""
@ -636,7 +907,8 @@ class SimulatorTab:
'cycle_time': self.cycle_time_var.get(), 'cycle_time': self.cycle_time_var.get(),
'samples_per_cycle': self.samples_per_cycle_var.get(), 'samples_per_cycle': self.samples_per_cycle_var.get(),
'manual_input_type': self.manual_input_type_var.get(), 'manual_input_type': self.manual_input_type_var.get(),
'manual_value': self.manual_value_var.get() 'manual_value': self.manual_value_var.get(),
'random_error_interval': self.random_error_interval_var.get()
} }
def set_config(self, config): def set_config(self, config):
@ -648,6 +920,7 @@ class SimulatorTab:
self.manual_input_type_var.set(config.get('manual_input_type', 'Brix')) self.manual_input_type_var.set(config.get('manual_input_type', 'Brix'))
self.manual_value_var.set(config.get('manual_value', '10.0')) self.manual_value_var.set(config.get('manual_value', '10.0'))
self.random_error_interval_var.set(config.get('random_error_interval', '10.0'))
try: try:
self.manual_slider_var.set(float(self.manual_value_var.get())) self.manual_slider_var.set(float(self.manual_value_var.get()))
@ -657,3 +930,9 @@ class SimulatorTab:
pass pass
self.on_function_type_change() # Esto llamará a on_manual_input_type_change si es necesario 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
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