Implementada persistencia de configuración en JSON y traducción de la interfaz al inglés. Se añadieron métodos para cargar y guardar configuraciones y variables, mejorando la usabilidad y la experiencia del usuario. Además, se actualizó el diseño visual a una paleta de grises más profesional.

This commit is contained in:
Miguel 2025-07-17 12:45:31 +02:00
parent 75bd73be68
commit c1d258fbf3
3 changed files with 303 additions and 181 deletions

View File

@ -1,32 +1,69 @@
### Objective
Create a python script with flask to do logging and do streaming to plotjuggler of live data from a PLC S7-315 of Siemens. The web gui must allow me to configure the connection data with the PLC and activate the connection. Then Also it must allow me to configure the UDP gateway for doing streaming to PlotJuggler. Also it must allow to create the list of variables to do polling to the PLC.
# Project Evolution Memory
### Application Description
## PLC S7-315 Streamer & Logger
**PLC S7-315 Streamer & Logger** is a web application developed in Python with Flask that enables real-time communication with Siemens S7-315 PLCs for data acquisition, logging, and streaming.
### Latest Modifications (Current Session)
#### Main Features:
#### Persistent Configuration System
**Decision**: Implemented JSON-based persistence for both PLC configuration and variables setup.
1. **PLC Connection**: Configuration and management of TCP/IP connections with S7-315 PLCs using the snap7 library
2. **Variable Management**: Dynamic configuration of variables for polling (real, int, bool, dint) with DB, offset, and type specification
3. **UDP Streaming**: Real-time data transmission to PlotJuggler via UDP with timestamped JSON format
4. **Logging**: Automatic data logging to local file with high-precision timestamps
5. **Web Interface**: Complete control panel for system configuration and status monitoring
**Rationale**: The application needed to remember user configurations between sessions to improve usability and prevent data loss.
#### Technical Architecture:
**Implementation**:
- Created two separate JSON files: `plc_config.json` for system settings and `plc_variables.json` for variable definitions
- Added automatic save/load functionality that triggers on every configuration change
- Configuration includes PLC connection settings, UDP gateway settings, and sampling intervals
- Variables are automatically saved when added or removed from the monitoring list
- **Backend**: Flask with threading for asynchronous streaming
- **PLC Communication**: snap7 client for S7 protocol
- **Streaming**: UDP socket for PlotJuggler transmission
- **Logging**: Python logging system with persistent file
- **Frontend**: HTML/CSS/JavaScript for user interface
**Impact**: Users no longer need to reconfigure the system every time they restart the application, significantly improving the user experience.
#### Operation Flow:
#### Interface Localization to English
**Decision**: Converted all user interface text and code comments from Spanish to English.
1. PLC parameter configuration (IP, rack, slot)
2. UDP destination configuration (host, port)
3. Variable definition for monitoring
4. PLC connection and validation
5. Streaming start with configurable interval
6. Continuous data transmission to PlotJuggler and local logging
**Rationale**: English provides better compatibility with international teams and technical documentation standards.
**Implementation**:
- Updated all HTML template text labels and messages
- Translated JavaScript functions and comments
- Converted Python docstrings and log messages to English
- Updated confirmation dialogs and status messages
**Impact**: The application is now accessible to a broader international audience and follows standard technical documentation practices.
#### Grayscale Visual Design
**Decision**: Replaced the colorful gradient design with a professional grayscale color scheme.
**Rationale**: Gray tones provide better visual consistency for industrial applications and reduce visual fatigue during extended monitoring sessions.
**Implementation**:
- Applied grayscale palette using standard gray color codes
- Maintained visual hierarchy through different gray intensities
- Preserved button states and interactive feedback using gray variations
- Ensured proper contrast for accessibility
**Impact**: The interface now has a more professional, industrial appearance suitable for PLC monitoring applications.
### Technical Architecture Decisions
#### Class-Based Streamer Design
The `PLCDataStreamer` class encapsulates all PLC communication logic, providing clear separation of concerns between data acquisition, UDP transmission, and web interface management.
#### Threaded Streaming Implementation
Streaming operations run in a separate daemon thread to prevent blocking the web interface, ensuring responsive user interaction during data collection.
#### Modular Configuration Management
Configuration handling is separated into distinct methods for PLC settings, UDP settings, and variable management, allowing independent updates without affecting other system components.
#### Initialization Order Fix
**Issue Resolved**: Fixed AttributeError during application startup where configuration loading was attempted before logger initialization.
**Solution**: Reordered the initialization sequence in the PLCDataStreamer constructor to setup logging first, then load configuration files.
**Technical Impact**: Ensured proper error handling and logging throughout the application lifecycle from the very first startup.
### Future Considerations
The persistent configuration system provides a foundation for more advanced features like configuration profiles, backup/restore functionality, and remote configuration management.
The English interface and standardized design make the application ready for potential integration with larger industrial monitoring systems or deployment in international environments.

