Primera Version
This commit is contained in:
commit
a1f582e402
|
@ -0,0 +1,200 @@
|
|||
import snap7
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any
|
||||
import struct
|
||||
|
||||
class PLCDataStreamer:
|
||||
def __init__(self, plc_ip: str, plc_rack: int = 0, plc_slot: int = 2,
|
||||
udp_host: str = "127.0.0.1", udp_port: int = 9870):
|
||||
"""
|
||||
Inicializa el streamer de datos del PLC
|
||||
|
||||
Args:
|
||||
plc_ip: IP del PLC S7-315
|
||||
plc_rack: Rack del PLC (típicamente 0)
|
||||
plc_slot: Slot del PLC (típicamente 2)
|
||||
udp_host: IP para el servidor UDP
|
||||
udp_port: Puerto UDP para PlotJuggler
|
||||
"""
|
||||
self.plc_ip = plc_ip
|
||||
self.plc_rack = plc_rack
|
||||
self.plc_slot = plc_slot
|
||||
self.udp_host = udp_host
|
||||
self.udp_port = udp_port
|
||||
|
||||
# Inicializar cliente PLC
|
||||
self.plc = snap7.client.Client()
|
||||
|
||||
# Inicializar socket UDP
|
||||
self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
|
||||
# Configurar logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.FileHandler('plc_data.log'),
|
||||
logging.StreamHandler()
|
||||
]
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Variables a leer del PLC (personalizar según tu aplicación)
|
||||
self.data_blocks = {
|
||||
'DB1': {
|
||||
'address': 1,
|
||||
'size': 100,
|
||||
'variables': {
|
||||
'temperatura': {'offset': 0, 'type': 'real'},
|
||||
'presion': {'offset': 4, 'type': 'real'},
|
||||
'nivel': {'offset': 8, 'type': 'real'},
|
||||
'estado_bomba': {'offset': 12, 'type': 'bool'},
|
||||
'contador': {'offset': 14, 'type': 'int'}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.running = False
|
||||
|
||||
def connect_plc(self) -> bool:
|
||||
"""Conecta al PLC S7-315"""
|
||||
try:
|
||||
self.plc.connect(self.plc_ip, self.plc_rack, self.plc_slot)
|
||||
self.logger.info(f"Conectado al PLC {self.plc_ip}")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error conectando al PLC: {e}")
|
||||
return False
|
||||
|
||||
def disconnect_plc(self):
|
||||
"""Desconecta del PLC"""
|
||||
try:
|
||||
self.plc.disconnect()
|
||||
self.logger.info("Desconectado del PLC")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error desconectando del PLC: {e}")
|
||||
|
||||
def read_data_block(self, db_name: str) -> Dict[str, Any]:
|
||||
"""Lee un bloque de datos del PLC"""
|
||||
db_config = self.data_blocks[db_name]
|
||||
|
||||
try:
|
||||
# Leer el bloque completo
|
||||
raw_data = self.plc.db_read(db_config['address'], 0, db_config['size'])
|
||||
|
||||
parsed_data = {}
|
||||
|
||||
# Parsear cada variable según su tipo
|
||||
for var_name, var_config in db_config['variables'].items():
|
||||
offset = var_config['offset']
|
||||
var_type = var_config['type']
|
||||
|
||||
if var_type == 'real':
|
||||
# REAL de Siemens (32-bit IEEE 754, big endian)
|
||||
value = struct.unpack('>f', raw_data[offset:offset+4])[0]
|
||||
elif var_type == 'int':
|
||||
# INT de Siemens (16-bit signed, big endian)
|
||||
value = struct.unpack('>h', raw_data[offset:offset+2])[0]
|
||||
elif var_type == 'bool':
|
||||
# BOOL - primer bit del byte
|
||||
value = bool(raw_data[offset] & 0x01)
|
||||
else:
|
||||
continue
|
||||
|
||||
parsed_data[var_name] = value
|
||||
|
||||
return parsed_data
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error leyendo {db_name}: {e}")
|
||||
return {}
|
||||
|
||||
def send_to_plotjuggler(self, data: Dict[str, Any]):
|
||||
"""Envía datos a PlotJuggler vía UDP JSON"""
|
||||
try:
|
||||
# Agregar timestamp
|
||||
message = {
|
||||
'timestamp': time.time(),
|
||||
'data': data
|
||||
}
|
||||
|
||||
# Convertir a JSON y enviar
|
||||
json_message = json.dumps(message)
|
||||
self.udp_socket.sendto(
|
||||
json_message.encode('utf-8'),
|
||||
(self.udp_host, self.udp_port)
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error enviando datos a PlotJuggler: {e}")
|
||||
|
||||
def log_data(self, data: Dict[str, Any]):
|
||||
"""Registra datos en el log"""
|
||||
timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
|
||||
self.logger.info(f"[{timestamp}] {data}")
|
||||
|
||||
def run(self, sampling_interval: float = 0.1):
|
||||
"""Ejecuta el bucle principal de lectura y streaming"""
|
||||
if not self.connect_plc():
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.logger.info(f"Iniciando streaming con intervalo de {sampling_interval}s")
|
||||
|
||||
try:
|
||||
while self.running:
|
||||
start_time = time.time()
|
||||
|
||||
# Leer todos los bloques de datos configurados
|
||||
all_data = {}
|
||||
for db_name in self.data_blocks:
|
||||
db_data = self.read_data_block(db_name)
|
||||
all_data.update(db_data)
|
||||
|
||||
if all_data:
|
||||
# Enviar a PlotJuggler
|
||||
self.send_to_plotjuggler(all_data)
|
||||
|
||||
# Registrar en log
|
||||
self.log_data(all_data)
|
||||
|
||||
# Mantener intervalo de muestreo
|
||||
elapsed = time.time() - start_time
|
||||
sleep_time = max(0, sampling_interval - elapsed)
|
||||
time.sleep(sleep_time)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
self.logger.info("Deteniendo por interrupción del usuario")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error en el bucle principal: {e}")
|
||||
finally:
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
"""Detiene el streaming y limpia recursos"""
|
||||
self.running = False
|
||||
self.disconnect_plc()
|
||||
self.udp_socket.close()
|
||||
self.logger.info("Streaming detenido")
|
||||
|
||||
def main():
|
||||
# Configuración
|
||||
PLC_IP = "192.168.1.100" # Cambiar por la IP de tu PLC
|
||||
SAMPLING_INTERVAL = 0.1 # 10 Hz
|
||||
|
||||
# Crear y ejecutar el streamer
|
||||
streamer = PLCDataStreamer(plc_ip=PLC_IP)
|
||||
|
||||
try:
|
||||
streamer.run(sampling_interval=SAMPLING_INTERVAL)
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
streamer.stop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,203 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[codz]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
#poetry.toml
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
||||
#pdm.lock
|
||||
#pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# pixi
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
||||
#pixi.lock
|
||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
||||
.pixi
|
||||
|
||||
# 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
|
||||
.envrc
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Abstra
|
||||
# Abstra is an AI-powered process automation framework.
|
||||
# Ignore directories containing user credentials, local state, and settings.
|
||||
# Learn more at https://abstra.io/docs
|
||||
.abstra/
|
||||
|
||||
# Visual Studio Code
|
||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
||||
# you could uncomment the following to ignore the entire vscode folder
|
||||
# .vscode/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Marimo
|
||||
marimo/_static/
|
||||
marimo/_lsp/
|
||||
__marimo__/
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
|
@ -0,0 +1,184 @@
|
|||
# PLC S7-315 Streamer & Logger
|
||||
|
||||
Sistema web para monitoreo y streaming en tiempo real de datos de PLC Siemens S7-315 hacia PlotJuggler.
|
||||
|
||||
## 🚀 Características
|
||||
|
||||
- **Interfaz Web Moderna**: Control completo desde navegador
|
||||
- **Configuración Dinámica**: PLC, UDP y variables configurables en tiempo real
|
||||
- **Streaming en Vivo**: Datos enviados a PlotJuggler vía UDP JSON
|
||||
- **Logging Completo**: Registro de todas las operaciones y datos
|
||||
- **Soporte Multiples Tipos**: REAL, INT, DINT, BOOL
|
||||
- **Estado en Tiempo Real**: Monitoreo del estado de conexión y streaming
|
||||
|
||||
## 📋 Requisitos
|
||||
|
||||
### Hardware
|
||||
- PLC Siemens S7-315 conectado a la red
|
||||
- PC con Windows y miniconda
|
||||
|
||||
### Software
|
||||
- Python 3.8+
|
||||
- PlotJuggler (para visualización de datos)
|
||||
- Bibliotecas snap7 de Siemens
|
||||
|
||||
## 🛠️ Instalación
|
||||
|
||||
### 1. Clonar o descargar el proyecto
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd S7_snap7_Stremer_n_Log
|
||||
```
|
||||
|
||||
### 2. Crear entorno virtual con miniconda
|
||||
```bash
|
||||
conda create -n plc_streamer python=3.10
|
||||
conda activate plc_streamer
|
||||
```
|
||||
|
||||
### 3. Instalar dependencias
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 4. Instalar bibliotecas snap7 de Siemens
|
||||
- Descargar snap7 library desde: https://snap7.sourceforge.net/
|
||||
- Extraer y copiar `snap7.dll` a la carpeta del sistema o proyecto
|
||||
- En Windows, típicamente en `C:\Windows\System32\` o en el directorio del proyecto
|
||||
|
||||
## 🎯 Uso
|
||||
|
||||
### 1. Iniciar el servidor
|
||||
```bash
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 2. Acceder a la interfaz web
|
||||
Abrir navegador en: `http://localhost:5000`
|
||||
|
||||
### 3. Configuración del PLC
|
||||
1. Ir a la sección "Configuración PLC S7-315"
|
||||
2. Introducir:
|
||||
- **IP del PLC**: Dirección IP del PLC (ej: 192.168.1.100)
|
||||
- **Rack**: Número de rack (típicamente 0)
|
||||
- **Slot**: Número de slot (típicamente 2)
|
||||
3. Hacer clic en "Guardar Configuración"
|
||||
4. Hacer clic en "Conectar PLC"
|
||||
|
||||
### 4. Configurar Gateway UDP para PlotJuggler
|
||||
1. Ir a la sección "Configuración Gateway UDP"
|
||||
2. Configurar:
|
||||
- **Host UDP**: 127.0.0.1 (localhost)
|
||||
- **Puerto UDP**: 9870 (puerto por defecto de PlotJuggler)
|
||||
- **Intervalo de Muestreo**: Tiempo entre lecturas (ej: 0.1s = 10Hz)
|
||||
3. Hacer clic en "Guardar Configuración"
|
||||
|
||||
### 5. Añadir Variables del PLC
|
||||
1. Ir a la sección "Variables del PLC"
|
||||
2. Para cada variable añadir:
|
||||
- **Nombre Variable**: Nombre descriptivo (ej: temperatura)
|
||||
- **Data Block (DB)**: Número del DB (ej: 1 para DB1)
|
||||
- **Offset**: Posición en bytes dentro del DB
|
||||
- **Tipo de Dato**: REAL, INT, DINT, o BOOL
|
||||
3. Hacer clic en "Añadir Variable"
|
||||
|
||||
### 6. Iniciar Streaming
|
||||
1. Ir a la sección "Control de Streaming"
|
||||
2. Hacer clic en "Iniciar Streaming"
|
||||
3. Los datos se enviarán automáticamente a PlotJuggler
|
||||
|
||||
## 📊 Configuración de PlotJuggler
|
||||
|
||||
### 1. Instalar PlotJuggler
|
||||
- Descargar desde: https://github.com/facontidavide/PlotJuggler
|
||||
- Instalar siguiendo las instrucciones
|
||||
|
||||
### 2. Configurar recepción UDP
|
||||
1. Abrir PlotJuggler
|
||||
2. Ir a: `Streaming` → `Start streaming`
|
||||
3. Seleccionar `UDP Server`
|
||||
4. Configurar:
|
||||
- **Port**: 9870 (mismo que en la configuración)
|
||||
- **Protocol**: JSON
|
||||
5. Hacer clic en `Start`
|
||||
|
||||
### 3. Visualizar datos
|
||||
- Los datos aparecerán automáticamente en el panel izquierdo
|
||||
- Arrastrar variables al área de gráficos para visualizar
|
||||
|
||||
## 🏗️ Estructura del Proyecto
|
||||
|
||||
```
|
||||
S7_snap7_Stremer_n_Log/
|
||||
├── main.py # Aplicación Flask principal
|
||||
├── templates/
|
||||
│ └── index.html # Interfaz web
|
||||
├── requirements.txt # Dependencias Python
|
||||
├── README.md # Este archivo
|
||||
├── plc_data.log # Log de datos (generado automáticamente)
|
||||
└── .doc/
|
||||
└── example.py # Ejemplo de referencia
|
||||
```
|
||||
|
||||
## 🔧 API Endpoints
|
||||
|
||||
La aplicación expone los siguientes endpoints:
|
||||
|
||||
- `POST /api/plc/config` - Actualizar configuración PLC
|
||||
- `POST /api/udp/config` - Actualizar configuración UDP
|
||||
- `POST /api/plc/connect` - Conectar al PLC
|
||||
- `POST /api/plc/disconnect` - Desconectar del PLC
|
||||
- `POST /api/variables` - Añadir variable
|
||||
- `DELETE /api/variables/<name>` - Eliminar variable
|
||||
- `POST /api/streaming/start` - Iniciar streaming
|
||||
- `POST /api/streaming/stop` - Detener streaming
|
||||
- `POST /api/sampling` - Actualizar intervalo de muestreo
|
||||
- `GET /api/status` - Obtener estado actual
|
||||
|
||||
## 📝 Logging
|
||||
|
||||
El sistema genera dos tipos de logs:
|
||||
|
||||
1. **Log de aplicación**: Eventos del sistema, conexiones, errores
|
||||
2. **Log de datos**: Valores de variables con timestamp
|
||||
|
||||
Los logs se guardan en `plc_data.log` y también se muestran en consola.
|
||||
|
||||
## 🛡️ Consideraciones de Seguridad
|
||||
|
||||
- El servidor Flask está configurado para desarrollo (`debug=True`)
|
||||
- Para producción, desactivar modo debug y configurar adecuadamente
|
||||
- Verificar que la red del PLC sea segura
|
||||
- Utilizar firewalls apropiados
|
||||
|
||||
## 🐛 Solución de Problemas
|
||||
|
||||
### Error de conexión al PLC
|
||||
- Verificar IP, rack y slot del PLC
|
||||
- Comprobar conectividad de red (`ping <ip_plc>`)
|
||||
- Verificar que el PLC esté configurado para permitir conexiones Ethernet
|
||||
|
||||
### Error snap7.dll
|
||||
- Descargar snap7 library desde el sitio oficial
|
||||
- Copiar `snap7.dll` al directorio del sistema o proyecto
|
||||
|
||||
### PlotJuggler no recibe datos
|
||||
- Verificar puerto UDP (debe coincidir en ambas aplicaciones)
|
||||
- Comprobar firewall de Windows
|
||||
- Verificar que PlotJuggler esté configurado para JSON
|
||||
|
||||
### Variables no se leen correctamente
|
||||
- Verificar offset y tipo de dato
|
||||
- Confirmar estructura del Data Block en el PLC
|
||||
- Comprobar que el DB existe y tiene el tamaño suficiente
|
||||
|
||||
## 📞 Soporte
|
||||
|
||||
Para problemas o mejoras, revisar:
|
||||
- Logs en `plc_data.log`
|
||||
- Consola del servidor Flask
|
||||
- Herramientas de desarrollo del navegador
|
||||
|
||||
## 📄 Licencia
|
||||
|
||||
Este proyecto está destinado para uso interno y educativo.
|
|
@ -0,0 +1,389 @@
|
|||
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
||||
import snap7
|
||||
import json
|
||||
import socket
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Dict, Any, Optional
|
||||
import struct
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "plc_streamer_secret_key"
|
||||
|
||||
|
||||
class PLCDataStreamer:
|
||||
def __init__(self):
|
||||
"""Inicializa el streamer de datos del PLC"""
|
||||
# Configuración por defectoclear
|
||||
self.plc_config = {"ip": "192.168.1.100", "rack": 0, "slot": 2}
|
||||
|
||||
self.udp_config = {"host": "127.0.0.1", "port": 9870}
|
||||
|
||||
# Variables configurables
|
||||
self.variables = {}
|
||||
|
||||
# Estados
|
||||
self.plc = None
|
||||
self.udp_socket = None
|
||||
self.connected = False
|
||||
self.streaming = False
|
||||
self.stream_thread = None
|
||||
self.sampling_interval = 0.1
|
||||
|
||||
# Configurar logging
|
||||
self.setup_logging()
|
||||
|
||||
def setup_logging(self):
|
||||
"""Configura el sistema de logging"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
handlers=[logging.FileHandler("plc_data.log"), logging.StreamHandler()],
|
||||
)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def update_plc_config(self, ip: str, rack: int, slot: int):
|
||||
"""Actualiza la configuración del PLC"""
|
||||
self.plc_config = {"ip": ip, "rack": rack, "slot": slot}
|
||||
self.logger.info(f"Configuración PLC actualizada: {self.plc_config}")
|
||||
|
||||
def update_udp_config(self, host: str, port: int):
|
||||
"""Actualiza la configuración UDP"""
|
||||
self.udp_config = {"host": host, "port": port}
|
||||
self.logger.info(f"Configuración UDP actualizada: {self.udp_config}")
|
||||
|
||||
def add_variable(self, name: str, db: int, offset: int, var_type: str):
|
||||
"""Añade una variable para polling"""
|
||||
self.variables[name] = {"db": db, "offset": offset, "type": var_type}
|
||||
self.logger.info(f"Variable añadida: {name} -> DB{db}.{offset} ({var_type})")
|
||||
|
||||
def remove_variable(self, name: str):
|
||||
"""Elimina una variable del polling"""
|
||||
if name in self.variables:
|
||||
del self.variables[name]
|
||||
self.logger.info(f"Variable eliminada: {name}")
|
||||
|
||||
def connect_plc(self) -> bool:
|
||||
"""Conecta al PLC S7-315"""
|
||||
try:
|
||||
if self.plc:
|
||||
self.plc.disconnect()
|
||||
|
||||
self.plc = snap7.client.Client()
|
||||
self.plc.connect(
|
||||
self.plc_config["ip"], self.plc_config["rack"], self.plc_config["slot"]
|
||||
)
|
||||
|
||||
self.connected = True
|
||||
self.logger.info(f"Conectado al PLC {self.plc_config['ip']}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.connected = False
|
||||
self.logger.error(f"Error conectando al PLC: {e}")
|
||||
return False
|
||||
|
||||
def disconnect_plc(self):
|
||||
"""Desconecta del PLC"""
|
||||
try:
|
||||
if self.plc:
|
||||
self.plc.disconnect()
|
||||
self.connected = False
|
||||
self.logger.info("Desconectado del PLC")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error desconectando del PLC: {e}")
|
||||
|
||||
def read_variable(self, var_config: Dict[str, Any]) -> Any:
|
||||
"""Lee una variable específica del PLC"""
|
||||
try:
|
||||
db = var_config["db"]
|
||||
offset = var_config["offset"]
|
||||
var_type = var_config["type"]
|
||||
|
||||
if var_type == "real":
|
||||
raw_data = self.plc.db_read(db, offset, 4)
|
||||
value = struct.unpack(">f", raw_data)[0]
|
||||
elif var_type == "int":
|
||||
raw_data = self.plc.db_read(db, offset, 2)
|
||||
value = struct.unpack(">h", raw_data)[0]
|
||||
elif var_type == "bool":
|
||||
raw_data = self.plc.db_read(db, offset, 1)
|
||||
value = bool(raw_data[0] & 0x01)
|
||||
elif var_type == "dint":
|
||||
raw_data = self.plc.db_read(db, offset, 4)
|
||||
value = struct.unpack(">l", raw_data)[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
return value
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error leyendo variable: {e}")
|
||||
return None
|
||||
|
||||
def read_all_variables(self) -> Dict[str, Any]:
|
||||
"""Lee todas las variables configuradas"""
|
||||
if not self.connected or not self.plc:
|
||||
return {}
|
||||
|
||||
data = {}
|
||||
for var_name, var_config in self.variables.items():
|
||||
value = self.read_variable(var_config)
|
||||
if value is not None:
|
||||
data[var_name] = value
|
||||
|
||||
return data
|
||||
|
||||
def setup_udp_socket(self) -> bool:
|
||||
"""Configura el socket UDP"""
|
||||
try:
|
||||
if self.udp_socket:
|
||||
self.udp_socket.close()
|
||||
|
||||
self.udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.logger.info(
|
||||
f"Socket UDP configurado para {self.udp_config['host']}:{self.udp_config['port']}"
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error configurando socket UDP: {e}")
|
||||
return False
|
||||
|
||||
def send_to_plotjuggler(self, data: Dict[str, Any]):
|
||||
"""Envía datos a PlotJuggler vía UDP JSON"""
|
||||
if not self.udp_socket:
|
||||
return
|
||||
|
||||
try:
|
||||
message = {"timestamp": time.time(), "data": data}
|
||||
|
||||
json_message = json.dumps(message)
|
||||
self.udp_socket.sendto(
|
||||
json_message.encode("utf-8"),
|
||||
(self.udp_config["host"], self.udp_config["port"]),
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error enviando datos a PlotJuggler: {e}")
|
||||
|
||||
def streaming_loop(self):
|
||||
"""Bucle principal de streaming"""
|
||||
self.logger.info(
|
||||
f"Iniciando streaming con intervalo de {self.sampling_interval}s"
|
||||
)
|
||||
|
||||
while self.streaming:
|
||||
try:
|
||||
start_time = time.time()
|
||||
|
||||
# Leer todas las variables
|
||||
data = self.read_all_variables()
|
||||
|
||||
if data:
|
||||
# Enviar a PlotJuggler
|
||||
self.send_to_plotjuggler(data)
|
||||
|
||||
# Log de datos
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
self.logger.info(f"[{timestamp}] {data}")
|
||||
|
||||
# Mantener intervalo de muestreo
|
||||
elapsed = time.time() - start_time
|
||||
sleep_time = max(0, self.sampling_interval - elapsed)
|
||||
time.sleep(sleep_time)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error en streaming loop: {e}")
|
||||
break
|
||||
|
||||
def start_streaming(self) -> bool:
|
||||
"""Inicia el streaming de datos"""
|
||||
if not self.connected:
|
||||
self.logger.error("PLC no conectado")
|
||||
return False
|
||||
|
||||
if not self.variables:
|
||||
self.logger.error("No hay variables configuradas")
|
||||
return False
|
||||
|
||||
if not self.setup_udp_socket():
|
||||
return False
|
||||
|
||||
self.streaming = True
|
||||
self.stream_thread = threading.Thread(target=self.streaming_loop)
|
||||
self.stream_thread.daemon = True
|
||||
self.stream_thread.start()
|
||||
|
||||
self.logger.info("Streaming iniciado")
|
||||
return True
|
||||
|
||||
def stop_streaming(self):
|
||||
"""Detiene el streaming"""
|
||||
self.streaming = False
|
||||
if self.stream_thread:
|
||||
self.stream_thread.join(timeout=2)
|
||||
|
||||
if self.udp_socket:
|
||||
self.udp_socket.close()
|
||||
self.udp_socket = None
|
||||
|
||||
self.logger.info("Streaming detenido")
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Obtiene el estado actual del sistema"""
|
||||
return {
|
||||
"plc_connected": self.connected,
|
||||
"streaming": self.streaming,
|
||||
"plc_config": self.plc_config,
|
||||
"udp_config": self.udp_config,
|
||||
"variables_count": len(self.variables),
|
||||
"sampling_interval": self.sampling_interval,
|
||||
}
|
||||
|
||||
|
||||
# Instancia global del streamer
|
||||
streamer = PLCDataStreamer()
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""Página principal"""
|
||||
return render_template(
|
||||
"index.html", status=streamer.get_status(), variables=streamer.variables
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/plc/config", methods=["POST"])
|
||||
def update_plc_config():
|
||||
"""Actualiza la configuración del PLC"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
ip = data.get("ip", "192.168.1.100")
|
||||
rack = int(data.get("rack", 0))
|
||||
slot = int(data.get("slot", 2))
|
||||
|
||||
streamer.update_plc_config(ip, rack, slot)
|
||||
return jsonify({"success": True, "message": "Configuración PLC actualizada"})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/udp/config", methods=["POST"])
|
||||
def update_udp_config():
|
||||
"""Actualiza la configuración UDP"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
host = data.get("host", "127.0.0.1")
|
||||
port = int(data.get("port", 9870))
|
||||
|
||||
streamer.update_udp_config(host, port)
|
||||
return jsonify({"success": True, "message": "Configuración UDP actualizada"})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/plc/connect", methods=["POST"])
|
||||
def connect_plc():
|
||||
"""Conecta al PLC"""
|
||||
if streamer.connect_plc():
|
||||
return jsonify({"success": True, "message": "Conectado al PLC"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "Error conectando al PLC"}), 500
|
||||
|
||||
|
||||
@app.route("/api/plc/disconnect", methods=["POST"])
|
||||
def disconnect_plc():
|
||||
"""Desconecta del PLC"""
|
||||
streamer.stop_streaming()
|
||||
streamer.disconnect_plc()
|
||||
return jsonify({"success": True, "message": "Desconectado del PLC"})
|
||||
|
||||
|
||||
@app.route("/api/variables", methods=["POST"])
|
||||
def add_variable():
|
||||
"""Añade una nueva variable"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
name = data.get("name")
|
||||
db = int(data.get("db"))
|
||||
offset = int(data.get("offset"))
|
||||
var_type = data.get("type")
|
||||
|
||||
if not name or var_type not in ["real", "int", "bool", "dint"]:
|
||||
return jsonify({"success": False, "message": "Datos inválidos"}), 400
|
||||
|
||||
streamer.add_variable(name, db, offset, var_type)
|
||||
return jsonify({"success": True, "message": f"Variable {name} añadida"})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/variables/<name>", methods=["DELETE"])
|
||||
def remove_variable(name):
|
||||
"""Elimina una variable"""
|
||||
streamer.remove_variable(name)
|
||||
return jsonify({"success": True, "message": f"Variable {name} eliminada"})
|
||||
|
||||
|
||||
@app.route("/api/streaming/start", methods=["POST"])
|
||||
def start_streaming():
|
||||
"""Inicia el streaming"""
|
||||
if streamer.start_streaming():
|
||||
return jsonify({"success": True, "message": "Streaming iniciado"})
|
||||
else:
|
||||
return jsonify({"success": False, "message": "Error iniciando streaming"}), 500
|
||||
|
||||
|
||||
@app.route("/api/streaming/stop", methods=["POST"])
|
||||
def stop_streaming():
|
||||
"""Detiene el streaming"""
|
||||
streamer.stop_streaming()
|
||||
return jsonify({"success": True, "message": "Streaming detenido"})
|
||||
|
||||
|
||||
@app.route("/api/sampling", methods=["POST"])
|
||||
def update_sampling():
|
||||
"""Actualiza el intervalo de muestreo"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
interval = float(data.get("interval", 0.1))
|
||||
|
||||
if interval < 0.01:
|
||||
interval = 0.01
|
||||
|
||||
streamer.sampling_interval = interval
|
||||
return jsonify(
|
||||
{"success": True, "message": f"Intervalo actualizado a {interval}s"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/status")
|
||||
def get_status():
|
||||
"""Obtiene el estado actual"""
|
||||
return jsonify(streamer.get_status())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Crear directorio de templates si no existe
|
||||
os.makedirs("templates", exist_ok=True)
|
||||
|
||||
print("🚀 Iniciando servidor Flask para PLC S7-315 Streamer")
|
||||
print("📊 Interfaz web disponible en: http://localhost:5050")
|
||||
print("🔧 Configure su PLC y variables a través de la interfaz web")
|
||||
|
||||
try:
|
||||
app.run(debug=True, host="0.0.0.0", port=5050)
|
||||
except KeyboardInterrupt:
|
||||
print("\n⏹️ Deteniendo servidor...")
|
||||
streamer.stop_streaming()
|
||||
streamer.disconnect_plc()
|
|
@ -0,0 +1,2 @@
|
|||
Flask==2.3.3
|
||||
python-snap7==1.3
|
|
@ -0,0 +1,555 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>PLC S7-315 Streamer & Logger</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
color: white;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background: linear-gradient(135deg, #4CAF50, #45a049);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background: linear-gradient(135deg, #f44336, #d32f2f);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-streaming {
|
||||
background: linear-gradient(135deg, #2196F3, #1976D2);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-idle {
|
||||
background: linear-gradient(135deg, #9E9E9E, #757575);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 15px;
|
||||
padding: 25px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
color: #667eea;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.5rem;
|
||||
border-bottom: 2px solid #667eea;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 25px;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: linear-gradient(135deg, #4CAF50, #45a049);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: linear-gradient(135deg, #f44336, #d32f2f);
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: linear-gradient(135deg, #ff9800, #f57c00);
|
||||
}
|
||||
|
||||
.variables-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.variables-table th,
|
||||
.variables-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.variables-table th {
|
||||
background: linear-gradient(135deg, #667eea, #764ba2);
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.variables-table tr:hover {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin: 10px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🏭 PLC S7-315 Streamer & Logger</h1>
|
||||
<p>Sistema de monitoreo y streaming en tiempo real</p>
|
||||
</div>
|
||||
|
||||
<!-- Barra de Estado -->
|
||||
<div class="status-bar">
|
||||
<div class="status-grid">
|
||||
<div class="status-item" id="plc-status">
|
||||
🔌 PLC: Desconectado
|
||||
</div>
|
||||
<div class="status-item" id="stream-status">
|
||||
📡 Streaming: Inactivo
|
||||
</div>
|
||||
<div class="status-item status-idle">
|
||||
📊 Variables: {{ status.variables_count }}
|
||||
</div>
|
||||
<div class="status-item status-idle">
|
||||
⏱️ Intervalo: {{ status.sampling_interval }}s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mensajes de estado -->
|
||||
<div id="messages"></div>
|
||||
|
||||
<!-- Configuración PLC -->
|
||||
<div class="card">
|
||||
<h2>⚙️ Configuración PLC S7-315</h2>
|
||||
<form id="plc-config-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>IP del PLC:</label>
|
||||
<input type="text" id="plc-ip" value="{{ status.plc_config.ip }}" placeholder="192.168.1.100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Rack:</label>
|
||||
<input type="number" id="plc-rack" value="{{ status.plc_config.rack }}" min="0" max="7">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Slot:</label>
|
||||
<input type="number" id="plc-slot" value="{{ status.plc_config.slot }}" min="0" max="31">
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button type="submit" class="btn">💾 Guardar Configuración</button>
|
||||
<button type="button" class="btn btn-success" id="connect-btn">🔗 Conectar PLC</button>
|
||||
<button type="button" class="btn btn-danger" id="disconnect-btn">❌ Desconectar PLC</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Configuración UDP -->
|
||||
<div class="card">
|
||||
<h2>🌐 Configuración Gateway UDP (PlotJuggler)</h2>
|
||||
<form id="udp-config-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Host UDP:</label>
|
||||
<input type="text" id="udp-host" value="{{ status.udp_config.host }}" placeholder="127.0.0.1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Puerto UDP:</label>
|
||||
<input type="number" id="udp-port" value="{{ status.udp_config.port }}" min="1" max="65535">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Intervalo de Muestreo (s):</label>
|
||||
<input type="number" id="sampling-interval" value="{{ status.sampling_interval }}" min="0.01"
|
||||
max="10" step="0.01">
|
||||
</div>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<button type="submit" class="btn">💾 Guardar Configuración</button>
|
||||
<button type="button" class="btn btn-warning" id="update-sampling-btn">⏱️ Actualizar
|
||||
Intervalo</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Variables del PLC -->
|
||||
<div class="card">
|
||||
<h2>📋 Variables del PLC</h2>
|
||||
|
||||
<!-- Formulario para añadir variables -->
|
||||
<form id="variable-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>Nombre Variable:</label>
|
||||
<input type="text" id="var-name" placeholder="temperatura" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Data Block (DB):</label>
|
||||
<input type="number" id="var-db" min="1" max="9999" value="1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Offset:</label>
|
||||
<input type="number" id="var-offset" min="0" max="8192" value="0" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tipo de Dato:</label>
|
||||
<select id="var-type" required>
|
||||
<option value="real">REAL (Float 32-bit)</option>
|
||||
<option value="int">INT (16-bit)</option>
|
||||
<option value="dint">DINT (32-bit)</option>
|
||||
<option value="bool">BOOL</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn">➕ Añadir Variable</button>
|
||||
</form>
|
||||
|
||||
<!-- Tabla de variables -->
|
||||
<table class="variables-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nombre</th>
|
||||
<th>Data Block</th>
|
||||
<th>Offset</th>
|
||||
<th>Tipo</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="variables-tbody">
|
||||
{% for name, var in variables.items() %}
|
||||
<tr>
|
||||
<td>{{ name }}</td>
|
||||
<td>DB{{ var.db }}</td>
|
||||
<td>{{ var.offset }}</td>
|
||||
<td>{{ var.type.upper() }}</td>
|
||||
<td>
|
||||
<button class="btn btn-danger" onclick="removeVariable('{{ name }}')">🗑️ Eliminar</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Control de Streaming -->
|
||||
<div class="card">
|
||||
<h2>🚀 Control de Streaming</h2>
|
||||
<div class="controls">
|
||||
<button class="btn btn-success" id="start-streaming-btn">▶️ Iniciar Streaming</button>
|
||||
<button class="btn btn-danger" id="stop-streaming-btn">⏹️ Detener Streaming</button>
|
||||
<button class="btn" onclick="location.reload()">🔄 Actualizar Estado</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Función para mostrar mensajes
|
||||
function showMessage(message, type = 'success') {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
const alertClass = type === 'success' ? 'alert-success' : 'alert-error';
|
||||
messagesDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
messagesDiv.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Función para actualizar estado visual
|
||||
function updateStatus() {
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const plcStatus = document.getElementById('plc-status');
|
||||
const streamStatus = document.getElementById('stream-status');
|
||||
|
||||
if (data.plc_connected) {
|
||||
plcStatus.textContent = '🔌 PLC: Conectado';
|
||||
plcStatus.className = 'status-item status-connected';
|
||||
} else {
|
||||
plcStatus.textContent = '🔌 PLC: Desconectado';
|
||||
plcStatus.className = 'status-item status-disconnected';
|
||||
}
|
||||
|
||||
if (data.streaming) {
|
||||
streamStatus.textContent = '📡 Streaming: Activo';
|
||||
streamStatus.className = 'status-item status-streaming';
|
||||
} else {
|
||||
streamStatus.textContent = '📡 Streaming: Inactivo';
|
||||
streamStatus.className = 'status-item status-idle';
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error actualizando estado:', error));
|
||||
}
|
||||
|
||||
// Configuración PLC
|
||||
document.getElementById('plc-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
ip: document.getElementById('plc-ip').value,
|
||||
rack: parseInt(document.getElementById('plc-rack').value),
|
||||
slot: parseInt(document.getElementById('plc-slot').value)
|
||||
};
|
||||
|
||||
fetch('/api/plc/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Configuración UDP
|
||||
document.getElementById('udp-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
host: document.getElementById('udp-host').value,
|
||||
port: parseInt(document.getElementById('udp-port').value)
|
||||
};
|
||||
|
||||
fetch('/api/udp/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Conectar PLC
|
||||
document.getElementById('connect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/connect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Desconectar PLC
|
||||
document.getElementById('disconnect-btn').addEventListener('click', function () {
|
||||
fetch('/api/plc/disconnect', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Añadir variable
|
||||
document.getElementById('variable-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
name: document.getElementById('var-name').value,
|
||||
db: parseInt(document.getElementById('var-db').value),
|
||||
offset: parseInt(document.getElementById('var-offset').value),
|
||||
type: document.getElementById('var-type').value
|
||||
};
|
||||
|
||||
fetch('/api/variables', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Eliminar variable
|
||||
function removeVariable(name) {
|
||||
if (confirm(`¿Está seguro de eliminar la variable "${name}"?`)) {
|
||||
fetch(`/api/variables/${name}`, { method: 'DELETE' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar streaming
|
||||
document.getElementById('start-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Detener streaming
|
||||
document.getElementById('stop-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Actualizar intervalo
|
||||
document.getElementById('update-sampling-btn').addEventListener('click', function () {
|
||||
const interval = parseFloat(document.getElementById('sampling-interval').value);
|
||||
fetch('/api/sampling', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interval: interval })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Actualizar estado cada 5 segundos
|
||||
setInterval(updateStatus, 5000);
|
||||
|
||||
// Actualización inicial
|
||||
updateStatus();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue