462 lines
22 KiB
Python
462 lines
22 KiB
Python
"""
|
|
Tab del Simulador - Genera valores de prueba en protocolo ADAM
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk, scrolledtext, messagebox
|
|
import threading
|
|
import time
|
|
import math
|
|
from collections import deque
|
|
|
|
from protocol_handler import ProtocolHandler
|
|
from connection_manager import ConnectionManager
|
|
from utils import Utils
|
|
|
|
class SimulatorTab:
|
|
def __init__(self, parent_frame, shared_config):
|
|
self.frame = parent_frame
|
|
self.shared_config = shared_config
|
|
|
|
# Estado del simulador
|
|
self.simulating = False
|
|
self.simulation_thread = None
|
|
self.simulation_step = 0
|
|
self.connection_manager = ConnectionManager()
|
|
|
|
# Datos para el gráfico
|
|
self.max_points = 100
|
|
self.time_data = deque(maxlen=self.max_points)
|
|
self.brix_data = deque(maxlen=self.max_points)
|
|
self.ma_data = deque(maxlen=self.max_points)
|
|
self.start_time = time.time()
|
|
|
|
self.create_widgets()
|
|
|
|
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 = tk.StringVar(value=self.shared_config.get('adam_address', '01'))
|
|
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 = tk.StringVar(value=self.shared_config.get('function_type', 'Lineal'))
|
|
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 = tk.StringVar(value=self.shared_config.get('cycle_time', '10.0'))
|
|
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 = tk.StringVar(value="100")
|
|
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
|
|
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="Valor Brix:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
self.manual_brix_var = tk.StringVar(value=self.shared_config.get('manual_brix', '10.0'))
|
|
self.manual_brix_entry = ttk.Entry(manual_frame, textvariable=self.manual_brix_var, width=10, state=tk.DISABLED)
|
|
self.manual_brix_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
|
self.manual_brix_entry.bind('<Return>', lambda e: self.update_slider_from_entry())
|
|
self.manual_brix_entry.bind('<FocusOut>', lambda e: self.update_slider_from_entry())
|
|
|
|
# Slider
|
|
self.manual_slider_var = tk.DoubleVar(value=float(self.shared_config.get('manual_brix', '10.0')))
|
|
self.manual_slider = ttk.Scale(manual_frame, from_=0, to=100, orient=tk.HORIZONTAL,
|
|
variable=self.manual_slider_var, command=self.on_slider_change,
|
|
state=tk.DISABLED, length=200)
|
|
self.manual_slider.grid(row=0, 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=0, column=3, 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")
|
|
|
|
self.start_button = ttk.Button(controls_frame, text="Iniciar", command=self.start_simulation)
|
|
self.start_button.pack(side=tk.LEFT, padx=5)
|
|
|
|
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_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="ew")
|
|
|
|
ttk.Label(display_frame, text="Brix:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
|
self.current_brix_var = tk.StringVar(value="---")
|
|
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 = tk.StringVar(value="--.-- mA")
|
|
ttk.Label(display_frame, textvariable=self.current_ma_var,
|
|
font=("Courier", 14, "bold")).grid(row=1, column=1, padx=5, pady=5, sticky="w")
|
|
|
|
# Log Frame
|
|
log_frame = ttk.LabelFrame(self.frame, text="Log de Comunicación")
|
|
log_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
|
|
|
|
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)
|
|
|
|
# Configurar pesos
|
|
self.frame.columnconfigure(0, weight=1)
|
|
self.frame.columnconfigure(1, weight=1)
|
|
self.frame.rowconfigure(2, weight=1)
|
|
|
|
# Inicializar estado
|
|
self.on_function_type_change()
|
|
|
|
def get_graph_frame(self):
|
|
"""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)
|
|
return graph_frame
|
|
|
|
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_brix_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)
|
|
self.stop_button.config(state=tk.DISABLED)
|
|
else:
|
|
self.manual_brix_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)
|
|
|
|
def on_slider_change(self, value):
|
|
"""Actualiza el valor del entry cuando cambia el slider"""
|
|
self.manual_brix_var.set(f"{float(value):.1f}")
|
|
|
|
def update_slider_from_entry(self):
|
|
"""Actualiza el slider cuando cambia el entry"""
|
|
try:
|
|
value = float(self.manual_brix_var.get())
|
|
value = max(0, min(100, value))
|
|
self.manual_slider_var.set(value)
|
|
self.manual_brix_var.set(f"{value:.1f}")
|
|
except ValueError:
|
|
pass
|
|
|
|
def send_manual_value(self):
|
|
"""Envía un valor manual único"""
|
|
try:
|
|
# Obtener valores de mapeo
|
|
min_brix = float(self.shared_config['min_brix_map_var'].get())
|
|
max_brix = float(self.shared_config['max_brix_map_var'].get())
|
|
adam_address = self.adam_address_var.get()
|
|
|
|
if len(adam_address) != 2:
|
|
messagebox.showerror("Error", "La dirección ADAM debe tener 2 caracteres.")
|
|
return
|
|
|
|
manual_brix = float(self.manual_brix_var.get())
|
|
|
|
# Crear mensaje
|
|
message, ma_value = ProtocolHandler.create_adam_message(adam_address, manual_brix, min_brix, max_brix)
|
|
|
|
# Actualizar display
|
|
self.current_brix_var.set(Utils.format_brix_display(manual_brix))
|
|
self.current_ma_var.set(Utils.format_ma_display(ma_value))
|
|
|
|
# Agregar al gráfico
|
|
self.add_data_point(manual_brix, ma_value)
|
|
|
|
# Enviar por conexión temporal
|
|
# Construct a dictionary of current config values for get_connection_params
|
|
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)
|
|
|
|
temp_conn = ConnectionManager()
|
|
try:
|
|
temp_conn.open_connection(conn_type, conn_params)
|
|
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)
|
|
|
|
# Intentar leer respuesta
|
|
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, max_brix)
|
|
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 as e:
|
|
messagebox.showerror("Error", "Valores inválidos en la configuración.")
|
|
|
|
def start_simulation(self):
|
|
"""Inicia la simulación continua"""
|
|
if self.simulating:
|
|
messagebox.showwarning("Advertencia", "La simulación ya está en curso.")
|
|
return
|
|
|
|
# Validar configuración
|
|
try:
|
|
adam_address = self.adam_address_var.get()
|
|
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
|
|
|
|
except ValueError:
|
|
messagebox.showerror("Error", "Valores inválidos en la configuración.")
|
|
return
|
|
|
|
# Abrir conexión
|
|
try:
|
|
# Construct a dictionary of current config values for get_connection_params
|
|
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)
|
|
|
|
self.connection_manager.open_connection(conn_type, conn_params)
|
|
Utils.log_message(self.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_button.config(state=tk.DISABLED)
|
|
self.stop_button.config(state=tk.NORMAL)
|
|
self._set_entries_state(tk.DISABLED)
|
|
|
|
self.simulation_thread = threading.Thread(target=self.run_simulation, daemon=True)
|
|
self.simulation_thread.start()
|
|
Utils.log_message(self.log_text, "Simulación iniciada.")
|
|
|
|
def stop_simulation(self):
|
|
"""Detiene la simulación"""
|
|
if not self.simulating:
|
|
return
|
|
|
|
self.simulating = False
|
|
|
|
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()
|
|
|
|
Utils.log_message(self.log_text, "Simulación detenida.")
|
|
self.current_brix_var.set("---")
|
|
self.current_ma_var.set("--.-- mA")
|
|
|
|
def run_simulation(self):
|
|
"""Thread principal de simulación"""
|
|
try:
|
|
# Obtener parámetros
|
|
adam_address = self.adam_address_var.get()
|
|
min_brix = float(self.shared_config['min_brix_map_var'].get())
|
|
max_brix = float(self.shared_config['max_brix_map_var'].get())
|
|
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())
|
|
|
|
# Calcular período entre muestras
|
|
sample_period = cycle_time / samples_per_cycle
|
|
|
|
while self.simulating:
|
|
# Calcular valor actual según la función
|
|
progress = (self.simulation_step % samples_per_cycle) / samples_per_cycle
|
|
|
|
if function_type == "Lineal":
|
|
# Onda triangular
|
|
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 + (max_brix - min_brix) * cycle_progress
|
|
|
|
elif function_type == "Sinusoidal":
|
|
phase = progress * 2 * math.pi
|
|
sin_val = (math.sin(phase) + 1) / 2
|
|
current_brix = min_brix + (max_brix - min_brix) * sin_val
|
|
|
|
# Crear y enviar mensaje
|
|
message, ma_value = ProtocolHandler.create_adam_message(adam_address, current_brix, min_brix, max_brix)
|
|
|
|
# Actualizar display
|
|
self.current_brix_var.set(Utils.format_brix_display(current_brix))
|
|
self.current_ma_var.set(Utils.format_ma_display(ma_value))
|
|
|
|
# Agregar al gráfico
|
|
self.frame.after(0, lambda b=current_brix, m=ma_value: self.add_data_point(b, m))
|
|
|
|
# Log y envío
|
|
Utils.log_message(self.log_text, f"Enviando: {ProtocolHandler.format_for_display(message)}")
|
|
|
|
try:
|
|
self.connection_manager.send_data(message)
|
|
|
|
# Leer respuesta sin bloquear demasiado
|
|
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, max_brix)
|
|
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 en comunicación: {e}")
|
|
self.frame.after(0, self.stop_simulation_error)
|
|
break
|
|
|
|
self.simulation_step += 1
|
|
time.sleep(sample_period)
|
|
|
|
except Exception as e:
|
|
Utils.log_message(self.log_text, f"Error en simulación: {e}")
|
|
self.frame.after(0, self.stop_simulation_error)
|
|
|
|
def stop_simulation_error(self):
|
|
"""Detiene la simulación debido a un error"""
|
|
if self.simulating:
|
|
messagebox.showerror("Error", "Error durante la simulación. Simulación detenida.")
|
|
self.stop_simulation()
|
|
|
|
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)
|
|
self.ma_data.append(ma_value)
|
|
|
|
# Notificar a la aplicación principal para actualizar el gráfico
|
|
if hasattr(self, 'graph_update_callback'):
|
|
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()
|
|
Utils.log_message(self.log_text, "Gráfico limpiado.")
|
|
|
|
if hasattr(self, 'graph_update_callback'):
|
|
self.graph_update_callback()
|
|
|
|
def _set_entries_state(self, state):
|
|
"""Habilita/deshabilita los controles durante la simulación"""
|
|
widgets = [
|
|
self.adam_address_entry,
|
|
self.function_type_combo,
|
|
self.cycle_time_entry,
|
|
self.samples_per_cycle_entry
|
|
]
|
|
Utils.set_widgets_state(widgets, state)
|
|
|
|
# También deshabilitar controles compartidos
|
|
if 'shared_widgets' in self.shared_config:
|
|
Utils.set_widgets_state(self.shared_config['shared_widgets'], state)
|
|
|
|
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(),
|
|
'cycle_time': self.cycle_time_var.get(),
|
|
'samples_per_cycle': self.samples_per_cycle_var.get(),
|
|
'manual_brix': self.manual_brix_var.get()
|
|
}
|
|
|
|
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'))
|
|
self.samples_per_cycle_var.set(config.get('samples_per_cycle', '100'))
|
|
self.manual_brix_var.set(config.get('manual_brix', '10.0'))
|
|
|
|
try:
|
|
self.manual_slider_var.set(float(config.get('manual_brix', '10.0')))
|
|
except:
|
|
pass
|
|
|
|
self.on_function_type_change()
|