555 lines
19 KiB
HTML
555 lines
19 KiB
HTML
<!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> |