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',
'manual_input_type': 'Brix', # Nuevo: 'Brix', 'mA', 'Voltaje'
'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
'netcom_com_port': 'COM3',
'netcom_baud_rate': '115200',
@ -135,6 +136,14 @@ class ConfigManager:
except ValueError:
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
if config.get('connection_type') == 'Serial':
com_port = config.get('com_port', '')

View File

@ -45,10 +45,10 @@ class MaselliApp:
# Inicializar animaciones de gráficos
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_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):
@ -419,4 +419,3 @@ class MaselliApp:
# Cerrar ventana
self.root.destroy()

View File

@ -9,9 +9,10 @@
"adam_address": "01",
"function_type": "Sinusoidal",
"cycle_time": "15",
"samples_per_cycle": "100",
"samples_per_cycle": "70",
"manual_input_type": "Brix",
"manual_value": "0.00",
"random_error_interval": "2.0",
"netcom_com_port": "COM11",
"netcom_baud_rate": "9600",
"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)
full_message_str = f"{message_part}{checksum}\r"
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
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
from tkinter import ttk, scrolledtext, messagebox
import threading
import random
import time
import math
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.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_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_ma_var = tk.StringVar(value="--.-- mA")
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()
@ -115,10 +125,6 @@ class SimulatorTab:
state=tk.DISABLED, length=200)
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)
# 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.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
self.frame.columnconfigure(0, weight=1)
self.frame.columnconfigure(1, weight=1)
self.frame.rowconfigure(2, weight=1) # Log frame
# Inicializar estado
self.on_function_type_change()
@ -172,38 +181,166 @@ class SimulatorTab:
"""Crea y retorna el frame para el gráfico"""
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")
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
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):
"""Maneja el cambio de tipo de función"""
func_type = self.function_type_var.get()
if func_type == "Manual":
if self.simulating:
self.stop_simulation()
self.manual_input_type_combo.config(state=tk.NORMAL)
self.manual_value_entry.config(state=tk.NORMAL)
self.manual_send_button.config(state=tk.NORMAL)
self.manual_slider.config(state=tk.NORMAL)
self.cycle_time_entry.config(state=tk.DISABLED)
self.samples_per_cycle_entry.config(state=tk.DISABLED)
self.start_button.config(state=tk.DISABLED)
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
# 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.on_manual_input_type_change() # Configurar según el tipo actual
else:
self.manual_input_type_combo.config(state=tk.DISABLED)
self.manual_value_entry.config(state=tk.DISABLED)
self.manual_send_button.config(state=tk.DISABLED)
self.manual_slider.config(state=tk.DISABLED)
self.cycle_time_entry.config(state=tk.NORMAL)
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)
# 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
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()
@ -280,127 +417,6 @@ class SimulatorTab:
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 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):
"""Inicia la simulación continua"""
@ -461,6 +477,7 @@ class SimulatorTab:
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
if conn_type == "TCP-Server":
self.shared_config['client_connected_var'].set("Esperando...")
@ -475,14 +492,20 @@ class SimulatorTab:
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():
self.simulation_thread.join(timeout=2.0)
self.connection_manager.close_connection()
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.on_function_type_change() # Re-evaluar estado de controles manuales
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_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.stop_button.config(state=tk.DISABLED)
self.update_error_controls_state() # Deshabilitar controles de error
def run_simulation(self):
"""Thread principal de simulación"""
@ -516,70 +543,146 @@ class SimulatorTab:
sample_period = cycle_time / samples_per_cycle
while self.simulating:
current_brix = 0.0
progress = (self.simulation_step % samples_per_cycle) / samples_per_cycle
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)
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
if cycle_progress > 1.0:
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":
progress = (self.simulation_step % samples_per_cycle) / samples_per_cycle
phase = progress * 2 * math.pi
sin_val = (math.sin(phase) + 1) / 2
current_brix = min_brix_map + (max_brix_map - min_brix_map) * sin_val
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)
self.current_brix_var.set(Utils.format_brix_display(current_brix))
self.current_ma_var.set(Utils.format_ma_display(ma_value))
self.current_voltage_var.set(ProtocolHandler.format_voltage_display(voltage_value))
self.frame.after(0, lambda b=current_brix, m=ma_value: self.add_data_point(b, m))
try:
if conn_type == "TCP-Server":
if not self.connection_manager.is_client_connected():
# Loguear solo si el estado cambia o periódicamente para evitar spam
if not hasattr(self, '_waiting_for_client_logged') or not self._waiting_for_client_logged:
port_to_log = self.shared_config['config_manager'].get_connection_params(current_config_values)['port']
Utils.log_message(self.log_text, f"TCP Server: Esperando cliente en puerto {port_to_log}...")
self._waiting_for_client_logged = True
if self.connection_manager.accept_client(timeout=0.05): # Intento corto no bloqueante
Utils.log_message(self.log_text, f"TCP Server: Cliente conectado desde {self.connection_manager.client_address}")
client_info = f"{self.connection_manager.client_address[0]}:{self.connection_manager.client_address[1]}"
self.shared_config['client_connected_var'].set(client_info)
self._waiting_for_client_logged = False # Resetear flag de log
elif not self.connection_manager.is_client_connected() and \
self.shared_config['client_connected_var'].get() != "Esperando...":
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
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"):
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...")
Utils.log_message(self.log_text, f"Enviando: {ProtocolHandler.format_for_display(message)}")
self.connection_manager.send_data(message)
if conn_type != "TCP-Server": # No leer respuesta en modo servidor
response = self.connection_manager.read_response(timeout=0.1)
if response and response.strip():
Utils.log_message(self.log_text, f"Respuesta: {ProtocolHandler.format_for_display(response)}")
parsed = ProtocolHandler.parse_adam_message(response)
if parsed:
brix_resp = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix_map, max_brix_map)
Utils.log_message(self.log_text,
f" -> Addr: {parsed['address']}, "
f"mA: {parsed['ma']:.3f}, "
f"Brix: {brix_resp:.3f}")
except self.connection_manager.ClientDisconnectedError:
Utils.log_message(self.log_text, "TCP Server: Cliente desconectado. Esperando nueva conexión.")
if conn_type == "TCP-Server":
self.shared_config['client_connected_var'].set("Esperando...")
self._waiting_for_client_logged = False # Permitir que se loguee "esperando" de nuevo
except Exception as e:
Utils.log_message(self.log_text, f"Error en comunicación ({conn_type}): {e}")
self.frame.after(0, self.stop_simulation_error) # Schedule GUI update from main thread
break
self._waiting_for_client_logged = False
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)
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.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)
@ -594,6 +697,172 @@ class SimulatorTab:
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.")
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):
"""Agrega un punto de datos al gráfico"""
@ -627,6 +896,8 @@ class SimulatorTab:
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"""
@ -636,7 +907,8 @@ class SimulatorTab:
'cycle_time': self.cycle_time_var.get(),
'samples_per_cycle': self.samples_per_cycle_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):
@ -648,6 +920,7 @@ class SimulatorTab:
self.manual_input_type_var.set(config.get('manual_input_type', 'Brix'))
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:
self.manual_slider_var.set(float(self.manual_value_var.get()))
@ -657,3 +930,9 @@ class SimulatorTab:
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
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