From 163a7bacbc4312702134d7dd1956af5c8292bd8b Mon Sep 17 00:00:00 2001 From: Miguel Date: Thu, 22 May 2025 21:46:28 +0200 Subject: [PATCH] Add configuration and trace files for Maselli simulator - Added icon image for the application. - Created a new configuration file `maselli_simulator_config.json` with TCP connection settings and parameters for the simulator. - Added a CSV file `maselli_trace_20250522_214450.csv` to store trace data with headers for Timestamp, mA, Brix, and Raw_Message. --- MaselliSimulatorApp.py | 767 ++++++++++++++++++++++-------- icon.png | Bin 0 -> 14691 bytes maselli_simulator_config.json | 13 + maselli_trace_20250522_214450.csv | 1 + 4 files changed, 575 insertions(+), 206 deletions(-) create mode 100644 icon.png create mode 100644 maselli_simulator_config.json create mode 100644 maselli_trace_20250522_214450.csv diff --git a/MaselliSimulatorApp.py b/MaselliSimulatorApp.py index 4d5f15c..021d3aa 100644 --- a/MaselliSimulatorApp.py +++ b/MaselliSimulatorApp.py @@ -1,5 +1,5 @@ import tkinter as tk -from tkinter import ttk, scrolledtext, messagebox +from tkinter import ttk, scrolledtext, messagebox, filedialog import serial import socket import threading @@ -7,6 +7,8 @@ import time import math import json import os +import csv +from datetime import datetime from collections import deque import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg @@ -16,56 +18,105 @@ import matplotlib.animation as animation class MaselliSimulatorApp: def __init__(self, root_window): self.root = root_window - self.root.title("Simulador Protocolo Maselli - Serial/Ethernet") + self.root.title("Simulador/Trace Protocolo Maselli") - self.connection = None # Puede ser serial.Serial o socket - self.connection_type = None # 'serial', 'tcp', o 'udp' + self.connection = None + self.connection_type = None self.simulating = False self.simulation_thread = None self.simulation_step = 0 - # Para el gráfico + # Para modo Trace + self.tracing = False + self.trace_thread = None + self.csv_file = None + self.csv_writer = None + + # Para los gráficos 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() + # Datos para simulador + self.sim_time_data = deque(maxlen=self.max_points) + self.sim_brix_data = deque(maxlen=self.max_points) + self.sim_ma_data = deque(maxlen=self.max_points) + self.sim_start_time = time.time() + + # Datos para trace + self.trace_time_data = deque(maxlen=self.max_points) + self.trace_brix_data = deque(maxlen=self.max_points) + self.trace_start_time = time.time() # Archivo de configuración self.config_file = "maselli_simulator_config.json" - # --- Configuration Frame --- - config_frame = ttk.LabelFrame(self.root, text="Configuración") - config_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew", columnspan=2) + # Crear notebook para tabs + self.notebook = ttk.Notebook(self.root) + self.notebook.grid(row=0, column=0, sticky="nsew", padx=5, pady=5) + + # Tab Simulador + self.simulator_tab = ttk.Frame(self.notebook) + self.notebook.add(self.simulator_tab, text="Simulador") + + # Tab Trace + self.trace_tab = ttk.Frame(self.notebook) + self.notebook.add(self.trace_tab, text="Trace") + + # --- Frame de Configuración Compartida --- + self.create_shared_config_frame() + + # --- Crear contenido de cada tab --- + self.create_simulator_tab() + self.create_trace_tab() + + # Configurar pesos + self.root.columnconfigure(0, weight=1) + self.root.rowconfigure(0, weight=1) + + self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + + # Cargar configuración si existe + self.load_config(silent=True) + + # Inicializar estado de la interfaz + self.on_connection_type_change() + self.on_function_type_change() + + # Iniciar animaciones de gráficos + self.sim_ani = animation.FuncAnimation(self.sim_fig, self.update_sim_graph, interval=100, blit=False) + self.trace_ani = animation.FuncAnimation(self.trace_fig, self.update_trace_graph, interval=100, blit=False) + + def create_shared_config_frame(self): + """Crea el frame de configuración compartida que aparece arriba del notebook""" + shared_config_frame = ttk.LabelFrame(self.root, text="Configuración de Conexión") + shared_config_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew") # Tipo de conexión - ttk.Label(config_frame, text="Tipo de Conexión:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + ttk.Label(shared_config_frame, text="Tipo:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.connection_type_var = tk.StringVar(value="Serial") - self.connection_type_combo = ttk.Combobox(config_frame, textvariable=self.connection_type_var, + self.connection_type_combo = ttk.Combobox(shared_config_frame, textvariable=self.connection_type_var, values=["Serial", "TCP", "UDP"], state="readonly", width=10) self.connection_type_combo.grid(row=0, column=1, padx=5, pady=5, sticky="ew") self.connection_type_combo.bind("<>", self.on_connection_type_change) # Frame para configuración Serial - self.serial_frame = ttk.Frame(config_frame) - self.serial_frame.grid(row=1, column=0, columnspan=4, padx=5, pady=5, sticky="ew") + self.serial_frame = ttk.Frame(shared_config_frame) + self.serial_frame.grid(row=0, column=2, columnspan=4, padx=5, pady=5, sticky="ew") - ttk.Label(self.serial_frame, text="Puerto COM:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + ttk.Label(self.serial_frame, text="Puerto:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.com_port_var = tk.StringVar(value="COM3") self.com_port_entry = ttk.Entry(self.serial_frame, textvariable=self.com_port_var, width=10) self.com_port_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") - ttk.Label(self.serial_frame, text="Baud Rate:").grid(row=0, column=2, padx=5, pady=5, sticky="w") + ttk.Label(self.serial_frame, text="Baud:").grid(row=0, column=2, padx=5, pady=5, sticky="w") self.baud_rate_var = tk.StringVar(value="115200") self.baud_rate_entry = ttk.Entry(self.serial_frame, textvariable=self.baud_rate_var, width=10) self.baud_rate_entry.grid(row=0, column=3, padx=5, pady=5, sticky="ew") # Frame para configuración Ethernet - self.ethernet_frame = ttk.Frame(config_frame) - self.ethernet_frame.grid(row=1, column=0, columnspan=4, padx=5, pady=5, sticky="ew") - self.ethernet_frame.grid_remove() # Oculto por defecto + self.ethernet_frame = ttk.Frame(shared_config_frame) + self.ethernet_frame.grid(row=0, column=2, columnspan=4, padx=5, pady=5, sticky="ew") + self.ethernet_frame.grid_remove() - ttk.Label(self.ethernet_frame, text="Dirección IP:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + ttk.Label(self.ethernet_frame, text="IP:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.ip_address_var = tk.StringVar(value="192.168.1.100") self.ip_address_entry = ttk.Entry(self.ethernet_frame, textvariable=self.ip_address_var, width=15) self.ip_address_entry.grid(row=0, column=1, padx=5, pady=5, sticky="ew") @@ -75,46 +126,53 @@ class MaselliSimulatorApp: self.port_entry = ttk.Entry(self.ethernet_frame, textvariable=self.port_var, width=10) self.port_entry.grid(row=0, column=3, padx=5, pady=5, sticky="ew") - # Configuración común - ttk.Label(config_frame, text="ADAM Address (2c):").grid(row=2, column=0, padx=5, pady=5, sticky="w") - self.adam_address_var = tk.StringVar(value="01") - self.adam_address_entry = ttk.Entry(config_frame, textvariable=self.adam_address_var, width=5) - self.adam_address_entry.grid(row=2, column=1, padx=5, pady=5, sticky="ew") - - ttk.Label(config_frame, text="Función:").grid(row=2, column=2, padx=5, pady=5, sticky="w") - self.function_type_var = tk.StringVar(value="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=2, column=3, padx=5, pady=5, sticky="ew") - self.function_type_combo.bind("<>", self.on_function_type_change) - - # Parámetros para mapeo 4-20mA - ttk.Label(config_frame, text="Valor Mínimo (Brix) [p/ 4mA]:").grid(row=3, column=0, padx=5, pady=5, sticky="w") + # Parámetros de mapeo (compartidos) + ttk.Label(shared_config_frame, text="Min Brix [4mA]:").grid(row=1, column=0, padx=5, pady=5, sticky="w") self.min_brix_map_var = tk.StringVar(value="0") - self.min_brix_map_entry = ttk.Entry(config_frame, textvariable=self.min_brix_map_var, width=10) - self.min_brix_map_entry.grid(row=3, column=1, padx=5, pady=5, sticky="ew") + self.min_brix_map_entry = ttk.Entry(shared_config_frame, textvariable=self.min_brix_map_var, width=10) + self.min_brix_map_entry.grid(row=1, column=1, padx=5, pady=5, sticky="ew") - ttk.Label(config_frame, text="Valor Máximo (Brix) [p/ 20mA]:").grid(row=3, column=2, padx=5, pady=5, sticky="w") + ttk.Label(shared_config_frame, text="Max Brix [20mA]:").grid(row=1, column=2, padx=5, pady=5, sticky="w") self.max_brix_map_var = tk.StringVar(value="80") - self.max_brix_map_entry = ttk.Entry(config_frame, textvariable=self.max_brix_map_var, width=10) - self.max_brix_map_entry.grid(row=3, column=3, padx=5, pady=5, sticky="ew") - - # Parámetros específicos para simulación continua - ttk.Label(config_frame, text="Periodo Sim. (s):").grid(row=4, column=0, padx=5, pady=5, sticky="w") - self.period_var = tk.StringVar(value="1.0") - self.period_entry = ttk.Entry(config_frame, textvariable=self.period_var, width=5) - self.period_entry.grid(row=4, column=1, padx=5, pady=5, sticky="ew") + self.max_brix_map_entry = ttk.Entry(shared_config_frame, textvariable=self.max_brix_map_var, width=10) + self.max_brix_map_entry.grid(row=1, column=3, padx=5, pady=5, sticky="ew") # Botones de persistencia - self.save_config_button = ttk.Button(config_frame, text="Guardar Config", command=self.save_config) - self.save_config_button.grid(row=4, column=2, padx=5, pady=5, sticky="ew") + self.save_config_button = ttk.Button(shared_config_frame, text="Guardar", command=self.save_config) + self.save_config_button.grid(row=1, column=4, padx=5, pady=5, sticky="ew") - self.load_config_button = ttk.Button(config_frame, text="Cargar Config", command=self.load_config) - self.load_config_button.grid(row=4, column=3, padx=5, pady=5, sticky="ew") + self.load_config_button = ttk.Button(shared_config_frame, text="Cargar", command=self.load_config) + self.load_config_button.grid(row=1, column=5, padx=5, pady=5, sticky="ew") - # Frame para modo Manual con slider - manual_frame = ttk.LabelFrame(config_frame, text="Modo Manual") - manual_frame.grid(row=5, column=0, columnspan=4, padx=5, pady=5, sticky="ew") + def create_simulator_tab(self): + """Crea el contenido del tab Simulador""" + # Frame de configuración del simulador + sim_config_frame = ttk.LabelFrame(self.simulator_tab, text="Configuración Simulador") + sim_config_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew", columnspan=2) + + # Dirección ADAM + ttk.Label(sim_config_frame, text="ADAM Address (2c):").grid(row=0, column=0, padx=5, pady=5, sticky="w") + self.adam_address_var = tk.StringVar(value="01") + self.adam_address_entry = ttk.Entry(sim_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(sim_config_frame, text="Función:").grid(row=0, column=2, padx=5, pady=5, sticky="w") + self.function_type_var = tk.StringVar(value="Lineal") + self.function_type_combo = ttk.Combobox(sim_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("<>", self.on_function_type_change) + + # Periodo + ttk.Label(sim_config_frame, text="Periodo (s):").grid(row=0, column=4, padx=5, pady=5, sticky="w") + self.period_var = tk.StringVar(value="1.0") + self.period_entry = ttk.Entry(sim_config_frame, textvariable=self.period_var, width=5) + self.period_entry.grid(row=0, column=5, padx=5, pady=5, sticky="ew") + + # Frame para modo Manual + manual_frame = ttk.LabelFrame(sim_config_frame, text="Modo Manual") + manual_frame.grid(row=1, column=0, columnspan=6, 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="10.0") @@ -123,7 +181,7 @@ class MaselliSimulatorApp: self.manual_brix_entry.bind('', lambda e: self.update_slider_from_entry()) self.manual_brix_entry.bind('', lambda e: self.update_slider_from_entry()) - # Slider para valor manual + # Slider self.manual_slider_var = tk.DoubleVar(value=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, @@ -135,86 +193,435 @@ class MaselliSimulatorApp: manual_frame.columnconfigure(2, weight=1) - # --- Controls Frame --- - controls_frame = ttk.LabelFrame(self.root, text="Control Simulación Continua") + # Controls Frame + controls_frame = ttk.LabelFrame(self.simulator_tab, 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 Simulación", command=self.start_simulation) + 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 Simulación", command=self.stop_simulation, state=tk.DISABLED) + 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) + self.clear_sim_graph_button = ttk.Button(controls_frame, text="Limpiar Gráfico", command=self.clear_sim_graph) + self.clear_sim_graph_button.pack(side=tk.LEFT, padx=5) - # --- Display Frame --- - display_frame = ttk.LabelFrame(self.root, text="Visualización") - display_frame.grid(row=1, column=1, rowspan=2, padx=10, pady=10, sticky="nsew") + # Display Frame + display_frame = ttk.LabelFrame(self.simulator_tab, text="Valores Actuales") + display_frame.grid(row=1, column=1, padx=10, pady=5, sticky="ew") - ttk.Label(display_frame, text="Valor Brix Actual:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + ttk.Label(display_frame, text="Brix:").grid(row=0, column=0, padx=5, pady=5, sticky="w") self.current_brix_display_var = tk.StringVar(value="---") - self.current_brix_label = ttk.Label(display_frame, textvariable=self.current_brix_display_var, font=("Courier", 14, "bold")) + self.current_brix_label = ttk.Label(display_frame, textvariable=self.current_brix_display_var, + font=("Courier", 14, "bold")) self.current_brix_label.grid(row=0, column=1, padx=5, pady=5, sticky="w") - ttk.Label(display_frame, text="Valor mA Correspondiente:").grid(row=1, column=0, 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_display_var = tk.StringVar(value="--.-- mA") - self.current_ma_label = ttk.Label(display_frame, textvariable=self.current_ma_display_var, font=("Courier", 14, "bold")) + self.current_ma_label = ttk.Label(display_frame, textvariable=self.current_ma_display_var, + font=("Courier", 14, "bold")) self.current_ma_label.grid(row=1, column=1, padx=5, pady=5, sticky="w") - # --- Graph Frame --- - graph_frame = ttk.LabelFrame(self.root, text="Gráfico de Valores") - graph_frame.grid(row=2, column=0, padx=10, pady=5, sticky="nsew") + # Graph Frame + sim_graph_frame = ttk.LabelFrame(self.simulator_tab, text="Gráfico Simulador") + sim_graph_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") - # Crear figura de matplotlib - self.fig = Figure(figsize=(6, 3.5), dpi=100) - self.ax1 = self.fig.add_subplot(111) - self.ax2 = self.ax1.twinx() + # Crear figura para simulador + self.sim_fig = Figure(figsize=(8, 3.5), dpi=100) + self.sim_ax1 = self.sim_fig.add_subplot(111) + self.sim_ax2 = self.sim_ax1.twinx() - # Configurar el gráfico - self.ax1.set_xlabel('Tiempo (s)') - self.ax1.set_ylabel('Brix', color='b') - self.ax2.set_ylabel('mA', color='r') - self.ax1.tick_params(axis='y', labelcolor='b') - self.ax2.tick_params(axis='y', labelcolor='r') - self.ax1.grid(True, alpha=0.3) + self.sim_ax1.set_xlabel('Tiempo (s)') + self.sim_ax1.set_ylabel('Brix', color='b') + self.sim_ax2.set_ylabel('mA', color='r') + self.sim_ax1.tick_params(axis='y', labelcolor='b') + self.sim_ax2.tick_params(axis='y', labelcolor='r') + self.sim_ax1.grid(True, alpha=0.3) - # Líneas del gráfico - self.line_brix, = self.ax1.plot([], [], 'b-', label='Brix', linewidth=2) - self.line_ma, = self.ax2.plot([], [], 'r-', label='mA', linewidth=2) + self.sim_line_brix, = self.sim_ax1.plot([], [], 'b-', label='Brix', linewidth=2) + self.sim_line_ma, = self.sim_ax2.plot([], [], 'r-', label='mA', linewidth=2) - # Canvas de matplotlib en tkinter - self.canvas = FigureCanvasTkAgg(self.fig, master=graph_frame) - self.canvas.draw() - self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + self.sim_canvas = FigureCanvasTkAgg(self.sim_fig, master=sim_graph_frame) + self.sim_canvas.draw() + self.sim_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=5, pady=5) - # --- Log Frame --- - log_frame = ttk.LabelFrame(self.root, text="Log de Comunicación") - log_frame.grid(row=3, column=0, padx=10, pady=10, sticky="nsew", columnspan=2) + # Log Frame + sim_log_frame = ttk.LabelFrame(self.simulator_tab, text="Log de Comunicación") + sim_log_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") - self.com_log_text = scrolledtext.ScrolledText(log_frame, height=10, width=70, wrap=tk.WORD, state=tk.DISABLED) - self.com_log_text.pack(padx=5,pady=5,fill=tk.BOTH, expand=True) + self.sim_log_text = scrolledtext.ScrolledText(sim_log_frame, height=8, width=70, wrap=tk.WORD, state=tk.DISABLED) + self.sim_log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) - # Configurar pesos de las filas y columnas - self.root.columnconfigure(0, weight=1) - self.root.columnconfigure(1, weight=1) - self.root.rowconfigure(2, weight=1) - self.root.rowconfigure(3, weight=1) + # Configurar pesos + self.simulator_tab.columnconfigure(0, weight=1) + self.simulator_tab.columnconfigure(1, weight=1) + self.simulator_tab.rowconfigure(2, weight=1) + self.simulator_tab.rowconfigure(3, weight=1) - self.root.protocol("WM_DELETE_WINDOW", self.on_closing) + def create_trace_tab(self): + """Crea el contenido del tab Trace""" + # Control Frame + trace_control_frame = ttk.LabelFrame(self.trace_tab, text="Control Trace") + trace_control_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky="ew") + + self.start_trace_button = ttk.Button(trace_control_frame, text="Iniciar Trace", command=self.start_trace) + self.start_trace_button.pack(side=tk.LEFT, padx=5) + + self.stop_trace_button = ttk.Button(trace_control_frame, text="Detener Trace", command=self.stop_trace, state=tk.DISABLED) + self.stop_trace_button.pack(side=tk.LEFT, padx=5) + + self.clear_trace_graph_button = ttk.Button(trace_control_frame, text="Limpiar Gráfico", command=self.clear_trace_graph) + self.clear_trace_graph_button.pack(side=tk.LEFT, padx=5) + + ttk.Label(trace_control_frame, text="Archivo CSV:").pack(side=tk.LEFT, padx=(20, 5)) + self.csv_filename_var = tk.StringVar(value="Sin archivo") + self.csv_filename_label = ttk.Label(trace_control_frame, textvariable=self.csv_filename_var) + self.csv_filename_label.pack(side=tk.LEFT, padx=5) + + # Display Frame + trace_display_frame = ttk.LabelFrame(self.trace_tab, text="Último Valor Recibido") + trace_display_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew") + + ttk.Label(trace_display_frame, text="Timestamp:").grid(row=0, column=0, padx=5, pady=5, sticky="w") + self.trace_timestamp_var = tk.StringVar(value="---") + ttk.Label(trace_display_frame, textvariable=self.trace_timestamp_var, font=("Courier", 12)).grid(row=0, column=1, padx=5, pady=5, sticky="w") + + ttk.Label(trace_display_frame, text="Valor mA:").grid(row=0, column=2, padx=5, pady=5, sticky="w") + self.trace_ma_var = tk.StringVar(value="---") + ttk.Label(trace_display_frame, textvariable=self.trace_ma_var, font=("Courier", 12, "bold")).grid(row=0, column=3, padx=5, pady=5, sticky="w") + + ttk.Label(trace_display_frame, text="Valor Brix:").grid(row=0, column=4, padx=5, pady=5, sticky="w") + self.trace_brix_var = tk.StringVar(value="---") + ttk.Label(trace_display_frame, textvariable=self.trace_brix_var, font=("Courier", 12, "bold")).grid(row=0, column=5, padx=5, pady=5, sticky="w") + + # Graph Frame + trace_graph_frame = ttk.LabelFrame(self.trace_tab, text="Gráfico Trace (Brix)") + trace_graph_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") + + # Crear figura para trace + self.trace_fig = Figure(figsize=(8, 4), dpi=100) + self.trace_ax = self.trace_fig.add_subplot(111) - # Cargar configuración si existe - self.load_config(silent=True) + self.trace_ax.set_xlabel('Tiempo (s)') + self.trace_ax.set_ylabel('Brix', color='b') + self.trace_ax.tick_params(axis='y', labelcolor='b') + self.trace_ax.grid(True, alpha=0.3) - # Inicializar estado de la interfaz - self.on_connection_type_change() - self.on_function_type_change() + self.trace_line_brix, = self.trace_ax.plot([], [], 'b-', label='Brix', linewidth=2, marker='o', markersize=4) - # Iniciar animación del gráfico - self.ani = animation.FuncAnimation(self.fig, self.update_graph, interval=100, blit=False) + self.trace_canvas = FigureCanvasTkAgg(self.trace_fig, master=trace_graph_frame) + self.trace_canvas.draw() + self.trace_canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # Log Frame + trace_log_frame = ttk.LabelFrame(self.trace_tab, text="Log de Recepción") + trace_log_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew") + + self.trace_log_text = scrolledtext.ScrolledText(trace_log_frame, height=10, width=70, wrap=tk.WORD, state=tk.DISABLED) + self.trace_log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True) + + # Configurar pesos + self.trace_tab.columnconfigure(0, weight=1) + self.trace_tab.columnconfigure(1, weight=1) + self.trace_tab.rowconfigure(2, weight=1) + self.trace_tab.rowconfigure(3, weight=1) + + def _log_message(self, message, log_widget=None): + """Escribe mensaje en el log especificado o en el log activo""" + if log_widget is None: + # Determinar qué log usar basado en la pestaña activa + current_tab = self.notebook.index(self.notebook.select()) + log_widget = self.sim_log_text if current_tab == 0 else self.trace_log_text + + log_widget.configure(state=tk.NORMAL) + log_widget.insert(tk.END, f"[{datetime.now().strftime('%H:%M:%S')}] {message}\n") + log_widget.see(tk.END) + log_widget.configure(state=tk.DISABLED) + + def parse_adam_message(self, data): + """Parsea un mensaje del protocolo ADAM y retorna el valor en mA""" + try: + # Formato esperado: #AA[valor_mA][checksum]\r + if not data.startswith('#') or not data.endswith('\r'): + return None + + # Remover # y \r + data = data[1:-1] + + # Los primeros 2 caracteres son la dirección + if len(data) < 9: # 2 addr + 6 valor + 2 checksum + return None + + address = data[:2] + value_str = data[2:9] # 6 caracteres para el valor + checksum = data[9:11] # 2 caracteres para checksum + + # Verificar checksum + message_part = f"#{address}{value_str}" + calculated_checksum = self.calculate_checksum(message_part) + + if checksum != calculated_checksum: + self._log_message(f"Checksum incorrecto: recibido={checksum}, calculado={calculated_checksum}", + self.trace_log_text) + return None + + # Convertir valor a float + ma_value = float(value_str) + return {'address': address, 'ma': ma_value} + + except Exception as e: + self._log_message(f"Error parseando mensaje: {e}", self.trace_log_text) + return None + + def ma_to_brix(self, ma_value): + """Convierte valor mA a Brix usando el mapeo configurado""" + try: + min_brix = float(self.min_brix_map_var.get()) + max_brix = float(self.max_brix_map_var.get()) + + if ma_value <= 4.0: + return min_brix + elif ma_value >= 20.0: + return max_brix + else: + # Interpolación lineal + percentage = (ma_value - 4.0) / 16.0 + return min_brix + percentage * (max_brix - min_brix) + except: + return 0.0 + + def start_trace(self): + """Inicia el modo trace para recibir datos""" + if self.tracing: + messagebox.showwarning("Advertencia", "El trace ya está en curso.") + return + + # Obtener parámetros de conexión + try: + conn_type = self.connection_type_var.get() + if conn_type == "Serial": + conn_params = { + 'port': self.com_port_var.get(), + 'baud': int(self.baud_rate_var.get()) + } + else: + conn_params = { + 'ip': self.ip_address_var.get(), + 'port': int(self.port_var.get()) + } + except ValueError as e: + messagebox.showerror("Error", f"Parámetros de conexión inválidos: {e}") + return + + # Crear archivo CSV + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + csv_filename = f"maselli_trace_{timestamp}.csv" + try: + self.csv_file = open(csv_filename, 'w', newline='') + self.csv_writer = csv.writer(self.csv_file) + self.csv_writer.writerow(['Timestamp', 'mA', 'Brix', 'Raw_Message']) + self.csv_filename_var.set(csv_filename) + except Exception as e: + messagebox.showerror("Error", f"No se pudo crear archivo CSV: {e}") + return + + # Abrir conexión + try: + self.connection = self._open_connection(conn_type, conn_params) + self.connection_type = conn_type + self._log_message(f"Conexión {conn_type} abierta para trace.", self.trace_log_text) + except Exception as e: + messagebox.showerror("Error de Conexión", str(e)) + if self.csv_file: + self.csv_file.close() + return + + self.tracing = True + self.trace_start_time = time.time() + self.start_trace_button.config(state=tk.DISABLED) + self.stop_trace_button.config(state=tk.NORMAL) + self._set_trace_entries_state(tk.DISABLED) + + # Iniciar thread de recepción + self.trace_thread = threading.Thread(target=self.run_trace, daemon=True) + self.trace_thread.start() + self._log_message("Trace iniciado.", self.trace_log_text) + + def stop_trace(self): + """Detiene el modo trace""" + if not self.tracing: + return + + self.tracing = False + + # Esperar a que termine el thread + if self.trace_thread and self.trace_thread.is_alive(): + self.trace_thread.join(timeout=2.0) + + # Cerrar conexión + if self.connection: + self._close_connection(self.connection, self.connection_type) + self._log_message("Conexión cerrada.", self.trace_log_text) + self.connection = None + + # Cerrar archivo CSV + if self.csv_file: + self.csv_file.close() + self.csv_file = None + self.csv_writer = None + self._log_message(f"Archivo CSV guardado: {self.csv_filename_var.get()}", self.trace_log_text) + + self.start_trace_button.config(state=tk.NORMAL) + self.stop_trace_button.config(state=tk.DISABLED) + self._set_trace_entries_state(tk.NORMAL) + + self._log_message("Trace detenido.", self.trace_log_text) + + def run_trace(self): + """Thread principal para recepción de datos en modo trace""" + buffer = "" + + while self.tracing: + try: + # Leer datos según el tipo de conexión + data = None + if self.connection_type == "Serial": + if self.connection.in_waiting > 0: + data = self.connection.read(self.connection.in_waiting).decode('ascii', errors='ignore') + elif self.connection_type == "TCP": + self.connection.settimeout(0.1) + try: + data = self.connection.recv(1024).decode('ascii', errors='ignore') + except socket.timeout: + continue + elif self.connection_type == "UDP": + self.connection.settimeout(0.1) + try: + data, addr = self.connection.recvfrom(1024) + data = data.decode('ascii', errors='ignore') + except socket.timeout: + continue + + if data: + buffer += data + + # Buscar mensajes completos (terminan con \r) + while '\r' in buffer: + end_idx = buffer.index('\r') + 1 + message = buffer[:end_idx] + buffer = buffer[end_idx:] + + # Procesar mensaje + self._process_trace_message(message) + + except Exception as e: + self._log_message(f"Error en trace: {e}", self.trace_log_text) + if not self.tracing: + break + time.sleep(0.1) + + def _process_trace_message(self, message): + """Procesa un mensaje recibido en modo trace""" + # Log del mensaje raw + display_msg = message.replace('\r', '').replace('\n', '') + self._log_message(f"Recibido: {display_msg}", self.trace_log_text) + + # Parsear mensaje + parsed = self.parse_adam_message(message) + if parsed: + ma_value = parsed['ma'] + brix_value = self.ma_to_brix(ma_value) + timestamp = datetime.now() + + # Actualizar display + self.trace_timestamp_var.set(timestamp.strftime("%H:%M:%S.%f")[:-3]) + self.trace_ma_var.set(f"{ma_value:.3f} mA") + self.trace_brix_var.set(f"{brix_value:.3f} Brix") + + # Guardar en CSV + if self.csv_writer: + self.csv_writer.writerow([ + timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3], + ma_value, + brix_value, + display_msg + ]) + if self.csv_file: + self.csv_file.flush() + + # Agregar al gráfico + current_time = time.time() - self.trace_start_time + self.trace_time_data.append(current_time) + self.trace_brix_data.append(brix_value) + + # Actualizar gráfico + self.root.after(0, self.trace_canvas.draw_idle) + + def _set_trace_entries_state(self, state): + """Habilita/deshabilita controles durante el trace""" + self.connection_type_combo.config(state=state) + self.com_port_entry.config(state=state) + self.baud_rate_entry.config(state=state) + self.ip_address_entry.config(state=state) + self.port_entry.config(state=state) + self.min_brix_map_entry.config(state=state) + self.max_brix_map_entry.config(state=state) + + def update_sim_graph(self, frame): + """Actualiza el gráfico del simulador""" + if len(self.sim_time_data) > 0: + self.sim_line_brix.set_data(list(self.sim_time_data), list(self.sim_brix_data)) + self.sim_line_ma.set_data(list(self.sim_time_data), list(self.sim_ma_data)) + + if len(self.sim_time_data) > 1: + self.sim_ax1.set_xlim(min(self.sim_time_data), max(self.sim_time_data)) + + if len(self.sim_brix_data) > 0: + brix_min = min(self.sim_brix_data) - 1 + brix_max = max(self.sim_brix_data) + 1 + self.sim_ax1.set_ylim(brix_min, brix_max) + + if len(self.sim_ma_data) > 0: + ma_min = min(self.sim_ma_data) - 0.5 + ma_max = max(self.sim_ma_data) + 0.5 + self.sim_ax2.set_ylim(ma_min, ma_max) + + return self.sim_line_brix, self.sim_line_ma + + def update_trace_graph(self, frame): + """Actualiza el gráfico del trace""" + if len(self.trace_time_data) > 0: + self.trace_line_brix.set_data(list(self.trace_time_data), list(self.trace_brix_data)) + + if len(self.trace_time_data) > 1: + self.trace_ax.set_xlim(min(self.trace_time_data), max(self.trace_time_data)) + + if len(self.trace_brix_data) > 0: + brix_min = min(self.trace_brix_data) - 1 + brix_max = max(self.trace_brix_data) + 1 + self.trace_ax.set_ylim(brix_min, brix_max) + + return self.trace_line_brix, + + def clear_sim_graph(self): + """Limpia el gráfico del simulador""" + self.sim_time_data.clear() + self.sim_brix_data.clear() + self.sim_ma_data.clear() + self.sim_start_time = time.time() + self.sim_canvas.draw_idle() + self._log_message("Gráfico del simulador limpiado.", self.sim_log_text) + + def clear_trace_graph(self): + """Limpia el gráfico del trace""" + self.trace_time_data.clear() + self.trace_brix_data.clear() + self.trace_start_time = time.time() + self.trace_canvas.draw_idle() + self._log_message("Gráfico del trace limpiado.", self.trace_log_text) def save_config(self): - """Guarda la configuración actual en un archivo JSON""" + """Guarda la configuración actual""" config = { 'connection_type': self.connection_type_var.get(), 'com_port': self.com_port_var.get(), @@ -239,7 +646,7 @@ class MaselliSimulatorApp: messagebox.showerror("Error", f"No se pudo guardar la configuración: {e}") def load_config(self, silent=False): - """Carga la configuración desde un archivo JSON""" + """Carga la configuración desde archivo""" if not os.path.exists(self.config_file): if not silent: messagebox.showinfo("Información", "No se encontró archivo de configuración.") @@ -249,7 +656,6 @@ class MaselliSimulatorApp: with open(self.config_file, 'r') as f: config = json.load(f) - # Aplicar configuración self.connection_type_var.set(config.get('connection_type', 'Serial')) self.com_port_var.set(config.get('com_port', 'COM3')) self.baud_rate_var.set(config.get('baud_rate', '115200')) @@ -262,13 +668,11 @@ class MaselliSimulatorApp: self.period_var.set(config.get('period', '1.0')) self.manual_brix_var.set(config.get('manual_brix', '10.0')) - # Actualizar slider try: self.manual_slider_var.set(float(config.get('manual_brix', '10.0'))) except: pass - # Actualizar interfaz self.on_connection_type_change() if not silent: @@ -279,68 +683,12 @@ class MaselliSimulatorApp: self._log_message(f"Error al cargar configuración: {e}") messagebox.showerror("Error", f"No se pudo cargar la configuración: {e}") - def on_slider_change(self, value): - """Actualiza el campo de texto cuando cambia el slider""" - self.manual_brix_var.set(f"{float(value):.1f}") - - def update_slider_from_entry(self): - """Actualiza el slider cuando se modifica el campo de texto""" - try: - value = float(self.manual_brix_var.get()) - # Limitar el valor al rango del slider - value = max(0, min(100, value)) - self.manual_slider_var.set(value) - self.manual_brix_var.set(f"{value:.1f}") - except ValueError: - pass - - def update_graph(self, frame): - """Actualiza el gráfico con los datos actuales""" - if len(self.time_data) > 0: - self.line_brix.set_data(list(self.time_data), list(self.brix_data)) - self.line_ma.set_data(list(self.time_data), list(self.ma_data)) - - # Ajustar límites - if len(self.time_data) > 1: - self.ax1.set_xlim(min(self.time_data), max(self.time_data)) - - if len(self.brix_data) > 0: - brix_min = min(self.brix_data) - 1 - brix_max = max(self.brix_data) + 1 - self.ax1.set_ylim(brix_min, brix_max) - - if len(self.ma_data) > 0: - ma_min = min(self.ma_data) - 0.5 - ma_max = max(self.ma_data) + 0.5 - self.ax2.set_ylim(ma_min, ma_max) - - return self.line_brix, self.line_ma - - 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) - - # Redibujar el canvas - self.canvas.draw_idle() - - def clear_graph(self): - """Limpia los datos del gráfico""" - self.time_data.clear() - self.brix_data.clear() - self.ma_data.clear() - self.start_time = time.time() - self.canvas.draw_idle() - self._log_message("Gráfico limpiado.") - def on_connection_type_change(self, event=None): conn_type = self.connection_type_var.get() if conn_type == "Serial": self.ethernet_frame.grid_remove() self.serial_frame.grid() - else: # TCP o UDP + else: self.serial_frame.grid_remove() self.ethernet_frame.grid() @@ -370,11 +718,17 @@ class MaselliSimulatorApp: self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) - def _log_message(self, message): - self.com_log_text.configure(state=tk.NORMAL) - self.com_log_text.insert(tk.END, message + "\n") - self.com_log_text.see(tk.END) - self.com_log_text.configure(state=tk.DISABLED) + def on_slider_change(self, value): + self.manual_brix_var.set(f"{float(value):.1f}") + + def update_slider_from_entry(self): + 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 calculate_checksum(self, message_part): s = sum(ord(c) for c in message_part) @@ -404,7 +758,7 @@ class MaselliSimulatorApp: min_brix_map = float(self.min_brix_map_var.get()) max_brix_map = float(self.max_brix_map_var.get()) if min_brix_map > max_brix_map: - messagebox.showerror("Error", "Valor Mínimo (Brix) para mapeo no puede ser mayor que Valor Máximo (Brix).") + messagebox.showerror("Error", "Valor Mínimo (Brix) no puede ser mayor que Valor Máximo (Brix).") return None conn_type = self.connection_type_var.get() @@ -413,7 +767,7 @@ class MaselliSimulatorApp: com_port = self.com_port_var.get() baud_rate = int(self.baud_rate_var.get()) return conn_type, {'port': com_port, 'baud': baud_rate}, adam_address, min_brix_map, max_brix_map - else: # TCP o UDP + else: ip_address = self.ip_address_var.get() port = int(self.port_var.get()) return conn_type, {'ip': ip_address, 'port': port}, adam_address, min_brix_map, max_brix_map @@ -423,7 +777,6 @@ class MaselliSimulatorApp: return None def _open_connection(self, conn_type, conn_params): - """Abre la conexión según el tipo especificado""" try: if conn_type == "Serial": return serial.Serial(conn_params['port'], conn_params['baud'], timeout=1) @@ -435,14 +788,12 @@ class MaselliSimulatorApp: elif conn_type == "UDP": sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(1.0) - # Para UDP guardamos la dirección destino sock.dest_address = (conn_params['ip'], conn_params['port']) return sock except Exception as e: raise Exception(f"Error al abrir conexión {conn_type}: {e}") def _close_connection(self, connection, conn_type): - """Cierra la conexión según el tipo""" try: if conn_type == "Serial": if connection and connection.is_open: @@ -454,7 +805,6 @@ class MaselliSimulatorApp: self._log_message(f"Error al cerrar conexión: {e}") def _send_data(self, connection, conn_type, data): - """Envía datos según el tipo de conexión""" try: if conn_type == "Serial": connection.write(data.encode('ascii')) @@ -465,6 +815,13 @@ class MaselliSimulatorApp: except Exception as e: raise Exception(f"Error al enviar datos: {e}") + def add_sim_data_point(self, brix_value, ma_value): + current_time = time.time() - self.sim_start_time + self.sim_time_data.append(current_time) + self.sim_brix_data.append(brix_value) + self.sim_ma_data.append(ma_value) + self.sim_canvas.draw_idle() + def send_manual_value(self): common_params = self._get_common_params() if not common_params: @@ -483,8 +840,7 @@ class MaselliSimulatorApp: self.current_brix_display_var.set(f"{manual_brix:.3f} Brix") self.current_ma_display_var.set(f"{mA_str} mA") - # Agregar punto al gráfico - self.add_data_point(manual_brix, mA_val) + self.add_sim_data_point(manual_brix, mA_val) message_part = f"#{adam_address}{mA_str}" checksum = self.calculate_checksum(message_part) @@ -494,16 +850,16 @@ class MaselliSimulatorApp: temp_connection = None try: temp_connection = self._open_connection(conn_type, conn_params) - self._log_message(f"Conexión {conn_type} abierta temporalmente para envío manual.") - self._log_message(f"Enviando Manual: {log_display_string}") + self._log_message(f"Conexión {conn_type} abierta temporalmente para envío manual.", self.sim_log_text) + self._log_message(f"Enviando Manual: {log_display_string}", self.sim_log_text) self._send_data(temp_connection, conn_type, full_string_to_send) except Exception as e: - self._log_message(f"Error al enviar manualmente: {e}") + self._log_message(f"Error al enviar manualmente: {e}", self.sim_log_text) messagebox.showerror("Error de Conexión", str(e)) finally: if temp_connection: self._close_connection(temp_connection, conn_type) - self._log_message(f"Conexión {conn_type} cerrada tras envío manual.") + self._log_message(f"Conexión {conn_type} cerrada tras envío manual.", self.sim_log_text) def start_simulation(self): if self.simulating: @@ -531,7 +887,7 @@ class MaselliSimulatorApp: try: self.connection = self._open_connection(self.connection_type, self.conn_params) - self._log_message(f"Conexión {self.connection_type} abierta para simulación continua.") + self._log_message(f"Conexión {self.connection_type} abierta para simulación continua.", self.sim_log_text) except Exception as e: messagebox.showerror("Error de Conexión", str(e)) self.connection = None @@ -541,13 +897,13 @@ class MaselliSimulatorApp: self.simulation_step = 0 self.start_button.config(state=tk.DISABLED) self.stop_button.config(state=tk.NORMAL) - self._set_config_entries_state(tk.DISABLED) + self._set_sim_config_entries_state(tk.DISABLED) self.simulation_thread = threading.Thread(target=self.run_simulation, daemon=True) self.simulation_thread.start() - self._log_message("Simulación continua iniciada.") + self._log_message("Simulación continua iniciada.", self.sim_log_text) - def _set_config_entries_state(self, state): + def _set_sim_config_entries_state(self, state): self.connection_type_combo.config(state=state) self.com_port_entry.config(state=state) self.baud_rate_entry.config(state=state) @@ -580,11 +936,11 @@ class MaselliSimulatorApp: def stop_simulation(self): if not self.simulating: if self.function_type_var.get() != "Manual": - self._log_message("Simulación continua ya estaba detenida.") + self._log_message("Simulación continua ya estaba detenida.", self.sim_log_text) if self.function_type_var.get() != "Manual": self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) - self._set_config_entries_state(tk.NORMAL) + self._set_sim_config_entries_state(tk.NORMAL) return self.simulating = False @@ -592,19 +948,19 @@ class MaselliSimulatorApp: try: self.simulation_thread.join(timeout=max(0.1, self.simulation_period * 1.5 if hasattr(self, 'simulation_period') else 2.0)) except Exception as e: - self._log_message(f"Error al esperar el hilo de simulación: {e}") + self._log_message(f"Error al esperar el hilo de simulación: {e}", self.sim_log_text) if self.connection: self._close_connection(self.connection, self.connection_type) - self._log_message(f"Conexión {self.connection_type} cerrada (simulación continua).") + self._log_message(f"Conexión {self.connection_type} cerrada (simulación continua).", self.sim_log_text) self.connection = None self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) - self._set_config_entries_state(tk.NORMAL) + self._set_sim_config_entries_state(tk.NORMAL) self.on_function_type_change() - self._log_message("Simulación continua detenida.") + self._log_message("Simulación continua detenida.", self.sim_log_text) self.current_brix_display_var.set("---") self.current_ma_display_var.set("--.-- mA") @@ -631,20 +987,19 @@ class MaselliSimulatorApp: self.current_brix_display_var.set(f"{current_brix_val:.3f} Brix") self.current_ma_display_var.set(f"{mA_str} mA") - # Agregar punto al gráfico - self.root.after(0, lambda b=current_brix_val, m=mA_val: self.add_data_point(b, m)) + self.root.after(0, lambda b=current_brix_val, m=mA_val: self.add_sim_data_point(b, m)) message_part = f"#{self.adam_address}{mA_str}" checksum = self.calculate_checksum(message_part) full_string_to_send = f"{message_part}{checksum}\r" log_display_string = full_string_to_send.replace('\r', '') - self._log_message(f"Enviando Sim: {log_display_string}") + self._log_message(f"Enviando Sim: {log_display_string}", self.sim_log_text) if self.connection: try: self._send_data(self.connection, self.connection_type, full_string_to_send) except Exception as e: - self._log_message(f"Error al escribir en conexión (sim): {e}") + self._log_message(f"Error al escribir en conexión (sim): {e}", self.sim_log_text) self.root.after(0, self.stop_simulation_from_thread_error) break @@ -655,26 +1010,26 @@ class MaselliSimulatorApp: self.root.after(0, self.ensure_gui_stopped_state_sim) def stop_simulation_from_thread_error(self): - """Called from main thread if connection error occurs in sim thread.""" if self.simulating: messagebox.showerror("Error de Simulación", "Error de conexión durante la simulación. Simulación detenida.") self.stop_simulation() def ensure_gui_stopped_state_sim(self): - """Asegura que la GUI refleje el estado detenido si el hilo de simulación continua termina.""" if not self.simulating: self.start_button.config(state=tk.NORMAL) self.stop_button.config(state=tk.DISABLED) - self._set_config_entries_state(tk.NORMAL) + self._set_sim_config_entries_state(tk.NORMAL) if self.connection: self._close_connection(self.connection, self.connection_type) - self._log_message(f"Conexión {self.connection_type} cerrada (auto, sim_end).") + self._log_message(f"Conexión {self.connection_type} cerrada (auto, sim_end).", self.sim_log_text) self.connection = None - self._log_message("Simulación continua terminada (hilo finalizado).") + self._log_message("Simulación continua terminada (hilo finalizado).", self.sim_log_text) def on_closing(self): if self.simulating: self.stop_simulation() + if self.tracing: + self.stop_trace() elif self.connection: self._close_connection(self.connection, self.connection_type) self.root.destroy() diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..7db3ece3f697ba67a7758ef7125b4302a7d961ba GIT binary patch literal 14691 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 literal 0 HcmV?d00001 diff --git a/maselli_simulator_config.json b/maselli_simulator_config.json new file mode 100644 index 0000000..39d73fd --- /dev/null +++ b/maselli_simulator_config.json @@ -0,0 +1,13 @@ +{ + "connection_type": "TCP", + "com_port": "COM8", + "baud_rate": "115200", + "ip_address": "10.1.33.18", + "port": "8899", + "adam_address": "01", + "function_type": "Sinusoidal", + "min_brix_map": "0", + "max_brix_map": "80", + "period": "0.5", + "manual_brix": "10.0" +} \ No newline at end of file diff --git a/maselli_trace_20250522_214450.csv b/maselli_trace_20250522_214450.csv new file mode 100644 index 0000000..4e6d2f3 --- /dev/null +++ b/maselli_trace_20250522_214450.csv @@ -0,0 +1 @@ +Timestamp,mA,Brix,Raw_Message