569 lines
19 KiB
HTML
569 lines
19 KiB
HTML
<!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);
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
@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>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>
|
||
</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>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>
|
||
<button class="btn btn-danger" onclick="removeVariable('{{ name }}')">🗑️ Remove</button>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- Streaming Control -->
|
||
<div class="card">
|
||
<h2>🚀 Streaming Control</h2>
|
||
<div class="controls">
|
||
<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>
|
||
// 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');
|
||
|
||
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';
|
||
streamStatus.className = 'status-item status-streaming';
|
||
} else {
|
||
streamStatus.textContent = '📡 Streaming: Inactive';
|
||
streamStatus.className = 'status-item status-idle';
|
||
}
|
||
})
|
||
.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();
|
||
}
|
||
});
|
||
});
|
||
|
||
// 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');
|
||
});
|
||
});
|
||
|
||
// Update status every 5 seconds
|
||
setInterval(updateStatus, 5000);
|
||
|
||
// Initial update
|
||
updateStatus();
|
||
</script>
|
||
</body>
|
||
|
||
</html> |