commit a1f582e40217250dcf445207d7b5e3172815d157 Author: Miguel Date: Wed Jul 16 16:37:51 2025 +0200 Primera Version diff --git a/.doc/example.py b/.doc/example.py new file mode 100644 index 0000000..77cf626 --- /dev/null +++ b/.doc/example.py @@ -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() \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb0f8dc --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..d85cf15 --- /dev/null +++ b/README.md @@ -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 +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/` - 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 `) +- 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. \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..48d6ae8 --- /dev/null +++ b/main.py @@ -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/", 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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..49a30a9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +Flask==2.3.3 +python-snap7==1.3 \ No newline at end of file diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..bae6e14 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,555 @@ + + + + + + + PLC S7-315 Streamer & Logger + + + + +
+
+

🏭 PLC S7-315 Streamer & Logger

+

Sistema de monitoreo y streaming en tiempo real

+
+ + +
+
+
+ 🔌 PLC: Desconectado +
+
+ 📡 Streaming: Inactivo +
+
+ 📊 Variables: {{ status.variables_count }} +
+
+ ⏱️ Intervalo: {{ status.sampling_interval }}s +
+
+
+ + +
+ + +
+

⚙️ Configuración PLC S7-315

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + +
+
+
+ + +
+

🌐 Configuración Gateway UDP (PlotJuggler)

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ + +
+

📋 Variables del PLC

+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + + + + + + + + + + + {% for name, var in variables.items() %} + + + + + + + + {% endfor %} + +
NombreData BlockOffsetTipoAcciones
{{ name }}DB{{ var.db }}{{ var.offset }}{{ var.type.upper() }} + +
+
+ + +
+

🚀 Control de Streaming

+
+ + + +
+
+
+ + + + + \ No newline at end of file