S7_snap7_Stremer_n_Recorder/templates/index.html

1840 lines
71 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;
}
}
/* Modal Styles */
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
}
.modal-content {
background-color: white;
margin: 5% auto;
padding: 0;
border-radius: 15px;
width: 90%;
max-width: 600px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
animation: modalShow 0.3s ease;
}
@keyframes modalShow {
from {
opacity: 0;
transform: translateY(-50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.modal-header {
background: linear-gradient(135deg, #6b7280, #4b5563);
color: white;
padding: 20px;
border-radius: 15px 15px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 1.5rem;
}
.close {
color: white;
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 1;
}
.close:hover {
color: #f1f5f9;
}
.modal-body {
padding: 30px;
}
.modal-footer {
padding: 20px 30px;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn-secondary {
background: linear-gradient(135deg, #9ca3af, #6b7280);
}
.btn-secondary:hover {
background: linear-gradient(135deg, #6b7280, #4b5563);
}
/* Modal Styles */
.modal {
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0, 0, 0, 0.4);
}
.modal-content {
background-color: #f9f9f9;
margin: 5% auto;
padding: 0;
border: none;
border-radius: 12px;
width: 90%;
max-width: 500px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.modal-header {
background: linear-gradient(135deg, #4a5568, #2d3748);
color: white;
padding: 20px;
border-radius: 12px 12px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
}
.close {
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
}
.close:hover,
.close:focus {
color: white;
text-decoration: none;
}
.modal form {
padding: 20px;
}
.modal-footer {
padding: 20px;
text-align: right;
border-top: 1px solid #ddd;
background-color: #f1f1f1;
border-radius: 0 0 12px 12px;
}
.modal-footer button {
margin-left: 10px;
}
/* Dataset Controls */
.dataset-controls {
background-color: #f8f9fa;
padding: 15px;
border-radius: 8px;
margin-bottom: 15px;
}
.dataset-controls .form-row {
align-items: center;
}
.dataset-controls select {
min-width: 200px;
}
/* Status indicators for datasets */
.status-active {
color: #28a745;
font-weight: bold;
}
.status-inactive {
color: #6c757d;
}
.status-error {
color: #dc3545;
}
</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">
📊 Datasets: {{ status.datasets_count }} ({{ status.active_datasets_count }} active)
</div>
<div class="status-item status-idle">
📋 Variables: {{ status.total_variables }}
</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>
<!-- Dataset Management -->
<div class="card">
<h2>📊 Dataset Management</h2>
<!-- Dataset Selector and Controls -->
<div class="dataset-controls">
<div class="form-row">
<div class="form-group">
<label>Current Dataset:</label>
<select id="dataset-selector">
<option value="">Select a dataset...</option>
</select>
</div>
<div class="form-group">
<button type="button" class="btn btn-primary" id="new-dataset-btn"> New Dataset</button>
<button type="button" class="btn btn-danger" id="delete-dataset-btn">🗑️ Delete Dataset</button>
</div>
</div>
<div class="form-row" id="dataset-actions" style="display: none;">
<div class="form-group">
<button type="button" class="btn btn-success" id="activate-dataset-btn">▶️ Activate</button>
<button type="button" class="btn btn-warning" id="deactivate-dataset-btn">⏹️ Deactivate</button>
</div>
<div class="form-group">
<span id="dataset-status" class="status-item"></span>
</div>
</div>
</div>
<!-- Dataset Information -->
<div id="dataset-info" style="display: none;">
<div class="info-section">
<p><strong>📂 Name:</strong> <span id="dataset-name"></span></p>
<p><strong>🏷️ Prefix:</strong> <span id="dataset-prefix"></span></p>
<p><strong>⏱️ Sampling:</strong> <span id="dataset-sampling"></span></p>
<p><strong>📊 Variables:</strong> <span id="dataset-var-count"></span> | <strong>📡
Streaming:</strong> <span id="dataset-stream-count"></span></p>
</div>
</div>
</div>
<!-- New Dataset Modal -->
<div id="dataset-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Create New Dataset</h3>
<span class="close" id="close-dataset-modal">&times;</span>
</div>
<form id="dataset-form">
<div class="form-group">
<label>Dataset ID:</label>
<input type="text" id="dataset-id" placeholder="e.g., temperature_sensors" required>
<small>Used internally, no spaces or special characters</small>
</div>
<div class="form-group">
<label>Dataset Name:</label>
<input type="text" id="dataset-name-input" placeholder="e.g., Temperature Sensors" required>
</div>
<div class="form-group">
<label>CSV Prefix:</label>
<input type="text" id="dataset-prefix-input" placeholder="e.g., temp" required>
<small>Files will be named: prefix_hour.csv (e.g., temp_14.csv)</small>
</div>
<div class="form-group">
<label>Sampling Interval (seconds):</label>
<input type="number" id="dataset-sampling-input"
placeholder="Leave empty to use global interval" min="0.01" step="0.01">
<small>Leave empty to use global sampling interval</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="cancel-dataset-btn">Cancel</button>
<button type="submit" class="btn btn-primary">Create Dataset</button>
</div>
</form>
</div>
</div>
<!-- PLC Variables for Current Dataset -->
<div class="card" id="variables-section" style="display: none;">
<h2>📋 Variables for: <span id="current-dataset-title"></span></h2>
<!-- Form to add variables to current dataset -->
<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>Memory Area:</label>
<select id="var-area" required onchange="toggleFields()">
<option value="db">DB (Data Block)</option>
<option value="mw">MW (Memory Words)</option>
<option value="pew">PEW (Process Input Words)</option>
<option value="paw">PAW (Process Output Words)</option>
<option value="e">E (Input Bits)</option>
<option value="a">A (Output Bits)</option>
<option value="mb">MB (Memory Bits)</option>
</select>
</div>
<div class="form-group" id="db-field">
<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" id="bit-field" style="display: none;">
<label>Bit Position:</label>
<select id="var-bit">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</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 Signed)</option>
<option value="uint">UINT (16-bit Unsigned)</option>
<option value="dint">DINT (32-bit Signed)</option>
<option value="udint">UDINT (32-bit Unsigned)</option>
<option value="word">WORD (16-bit)</option>
<option value="byte">BYTE (8-bit)</option>
<option value="sint">SINT (8-bit Signed)</option>
<option value="usint">USINT (8-bit Unsigned)</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>Memory Area</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>
{% if var.area == 'db' or var.get('db') %}
DB{{ var.get('db', 'N/A') }}.{{ var.offset }}
{% elif var.area == 'mw' or var.area == 'm' %}
MW{{ var.offset }}
{% elif var.area == 'pew' or var.area == 'pe' %}
PEW{{ var.offset }}
{% elif var.area == 'paw' or var.area == 'pa' %}
PAW{{ var.offset }}
{% elif var.area == 'e' %}
E{{ var.offset }}.{{ var.bit }}
{% elif var.area == 'a' %}
A{{ var.offset }}.{{ var.bit }}
{% elif var.area == 'mb' %}
M{{ var.offset }}.{{ var.bit }}
{% else %}
DB{{ var.get('db', 'N/A') }}.{{ var.offset }}
{% endif %}
</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-primary" onclick="editVariable('{{ name }}')">✏️ Edit</button>
<button class="btn btn-danger" onclick="removeVariable('{{ name }}')">🗑️ Remove</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Edit Variable Modal -->
<div id="edit-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>✏️ Edit Variable</h3>
<span class="close" onclick="closeEditModal()">&times;</span>
</div>
<div class="modal-body">
<form id="edit-variable-form">
<div class="form-row">
<div class="form-group">
<label>Variable Name:</label>
<input type="text" id="edit-var-name" required>
</div>
<div class="form-group">
<label>Memory Area:</label>
<select id="edit-var-area" required onchange="toggleEditFields()">
<option value="db">DB (Data Block)</option>
<option value="mw">MW (Memory Words)</option>
<option value="pew">PEW (Process Input Words)</option>
<option value="paw">PAW (Process Output Words)</option>
<option value="e">E (Input Bits)</option>
<option value="a">A (Output Bits)</option>
<option value="mb">MB (Memory Bits)</option>
</select>
</div>
<div class="form-group" id="edit-db-field">
<label>Data Block (DB):</label>
<input type="number" id="edit-var-db" min="1" max="9999" value="1" required>
</div>
<div class="form-group">
<label>Offset:</label>
<input type="number" id="edit-var-offset" min="0" max="8192" value="0" required>
</div>
<div class="form-group" id="edit-bit-field" style="display: none;">
<label>Bit Position:</label>
<select id="edit-var-bit">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
<option value="6">6</option>
<option value="7">7</option>
</select>
</div>
<div class="form-group">
<label>Data Type:</label>
<select id="edit-var-type" required>
<option value="real">REAL (Float 32-bit)</option>
<option value="int">INT (16-bit Signed)</option>
<option value="uint">UINT (16-bit Unsigned)</option>
<option value="dint">DINT (32-bit Signed)</option>
<option value="udint">UDINT (32-bit Unsigned)</option>
<option value="word">WORD (16-bit)</option>
<option value="byte">BYTE (8-bit)</option>
<option value="sint">SINT (8-bit Signed)</option>
<option value="usint">USINT (8-bit Unsigned)</option>
<option value="bool">BOOL (Boolean)</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeEditModal()">❌ Cancel</button>
<button type="submit" class="btn btn-success">💾 Update Variable</button>
</div>
</form>
</div>
</div>
</div>
<!-- Multi-Dataset Streaming Control -->
<div class="card">
<h2>🚀 Multi-Dataset Streaming Control</h2>
<div class="info-section">
<p><strong>📡 Streaming Mode:</strong> Only variables marked for streaming in active datasets are sent
to PlotJuggler</p>
<p><strong>💾 CSV Recording:</strong> Each active dataset automatically records ALL its variables to
separate CSV files</p>
<p><strong>📁 File Organization:</strong> records/[dd-mm-yyyy]/[prefix]_[hour].csv (e.g., temp_14.csv,
pressure_14.csv)</p>
<p><strong>⏱️ Individual Sampling:</strong> Each dataset can have its own sampling interval or use the
global one</p>
</div>
<div class="controls">
<button class="btn btn-success" id="start-streaming-btn">▶️ Start All Active Datasets</button>
<button class="btn btn-danger" id="stop-streaming-btn">⏹️ Stop All Streaming</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();
});
});
// Toggle DB and Bit field visibility based on memory area selection
function toggleFields() {
const area = document.getElementById('var-area').value;
const dbField = document.getElementById('db-field');
const dbInput = document.getElementById('var-db');
const bitField = document.getElementById('bit-field');
const typeSelect = document.getElementById('var-type');
// Handle DB field
if (area === 'db') {
dbField.style.display = 'block';
dbInput.required = true;
} else {
dbField.style.display = 'none';
dbInput.required = false;
dbInput.value = 1; // Default value for non-DB areas
}
// Handle Bit field and data type restrictions
if (area === 'e' || area === 'a' || area === 'mb') {
bitField.style.display = 'block';
// For bit areas, force data type to bool
typeSelect.value = 'bool';
// Disable other data types for bit areas
Array.from(typeSelect.options).forEach(option => {
option.disabled = (option.value !== 'bool');
});
} else {
bitField.style.display = 'none';
// Re-enable all data types for non-bit areas
Array.from(typeSelect.options).forEach(option => {
option.disabled = false;
});
}
}
// Add variable to current dataset
document.getElementById('variable-form').addEventListener('submit', function (e) {
e.preventDefault();
if (!currentDatasetId) {
showMessage('No dataset selected. Please select a dataset first.', 'error');
return;
}
const area = document.getElementById('var-area').value;
const data = {
name: document.getElementById('var-name').value,
area: area,
db: area === 'db' ? parseInt(document.getElementById('var-db').value) : 1,
offset: parseInt(document.getElementById('var-offset').value),
type: document.getElementById('var-type').value,
streaming: false // Default to not streaming
};
// Add bit parameter for bit areas
if (area === 'e' || area === 'a' || area === 'mb') {
data.bit = parseInt(document.getElementById('var-bit').value);
}
fetch(`/api/datasets/${currentDatasetId}/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) {
document.getElementById('variable-form').reset();
loadDatasets(); // Reload to update counts
// Update status instead of full reload
updateStatus();
}
});
});
// Toggle streaming for variable in current dataset
function toggleStreaming(varName, enabled) {
if (!currentDatasetId) {
showMessage('No dataset selected', 'error');
return;
}
fetch(`/api/datasets/${currentDatasetId}/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 from current dataset
function removeVariable(name) {
if (!currentDatasetId) {
showMessage('No dataset selected', 'error');
return;
}
if (confirm(`Are you sure you want to remove the variable "${name}" from this dataset?`)) {
fetch(`/api/datasets/${currentDatasetId}/variables/${name}`, { method: 'DELETE' })
.then(response => response.json())
.then(data => {
showMessage(data.message, data.success ? 'success' : 'error');
if (data.success) {
loadDatasets(); // Reload to update counts
updateStatus();
}
});
}
}
// 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');
});
});
// 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();
// Edit Variable Functions
let currentEditingVariable = null;
function editVariable(name) {
if (!currentDatasetId) {
showMessage('No dataset selected', 'error');
return;
}
currentEditingVariable = name;
// Get variable data from current dataset
const dataset = currentDatasets[currentDatasetId];
if (dataset && dataset.variables && dataset.variables[name]) {
const variable = dataset.variables[name];
const streamingVars = dataset.streaming_variables || [];
// Create variable object with the same structure as the API
const variableData = {
name: name,
area: variable.area,
db: variable.db,
offset: variable.offset,
type: variable.type,
bit: variable.bit,
streaming: streamingVars.includes(name)
};
populateEditForm(variableData);
document.getElementById('edit-modal').style.display = 'block';
} else {
showMessage('Variable not found in current dataset', 'error');
}
}
function populateEditForm(variable) {
document.getElementById('edit-var-name').value = variable.name;
document.getElementById('edit-var-area').value = variable.area;
document.getElementById('edit-var-offset').value = variable.offset;
document.getElementById('edit-var-type').value = variable.type;
if (variable.db) {
document.getElementById('edit-var-db').value = variable.db;
}
if (variable.bit !== undefined) {
document.getElementById('edit-var-bit').value = variable.bit;
}
// Update field visibility based on area
toggleEditFields();
}
function toggleEditFields() {
const area = document.getElementById('edit-var-area').value;
const dbField = document.getElementById('edit-db-field');
const dbInput = document.getElementById('edit-var-db');
const bitField = document.getElementById('edit-bit-field');
const typeSelect = document.getElementById('edit-var-type');
// Handle DB field
if (area === 'db') {
dbField.style.display = 'block';
dbInput.required = true;
} else {
dbField.style.display = 'none';
dbInput.required = false;
dbInput.value = 1; // Default value for non-DB areas
}
// Handle Bit field and data type restrictions
if (area === 'e' || area === 'a' || area === 'mb') {
bitField.style.display = 'block';
// For bit areas, force data type to bool
typeSelect.value = 'bool';
// Disable other data types for bit areas
Array.from(typeSelect.options).forEach(option => {
option.disabled = (option.value !== 'bool');
});
} else {
bitField.style.display = 'none';
// Re-enable all data types for non-bit areas
Array.from(typeSelect.options).forEach(option => {
option.disabled = false;
});
}
}
function closeEditModal() {
document.getElementById('edit-modal').style.display = 'none';
currentEditingVariable = null;
}
// Handle edit form submission
document.getElementById('edit-variable-form').addEventListener('submit', function (e) {
e.preventDefault();
if (!currentEditingVariable || !currentDatasetId) {
showMessage('No variable or dataset selected for editing', 'error');
return;
}
const area = document.getElementById('edit-var-area').value;
const newName = document.getElementById('edit-var-name').value;
// First remove the old variable
fetch(`/api/datasets/${currentDatasetId}/variables/${currentEditingVariable}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(deleteResult => {
if (deleteResult.success) {
// Then add the updated variable
const data = {
name: newName,
area: area,
db: area === 'db' ? parseInt(document.getElementById('edit-var-db').value) : 1,
offset: parseInt(document.getElementById('edit-var-offset').value),
type: document.getElementById('edit-var-type').value,
streaming: false // Will be restored below if it was enabled
};
// Add bit parameter for bit areas
if (area === 'e' || area === 'a' || area === 'mb') {
data.bit = parseInt(document.getElementById('edit-var-bit').value);
}
return fetch(`/api/datasets/${currentDatasetId}/variables`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
} else {
throw new Error(deleteResult.message);
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
showMessage('Variable updated successfully', 'success');
closeEditModal();
loadDatasets();
updateStatus();
} else {
showMessage(data.message, 'error');
}
})
.catch(error => {
showMessage(`Error updating variable: ${error}`, 'error');
});
});
// Close modal when clicking outside of it
window.onclick = function (event) {
const modal = document.getElementById('edit-modal');
if (event.target === modal) {
closeEditModal();
}
}
// Initialize field visibility on page load
toggleFields();
// ===== DATASET MANAGEMENT FUNCTIONS =====
// Load datasets on page load
loadDatasets();
// Dataset management variables
let currentDatasets = {};
let currentDatasetId = null;
// Load all datasets from API
function loadDatasets() {
fetch('/api/datasets')
.then(response => response.json())
.then(data => {
if (data.success) {
currentDatasets = data.datasets;
currentDatasetId = data.current_dataset_id;
updateDatasetSelector();
updateDatasetInfo();
}
})
.catch(error => {
console.error('Error loading datasets:', error);
showMessage('Error loading datasets', 'error');
});
}
// Update the dataset selector dropdown
function updateDatasetSelector() {
const selector = document.getElementById('dataset-selector');
selector.innerHTML = '<option value="">Select a dataset...</option>';
Object.keys(currentDatasets).forEach(datasetId => {
const dataset = currentDatasets[datasetId];
const option = document.createElement('option');
option.value = datasetId;
option.textContent = `${dataset.name} (${dataset.prefix})`;
if (datasetId === currentDatasetId) {
option.selected = true;
}
selector.appendChild(option);
});
}
// Update dataset information display
function updateDatasetInfo() {
const infoSection = document.getElementById('dataset-info');
const actionsSection = document.getElementById('dataset-actions');
const variablesSection = document.getElementById('variables-section');
if (currentDatasetId && currentDatasets[currentDatasetId]) {
const dataset = currentDatasets[currentDatasetId];
// Show dataset info
document.getElementById('dataset-name').textContent = dataset.name;
document.getElementById('dataset-prefix').textContent = dataset.prefix;
document.getElementById('dataset-sampling').textContent =
dataset.sampling_interval ? `${dataset.sampling_interval}s` : 'Global interval';
document.getElementById('dataset-var-count').textContent = Object.keys(dataset.variables).length;
document.getElementById('dataset-stream-count').textContent = dataset.streaming_variables.length;
// Update dataset status
const statusSpan = document.getElementById('dataset-status');
const isActive = dataset.enabled;
statusSpan.textContent = isActive ? '🟢 Active' : '⭕ Inactive';
statusSpan.className = `status-item ${isActive ? 'status-active' : 'status-inactive'}`;
// Update action buttons
document.getElementById('activate-dataset-btn').style.display = isActive ? 'none' : 'inline-block';
document.getElementById('deactivate-dataset-btn').style.display = isActive ? 'inline-block' : 'none';
// Show sections
infoSection.style.display = 'block';
actionsSection.style.display = 'block';
variablesSection.style.display = 'block';
// Update variables section title
document.getElementById('current-dataset-title').textContent = dataset.name;
// Load variables for this dataset
loadDatasetVariables(currentDatasetId);
} else {
infoSection.style.display = 'none';
actionsSection.style.display = 'none';
variablesSection.style.display = 'none';
}
}
// Load variables for a specific dataset
function loadDatasetVariables(datasetId) {
if (!datasetId || !currentDatasets[datasetId]) {
// Clear the table if no valid dataset
document.getElementById('variables-tbody').innerHTML = '';
return;
}
const dataset = currentDatasets[datasetId];
const variables = dataset.variables || {};
const streamingVars = dataset.streaming_variables || [];
const tbody = document.getElementById('variables-tbody');
// Clear existing rows
tbody.innerHTML = '';
// Add a row for each variable
Object.keys(variables).forEach(varName => {
const variable = variables[varName];
const row = document.createElement('tr');
// Format memory area display
let memoryAreaDisplay = '';
if (variable.area === 'db') {
memoryAreaDisplay = `DB${variable.db || 'N/A'}.${variable.offset}`;
} else if (variable.area === 'mw' || variable.area === 'm') {
memoryAreaDisplay = `MW${variable.offset}`;
} else if (variable.area === 'pew' || variable.area === 'pe') {
memoryAreaDisplay = `PEW${variable.offset}`;
} else if (variable.area === 'paw' || variable.area === 'pa') {
memoryAreaDisplay = `PAW${variable.offset}`;
} else if (variable.area === 'e') {
memoryAreaDisplay = `E${variable.offset}.${variable.bit}`;
} else if (variable.area === 'a') {
memoryAreaDisplay = `A${variable.offset}.${variable.bit}`;
} else if (variable.area === 'mb') {
memoryAreaDisplay = `M${variable.offset}.${variable.bit}`;
} else {
memoryAreaDisplay = `${variable.area.toUpperCase()}${variable.offset}`;
}
// Check if variable is in streaming list
const isStreaming = streamingVars.includes(varName);
row.innerHTML = `
<td>${varName}</td>
<td>${memoryAreaDisplay}</td>
<td>${variable.offset}</td>
<td>${variable.type.toUpperCase()}</td>
<td>
<input type="checkbox" id="stream-${varName}" ${isStreaming ? 'checked' : ''}
onchange="toggleStreaming('${varName}', this.checked)">
<label for="stream-${varName}">Enable</label>
</td>
<td>
<button class="btn btn-primary" onclick="editVariable('${varName}')">✏️ Edit</button>
<button class="btn btn-danger" onclick="removeVariable('${varName}')">🗑️ Remove</button>
</td>
`;
tbody.appendChild(row);
});
}
// Dataset selector change handler
document.getElementById('dataset-selector').addEventListener('change', function () {
const selectedDatasetId = this.value;
if (selectedDatasetId) {
// Set as current dataset
fetch('/api/datasets/current', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dataset_id: selectedDatasetId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
currentDatasetId = selectedDatasetId;
// Reload datasets to get fresh data, then update info
loadDatasets();
} else {
showMessage(data.message, 'error');
}
})
.catch(error => {
showMessage('Error setting current dataset', 'error');
});
} else {
currentDatasetId = null;
updateDatasetInfo();
}
});
// New dataset button
document.getElementById('new-dataset-btn').addEventListener('click', function () {
document.getElementById('dataset-modal').style.display = 'block';
});
// Close dataset modal
document.getElementById('close-dataset-modal').addEventListener('click', function () {
document.getElementById('dataset-modal').style.display = 'none';
});
document.getElementById('cancel-dataset-btn').addEventListener('click', function () {
document.getElementById('dataset-modal').style.display = 'none';
});
// Create new dataset
document.getElementById('dataset-form').addEventListener('submit', function (e) {
e.preventDefault();
const data = {
dataset_id: document.getElementById('dataset-id').value.trim(),
name: document.getElementById('dataset-name-input').value.trim(),
prefix: document.getElementById('dataset-prefix-input').value.trim(),
sampling_interval: document.getElementById('dataset-sampling-input').value || null
};
if (data.sampling_interval) {
data.sampling_interval = parseFloat(data.sampling_interval);
}
fetch('/api/datasets', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
.then(response => response.json())
.then(data => {
if (data.success) {
showMessage(data.message, 'success');
document.getElementById('dataset-modal').style.display = 'none';
document.getElementById('dataset-form').reset();
loadDatasets();
} else {
showMessage(data.message, 'error');
}
})
.catch(error => {
showMessage('Error creating dataset', 'error');
});
});
// Delete dataset button
document.getElementById('delete-dataset-btn').addEventListener('click', function () {
if (!currentDatasetId) {
showMessage('No dataset selected', 'error');
return;
}
const dataset = currentDatasets[currentDatasetId];
if (confirm(`Are you sure you want to delete dataset "${dataset.name}"?`)) {
fetch(`/api/datasets/${currentDatasetId}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showMessage(data.message, 'success');
loadDatasets();
} else {
showMessage(data.message, 'error');
}
})
.catch(error => {
showMessage('Error deleting dataset', 'error');
});
}
});
// Activate dataset button
document.getElementById('activate-dataset-btn').addEventListener('click', function () {
if (!currentDatasetId) return;
fetch(`/api/datasets/${currentDatasetId}/activate`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showMessage(data.message, 'success');
loadDatasets();
} else {
showMessage(data.message, 'error');
}
})
.catch(error => {
showMessage('Error activating dataset', 'error');
});
});
// Deactivate dataset button
document.getElementById('deactivate-dataset-btn').addEventListener('click', function () {
if (!currentDatasetId) return;
fetch(`/api/datasets/${currentDatasetId}/deactivate`, {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
showMessage(data.message, 'success');
loadDatasets();
} else {
showMessage(data.message, 'error');
}
})
.catch(error => {
showMessage('Error deactivating dataset', 'error');
});
});
// Close modal when clicking outside
window.addEventListener('click', function (event) {
const modal = document.getElementById('dataset-modal');
if (event.target === modal) {
modal.style.display = 'none';
}
});
</script>
</body>
</html>