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__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
.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/
|
env/
|
||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
.venv
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
# IDEs
|
||||||
.spyderproject
|
.vscode/
|
||||||
.spyproject
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
# Rope project settings
|
# Archivos generados
|
||||||
.ropeproject
|
*.csv
|
||||||
|
*.log
|
||||||
|
|
||||||
# mkdocs documentation
|
# Archivos de configuración local (opcional, quitar si quieres versionar la config)
|
||||||
/site
|
# maselli_simulator_config.json
|
||||||
|
|
||||||
# mypy
|
# Sistema
|
||||||
.mypy_cache/
|
.DS_Store
|
||||||
.dmypy.json
|
Thumbs.db
|
||||||
dmypy.json
|
desktop.ini
|
||||||
|
|
||||||
# Pyre type checker
|
# Iconos (si son específicos del usuario)
|
||||||
.pyre/
|
# icon.png
|
||||||
|
# icon.ico
|
||||||
# pytype static type analyzer
|
# icon.gif
|
||||||
.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
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
from tkinter import ttk, scrolledtext, messagebox
|
||||||
import serial
|
import serial
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
@ -15,10 +15,45 @@ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
|
||||||
from matplotlib.figure import Figure
|
from matplotlib.figure import Figure
|
||||||
import matplotlib.animation as animation
|
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:
|
class MaselliSimulatorApp:
|
||||||
def __init__(self, root_window):
|
def __init__(self, root_window):
|
||||||
self.root = root_window
|
self.root = root_window
|
||||||
self.root.title("Simulador/Trace Protocolo Maselli")
|
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 = None
|
||||||
self.connection_type = None
|
self.connection_type = None
|
||||||
|
@ -338,39 +373,60 @@ class MaselliSimulatorApp:
|
||||||
log_widget.see(tk.END)
|
log_widget.see(tk.END)
|
||||||
log_widget.configure(state=tk.DISABLED)
|
log_widget.configure(state=tk.DISABLED)
|
||||||
|
|
||||||
def parse_adam_message(self, data):
|
def parse_adam_message(self, data, log_widget=None):
|
||||||
"""Parsea un mensaje del protocolo ADAM y retorna el valor en mA"""
|
"""
|
||||||
|
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:
|
try:
|
||||||
# Formato esperado: #AA[valor_mA][checksum]\r
|
# Formato esperado: #AA[valor_mA][checksum]\r
|
||||||
if not data.startswith('#') or not data.endswith('\r'):
|
# Pero también manejar respuestas sin # inicial o sin \r final
|
||||||
return None
|
data = data.strip()
|
||||||
|
|
||||||
# Remover # y \r
|
# Si empieza con #, es un mensaje estándar
|
||||||
data = data[1:-1]
|
if data.startswith('#'):
|
||||||
|
data = data[1:] # Remover #
|
||||||
|
|
||||||
# Los primeros 2 caracteres son la dirección
|
# Si termina con \r, removerlo
|
||||||
if len(data) < 9: # 2 addr + 6 valor + 2 checksum
|
if data.endswith('\r'):
|
||||||
|
data = data[:-1]
|
||||||
|
|
||||||
|
# Verificar longitud mínima
|
||||||
|
if len(data) < 8: # 2 addr + 6 valor mínimo
|
||||||
return None
|
return None
|
||||||
|
|
||||||
address = data[:2]
|
address = data[:2]
|
||||||
value_str = data[2:9] # 6 caracteres para el valor
|
value_str = data[2:8] # 6 caracteres para el valor (XX.XXX)
|
||||||
checksum = data[9:11] # 2 caracteres para checksum
|
|
||||||
|
|
||||||
# Verificar checksum
|
# Verificar si hay checksum
|
||||||
message_part = f"#{address}{value_str}"
|
if len(data) >= 10:
|
||||||
calculated_checksum = self.calculate_checksum(message_part)
|
checksum = data[8:10] # 2 caracteres para checksum
|
||||||
|
|
||||||
if checksum != calculated_checksum:
|
# Verificar checksum
|
||||||
self._log_message(f"Checksum incorrecto: recibido={checksum}, calculado={calculated_checksum}",
|
message_part = f"#{address}{value_str}"
|
||||||
self.trace_log_text)
|
calculated_checksum = self.calculate_checksum(message_part)
|
||||||
return None
|
|
||||||
|
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
|
# Convertir valor a float
|
||||||
ma_value = float(value_str)
|
try:
|
||||||
return {'address': address, 'ma': ma_value}
|
ma_value = float(value_str)
|
||||||
|
return {'address': address, 'ma': ma_value}
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
except Exception as e:
|
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
|
return None
|
||||||
|
|
||||||
def ma_to_brix(self, ma_value):
|
def ma_to_brix(self, ma_value):
|
||||||
|
@ -419,7 +475,7 @@ class MaselliSimulatorApp:
|
||||||
try:
|
try:
|
||||||
self.csv_file = open(csv_filename, 'w', newline='')
|
self.csv_file = open(csv_filename, 'w', newline='')
|
||||||
self.csv_writer = csv.writer(self.csv_file)
|
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)
|
self.csv_filename_var.set(csv_filename)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messagebox.showerror("Error", f"No se pudo crear archivo CSV: {e}")
|
messagebox.showerror("Error", f"No se pudo crear archivo CSV: {e}")
|
||||||
|
@ -492,6 +548,9 @@ class MaselliSimulatorApp:
|
||||||
self.connection.settimeout(0.1)
|
self.connection.settimeout(0.1)
|
||||||
try:
|
try:
|
||||||
data = self.connection.recv(1024).decode('ascii', errors='ignore')
|
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:
|
except socket.timeout:
|
||||||
continue
|
continue
|
||||||
elif self.connection_type == "UDP":
|
elif self.connection_type == "UDP":
|
||||||
|
@ -505,20 +564,42 @@ class MaselliSimulatorApp:
|
||||||
if data:
|
if data:
|
||||||
buffer += data
|
buffer += data
|
||||||
|
|
||||||
# Buscar mensajes completos (terminan con \r)
|
# Buscar mensajes completos (terminan con \r o \n)
|
||||||
while '\r' in buffer:
|
while '\r' in buffer or '\n' in buffer:
|
||||||
end_idx = buffer.index('\r') + 1
|
# Encontrar el primer terminador
|
||||||
message = buffer[:end_idx]
|
end_idx = len(buffer)
|
||||||
buffer = buffer[end_idx:]
|
for term in ['\r', '\n']:
|
||||||
|
if term in buffer:
|
||||||
|
idx = buffer.index(term) + 1
|
||||||
|
if idx < end_idx:
|
||||||
|
end_idx = idx
|
||||||
|
|
||||||
# Procesar mensaje
|
if end_idx > 0:
|
||||||
self._process_trace_message(message)
|
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:
|
except Exception as e:
|
||||||
self._log_message(f"Error en trace: {e}", self.trace_log_text)
|
if self.tracing: # Solo loguear si todavía estamos en trace
|
||||||
if not self.tracing:
|
self._log_message(f"Error en trace: {e}", self.trace_log_text)
|
||||||
break
|
break
|
||||||
time.sleep(0.1)
|
|
||||||
|
# Pequeña pausa para no consumir demasiado CPU
|
||||||
|
if not data:
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
def _process_trace_message(self, message):
|
def _process_trace_message(self, message):
|
||||||
"""Procesa un mensaje recibido en modo trace"""
|
"""Procesa un mensaje recibido en modo trace"""
|
||||||
|
@ -527,7 +608,7 @@ class MaselliSimulatorApp:
|
||||||
self._log_message(f"Recibido: {display_msg}", self.trace_log_text)
|
self._log_message(f"Recibido: {display_msg}", self.trace_log_text)
|
||||||
|
|
||||||
# Parsear mensaje
|
# Parsear mensaje
|
||||||
parsed = self.parse_adam_message(message)
|
parsed = self.parse_adam_message(message, self.trace_log_text)
|
||||||
if parsed:
|
if parsed:
|
||||||
ma_value = parsed['ma']
|
ma_value = parsed['ma']
|
||||||
brix_value = self.ma_to_brix(ma_value)
|
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_ma_var.set(f"{ma_value:.3f} mA")
|
||||||
self.trace_brix_var.set(f"{brix_value:.3f} Brix")
|
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
|
# Guardar en CSV
|
||||||
if self.csv_writer:
|
if self.csv_writer:
|
||||||
self.csv_writer.writerow([
|
self.csv_writer.writerow([
|
||||||
timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
|
timestamp.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3],
|
||||||
ma_value,
|
parsed['address'],
|
||||||
brix_value,
|
f"{ma_value:.3f}",
|
||||||
|
f"{brix_value:.3f}",
|
||||||
display_msg
|
display_msg
|
||||||
])
|
])
|
||||||
if self.csv_file:
|
if self.csv_file:
|
||||||
|
@ -556,6 +642,9 @@ class MaselliSimulatorApp:
|
||||||
|
|
||||||
# Actualizar gráfico
|
# Actualizar gráfico
|
||||||
self.root.after(0, self.trace_canvas.draw_idle)
|
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):
|
def _set_trace_entries_state(self, state):
|
||||||
"""Habilita/deshabilita controles durante el trace"""
|
"""Habilita/deshabilita controles durante el trace"""
|
||||||
|
@ -746,6 +835,7 @@ class MaselliSimulatorApp:
|
||||||
return mA_value
|
return mA_value
|
||||||
|
|
||||||
def format_mA_value(self, mA_val):
|
def format_mA_value(self, mA_val):
|
||||||
|
# Formato: "XX.XXX" (6 caracteres incluyendo el punto)
|
||||||
return f"{mA_val:06.3f}"
|
return f"{mA_val:06.3f}"
|
||||||
|
|
||||||
def _get_common_params(self):
|
def _get_common_params(self):
|
||||||
|
@ -822,6 +912,50 @@ class MaselliSimulatorApp:
|
||||||
self.sim_ma_data.append(ma_value)
|
self.sim_ma_data.append(ma_value)
|
||||||
self.sim_canvas.draw_idle()
|
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):
|
def send_manual_value(self):
|
||||||
common_params = self._get_common_params()
|
common_params = self._get_common_params()
|
||||||
if not 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"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._log_message(f"Enviando Manual: {log_display_string}", self.sim_log_text)
|
||||||
self._send_data(temp_connection, conn_type, full_string_to_send)
|
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:
|
except Exception as e:
|
||||||
self._log_message(f"Error al enviar manualmente: {e}", self.sim_log_text)
|
self._log_message(f"Error al enviar manualmente: {e}", self.sim_log_text)
|
||||||
messagebox.showerror("Error de Conexión", str(e))
|
messagebox.showerror("Error de Conexión", str(e))
|
||||||
|
@ -998,6 +1146,20 @@ class MaselliSimulatorApp:
|
||||||
if self.connection:
|
if self.connection:
|
||||||
try:
|
try:
|
||||||
self._send_data(self.connection, self.connection_type, full_string_to_send)
|
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:
|
except Exception as e:
|
||||||
self._log_message(f"Error al escribir en conexión (sim): {e}", self.sim_log_text)
|
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)
|
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)
|
self._log_message("Simulación continua terminada (hilo finalizado).", self.sim_log_text)
|
||||||
|
|
||||||
def on_closing(self):
|
def on_closing(self):
|
||||||
|
"""Maneja el cierre de la aplicación"""
|
||||||
|
# Detener simulación si está activa
|
||||||
if self.simulating:
|
if self.simulating:
|
||||||
self.stop_simulation()
|
self.stop_simulation()
|
||||||
|
|
||||||
|
# Detener trace si está activo
|
||||||
if self.tracing:
|
if self.tracing:
|
||||||
self.stop_trace()
|
self.stop_trace()
|
||||||
elif self.connection:
|
|
||||||
|
# Cerrar cualquier conexión abierta
|
||||||
|
if self.connection:
|
||||||
self._close_connection(self.connection, self.connection_type)
|
self._close_connection(self.connection, self.connection_type)
|
||||||
|
|
||||||
|
# Destruir ventana
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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",
|
"baud_rate": "115200",
|
||||||
"ip_address": "10.1.33.18",
|
"ip_address": "10.1.33.18",
|
||||||
"port": "8899",
|
"port": "8899",
|
||||||
"adam_address": "01",
|
|
||||||
"function_type": "Sinusoidal",
|
|
||||||
"min_brix_map": "0",
|
"min_brix_map": "0",
|
||||||
"max_brix_map": "80",
|
"max_brix_map": "80",
|
||||||
"period": "0.5",
|
"adam_address": "01",
|
||||||
"manual_brix": "10.0"
|
"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