Implementadas mejoras significativas en el sistema de streaming y grabación de CSV. Se añadió un control independiente para la grabación de CSV, permitiendo la organización automática de archivos por hora. Se implementó un sistema de persistencia del estado del sistema y recuperación automática, mejorando la fiabilidad en entornos industriales. Además, se integró el logo de SIDEL en la interfaz y se realizaron ajustes en el diseño para una mejor experiencia de usuario.
This commit is contained in:
parent
c1d258fbf3
commit
cc729d8f82
|
@ -43,6 +43,26 @@
|
|||
|
||||
**Impact**: The interface now has a more professional, industrial appearance suitable for PLC monitoring applications.
|
||||
|
||||
#### SIDEL Corporate Logo Integration
|
||||
**Decision**: Integrated the SIDEL company logo into the main application header alongside the existing factory icon.
|
||||
|
||||
**Rationale**: Brand visibility and corporate identity integration for customized industrial applications deployed in SIDEL facilities.
|
||||
|
||||
**Implementation**:
|
||||
- Added Flask static file serving route `/images/<filename>` to serve images from `.images` directory
|
||||
- Implemented responsive CSS styling with flexbox layout for proper logo positioning
|
||||
- Applied visual effects (drop-shadow) consistent with existing header text styling
|
||||
- Added mobile-responsive design with smaller logo size and vertical layout for mobile devices
|
||||
- Logo positioned before the factory emoji icon maintaining visual hierarchy
|
||||
|
||||
**Technical Details**:
|
||||
- Logo served from `.images/SIDEL.png` through dedicated Flask route
|
||||
- CSS styling includes 60px height (45px on mobile) with automatic width scaling
|
||||
- Flexbox implementation ensures proper alignment and spacing
|
||||
- Drop-shadow filter maintains visual consistency with text shadows
|
||||
|
||||
**Impact**: The application now displays clear corporate branding while maintaining professional appearance and responsive behavior across all device types.
|
||||
|
||||
### Technical Architecture Decisions
|
||||
|
||||
#### Class-Based Streamer Design
|
||||
|
@ -61,6 +81,27 @@ Configuration handling is separated into distinct methods for PLC settings, UDP
|
|||
|
||||
**Technical Impact**: Ensured proper error handling and logging throughout the application lifecycle from the very first startup.
|
||||
|
||||
#### CSV Recording System Implementation
|
||||
**Decision**: Added comprehensive CSV recording system with hourly file organization and selective streaming capability.
|
||||
|
||||
**Rationale**: Industrial applications require both real-time visualization (PlotJuggler) and historical data storage (CSV) with different variable sets for each purpose.
|
||||
|
||||
**Implementation**:
|
||||
- Automatic directory structure creation: `records/dd-mm-yyyy/hour.csv`
|
||||
- Hourly file rotation with timestamp-based organization
|
||||
- Complete variable set recording to CSV regardless of streaming selection
|
||||
- Selective variable streaming to PlotJuggler through checkbox interface
|
||||
- Independent control for CSV recording and UDP streaming
|
||||
- Automatic CSV header management and file handling
|
||||
|
||||
**Architecture Impact**:
|
||||
- Separated data flow: All variables → CSV, Selected variables → PlotJuggler
|
||||
- Thread-safe file operations with automatic directory creation
|
||||
- Real-time file path updates in the interface
|
||||
- Dual recording modes: Combined (streaming + CSV) and Independent (CSV only)
|
||||
|
||||
**User Experience**: Users can now record all process data for historical analysis while sending only relevant variables to real-time visualization tools, reducing network traffic and improving PlotJuggler performance.
|
||||
|
||||
### Future Considerations
|
||||
|
||||
The persistent configuration system provides a foundation for more advanced features like configuration profiles, backup/restore functionality, and remote configuration management.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
225
main.py
225
main.py
|
@ -1,4 +1,12 @@
|
|||
from flask import Flask, render_template, request, jsonify, redirect, url_for
|
||||
from flask import (
|
||||
Flask,
|
||||
render_template,
|
||||
request,
|
||||
jsonify,
|
||||
redirect,
|
||||
url_for,
|
||||
send_from_directory,
|
||||
)
|
||||
import snap7
|
||||
import json
|
||||
import socket
|
||||
|
@ -9,6 +17,8 @@ from datetime import datetime
|
|||
from typing import Dict, Any, Optional
|
||||
import struct
|
||||
import os
|
||||
import csv
|
||||
from pathlib import Path
|
||||
|
||||
app = Flask(__name__)
|
||||
app.secret_key = "plc_streamer_secret_key"
|
||||
|
@ -27,6 +37,15 @@ class PLCDataStreamer:
|
|||
|
||||
# Configurable variables
|
||||
self.variables = {}
|
||||
self.streaming_variables = set() # Variables selected for streaming
|
||||
|
||||
# CSV recording settings
|
||||
self.csv_recording = False
|
||||
self.csv_record_thread = None
|
||||
self.current_csv_file = None
|
||||
self.current_csv_writer = None
|
||||
self.current_hour = None
|
||||
self.csv_headers_written = False
|
||||
|
||||
# System states
|
||||
self.plc = None
|
||||
|
@ -136,9 +155,129 @@ class PLCDataStreamer:
|
|||
"""Remove a variable from polling"""
|
||||
if name in self.variables:
|
||||
del self.variables[name]
|
||||
# Also remove from streaming variables if present
|
||||
self.streaming_variables.discard(name)
|
||||
self.save_variables()
|
||||
self.logger.info(f"Variable removed: {name}")
|
||||
|
||||
def toggle_streaming_variable(self, name: str, enabled: bool):
|
||||
"""Enable or disable a variable for streaming"""
|
||||
if name in self.variables:
|
||||
if enabled:
|
||||
self.streaming_variables.add(name)
|
||||
else:
|
||||
self.streaming_variables.discard(name)
|
||||
self.logger.info(
|
||||
f"Variable {name} streaming: {'enabled' if enabled else 'disabled'}"
|
||||
)
|
||||
|
||||
def get_csv_directory_path(self) -> str:
|
||||
"""Get the directory path for current day's CSV files"""
|
||||
now = datetime.now()
|
||||
day_folder = now.strftime("%d-%m-%Y")
|
||||
return os.path.join("records", day_folder)
|
||||
|
||||
def get_csv_file_path(self) -> str:
|
||||
"""Get the complete file path for current hour's CSV file"""
|
||||
now = datetime.now()
|
||||
hour = now.strftime("%H")
|
||||
directory = self.get_csv_directory_path()
|
||||
return os.path.join(directory, f"{hour}.csv")
|
||||
|
||||
def ensure_csv_directory(self):
|
||||
"""Create CSV directory structure if it doesn't exist"""
|
||||
directory = self.get_csv_directory_path()
|
||||
Path(directory).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def setup_csv_file(self):
|
||||
"""Setup CSV file for the current hour"""
|
||||
current_hour = datetime.now().hour
|
||||
|
||||
# Check if we need to create a new file
|
||||
if self.current_hour != current_hour or self.current_csv_file is None:
|
||||
# Close previous file if open
|
||||
if self.current_csv_file:
|
||||
self.current_csv_file.close()
|
||||
|
||||
# Create directory and file for current hour
|
||||
self.ensure_csv_directory()
|
||||
csv_path = self.get_csv_file_path()
|
||||
|
||||
# Check if file exists to determine if we need headers
|
||||
file_exists = os.path.exists(csv_path)
|
||||
|
||||
self.current_csv_file = open(csv_path, "a", newline="", encoding="utf-8")
|
||||
self.current_csv_writer = csv.writer(self.current_csv_file)
|
||||
self.current_hour = current_hour
|
||||
|
||||
# Write headers if it's a new file
|
||||
if not file_exists and self.variables:
|
||||
headers = ["timestamp"] + list(self.variables.keys())
|
||||
self.current_csv_writer.writerow(headers)
|
||||
self.current_csv_file.flush()
|
||||
self.csv_headers_written = True
|
||||
self.logger.info(f"CSV file created: {csv_path}")
|
||||
|
||||
def write_csv_data(self, data: Dict[str, Any]):
|
||||
"""Write data to CSV file"""
|
||||
if not self.csv_recording or not self.variables:
|
||||
return
|
||||
|
||||
try:
|
||||
self.setup_csv_file()
|
||||
|
||||
if self.current_csv_writer:
|
||||
# Create timestamp
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
|
||||
# Create row with all variables (use None for missing values)
|
||||
row = [timestamp]
|
||||
for var_name in self.variables.keys():
|
||||
row.append(data.get(var_name, None))
|
||||
|
||||
self.current_csv_writer.writerow(row)
|
||||
self.current_csv_file.flush()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error writing CSV data: {e}")
|
||||
|
||||
def get_streaming_data(self, all_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Filter data for streaming based on selected variables"""
|
||||
if not self.streaming_variables:
|
||||
return all_data
|
||||
|
||||
return {
|
||||
name: value
|
||||
for name, value in all_data.items()
|
||||
if name in self.streaming_variables
|
||||
}
|
||||
|
||||
def start_csv_recording(self) -> bool:
|
||||
"""Start CSV recording"""
|
||||
if not self.connected:
|
||||
self.logger.error("PLC not connected")
|
||||
return False
|
||||
|
||||
if not self.variables:
|
||||
self.logger.error("No variables configured")
|
||||
return False
|
||||
|
||||
self.csv_recording = True
|
||||
self.logger.info("CSV recording started")
|
||||
return True
|
||||
|
||||
def stop_csv_recording(self):
|
||||
"""Stop CSV recording"""
|
||||
self.csv_recording = False
|
||||
|
||||
if self.current_csv_file:
|
||||
self.current_csv_file.close()
|
||||
self.current_csv_file = None
|
||||
self.current_csv_writer = None
|
||||
self.current_hour = None
|
||||
|
||||
self.logger.info("CSV recording stopped")
|
||||
|
||||
def connect_plc(self) -> bool:
|
||||
"""Connect to S7-315 PLC"""
|
||||
try:
|
||||
|
@ -254,15 +393,24 @@ class PLCDataStreamer:
|
|||
start_time = time.time()
|
||||
|
||||
# Read all variables
|
||||
data = self.read_all_variables()
|
||||
all_data = self.read_all_variables()
|
||||
|
||||
if data:
|
||||
# Send to PlotJuggler
|
||||
self.send_to_plotjuggler(data)
|
||||
if all_data:
|
||||
# Write to CSV (all variables)
|
||||
self.write_csv_data(all_data)
|
||||
|
||||
# Get filtered data for streaming
|
||||
streaming_data = self.get_streaming_data(all_data)
|
||||
|
||||
# Send filtered data to PlotJuggler
|
||||
if streaming_data:
|
||||
self.send_to_plotjuggler(streaming_data)
|
||||
|
||||
# Log data
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
|
||||
self.logger.info(f"[{timestamp}] {data}")
|
||||
self.logger.info(
|
||||
f"[{timestamp}] CSV: {len(all_data)} vars, Streaming: {len(streaming_data)} vars"
|
||||
)
|
||||
|
||||
# Maintain sampling interval
|
||||
elapsed = time.time() - start_time
|
||||
|
@ -286,12 +434,15 @@ class PLCDataStreamer:
|
|||
if not self.setup_udp_socket():
|
||||
return False
|
||||
|
||||
# Start CSV recording automatically
|
||||
self.start_csv_recording()
|
||||
|
||||
self.streaming = True
|
||||
self.stream_thread = threading.Thread(target=self.streaming_loop)
|
||||
self.stream_thread.daemon = True
|
||||
self.stream_thread.start()
|
||||
|
||||
self.logger.info("Streaming started")
|
||||
self.logger.info("Streaming and CSV recording started")
|
||||
return True
|
||||
|
||||
def stop_streaming(self):
|
||||
|
@ -300,21 +451,29 @@ class PLCDataStreamer:
|
|||
if self.stream_thread:
|
||||
self.stream_thread.join(timeout=2)
|
||||
|
||||
# Stop CSV recording
|
||||
self.stop_csv_recording()
|
||||
|
||||
if self.udp_socket:
|
||||
self.udp_socket.close()
|
||||
self.udp_socket = None
|
||||
|
||||
self.logger.info("Streaming stopped")
|
||||
self.logger.info("Streaming and CSV recording stopped")
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""Get current system status"""
|
||||
return {
|
||||
"plc_connected": self.connected,
|
||||
"streaming": self.streaming,
|
||||
"csv_recording": self.csv_recording,
|
||||
"plc_config": self.plc_config,
|
||||
"udp_config": self.udp_config,
|
||||
"variables_count": len(self.variables),
|
||||
"streaming_variables_count": len(self.streaming_variables),
|
||||
"sampling_interval": self.sampling_interval,
|
||||
"current_csv_file": (
|
||||
self.get_csv_file_path() if self.csv_recording else None
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -322,6 +481,12 @@ class PLCDataStreamer:
|
|||
streamer = PLCDataStreamer()
|
||||
|
||||
|
||||
@app.route("/images/<filename>")
|
||||
def serve_image(filename):
|
||||
"""Serve images from .images directory"""
|
||||
return send_from_directory(".images", filename)
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""Main page"""
|
||||
|
@ -405,6 +570,31 @@ def remove_variable(name):
|
|||
return jsonify({"success": True, "message": f"Variable {name} removed"})
|
||||
|
||||
|
||||
@app.route("/api/variables/<name>/streaming", methods=["POST"])
|
||||
def toggle_variable_streaming(name):
|
||||
"""Toggle streaming for a specific variable"""
|
||||
try:
|
||||
data = request.get_json()
|
||||
enabled = data.get("enabled", False)
|
||||
|
||||
streamer.toggle_streaming_variable(name, enabled)
|
||||
status = "enabled" if enabled else "disabled"
|
||||
return jsonify(
|
||||
{"success": True, "message": f"Variable {name} streaming {status}"}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"success": False, "message": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/variables/streaming", methods=["GET"])
|
||||
def get_streaming_variables():
|
||||
"""Get list of variables enabled for streaming"""
|
||||
return jsonify(
|
||||
{"success": True, "streaming_variables": list(streamer.streaming_variables)}
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/streaming/start", methods=["POST"])
|
||||
def start_streaming():
|
||||
"""Start streaming"""
|
||||
|
@ -438,6 +628,25 @@ def update_sampling():
|
|||
return jsonify({"success": False, "message": str(e)}), 400
|
||||
|
||||
|
||||
@app.route("/api/csv/start", methods=["POST"])
|
||||
def start_csv_recording():
|
||||
"""Start CSV recording independently"""
|
||||
if streamer.start_csv_recording():
|
||||
return jsonify({"success": True, "message": "CSV recording started"})
|
||||
else:
|
||||
return (
|
||||
jsonify({"success": False, "message": "Error starting CSV recording"}),
|
||||
500,
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/csv/stop", methods=["POST"])
|
||||
def stop_csv_recording():
|
||||
"""Stop CSV recording independently"""
|
||||
streamer.stop_csv_recording()
|
||||
return jsonify({"success": True, "message": "CSV recording stopped"})
|
||||
|
||||
|
||||
@app.route("/api/status")
|
||||
def get_status():
|
||||
"""Get current status"""
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"plc_config": {
|
||||
"ip": "10.1.33.11",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
},
|
||||
"udp_config": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9870
|
||||
},
|
||||
"sampling_interval": 0.1
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"UR29_Brix": {
|
||||
"db": 2121,
|
||||
"offset": 18,
|
||||
"type": "real"
|
||||
},
|
||||
"UR62_Brix": {
|
||||
"db": 2122,
|
||||
"offset": 18,
|
||||
"type": "real"
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -35,6 +35,16 @@
|
|||
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 {
|
||||
|
@ -223,9 +233,32 @@
|
|||
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;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 45px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
|
@ -242,7 +275,10 @@
|
|||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🏭 PLC S7-315 Streamer & Logger</h1>
|
||||
<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>
|
||||
|
||||
|
@ -261,6 +297,9 @@
|
|||
<div class="status-item status-idle">
|
||||
⏱️ Interval: {{ status.sampling_interval }}s
|
||||
</div>
|
||||
<div class="status-item" id="csv-status">
|
||||
💾 CSV: Inactive
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -359,6 +398,7 @@
|
|||
<th>Data Block</th>
|
||||
<th>Offset</th>
|
||||
<th>Type</th>
|
||||
<th>Stream to PlotJuggler</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -369,6 +409,11 @@
|
|||
<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>
|
||||
|
@ -378,12 +423,32 @@
|
|||
</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</button>
|
||||
<button class="btn btn-danger" id="stop-streaming-btn">⏹️ Stop Streaming</button>
|
||||
<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>
|
||||
|
@ -407,6 +472,7 @@
|
|||
.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';
|
||||
|
@ -417,12 +483,28 @@
|
|||
}
|
||||
|
||||
if (data.streaming) {
|
||||
streamStatus.textContent = '📡 Streaming: Active';
|
||||
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));
|
||||
}
|
||||
|
@ -510,6 +592,41 @@
|
|||
});
|
||||
});
|
||||
|
||||
// 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}"?`)) {
|
||||
|
@ -558,11 +675,32 @@
|
|||
});
|
||||
});
|
||||
|
||||
// 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();
|
||||
});
|
||||
});
|
||||
|
||||
// Update status every 5 seconds
|
||||
setInterval(updateStatus, 5000);
|
||||
|
||||
// Initial update
|
||||
updateStatus();
|
||||
loadStreamingStatus();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
|
Loading…
Reference in New Issue