Modularizacion y Agregado de NetCom
This commit is contained in:
parent
163a7bacbc
commit
e1c9199cb5
|
@ -0,0 +1,97 @@
|
|||
# Resumen de Cambios y Nueva Estructura
|
||||
|
||||
## Estructura Modular Creada
|
||||
|
||||
El proyecto ha sido dividido en los siguientes módulos:
|
||||
|
||||
### Módulos Principales:
|
||||
1. **main.py** - Punto de entrada de la aplicación
|
||||
2. **maselli_app.py** - Aplicación principal y coordinación de la GUI
|
||||
3. **protocol_handler.py** - Manejo del protocolo ADAM/Maselli
|
||||
4. **connection_manager.py** - Gestión unificada de conexiones (Serial/TCP/UDP)
|
||||
5. **config_manager.py** - Gestión de configuración y persistencia
|
||||
6. **utils.py** - Utilidades comunes
|
||||
|
||||
### Módulos de Tabs:
|
||||
1. **tabs/simulator_tab.py** - Lógica del simulador
|
||||
2. **tabs/trace_tab.py** - Lógica del trace
|
||||
3. **tabs/netcom_tab.py** - Nueva funcionalidad de gateway
|
||||
|
||||
## Nuevas Características Implementadas:
|
||||
|
||||
### 1. Tiempo de Ciclo Completo (Simulador)
|
||||
- Reemplaza el período entre muestras por un tiempo total de ciclo más intuitivo
|
||||
- Campo "Tiempo Ciclo (s)" configura la duración total de un ciclo completo
|
||||
- Campo "Muestras/ciclo" permite ajustar la resolución
|
||||
- El período entre muestras se calcula automáticamente
|
||||
|
||||
### 2. Gráfico de Trace Mejorado
|
||||
- Ahora muestra tanto Brix (eje Y izquierdo, azul) como mA (eje Y derecho, rojo)
|
||||
- Marcadores diferentes para cada línea (círculos para Brix, cuadrados para mA)
|
||||
- Actualización en tiempo real de ambos valores
|
||||
|
||||
### 3. Modo NetCom (Gateway)
|
||||
- Nueva pestaña para funcionalidad de bridge/gateway
|
||||
- Conecta un puerto COM físico con una conexión TCP/UDP
|
||||
- Log tipo sniffer que muestra:
|
||||
- Dirección del tráfico (COM→NET o NET→COM)
|
||||
- Datos raw con caracteres de control visibles
|
||||
- Parseo opcional de mensajes ADAM
|
||||
- Estadísticas de transferencias
|
||||
- Filtros configurables para el log
|
||||
- Colores diferenciados por tipo de mensaje
|
||||
|
||||
## Mejoras Adicionales:
|
||||
|
||||
1. **Mejor Organización del Código**
|
||||
- Separación clara de responsabilidades
|
||||
- Código más mantenible y extensible
|
||||
- Fácil agregar nuevas funcionalidades
|
||||
|
||||
2. **Gestión de Configuración Mejorada**
|
||||
- Validación de parámetros
|
||||
- Migración automática de configuraciones antiguas
|
||||
- Valores por defecto sensatos
|
||||
|
||||
3. **Manejo de Conexiones Unificado**
|
||||
- Clase ConnectionManager centraliza toda la lógica de comunicación
|
||||
- Soporte consistente para Serial, TCP y UDP
|
||||
- Mejor manejo de errores y timeouts
|
||||
|
||||
4. **Interfaz de Usuario Mejorada**
|
||||
- Configuración compartida visible en todo momento
|
||||
- Estados visuales claros (colores en NetCom)
|
||||
- Estadísticas en tiempo real
|
||||
|
||||
## Archivos de Soporte:
|
||||
- **README.md** - Documentación completa actualizada
|
||||
- **requirements.txt** - Dependencias del proyecto
|
||||
- **.gitignore** - Para control de versiones
|
||||
- **run.bat** - Script de inicio fácil para Windows
|
||||
- **maselli_simulator_config.json** - Configuración de ejemplo
|
||||
|
||||
## Cómo Ejecutar:
|
||||
|
||||
1. Navegar al directorio del proyecto:
|
||||
```
|
||||
cd D:\Proyectos\Scripts\Siemens\MaselliSimulatorApp
|
||||
```
|
||||
|
||||
2. Instalar dependencias (solo la primera vez):
|
||||
```
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Ejecutar la aplicación:
|
||||
```
|
||||
python main.py
|
||||
```
|
||||
O simplemente doble clic en `run.bat`
|
||||
|
||||
## Notas de Migración:
|
||||
|
||||
- La configuración existente se migrará automáticamente
|
||||
- El campo "period" se convierte a "cycle_time"
|
||||
- Los valores de configuración de NetCom tienen valores por defecto
|
||||
|
||||
La aplicación mantiene toda la funcionalidad original y agrega las nuevas características solicitadas de manera integrada.
|
|
@ -0,0 +1,167 @@
|
|||
# Maselli Protocol Simulator/Tracer/NetCom Gateway
|
||||
|
||||
## Descripción General
|
||||
|
||||
Aplicación de escritorio basada en Python para simular, monitorear y hacer bridge de dispositivos que utilizan el protocolo ADAM/Maselli. La aplicación soporta comunicación Serial (RS485/RS232), TCP y UDP, proporcionando una interfaz gráfica intuitiva construida con Tkinter.
|
||||
|
||||
## Características Principales
|
||||
|
||||
### 1. **Modo Simulador**
|
||||
- Emula un dispositivo Maselli enviando paquetes de datos en protocolo ADAM
|
||||
- Patrones de generación de datos:
|
||||
- **Lineal**: Onda triangular entre valores mínimo y máximo
|
||||
- **Sinusoidal**: Onda sinusoidal suave
|
||||
- **Manual**: Envío de valores individuales mediante slider o entrada directa
|
||||
- **Tiempo de ciclo configurable**: Define el tiempo total para completar un ciclo de simulación
|
||||
- Muestras por ciclo ajustables para control fino de la resolución
|
||||
- Visualización en tiempo real de valores Brix y mA
|
||||
- Gráfico dual con ejes Y independientes para Brix (azul) y mA (rojo)
|
||||
|
||||
### 2. **Modo Trace**
|
||||
- Escucha y registra datos entrantes de dispositivos Maselli reales
|
||||
- Parseo automático de mensajes del protocolo ADAM
|
||||
- Conversión mA ↔ Brix basada en mapeo configurable
|
||||
- Registro de datos en archivo CSV con campos:
|
||||
- Timestamp
|
||||
- Dirección ADAM
|
||||
- Valor mA
|
||||
- Valor Brix calculado
|
||||
- Validez del checksum
|
||||
- Mensaje raw
|
||||
- **Gráfico mejorado**: Muestra tanto Brix como mA en tiempo real
|
||||
- Estadísticas de mensajes recibidos y errores de checksum
|
||||
|
||||
### 3. **Modo NetCom (Gateway)**
|
||||
- Actúa como puente transparente entre:
|
||||
- Un puerto COM físico (configurable)
|
||||
- Una conexión de red (TCP/UDP usando la configuración compartida)
|
||||
- **Función Sniffer**:
|
||||
- Log detallado de todo el tráfico en ambas direcciones
|
||||
- Identificación visual de la dirección del tráfico (COM→NET, NET→COM)
|
||||
- Parseo opcional de mensajes ADAM para mostrar valores interpretados
|
||||
- Filtros de visualización configurables
|
||||
- Estadísticas de transferencias y errores
|
||||
|
||||
## Estructura Modular
|
||||
|
||||
```
|
||||
MaselliSimulatorApp/
|
||||
├── main.py # Punto de entrada
|
||||
├── maselli_app.py # Aplicación principal y GUI
|
||||
├── protocol_handler.py # Manejo del protocolo ADAM
|
||||
├── connection_manager.py # Gestión de conexiones
|
||||
├── config_manager.py # Gestión de configuración
|
||||
├── utils.py # Utilidades comunes
|
||||
└── tabs/
|
||||
├── __init__.py
|
||||
├── simulator_tab.py # Lógica del simulador
|
||||
├── trace_tab.py # Lógica del trace
|
||||
└── netcom_tab.py # Lógica del gateway
|
||||
```
|
||||
|
||||
## Protocolo ADAM
|
||||
|
||||
Formato de mensaje:
|
||||
```
|
||||
#AA[valor_mA][checksum]\r
|
||||
```
|
||||
- `#`: Carácter inicial (opcional en respuestas)
|
||||
- `AA`: Dirección del dispositivo (2 caracteres)
|
||||
- `valor_mA`: Valor en mA (6 caracteres, formato XX.XXX)
|
||||
- `checksum`: Suma de verificación (2 caracteres hex)
|
||||
- `\r`: Carácter de fin
|
||||
|
||||
## Requisitos
|
||||
|
||||
- Python 3.7+
|
||||
- Bibliotecas requeridas:
|
||||
```bash
|
||||
pip install pyserial matplotlib tkinter
|
||||
```
|
||||
|
||||
## Instalación y Uso
|
||||
|
||||
1. Clonar o descargar el proyecto
|
||||
2. Instalar dependencias:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
3. Ejecutar la aplicación:
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
## Configuración
|
||||
|
||||
### Parámetros de Conexión
|
||||
- **Serial**: Puerto COM y velocidad de baudios
|
||||
- **TCP/UDP**: Dirección IP y puerto
|
||||
|
||||
### Mapeo Brix ↔ mA
|
||||
- **Min Brix [4mA]**: Valor Brix correspondiente a 4mA
|
||||
- **Max Brix [20mA]**: Valor Brix correspondiente a 20mA
|
||||
- Interpolación lineal para valores intermedios
|
||||
|
||||
### Configuración del Simulador
|
||||
- **Dirección ADAM**: 2 caracteres (ej: "01")
|
||||
- **Tiempo de ciclo**: Duración total de un ciclo completo de simulación
|
||||
- **Muestras/ciclo**: Número de puntos por ciclo (resolución)
|
||||
|
||||
### Configuración NetCom
|
||||
- **Puerto COM físico**: Puerto para el dispositivo real
|
||||
- **Baud Rate**: Velocidad del puerto COM físico
|
||||
|
||||
## Archivos Generados
|
||||
|
||||
- `maselli_simulator_config.json`: Configuración guardada
|
||||
- `maselli_trace_YYYYMMDD_HHMMSS.csv`: Datos capturados en modo Trace
|
||||
|
||||
## Iconos
|
||||
|
||||
La aplicación buscará automáticamente archivos de icono en el directorio raíz:
|
||||
- `icon.png` (recomendado)
|
||||
- `icon.ico` (Windows)
|
||||
- `icon.gif`
|
||||
|
||||
## Uso Típico
|
||||
|
||||
### Como Simulador
|
||||
1. Configurar tipo de conexión y parámetros
|
||||
2. Seleccionar función de simulación (Lineal/Sinusoidal)
|
||||
3. Ajustar tiempo de ciclo según necesidad
|
||||
4. Iniciar simulación
|
||||
|
||||
### Como Monitor (Trace)
|
||||
1. Configurar conexión según el dispositivo a monitorear
|
||||
2. Iniciar Trace
|
||||
3. Los datos se mostrarán en tiempo real y se guardarán en CSV
|
||||
|
||||
### Como Gateway (NetCom)
|
||||
1. Configurar puerto COM del dispositivo físico
|
||||
2. Configurar conexión de red destino
|
||||
3. Iniciar Gateway
|
||||
4. Monitorear el tráfico bidireccional en el log
|
||||
|
||||
## Notas de Desarrollo
|
||||
|
||||
- La aplicación usa threading para operaciones de comunicación sin bloquear la GUI
|
||||
- Los gráficos se actualizan mediante matplotlib animation
|
||||
- El protocolo ADAM es parseado con validación de checksum opcional
|
||||
- Todos los módulos están diseñados para ser reutilizables
|
||||
|
||||
## Mejoras Respecto a la Versión Original
|
||||
|
||||
1. **Arquitectura modular**: Código dividido en módulos especializados
|
||||
2. **Tiempo de ciclo configurable**: Control más intuitivo de la velocidad de simulación
|
||||
3. **Gráfico de Trace mejorado**: Visualización dual de Brix y mA
|
||||
4. **Modo NetCom**: Nueva funcionalidad de gateway/bridge con sniffer integrado
|
||||
5. **Mejor manejo de errores**: Validación robusta y recuperación de errores
|
||||
6. **Estadísticas detalladas**: Contadores de mensajes, errores y transferencias
|
||||
|
||||
## Licencia
|
||||
|
||||
Este proyecto es de código abierto. Úselo bajo su propia responsabilidad.
|
||||
|
||||
## Autor
|
||||
|
||||
Desarrollado para monitoreo y simulación de dispositivos Maselli con protocolo ADAM.
|
|
@ -1,181 +1,33 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
.venv
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
# Archivos generados
|
||||
*.csv
|
||||
*.log
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
# Archivos de configuración local (opcional, quitar si quieres versionar la config)
|
||||
# maselli_simulator_config.json
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
# Sistema
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
desktop.ini
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Cursor
|
||||
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
|
||||
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
|
||||
# refer to https://docs.cursor.com/context/ignore-files
|
||||
.cursorignore
|
||||
.cursorindexingignore
|
||||
# Iconos (si son específicos del usuario)
|
||||
# icon.png
|
||||
# icon.ico
|
||||
# icon.gif
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
||||
from tkinter import ttk, scrolledtext, messagebox
|
||||
import serial
|
||||
import socket
|
||||
import threading
|
||||
|
@ -15,10 +15,45 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
|||
from matplotlib.figure import Figure
|
||||
import matplotlib.animation as animation
|
||||
|
||||
# Simulador y Trace para Protocolo Maselli (ADAM)
|
||||
# Soporta conexiones Serial, TCP y UDP
|
||||
#
|
||||
# Para cambiar el icono, coloca uno de estos archivos en el mismo directorio:
|
||||
# - icon.png (recomendado)
|
||||
# - icon.ico (para Windows)
|
||||
# - icon.gif
|
||||
#
|
||||
# Características:
|
||||
# - Modo Simulador: Genera valores de prueba en protocolo ADAM
|
||||
# - Modo Trace: Recibe y registra valores del medidor real
|
||||
# - Conversión automática mA <-> Brix
|
||||
# - Registro en CSV con timestamp
|
||||
# - Gráficos en tiempo real
|
||||
# - Respuestas del dispositivo mostradas en el log
|
||||
|
||||
class MaselliSimulatorApp:
|
||||
def __init__(self, root_window):
|
||||
self.root = root_window
|
||||
self.root.title("Simulador/Trace Protocolo Maselli")
|
||||
self.root.geometry("900x700") # Tamaño inicial de ventana
|
||||
|
||||
# Intentar cargar el icono
|
||||
icon_loaded = False
|
||||
for icon_file in ['icon.png', 'icon.ico', 'icon.gif']:
|
||||
if os.path.exists(icon_file):
|
||||
try:
|
||||
if icon_file.endswith('.ico'):
|
||||
self.root.iconbitmap(icon_file)
|
||||
else:
|
||||
icon = tk.PhotoImage(file=icon_file)
|
||||
self.root.iconphoto(True, icon)
|
||||
icon_loaded = True
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"No se pudo cargar {icon_file}: {e}")
|
||||
|
||||
if not icon_loaded:
|
||||
print("No se encontró ningún archivo de icono (icon.png, icon.ico, icon.gif)")
|
||||
|
||||
self.connection = None
|
||||
self.connection_type = None
|
||||
|
@ -338,39 +373,60 @@ class MaselliSimulatorApp:
|
|||
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"""
|
||||
def parse_adam_message(self, data, log_widget=None):
|
||||
"""
|
||||
Parsea un mensaje del protocolo ADAM y retorna el valor en mA
|
||||
Formato esperado: #AA[valor_mA][checksum]\r
|
||||
Donde:
|
||||
- # : Carácter inicial (opcional en algunas respuestas)
|
||||
- AA : Dirección del dispositivo (2 caracteres)
|
||||
- valor_mA : Valor en mA (6 caracteres, formato XX.XXX)
|
||||
- checksum : Suma de verificación (2 caracteres hex)
|
||||
- \r : Carácter de fin (opcional)
|
||||
"""
|
||||
try:
|
||||
# Formato esperado: #AA[valor_mA][checksum]\r
|
||||
if not data.startswith('#') or not data.endswith('\r'):
|
||||
return None
|
||||
# Pero también manejar respuestas sin # inicial o sin \r final
|
||||
data = data.strip()
|
||||
|
||||
# Remover # y \r
|
||||
data = data[1:-1]
|
||||
# Si empieza con #, es un mensaje estándar
|
||||
if data.startswith('#'):
|
||||
data = data[1:] # Remover #
|
||||
|
||||
# Los primeros 2 caracteres son la dirección
|
||||
if len(data) < 9: # 2 addr + 6 valor + 2 checksum
|
||||
# Si termina con \r, removerlo
|
||||
if data.endswith('\r'):
|
||||
data = data[:-1]
|
||||
|
||||
# Verificar longitud mínima
|
||||
if len(data) < 8: # 2 addr + 6 valor mínimo
|
||||
return None
|
||||
|
||||
address = data[:2]
|
||||
value_str = data[2:9] # 6 caracteres para el valor
|
||||
checksum = data[9:11] # 2 caracteres para checksum
|
||||
value_str = data[2:8] # 6 caracteres para el valor (XX.XXX)
|
||||
|
||||
# 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
|
||||
# Verificar si hay checksum
|
||||
if len(data) >= 10:
|
||||
checksum = data[8:10] # 2 caracteres para checksum
|
||||
|
||||
# Verificar checksum
|
||||
message_part = f"#{address}{value_str}"
|
||||
calculated_checksum = self.calculate_checksum(message_part)
|
||||
|
||||
if checksum != calculated_checksum and log_widget:
|
||||
self._log_message(f"Checksum incorrecto: recibido={checksum}, calculado={calculated_checksum}",
|
||||
log_widget)
|
||||
# Continuar de todos modos si el valor parece válido
|
||||
|
||||
# Convertir valor a float
|
||||
ma_value = float(value_str)
|
||||
return {'address': address, 'ma': ma_value}
|
||||
try:
|
||||
ma_value = float(value_str)
|
||||
return {'address': address, 'ma': ma_value}
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
self._log_message(f"Error parseando mensaje: {e}", self.trace_log_text)
|
||||
if log_widget:
|
||||
self._log_message(f"Error parseando mensaje: {e}", log_widget)
|
||||
return None
|
||||
|
||||
def ma_to_brix(self, ma_value):
|
||||
|
@ -419,7 +475,7 @@ class MaselliSimulatorApp:
|
|||
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_writer.writerow(['Timestamp', 'Address', '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}")
|
||||
|
@ -492,6 +548,9 @@ class MaselliSimulatorApp:
|
|||
self.connection.settimeout(0.1)
|
||||
try:
|
||||
data = self.connection.recv(1024).decode('ascii', errors='ignore')
|
||||
if not data: # Conexión cerrada
|
||||
self._log_message("Conexión TCP cerrada por el servidor.", self.trace_log_text)
|
||||
break
|
||||
except socket.timeout:
|
||||
continue
|
||||
elif self.connection_type == "UDP":
|
||||
|
@ -505,20 +564,42 @@ class MaselliSimulatorApp:
|
|||
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:]
|
||||
# Buscar mensajes completos (terminan con \r o \n)
|
||||
while '\r' in buffer or '\n' in buffer:
|
||||
# Encontrar el primer terminador
|
||||
end_idx = len(buffer)
|
||||
for term in ['\r', '\n']:
|
||||
if term in buffer:
|
||||
idx = buffer.index(term) + 1
|
||||
if idx < end_idx:
|
||||
end_idx = idx
|
||||
|
||||
# Procesar mensaje
|
||||
self._process_trace_message(message)
|
||||
if end_idx > 0:
|
||||
message = buffer[:end_idx]
|
||||
buffer = buffer[end_idx:]
|
||||
|
||||
# Procesar mensaje si tiene contenido
|
||||
if message.strip():
|
||||
self._process_trace_message(message)
|
||||
else:
|
||||
break
|
||||
|
||||
# Si el buffer tiene un mensaje completo sin terminador (>= 10 chars)
|
||||
# y no han llegado más datos en un tiempo, procesarlo
|
||||
if len(buffer) >= 10 and not ('\r' in buffer or '\n' in buffer):
|
||||
# Verificar si parece un mensaje ADAM completo
|
||||
if buffer.startswith('#') or len(buffer) == 10:
|
||||
self._process_trace_message(buffer)
|
||||
buffer = ""
|
||||
|
||||
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)
|
||||
if self.tracing: # Solo loguear si todavía estamos en trace
|
||||
self._log_message(f"Error en trace: {e}", self.trace_log_text)
|
||||
break
|
||||
|
||||
# Pequeña pausa para no consumir demasiado CPU
|
||||
if not data:
|
||||
time.sleep(0.01)
|
||||
|
||||
def _process_trace_message(self, message):
|
||||
"""Procesa un mensaje recibido en modo trace"""
|
||||
|
@ -527,7 +608,7 @@ class MaselliSimulatorApp:
|
|||
self._log_message(f"Recibido: {display_msg}", self.trace_log_text)
|
||||
|
||||
# Parsear mensaje
|
||||
parsed = self.parse_adam_message(message)
|
||||
parsed = self.parse_adam_message(message, self.trace_log_text)
|
||||
if parsed:
|
||||
ma_value = parsed['ma']
|
||||
brix_value = self.ma_to_brix(ma_value)
|
||||
|
@ -538,12 +619,17 @@ class MaselliSimulatorApp:
|
|||
self.trace_ma_var.set(f"{ma_value:.3f} mA")
|
||||
self.trace_brix_var.set(f"{brix_value:.3f} Brix")
|
||||
|
||||
# Log con detalles parseados
|
||||
self._log_message(f" -> Addr: {parsed['address']}, mA: {ma_value:.3f}, Brix: {brix_value:.3f}",
|
||||
self.trace_log_text)
|
||||
|
||||
# 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,
|
||||
parsed['address'],
|
||||
f"{ma_value:.3f}",
|
||||
f"{brix_value:.3f}",
|
||||
display_msg
|
||||
])
|
||||
if self.csv_file:
|
||||
|
@ -556,6 +642,9 @@ class MaselliSimulatorApp:
|
|||
|
||||
# Actualizar gráfico
|
||||
self.root.after(0, self.trace_canvas.draw_idle)
|
||||
else:
|
||||
# Si no es un mensaje ADAM válido, podría ser otro tipo de respuesta
|
||||
self._log_message(f"Mensaje no ADAM: {display_msg}", self.trace_log_text)
|
||||
|
||||
def _set_trace_entries_state(self, state):
|
||||
"""Habilita/deshabilita controles durante el trace"""
|
||||
|
@ -746,6 +835,7 @@ class MaselliSimulatorApp:
|
|||
return mA_value
|
||||
|
||||
def format_mA_value(self, mA_val):
|
||||
# Formato: "XX.XXX" (6 caracteres incluyendo el punto)
|
||||
return f"{mA_val:06.3f}"
|
||||
|
||||
def _get_common_params(self):
|
||||
|
@ -822,6 +912,50 @@ class MaselliSimulatorApp:
|
|||
self.sim_ma_data.append(ma_value)
|
||||
self.sim_canvas.draw_idle()
|
||||
|
||||
def _read_response(self, connection, conn_type, timeout=0.5):
|
||||
"""Intenta leer una respuesta del dispositivo"""
|
||||
try:
|
||||
response = None
|
||||
if conn_type == "Serial":
|
||||
# Guardar timeout original
|
||||
original_timeout = connection.timeout
|
||||
connection.timeout = timeout
|
||||
# Esperar un poco para que llegue la respuesta
|
||||
time.sleep(0.05)
|
||||
# Leer todos los bytes disponibles
|
||||
response_bytes = b""
|
||||
start_time = time.time()
|
||||
while (time.time() - start_time) < timeout:
|
||||
if connection.in_waiting > 0:
|
||||
response_bytes += connection.read(connection.in_waiting)
|
||||
# Si encontramos un terminador, salir
|
||||
if b'\r' in response_bytes or b'\n' in response_bytes:
|
||||
break
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
|
||||
if response_bytes:
|
||||
response = response_bytes.decode('ascii', errors='ignore')
|
||||
connection.timeout = original_timeout
|
||||
elif conn_type == "TCP":
|
||||
connection.settimeout(timeout)
|
||||
try:
|
||||
response = connection.recv(1024).decode('ascii', errors='ignore')
|
||||
except socket.timeout:
|
||||
pass
|
||||
elif conn_type == "UDP":
|
||||
connection.settimeout(timeout)
|
||||
try:
|
||||
response, addr = connection.recvfrom(1024)
|
||||
response = response.decode('ascii', errors='ignore')
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
self._log_message(f"Error al leer respuesta: {e}", self.sim_log_text)
|
||||
return None
|
||||
|
||||
def send_manual_value(self):
|
||||
common_params = self._get_common_params()
|
||||
if not common_params:
|
||||
|
@ -853,6 +987,20 @@ class MaselliSimulatorApp:
|
|||
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)
|
||||
|
||||
# Intentar leer respuesta
|
||||
response = self._read_response(temp_connection, conn_type)
|
||||
if response and response.strip(): # Solo procesar si hay contenido
|
||||
display_resp = response.replace('\r', '<CR>').replace('\n', '<LF>')
|
||||
self._log_message(f"Respuesta: {display_resp}", self.sim_log_text)
|
||||
|
||||
# Intentar parsear como mensaje ADAM
|
||||
parsed = self.parse_adam_message(response, self.sim_log_text)
|
||||
if parsed:
|
||||
# Convertir mA a Brix para mostrar
|
||||
brix_value = self.ma_to_brix(parsed['ma'])
|
||||
self._log_message(f" -> Dirección: {parsed['address']}, Valor: {parsed['ma']:.3f} mA ({brix_value:.3f} Brix)", self.sim_log_text)
|
||||
|
||||
except Exception as e:
|
||||
self._log_message(f"Error al enviar manualmente: {e}", self.sim_log_text)
|
||||
messagebox.showerror("Error de Conexión", str(e))
|
||||
|
@ -998,6 +1146,20 @@ class MaselliSimulatorApp:
|
|||
if self.connection:
|
||||
try:
|
||||
self._send_data(self.connection, self.connection_type, full_string_to_send)
|
||||
|
||||
# Intentar leer respuesta (timeout corto para no ralentizar simulación)
|
||||
response = self._read_response(self.connection, self.connection_type, timeout=0.1)
|
||||
if response and response.strip(): # Solo procesar si hay contenido
|
||||
display_resp = response.replace('\r', '<CR>').replace('\n', '<LF>')
|
||||
self._log_message(f"Respuesta: {display_resp}", self.sim_log_text)
|
||||
|
||||
# Intentar parsear como mensaje ADAM
|
||||
parsed = self.parse_adam_message(response, self.sim_log_text)
|
||||
if parsed:
|
||||
# Convertir mA a Brix para mostrar
|
||||
brix_value = self.ma_to_brix(parsed['ma'])
|
||||
self._log_message(f" -> Dirección: {parsed['address']}, Valor: {parsed['ma']:.3f} mA ({brix_value:.3f} Brix)", self.sim_log_text)
|
||||
|
||||
except Exception as 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)
|
||||
|
@ -1026,12 +1188,20 @@ class MaselliSimulatorApp:
|
|||
self._log_message("Simulación continua terminada (hilo finalizado).", self.sim_log_text)
|
||||
|
||||
def on_closing(self):
|
||||
"""Maneja el cierre de la aplicación"""
|
||||
# Detener simulación si está activa
|
||||
if self.simulating:
|
||||
self.stop_simulation()
|
||||
|
||||
# Detener trace si está activo
|
||||
if self.tracing:
|
||||
self.stop_trace()
|
||||
elif self.connection:
|
||||
|
||||
# Cerrar cualquier conexión abierta
|
||||
if self.connection:
|
||||
self._close_connection(self.connection, self.connection_type)
|
||||
|
||||
# Destruir ventana
|
||||
self.root.destroy()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
"""
|
||||
Gestor de configuración para guardar y cargar ajustes
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
class ConfigManager:
|
||||
def __init__(self, config_file="maselli_simulator_config.json"):
|
||||
self.config_file = config_file
|
||||
self.default_config = {
|
||||
'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',
|
||||
'cycle_time': '0.5', # Cambiado de 'period' a 'cycle_time' para tiempo de ciclo completo
|
||||
'manual_brix': '10.0',
|
||||
# Configuración para NetCom
|
||||
'netcom_com_port': 'COM3',
|
||||
'netcom_baud_rate': '115200'
|
||||
}
|
||||
|
||||
def save_config(self, config_data):
|
||||
"""Guarda la configuración en archivo JSON"""
|
||||
try:
|
||||
with open(self.config_file, 'w') as f:
|
||||
json.dump(config_data, f, indent=4)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error al guardar configuración: {e}")
|
||||
return False
|
||||
|
||||
def load_config(self):
|
||||
"""Carga la configuración desde archivo JSON"""
|
||||
if not os.path.exists(self.config_file):
|
||||
return self.default_config.copy()
|
||||
|
||||
try:
|
||||
with open(self.config_file, 'r') as f:
|
||||
config = json.load(f)
|
||||
|
||||
# Asegurarse de que todas las claves necesarias estén presentes
|
||||
for key, value in self.default_config.items():
|
||||
if key not in config:
|
||||
config[key] = value
|
||||
|
||||
# Migrar 'period' a 'cycle_time' si existe
|
||||
if 'period' in config and 'cycle_time' not in config:
|
||||
config['cycle_time'] = config['period']
|
||||
del config['period']
|
||||
|
||||
return config
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error al cargar configuración: {e}")
|
||||
return self.default_config.copy()
|
||||
|
||||
def get_connection_params(self, config, use_netcom_port=False):
|
||||
"""Extrae los parámetros de conexión de la configuración"""
|
||||
conn_type = config.get('connection_type', 'Serial')
|
||||
|
||||
if conn_type == "Serial":
|
||||
if use_netcom_port:
|
||||
# Para NetCom, usar el puerto COM físico dedicado
|
||||
return {
|
||||
'port': config.get('netcom_com_port', 'COM3'),
|
||||
'baud': int(config.get('netcom_baud_rate', '115200'))
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'port': config.get('com_port', 'COM3'),
|
||||
'baud': int(config.get('baud_rate', '115200'))
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'ip': config.get('ip_address', '192.168.1.100'),
|
||||
'port': int(config.get('port', '502'))
|
||||
}
|
||||
|
||||
def validate_config(self, config):
|
||||
"""Valida que la configuración tenga valores correctos"""
|
||||
errors = []
|
||||
|
||||
# Validar dirección ADAM
|
||||
adam_address = config.get('adam_address', '')
|
||||
if len(adam_address) != 2:
|
||||
errors.append("La dirección ADAM debe tener exactamente 2 caracteres")
|
||||
|
||||
# Validar rango de Brix
|
||||
try:
|
||||
min_brix = float(config.get('min_brix_map', '0'))
|
||||
max_brix = float(config.get('max_brix_map', '80'))
|
||||
if min_brix >= max_brix:
|
||||
errors.append("El valor mínimo de Brix debe ser menor que el máximo")
|
||||
except ValueError:
|
||||
errors.append("Los valores de Brix deben ser números válidos")
|
||||
|
||||
# Validar tiempo de ciclo
|
||||
try:
|
||||
cycle_time = float(config.get('cycle_time', '1.0'))
|
||||
if cycle_time <= 0:
|
||||
errors.append("El tiempo de ciclo debe ser mayor que 0")
|
||||
except ValueError:
|
||||
errors.append("El tiempo de ciclo debe ser un número válido")
|
||||
|
||||
# Validar puerto serie
|
||||
if config.get('connection_type') == 'Serial':
|
||||
com_port = config.get('com_port', '')
|
||||
if not com_port.upper().startswith('COM'):
|
||||
errors.append("El puerto COM debe tener formato 'COMx'")
|
||||
|
||||
try:
|
||||
baud_rate = int(config.get('baud_rate', '9600'))
|
||||
if baud_rate <= 0:
|
||||
errors.append("La velocidad de baudios debe ser mayor que 0")
|
||||
except ValueError:
|
||||
errors.append("La velocidad de baudios debe ser un número entero")
|
||||
|
||||
# Validar configuración TCP/UDP
|
||||
else:
|
||||
try:
|
||||
port = int(config.get('port', '502'))
|
||||
if port <= 0 or port > 65535:
|
||||
errors.append("El puerto debe estar entre 1 y 65535")
|
||||
except ValueError:
|
||||
errors.append("El puerto debe ser un número entero")
|
||||
|
||||
return errors
|
|
@ -0,0 +1,165 @@
|
|||
"""
|
||||
Gestor de conexiones para Serial, TCP y UDP
|
||||
"""
|
||||
|
||||
import serial
|
||||
import socket
|
||||
import time
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.connection = None
|
||||
self.connection_type = None
|
||||
self.dest_address = None # Para UDP
|
||||
|
||||
def open_connection(self, conn_type, conn_params):
|
||||
"""Abre una conexión según el tipo especificado"""
|
||||
try:
|
||||
if conn_type == "Serial":
|
||||
self.connection = serial.Serial(
|
||||
conn_params['port'],
|
||||
conn_params['baud'],
|
||||
timeout=1
|
||||
)
|
||||
self.connection_type = "Serial"
|
||||
|
||||
elif conn_type == "TCP":
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(5.0)
|
||||
sock.connect((conn_params['ip'], conn_params['port']))
|
||||
self.connection = sock
|
||||
self.connection_type = "TCP"
|
||||
|
||||
elif conn_type == "UDP":
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
sock.settimeout(1.0)
|
||||
self.dest_address = (conn_params['ip'], conn_params['port'])
|
||||
self.connection = sock
|
||||
self.connection_type = "UDP"
|
||||
|
||||
return self.connection
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"Error al abrir conexión {conn_type}: {e}")
|
||||
|
||||
def close_connection(self):
|
||||
"""Cierra la conexión actual"""
|
||||
try:
|
||||
if self.connection_type == "Serial":
|
||||
if self.connection and self.connection.is_open:
|
||||
self.connection.close()
|
||||
elif self.connection_type in ["TCP", "UDP"]:
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
except Exception as e:
|
||||
print(f"Error al cerrar conexión: {e}")
|
||||
finally:
|
||||
self.connection = None
|
||||
self.connection_type = None
|
||||
self.dest_address = None
|
||||
|
||||
def send_data(self, data):
|
||||
"""Envía datos por la conexión actual"""
|
||||
if not self.connection:
|
||||
raise Exception("No hay conexión activa")
|
||||
|
||||
try:
|
||||
if self.connection_type == "Serial":
|
||||
self.connection.write(data.encode('ascii'))
|
||||
elif self.connection_type == "TCP":
|
||||
self.connection.send(data.encode('ascii'))
|
||||
elif self.connection_type == "UDP":
|
||||
self.connection.sendto(data.encode('ascii'), self.dest_address)
|
||||
except Exception as e:
|
||||
raise Exception(f"Error al enviar datos: {e}")
|
||||
|
||||
def read_response(self, timeout=0.5):
|
||||
"""Intenta leer una respuesta del dispositivo"""
|
||||
if not self.connection:
|
||||
return None
|
||||
|
||||
try:
|
||||
response = None
|
||||
if self.connection_type == "Serial":
|
||||
# Guardar timeout original
|
||||
original_timeout = self.connection.timeout
|
||||
self.connection.timeout = timeout
|
||||
# Esperar un poco para que llegue la respuesta
|
||||
time.sleep(0.05)
|
||||
# Leer todos los bytes disponibles
|
||||
response_bytes = b""
|
||||
start_time = time.time()
|
||||
while (time.time() - start_time) < timeout:
|
||||
if self.connection.in_waiting > 0:
|
||||
response_bytes += self.connection.read(self.connection.in_waiting)
|
||||
# Si encontramos un terminador, salir
|
||||
if b'\r' in response_bytes or b'\n' in response_bytes:
|
||||
break
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
|
||||
if response_bytes:
|
||||
response = response_bytes.decode('ascii', errors='ignore')
|
||||
self.connection.timeout = original_timeout
|
||||
|
||||
elif self.connection_type == "TCP":
|
||||
self.connection.settimeout(timeout)
|
||||
try:
|
||||
response = self.connection.recv(1024).decode('ascii', errors='ignore')
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
elif self.connection_type == "UDP":
|
||||
self.connection.settimeout(timeout)
|
||||
try:
|
||||
response, addr = self.connection.recvfrom(1024)
|
||||
response = response.decode('ascii', errors='ignore')
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error al leer respuesta: {e}")
|
||||
return None
|
||||
|
||||
def read_data_non_blocking(self):
|
||||
"""Lee datos disponibles sin bloquear (para modo trace y netcom)"""
|
||||
if not self.connection:
|
||||
return None
|
||||
|
||||
try:
|
||||
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')
|
||||
if not data: # Conexión cerrada
|
||||
return None
|
||||
except socket.timeout:
|
||||
pass
|
||||
|
||||
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:
|
||||
pass
|
||||
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error al leer datos: {e}")
|
||||
return None
|
||||
|
||||
def is_connected(self):
|
||||
"""Verifica si hay una conexión activa"""
|
||||
if self.connection_type == "Serial":
|
||||
return self.connection and self.connection.is_open
|
||||
else:
|
||||
return self.connection is not None
|
|
@ -0,0 +1,21 @@
|
|||
"""
|
||||
Punto de entrada principal para la aplicación Maselli Simulator/Trace/NetCom
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Agregar el directorio actual al path para importaciones
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from maselli_app import MaselliApp
|
||||
|
||||
def main():
|
||||
"""Función principal que inicia la aplicación"""
|
||||
root = tk.Tk()
|
||||
app = MaselliApp(root)
|
||||
root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,354 @@
|
|||
"""
|
||||
Aplicación principal del Simulador/Trace Maselli
|
||||
Une todos los módulos y maneja la interfaz principal
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||
from matplotlib.figure import Figure
|
||||
import matplotlib.animation as animation
|
||||
|
||||
from config_manager import ConfigManager
|
||||
from utils import Utils
|
||||
from tabs.simulator_tab import SimulatorTab
|
||||
from tabs.trace_tab import TraceTab
|
||||
from tabs.netcom_tab import NetComTab
|
||||
|
||||
class MaselliApp:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("Simulador/Trace/NetCom Protocolo Maselli")
|
||||
self.root.geometry("1000x800")
|
||||
|
||||
# Cargar icono
|
||||
Utils.load_icon(self.root)
|
||||
|
||||
# Gestor de configuración
|
||||
self.config_manager = ConfigManager()
|
||||
self.config = self.config_manager.load_config()
|
||||
|
||||
# Diccionario para compartir configuración entre tabs
|
||||
self.shared_config = {
|
||||
'config_manager': self.config_manager
|
||||
}
|
||||
|
||||
# Crear interfaz
|
||||
self.create_widgets()
|
||||
|
||||
# Cargar configuración inicial
|
||||
self.load_config_to_gui()
|
||||
|
||||
# Configurar eventos
|
||||
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||
|
||||
# Inicializar 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_widgets(self):
|
||||
"""Crea todos los widgets de la aplicación"""
|
||||
# Frame principal
|
||||
main_frame = ttk.Frame(self.root)
|
||||
main_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
||||
|
||||
# Configurar pesos
|
||||
self.root.columnconfigure(0, weight=1)
|
||||
self.root.rowconfigure(0, weight=1)
|
||||
main_frame.columnconfigure(0, weight=1)
|
||||
main_frame.rowconfigure(1, weight=1)
|
||||
|
||||
# Frame de configuración compartida
|
||||
self.create_shared_config_frame(main_frame)
|
||||
|
||||
# Notebook para tabs
|
||||
self.notebook = ttk.Notebook(main_frame)
|
||||
self.notebook.grid(row=1, column=0, sticky="nsew")
|
||||
|
||||
# Tab Simulador
|
||||
sim_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(sim_frame, text="Simulador")
|
||||
self.simulator_tab = SimulatorTab(sim_frame, self.shared_config)
|
||||
|
||||
# Tab Trace
|
||||
trace_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(trace_frame, text="Trace")
|
||||
self.trace_tab = TraceTab(trace_frame, self.shared_config)
|
||||
|
||||
# Tab NetCom
|
||||
netcom_frame = ttk.Frame(self.notebook)
|
||||
self.notebook.add(netcom_frame, text="NetCom (Gateway)")
|
||||
self.netcom_tab = NetComTab(netcom_frame, self.shared_config)
|
||||
|
||||
# Crear gráficos
|
||||
self.create_graphs()
|
||||
|
||||
# Establecer callbacks para actualización de gráficos
|
||||
self.simulator_tab.graph_update_callback = self.update_sim_graph
|
||||
self.trace_tab.graph_update_callback = self.update_trace_graph
|
||||
|
||||
def create_shared_config_frame(self, parent):
|
||||
"""Crea el frame de configuración compartida"""
|
||||
config_frame = ttk.LabelFrame(parent, text="Configuración de Conexión")
|
||||
config_frame.grid(row=0, column=0, sticky="ew", pady=(0, 5))
|
||||
|
||||
# Tipo de conexión
|
||||
ttk.Label(config_frame, text="Tipo:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||
self.connection_type_var = tk.StringVar()
|
||||
self.connection_type_combo = ttk.Combobox(
|
||||
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)
|
||||
self.connection_type_combo.bind("<<ComboboxSelected>>", self.on_connection_type_change)
|
||||
|
||||
# Frame para Serial
|
||||
self.serial_frame = ttk.Frame(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:").grid(row=0, column=0, padx=5, sticky="w")
|
||||
self.com_port_var = tk.StringVar()
|
||||
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)
|
||||
|
||||
ttk.Label(self.serial_frame, text="Baud:").grid(row=0, column=2, padx=5, sticky="w")
|
||||
self.baud_rate_var = tk.StringVar()
|
||||
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)
|
||||
|
||||
# Frame para Ethernet
|
||||
self.ethernet_frame = ttk.Frame(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="IP:").grid(row=0, column=0, padx=5, sticky="w")
|
||||
self.ip_address_var = tk.StringVar()
|
||||
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)
|
||||
|
||||
ttk.Label(self.ethernet_frame, text="Puerto:").grid(row=0, column=2, padx=5, sticky="w")
|
||||
self.port_var = tk.StringVar()
|
||||
self.port_entry = ttk.Entry(self.ethernet_frame, textvariable=self.port_var, width=8)
|
||||
self.port_entry.grid(row=0, column=3, padx=5)
|
||||
|
||||
# Parámetros de mapeo
|
||||
ttk.Label(config_frame, text="Min Brix [4mA]:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
||||
self.min_brix_map_var = tk.StringVar()
|
||||
self.min_brix_map_entry = ttk.Entry(config_frame, textvariable=self.min_brix_map_var, width=10)
|
||||
self.min_brix_map_entry.grid(row=1, column=1, padx=5, pady=5)
|
||||
|
||||
ttk.Label(config_frame, text="Max Brix [20mA]:").grid(row=1, column=2, padx=5, pady=5, sticky="w")
|
||||
self.max_brix_map_var = tk.StringVar()
|
||||
self.max_brix_map_entry = ttk.Entry(config_frame, textvariable=self.max_brix_map_var, width=10)
|
||||
self.max_brix_map_entry.grid(row=1, column=3, padx=5, pady=5)
|
||||
|
||||
# Botones
|
||||
ttk.Button(config_frame, text="Guardar Config",
|
||||
command=self.save_config).grid(row=1, column=4, padx=5, pady=5)
|
||||
ttk.Button(config_frame, text="Cargar Config",
|
||||
command=self.load_config).grid(row=1, column=5, padx=5, pady=5)
|
||||
|
||||
# Guardar referencias para compartir
|
||||
self.shared_config.update({
|
||||
'connection_type_var': self.connection_type_var,
|
||||
'com_port_var': self.com_port_var,
|
||||
'baud_rate_var': self.baud_rate_var,
|
||||
'ip_address_var': self.ip_address_var,
|
||||
'port_var': self.port_var,
|
||||
'min_brix_map_var': self.min_brix_map_var,
|
||||
'max_brix_map_var': self.max_brix_map_var,
|
||||
'shared_widgets': [
|
||||
self.connection_type_combo,
|
||||
self.com_port_entry,
|
||||
self.baud_rate_entry,
|
||||
self.ip_address_entry,
|
||||
self.port_entry,
|
||||
self.min_brix_map_entry,
|
||||
self.max_brix_map_entry
|
||||
]
|
||||
})
|
||||
|
||||
def create_graphs(self):
|
||||
"""Crea los gráficos para simulador y trace"""
|
||||
# Gráfico del simulador
|
||||
sim_graph_frame = self.simulator_tab.get_graph_frame()
|
||||
|
||||
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()
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# Gráfico del trace (ahora con doble eje Y)
|
||||
trace_graph_frame = self.trace_tab.get_graph_frame()
|
||||
|
||||
self.trace_fig = Figure(figsize=(8, 4), dpi=100)
|
||||
self.trace_ax1 = self.trace_fig.add_subplot(111)
|
||||
self.trace_ax2 = self.trace_ax1.twinx()
|
||||
|
||||
self.trace_ax1.set_xlabel('Tiempo (s)')
|
||||
self.trace_ax1.set_ylabel('Brix', color='b')
|
||||
self.trace_ax2.set_ylabel('mA', color='r')
|
||||
self.trace_ax1.tick_params(axis='y', labelcolor='b')
|
||||
self.trace_ax2.tick_params(axis='y', labelcolor='r')
|
||||
self.trace_ax1.grid(True, alpha=0.3)
|
||||
|
||||
self.trace_line_brix, = self.trace_ax1.plot([], [], 'b-', label='Brix', linewidth=2, marker='o', markersize=4)
|
||||
self.trace_line_ma, = self.trace_ax2.plot([], [], 'r-', label='mA', linewidth=2, marker='s', markersize=3)
|
||||
|
||||
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)
|
||||
|
||||
def update_sim_graph(self, frame=None):
|
||||
"""Actualiza el gráfico del simulador"""
|
||||
time_data = list(self.simulator_tab.time_data)
|
||||
brix_data = list(self.simulator_tab.brix_data)
|
||||
ma_data = list(self.simulator_tab.ma_data)
|
||||
|
||||
if len(time_data) > 0:
|
||||
self.sim_line_brix.set_data(time_data, brix_data)
|
||||
self.sim_line_ma.set_data(time_data, ma_data)
|
||||
|
||||
if len(time_data) > 1:
|
||||
self.sim_ax1.set_xlim(min(time_data), max(time_data))
|
||||
|
||||
if brix_data:
|
||||
brix_min = min(brix_data) - 1
|
||||
brix_max = max(brix_data) + 1
|
||||
self.sim_ax1.set_ylim(brix_min, brix_max)
|
||||
|
||||
if ma_data:
|
||||
ma_min = min(ma_data) - 0.5
|
||||
ma_max = max(ma_data) + 0.5
|
||||
self.sim_ax2.set_ylim(ma_min, ma_max)
|
||||
|
||||
self.sim_canvas.draw_idle()
|
||||
|
||||
return self.sim_line_brix, self.sim_line_ma
|
||||
|
||||
def update_trace_graph(self, frame=None):
|
||||
"""Actualiza el gráfico del trace"""
|
||||
time_data = list(self.trace_tab.time_data)
|
||||
brix_data = list(self.trace_tab.brix_data)
|
||||
ma_data = list(self.trace_tab.ma_data)
|
||||
|
||||
if len(time_data) > 0:
|
||||
self.trace_line_brix.set_data(time_data, brix_data)
|
||||
self.trace_line_ma.set_data(time_data, ma_data)
|
||||
|
||||
if len(time_data) > 1:
|
||||
self.trace_ax1.set_xlim(min(time_data), max(time_data))
|
||||
|
||||
if brix_data:
|
||||
brix_min = min(brix_data) - 1
|
||||
brix_max = max(brix_data) + 1
|
||||
self.trace_ax1.set_ylim(brix_min, brix_max)
|
||||
|
||||
if ma_data:
|
||||
ma_min = min(ma_data) - 0.5
|
||||
ma_max = max(ma_data) + 0.5
|
||||
self.trace_ax2.set_ylim(ma_min, ma_max)
|
||||
|
||||
self.trace_canvas.draw_idle()
|
||||
|
||||
return self.trace_line_brix, self.trace_line_ma
|
||||
|
||||
def on_connection_type_change(self, event=None):
|
||||
"""Maneja el cambio de tipo de conexión"""
|
||||
conn_type = self.connection_type_var.get()
|
||||
if conn_type == "Serial":
|
||||
self.ethernet_frame.grid_remove()
|
||||
self.serial_frame.grid()
|
||||
else:
|
||||
self.serial_frame.grid_remove()
|
||||
self.ethernet_frame.grid()
|
||||
|
||||
# Actualizar info en NetCom
|
||||
if hasattr(self, 'netcom_tab'):
|
||||
self.netcom_tab.update_net_info()
|
||||
|
||||
def save_config(self):
|
||||
"""Guarda la configuración actual"""
|
||||
# Recopilar configuración de todos los componentes
|
||||
config = {
|
||||
'connection_type': self.connection_type_var.get(),
|
||||
'com_port': self.com_port_var.get(),
|
||||
'baud_rate': self.baud_rate_var.get(),
|
||||
'ip_address': self.ip_address_var.get(),
|
||||
'port': self.port_var.get(),
|
||||
'min_brix_map': self.min_brix_map_var.get(),
|
||||
'max_brix_map': self.max_brix_map_var.get()
|
||||
}
|
||||
|
||||
# Agregar configuración de cada tab
|
||||
config.update(self.simulator_tab.get_config())
|
||||
config.update(self.netcom_tab.get_config())
|
||||
|
||||
# Validar configuración
|
||||
errors = self.config_manager.validate_config(config)
|
||||
if errors:
|
||||
messagebox.showerror("Error de Configuración", "\n".join(errors))
|
||||
return
|
||||
|
||||
# Guardar
|
||||
if self.config_manager.save_config(config):
|
||||
messagebox.showinfo("Éxito", "Configuración guardada correctamente.")
|
||||
else:
|
||||
messagebox.showerror("Error", "No se pudo guardar la configuración.")
|
||||
|
||||
def load_config(self):
|
||||
"""Carga la configuración desde archivo"""
|
||||
self.config = self.config_manager.load_config()
|
||||
self.load_config_to_gui()
|
||||
messagebox.showinfo("Éxito", "Configuración cargada correctamente.")
|
||||
|
||||
def load_config_to_gui(self):
|
||||
"""Carga la configuración en los widgets de la GUI"""
|
||||
# Configuración compartida
|
||||
self.connection_type_var.set(self.config.get('connection_type', 'Serial'))
|
||||
self.com_port_var.set(self.config.get('com_port', 'COM3'))
|
||||
self.baud_rate_var.set(self.config.get('baud_rate', '115200'))
|
||||
self.ip_address_var.set(self.config.get('ip_address', '192.168.1.100'))
|
||||
self.port_var.set(self.config.get('port', '502'))
|
||||
self.min_brix_map_var.set(self.config.get('min_brix_map', '0'))
|
||||
self.max_brix_map_var.set(self.config.get('max_brix_map', '80'))
|
||||
|
||||
# Configuración específica de cada tab
|
||||
self.simulator_tab.set_config(self.config)
|
||||
self.netcom_tab.set_config(self.config)
|
||||
|
||||
# Actualizar vista
|
||||
self.on_connection_type_change()
|
||||
|
||||
def on_closing(self):
|
||||
"""Maneja el cierre de la aplicación"""
|
||||
# Detener cualquier operación activa
|
||||
if hasattr(self.simulator_tab, 'simulating') and self.simulator_tab.simulating:
|
||||
self.simulator_tab.stop_simulation()
|
||||
|
||||
if hasattr(self.trace_tab, 'tracing') and self.trace_tab.tracing:
|
||||
self.trace_tab.stop_trace()
|
||||
|
||||
if hasattr(self.netcom_tab, 'bridging') and self.netcom_tab.bridging:
|
||||
self.netcom_tab.stop_bridge()
|
||||
|
||||
# Cerrar ventana
|
||||
self.root.destroy()
|
|
@ -4,10 +4,13 @@
|
|||
"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"
|
||||
"adam_address": "01",
|
||||
"function_type": "Manual",
|
||||
"cycle_time": "10.0",
|
||||
"samples_per_cycle": "100",
|
||||
"manual_brix": "10.0",
|
||||
"netcom_com_port": "COM3",
|
||||
"netcom_baud_rate": "115200"
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
"""
|
||||
Manejador del protocolo ADAM/Maselli
|
||||
Contiene las funciones para formatear mensajes, calcular checksums y parsear respuestas
|
||||
"""
|
||||
|
||||
class ProtocolHandler:
|
||||
@staticmethod
|
||||
def calculate_checksum(message_part):
|
||||
"""Calcula el checksum de un mensaje ADAM"""
|
||||
s = sum(ord(c) for c in message_part)
|
||||
checksum_byte = s % 256
|
||||
return f"{checksum_byte:02X}"
|
||||
|
||||
@staticmethod
|
||||
def format_ma_value(ma_val):
|
||||
"""Formatea un valor mA al formato ADAM: XX.XXX (6 caracteres)"""
|
||||
return f"{ma_val:06.3f}"
|
||||
|
||||
@staticmethod
|
||||
def scale_to_ma(brix_value, min_brix_map, max_brix_map):
|
||||
"""Convierte valor Brix a mA usando el mapeo configurado"""
|
||||
if max_brix_map == min_brix_map:
|
||||
return 4.0
|
||||
|
||||
percentage = (brix_value - min_brix_map) / (max_brix_map - min_brix_map)
|
||||
percentage = max(0.0, min(1.0, percentage))
|
||||
|
||||
ma_value = 4.0 + percentage * 16.0
|
||||
return ma_value
|
||||
|
||||
@staticmethod
|
||||
def ma_to_brix(ma_value, min_brix_map, max_brix_map):
|
||||
"""Convierte valor mA a Brix usando el mapeo configurado"""
|
||||
try:
|
||||
if ma_value <= 4.0:
|
||||
return min_brix_map
|
||||
elif ma_value >= 20.0:
|
||||
return max_brix_map
|
||||
else:
|
||||
# Interpolación lineal
|
||||
percentage = (ma_value - 4.0) / 16.0
|
||||
return min_brix_map + percentage * (max_brix_map - min_brix_map)
|
||||
except:
|
||||
return 0.0
|
||||
|
||||
@staticmethod
|
||||
def create_adam_message(adam_address, brix_value, min_brix_map, max_brix_map):
|
||||
"""Crea un mensaje completo ADAM a partir de un valor Brix"""
|
||||
ma_val = ProtocolHandler.scale_to_ma(brix_value, min_brix_map, max_brix_map)
|
||||
ma_str = ProtocolHandler.format_ma_value(ma_val)
|
||||
|
||||
message_part = f"#{adam_address}{ma_str}"
|
||||
checksum = ProtocolHandler.calculate_checksum(message_part)
|
||||
full_message = f"{message_part}{checksum}\r"
|
||||
|
||||
return full_message, ma_val
|
||||
|
||||
@staticmethod
|
||||
def parse_adam_message(data):
|
||||
"""
|
||||
Parsea un mensaje del protocolo ADAM y retorna el valor en mA
|
||||
Formato esperado: #AA[valor_mA][checksum]\r
|
||||
Donde:
|
||||
- # : Carácter inicial (opcional en algunas respuestas)
|
||||
- AA : Dirección del dispositivo (2 caracteres)
|
||||
- valor_mA : Valor en mA (6 caracteres, formato XX.XXX)
|
||||
- checksum : Suma de verificación (2 caracteres hex)
|
||||
- \r : Carácter de fin (opcional)
|
||||
|
||||
Retorna: dict con 'address' y 'ma', o None si no es válido
|
||||
"""
|
||||
try:
|
||||
# Formato esperado: #AA[valor_mA][checksum]\r
|
||||
# Pero también manejar respuestas sin # inicial o sin \r final
|
||||
data = data.strip()
|
||||
|
||||
# Si empieza con #, es un mensaje estándar
|
||||
if data.startswith('#'):
|
||||
data = data[1:] # Remover #
|
||||
|
||||
# Si termina con \r, removerlo
|
||||
if data.endswith('\r'):
|
||||
data = data[:-1]
|
||||
|
||||
# Verificar longitud mínima
|
||||
if len(data) < 8: # 2 addr + 6 valor mínimo
|
||||
return None
|
||||
|
||||
address = data[:2]
|
||||
value_str = data[2:8] # 6 caracteres para el valor (XX.XXX)
|
||||
|
||||
# Verificar si hay checksum
|
||||
checksum_valid = True
|
||||
if len(data) >= 10:
|
||||
checksum = data[8:10] # 2 caracteres para checksum
|
||||
|
||||
# Verificar checksum
|
||||
message_part = f"#{address}{value_str}"
|
||||
calculated_checksum = ProtocolHandler.calculate_checksum(message_part)
|
||||
|
||||
if checksum != calculated_checksum:
|
||||
checksum_valid = False
|
||||
|
||||
# Convertir valor a float
|
||||
try:
|
||||
ma_value = float(value_str)
|
||||
return {
|
||||
'address': address,
|
||||
'ma': ma_value,
|
||||
'checksum_valid': checksum_valid
|
||||
}
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def format_for_display(message):
|
||||
"""Formatea un mensaje para mostrar en el log (reemplaza caracteres no imprimibles)"""
|
||||
return message.replace('\r', '<CR>').replace('\n', '<LF>').replace('\t', '<TAB>')
|
|
@ -0,0 +1,3 @@
|
|||
# Dependencias para Maselli Simulator/Trace/NetCom
|
||||
pyserial==3.5
|
||||
matplotlib==3.7.1
|
|
@ -0,0 +1,4 @@
|
|||
@echo off
|
||||
echo Iniciando Maselli Simulator/Trace/NetCom...
|
||||
python main.py
|
||||
pause
|
|
@ -0,0 +1 @@
|
|||
# Paquete para los tabs de la aplicación
|
|
@ -0,0 +1,453 @@
|
|||
"""
|
||||
Tab NetCom - Gateway/Bridge entre puerto COM físico y conexión TCP/UDP
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
from connection_manager import ConnectionManager
|
||||
from protocol_handler import ProtocolHandler
|
||||
from utils import Utils
|
||||
|
||||
class NetComTab:
|
||||
def __init__(self, parent_frame, shared_config):
|
||||
self.frame = parent_frame
|
||||
self.shared_config = shared_config
|
||||
|
||||
# Estado del gateway
|
||||
self.bridging = False
|
||||
self.bridge_thread = None
|
||||
|
||||
# Conexiones
|
||||
self.com_connection = ConnectionManager()
|
||||
self.net_connection = ConnectionManager()
|
||||
|
||||
# Estadísticas
|
||||
self.com_to_net_count = 0
|
||||
self.net_to_com_count = 0
|
||||
self.error_count = 0
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
"""Crea los widgets del tab NetCom"""
|
||||
# Frame de configuración COM física
|
||||
com_config_frame = ttk.LabelFrame(self.frame, text="Configuración Puerto COM Físico")
|
||||
com_config_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
|
||||
|
||||
ttk.Label(com_config_frame, text="Puerto COM:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||
self.com_port_var = tk.StringVar(value=self.shared_config.get('netcom_com_port', 'COM3'))
|
||||
self.com_port_entry = ttk.Entry(com_config_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(com_config_frame, text="Baud Rate:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
|
||||
self.baud_rate_var = tk.StringVar(value=self.shared_config.get('netcom_baud_rate', '115200'))
|
||||
self.baud_rate_entry = ttk.Entry(com_config_frame, textvariable=self.baud_rate_var, width=10)
|
||||
self.baud_rate_entry.grid(row=0, column=3, padx=5, pady=5, sticky="ew")
|
||||
|
||||
# Info frame
|
||||
info_frame = ttk.LabelFrame(self.frame, text="Información de Conexión")
|
||||
info_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
|
||||
|
||||
ttk.Label(info_frame, text="Conexión de Red:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||
self.net_info_var = tk.StringVar(value="No configurada")
|
||||
ttk.Label(info_frame, textvariable=self.net_info_var, font=("Courier", 10)).grid(row=0, column=1, padx=5, pady=5, sticky="w")
|
||||
|
||||
ttk.Label(info_frame, text="Estado:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
||||
self.status_var = tk.StringVar(value="Desconectado")
|
||||
self.status_label = ttk.Label(info_frame, textvariable=self.status_var, font=("Courier", 10, "bold"))
|
||||
self.status_label.grid(row=1, column=1, padx=5, pady=5, sticky="w")
|
||||
|
||||
# Control Frame
|
||||
control_frame = ttk.LabelFrame(self.frame, text="Control Gateway")
|
||||
control_frame.grid(row=2, column=0, padx=10, pady=5, sticky="ew")
|
||||
|
||||
self.start_button = ttk.Button(control_frame, text="Iniciar Gateway", command=self.start_bridge)
|
||||
self.start_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.stop_button = ttk.Button(control_frame, text="Detener Gateway", command=self.stop_bridge, state=tk.DISABLED)
|
||||
self.stop_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.clear_log_button = ttk.Button(control_frame, text="Limpiar Log", command=self.clear_log)
|
||||
self.clear_log_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Statistics Frame
|
||||
stats_frame = ttk.LabelFrame(self.frame, text="Estadísticas")
|
||||
stats_frame.grid(row=2, column=1, padx=10, pady=5, sticky="ew")
|
||||
|
||||
ttk.Label(stats_frame, text="COM → NET:").grid(row=0, column=0, padx=5, pady=2, sticky="w")
|
||||
self.com_to_net_var = tk.StringVar(value="0")
|
||||
ttk.Label(stats_frame, textvariable=self.com_to_net_var).grid(row=0, column=1, padx=5, pady=2, sticky="w")
|
||||
|
||||
ttk.Label(stats_frame, text="NET → COM:").grid(row=0, column=2, padx=5, pady=2, sticky="w")
|
||||
self.net_to_com_var = tk.StringVar(value="0")
|
||||
ttk.Label(stats_frame, textvariable=self.net_to_com_var).grid(row=0, column=3, padx=5, pady=2, sticky="w")
|
||||
|
||||
ttk.Label(stats_frame, text="Errores:").grid(row=1, column=0, padx=5, pady=2, sticky="w")
|
||||
self.errors_var = tk.StringVar(value="0")
|
||||
ttk.Label(stats_frame, textvariable=self.errors_var, foreground="red").grid(row=1, column=1, padx=5, pady=2, sticky="w")
|
||||
|
||||
# Log Frame con filtros
|
||||
log_control_frame = ttk.Frame(self.frame)
|
||||
log_control_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=(10,0), sticky="ew")
|
||||
|
||||
ttk.Label(log_control_frame, text="Filtros:").pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.show_com_to_net_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(log_control_frame, text="COM→NET",
|
||||
variable=self.show_com_to_net_var).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.show_net_to_com_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(log_control_frame, text="NET→COM",
|
||||
variable=self.show_net_to_com_var).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.show_parsed_var = tk.BooleanVar(value=True)
|
||||
ttk.Checkbutton(log_control_frame, text="Mostrar datos parseados",
|
||||
variable=self.show_parsed_var).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Log Frame
|
||||
log_frame = ttk.LabelFrame(self.frame, text="Log de Gateway (Sniffer)")
|
||||
log_frame.grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
|
||||
|
||||
self.log_text = scrolledtext.ScrolledText(log_frame, height=20, width=80, wrap=tk.WORD, state=tk.DISABLED)
|
||||
self.log_text.pack(padx=5, pady=5, fill=tk.BOTH, expand=True)
|
||||
|
||||
# Configurar tags para colores
|
||||
self.log_text.tag_config("com_to_net", foreground="blue")
|
||||
self.log_text.tag_config("net_to_com", foreground="green")
|
||||
self.log_text.tag_config("error", foreground="red")
|
||||
self.log_text.tag_config("info", foreground="black")
|
||||
self.log_text.tag_config("parsed", foreground="purple")
|
||||
|
||||
# Configurar pesos
|
||||
self.frame.columnconfigure(0, weight=1)
|
||||
self.frame.columnconfigure(1, weight=1)
|
||||
self.frame.rowconfigure(4, weight=1)
|
||||
|
||||
# Actualizar info de red
|
||||
self.update_net_info()
|
||||
|
||||
def update_net_info(self):
|
||||
"""Actualiza la información de la conexión de red configurada"""
|
||||
conn_type = self.shared_config['connection_type_var'].get()
|
||||
if conn_type == "Serial":
|
||||
port = self.shared_config['com_port_var'].get()
|
||||
baud = self.shared_config['baud_rate_var'].get()
|
||||
self.net_info_var.set(f"Serial: {port} @ {baud} bps")
|
||||
elif conn_type == "TCP":
|
||||
ip = self.shared_config['ip_address_var'].get()
|
||||
port = self.shared_config['port_var'].get()
|
||||
self.net_info_var.set(f"TCP: {ip}:{port}")
|
||||
elif conn_type == "UDP":
|
||||
ip = self.shared_config['ip_address_var'].get()
|
||||
port = self.shared_config['port_var'].get()
|
||||
self.net_info_var.set(f"UDP: {ip}:{port}")
|
||||
|
||||
def log_message(self, message, tag="info", force=False):
|
||||
"""Log con formato especial para el sniffer"""
|
||||
# Verificar filtros
|
||||
if not force:
|
||||
if tag == "com_to_net" and not self.show_com_to_net_var.get():
|
||||
return
|
||||
if tag == "net_to_com" and not self.show_net_to_com_var.get():
|
||||
return
|
||||
|
||||
self.log_text.configure(state=tk.NORMAL)
|
||||
timestamp = datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
||||
|
||||
# Agregar prefijo según la dirección
|
||||
if tag == "com_to_net":
|
||||
prefix = "[COM→NET]"
|
||||
elif tag == "net_to_com":
|
||||
prefix = "[NET→COM]"
|
||||
elif tag == "error":
|
||||
prefix = "[ERROR] "
|
||||
elif tag == "parsed":
|
||||
prefix = "[PARSED] "
|
||||
else:
|
||||
prefix = "[INFO] "
|
||||
|
||||
full_message = f"[{timestamp}] {prefix} {message}\n"
|
||||
|
||||
# Insertar con color
|
||||
start_index = self.log_text.index(tk.END)
|
||||
self.log_text.insert(tk.END, full_message)
|
||||
end_index = self.log_text.index(tk.END)
|
||||
self.log_text.tag_add(tag, start_index, end_index)
|
||||
|
||||
self.log_text.see(tk.END)
|
||||
self.log_text.configure(state=tk.DISABLED)
|
||||
|
||||
def start_bridge(self):
|
||||
"""Inicia el gateway/bridge"""
|
||||
if self.bridging:
|
||||
messagebox.showwarning("Advertencia", "El gateway ya está activo.")
|
||||
return
|
||||
|
||||
# Actualizar info de red
|
||||
self.update_net_info()
|
||||
|
||||
# Validar configuración
|
||||
try:
|
||||
com_port = self.com_port_var.get()
|
||||
baud_rate = int(self.baud_rate_var.get())
|
||||
|
||||
if not com_port.upper().startswith('COM'):
|
||||
raise ValueError("Puerto COM inválido")
|
||||
if baud_rate <= 0:
|
||||
raise ValueError("Baud rate debe ser mayor que 0")
|
||||
|
||||
except ValueError as e:
|
||||
messagebox.showerror("Error", f"Configuración inválida: {e}")
|
||||
return
|
||||
|
||||
# Abrir conexión COM física
|
||||
try:
|
||||
self.com_connection.open_connection("Serial", {
|
||||
'port': com_port,
|
||||
'baud': baud_rate
|
||||
})
|
||||
self.log_message(f"Puerto COM abierto: {com_port} @ {baud_rate} bps")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error", f"No se pudo abrir puerto COM: {e}")
|
||||
return
|
||||
|
||||
# Abrir conexión de red
|
||||
try:
|
||||
# For the network side of the bridge, use shared connection settings
|
||||
net_conn_type_actual = self.shared_config['connection_type_var'].get()
|
||||
|
||||
current_shared_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(),
|
||||
}
|
||||
# The first argument to get_connection_params is the dictionary it will read from.
|
||||
net_conn_params = self.shared_config['config_manager'].get_connection_params(current_shared_config_values)
|
||||
|
||||
self.net_connection.open_connection(net_conn_type_actual, net_conn_params)
|
||||
self.log_message(f"Conexión {net_conn_type_actual} abierta: {self.net_info_var.get()}")
|
||||
except Exception as e:
|
||||
self.com_connection.close_connection()
|
||||
messagebox.showerror("Error", f"No se pudo abrir conexión de red: {e}")
|
||||
return
|
||||
|
||||
# Resetear estadísticas
|
||||
self.com_to_net_count = 0
|
||||
self.net_to_com_count = 0
|
||||
self.error_count = 0
|
||||
self.update_stats()
|
||||
|
||||
# Iniciar bridge
|
||||
self.bridging = True
|
||||
self.status_var.set("Conectado")
|
||||
self.status_label.config(foreground="green")
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.stop_button.config(state=tk.NORMAL)
|
||||
self._set_entries_state(tk.DISABLED)
|
||||
|
||||
self.bridge_thread = threading.Thread(target=self.run_bridge, daemon=True)
|
||||
self.bridge_thread.start()
|
||||
|
||||
self.log_message("Gateway iniciado - Modo bridge activo")
|
||||
|
||||
def stop_bridge(self):
|
||||
"""Detiene el gateway/bridge"""
|
||||
if not self.bridging:
|
||||
return
|
||||
|
||||
self.bridging = False
|
||||
|
||||
# Esperar a que termine el thread
|
||||
if self.bridge_thread and self.bridge_thread.is_alive():
|
||||
self.bridge_thread.join(timeout=2.0)
|
||||
|
||||
# Cerrar conexiones
|
||||
self.com_connection.close_connection()
|
||||
self.net_connection.close_connection()
|
||||
|
||||
self.status_var.set("Desconectado")
|
||||
self.status_label.config(foreground="black")
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
self.stop_button.config(state=tk.DISABLED)
|
||||
self._set_entries_state(tk.NORMAL)
|
||||
|
||||
self.log_message("Gateway detenido")
|
||||
self.log_message(f"Total transferencias - COM→NET: {self.com_to_net_count}, NET→COM: {self.net_to_com_count}, Errores: {self.error_count}")
|
||||
|
||||
def run_bridge(self):
|
||||
"""Thread principal del bridge"""
|
||||
com_buffer = ""
|
||||
net_buffer = ""
|
||||
|
||||
while self.bridging:
|
||||
try:
|
||||
# Leer del COM físico
|
||||
com_data = self.com_connection.read_data_non_blocking()
|
||||
if com_data:
|
||||
com_buffer += com_data
|
||||
|
||||
# Buscar mensajes completos para logging
|
||||
while '\r' in com_buffer or '\n' in com_buffer or len(com_buffer) >= 10:
|
||||
end_idx = self._find_message_end(com_buffer)
|
||||
if end_idx > 0:
|
||||
message = com_buffer[:end_idx]
|
||||
com_buffer = com_buffer[end_idx:]
|
||||
|
||||
# Log y parseo
|
||||
display_msg = ProtocolHandler.format_for_display(message)
|
||||
self.log_message(f"Data: {display_msg}", "com_to_net")
|
||||
|
||||
# Intentar parsear si está habilitado
|
||||
if self.show_parsed_var.get():
|
||||
parsed = ProtocolHandler.parse_adam_message(message)
|
||||
if parsed:
|
||||
# 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())
|
||||
brix_value = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix, max_brix)
|
||||
|
||||
self.log_message(
|
||||
f"ADAM - Addr: {parsed['address']}, "
|
||||
f"mA: {parsed['ma']:.3f}, "
|
||||
f"Brix: {brix_value:.3f}, "
|
||||
f"Checksum: {'OK' if parsed.get('checksum_valid', True) else 'ERROR'}",
|
||||
"parsed"
|
||||
)
|
||||
|
||||
# Reenviar a la red
|
||||
try:
|
||||
self.net_connection.send_data(message)
|
||||
self.com_to_net_count += 1
|
||||
self.update_stats()
|
||||
except Exception as e:
|
||||
self.log_message(f"Error enviando a red: {e}", "error")
|
||||
self.error_count += 1
|
||||
self.update_stats()
|
||||
else:
|
||||
break
|
||||
|
||||
# Leer de la red
|
||||
net_data = self.net_connection.read_data_non_blocking()
|
||||
if net_data:
|
||||
net_buffer += net_data
|
||||
|
||||
# Buscar mensajes completos para logging
|
||||
while '\r' in net_buffer or '\n' in net_buffer or len(net_buffer) >= 10:
|
||||
end_idx = self._find_message_end(net_buffer)
|
||||
if end_idx > 0:
|
||||
message = net_buffer[:end_idx]
|
||||
net_buffer = net_buffer[end_idx:]
|
||||
|
||||
# Log y parseo
|
||||
display_msg = ProtocolHandler.format_for_display(message)
|
||||
self.log_message(f"Data: {display_msg}", "net_to_com")
|
||||
|
||||
# Intentar parsear si está habilitado
|
||||
if self.show_parsed_var.get():
|
||||
parsed = ProtocolHandler.parse_adam_message(message)
|
||||
if parsed:
|
||||
# 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())
|
||||
brix_value = ProtocolHandler.ma_to_brix(parsed['ma'], min_brix, max_brix)
|
||||
|
||||
self.log_message(
|
||||
f"ADAM - Addr: {parsed['address']}, "
|
||||
f"mA: {parsed['ma']:.3f}, "
|
||||
f"Brix: {brix_value:.3f}, "
|
||||
f"Checksum: {'OK' if parsed.get('checksum_valid', True) else 'ERROR'}",
|
||||
"parsed"
|
||||
)
|
||||
|
||||
# Reenviar al COM
|
||||
try:
|
||||
self.com_connection.send_data(message)
|
||||
self.net_to_com_count += 1
|
||||
self.update_stats()
|
||||
except Exception as e:
|
||||
self.log_message(f"Error enviando a COM: {e}", "error")
|
||||
self.error_count += 1
|
||||
self.update_stats()
|
||||
else:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
if self.bridging:
|
||||
self.log_message(f"Error en bridge: {e}", "error")
|
||||
self.error_count += 1
|
||||
self.update_stats()
|
||||
break
|
||||
|
||||
# Pequeña pausa para no consumir demasiado CPU
|
||||
if not com_data and not net_data:
|
||||
time.sleep(0.001)
|
||||
|
||||
# Asegurar que el estado se actualice
|
||||
if not self.bridging:
|
||||
self.frame.after(0, self._ensure_stopped_state)
|
||||
|
||||
def _find_message_end(self, buffer):
|
||||
"""Encuentra el final de un mensaje en el buffer"""
|
||||
# Buscar terminadores
|
||||
for i, char in enumerate(buffer):
|
||||
if char in ['\r', '\n']:
|
||||
return i + 1
|
||||
|
||||
# Si no hay terminador pero el buffer es largo, buscar mensaje ADAM completo
|
||||
if len(buffer) >= 10:
|
||||
if buffer[0] == '#' or (buffer[2:8].replace('.', '').replace(' ', '').replace('-', '').isdigit()):
|
||||
# Parece un mensaje ADAM
|
||||
if len(buffer) > 10 and buffer[10] in ['\r', '\n']:
|
||||
return 11
|
||||
else:
|
||||
return 10
|
||||
|
||||
return -1
|
||||
|
||||
def update_stats(self):
|
||||
"""Actualiza las estadísticas en la GUI"""
|
||||
self.com_to_net_var.set(str(self.com_to_net_count))
|
||||
self.net_to_com_var.set(str(self.net_to_com_count))
|
||||
self.errors_var.set(str(self.error_count))
|
||||
|
||||
def clear_log(self):
|
||||
"""Limpia el log"""
|
||||
self.log_text.configure(state=tk.NORMAL)
|
||||
self.log_text.delete(1.0, tk.END)
|
||||
self.log_text.configure(state=tk.DISABLED)
|
||||
self.log_message("Log limpiado", force=True)
|
||||
|
||||
def _set_entries_state(self, state):
|
||||
"""Habilita/deshabilita los controles durante el bridge"""
|
||||
self.com_port_entry.config(state=state)
|
||||
self.baud_rate_entry.config(state=state)
|
||||
|
||||
# También deshabilitar controles compartidos
|
||||
if 'shared_widgets' in self.shared_config:
|
||||
Utils.set_widgets_state(self.shared_config['shared_widgets'], state)
|
||||
|
||||
def _ensure_stopped_state(self):
|
||||
"""Asegura que la GUI refleje el estado detenido"""
|
||||
self.status_var.set("Desconectado")
|
||||
self.status_label.config(foreground="black")
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
self.stop_button.config(state=tk.DISABLED)
|
||||
self._set_entries_state(tk.NORMAL)
|
||||
|
||||
def get_config(self):
|
||||
"""Obtiene la configuración actual del NetCom"""
|
||||
return {
|
||||
'netcom_com_port': self.com_port_var.get(),
|
||||
'netcom_baud_rate': self.baud_rate_var.get()
|
||||
}
|
||||
|
||||
def set_config(self, config):
|
||||
"""Establece la configuración del NetCom"""
|
||||
self.com_port_var.set(config.get('netcom_com_port', 'COM3'))
|
||||
self.baud_rate_var.set(config.get('netcom_baud_rate', '115200'))
|
|
@ -0,0 +1,461 @@
|
|||
"""
|
||||
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()
|
|
@ -0,0 +1,336 @@
|
|||
"""
|
||||
Tab del Trace - Escucha datos de un dispositivo Maselli real
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, scrolledtext, messagebox
|
||||
import threading
|
||||
import time
|
||||
import csv
|
||||
from collections import deque
|
||||
from datetime import datetime
|
||||
|
||||
from protocol_handler import ProtocolHandler
|
||||
from connection_manager import ConnectionManager
|
||||
from utils import Utils
|
||||
|
||||
class TraceTab:
|
||||
def __init__(self, parent_frame, shared_config):
|
||||
self.frame = parent_frame
|
||||
self.shared_config = shared_config
|
||||
|
||||
# Estado del trace
|
||||
self.tracing = False
|
||||
self.trace_thread = None
|
||||
self.connection_manager = ConnectionManager()
|
||||
|
||||
# Archivo CSV
|
||||
self.csv_file = None
|
||||
self.csv_writer = None
|
||||
|
||||
# Datos para el gráfico (ahora con mA también)
|
||||
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) # Nueva línea para mA
|
||||
self.start_time = time.time()
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
"""Crea los widgets del tab trace"""
|
||||
# Control Frame
|
||||
control_frame = ttk.LabelFrame(self.frame, text="Control Trace")
|
||||
control_frame.grid(row=0, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
|
||||
|
||||
self.start_button = ttk.Button(control_frame, text="Iniciar Trace", command=self.start_trace)
|
||||
self.start_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.stop_button = ttk.Button(control_frame, text="Detener Trace", command=self.stop_trace, state=tk.DISABLED)
|
||||
self.stop_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
self.clear_graph_button = ttk.Button(control_frame, text="Limpiar Gráfico", command=self.clear_graph)
|
||||
self.clear_graph_button.pack(side=tk.LEFT, padx=5)
|
||||
|
||||
ttk.Label(control_frame, text="Archivo CSV:").pack(side=tk.LEFT, padx=(20, 5))
|
||||
self.csv_filename_var = tk.StringVar(value="Sin archivo")
|
||||
ttk.Label(control_frame, textvariable=self.csv_filename_var).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
# Display Frame
|
||||
display_frame = ttk.LabelFrame(self.frame, text="Último Valor Recibido")
|
||||
display_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
|
||||
|
||||
ttk.Label(display_frame, text="Timestamp:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||
self.timestamp_var = tk.StringVar(value="---")
|
||||
ttk.Label(display_frame, textvariable=self.timestamp_var,
|
||||
font=("Courier", 12)).grid(row=0, column=1, padx=5, pady=5, sticky="w")
|
||||
|
||||
ttk.Label(display_frame, text="Dirección:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
|
||||
self.address_var = tk.StringVar(value="--")
|
||||
ttk.Label(display_frame, textvariable=self.address_var,
|
||||
font=("Courier", 12)).grid(row=0, column=3, padx=5, pady=5, sticky="w")
|
||||
|
||||
ttk.Label(display_frame, text="Valor mA:").grid(row=1, column=0, padx=5, pady=5, sticky="w")
|
||||
self.ma_var = tk.StringVar(value="---")
|
||||
ttk.Label(display_frame, textvariable=self.ma_var,
|
||||
font=("Courier", 12, "bold"), foreground="red").grid(row=1, column=1, padx=5, pady=5, sticky="w")
|
||||
|
||||
ttk.Label(display_frame, text="Valor Brix:").grid(row=1, column=2, padx=5, pady=5, sticky="w")
|
||||
self.brix_var = tk.StringVar(value="---")
|
||||
ttk.Label(display_frame, textvariable=self.brix_var,
|
||||
font=("Courier", 12, "bold"), foreground="blue").grid(row=1, column=3, padx=5, pady=5, sticky="w")
|
||||
|
||||
# Statistics Frame
|
||||
stats_frame = ttk.LabelFrame(self.frame, text="Estadísticas")
|
||||
stats_frame.grid(row=2, column=0, columnspan=2, padx=10, pady=5, sticky="ew")
|
||||
|
||||
ttk.Label(stats_frame, text="Mensajes recibidos:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||
self.msg_count_var = tk.StringVar(value="0")
|
||||
ttk.Label(stats_frame, textvariable=self.msg_count_var).grid(row=0, column=1, padx=5, pady=5, sticky="w")
|
||||
|
||||
ttk.Label(stats_frame, text="Errores checksum:").grid(row=0, column=2, padx=5, pady=5, sticky="w")
|
||||
self.checksum_errors_var = tk.StringVar(value="0")
|
||||
ttk.Label(stats_frame, textvariable=self.checksum_errors_var).grid(row=0, column=3, padx=5, pady=5, sticky="w")
|
||||
|
||||
# Log Frame
|
||||
log_frame = ttk.LabelFrame(self.frame, text="Log de Recepción")
|
||||
log_frame.grid(row=3, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
|
||||
|
||||
self.log_text = scrolledtext.ScrolledText(log_frame, height=10, 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(3, weight=1)
|
||||
|
||||
# Contadores
|
||||
self.message_count = 0
|
||||
self.checksum_error_count = 0
|
||||
|
||||
def get_graph_frame(self):
|
||||
"""Crea y retorna el frame para el gráfico"""
|
||||
graph_frame = ttk.LabelFrame(self.frame, text="Gráfico Trace (Brix y mA)")
|
||||
graph_frame.grid(row=4, column=0, columnspan=2, padx=10, pady=5, sticky="nsew")
|
||||
self.frame.rowconfigure(4, weight=1)
|
||||
return graph_frame
|
||||
|
||||
def start_trace(self):
|
||||
"""Inicia el modo trace"""
|
||||
if self.tracing:
|
||||
messagebox.showwarning("Advertencia", "El trace ya está en curso.")
|
||||
return
|
||||
|
||||
# Crear archivo CSV
|
||||
csv_filename = Utils.create_csv_filename("maselli_trace")
|
||||
try:
|
||||
self.csv_file = open(csv_filename, 'w', newline='', encoding='utf-8')
|
||||
self.csv_writer = csv.writer(self.csv_file)
|
||||
self.csv_writer.writerow(['Timestamp', 'Address', 'mA', 'Brix', 'Checksum_Valid', '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:
|
||||
# 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 trace.")
|
||||
except Exception as e:
|
||||
messagebox.showerror("Error de Conexión", str(e))
|
||||
if self.csv_file:
|
||||
self.csv_file.close()
|
||||
return
|
||||
|
||||
# Resetear contadores
|
||||
self.message_count = 0
|
||||
self.checksum_error_count = 0
|
||||
self.msg_count_var.set("0")
|
||||
self.checksum_errors_var.set("0")
|
||||
|
||||
self.tracing = True
|
||||
self.start_time = time.time()
|
||||
self.start_button.config(state=tk.DISABLED)
|
||||
self.stop_button.config(state=tk.NORMAL)
|
||||
self._set_entries_state(tk.DISABLED)
|
||||
|
||||
# Iniciar thread de recepción
|
||||
self.trace_thread = threading.Thread(target=self.run_trace, daemon=True)
|
||||
self.trace_thread.start()
|
||||
Utils.log_message(self.log_text, "Trace iniciado.")
|
||||
|
||||
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
|
||||
self.connection_manager.close_connection()
|
||||
Utils.log_message(self.log_text, "Conexión cerrada.")
|
||||
|
||||
# Cerrar archivo CSV
|
||||
if self.csv_file:
|
||||
self.csv_file.close()
|
||||
self.csv_file = None
|
||||
self.csv_writer = None
|
||||
Utils.log_message(self.log_text, f"Archivo CSV guardado: {self.csv_filename_var.get()}")
|
||||
|
||||
self.start_button.config(state=tk.NORMAL)
|
||||
self.stop_button.config(state=tk.DISABLED)
|
||||
self._set_entries_state(tk.NORMAL)
|
||||
|
||||
Utils.log_message(self.log_text, "Trace detenido.")
|
||||
Utils.log_message(self.log_text, f"Total mensajes: {self.message_count}, Errores checksum: {self.checksum_error_count}")
|
||||
|
||||
def run_trace(self):
|
||||
"""Thread principal para recepción de datos"""
|
||||
buffer = ""
|
||||
|
||||
while self.tracing:
|
||||
try:
|
||||
# Leer datos disponibles
|
||||
data = self.connection_manager.read_data_non_blocking()
|
||||
|
||||
if data:
|
||||
buffer += data
|
||||
|
||||
# Buscar mensajes completos
|
||||
while '\r' in buffer or '\n' in buffer or len(buffer) >= 10:
|
||||
# Encontrar el primer terminador
|
||||
end_idx = -1
|
||||
for i, char in enumerate(buffer):
|
||||
if char in ['\r', '\n']:
|
||||
end_idx = i + 1
|
||||
break
|
||||
|
||||
# Si no hay terminador pero el buffer es largo, buscar mensaje completo
|
||||
if end_idx == -1 and len(buffer) >= 10:
|
||||
# Verificar si hay un mensaje ADAM completo
|
||||
if buffer[0] == '#' or (len(buffer) >= 10 and buffer[2:8].replace('.', '').isdigit()):
|
||||
end_idx = 10 # Longitud mínima de un mensaje ADAM
|
||||
if len(buffer) > 10 and buffer[10] in ['\r', '\n']:
|
||||
end_idx = 11
|
||||
|
||||
if end_idx > 0:
|
||||
message = buffer[:end_idx]
|
||||
buffer = buffer[end_idx:]
|
||||
|
||||
# Procesar mensaje si tiene contenido
|
||||
if message.strip():
|
||||
self._process_message(message)
|
||||
else:
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
if self.tracing:
|
||||
Utils.log_message(self.log_text, f"Error en trace: {e}")
|
||||
break
|
||||
|
||||
# Pequeña pausa para no consumir demasiado CPU
|
||||
if not data:
|
||||
time.sleep(0.01)
|
||||
|
||||
def _process_message(self, message):
|
||||
"""Procesa un mensaje recibido"""
|
||||
# Log del mensaje raw
|
||||
display_msg = ProtocolHandler.format_for_display(message)
|
||||
Utils.log_message(self.log_text, f"Recibido: {display_msg}")
|
||||
|
||||
# Parsear mensaje
|
||||
parsed = ProtocolHandler.parse_adam_message(message)
|
||||
if parsed:
|
||||
# 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())
|
||||
|
||||
ma_value = parsed['ma']
|
||||
brix_value = ProtocolHandler.ma_to_brix(ma_value, min_brix, max_brix)
|
||||
timestamp = datetime.now()
|
||||
|
||||
# Actualizar contadores
|
||||
self.message_count += 1
|
||||
self.msg_count_var.set(str(self.message_count))
|
||||
|
||||
if not parsed.get('checksum_valid', True):
|
||||
self.checksum_error_count += 1
|
||||
self.checksum_errors_var.set(str(self.checksum_error_count))
|
||||
|
||||
# Actualizar display
|
||||
self.timestamp_var.set(timestamp.strftime("%H:%M:%S.%f")[:-3])
|
||||
self.address_var.set(parsed['address'])
|
||||
self.ma_var.set(Utils.format_ma_display(ma_value))
|
||||
self.brix_var.set(Utils.format_brix_display(brix_value))
|
||||
|
||||
# Log con detalles
|
||||
checksum_status = "OK" if parsed.get('checksum_valid', True) else "ERROR"
|
||||
Utils.log_message(self.log_text,
|
||||
f" -> Addr: {parsed['address']}, "
|
||||
f"mA: {ma_value:.3f}, "
|
||||
f"Brix: {brix_value:.3f}, "
|
||||
f"Checksum: {checksum_status}")
|
||||
|
||||
# Guardar en CSV
|
||||
if self.csv_writer:
|
||||
self.csv_writer.writerow([
|
||||
timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
|
||||
parsed['address'],
|
||||
f"{ma_value:.3f}",
|
||||
f"{brix_value:.3f}",
|
||||
parsed.get('checksum_valid', True),
|
||||
display_msg
|
||||
])
|
||||
if self.csv_file:
|
||||
self.csv_file.flush()
|
||||
|
||||
# Agregar 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) # Agregar también mA
|
||||
|
||||
# Actualizar gráfico
|
||||
if hasattr(self, 'graph_update_callback'):
|
||||
self.frame.after(0, self.graph_update_callback)
|
||||
else:
|
||||
# Mensaje no válido
|
||||
Utils.log_message(self.log_text, f" -> Mensaje no válido ADAM")
|
||||
|
||||
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 el trace"""
|
||||
# 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 (no hay configuración específica para trace)"""
|
||||
return {}
|
||||
|
||||
def set_config(self, config):
|
||||
"""Establece la configuración (no hay configuración específica para trace)"""
|
||||
pass
|
|
@ -0,0 +1,83 @@
|
|||
"""
|
||||
Utilidades comunes para el proyecto
|
||||
"""
|
||||
|
||||
import tkinter as tk
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
class Utils:
|
||||
@staticmethod
|
||||
def log_message(log_widget, message):
|
||||
"""Escribe un mensaje con timestamp en el widget de log especificado"""
|
||||
if log_widget:
|
||||
log_widget.configure(state=tk.NORMAL)
|
||||
timestamp = datetime.now().strftime('%H:%M:%S')
|
||||
log_widget.insert(tk.END, f"[{timestamp}] {message}\n")
|
||||
log_widget.see(tk.END)
|
||||
log_widget.configure(state=tk.DISABLED)
|
||||
|
||||
@staticmethod
|
||||
def load_icon(root):
|
||||
"""Intenta cargar un icono para la ventana"""
|
||||
icon_loaded = False
|
||||
for icon_file in ['icon.png', 'icon.ico', 'icon.gif']:
|
||||
if os.path.exists(icon_file):
|
||||
try:
|
||||
if icon_file.endswith('.ico'):
|
||||
root.iconbitmap(icon_file)
|
||||
else:
|
||||
icon = tk.PhotoImage(file=icon_file)
|
||||
root.iconphoto(True, icon)
|
||||
icon_loaded = True
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"No se pudo cargar {icon_file}: {e}")
|
||||
|
||||
if not icon_loaded:
|
||||
print("No se encontró ningún archivo de icono (icon.png, icon.ico, icon.gif)")
|
||||
|
||||
@staticmethod
|
||||
def create_csv_filename(prefix="maselli"):
|
||||
"""Crea un nombre de archivo CSV con timestamp"""
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
return f"{prefix}_{timestamp}.csv"
|
||||
|
||||
@staticmethod
|
||||
def validate_number_entry(value, min_val=None, max_val=None, is_float=True):
|
||||
"""Valida que una entrada sea un número válido dentro del rango especificado"""
|
||||
try:
|
||||
num = float(value) if is_float else int(value)
|
||||
if min_val is not None and num < min_val:
|
||||
return False
|
||||
if max_val is not None and num > max_val:
|
||||
return False
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def set_widgets_state(widgets, state):
|
||||
"""Establece el estado de múltiples widgets"""
|
||||
for widget in widgets:
|
||||
try:
|
||||
widget.config(state=state)
|
||||
except:
|
||||
pass # Algunos widgets pueden no tener la propiedad state
|
||||
|
||||
@staticmethod
|
||||
def format_brix_display(brix_value):
|
||||
"""Formatea un valor Brix para mostrar"""
|
||||
return f"{brix_value:.3f} Brix"
|
||||
|
||||
@staticmethod
|
||||
def format_ma_display(ma_value):
|
||||
"""Formatea un valor mA para mostrar"""
|
||||
return f"{ma_value:.3f} mA"
|
||||
|
||||
@staticmethod
|
||||
def clear_graph_data(*data_containers):
|
||||
"""Limpia los contenedores de datos del gráfico"""
|
||||
for container in data_containers:
|
||||
if hasattr(container, 'clear'):
|
||||
container.clear()
|
Loading…
Reference in New Issue