Refactorizado de index.html
This commit is contained in:
parent
5ea9e51cd4
commit
a37cb8be3b
|
@ -2,7 +2,7 @@
|
|||
|
||||
## PLC S7-315 Streamer & Logger
|
||||
|
||||
Functional Description of the Application
|
||||
## Functional Description of the Application
|
||||
|
||||
This application is a web server (created with the Flask framework in Python) that acts as an intermediary to monitor and record data in CSV format from a PLC Siemens S7 with the SNAP7 library to be used on a low-resource PC connected to the PLC.
|
||||
It must be as simple as possible to allow the pack using PyInstaller
|
||||
|
|
|
@ -2527,8 +2527,232 @@
|
|||
"rack": 0,
|
||||
"slot": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T19:55:09.970238",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T19:55:21.690709",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.249",
|
||||
"details": {
|
||||
"ip": "10.1.33.249",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T19:55:32.161728",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "dar",
|
||||
"variables_count": 6,
|
||||
"streaming_count": 4,
|
||||
"prefix": "dar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T19:55:32.167727",
|
||||
"level": "info",
|
||||
"event_type": "streaming_started",
|
||||
"message": "Multi-dataset streaming started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 2,
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T22:58:33.195123",
|
||||
"level": "info",
|
||||
"event_type": "application_started",
|
||||
"message": "Application initialization completed successfully",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:00:02.947627",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.249",
|
||||
"details": {
|
||||
"ip": "10.1.33.249",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:00:05.459324",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.249",
|
||||
"details": {
|
||||
"ip": "10.1.33.249",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:00:10.767162",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "dar",
|
||||
"variables_count": 6,
|
||||
"streaming_count": 4,
|
||||
"prefix": "dar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:00:10.770176",
|
||||
"level": "info",
|
||||
"event_type": "streaming_started",
|
||||
"message": "Multi-dataset streaming started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 2,
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:00:21.646936",
|
||||
"level": "info",
|
||||
"event_type": "dataset_deactivated",
|
||||
"message": "Dataset deactivated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "dar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:00:21.651937",
|
||||
"level": "info",
|
||||
"event_type": "streaming_stopped",
|
||||
"message": "Multi-dataset streaming stopped: 1 datasets deactivated",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:00:21.655936",
|
||||
"level": "info",
|
||||
"event_type": "streaming_stopped",
|
||||
"message": "Multi-dataset streaming stopped: 0 datasets deactivated",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:00:21.658937",
|
||||
"level": "info",
|
||||
"event_type": "plc_disconnection",
|
||||
"message": "Disconnected from PLC 10.1.33.249",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:01:23.105142",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.249",
|
||||
"details": {
|
||||
"ip": "10.1.33.249",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:01:33.860399",
|
||||
"level": "info",
|
||||
"event_type": "streaming_stopped",
|
||||
"message": "Multi-dataset streaming stopped: 0 datasets deactivated",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:01:33.863399",
|
||||
"level": "info",
|
||||
"event_type": "streaming_stopped",
|
||||
"message": "Multi-dataset streaming stopped: 0 datasets deactivated",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:01:33.866398",
|
||||
"level": "info",
|
||||
"event_type": "plc_disconnection",
|
||||
"message": "Disconnected from PLC 10.1.33.249",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:02:06.003577",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.249",
|
||||
"details": {
|
||||
"ip": "10.1.33.249",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:02:11.447466",
|
||||
"level": "info",
|
||||
"event_type": "streaming_stopped",
|
||||
"message": "Multi-dataset streaming stopped: 0 datasets deactivated",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:02:11.452936",
|
||||
"level": "info",
|
||||
"event_type": "streaming_stopped",
|
||||
"message": "Multi-dataset streaming stopped: 0 datasets deactivated",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:02:11.456927",
|
||||
"level": "info",
|
||||
"event_type": "plc_disconnection",
|
||||
"message": "Disconnected from PLC 10.1.33.249",
|
||||
"details": {}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:03:30.463799",
|
||||
"level": "info",
|
||||
"event_type": "plc_connection",
|
||||
"message": "Successfully connected to PLC 10.1.33.249",
|
||||
"details": {
|
||||
"ip": "10.1.33.249",
|
||||
"rack": 0,
|
||||
"slot": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:03:40.709674",
|
||||
"level": "info",
|
||||
"event_type": "dataset_activated",
|
||||
"message": "Dataset activated: DAR",
|
||||
"details": {
|
||||
"dataset_id": "dar",
|
||||
"variables_count": 6,
|
||||
"streaming_count": 4,
|
||||
"prefix": "dar"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-07-20T23:03:40.714677",
|
||||
"level": "info",
|
||||
"event_type": "streaming_started",
|
||||
"message": "Multi-dataset streaming started: 1 datasets activated",
|
||||
"details": {
|
||||
"activated_datasets": 1,
|
||||
"total_datasets": 2,
|
||||
"udp_host": "127.0.0.1",
|
||||
"udp_port": 9870
|
||||
}
|
||||
}
|
||||
],
|
||||
"last_updated": "2025-07-20T10:58:20.099906",
|
||||
"total_entries": 236
|
||||
"last_updated": "2025-07-20T23:03:40.714677",
|
||||
"total_entries": 260
|
||||
}
|
|
@ -248,7 +248,15 @@ class PLCClient:
|
|||
def read_multiple_variables(
|
||||
self, variables: Dict[str, Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""Read multiple variables from the PLC"""
|
||||
"""Read multiple variables from the PLC
|
||||
|
||||
⚠️ INTERNAL USE ONLY - APIs should use cache instead
|
||||
This method performs direct PLC reads and should only be called by:
|
||||
- DataStreamer.read_dataset_variables() during streaming cycles
|
||||
- Internal diagnostic operations
|
||||
|
||||
Public APIs should use cached values via DataStreamer.get_cached_dataset_values()
|
||||
"""
|
||||
if not self.is_connected():
|
||||
return {}
|
||||
|
||||
|
@ -267,7 +275,17 @@ class PLCClient:
|
|||
def read_multiple_variables_with_diagnostics(
|
||||
self, variables: Dict[str, Dict[str, Any]]
|
||||
) -> Dict[str, Any]:
|
||||
"""Read multiple variables from the PLC with detailed error diagnostics"""
|
||||
"""Read multiple variables from the PLC with detailed error diagnostics
|
||||
|
||||
⚠️ INTERNAL USE ONLY - APIs should use cache instead
|
||||
This method performs direct PLC reads and should only be called by:
|
||||
- DataStreamer.read_dataset_variables() during streaming cycles
|
||||
- Internal diagnostic operations
|
||||
|
||||
Public APIs should use cached values via DataStreamer.get_cached_dataset_values()
|
||||
according to the application's single-read principle where variables are read
|
||||
only once per dataset interval and cached for all other uses.
|
||||
"""
|
||||
if not self.is_connected():
|
||||
return {
|
||||
"success": False,
|
||||
|
@ -314,33 +332,24 @@ class PLCClient:
|
|||
|
||||
except Exception as e:
|
||||
data[var_name] = None
|
||||
error_msg = f"Unexpected error: {type(e).__name__}: {str(e)}"
|
||||
errors[var_name] = error_msg
|
||||
errors[var_name] = f"Unexpected error: {str(e)}"
|
||||
if self.logger:
|
||||
self.logger.error(f"Unexpected error reading {var_name}: {e}")
|
||||
|
||||
# Determine overall success
|
||||
if success_count == 0:
|
||||
if total_count == 0:
|
||||
return {
|
||||
"success": True,
|
||||
"message": "No variables to read",
|
||||
"values": {},
|
||||
"errors": {},
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Failed to read any variables",
|
||||
"error_type": "all_failed",
|
||||
"values": data,
|
||||
"errors": errors,
|
||||
"stats": {
|
||||
"success": 0,
|
||||
"failed": total_count,
|
||||
"total": total_count,
|
||||
},
|
||||
}
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Failed to read any variables (0/{total_count})",
|
||||
"error_type": "all_failed",
|
||||
"values": data,
|
||||
"errors": errors,
|
||||
"stats": {
|
||||
"success": success_count,
|
||||
"failed": total_count - success_count,
|
||||
"total": total_count,
|
||||
},
|
||||
}
|
||||
elif success_count < total_count:
|
||||
return {
|
||||
"success": True,
|
||||
|
@ -356,10 +365,13 @@ class PLCClient:
|
|||
else:
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Successfully read all {total_count} variables",
|
||||
"values": data,
|
||||
"errors": {},
|
||||
"stats": {"success": success_count, "failed": 0, "total": total_count},
|
||||
"stats": {
|
||||
"success": success_count,
|
||||
"failed": 0,
|
||||
"total": total_count,
|
||||
},
|
||||
}
|
||||
|
||||
def get_connection_info(self) -> Dict[str, Any]:
|
||||
|
|
|
@ -22,7 +22,22 @@ def resource_path(relative_path):
|
|||
|
||||
|
||||
class DataStreamer:
|
||||
"""Handles data streaming, CSV recording, and dataset management"""
|
||||
"""Handles data streaming, CSV recording, and dataset management
|
||||
|
||||
🔑 CORE PRINCIPLE: Single PLC Read per Dataset Interval
|
||||
========================================================
|
||||
This class implements the application's core principle of reading PLC variables
|
||||
only once per dataset at their configured sampling intervals, then using cached
|
||||
values for all other operations (CSV recording, UDP streaming, web interface).
|
||||
|
||||
Data Flow:
|
||||
1. dataset_streaming_loop() reads ALL variables in a dataset at configured interval
|
||||
2. read_dataset_variables() performs the actual PLC read and updates cache
|
||||
3. All other functions (APIs, streaming, frontend) use get_cached_dataset_values()
|
||||
4. NO direct PLC reads outside of the streaming loops
|
||||
|
||||
This protects the PLC from overload and ensures data consistency across all outputs.
|
||||
"""
|
||||
|
||||
def __init__(self, config_manager, plc_client, event_logger, logger=None):
|
||||
"""Initialize data streamer"""
|
||||
|
@ -44,7 +59,9 @@ class DataStreamer:
|
|||
self.dataset_csv_hours = {} # dataset_id -> current hour
|
||||
self.dataset_using_modification_files = {} # dataset_id -> bool
|
||||
|
||||
# Cache for last read values (exactly what's being written to CSV)
|
||||
# 📊 CACHE SYSTEM - Central data storage
|
||||
# This is the ONLY source of data for all APIs and interfaces
|
||||
# Updated ONLY by read_dataset_variables() during streaming cycles
|
||||
self.last_read_values = {} # dataset_id -> {var_name: value}
|
||||
self.last_read_timestamps = {} # dataset_id -> timestamp
|
||||
self.last_read_errors = {} # dataset_id -> {var_name: error_message}
|
||||
|
@ -281,7 +298,18 @@ class DataStreamer:
|
|||
def read_dataset_variables(
|
||||
self, dataset_id: str, variables: Dict[str, Any]
|
||||
) -> Dict[str, Any]:
|
||||
"""Read all variables for a specific dataset and update cache"""
|
||||
"""Read all variables for a specific dataset and update cache
|
||||
|
||||
🔑 THIS IS THE ONLY FUNCTION THAT READS FROM PLC
|
||||
================================================
|
||||
This function implements the application's core principle:
|
||||
- Variables are read from PLC ONLY here, at dataset intervals
|
||||
- All other functions must use get_cached_dataset_values()
|
||||
- This ensures single read per interval, protecting PLC from overload
|
||||
|
||||
Called by: dataset_streaming_loop() at configured intervals
|
||||
Updates: self.last_read_values cache for use by all other functions
|
||||
"""
|
||||
data = {}
|
||||
errors = {}
|
||||
timestamp = datetime.now()
|
||||
|
@ -323,7 +351,19 @@ class DataStreamer:
|
|||
return data
|
||||
|
||||
def get_cached_dataset_values(self, dataset_id: str) -> Dict[str, Any]:
|
||||
"""Get cached values for a dataset (values used for CSV generation)"""
|
||||
"""Get cached values for a dataset (values used for CSV generation)
|
||||
|
||||
🎯 CACHE-ONLY DATA ACCESS - NO PLC READS
|
||||
========================================
|
||||
This function provides the ONLY way for APIs and interfaces to access
|
||||
variable values according to the application's design principle.
|
||||
|
||||
Data Source: Cache populated by read_dataset_variables() only
|
||||
Used by: Web APIs, frontend refresh, SSE streaming, monitoring
|
||||
Never: Performs direct PLC reads (use read_dataset_variables for that)
|
||||
|
||||
Returns formatted response with success/error status and detailed diagnostics
|
||||
"""
|
||||
if dataset_id not in self.last_read_values:
|
||||
return {
|
||||
"success": False,
|
||||
|
|
359
main.py
359
main.py
|
@ -552,7 +552,7 @@ def get_streaming_variables():
|
|||
|
||||
@app.route("/api/datasets/<dataset_id>/variables/values", methods=["GET"])
|
||||
def get_dataset_variable_values(dataset_id):
|
||||
"""Get current values of all variables in a dataset"""
|
||||
"""Get current values of all variables in a dataset - CACHE ONLY"""
|
||||
error_response = check_streamer_initialized()
|
||||
if error_response:
|
||||
return error_response
|
||||
|
@ -567,19 +567,6 @@ def get_dataset_variable_values(dataset_id):
|
|||
404,
|
||||
)
|
||||
|
||||
# Check if PLC is connected
|
||||
if not streamer.plc_client.is_connected():
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "PLC not connected. Please connect to PLC first.",
|
||||
"values": {},
|
||||
}
|
||||
),
|
||||
400,
|
||||
)
|
||||
|
||||
# Get dataset variables
|
||||
dataset_variables = streamer.get_dataset_variables(dataset_id)
|
||||
|
||||
|
@ -589,28 +576,83 @@ def get_dataset_variable_values(dataset_id):
|
|||
"success": True,
|
||||
"message": "No variables defined in this dataset",
|
||||
"values": {},
|
||||
"source": "no_variables",
|
||||
}
|
||||
)
|
||||
|
||||
# First, try to get cached values (values used for CSV generation)
|
||||
if streamer.has_cached_values(dataset_id):
|
||||
read_result = streamer.get_cached_dataset_values(dataset_id)
|
||||
|
||||
# Convert timestamp from ISO format to readable format for consistency
|
||||
if read_result.get("timestamp"):
|
||||
try:
|
||||
cached_timestamp = datetime.fromisoformat(read_result["timestamp"])
|
||||
read_result["timestamp"] = cached_timestamp.strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
except:
|
||||
pass # Keep original timestamp if conversion fails
|
||||
else:
|
||||
# Fallback: Read directly from PLC if no cached values available
|
||||
read_result = streamer.plc_client.read_multiple_variables_with_diagnostics(
|
||||
dataset_variables
|
||||
# Check if dataset is active (required for cache to be populated)
|
||||
if dataset_id not in streamer.active_datasets:
|
||||
return jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": f"Dataset '{dataset_id}' is not active. Activate the dataset to start reading variables and populate cache.",
|
||||
"error_type": "dataset_inactive",
|
||||
"values": {},
|
||||
"detailed_errors": {},
|
||||
"stats": {
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
"total": len(dataset_variables),
|
||||
},
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"source": "no_cache_dataset_inactive",
|
||||
"is_cached": False,
|
||||
}
|
||||
)
|
||||
read_result["source"] = "plc_direct"
|
||||
|
||||
# Check if PLC is connected (required for streaming to populate cache)
|
||||
if not streamer.plc_client.is_connected():
|
||||
return jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": "PLC not connected. Connect to PLC and activate dataset to populate cache.",
|
||||
"error_type": "plc_disconnected",
|
||||
"values": {},
|
||||
"detailed_errors": {},
|
||||
"stats": {
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
"total": len(dataset_variables),
|
||||
},
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"source": "no_cache_plc_disconnected",
|
||||
"is_cached": False,
|
||||
}
|
||||
)
|
||||
|
||||
# Get cached values - this is the ONLY source of data according to application principles
|
||||
if not streamer.has_cached_values(dataset_id):
|
||||
return jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": f"No cached values available for dataset '{dataset_id}'. Cache is populated by the streaming process at the dataset's configured interval. Please wait for the next reading cycle.",
|
||||
"error_type": "no_cache_available",
|
||||
"values": {},
|
||||
"detailed_errors": {},
|
||||
"stats": {
|
||||
"success": 0,
|
||||
"failed": 0,
|
||||
"total": len(dataset_variables),
|
||||
},
|
||||
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"source": "no_cache_available",
|
||||
"is_cached": False,
|
||||
"note": f"Dataset reads every {streamer.config_manager.get_dataset_sampling_interval(dataset_id)}s",
|
||||
}
|
||||
)
|
||||
|
||||
# Get cached values (the ONLY valid source according to application design)
|
||||
read_result = streamer.get_cached_dataset_values(dataset_id)
|
||||
|
||||
# Convert timestamp from ISO format to readable format for consistency
|
||||
if read_result.get("timestamp"):
|
||||
try:
|
||||
cached_timestamp = datetime.fromisoformat(read_result["timestamp"])
|
||||
read_result["timestamp"] = cached_timestamp.strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
except:
|
||||
pass # Keep original timestamp if conversion fails
|
||||
|
||||
# Extract values and handle diagnostics
|
||||
if not read_result.get("success", False):
|
||||
|
@ -621,15 +663,12 @@ def get_dataset_variable_values(dataset_id):
|
|||
# Log detailed error information
|
||||
if streamer.logger:
|
||||
streamer.logger.error(
|
||||
f"Failed to read any variables from dataset '{dataset_id}': {error_msg}"
|
||||
f"Cached values indicate failure for dataset '{dataset_id}': {error_msg}"
|
||||
)
|
||||
if read_result.get("errors"):
|
||||
for var_name, var_error in read_result["errors"].items():
|
||||
streamer.logger.error(f" Variable '{var_name}': {var_error}")
|
||||
|
||||
# Determine source for error case
|
||||
error_source = read_result.get("source", "unknown")
|
||||
|
||||
return (
|
||||
jsonify(
|
||||
{
|
||||
|
@ -642,8 +681,8 @@ def get_dataset_variable_values(dataset_id):
|
|||
"timestamp": read_result.get(
|
||||
"timestamp", datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
),
|
||||
"source": error_source,
|
||||
"is_cached": error_source == "cache",
|
||||
"source": "cache",
|
||||
"is_cached": True,
|
||||
}
|
||||
),
|
||||
500,
|
||||
|
@ -704,25 +743,23 @@ def get_dataset_variable_values(dataset_id):
|
|||
success_vars = stats.get("success", 0)
|
||||
failed_vars = stats.get("failed", 0)
|
||||
|
||||
# Determine data source for message
|
||||
data_source = read_result.get("source", "unknown")
|
||||
source_text = ""
|
||||
if data_source == "cache":
|
||||
source_text = " (from last streaming cycle)"
|
||||
elif data_source == "plc_direct":
|
||||
source_text = " (direct PLC read)"
|
||||
# Determine data source for message (always cache now)
|
||||
data_source = "cache"
|
||||
source_text = " (from streaming cache)"
|
||||
|
||||
if failed_vars == 0:
|
||||
message = f"Successfully read all {success_vars} variables{source_text}"
|
||||
message = (
|
||||
f"Successfully displaying all {success_vars} variables{source_text}"
|
||||
)
|
||||
response_success = True
|
||||
else:
|
||||
message = f"Partial success: {success_vars}/{total_vars} variables read successfully, {failed_vars} failed{source_text}"
|
||||
message = f"Partial data available: {success_vars}/{total_vars} variables have valid cached values, {failed_vars} failed{source_text}"
|
||||
response_success = True # Still success if we got some values
|
||||
|
||||
# Log warnings for partial failures
|
||||
if streamer.logger:
|
||||
streamer.logger.warning(
|
||||
f"Partial failure reading variables from dataset '{dataset_id}': {message}"
|
||||
f"Partial failure in cached values for dataset '{dataset_id}': {message}"
|
||||
)
|
||||
for var_name, var_error in error_details.items():
|
||||
if formatted_values.get(var_name) == "ERROR":
|
||||
|
@ -740,7 +777,8 @@ def get_dataset_variable_values(dataset_id):
|
|||
),
|
||||
"warning": read_result.get("warning"),
|
||||
"source": data_source,
|
||||
"is_cached": data_source == "cache",
|
||||
"is_cached": True,
|
||||
"cache_info": f"Dataset reads every {streamer.config_manager.get_dataset_sampling_interval(dataset_id)}s",
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -749,7 +787,7 @@ def get_dataset_variable_values(dataset_id):
|
|||
jsonify(
|
||||
{
|
||||
"success": False,
|
||||
"message": f"Error reading variable values: {str(e)}",
|
||||
"message": f"Error retrieving cached variable values: {str(e)}",
|
||||
"values": {},
|
||||
}
|
||||
),
|
||||
|
@ -1146,119 +1184,150 @@ def stream_variables():
|
|||
return jsonify({"error": f"Dataset {dataset_id} not found"}), 404
|
||||
|
||||
def generate():
|
||||
"""Generate SSE data stream"""
|
||||
"""Generate SSE data stream - CACHE ONLY"""
|
||||
|
||||
# Send initial connection message
|
||||
yield f"data: {json.dumps({'type': 'connected', 'message': 'SSE connection established'})}\n\n"
|
||||
yield f"data: {json.dumps({'type': 'connected', 'message': 'SSE connection established - monitoring cache'})}\n\n"
|
||||
|
||||
last_values = {}
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Get current variable values
|
||||
if streamer.plc_client.is_connected():
|
||||
dataset_variables = streamer.get_dataset_variables(dataset_id)
|
||||
|
||||
if dataset_variables:
|
||||
# Try to get cached values first
|
||||
if streamer.has_cached_values(dataset_id):
|
||||
read_result = streamer.get_cached_dataset_values(dataset_id)
|
||||
else:
|
||||
# Fallback to direct PLC read
|
||||
read_result = streamer.plc_client.read_multiple_variables_with_diagnostics(
|
||||
dataset_variables
|
||||
)
|
||||
read_result["source"] = "plc_direct"
|
||||
|
||||
if read_result.get("success", False):
|
||||
values = read_result.get("values", {})
|
||||
timestamp = read_result.get(
|
||||
"timestamp", datetime.now().isoformat()
|
||||
)
|
||||
|
||||
# Format values for display
|
||||
formatted_values = {}
|
||||
for var_name, value in values.items():
|
||||
if value is not None:
|
||||
var_config = dataset_variables[var_name]
|
||||
var_type = var_config.get("type", "unknown")
|
||||
|
||||
try:
|
||||
if var_type == "real":
|
||||
formatted_values[var_name] = (
|
||||
f"{value:.3f}"
|
||||
if isinstance(value, (int, float))
|
||||
else str(value)
|
||||
)
|
||||
elif var_type == "bool":
|
||||
formatted_values[var_name] = (
|
||||
"TRUE" if value else "FALSE"
|
||||
)
|
||||
elif var_type in [
|
||||
"int",
|
||||
"uint",
|
||||
"dint",
|
||||
"udint",
|
||||
"word",
|
||||
"byte",
|
||||
"sint",
|
||||
"usint",
|
||||
]:
|
||||
formatted_values[var_name] = (
|
||||
str(int(value))
|
||||
if isinstance(value, (int, float))
|
||||
else str(value)
|
||||
)
|
||||
else:
|
||||
formatted_values[var_name] = str(value)
|
||||
except:
|
||||
formatted_values[var_name] = "FORMAT_ERROR"
|
||||
else:
|
||||
formatted_values[var_name] = "ERROR"
|
||||
|
||||
# Only send if values changed
|
||||
if formatted_values != last_values:
|
||||
data = {
|
||||
"type": "values",
|
||||
"values": formatted_values,
|
||||
"timestamp": timestamp,
|
||||
"source": read_result.get("source", "unknown"),
|
||||
"stats": read_result.get("stats", {}),
|
||||
}
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
last_values = formatted_values.copy()
|
||||
else:
|
||||
# Send error data
|
||||
error_data = {
|
||||
"type": "error",
|
||||
"message": read_result.get("error", "Unknown error"),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
yield f"data: {json.dumps(error_data)}\n\n"
|
||||
else:
|
||||
# No variables in dataset
|
||||
data = {
|
||||
"type": "no_variables",
|
||||
"message": "No variables defined in this dataset",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
else:
|
||||
# PLC not connected
|
||||
# Check basic preconditions for cache availability
|
||||
if not streamer.plc_client.is_connected():
|
||||
# PLC not connected - cache won't be populated
|
||||
data = {
|
||||
"type": "plc_disconnected",
|
||||
"message": "PLC not connected",
|
||||
"message": "PLC not connected - cache not being populated",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
time.sleep(interval)
|
||||
continue
|
||||
|
||||
# Check if dataset is active
|
||||
if dataset_id not in streamer.active_datasets:
|
||||
data = {
|
||||
"type": "dataset_inactive",
|
||||
"message": f"Dataset '{dataset_id}' is not active - activate to populate cache",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
time.sleep(interval)
|
||||
continue
|
||||
|
||||
# Get dataset variables
|
||||
dataset_variables = streamer.get_dataset_variables(dataset_id)
|
||||
|
||||
if not dataset_variables:
|
||||
# No variables in dataset
|
||||
data = {
|
||||
"type": "no_variables",
|
||||
"message": "No variables defined in this dataset",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
time.sleep(interval)
|
||||
continue
|
||||
|
||||
# Get cached values - the ONLY source according to application principles
|
||||
if not streamer.has_cached_values(dataset_id):
|
||||
# No cache available yet - dataset might be starting up
|
||||
sampling_interval = (
|
||||
streamer.config_manager.get_dataset_sampling_interval(
|
||||
dataset_id
|
||||
)
|
||||
)
|
||||
data = {
|
||||
"type": "no_cache",
|
||||
"message": f"Waiting for cache to be populated (dataset reads every {sampling_interval}s)",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"sampling_interval": sampling_interval,
|
||||
}
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
time.sleep(interval)
|
||||
continue
|
||||
|
||||
# Get cached values (the ONLY valid source)
|
||||
read_result = streamer.get_cached_dataset_values(dataset_id)
|
||||
|
||||
if read_result.get("success", False):
|
||||
values = read_result.get("values", {})
|
||||
timestamp = read_result.get("timestamp", datetime.now().isoformat())
|
||||
|
||||
# Format values for display
|
||||
formatted_values = {}
|
||||
for var_name, value in values.items():
|
||||
if value is not None:
|
||||
var_config = dataset_variables[var_name]
|
||||
var_type = var_config.get("type", "unknown")
|
||||
|
||||
try:
|
||||
if var_type == "real":
|
||||
formatted_values[var_name] = (
|
||||
f"{value:.3f}"
|
||||
if isinstance(value, (int, float))
|
||||
else str(value)
|
||||
)
|
||||
elif var_type == "bool":
|
||||
formatted_values[var_name] = (
|
||||
"TRUE" if value else "FALSE"
|
||||
)
|
||||
elif var_type in [
|
||||
"int",
|
||||
"uint",
|
||||
"dint",
|
||||
"udint",
|
||||
"word",
|
||||
"byte",
|
||||
"sint",
|
||||
"usint",
|
||||
]:
|
||||
formatted_values[var_name] = (
|
||||
str(int(value))
|
||||
if isinstance(value, (int, float))
|
||||
else str(value)
|
||||
)
|
||||
else:
|
||||
formatted_values[var_name] = str(value)
|
||||
except:
|
||||
formatted_values[var_name] = "FORMAT_ERROR"
|
||||
else:
|
||||
formatted_values[var_name] = "ERROR"
|
||||
|
||||
# Only send if values changed
|
||||
if formatted_values != last_values:
|
||||
data = {
|
||||
"type": "values",
|
||||
"values": formatted_values,
|
||||
"timestamp": timestamp,
|
||||
"source": "cache",
|
||||
"stats": read_result.get("stats", {}),
|
||||
"cache_age_info": f"Dataset reads every {streamer.config_manager.get_dataset_sampling_interval(dataset_id)}s",
|
||||
}
|
||||
yield f"data: {json.dumps(data)}\n\n"
|
||||
last_values = formatted_values.copy()
|
||||
else:
|
||||
# Send error data from cache
|
||||
error_data = {
|
||||
"type": "cache_error",
|
||||
"message": read_result.get(
|
||||
"error", "Cached data indicates error"
|
||||
),
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"error_type": read_result.get("error_type", "unknown"),
|
||||
"source": "cache",
|
||||
}
|
||||
yield f"data: {json.dumps(error_data)}\n\n"
|
||||
|
||||
time.sleep(interval)
|
||||
|
||||
except Exception as e:
|
||||
error_data = {
|
||||
"type": "error",
|
||||
"message": f"Stream error: {str(e)}",
|
||||
"type": "stream_error",
|
||||
"message": f"SSE stream error: {str(e)}",
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"source": "cache_monitoring",
|
||||
}
|
||||
yield f"data: {json.dumps(error_data)}\n\n"
|
||||
time.sleep(interval)
|
||||
|
|
|
@ -70,5 +70,5 @@
|
|||
],
|
||||
"current_dataset_id": "dar",
|
||||
"version": "1.0",
|
||||
"last_update": "2025-07-19T23:55:26.866407"
|
||||
"last_update": "2025-07-20T23:03:40.707669"
|
||||
}
|
|
@ -0,0 +1,520 @@
|
|||
/* Header with logo */
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 1.2em;
|
||||
width: auto;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Status grid */
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding: 1rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
background: var(--pico-card-background-color);
|
||||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||||
}
|
||||
|
||||
.status-connected {
|
||||
background: var(--pico-primary-background);
|
||||
color: var(--pico-primary-inverse);
|
||||
}
|
||||
|
||||
.status-disconnected {
|
||||
background: var(--pico-secondary-background);
|
||||
color: var(--pico-secondary-inverse);
|
||||
}
|
||||
|
||||
.status-streaming {
|
||||
background: var(--pico-primary-background);
|
||||
color: var(--pico-primary-inverse);
|
||||
}
|
||||
|
||||
.status-idle {
|
||||
background: var(--pico-muted-background-color);
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
/* Configuration grid */
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Form styling */
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Variables table */
|
||||
.variables-table {
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* Variable value cells styling */
|
||||
.variables-table td[id^="value-"] {
|
||||
font-family: var(--pico-font-family-monospace);
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
/* Refresh button styling */
|
||||
#refresh-values-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Diagnose button styling */
|
||||
#diagnose-btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Last refresh time styling */
|
||||
#last-refresh-time {
|
||||
font-style: italic;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Error cell tooltips */
|
||||
.variables-table td[id^="value-"]:hover {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Variable value status colors */
|
||||
.value-success {
|
||||
color: var(--pico-color-green-600) !important;
|
||||
}
|
||||
|
||||
.value-error {
|
||||
color: var(--pico-color-red-500) !important;
|
||||
}
|
||||
|
||||
.value-warning {
|
||||
color: var(--pico-color-amber-600) !important;
|
||||
}
|
||||
|
||||
.value-offline {
|
||||
color: var(--pico-muted-color) !important;
|
||||
}
|
||||
|
||||
/* Alert styles */
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin: 1rem 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: var(--pico-primary-background);
|
||||
color: var(--pico-primary-inverse);
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background-color: var(--pico-color-red-400);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: var(--pico-color-amber-100);
|
||||
color: var(--pico-color-amber-800);
|
||||
border: 1px solid var(--pico-color-amber-300);
|
||||
}
|
||||
|
||||
.alert-info {
|
||||
background-color: var(--pico-color-blue-100);
|
||||
color: var(--pico-color-blue-800);
|
||||
border: 1px solid var(--pico-color-blue-300);
|
||||
}
|
||||
|
||||
/* Dataset controls */
|
||||
.dataset-controls {
|
||||
background: var(--pico-card-background-color);
|
||||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||||
padding: 1.5rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* Info section */
|
||||
.info-section {
|
||||
background: var(--pico-muted-background-color);
|
||||
border: var(--pico-border-width) solid var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-section p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* Log container */
|
||||
.log-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: var(--pico-background-color);
|
||||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 1rem;
|
||||
font-family: var(--pico-font-family-monospace);
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
font-size: 0.875rem;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.log-entry.log-info {
|
||||
background: var(--pico-card-background-color);
|
||||
border-left-color: var(--pico-primary);
|
||||
}
|
||||
|
||||
.log-entry.log-warning {
|
||||
background: var(--pico-color-amber-50);
|
||||
border-left-color: var(--pico-color-amber-500);
|
||||
}
|
||||
|
||||
.log-entry.log-error {
|
||||
background: var(--pico-color-red-50);
|
||||
border-left-color: var(--pico-color-red-500);
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
margin-bottom: 0.25rem;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.log-details {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin-top: 0.25rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.log-controls {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.log-stats {
|
||||
background: var(--pico-muted-background-color);
|
||||
border: var(--pico-border-width) solid var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Utility classes */
|
||||
.status-active {
|
||||
color: var(--pico-primary);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-inactive {
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: var(--pico-color-red-500);
|
||||
}
|
||||
|
||||
/* Modal improvements */
|
||||
.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 article {
|
||||
margin: 5% auto;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.modal header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.close:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
.header h1 {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Theme selector */
|
||||
.theme-selector {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1000;
|
||||
background: var(--pico-card-background-color);
|
||||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 0.5rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
box-shadow: var(--pico-box-shadow);
|
||||
}
|
||||
|
||||
.theme-selector button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.theme-selector button.active {
|
||||
background: var(--pico-primary-background);
|
||||
color: var(--pico-primary-inverse);
|
||||
}
|
||||
|
||||
.theme-selector button:not(.active) {
|
||||
background: var(--pico-muted-background-color);
|
||||
color: var(--pico-muted-color);
|
||||
}
|
||||
|
||||
.theme-selector button:hover:not(.active) {
|
||||
background: var(--pico-primary-hover);
|
||||
color: var(--pico-primary-inverse);
|
||||
}
|
||||
|
||||
/* Font size reduction - 15% smaller (more balanced) */
|
||||
html {
|
||||
font-size: 85%;
|
||||
/* 15% reduction from 100% - more balanced */
|
||||
}
|
||||
|
||||
/* Adjust specific elements that might need fine-tuning */
|
||||
.header h1 {
|
||||
font-size: 2.2rem;
|
||||
/* Adjusted for smaller base font */
|
||||
}
|
||||
|
||||
.header p {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
font-size: 0.9rem;
|
||||
/* Adjusted for smaller base font */
|
||||
}
|
||||
|
||||
.log-timestamp {
|
||||
font-size: 0.8rem;
|
||||
/* Adjusted for smaller base font */
|
||||
}
|
||||
|
||||
.log-details {
|
||||
font-size: 0.8rem;
|
||||
/* Adjusted for smaller base font */
|
||||
}
|
||||
|
||||
.log-stats {
|
||||
font-size: 0.9rem;
|
||||
/* Adjusted for smaller base font */
|
||||
}
|
||||
|
||||
/* Ensure buttons and inputs remain readable */
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Table adjustments */
|
||||
.variables-table th,
|
||||
.variables-table td {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Modal adjustments */
|
||||
.modal article {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.modal h3 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
/* CSV Configuration styles */
|
||||
.csv-config-display {
|
||||
background: var(--pico-card-background-color);
|
||||
border: var(--pico-border-width) solid var(--pico-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.config-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.config-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: 1px solid var(--pico-muted-border-color);
|
||||
}
|
||||
|
||||
.config-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.config-item span {
|
||||
font-weight: normal;
|
||||
color: var(--pico-primary);
|
||||
}
|
||||
|
||||
.directory-stats {
|
||||
padding: 1rem;
|
||||
background: var(--pico-muted-background-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.directory-stats .stat-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.day-folder-item {
|
||||
background: var(--pico-card-background-color);
|
||||
border: 1px solid var(--pico-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 0.75rem;
|
||||
margin: 0.5rem 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cleanup-status {
|
||||
padding: 0.5rem;
|
||||
border-radius: var(--pico-border-radius);
|
||||
margin: 0.5rem 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cleanup-success {
|
||||
background-color: var(--pico-color-green-100);
|
||||
color: var(--pico-color-green-800);
|
||||
border: 1px solid var(--pico-color-green-300);
|
||||
}
|
||||
|
||||
.cleanup-error {
|
||||
background-color: var(--pico-color-red-100);
|
||||
color: var(--pico-color-red-800);
|
||||
border: 1px solid var(--pico-color-red-300);
|
||||
}
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* Gestión de la configuración CSV y operaciones relacionadas
|
||||
*/
|
||||
|
||||
// Cargar configuración CSV
|
||||
function loadCsvConfig() {
|
||||
fetch('/api/csv/config')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const config = data.config;
|
||||
|
||||
// Actualizar elementos de visualización
|
||||
document.getElementById('csv-directory-path').textContent = config.current_directory || 'N/A';
|
||||
document.getElementById('csv-rotation-enabled').textContent = config.rotation_enabled ? '✅ Yes' : '❌ No';
|
||||
document.getElementById('csv-max-size').textContent = config.max_size_mb ? `${config.max_size_mb} MB` : 'No limit';
|
||||
document.getElementById('csv-max-days').textContent = config.max_days ? `${config.max_days} days` : 'No limit';
|
||||
document.getElementById('csv-max-hours').textContent = config.max_hours ? `${config.max_hours} hours` : 'No limit';
|
||||
document.getElementById('csv-cleanup-interval').textContent = `${config.cleanup_interval_hours} hours`;
|
||||
|
||||
// Actualizar campos del formulario
|
||||
document.getElementById('records-directory').value = config.records_directory || '';
|
||||
document.getElementById('rotation-enabled').checked = config.rotation_enabled || false;
|
||||
document.getElementById('max-size-mb').value = config.max_size_mb || '';
|
||||
document.getElementById('max-days').value = config.max_days || '';
|
||||
document.getElementById('max-hours').value = config.max_hours || '';
|
||||
document.getElementById('cleanup-interval').value = config.cleanup_interval_hours || 24;
|
||||
|
||||
// Cargar información del directorio
|
||||
loadCsvDirectoryInfo();
|
||||
} else {
|
||||
showMessage('Error loading CSV configuration: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error loading CSV configuration', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Cargar información del directorio CSV
|
||||
function loadCsvDirectoryInfo() {
|
||||
fetch('/api/csv/directory/info')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
const info = data.info;
|
||||
const statsDiv = document.getElementById('directory-stats');
|
||||
|
||||
let html = `
|
||||
<div class="stat-item">
|
||||
<strong>📁 Directory:</strong>
|
||||
<span>${info.base_directory}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>📊 Total Files:</strong>
|
||||
<span>${info.total_files}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<strong>💾 Total Size:</strong>
|
||||
<span>${info.total_size_mb} MB</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (info.oldest_file) {
|
||||
html += `
|
||||
<div class="stat-item">
|
||||
<strong>📅 Oldest File:</strong>
|
||||
<span>${new Date(info.oldest_file).toLocaleString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (info.newest_file) {
|
||||
html += `
|
||||
<div class="stat-item">
|
||||
<strong>🆕 Newest File:</strong>
|
||||
<span>${new Date(info.newest_file).toLocaleString()}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
if (info.day_folders && info.day_folders.length > 0) {
|
||||
html += '<h4>📂 Day Folders:</h4>';
|
||||
info.day_folders.forEach(folder => {
|
||||
html += `
|
||||
<div class="day-folder-item">
|
||||
<span><strong>${folder.name}</strong></span>
|
||||
<span>${folder.files} files, ${folder.size_mb} MB</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
statsDiv.innerHTML = html;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
document.getElementById('directory-stats').innerHTML = '<p>Error loading directory information</p>';
|
||||
});
|
||||
}
|
||||
|
||||
// Ejecutar limpieza manual
|
||||
function triggerManualCleanup() {
|
||||
if (!confirm('¿Estás seguro de que quieres ejecutar la limpieza manual? Esto eliminará archivos antiguos según la configuración actual.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch('/api/csv/cleanup', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Limpieza ejecutada correctamente', 'success');
|
||||
loadCsvDirectoryInfo(); // Recargar información del directorio
|
||||
} else {
|
||||
showMessage('Error en la limpieza: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error ejecutando la limpieza', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar listeners para la configuración CSV
|
||||
function initCsvListeners() {
|
||||
// Manejar envío del formulario de configuración CSV
|
||||
document.getElementById('csv-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const configData = {};
|
||||
|
||||
// Convertir datos del formulario a objeto, manejando valores vacíos
|
||||
for (let [key, value] of formData.entries()) {
|
||||
if (key === 'rotation_enabled') {
|
||||
configData[key] = document.getElementById('rotation-enabled').checked;
|
||||
} else if (value.trim() === '') {
|
||||
configData[key] = null;
|
||||
} else if (key.includes('max_') || key.includes('cleanup_interval')) {
|
||||
configData[key] = parseFloat(value) || null;
|
||||
} else {
|
||||
configData[key] = value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
fetch('/api/csv/config', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(configData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showMessage('Configuración CSV actualizada correctamente', 'success');
|
||||
loadCsvConfig(); // Recargar para mostrar valores actualizados
|
||||
} else {
|
||||
showMessage('Error actualizando configuración CSV: ' + data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error actualizando configuración CSV', 'error');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,519 @@
|
|||
/**
|
||||
* Gestión de datasets y variables asociadas
|
||||
*/
|
||||
|
||||
// Variables de gestión de datasets
|
||||
let currentDatasets = {};
|
||||
let currentDatasetId = null;
|
||||
|
||||
// Cargar todos los datasets desde 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');
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar el selector de datasets
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar información del dataset
|
||||
function updateDatasetInfo() {
|
||||
const statusBar = document.getElementById('dataset-status-bar');
|
||||
const variablesManagement = document.getElementById('variables-management');
|
||||
const noDatasetMessage = document.getElementById('no-dataset-message');
|
||||
|
||||
if (currentDatasetId && currentDatasets[currentDatasetId]) {
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
|
||||
// Mostrar info del dataset en la barra de estado
|
||||
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;
|
||||
|
||||
// Actualizar estado del dataset
|
||||
const statusSpan = document.getElementById('dataset-status');
|
||||
const isActive = dataset.enabled;
|
||||
statusSpan.textContent = isActive ? '🟢 Active' : '⭕ Inactive';
|
||||
statusSpan.className = `status-item ${isActive ? 'status-active' : 'status-inactive'}`;
|
||||
|
||||
// Actualizar botones de acción
|
||||
document.getElementById('activate-dataset-btn').style.display = isActive ? 'none' : 'inline-block';
|
||||
document.getElementById('deactivate-dataset-btn').style.display = isActive ? 'inline-block' : 'none';
|
||||
|
||||
// Mostrar secciones
|
||||
statusBar.style.display = 'block';
|
||||
variablesManagement.style.display = 'block';
|
||||
noDatasetMessage.style.display = 'none';
|
||||
|
||||
// Cargar variables para este dataset
|
||||
loadDatasetVariables(currentDatasetId);
|
||||
} else {
|
||||
statusBar.style.display = 'none';
|
||||
variablesManagement.style.display = 'none';
|
||||
noDatasetMessage.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Cargar variables para un dataset específico
|
||||
function loadDatasetVariables(datasetId) {
|
||||
if (!datasetId || !currentDatasets[datasetId]) {
|
||||
// Limpiar la tabla si no hay dataset válido
|
||||
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');
|
||||
|
||||
// Limpiar filas existentes
|
||||
tbody.innerHTML = '';
|
||||
|
||||
// Añadir una fila para cada variable
|
||||
Object.keys(variables).forEach(varName => {
|
||||
const variable = variables[varName];
|
||||
const row = document.createElement('tr');
|
||||
|
||||
// Formatear visualización del área de memoria
|
||||
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}`;
|
||||
}
|
||||
|
||||
// Comprobar si la variable está en la lista de streaming
|
||||
const isStreaming = streamingVars.includes(varName);
|
||||
|
||||
row.innerHTML = `
|
||||
<td>${varName}</td>
|
||||
<td>${memoryAreaDisplay}</td>
|
||||
<td>${variable.offset}</td>
|
||||
<td>${variable.type.toUpperCase()}</td>
|
||||
<td id="value-${varName}" style="font-family: monospace; font-weight: bold; color: var(--pico-muted-color);">
|
||||
--
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" id="stream-${varName}" role="switch" ${isStreaming ? 'checked' : ''}
|
||||
onchange="toggleStreaming('${varName}', this.checked)">
|
||||
Enable
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<button class="outline" onclick="editVariable('${varName}')">✏️ Edit</button>
|
||||
<button class="secondary" onclick="removeVariable('${varName}')">🗑️ Remove</button>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// Inicializar listeners de eventos para datasets
|
||||
function initDatasetListeners() {
|
||||
// Cambio de selector de dataset
|
||||
document.getElementById('dataset-selector').addEventListener('change', function () {
|
||||
const selectedDatasetId = this.value;
|
||||
if (selectedDatasetId) {
|
||||
// Detener streaming de variables actual si está activo
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
}
|
||||
|
||||
// Establecer como dataset actual
|
||||
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;
|
||||
// Recargar datasets para obtener datos frescos, luego actualizar info
|
||||
loadDatasets();
|
||||
|
||||
// Actualizar texto del botón de streaming
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Streaming';
|
||||
}
|
||||
|
||||
// Auto-refrescar valores para el nuevo dataset
|
||||
autoRefreshOnDatasetChange();
|
||||
} else {
|
||||
showMessage(data.message, 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showMessage('Error setting current dataset', 'error');
|
||||
});
|
||||
} else {
|
||||
// Detener streaming de variables si está activo
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
}
|
||||
|
||||
currentDatasetId = null;
|
||||
updateDatasetInfo();
|
||||
// Limpiar valores cuando no hay dataset seleccionado
|
||||
clearVariableValues();
|
||||
}
|
||||
});
|
||||
|
||||
// Botón de nuevo dataset
|
||||
document.getElementById('new-dataset-btn').addEventListener('click', function () {
|
||||
document.getElementById('dataset-modal').style.display = 'block';
|
||||
});
|
||||
|
||||
// Cerrar modal de dataset
|
||||
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';
|
||||
});
|
||||
|
||||
// Crear nuevo 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');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de eliminar dataset
|
||||
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');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Botón de activar dataset
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de desactivar dataset
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
// Formulario de variables
|
||||
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
|
||||
};
|
||||
|
||||
// Añadir parámetro bit para áreas de bit
|
||||
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(); // Recargar para actualizar conteos
|
||||
updateStatus();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Eliminar variable del dataset actual
|
||||
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(); // Recargar para actualizar conteos
|
||||
updateStatus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Variables para edición de variables
|
||||
let currentEditingVariable = null;
|
||||
|
||||
// Editar variable
|
||||
function editVariable(name) {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
currentEditingVariable = name;
|
||||
|
||||
// Obtener datos de la variable del dataset actual
|
||||
const dataset = currentDatasets[currentDatasetId];
|
||||
if (dataset && dataset.variables && dataset.variables[name]) {
|
||||
const variable = dataset.variables[name];
|
||||
const streamingVars = dataset.streaming_variables || [];
|
||||
|
||||
// Crear objeto de variable con la misma estructura que la 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');
|
||||
}
|
||||
}
|
||||
|
||||
// Rellenar formulario de edición
|
||||
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;
|
||||
}
|
||||
|
||||
// Actualizar visibilidad de campos según el área
|
||||
toggleEditFields();
|
||||
}
|
||||
|
||||
// Cerrar modal de edición
|
||||
function closeEditModal() {
|
||||
document.getElementById('edit-modal').style.display = 'none';
|
||||
currentEditingVariable = null;
|
||||
}
|
||||
|
||||
// Inicializar listeners para edición de variables
|
||||
function initVariableEditListeners() {
|
||||
// Manejar envío del formulario de edición
|
||||
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;
|
||||
|
||||
// Primero eliminar la variable antigua
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/${currentEditingVariable}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(deleteResult => {
|
||||
if (deleteResult.success) {
|
||||
// Luego añadir la variable actualizada
|
||||
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 // Se restaurará abajo si estaba habilitado
|
||||
};
|
||||
|
||||
// Añadir parámetro bit para áreas de bit
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
// Cerrar modal al hacer clic fuera de él
|
||||
window.onclick = function (event) {
|
||||
const editModal = document.getElementById('edit-modal');
|
||||
const datasetModal = document.getElementById('dataset-modal');
|
||||
if (event.target === editModal) {
|
||||
closeEditModal();
|
||||
}
|
||||
if (event.target === datasetModal) {
|
||||
datasetModal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,98 @@
|
|||
/**
|
||||
* Gestión de eventos de la aplicación y log de eventos
|
||||
*/
|
||||
|
||||
// Refrescar log de eventos
|
||||
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');
|
||||
|
||||
// Limpiar entradas existentes
|
||||
logContainer.innerHTML = '';
|
||||
|
||||
// Actualizar estadísticas
|
||||
logStats.textContent = `Showing ${data.showing} of ${data.total_events} events`;
|
||||
|
||||
// Añadir eventos (orden inverso para mostrar primero los más nuevos)
|
||||
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 al inicio para mostrar eventos más nuevos
|
||||
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');
|
||||
});
|
||||
}
|
||||
|
||||
// Crear entrada de log
|
||||
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;
|
||||
}
|
||||
|
||||
// Limpiar vista de log
|
||||
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';
|
||||
}
|
||||
|
||||
// Inicializar listeners para eventos
|
||||
function initEventListeners() {
|
||||
// Botones de control de log
|
||||
document.querySelector('button[onclick="refreshEventLog()"]').addEventListener('click', refreshEventLog);
|
||||
document.querySelector('button[onclick="clearLogView()"]').addEventListener('click', clearLogView);
|
||||
|
||||
// Selector de límite de log
|
||||
document.getElementById('log-limit').addEventListener('change', refreshEventLog);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* Archivo principal que inicializa todos los componentes
|
||||
*/
|
||||
|
||||
// Inicializar la aplicación al cargar el documento
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Inicializar tema
|
||||
loadTheme();
|
||||
|
||||
// Iniciar streaming de estado automáticamente
|
||||
startStatusStreaming();
|
||||
|
||||
// Cargar datos iniciales
|
||||
loadDatasets();
|
||||
updateStatus();
|
||||
loadCsvConfig();
|
||||
refreshEventLog();
|
||||
|
||||
// Inicializar listeners de eventos
|
||||
initPlcListeners();
|
||||
initDatasetListeners();
|
||||
initVariableEditListeners();
|
||||
initStreamingListeners();
|
||||
initCsvListeners();
|
||||
initEventListeners();
|
||||
|
||||
// Configurar actualizaciones periódicas como respaldo
|
||||
setInterval(updateStatus, 30000); // Cada 30 segundos como respaldo
|
||||
setInterval(refreshEventLog, 10000); // Cada 10 segundos
|
||||
|
||||
// Inicializar visibilidad de campos en formularios
|
||||
toggleFields();
|
||||
});
|
||||
|
||||
// Limpiar conexiones SSE cuando se descarga la página
|
||||
window.addEventListener('beforeunload', function () {
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
}
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
}
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Gestión de la conexión con el PLC y configuración relacionada
|
||||
*/
|
||||
|
||||
// Inicializar listeners de eventos para PLC
|
||||
function initPlcListeners() {
|
||||
// Configuración del PLC
|
||||
document.getElementById('plc-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
ip: document.getElementById('plc-ip').value,
|
||||
rack: parseInt(document.getElementById('plc-rack').value),
|
||||
slot: parseInt(document.getElementById('plc-slot').value)
|
||||
};
|
||||
|
||||
fetch('/api/plc/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Configuración UDP
|
||||
document.getElementById('udp-config-form').addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
host: document.getElementById('udp-host').value,
|
||||
port: parseInt(document.getElementById('udp-port').value)
|
||||
};
|
||||
|
||||
fetch('/api/udp/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de conexión 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();
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de desconexión 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();
|
||||
});
|
||||
});
|
||||
|
||||
// Botón de actualización de intervalo
|
||||
document.getElementById('update-sampling-btn').addEventListener('click', function () {
|
||||
const interval = parseFloat(document.getElementById('sampling-interval').value);
|
||||
fetch('/api/sampling', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ interval: interval })
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,204 @@
|
|||
/**
|
||||
* Gestión del estado del sistema y actualizaciones en tiempo real
|
||||
*/
|
||||
|
||||
// Variables para el streaming de estado
|
||||
let statusEventSource = null;
|
||||
let isStreamingStatus = false;
|
||||
|
||||
// Actualizar el estado del sistema
|
||||
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');
|
||||
const diskSpaceStatus = document.getElementById('disk-space');
|
||||
|
||||
// Actualizar estado de conexión PLC
|
||||
if (data.plc_connected) {
|
||||
plcStatus.innerHTML = '🔌 PLC: Connected <div style="margin-top: 8px;"><button type="button" id="status-disconnect-btn">❌ Disconnect</button></div>';
|
||||
plcStatus.className = 'status-item status-connected';
|
||||
|
||||
// Añadir event listener al nuevo botón de desconexión
|
||||
document.getElementById('status-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();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
plcStatus.innerHTML = '🔌 PLC: Disconnected <div style="margin-top: 8px;"><button type="button" id="status-connect-btn">🔗 Connect</button></div>';
|
||||
plcStatus.className = 'status-item status-disconnected';
|
||||
|
||||
// Añadir event listener al botón de conexión
|
||||
document.getElementById('status-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();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar estado de streaming
|
||||
if (data.streaming) {
|
||||
streamStatus.innerHTML = '📡 Streaming: Active <div style="margin-top: 8px;"><button type="button" id="status-streaming-btn">⏹️ Stop</button></div>';
|
||||
streamStatus.className = 'status-item status-streaming';
|
||||
|
||||
// Añadir event listener al botón de parar streaming
|
||||
document.getElementById('status-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();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
streamStatus.innerHTML = '📡 Streaming: Inactive <div style="margin-top: 8px;"><button type="button" id="status-start-btn">▶️ Start</button></div>';
|
||||
streamStatus.className = 'status-item status-idle';
|
||||
|
||||
// Añadir event listener al botón de iniciar streaming
|
||||
document.getElementById('status-start-btn').addEventListener('click', function () {
|
||||
fetch('/api/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Actualizar estado de grabación CSV
|
||||
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';
|
||||
}
|
||||
|
||||
// Actualizar estado de espacio en disco
|
||||
if (data.disk_space_info) {
|
||||
diskSpaceStatus.innerHTML = `💽 Disk: ${data.disk_space_info.free_space} free<br>
|
||||
⏱️ ~${data.disk_space_info.recording_time_left}`;
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
} else {
|
||||
diskSpaceStatus.textContent = '💽 Disk Space: Calculating...';
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
}
|
||||
})
|
||||
.catch(error => console.error('Error updating status:', error));
|
||||
}
|
||||
|
||||
// Iniciar streaming de estado en tiempo real
|
||||
function startStatusStreaming() {
|
||||
if (isStreamingStatus) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cerrar conexión existente si hay alguna
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
}
|
||||
|
||||
// Crear nueva conexión EventSource
|
||||
statusEventSource = new EventSource('/api/stream/status?interval=2.0');
|
||||
|
||||
statusEventSource.onopen = function (event) {
|
||||
console.log('Status streaming connected');
|
||||
isStreamingStatus = true;
|
||||
};
|
||||
|
||||
statusEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
console.log('Status stream connected:', data.message);
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
// Actualizar estado en tiempo real
|
||||
updateStatusFromStream(data.status);
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
console.error('Status stream error:', data.message);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing status SSE data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
statusEventSource.onerror = function (event) {
|
||||
console.error('Status stream error:', event);
|
||||
isStreamingStatus = false;
|
||||
|
||||
// Intentar reconectar después de un retraso
|
||||
setTimeout(() => {
|
||||
startStatusStreaming();
|
||||
}, 10000);
|
||||
};
|
||||
}
|
||||
|
||||
// Detener streaming de estado en tiempo real
|
||||
function stopStatusStreaming() {
|
||||
if (statusEventSource) {
|
||||
statusEventSource.close();
|
||||
statusEventSource = null;
|
||||
}
|
||||
isStreamingStatus = false;
|
||||
}
|
||||
|
||||
// Actualizar estado desde datos de streaming
|
||||
function updateStatusFromStream(status) {
|
||||
const plcStatus = document.getElementById('plc-status');
|
||||
const streamStatus = document.getElementById('stream-status');
|
||||
const csvStatus = document.getElementById('csv-status');
|
||||
const diskSpaceStatus = document.getElementById('disk-space');
|
||||
|
||||
// Actualizar estado de conexión PLC
|
||||
if (status.plc_connected) {
|
||||
plcStatus.innerHTML = '🔌 PLC: Connected <div style="margin-top: 8px;"><button type="button" id="status-disconnect-btn">❌ Disconnect</button></div>';
|
||||
plcStatus.className = 'status-item status-connected';
|
||||
} else {
|
||||
plcStatus.innerHTML = '🔌 PLC: Disconnected <div style="margin-top: 8px;"><button type="button" id="status-connect-btn">🔗 Connect</button></div>';
|
||||
plcStatus.className = 'status-item status-disconnected';
|
||||
}
|
||||
|
||||
// Actualizar estado de streaming
|
||||
if (status.streaming) {
|
||||
streamStatus.innerHTML = '📡 Streaming: Active <div style="margin-top: 8px;"><button type="button" id="status-streaming-btn">⏹️ Stop</button></div>';
|
||||
streamStatus.className = 'status-item status-streaming';
|
||||
} else {
|
||||
streamStatus.innerHTML = '📡 Streaming: Inactive <div style="margin-top: 8px;"><button type="button" id="status-start-btn">▶️ Start</button></div>';
|
||||
streamStatus.className = 'status-item status-idle';
|
||||
}
|
||||
|
||||
// Actualizar estado de grabación CSV
|
||||
if (status.csv_recording) {
|
||||
csvStatus.textContent = `💾 CSV: Recording`;
|
||||
csvStatus.className = 'status-item status-streaming';
|
||||
} else {
|
||||
csvStatus.textContent = `💾 CSV: Inactive`;
|
||||
csvStatus.className = 'status-item status-idle';
|
||||
}
|
||||
|
||||
// Actualizar estado de espacio en disco
|
||||
if (status.disk_space_info) {
|
||||
diskSpaceStatus.innerHTML = `💽 Disk: ${status.disk_space_info.free_space} free<br>
|
||||
⏱️ ~${status.disk_space_info.recording_time_left}`;
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
} else {
|
||||
diskSpaceStatus.textContent = '💽 Disk Space: Calculating...';
|
||||
diskSpaceStatus.className = 'status-item status-idle';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
/**
|
||||
* Gestión del streaming de datos a PlotJuggler
|
||||
*/
|
||||
|
||||
// Inicializar listeners para el control de streaming
|
||||
function initStreamingListeners() {
|
||||
// Iniciar streaming
|
||||
document.getElementById('start-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/streaming/start', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Detener streaming
|
||||
document.getElementById('stop-streaming-btn').addEventListener('click', function () {
|
||||
fetch('/api/streaming/stop', { method: 'POST' })
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showMessage(data.message, data.success ? 'success' : 'error');
|
||||
updateStatus();
|
||||
});
|
||||
});
|
||||
|
||||
// Cargar estado de streaming de variables
|
||||
loadStreamingStatus();
|
||||
}
|
||||
|
||||
// Cargar estado de variables en streaming
|
||||
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));
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Gestión del tema de la aplicación (claro/oscuro/auto)
|
||||
*/
|
||||
|
||||
// Establecer el tema
|
||||
function setTheme(theme) {
|
||||
const html = document.documentElement;
|
||||
const buttons = document.querySelectorAll('.theme-selector button');
|
||||
|
||||
// Eliminar clase active de todos los botones
|
||||
buttons.forEach(btn => btn.classList.remove('active'));
|
||||
|
||||
// Establecer tema
|
||||
html.setAttribute('data-theme', theme);
|
||||
|
||||
// Añadir clase active al botón seleccionado
|
||||
document.getElementById(`theme-${theme}`).classList.add('active');
|
||||
|
||||
// Guardar preferencia en localStorage
|
||||
localStorage.setItem('theme', theme);
|
||||
}
|
||||
|
||||
// Cargar tema guardado al cargar la página
|
||||
function loadTheme() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'light';
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
|
||||
// Inicializar tema al cargar la página
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
loadTheme();
|
||||
});
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Funciones de utilidad general para la aplicación
|
||||
*/
|
||||
|
||||
// Función para mostrar mensajes en la interfaz
|
||||
function showMessage(message, type = 'success') {
|
||||
const messagesDiv = document.getElementById('messages');
|
||||
let alertClass;
|
||||
|
||||
switch (type) {
|
||||
case 'success':
|
||||
alertClass = 'alert-success';
|
||||
break;
|
||||
case 'warning':
|
||||
alertClass = 'alert-warning';
|
||||
break;
|
||||
case 'info':
|
||||
alertClass = 'alert-info';
|
||||
break;
|
||||
case 'error':
|
||||
default:
|
||||
alertClass = 'alert-error';
|
||||
break;
|
||||
}
|
||||
|
||||
messagesDiv.innerHTML = `<div class="alert ${alertClass}">${message}</div>`;
|
||||
setTimeout(() => {
|
||||
messagesDiv.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Formatear timestamp para los logs
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
// Obtener icono para tipo de evento
|
||||
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] || '📋';
|
||||
}
|
|
@ -0,0 +1,637 @@
|
|||
/**
|
||||
* Gestión de variables y streaming de valores en tiempo real
|
||||
*/
|
||||
|
||||
// Variables para el streaming de variables
|
||||
let variableEventSource = null;
|
||||
let isStreamingVariables = false;
|
||||
|
||||
// Toggle de campos de variables según el área de memoria
|
||||
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');
|
||||
|
||||
// Manejar campo DB
|
||||
if (area === 'db') {
|
||||
dbField.style.display = 'block';
|
||||
dbInput.required = true;
|
||||
} else {
|
||||
dbField.style.display = 'none';
|
||||
dbInput.required = false;
|
||||
dbInput.value = 1; // Valor por defecto para áreas no DB
|
||||
}
|
||||
|
||||
// Manejar campo Bit y restricciones de tipo de datos
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
bitField.style.display = 'block';
|
||||
// Para áreas de bit, forzar tipo de dato a bool
|
||||
typeSelect.value = 'bool';
|
||||
// Deshabilitar otros tipos de datos para áreas de bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = (option.value !== 'bool');
|
||||
});
|
||||
} else {
|
||||
bitField.style.display = 'none';
|
||||
// Re-habilitar todos los tipos de datos para áreas no-bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle de campos de edición de variables
|
||||
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');
|
||||
|
||||
// Manejar campo DB
|
||||
if (area === 'db') {
|
||||
dbField.style.display = 'block';
|
||||
dbInput.required = true;
|
||||
} else {
|
||||
dbField.style.display = 'none';
|
||||
dbInput.required = false;
|
||||
dbInput.value = 1; // Valor por defecto para áreas no DB
|
||||
}
|
||||
|
||||
// Manejar campo Bit y restricciones de tipo de datos
|
||||
if (area === 'e' || area === 'a' || area === 'mb') {
|
||||
bitField.style.display = 'block';
|
||||
// Para áreas de bit, forzar tipo de dato a bool
|
||||
typeSelect.value = 'bool';
|
||||
// Deshabilitar otros tipos de datos para áreas de bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = (option.value !== 'bool');
|
||||
});
|
||||
} else {
|
||||
bitField.style.display = 'none';
|
||||
// Re-habilitar todos los tipos de datos para áreas no-bit
|
||||
Array.from(typeSelect.options).forEach(option => {
|
||||
option.disabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar streaming para una variable
|
||||
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(); // Actualizar contador de variables de streaming
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error toggling streaming:', error);
|
||||
showMessage('Error updating streaming setting', 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// Refrescar valores de variables desde el PLC
|
||||
function refreshVariableValues() {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('Please select a dataset first', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const refreshBtn = document.getElementById('refresh-values-btn');
|
||||
const lastRefreshTime = document.getElementById('last-refresh-time');
|
||||
|
||||
// Deshabilitar botón y mostrar estado de carga
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.innerHTML = '⏳ Reading...';
|
||||
|
||||
fetch(`/api/datasets/${currentDatasetId}/variables/values`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Actualizar valores de variables en la tabla
|
||||
Object.keys(data.values).forEach(varName => {
|
||||
const valueCell = document.getElementById(`value-${varName}`);
|
||||
if (valueCell) {
|
||||
const value = data.values[varName];
|
||||
valueCell.textContent = value;
|
||||
|
||||
// Código de color y tooltip basado en el estado del valor
|
||||
if (value === 'ERROR' || value === 'FORMAT_ERROR') {
|
||||
valueCell.style.color = 'var(--pico-color-red-500)';
|
||||
|
||||
// Añadir tooltip con error detallado si está disponible
|
||||
const errorDetail = data.detailed_errors && data.detailed_errors[varName];
|
||||
if (errorDetail) {
|
||||
valueCell.title = `Error: ${errorDetail}`;
|
||||
valueCell.style.cursor = 'help';
|
||||
}
|
||||
} else {
|
||||
valueCell.style.color = 'var(--pico-color-green-600)';
|
||||
valueCell.title = `Value: ${value}`;
|
||||
valueCell.style.cursor = 'default';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Actualizar timestamp, estadísticas e información de origen
|
||||
if (data.timestamp) {
|
||||
const stats = data.stats;
|
||||
const source = data.source || 'cache';
|
||||
const isCache = data.is_cached;
|
||||
|
||||
// Crear indicador de origen (siempre caché ahora)
|
||||
const sourceIcon = '📊';
|
||||
const sourceText = 'from streaming cache';
|
||||
|
||||
let statsText = '';
|
||||
if (stats && stats.total > 0) {
|
||||
statsText = `<br/><small style="color: var(--pico-muted-color);">📈 ${stats.success}/${stats.total} variables</small>`;
|
||||
}
|
||||
|
||||
lastRefreshTime.innerHTML = `
|
||||
Last refresh: ${data.timestamp}<br/>
|
||||
<small style="color: var(--pico-color-green-600);">
|
||||
✅ ${data.message}
|
||||
</small>${statsText}<br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText}
|
||||
</small>
|
||||
${data.cache_info ? `<br/><small style="color: var(--pico-muted-color);">${data.cache_info}</small>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
// Mostrar mensaje apropiado
|
||||
if (data.warning) {
|
||||
showMessage(data.warning, 'warning');
|
||||
// Mostrar información detallada de error en consola para depuración
|
||||
if (data.detailed_errors && Object.keys(data.detailed_errors).length > 0) {
|
||||
console.warn('Variable read errors:', data.detailed_errors);
|
||||
}
|
||||
} else {
|
||||
showMessage(data.message, 'success');
|
||||
}
|
||||
|
||||
} else {
|
||||
// Manejar diferentes tipos de casos de fallo
|
||||
const errorType = data.error_type;
|
||||
|
||||
if (errorType === 'dataset_inactive') {
|
||||
// Dataset no está activo - guiar al usuario para activarlo
|
||||
showMessage(`⚠️ ${data.message}`, 'warning');
|
||||
clearVariableValues('DATASET INACTIVE');
|
||||
lastRefreshTime.innerHTML = `
|
||||
Last refresh attempt: ${data.timestamp}<br/>
|
||||
<small style="color: var(--pico-color-amber-500);">
|
||||
⚠️ Dataset not active - activate dataset to populate cache
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
💡 Use "Activate" button in dataset controls above
|
||||
</small>
|
||||
`;
|
||||
} else if (errorType === 'plc_disconnected') {
|
||||
// PLC no conectado - guiar al usuario para conectar
|
||||
showMessage(`🔌 ${data.message}`, 'warning');
|
||||
clearVariableValues('PLC OFFLINE');
|
||||
lastRefreshTime.innerHTML = `
|
||||
Last refresh attempt: ${data.timestamp}<br/>
|
||||
<small style="color: var(--pico-color-red-500);">
|
||||
🔌 PLC not connected - cache cannot be populated
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
💡 Connect to PLC first, then activate dataset
|
||||
</small>
|
||||
`;
|
||||
} else if (errorType === 'no_cache_available') {
|
||||
// No hay caché todavía - el streaming está iniciando
|
||||
showMessage(`⏳ ${data.message}`, 'info');
|
||||
clearVariableValues('READING...');
|
||||
lastRefreshTime.innerHTML = `
|
||||
Last refresh attempt: ${data.timestamp}<br/>
|
||||
<small style="color: var(--pico-color-blue-500);">
|
||||
⏳ Cache being populated by streaming process
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${data.note || 'Please wait for next reading cycle'}
|
||||
</small>
|
||||
`;
|
||||
} else {
|
||||
// Caso de fallo completo u otros errores
|
||||
showMessage(`❌ ${data.message}`, 'error');
|
||||
clearVariableValues('ERROR');
|
||||
|
||||
const source = data.source || 'cache';
|
||||
const sourceIcon = '📊';
|
||||
const sourceText = 'from streaming cache';
|
||||
|
||||
lastRefreshTime.innerHTML = `
|
||||
Last refresh attempt: ${data.timestamp}<br/>
|
||||
<small style="color: var(--pico-color-red-500);">
|
||||
❌ ${data.message}
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText}
|
||||
</small>
|
||||
`;
|
||||
|
||||
// Mostrar información detallada de error si está disponible
|
||||
if (data.detailed_errors && Object.keys(data.detailed_errors).length > 0) {
|
||||
console.error('Detailed variable errors:', data.detailed_errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error refreshing variable values:', error);
|
||||
showMessage('Network error retrieving cached variable values', 'error');
|
||||
clearVariableValues('COMM ERROR');
|
||||
|
||||
lastRefreshTime.innerHTML = `
|
||||
Last refresh attempt: ${new Date().toLocaleString()}<br/>
|
||||
<small style="color: var(--pico-color-red-500);">
|
||||
🌐 Network error communicating with server
|
||||
</small>
|
||||
`;
|
||||
})
|
||||
.finally(() => {
|
||||
// Re-habilitar botón
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.innerHTML = '🔄 Refresh Values';
|
||||
});
|
||||
}
|
||||
|
||||
// Limpiar todos los valores de variables y establecer mensaje de estado
|
||||
function clearVariableValues(statusMessage = '--') {
|
||||
// Encontrar todas las celdas de valor y limpiarlas
|
||||
const valueCells = document.querySelectorAll('[id^="value-"]');
|
||||
valueCells.forEach(cell => {
|
||||
cell.textContent = statusMessage;
|
||||
cell.style.color = 'var(--pico-muted-color)';
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-refrescar valores cuando cambia el dataset (opcional)
|
||||
function autoRefreshOnDatasetChange() {
|
||||
if (currentDatasetId) {
|
||||
// Pequeño retraso para asegurar que la tabla está cargada
|
||||
setTimeout(() => {
|
||||
refreshVariableValues();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
// Iniciar streaming de variables en tiempo real
|
||||
function startVariableStreaming() {
|
||||
if (!currentDatasetId || isStreamingVariables) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cerrar conexión existente si hay alguna
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
}
|
||||
|
||||
// Crear nueva conexión EventSource
|
||||
variableEventSource = new EventSource(`/api/stream/variables?dataset_id=${currentDatasetId}&interval=1.0`);
|
||||
|
||||
variableEventSource.onopen = function (event) {
|
||||
console.log('Variable streaming connected');
|
||||
isStreamingVariables = true;
|
||||
updateStreamingIndicator(true);
|
||||
};
|
||||
|
||||
variableEventSource.onmessage = function (event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
|
||||
switch (data.type) {
|
||||
case 'connected':
|
||||
console.log('Variable stream connected:', data.message);
|
||||
break;
|
||||
|
||||
case 'values':
|
||||
// Actualizar valores de variables en tiempo real desde caché
|
||||
updateVariableValuesFromStream(data);
|
||||
break;
|
||||
|
||||
case 'cache_error':
|
||||
console.error('Cache error in variable stream:', data.message);
|
||||
showMessage(`Cache error: ${data.message}`, 'error');
|
||||
clearVariableValues('CACHE ERROR');
|
||||
break;
|
||||
|
||||
case 'plc_disconnected':
|
||||
clearVariableValues('PLC OFFLINE');
|
||||
showMessage('PLC disconnected - cache not being populated', 'warning');
|
||||
break;
|
||||
|
||||
case 'dataset_inactive':
|
||||
clearVariableValues('DATASET INACTIVE');
|
||||
showMessage('Dataset is not active - activate to populate cache', 'warning');
|
||||
break;
|
||||
|
||||
case 'no_variables':
|
||||
clearVariableValues('NO VARIABLES');
|
||||
showMessage('No variables defined in this dataset', 'info');
|
||||
break;
|
||||
|
||||
case 'no_cache':
|
||||
clearVariableValues('READING...');
|
||||
const samplingInfo = data.sampling_interval ? ` (every ${data.sampling_interval}s)` : '';
|
||||
showMessage(`Waiting for cache to be populated${samplingInfo}`, 'info');
|
||||
break;
|
||||
|
||||
case 'stream_error':
|
||||
console.error('SSE stream error:', data.message);
|
||||
showMessage(`Streaming error: ${data.message}`, 'error');
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn('Unknown SSE message type:', data.type);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing SSE data:', error);
|
||||
}
|
||||
};
|
||||
|
||||
variableEventSource.onerror = function (event) {
|
||||
console.error('Variable stream error:', event);
|
||||
isStreamingVariables = false;
|
||||
updateStreamingIndicator(false);
|
||||
|
||||
// Intentar reconectar después de un retraso
|
||||
setTimeout(() => {
|
||||
if (currentDatasetId) {
|
||||
startVariableStreaming();
|
||||
}
|
||||
}, 5000);
|
||||
};
|
||||
}
|
||||
|
||||
// Detener streaming de variables en tiempo real
|
||||
function stopVariableStreaming() {
|
||||
if (variableEventSource) {
|
||||
variableEventSource.close();
|
||||
variableEventSource = null;
|
||||
}
|
||||
isStreamingVariables = false;
|
||||
updateStreamingIndicator(false);
|
||||
}
|
||||
|
||||
// Actualizar valores de variables desde datos de streaming
|
||||
function updateVariableValuesFromStream(data) {
|
||||
const values = data.values;
|
||||
const timestamp = data.timestamp;
|
||||
const source = data.source;
|
||||
const stats = data.stats;
|
||||
|
||||
// Actualizar cada valor de variable
|
||||
Object.keys(values).forEach(varName => {
|
||||
const valueCell = document.getElementById(`value-${varName}`);
|
||||
if (valueCell) {
|
||||
const value = values[varName];
|
||||
valueCell.textContent = value;
|
||||
|
||||
// Código de color basado en el estado del valor
|
||||
if (value === 'ERROR' || value === 'FORMAT_ERROR') {
|
||||
valueCell.style.color = 'var(--pico-color-red-500)';
|
||||
valueCell.style.fontWeight = 'bold';
|
||||
} else {
|
||||
valueCell.style.color = 'var(--pico-color-green-600)';
|
||||
valueCell.style.fontWeight = 'bold';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Actualizar timestamp e información de origen
|
||||
const lastRefreshTime = document.getElementById('last-refresh-time');
|
||||
if (lastRefreshTime) {
|
||||
const sourceIcon = source === 'cache' ? '📊' : '🔗';
|
||||
const sourceText = source === 'cache' ? 'streaming cache' : 'direct PLC';
|
||||
|
||||
if (stats && stats.failed > 0) {
|
||||
lastRefreshTime.innerHTML = `
|
||||
<span style="color: var(--pico-color-green-600);">🔄 Live streaming</span><br/>
|
||||
<small style="color: var(--pico-color-amber-600);">
|
||||
⚠️ ${stats.success}/${stats.total} variables (${stats.failed} failed)
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
|
||||
</small>
|
||||
`;
|
||||
} else {
|
||||
lastRefreshTime.innerHTML = `
|
||||
<span style="color: var(--pico-color-green-600);">🔄 Live streaming</span><br/>
|
||||
<small style="color: var(--pico-color-green-600);">
|
||||
✅ All ${stats ? stats.success : 'N/A'} variables OK
|
||||
</small><br/>
|
||||
<small style="color: var(--pico-muted-color);">
|
||||
${sourceIcon} ${sourceText} • ${new Date(timestamp).toLocaleTimeString()}
|
||||
</small>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Actualizar indicador de streaming
|
||||
function updateStreamingIndicator(isStreaming) {
|
||||
const refreshBtn = document.getElementById('refresh-values-btn');
|
||||
if (refreshBtn) {
|
||||
if (isStreaming) {
|
||||
refreshBtn.innerHTML = '🔄 Live Streaming';
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.title = 'Real-time streaming is active - values update automatically';
|
||||
} else {
|
||||
refreshBtn.innerHTML = '🔄 Refresh Values';
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.title = 'Click to refresh variable values';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Alternar streaming en tiempo real
|
||||
function toggleRealTimeStreaming() {
|
||||
if (isStreamingVariables) {
|
||||
stopVariableStreaming();
|
||||
showMessage('Real-time streaming stopped', 'info');
|
||||
} else {
|
||||
startVariableStreaming();
|
||||
showMessage('Real-time streaming started', 'success');
|
||||
}
|
||||
|
||||
// Actualizar texto del botón
|
||||
const toggleBtn = document.getElementById('toggle-streaming-btn');
|
||||
if (toggleBtn) {
|
||||
if (isStreamingVariables) {
|
||||
toggleBtn.innerHTML = '⏹️ Stop Live Streaming';
|
||||
} else {
|
||||
toggleBtn.innerHTML = '▶️ Start Live Streaming';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Función de diagnóstico para problemas de conexión y variables
|
||||
function diagnoseConnection() {
|
||||
if (!currentDatasetId) {
|
||||
showMessage('No dataset selected for diagnosis', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const diagnoseBtn = document.getElementById('diagnose-btn');
|
||||
const originalText = diagnoseBtn.innerHTML;
|
||||
|
||||
// Deshabilitar botón y mostrar estado de diagnóstico
|
||||
diagnoseBtn.disabled = true;
|
||||
diagnoseBtn.innerHTML = '🔍 Diagnosing...';
|
||||
|
||||
// Crear informe de diagnóstico
|
||||
let diagnosticReport = [];
|
||||
|
||||
diagnosticReport.push('=== PLC CONNECTION DIAGNOSTICS ===');
|
||||
diagnosticReport.push(`Dataset: ${currentDatasetId}`);
|
||||
diagnosticReport.push(`Timestamp: ${new Date().toLocaleString()}`);
|
||||
diagnosticReport.push('');
|
||||
|
||||
// Paso 1: Verificar estado de conexión PLC
|
||||
fetch('/api/status')
|
||||
.then(response => response.json())
|
||||
.then(statusData => {
|
||||
diagnosticReport.push('1. PLC Connection Status:');
|
||||
diagnosticReport.push(` Connected: ${statusData.plc_connected ? 'YES' : 'NO'}`);
|
||||
diagnosticReport.push(` PLC IP: ${statusData.plc_config.ip}`);
|
||||
diagnosticReport.push(` Rack: ${statusData.plc_config.rack}`);
|
||||
diagnosticReport.push(` Slot: ${statusData.plc_config.slot}`);
|
||||
diagnosticReport.push('');
|
||||
|
||||
if (!statusData.plc_connected) {
|
||||
diagnosticReport.push(' ❌ PLC is not connected. Please check:');
|
||||
diagnosticReport.push(' - Network connectivity to PLC');
|
||||
diagnosticReport.push(' - PLC IP address, rack, and slot configuration');
|
||||
diagnosticReport.push(' - PLC is powered on and operational');
|
||||
diagnosticReport.push('');
|
||||
showDiagnosticResults(diagnosticReport);
|
||||
return;
|
||||
}
|
||||
|
||||
// Paso 2: Obtener información del dataset
|
||||
return fetch('/api/datasets')
|
||||
.then(response => response.json())
|
||||
.then(datasetData => {
|
||||
const dataset = datasetData.datasets[currentDatasetId];
|
||||
if (!dataset) {
|
||||
diagnosticReport.push('2. Dataset Status:');
|
||||
diagnosticReport.push(' ❌ Dataset not found');
|
||||
showDiagnosticResults(diagnosticReport);
|
||||
return;
|
||||
}
|
||||
|
||||
diagnosticReport.push('2. Dataset Information:');
|
||||
diagnosticReport.push(` Name: ${dataset.name}`);
|
||||
diagnosticReport.push(` Variables: ${Object.keys(dataset.variables).length}`);
|
||||
diagnosticReport.push(` Active: ${dataset.enabled ? 'YES' : 'NO'}`);
|
||||
diagnosticReport.push('');
|
||||
|
||||
// Paso 3: Probar lectura de variables con diagnósticos
|
||||
diagnosticReport.push('3. Variable Reading Test:');
|
||||
return fetch(`/api/datasets/${currentDatasetId}/variables/values`)
|
||||
.then(response => response.json())
|
||||
.then(valueData => {
|
||||
if (valueData.success) {
|
||||
const stats = valueData.stats || {};
|
||||
diagnosticReport.push(` ✅ Success: ${stats.success || 0}/${stats.total || 0} variables read`);
|
||||
|
||||
if (stats.failed > 0) {
|
||||
diagnosticReport.push(` ⚠️ Failed: ${stats.failed} variables had errors`);
|
||||
diagnosticReport.push('');
|
||||
diagnosticReport.push('4. Variable-Specific Errors:');
|
||||
|
||||
if (valueData.detailed_errors) {
|
||||
Object.keys(valueData.detailed_errors).forEach(varName => {
|
||||
diagnosticReport.push(` ${varName}: ${valueData.detailed_errors[varName]}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
diagnosticReport.push(' ✅ All variables read successfully');
|
||||
}
|
||||
} else {
|
||||
diagnosticReport.push(` ❌ Complete failure: ${valueData.message}`);
|
||||
diagnosticReport.push('');
|
||||
diagnosticReport.push('4. Detailed Error Information:');
|
||||
|
||||
if (valueData.detailed_errors) {
|
||||
Object.keys(valueData.detailed_errors).forEach(varName => {
|
||||
diagnosticReport.push(` ${varName}: ${valueData.detailed_errors[varName]}`);
|
||||
});
|
||||
}
|
||||
|
||||
diagnosticReport.push('');
|
||||
diagnosticReport.push('5. Troubleshooting Suggestions:');
|
||||
if (valueData.error_type === 'connection_error') {
|
||||
diagnosticReport.push(' - Check PLC network connection');
|
||||
diagnosticReport.push(' - Verify PLC is responding to network requests');
|
||||
diagnosticReport.push(' - Check firewall settings');
|
||||
} else if (valueData.error_type === 'all_failed') {
|
||||
diagnosticReport.push(' - Verify variable memory addresses are correct');
|
||||
diagnosticReport.push(' - Check if data blocks exist in PLC program');
|
||||
diagnosticReport.push(' - Ensure variable types match PLC configuration');
|
||||
}
|
||||
}
|
||||
|
||||
showDiagnosticResults(diagnosticReport);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
diagnosticReport.push('❌ Diagnostic failed with network error:');
|
||||
diagnosticReport.push(` ${error.message}`);
|
||||
diagnosticReport.push('');
|
||||
diagnosticReport.push('Troubleshooting:');
|
||||
diagnosticReport.push(' - Check web server connection');
|
||||
diagnosticReport.push(' - Refresh the page and try again');
|
||||
showDiagnosticResults(diagnosticReport);
|
||||
})
|
||||
.finally(() => {
|
||||
// Re-habilitar botón
|
||||
diagnoseBtn.disabled = false;
|
||||
diagnoseBtn.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
// Mostrar resultados de diagnóstico en consola y como mensaje
|
||||
function showDiagnosticResults(diagnosticReport) {
|
||||
const reportText = diagnosticReport.join('\n');
|
||||
|
||||
// Log a consola para análisis detallado
|
||||
console.log(reportText);
|
||||
|
||||
// Mostrar mensaje de resumen al usuario
|
||||
const errorCount = reportText.match(/❌/g)?.length || 0;
|
||||
const warningCount = reportText.match(/⚠️/g)?.length || 0;
|
||||
const successCount = reportText.match(/✅/g)?.length || 0;
|
||||
|
||||
let summaryMessage = 'Diagnosis completed. ';
|
||||
if (errorCount > 0) {
|
||||
summaryMessage += `${errorCount} errors found. `;
|
||||
}
|
||||
if (warningCount > 0) {
|
||||
summaryMessage += `${warningCount} warnings found. `;
|
||||
}
|
||||
if (successCount > 0) {
|
||||
summaryMessage += `${successCount} checks passed. `;
|
||||
}
|
||||
summaryMessage += 'Check browser console (F12) for detailed report.';
|
||||
|
||||
const messageType = errorCount > 0 ? 'error' : (warningCount > 0 ? 'warning' : 'success');
|
||||
showMessage(summaryMessage, messageType);
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"last_state": {
|
||||
"should_connect": true,
|
||||
"should_stream": false,
|
||||
"should_stream": true,
|
||||
"active_datasets": [
|
||||
"dar"
|
||||
]
|
||||
},
|
||||
"auto_recovery_enabled": true,
|
||||
"last_update": "2025-07-20T10:58:20.099906"
|
||||
"last_update": "2025-07-20T23:03:40.714677"
|
||||
}
|
2572
templates/index.html
2572
templates/index.html
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue