Primera Version

This commit is contained in:
Miguel 2025-07-16 16:37:51 +02:00
commit a1f582e402
6 changed files with 1533 additions and 0 deletions

200
.doc/example.py Normal file
View File

@ -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()

203
.gitignore vendored Normal file
View File

@ -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

184
README.md Normal file
View File

@ -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.

389
main.py Normal file
View File

@ -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()

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
Flask==2.3.3
python-snap7==1.3

555
templates/index.html Normal file
View File

@ -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>