S7_snap7_Stremer_n_Recorder/templates/index.html

952 lines
34 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<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, #6b7280 0%, #4b5563 100%);
min-height: 100vh;
color: #1f2937;
}
.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);
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.header-logo {
height: 60px;
width: auto;
filter: drop-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, #6b7280, #4b5563);
color: white;
}
.status-disconnected {
background: linear-gradient(135deg, #9ca3af, #6b7280);
color: white;
}
.status-streaming {
background: linear-gradient(135deg, #374151, #1f2937);
color: white;
}
.status-idle {
background: linear-gradient(135deg, #d1d5db, #9ca3af);
color: #374151;
}
.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: #4b5563;
margin-bottom: 20px;
font-size: 1.5rem;
border-bottom: 2px solid #6b7280;
padding-bottom: 10px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #374151;
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #6b7280;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.btn {
background: linear-gradient(135deg, #6b7280, #4b5563);
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);
background: linear-gradient(135deg, #374151, #1f2937);
}
.btn-success {
background: linear-gradient(135deg, #6b7280, #374151);
}
.btn-success:hover {
background: linear-gradient(135deg, #4b5563, #1f2937);
}
.btn-danger {
background: linear-gradient(135deg, #9ca3af, #6b7280);
}
.btn-danger:hover {
background: linear-gradient(135deg, #6b7280, #4b5563);
}
.btn-warning {
background: linear-gradient(135deg, #d1d5db, #9ca3af);
color: #374151;
}
.btn-warning:hover {
background: linear-gradient(135deg, #9ca3af, #6b7280);
color: white;
}
.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 #e5e7eb;
}
.variables-table th {
background: linear-gradient(135deg, #6b7280, #4b5563);
color: white;
font-weight: bold;
}
.variables-table tr:hover {
background-color: #f9fafb;
}
.alert {
padding: 15px;
border-radius: 8px;
margin: 10px 0;
font-weight: bold;
}
.alert-success {
background-color: #f3f4f6;
border: 1px solid #d1d5db;
color: #374151;
}
.alert-error {
background-color: #fef2f2;
border: 1px solid #fecaca;
color: #7f1d1d;
}
.controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.info-section {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
}
.info-section p {
margin: 5px 0;
color: #374151;
}
.info-section strong {
color: #1f2937;
}
.log-container {
max-height: 400px;
overflow-y: auto;
background-color: #1f2937;
border-radius: 8px;
padding: 15px;
border: 1px solid #374151;
}
.log-entry {
display: flex;
flex-direction: column;
margin-bottom: 10px;
padding: 8px;
border-radius: 5px;
font-family: 'Courier New', monospace;
font-size: 12px;
border-left: 3px solid transparent;
}
.log-entry.log-info {
background-color: #374151;
border-left-color: #6b7280;
color: #e5e7eb;
}
.log-entry.log-warning {
background-color: #451a03;
border-left-color: #f59e0b;
color: #fef3c7;
}
.log-entry.log-error {
background-color: #450a0a;
border-left-color: #ef4444;
color: #fecaca;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
margin-bottom: 4px;
}
.log-timestamp {
font-size: 10px;
opacity: 0.7;
}
.log-type {
background-color: rgba(255, 255, 255, 0.1);
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
text-transform: uppercase;
}
.log-message {
margin-bottom: 4px;
word-wrap: break-word;
}
.log-details {
font-size: 10px;
opacity: 0.8;
background-color: rgba(0, 0, 0, 0.2);
padding: 4px;
border-radius: 3px;
margin-top: 4px;
white-space: pre-wrap;
}
.log-controls {
display: flex;
gap: 10px;
margin-bottom: 15px;
flex-wrap: wrap;
align-items: center;
}
.log-stats {
background-color: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 5px;
padding: 8px 12px;
font-size: 12px;
color: #374151;
}
@media (max-width: 768px) {
.header h1 {
font-size: 2rem;
flex-direction: column;
gap: 10px;
}
.header-logo {
height: 45px;
}
.form-row {
grid-template-columns: 1fr;
}
.controls {
flex-direction: column;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>
<img src="/images/SIDEL.png" alt="SIDEL Logo" class="header-logo">
🏭 PLC S7-315 Streamer & Logger
</h1>
<p>Real-time monitoring and streaming system</p>
</div>
<!-- Status Bar -->
<div class="status-bar">
<div class="status-grid">
<div class="status-item" id="plc-status">
🔌 PLC: Disconnected
</div>
<div class="status-item" id="stream-status">
📡 Streaming: Inactive
</div>
<div class="status-item status-idle">
📊 Variables: {{ status.variables_count }}
</div>
<div class="status-item status-idle">
⏱️ Interval: {{ status.sampling_interval }}s
</div>
<div class="status-item" id="csv-status">
💾 CSV: Inactive
</div>
</div>
</div>
<!-- Status messages -->
<div id="messages"></div>
<!-- PLC Configuration -->
<div class="card">
<h2>⚙️ PLC S7-315 Configuration</h2>
<form id="plc-config-form">
<div class="form-row">
<div class="form-group">
<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">
<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">💾 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>
<!-- UDP Configuration -->
<div class="card">
<h2>🌐 UDP Gateway Configuration (PlotJuggler)</h2>
<form id="udp-config-form">
<div class="form-row">
<div class="form-group">
<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>UDP Port:</label>
<input type="number" id="udp-port" value="{{ status.udp_config.port }}" min="1" max="65535">
</div>
<div class="form-group">
<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">💾 Save Configuration</button>
<button type="button" class="btn btn-warning" id="update-sampling-btn">⏱️ Update Interval</button>
</div>
</form>
</div>
<!-- PLC Variables -->
<div class="card">
<h2>📋 PLC Variables</h2>
<!-- Form to add variables -->
<form id="variable-form">
<div class="form-row">
<div class="form-group">
<label>Variable Name:</label>
<input type="text" id="var-name" placeholder="temperature" 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>Data Type:</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"> Add Variable</button>
</form>
<!-- Variables table -->
<table class="variables-table">
<thead>
<tr>
<th>Name</th>
<th>Data Block</th>
<th>Offset</th>
<th>Type</th>
<th>Stream to PlotJuggler</th>
<th>Actions</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>
<input type="checkbox" id="stream-{{ name }}"
onchange="toggleStreaming('{{ name }}', this.checked)">
<label for="stream-{{ name }}">Enable</label>
</td>
<td>
<button class="btn btn-danger" onclick="removeVariable('{{ name }}')">🗑️ Remove</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- CSV Recording Control -->
<div class="card">
<h2>💾 CSV Recording Control</h2>
<div class="info-section">
<p><strong>📁 Recording Location:</strong> records/[dd-mm-yyyy]/[hour].csv</p>
<p><strong>📊 Recording Mode:</strong> All defined variables are automatically saved to CSV</p>
<p><strong>📅 File Organization:</strong> One file per hour, automatic directory creation</p>
<p id="current-csv-file"></p>
</div>
<div class="controls">
<button class="btn btn-success" id="start-csv-btn">💾 Start CSV Recording</button>
<button class="btn btn-danger" id="stop-csv-btn">⏹️ Stop CSV Recording</button>
</div>
</div>
<!-- Streaming Control -->
<div class="card">
<h2>🚀 Streaming Control</h2>
<div class="info-section">
<p><strong>📡 Streaming Mode:</strong> Only variables marked for streaming are sent to PlotJuggler</p>
<p><strong>🔄 Combined Operation:</strong> Starting streaming also starts CSV recording automatically
</p>
</div>
<div class="controls">
<button class="btn btn-success" id="start-streaming-btn">▶️ Start Streaming & CSV</button>
<button class="btn btn-danger" id="stop-streaming-btn">⏹️ Stop Streaming & CSV</button>
<button class="btn" onclick="location.reload()">🔄 Refresh Status</button>
</div>
</div>
<!-- Application Events Log -->
<div class="card">
<h2>📋 Application Events Log</h2>
<div class="info-section">
<p><strong>📝 Event Tracking:</strong> Connection events, configuration changes, errors and system
status</p>
<p><strong>💾 Persistent Storage:</strong> Events are saved to disk and persist between application
restarts</p>
</div>
<div class="log-controls">
<button class="btn" onclick="refreshEventLog()">🔄 Refresh Log</button>
<button class="btn" onclick="clearLogView()">🧹 Clear View</button>
<select id="log-limit" onchange="refreshEventLog()">
<option value="25">Last 25 events</option>
<option value="50" selected>Last 50 events</option>
<option value="100">Last 100 events</option>
<option value="200">Last 200 events</option>
</select>
<div class="log-stats" id="log-stats">
Loading log statistics...
</div>
</div>
<div class="log-container" id="events-log">
<div class="log-entry log-info">
<div class="log-header">
<span>📡 System</span>
<span class="log-timestamp">Loading...</span>
</div>
<div class="log-message">Loading application events...</div>
</div>
</div>
</div>
</div>
<script>
// Function to display messages
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);
}
// Function to update visual status
function updateStatus() {
fetch('/api/status')
.then(response => response.json())
.then(data => {
const plcStatus = document.getElementById('plc-status');
const streamStatus = document.getElementById('stream-status');
const csvStatus = document.getElementById('csv-status');
if (data.plc_connected) {
plcStatus.textContent = '🔌 PLC: Connected';
plcStatus.className = 'status-item status-connected';
} else {
plcStatus.textContent = '🔌 PLC: Disconnected';
plcStatus.className = 'status-item status-disconnected';
}
if (data.streaming) {
streamStatus.textContent = `📡 Streaming: Active (${data.streaming_variables_count} vars)`;
streamStatus.className = 'status-item status-streaming';
} else {
streamStatus.textContent = '📡 Streaming: Inactive';
streamStatus.className = 'status-item status-idle';
}
if (data.csv_recording) {
csvStatus.textContent = '💾 CSV: Recording';
csvStatus.className = 'status-item status-streaming';
} else {
csvStatus.textContent = '💾 CSV: Inactive';
csvStatus.className = 'status-item status-idle';
}
// Update current CSV file info
const csvFileInfo = document.getElementById('current-csv-file');
if (data.current_csv_file && data.csv_recording) {
csvFileInfo.innerHTML = `<strong>📁 Current File:</strong> ${data.current_csv_file}`;
} else {
csvFileInfo.innerHTML = '';
}
})
.catch(error => console.error('Error updating status:', error));
}
// PLC Configuration
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');
});
});
// UDP Configuration
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');
});
});
// Connect 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();
});
});
// Disconnect 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();
});
});
// Add 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();
}
});
});
// Toggle streaming for variable
function toggleStreaming(varName, enabled) {
fetch(`/api/variables/${varName}/streaming`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: enabled })
})
.then(response => response.json())
.then(data => {
showMessage(data.message, data.success ? 'success' : 'error');
updateStatus(); // Update streaming variable count
})
.catch(error => {
console.error('Error toggling streaming:', error);
showMessage('Error updating streaming setting', 'error');
});
}
// Load streaming variables status
function loadStreamingStatus() {
fetch('/api/variables/streaming')
.then(response => response.json())
.then(data => {
if (data.success) {
data.streaming_variables.forEach(varName => {
const checkbox = document.getElementById(`stream-${varName}`);
if (checkbox) {
checkbox.checked = true;
}
});
}
})
.catch(error => console.error('Error loading streaming status:', error));
}
// Remove variable
function removeVariable(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 => {
showMessage(data.message, data.success ? 'success' : 'error');
if (data.success) {
location.reload();
}
});
}
}
// Start 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();
});
});
// Stop 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();
});
});
// Update interval
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');
});
});
// Start CSV recording
document.getElementById('start-csv-btn').addEventListener('click', function () {
fetch('/api/csv/start', { method: 'POST' })
.then(response => response.json())
.then(data => {
showMessage(data.message, data.success ? 'success' : 'error');
updateStatus();
});
});
// Stop CSV recording
document.getElementById('stop-csv-btn').addEventListener('click', function () {
fetch('/api/csv/stop', { method: 'POST' })
.then(response => response.json())
.then(data => {
showMessage(data.message, data.success ? 'success' : 'error');
updateStatus();
});
});
// Application Events Log Functions
function formatTimestamp(isoString) {
const date = new Date(isoString);
return date.toLocaleString('es-ES', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
function getEventIcon(eventType) {
const icons = {
'plc_connection': '🔗',
'plc_connection_failed': '❌',
'plc_disconnection': '🔌',
'plc_disconnection_error': '⚠️',
'streaming_started': '▶️',
'streaming_stopped': '⏹️',
'streaming_error': '❌',
'csv_started': '💾',
'csv_stopped': '📁',
'csv_error': '❌',
'config_change': '⚙️',
'variable_added': '',
'variable_removed': '',
'application_started': '🚀'
};
return icons[eventType] || '📋';
}
function createLogEntry(event) {
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${event.level}`;
const hasDetails = event.details && Object.keys(event.details).length > 0;
logEntry.innerHTML = `
<div class="log-header">
<span>${getEventIcon(event.event_type)} ${event.event_type.replace(/_/g, ' ').toUpperCase()}</span>
<span class="log-timestamp">${formatTimestamp(event.timestamp)}</span>
</div>
<div class="log-message">${event.message}</div>
${hasDetails ? `<div class="log-details">${JSON.stringify(event.details, null, 2)}</div>` : ''}
`;
return logEntry;
}
function refreshEventLog() {
const limit = document.getElementById('log-limit').value;
fetch(`/api/events?limit=${limit}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const logContainer = document.getElementById('events-log');
const logStats = document.getElementById('log-stats');
// Clear existing entries
logContainer.innerHTML = '';
// Update statistics
logStats.textContent = `Showing ${data.showing} of ${data.total_events} events`;
// Add events (reverse order to show newest first)
const events = data.events.reverse();
if (events.length === 0) {
logContainer.innerHTML = `
<div class="log-entry log-info">
<div class="log-header">
<span>📋 System</span>
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
</div>
<div class="log-message">No events found</div>
</div>
`;
} else {
events.forEach(event => {
logContainer.appendChild(createLogEntry(event));
});
}
// Auto-scroll to top to show newest events
logContainer.scrollTop = 0;
} else {
console.error('Error loading events:', data.error);
showMessage('Error loading events log', 'error');
}
})
.catch(error => {
console.error('Error fetching events:', error);
showMessage('Error fetching events log', 'error');
});
}
function clearLogView() {
const logContainer = document.getElementById('events-log');
logContainer.innerHTML = `
<div class="log-entry log-info">
<div class="log-header">
<span>🧹 System</span>
<span class="log-timestamp">${new Date().toLocaleString('es-ES')}</span>
</div>
<div class="log-message">Log view cleared. Click refresh to reload events.</div>
</div>
`;
const logStats = document.getElementById('log-stats');
logStats.textContent = 'Log view cleared';
}
// Update status every 5 seconds
setInterval(updateStatus, 5000);
// Update event log every 10 seconds
setInterval(refreshEventLog, 10000);
// Initial update
updateStatus();
loadStreamingStatus();
refreshEventLog();
</script>
</body>
</html>