223
main.py
View File

@ -16,16 +16,19 @@ 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}
"""Initialize the PLC data streamer"""
# Configuration file paths
self.config_file = "plc_config.json"
self.variables_file = "plc_variables.json"
# Default configuration
self.plc_config = {"ip": "192.168.1.100", "rack": 0, "slot": 2}
self.udp_config = {"host": "127.0.0.1", "port": 9870}
# Variables configurables
# Configurable variables
self.variables = {}
# Estados
# System states
self.plc = None
self.udp_socket = None
self.connected = False
@ -33,11 +36,15 @@ class PLCDataStreamer:
self.stream_thread = None
self.sampling_interval = 0.1
# Configurar logging
# Setup logging first
self.setup_logging()
# Load configuration from files
self.load_configuration()
self.load_variables()
def setup_logging(self):
"""Configura el sistema de logging"""
"""Configure the logging system"""
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(levelname)s - %(message)s",
@ -45,29 +52,95 @@ class PLCDataStreamer:
)
self.logger = logging.getLogger(__name__)
def load_configuration(self):
"""Load PLC and UDP configuration from JSON file"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, "r") as f:
config = json.load(f)
self.plc_config = config.get("plc_config", self.plc_config)
self.udp_config = config.get("udp_config", self.udp_config)
self.sampling_interval = config.get(
"sampling_interval", self.sampling_interval
)
self.logger.info(f"Configuration loaded from {self.config_file}")
else:
self.logger.info("No configuration file found, using defaults")
except Exception as e:
self.logger.error(f"Error loading configuration: {e}")
def save_configuration(self):
"""Save PLC and UDP configuration to JSON file"""
try:
config = {
"plc_config": self.plc_config,
"udp_config": self.udp_config,
"sampling_interval": self.sampling_interval,
}
with open(self.config_file, "w") as f:
json.dump(config, f, indent=4)
self.logger.info(f"Configuration saved to {self.config_file}")
except Exception as e:
self.logger.error(f"Error saving configuration: {e}")
def load_variables(self):
"""Load variables configuration from JSON file"""
try:
if os.path.exists(self.variables_file):
with open(self.variables_file, "r") as f:
self.variables = json.load(f)
self.logger.info(
f"Variables loaded from {self.variables_file}: {len(self.variables)} variables"
)
else:
self.logger.info(
"No variables file found, starting with empty variables"
)
except Exception as e:
self.logger.error(f"Error loading variables: {e}")
def save_variables(self):
"""Save variables configuration to JSON file"""
try:
with open(self.variables_file, "w") as f:
json.dump(self.variables, f, indent=4)
self.logger.info(f"Variables saved to {self.variables_file}")
except Exception as e:
self.logger.error(f"Error saving variables: {e}")
def update_plc_config(self, ip: str, rack: int, slot: int):
"""Actualiza la configuración del PLC"""
"""Update PLC configuration"""
self.plc_config = {"ip": ip, "rack": rack, "slot": slot}
self.logger.info(f"Configuración PLC actualizada: {self.plc_config}")
self.save_configuration()
self.logger.info(f"PLC configuration updated: {self.plc_config}")
def update_udp_config(self, host: str, port: int):
"""Actualiza la configuración UDP"""
"""Update UDP configuration"""
self.udp_config = {"host": host, "port": port}
self.logger.info(f"Configuración UDP actualizada: {self.udp_config}")
self.save_configuration()
self.logger.info(f"UDP configuration updated: {self.udp_config}")
def update_sampling_interval(self, interval: float):
"""Update sampling interval"""
self.sampling_interval = interval
self.save_configuration()
self.logger.info(f"Sampling interval updated: {interval}s")
def add_variable(self, name: str, db: int, offset: int, var_type: str):
"""Añade una variable para polling"""
"""Add a variable for polling"""
self.variables[name] = {"db": db, "offset": offset, "type": var_type}
self.logger.info(f"Variable añadida: {name} -> DB{db}.{offset} ({var_type})")
self.save_variables()
self.logger.info(f"Variable added: {name} -> DB{db}.{offset} ({var_type})")
def remove_variable(self, name: str):
"""Elimina una variable del polling"""
"""Remove a variable from polling"""
if name in self.variables:
del self.variables[name]
self.logger.info(f"Variable eliminada: {name}")
self.save_variables()
self.logger.info(f"Variable removed: {name}")
def connect_plc(self) -> bool:
"""Conecta al PLC S7-315"""
"""Connect to S7-315 PLC"""
try:
if self.plc:
self.plc.disconnect()
@ -78,26 +151,26 @@ class PLCDataStreamer:
)
self.connected = True
self.logger.info(f"Conectado al PLC {self.plc_config['ip']}")
self.logger.info(f"Connected to PLC {self.plc_config['ip']}")
return True
except Exception as e:
self.connected = False
self.logger.error(f"Error conectando al PLC: {e}")
self.logger.error(f"Error connecting to PLC: {e}")
return False
def disconnect_plc(self):
"""Desconecta del PLC"""
"""Disconnect from PLC"""
try:
if self.plc:
self.plc.disconnect()
self.connected = False
self.logger.info("Desconectado del PLC")
self.logger.info("Disconnected from PLC")
except Exception as e:
self.logger.error(f"Error desconectando del PLC: {e}")
self.logger.error(f"Error disconnecting from PLC: {e}")
def read_variable(self, var_config: Dict[str, Any]) -> Any:
"""Lee una variable específica del PLC"""
"""Read a specific variable from the PLC"""
try:
db = var_config["db"]
offset = var_config["offset"]
@ -121,11 +194,11 @@ class PLCDataStreamer:
return value
except Exception as e:
self.logger.error(f"Error leyendo variable: {e}")
self.logger.error(f"Error reading variable: {e}")
return None
def read_all_variables(self) -> Dict[str, Any]:
"""Lee todas las variables configuradas"""
"""Read all configured variables"""
if not self.connected or not self.plc:
return {}
@ -138,23 +211,23 @@ class PLCDataStreamer:
return data
def setup_udp_socket(self) -> bool:
"""Configura el socket UDP"""
"""Setup UDP socket"""
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']}"
f"UDP socket configured for {self.udp_config['host']}:{self.udp_config['port']}"
)
return True
except Exception as e:
self.logger.error(f"Error configurando socket UDP: {e}")
self.logger.error(f"Error configuring UDP socket: {e}")
return False
def send_to_plotjuggler(self, data: Dict[str, Any]):
"""Envía datos a PlotJuggler vía UDP JSON"""
"""Send data to PlotJuggler via UDP JSON"""
if not self.udp_socket:
return
@ -168,46 +241,46 @@ class PLCDataStreamer:
)
except Exception as e:
self.logger.error(f"Error enviando datos a PlotJuggler: {e}")
self.logger.error(f"Error sending data to PlotJuggler: {e}")
def streaming_loop(self):
"""Bucle principal de streaming"""
"""Main streaming loop"""
self.logger.info(
f"Iniciando streaming con intervalo de {self.sampling_interval}s"
f"Starting streaming with interval of {self.sampling_interval}s"
)
while self.streaming:
try:
start_time = time.time()
# Leer todas las variables
# Read all variables
data = self.read_all_variables()
if data:
# Enviar a PlotJuggler
# Send to PlotJuggler
self.send_to_plotjuggler(data)
# Log de datos
# Log data
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
self.logger.info(f"[{timestamp}] {data}")
# Mantener intervalo de muestreo
# Maintain sampling interval
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}")
self.logger.error(f"Error in streaming loop: {e}")
break
def start_streaming(self) -> bool:
"""Inicia el streaming de datos"""
"""Start data streaming"""
if not self.connected:
self.logger.error("PLC no conectado")
self.logger.error("PLC not connected")
return False
if not self.variables:
self.logger.error("No hay variables configuradas")
self.logger.error("No variables configured")
return False
if not self.setup_udp_socket():
@ -218,11 +291,11 @@ class PLCDataStreamer:
self.stream_thread.daemon = True
self.stream_thread.start()
self.logger.info("Streaming iniciado")
self.logger.info("Streaming started")
return True
def stop_streaming(self):
"""Detiene el streaming"""
"""Stop streaming"""
self.streaming = False
if self.stream_thread:
self.stream_thread.join(timeout=2)
@ -231,10 +304,10 @@ class PLCDataStreamer:
self.udp_socket.close()
self.udp_socket = None
self.logger.info("Streaming detenido")
self.logger.info("Streaming stopped")
def get_status(self) -> Dict[str, Any]:
"""Obtiene el estado actual del sistema"""
"""Get current system status"""
return {
"plc_connected": self.connected,
"streaming": self.streaming,
@ -245,13 +318,13 @@ class PLCDataStreamer:
}
# Instancia global del streamer
# Global streamer instance
streamer = PLCDataStreamer()
@app.route("/")
def index():
"""Página principal"""
"""Main page"""
return render_template(
"index.html", status=streamer.get_status(), variables=streamer.variables
)
@ -259,15 +332,15 @@ def index():
@app.route("/api/plc/config", methods=["POST"])
def update_plc_config():
"""Actualiza la configuración del PLC"""
"""Update PLC configuration"""
try:
data = request.get_json()
ip = data.get("ip", "192.168.1.100")
ip = data.get("ip", "10.1.33.11")
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"})
return jsonify({"success": True, "message": "PLC configuration updated"})
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 400
@ -275,14 +348,14 @@ def update_plc_config():
@app.route("/api/udp/config", methods=["POST"])
def update_udp_config():
"""Actualiza la configuración UDP"""
"""Update UDP configuration"""
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"})
return jsonify({"success": True, "message": "UDP configuration updated"})
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 400
@ -290,24 +363,24 @@ def update_udp_config():
@app.route("/api/plc/connect", methods=["POST"])
def connect_plc():
"""Conecta al PLC"""
"""Connect to PLC"""
if streamer.connect_plc():
return jsonify({"success": True, "message": "Conectado al PLC"})
return jsonify({"success": True, "message": "Connected to PLC"})
else:
return jsonify({"success": False, "message": "Error conectando al PLC"}), 500
return jsonify({"success": False, "message": "Error connecting to PLC"}), 500
@app.route("/api/plc/disconnect", methods=["POST"])
def disconnect_plc():
"""Desconecta del PLC"""
"""Disconnect from PLC"""
streamer.stop_streaming()
streamer.disconnect_plc()
return jsonify({"success": True, "message": "Desconectado del PLC"})
return jsonify({"success": True, "message": "Disconnected from PLC"})
@app.route("/api/variables", methods=["POST"])
def add_variable():
"""Añade una nueva variable"""
"""Add a new variable"""
try:
data = request.get_json()
name = data.get("name")
@ -316,10 +389,10 @@ def add_variable():
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
return jsonify({"success": False, "message": "Invalid data"}), 400
streamer.add_variable(name, db, offset, var_type)
return jsonify({"success": True, "message": f"Variable {name} añadida"})
return jsonify({"success": True, "message": f"Variable {name} added"})
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 400
@ -327,30 +400,30 @@ def add_variable():
@app.route("/api/variables/<name>", methods=["DELETE"])
def remove_variable(name):
"""Elimina una variable"""
"""Remove a variable"""
streamer.remove_variable(name)
return jsonify({"success": True, "message": f"Variable {name} eliminada"})
return jsonify({"success": True, "message": f"Variable {name} removed"})
@app.route("/api/streaming/start", methods=["POST"])
def start_streaming():
"""Inicia el streaming"""
"""Start streaming"""
if streamer.start_streaming():
return jsonify({"success": True, "message": "Streaming iniciado"})
return jsonify({"success": True, "message": "Streaming started"})
else:
return jsonify({"success": False, "message": "Error iniciando streaming"}), 500
return jsonify({"success": False, "message": "Error starting streaming"}), 500
@app.route("/api/streaming/stop", methods=["POST"])
def stop_streaming():
"""Detiene el streaming"""
"""Stop streaming"""
streamer.stop_streaming()
return jsonify({"success": True, "message": "Streaming detenido"})
return jsonify({"success": True, "message": "Streaming stopped"})
@app.route("/api/sampling", methods=["POST"])
def update_sampling():
"""Actualiza el intervalo de muestreo"""
"""Update sampling interval"""
try:
data = request.get_json()
interval = float(data.get("interval", 0.1))
@ -358,10 +431,8 @@ def update_sampling():
if interval < 0.01:
interval = 0.01
streamer.sampling_interval = interval
return jsonify(
{"success": True, "message": f"Intervalo actualizado a {interval}s"}
)
streamer.update_sampling_interval(interval)
return jsonify({"success": True, "message": f"Interval updated to {interval}s"})
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 400
@ -369,21 +440,21 @@ def update_sampling():
@app.route("/api/status")
def get_status():
"""Obtiene el estado actual"""
"""Get current status"""
return jsonify(streamer.get_status())
if __name__ == "__main__":
# Crear directorio de templates si no existe
# Create templates directory if it doesn't exist
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")
print("🚀 Starting Flask server for PLC S7-315 Streamer")
print("📊 Web interface available at: http://localhost:5050")
print("🔧 Configure your PLC and variables through the web interface")
try:
app.run(debug=True, host="0.0.0.0", port=5050)
except KeyboardInterrupt:
print("\n⏹️ Deteniendo servidor...")
print("\n⏹️ Stopping server...")
streamer.stop_streaming()
streamer.disconnect_plc()

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="es">
<html lang="en">
<head>
<meta charset="UTF-8">
@ -14,9 +14,9 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
min-height: 100vh;
color: #333;
color: #1f2937;
}
.container {
@ -60,23 +60,23 @@
}
.status-connected {
background: linear-gradient(135deg, #4CAF50, #45a049);
background: linear-gradient(135deg, #6b7280, #4b5563);
color: white;
}
.status-disconnected {
background: linear-gradient(135deg, #f44336, #d32f2f);
background: linear-gradient(135deg, #9ca3af, #6b7280);
color: white;
}
.status-streaming {
background: linear-gradient(135deg, #2196F3, #1976D2);
background: linear-gradient(135deg, #374151, #1f2937);
color: white;
}
.status-idle {
background: linear-gradient(135deg, #9E9E9E, #757575);
color: white;
background: linear-gradient(135deg, #d1d5db, #9ca3af);
color: #374151;
}
.card {
@ -89,10 +89,10 @@
}
.card h2 {
color: #667eea;
color: #4b5563;
margin-bottom: 20px;
font-size: 1.5rem;
border-bottom: 2px solid #667eea;
border-bottom: 2px solid #6b7280;
padding-bottom: 10px;
}
@ -104,14 +104,14 @@
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
color: #374151;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 2px solid #ddd;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
@ -120,7 +120,7 @@
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
border-color: #6b7280;
}
.form-row {
@ -130,7 +130,7 @@
}
.btn {
background: linear-gradient(135deg, #667eea, #764ba2);
background: linear-gradient(135deg, #6b7280, #4b5563);
color: white;
border: none;
padding: 12px 25px;
@ -145,18 +145,33 @@
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
background: linear-gradient(135deg, #374151, #1f2937);
}
.btn-success {
background: linear-gradient(135deg, #4CAF50, #45a049);
background: linear-gradient(135deg, #6b7280, #374151);
}
.btn-success:hover {
background: linear-gradient(135deg, #4b5563, #1f2937);
}
.btn-danger {
background: linear-gradient(135deg, #f44336, #d32f2f);
background: linear-gradient(135deg, #9ca3af, #6b7280);
}
.btn-danger:hover {
background: linear-gradient(135deg, #6b7280, #4b5563);
}
.btn-warning {
background: linear-gradient(135deg, #ff9800, #f57c00);
background: linear-gradient(135deg, #d1d5db, #9ca3af);
color: #374151;
}
.btn-warning:hover {
background: linear-gradient(135deg, #9ca3af, #6b7280);
color: white;
}
.variables-table {
@ -169,17 +184,17 @@
.variables-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
border-bottom: 1px solid #e5e7eb;
}
.variables-table th {
background: linear-gradient(135deg, #667eea, #764ba2);
background: linear-gradient(135deg, #6b7280, #4b5563);
color: white;
font-weight: bold;
}
.variables-table tr:hover {
background-color: #f5f5f5;
background-color: #f9fafb;
}
.alert {
@ -190,15 +205,15 @@
}
.alert-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
background-color: #f3f4f6;
border: 1px solid #d1d5db;
color: #374151;
}
.alert-error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
background-color: #fef2f2;
border: 1px solid #fecaca;
color: #7f1d1d;
}
.controls {
@ -228,37 +243,37 @@
<div class="container">
<div class="header">
<h1>🏭 PLC S7-315 Streamer & Logger</h1>
<p>Sistema de monitoreo y streaming en tiempo real</p>
<p>Real-time monitoring and streaming system</p>
</div>
<!-- Barra de Estado -->
<!-- Status Bar -->
<div class="status-bar">
<div class="status-grid">
<div class="status-item" id="plc-status">
🔌 PLC: Desconectado
🔌 PLC: Disconnected
</div>
<div class="status-item" id="stream-status">
📡 Streaming: Inactivo
📡 Streaming: Inactive
</div>
<div class="status-item status-idle">
📊 Variables: {{ status.variables_count }}
</div>
<div class="status-item status-idle">
⏱️ Intervalo: {{ status.sampling_interval }}s
⏱️ Interval: {{ status.sampling_interval }}s
</div>
</div>
</div>
<!-- Mensajes de estado -->
<!-- Status messages -->
<div id="messages"></div>
<!-- Configuración PLC -->
<!-- PLC Configuration -->
<div class="card">
<h2>⚙️ Configuración PLC S7-315</h2>
<h2>⚙️ PLC S7-315 Configuration</h2>
<form id="plc-config-form">
<div class="form-row">
<div class="form-group">
<label>IP del PLC:</label>
<label>PLC IP Address:</label>
<input type="text" id="plc-ip" value="{{ status.plc_config.ip }}" placeholder="192.168.1.100">
</div>
<div class="form-group">
@ -271,50 +286,49 @@
</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>
<button type="submit" class="btn">💾 Save Configuration</button>
<button type="button" class="btn btn-success" id="connect-btn">🔗 Connect PLC</button>
<button type="button" class="btn btn-danger" id="disconnect-btn">❌ Disconnect PLC</button>
</div>
</form>
</div>
<!-- Configuración UDP -->
<!-- UDP Configuration -->
<div class="card">
<h2>🌐 Configuración Gateway UDP (PlotJuggler)</h2>
<h2>🌐 UDP Gateway Configuration (PlotJuggler)</h2>
<form id="udp-config-form">
<div class="form-row">
<div class="form-group">
<label>Host UDP:</label>
<label>UDP Host:</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>
<label>UDP Port:</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>
<label>Sampling Interval (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>
<button type="submit" class="btn">💾 Save Configuration</button>
<button type="button" class="btn btn-warning" id="update-sampling-btn">⏱️ Update Interval</button>
</div>
</form>
</div>
<!-- Variables del PLC -->
<!-- PLC Variables -->
<div class="card">
<h2>📋 Variables del PLC</h2>
<h2>📋 PLC Variables</h2>
<!-- Formulario para añadir variables -->
<!-- Form to add 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>
<label>Variable Name:</label>
<input type="text" id="var-name" placeholder="temperature" required>
</div>
<div class="form-group">
<label>Data Block (DB):</label>
@ -325,7 +339,7 @@
<input type="number" id="var-offset" min="0" max="8192" value="0" required>
</div>
<div class="form-group">
<label>Tipo de Dato:</label>
<label>Data Type:</label>
<select id="var-type" required>
<option value="real">REAL (Float 32-bit)</option>
<option value="int">INT (16-bit)</option>
@ -334,18 +348,18 @@
</select>
</div>
</div>
<button type="submit" class="btn"> Añadir Variable</button>
<button type="submit" class="btn"> Add Variable</button>
</form>
<!-- Tabla de variables -->
<!-- Variables table -->
<table class="variables-table">
<thead>
<tr>
<th>Nombre</th>
<th>Name</th>
<th>Data Block</th>
<th>Offset</th>
<th>Tipo</th>
<th>Acciones</th>
<th>Type</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="variables-tbody">
@ -356,7 +370,7 @@
<td>{{ var.offset }}</td>
<td>{{ var.type.upper() }}</td>
<td>
<button class="btn btn-danger" onclick="removeVariable('{{ name }}')">🗑️ Eliminar</button>
<button class="btn btn-danger" onclick="removeVariable('{{ name }}')">🗑️ Remove</button>
</td>
</tr>
{% endfor %}
@ -364,19 +378,19 @@
</table>
</div>
<!-- Control de Streaming -->
<!-- Streaming Control -->
<div class="card">
<h2>🚀 Control de Streaming</h2>
<h2>🚀 Streaming Control</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>
<button class="btn btn-success" id="start-streaming-btn">▶️ Start Streaming</button>
<button class="btn btn-danger" id="stop-streaming-btn">⏹️ Stop Streaming</button>
<button class="btn" onclick="location.reload()">🔄 Refresh Status</button>
</div>
</div>
</div>
<script>
// Función para mostrar mensajes
// Function to display messages
function showMessage(message, type = 'success') {
const messagesDiv = document.getElementById('messages');
const alertClass = type === 'success' ? 'alert-success' : 'alert-error';
@ -386,7 +400,7 @@
}, 5000);
}
// Función para actualizar estado visual
// Function to update visual status
function updateStatus() {
fetch('/api/status')
.then(response => response.json())
@ -395,25 +409,25 @@
const streamStatus = document.getElementById('stream-status');
if (data.plc_connected) {
plcStatus.textContent = '🔌 PLC: Conectado';
plcStatus.textContent = '🔌 PLC: Connected';
plcStatus.className = 'status-item status-connected';
} else {
plcStatus.textContent = '🔌 PLC: Desconectado';
plcStatus.textContent = '🔌 PLC: Disconnected';
plcStatus.className = 'status-item status-disconnected';
}
if (data.streaming) {
streamStatus.textContent = '📡 Streaming: Activo';
streamStatus.textContent = '📡 Streaming: Active';
streamStatus.className = 'status-item status-streaming';
} else {
streamStatus.textContent = '📡 Streaming: Inactivo';
streamStatus.textContent = '📡 Streaming: Inactive';
streamStatus.className = 'status-item status-idle';
}
})
.catch(error => console.error('Error actualizando estado:', error));
.catch(error => console.error('Error updating status:', error));
}
// Configuración PLC
// PLC Configuration
document.getElementById('plc-config-form').addEventListener('submit', function (e) {
e.preventDefault();
const data = {
@ -433,7 +447,7 @@
});
});
// Configuración UDP
// UDP Configuration
document.getElementById('udp-config-form').addEventListener('submit', function (e) {
e.preventDefault();
const data = {
@ -452,7 +466,7 @@
});
});
// Conectar PLC
// Connect PLC
document.getElementById('connect-btn').addEventListener('click', function () {
fetch('/api/plc/connect', { method: 'POST' })
.then(response => response.json())
@ -462,7 +476,7 @@
});
});
// Desconectar PLC
// Disconnect PLC
document.getElementById('disconnect-btn').addEventListener('click', function () {
fetch('/api/plc/disconnect', { method: 'POST' })
.then(response => response.json())
@ -472,7 +486,7 @@
});
});
// Añadir variable
// Add variable
document.getElementById('variable-form').addEventListener('submit', function (e) {
e.preventDefault();
const data = {
@ -496,9 +510,9 @@
});
});
// Eliminar variable
// Remove variable
function removeVariable(name) {
if (confirm(`¿Está seguro de eliminar la variable "${name}"?`)) {
if (confirm(`Are you sure you want to remove the variable "${name}"?`)) {
fetch(`/api/variables/${name}`, { method: 'DELETE' })
.then(response => response.json())
.then(data => {
@ -510,7 +524,7 @@
}
}
// Iniciar streaming
// Start streaming
document.getElementById('start-streaming-btn').addEventListener('click', function () {
fetch('/api/streaming/start', { method: 'POST' })
.then(response => response.json())
@ -520,7 +534,7 @@
});
});
// Detener streaming
// Stop streaming
document.getElementById('stop-streaming-btn').addEventListener('click', function () {
fetch('/api/streaming/stop', { method: 'POST' })
.then(response => response.json())
@ -530,7 +544,7 @@
});
});
// Actualizar intervalo
// Update interval
document.getElementById('update-sampling-btn').addEventListener('click', function () {
const interval = parseFloat(document.getElementById('sampling-interval').value);
fetch('/api/sampling', {
@ -544,10 +558,10 @@
});
});
// Actualizar estado cada 5 segundos
// Update status every 5 seconds
setInterval(updateStatus, 5000);
// Actualización inicial
// Initial update
updateStatus();
</script>
</body>