Actualización de application_events.json con nuevos eventos para la creación y activación de datasets, así como la gestión de sesiones de plot. Se ajustaron las fechas de última actualización en varios archivos de configuración, incluyendo plc_config.json, plc_datasets.json y system_state.json. Se implementaron mejoras en la interfaz de usuario para la visualización de datos en tiempo real y se optimizó el código en plotting.js para una mejor gestión de gráficos.

This commit is contained in:
Miguel 2025-08-10 00:38:08 +02:00
parent 8d693c48c7
commit 5e2149b9d4
9 changed files with 1282 additions and 206 deletions

View File

@ -8145,8 +8145,882 @@
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T01:15:37.931252",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T01:15:37.995825",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T01:15:38.011841",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T01:15:53.487852",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_0",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 20,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-09T01:27:47.413936",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T01:27:47.482826",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T01:27:47.500264",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T01:32:23.076094",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T01:32:23.143386",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T01:32:23.158384",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T01:37:00.136346",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T01:37:00.187014",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T01:37:00.197221",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T01:39:03.481139",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_0",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 120,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-09T01:39:35.253297",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_0",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 500,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-09T01:41:40.082777",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T01:41:40.149440",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T01:41:40.164902",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T01:42:00.022025",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_0",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 50,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-09T01:47:39.354931",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T01:47:39.421225",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T01:47:39.436596",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T01:50:13.074173",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T01:50:13.159596",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T01:50:13.174855",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T01:53:53.601636",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T01:53:53.683028",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T01:53:53.697931",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T01:54:09.106165",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_0",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 10,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-09T01:54:12.772022",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_0",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 24,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-09T01:54:24.565758",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_0",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 5,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-09T01:55:13.651682",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_0",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 20,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-09T01:58:11.878053",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T01:58:11.947102",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T01:58:11.963722",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T02:02:17.438722",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T02:02:17.493178",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 2,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T02:02:17.504654",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 1
}
},
{
"timestamp": "2025-08-09T02:04:32.017747",
"level": "info",
"event_type": "plot_session_removed",
"message": "Plot session 'UR29' removed",
"details": {
"session_id": "plot_0"
}
},
{
"timestamp": "2025-08-09T02:05:35.486330",
"level": "info",
"event_type": "plot_session_created",
"message": "Plot session 'UR29' created and started",
"details": {
"session_id": "plot_1",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 60,
"trigger_variable": null,
"auto_started": true
}
},
{
"timestamp": "2025-08-09T02:06:26.841012",
"level": "info",
"event_type": "dataset_created",
"message": "Dataset created: Fast (prefix: fast)",
"details": {
"dataset_id": "Fast",
"name": "Fast",
"prefix": "fast",
"sampling_interval": 0.1
}
},
{
"timestamp": "2025-08-09T02:06:51.166985",
"level": "info",
"event_type": "dataset_csv_file_created",
"message": "New CSV file created after variable modification for dataset 'DAR': gateway_phoenix_02_06_51.csv",
"details": {
"dataset_id": "DAR",
"file_path": "records\\09-08-2025\\gateway_phoenix_02_06_51.csv",
"variables_count": 3,
"reason": "variable_modification"
}
},
{
"timestamp": "2025-08-09T02:06:51.178095",
"level": "info",
"event_type": "variable_added",
"message": "Variable added to dataset 'DAR': fUR29_Brix -> DB1011.1322 (real)",
"details": {
"dataset_id": "DAR",
"name": "fUR29_Brix",
"area": "db",
"db": 1011,
"offset": 1322,
"bit": null,
"type": "real",
"streaming": false
}
},
{
"timestamp": "2025-08-09T02:07:40.947419",
"level": "info",
"event_type": "variable_added",
"message": "Variable added to dataset 'Fast': fUR29_Brix -> DB1011.1322 (real)",
"details": {
"dataset_id": "Fast",
"name": "fUR29_Brix",
"area": "db",
"db": 1011,
"offset": 1322,
"bit": null,
"type": "real",
"streaming": false
}
},
{
"timestamp": "2025-08-09T02:08:07.444267",
"level": "info",
"event_type": "variable_added",
"message": "Variable added to dataset 'Fast': fUR29_ma -> DB1011.1296 (real)",
"details": {
"dataset_id": "Fast",
"name": "fUR29_ma",
"area": "db",
"db": 1011,
"offset": 1296,
"bit": null,
"type": "real",
"streaming": false
}
},
{
"timestamp": "2025-08-09T02:08:26.500463",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: Fast",
"details": {
"dataset_id": "Fast",
"variables_count": 2,
"streaming_count": 0,
"prefix": "fast"
}
},
{
"timestamp": "2025-08-09T02:09:13.534886",
"level": "info",
"event_type": "plot_session_created",
"message": "Plot session 'Fast' created and started",
"details": {
"session_id": "plot_2",
"variables": [
"fUR29_ma",
"UR29_Brix",
"UR29_ma",
"fUR29_Brix"
],
"time_window": 60,
"trigger_variable": null,
"auto_started": true
}
},
{
"timestamp": "2025-08-09T22:43:54.204995",
"level": "info",
"event_type": "datasets_resumed_after_reconnection",
"message": "Automatically resumed streaming for 2 datasets after PLC reconnection",
"details": {
"resumed_datasets": 2,
"total_attempted": 2
}
},
{
"timestamp": "2025-08-09T23:22:59.642234",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T23:22:59.723755",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 3,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T23:22:59.737379",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: Fast",
"details": {
"dataset_id": "Fast",
"variables_count": 2,
"streaming_count": 0,
"prefix": "fast"
}
},
{
"timestamp": "2025-08-09T23:22:59.748890",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 2 datasets activated",
"details": {
"activated_datasets": 2,
"total_datasets": 2
}
},
{
"timestamp": "2025-08-09T23:27:02.476867",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'Fast' configuration updated",
"details": {
"session_id": "plot_2",
"new_config": {
"name": "Fast",
"variables": [
"fUR29_ma",
"UR29_Brix",
"UR29_ma",
"fUR29_Brix"
],
"time_window": 10,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-09T23:42:15.949524",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-09T23:42:16.047945",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 3,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-09T23:42:16.061947",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: Fast",
"details": {
"dataset_id": "Fast",
"variables_count": 2,
"streaming_count": 0,
"prefix": "fast"
}
},
{
"timestamp": "2025-08-09T23:42:16.079052",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 2 datasets activated",
"details": {
"activated_datasets": 2,
"total_datasets": 2
}
},
{
"timestamp": "2025-08-10T00:32:18.359734",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'Fast' configuration updated",
"details": {
"session_id": "plot_2",
"new_config": {
"name": "Fast",
"variables": [
"fUR29_ma",
"UR29_Brix",
"UR29_ma",
"fUR29_Brix"
],
"time_window": 60,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-10T00:32:50.990081",
"level": "info",
"event_type": "plot_session_removed",
"message": "Plot session 'Fast' removed",
"details": {
"session_id": "plot_2"
}
},
{
"timestamp": "2025-08-10T00:33:55.840458",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_1",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 65,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-10T00:34:06.009225",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_1",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
],
"time_window": 75,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-10T00:34:26.104532",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_1",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma",
"fUR29_Brix"
],
"time_window": 75,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
},
{
"timestamp": "2025-08-10T00:37:18.004596",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-10T00:37:18.089614",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "DAR",
"variables_count": 3,
"streaming_count": 2,
"prefix": "gateway_phoenix"
}
},
{
"timestamp": "2025-08-10T00:37:18.104615",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: Fast",
"details": {
"dataset_id": "Fast",
"variables_count": 2,
"streaming_count": 0,
"prefix": "fast"
}
},
{
"timestamp": "2025-08-10T00:37:18.121614",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 2 datasets activated",
"details": {
"activated_datasets": 2,
"total_datasets": 2
}
},
{
"timestamp": "2025-08-10T00:37:46.526185",
"level": "info",
"event_type": "plot_session_updated",
"message": "Plot session 'UR29' configuration updated",
"details": {
"session_id": "plot_1",
"new_config": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma",
"fUR29_Brix",
"fUR29_ma"
],
"time_window": 75,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true
}
}
}
],
"last_updated": "2025-08-09T01:04:33.328051",
"total_entries": 767
"last_updated": "2025-08-10T00:37:46.526185",
"total_entries": 834
}

View File

@ -35,6 +35,12 @@ class PLCClient:
[]
) # List of callback functions for disconnection tracking
# Global I/O serialization to avoid concurrent snap7 calls
# Acts as a simple read queue to prevent 'CLI : Job pending'
self.io_lock = threading.RLock()
# Small inter-read delay to give PLC time between requests (seconds)
self.inter_read_delay_seconds = 0.002
def connect(self, ip: str, rack: int, slot: int) -> bool:
"""Connect to S7-315 PLC"""
try:
@ -223,65 +229,86 @@ class PLCClient:
self.reconnection_thread.start()
def read_variable(self, var_config: Dict[str, Any]) -> Any:
"""Read a specific variable from the PLC"""
"""Read a specific variable from the PLC, serialized across threads"""
if not self.is_connected():
return None
try:
area_type = var_config.get("area", "db").lower()
offset = var_config["offset"]
var_type = var_config["type"]
bit = var_config.get("bit")
# Ensure only one snap7 operation at a time
with self.io_lock:
try:
area_type = var_config.get("area", "db").lower()
offset = var_config["offset"]
var_type = var_config["type"]
bit = var_config.get("bit")
if area_type == "db":
return self._read_db_variable(var_config, offset, var_type, bit)
elif area_type in ["mw", "m"]:
return self._read_memory_variable(offset, var_type)
elif area_type in ["pew", "pe"]:
return self._read_input_variable(offset, var_type)
elif area_type in ["paw", "pa"]:
return self._read_output_variable(offset, var_type)
elif area_type == "e":
return self._read_input_bit(offset, bit)
elif area_type == "a":
return self._read_output_bit(offset, bit)
elif area_type == "mb":
return self._read_memory_bit(offset, bit)
else:
if self.logger:
self.logger.error(f"Unsupported area type: {area_type}")
return None
except Exception as e:
if self.logger:
self.logger.error(f"Error reading variable: {e}")
# Check if this is a connection error and start automatic reconnection
if self._is_connection_error(str(e)):
was_connected_before = self.connected
self.connected = False
self.consecutive_failures += 1
if self.logger:
failure_num = self.consecutive_failures
msg = (
"Connection error detected, starting automatic "
f"reconnection (failure #{failure_num})"
if area_type == "db":
result = self._read_db_variable(
var_config,
offset,
var_type,
bit,
)
self.logger.warning(msg)
# If we were connected before, notify disconnection callbacks FIRST
if was_connected_before:
elif area_type in ["mw", "m"]:
result = self._read_memory_variable(offset, var_type)
elif area_type in [
"pew",
"pe",
]:
result = self._read_input_variable(offset, var_type)
elif area_type in [
"paw",
"pa",
]:
result = self._read_output_variable(offset, var_type)
elif area_type == "e":
result = self._read_input_bit(offset, bit)
elif area_type == "a":
result = self._read_output_bit(offset, bit)
elif area_type == "mb":
result = self._read_memory_bit(offset, bit)
else:
if self.logger:
self.logger.info(
"Notifying disconnection callbacks for dataset tracking"
self.logger.error(f"Unsupported area type: {area_type}")
result = None
# Small pacing delay between PLC I/O to avoid job overlap
if self.inter_read_delay_seconds and self.inter_read_delay_seconds > 0:
time.sleep(self.inter_read_delay_seconds)
return result
except Exception as e:
if self.logger:
self.logger.error(f"Error reading variable: {e}")
# Check if this is a connection error and start automatic reconnection
if self._is_connection_error(str(e)):
was_connected_before = self.connected
self.connected = False
self.consecutive_failures += 1
if self.logger:
failure_num = self.consecutive_failures
msg = (
"Connection error detected, starting automatic "
f"reconnection (failure #{failure_num})"
)
self._notify_disconnection_detected()
self.logger.warning(msg)
# Start automatic reconnection in background
self._start_automatic_reconnection()
# If we were connected before, notify disconnection
# callbacks FIRST
if was_connected_before:
if self.logger:
self.logger.info(
"Notifying disconnection callbacks for "
"dataset tracking"
)
self._notify_disconnection_detected()
return None
# Start automatic reconnection in background
self._start_automatic_reconnection()
return None
def _read_db_variable(
self, var_config: Dict[str, Any], offset: int, var_type: str, bit: Optional[int]
@ -451,7 +478,8 @@ class PLCClient:
- DataStreamer.read_dataset_variables() during streaming cycles
- Internal diagnostic operations
Public APIs should use cached values via DataStreamer.get_cached_dataset_values()
Public APIs should use cached values via
DataStreamer.get_cached_dataset_values()
"""
if not self.is_connected():
return {}
@ -478,9 +506,10 @@ class PLCClient:
- 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.
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 {
@ -550,7 +579,7 @@ class PLCClient:
}
elif success_count < total_count:
warning_msg = (
f"Partial success: {success_count}/{total_count} " "variables read"
f"Partial success: {success_count}/{total_count} " f"variables read"
)
return {
"success": True,

View File

@ -481,11 +481,15 @@ class DataStreamer:
try:
start_time = time.time()
# Read variables for this dataset
# Read variables for this dataset (serialized across datasets)
dataset_variables = self.config_manager.get_dataset_variables(
dataset_id
)
all_data = self.read_dataset_variables(dataset_id, dataset_variables)
# Ensure entire dataset read is atomic w.r.t. other datasets
with self.plc_client.io_lock:
all_data = self.read_dataset_variables(
dataset_id, dataset_variables
)
if all_data:
consecutive_errors = 0

View File

@ -16,6 +16,6 @@
"max_days": 30,
"max_hours": null,
"cleanup_interval_hours": 24,
"last_cleanup": "2025-08-08T15:50:27.922821"
"last_cleanup": "2025-08-09T22:43:54.224975"
}
}

View File

@ -17,6 +17,13 @@
"type": "real",
"streaming": true,
"db": 1011
},
"fUR29_Brix": {
"area": "db",
"offset": 1322,
"type": "real",
"streaming": false,
"db": 1011
}
},
"streaming_variables": [
@ -26,12 +33,37 @@
"sampling_interval": 1.0,
"enabled": true,
"created": "2025-08-08T15:47:18.566053"
},
"Fast": {
"name": "Fast",
"prefix": "fast",
"variables": {
"fUR29_Brix": {
"area": "db",
"offset": 1322,
"type": "real",
"streaming": false,
"db": 1011
},
"fUR29_ma": {
"area": "db",
"offset": 1296,
"type": "real",
"streaming": false,
"db": 1011
}
},
"streaming_variables": [],
"sampling_interval": 0.1,
"enabled": true,
"created": "2025-08-09T02:06:26.840011"
}
},
"active_datasets": [
"DAR"
"DAR",
"Fast"
],
"current_dataset_id": "DAR",
"current_dataset_id": "Fast",
"version": "1.0",
"last_update": "2025-08-09T01:04:33.316045"
"last_update": "2025-08-10T00:37:18.103618"
}

View File

@ -1,21 +1,23 @@
{
"plots": {
"plot_0": {
"plot_1": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR29_ma"
"UR29_ma",
"fUR29_Brix",
"fUR29_ma"
],
"time_window": 50,
"time_window": 75,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true,
"session_id": "plot_0"
"session_id": "plot_1"
}
},
"session_counter": 1,
"last_saved": "2025-08-09T00:59:52.219460",
"session_counter": 2,
"last_saved": "2025-08-10T00:37:46.525175",
"version": "1.0"
}

View File

@ -545,7 +545,8 @@ textarea {
border: var(--pico-border-width) solid var(--pico-border-color);
border-radius: var(--pico-border-radius);
margin-bottom: 1rem;
overflow: hidden;
/* Evitar recortar etiquetas inferiores del eje del chart */
overflow: visible;
}
.plot-header {
@ -631,7 +632,9 @@ textarea {
.plot-canvas canvas {
width: 100% !important;
height: 100% !important;
/* Permitir a Chart.js ajustar el alto con precisión */
height: 100%;
display: block;
max-height: 100%;
}

View File

@ -166,7 +166,7 @@ class PlotManager {
</span>
</div>
<div class="plot-canvas">
<canvas id="chart-${sessionId}" style="height: 400px;"></canvas>
<canvas id="chart-${sessionId}"></canvas>
</div>
`;
@ -196,9 +196,13 @@ class PlotManager {
backgroundColor: color + '20',
borderWidth: 2,
fill: false,
pointRadius: 0,
pointHoverRadius: 3,
tension: 0.1
// Unir huecos por defecto; los cortes reales los forzamos con NaN al reanudar
spanGaps: true,
pointRadius: 1,
pointHoverRadius: 4,
cubicInterpolationMode: 'monotone',
// stepped: true,
tension: 0.4
});
});
chartConfig.data.datasets = datasets;
@ -224,7 +228,10 @@ class PlotManager {
lastDataFetch: 0,
datasetIndex: new Map(), // Mapeo de variable -> índice de dataset
isRealTimeMode: isRealTimeMode,
lastPushedXByDataset: new Map() // índice de dataset -> último timestamp x añadido
lastPushedXByDataset: new Map(), // índice de dataset -> último timestamp x añadido
ingestPaused: false, // si true, no se agregan puntos nuevos (marca hueco)
insertNaNOnNextIngest: false,
isPaused: false
});
// Fijar refresh de datos base (no de render). El render es 30 FPS en el plugin.
@ -260,6 +267,9 @@ class PlotManager {
responsive: true,
maintainAspectRatio: false,
animation: false,
layout: {
padding: { bottom: 16 }
},
scales: {
x: {
@ -274,6 +284,21 @@ class PlotManager {
this.onStreamingRefresh(sessionId, chart);
}
},
// Asegurar etiquetas de tiempo visibles y formateadas
ticks: {
display: true,
autoSkip: true,
maxRotation: 0
},
// Formatos de visualización para diferentes unidades de tiempo
time: {
displayFormats: {
millisecond: 'HH:mm:ss.SSS',
second: 'HH:mm:ss',
minute: 'HH:mm',
hour: 'HH:mm'
}
},
title: {
display: true,
text: 'Tiempo'
@ -354,6 +379,9 @@ class PlotManager {
responsive: true,
maintainAspectRatio: false,
animation: false,
layout: {
padding: { bottom: 16 }
},
scales: {
x: {
@ -364,6 +392,11 @@ class PlotManager {
second: 'HH:mm:ss'
}
},
ticks: {
display: true,
autoSkip: true,
maxRotation: 0
},
title: {
display: true,
text: 'Tiempo'
@ -451,6 +484,7 @@ class PlotManager {
backgroundColor: color + '20',
borderWidth: 2,
fill: false,
spanGaps: true,
pointRadius: 0,
pointHoverRadius: 3,
tension: 0.1
@ -478,7 +512,7 @@ class PlotManager {
// Evitar llamadas muy frecuentes basado en el refresh rate dinámico
const now = Date.now();
const refreshRate = this.refreshRates.get(sessionId) || 1000;
const minInterval = Math.max(refreshRate * 0.8, 100); // 80% del refresh rate, mínimo 100ms
const minInterval = Math.max(refreshRate * 0.5, 50); // menos restrictivo para alta frecuencia
if (now - sessionData.lastDataFetch < minInterval) {
const timeSinceLastUpdate = now - sessionData.lastDataFetch;
@ -517,29 +551,68 @@ class PlotManager {
const chart = sessionData.chart;
const isRealTimeMode = sessionData.isRealTimeMode;
// Si está en pausa, no ingerir datos ni empujar puntos
try {
if (isRealTimeMode) {
const xScale = chart.scales && chart.scales.x;
if (xScale && xScale.realtime && xScale.realtime.pause) {
return;
}
}
if (sessionData.ingestPaused) {
return;
}
} catch (_) { /* noop */ }
// En modo realtime, dejamos el panning continuo al plugin mediante 'delay'.
// Solo empujamos puntos cuando haya datos nuevos del backend, usando su timestamp real.
// Empujar todos los puntos nuevos disponibles para minimizar huecos.
let pointsAdded = 0;
chart.data.datasets.forEach((chartDataset, datasetIndex) => {
const lastPushedX = sessionData.lastPushedXByDataset.get(datasetIndex) || 0;
const backendDataset = plotData && plotData.datasets ? plotData.datasets[datasetIndex] : undefined;
if (!backendDataset || !backendDataset.data || backendDataset.data.length === 0) {
if (!backendDataset || !Array.isArray(backendDataset.data) || backendDataset.data.length === 0) {
return; // No hay datos nuevos para este dataset
}
const latestPoint = backendDataset.data[backendDataset.data.length - 1];
if (!latestPoint || latestPoint.y === null || latestPoint.y === undefined) {
return; // Punto inválido
// Filtrar y normalizar puntos con y válido y x en ms
const newPoints = [];
for (let i = 0; i < backendDataset.data.length; i++) {
const p = backendDataset.data[i];
const yNum = (typeof p.y === 'number') ? p.y : Number(p.y);
if (!isFinite(yNum)) continue;
let xNum = (typeof p.x === 'number') ? p.x : Number(p.x);
if (!isFinite(xNum)) continue;
if (xNum < 1e12) xNum = xNum * 1000; // segundos -> ms
if (xNum > lastPushedX) newPoints.push({ x: xNum, y: yNum });
}
// Garantizar monotonicidad en X sin forzar Date.now(): usar timestamp real del backend
const candidateX = typeof latestPoint.x === 'number' ? latestPoint.x : 0;
const finalX = Math.max(candidateX, lastPushedX + 1);
if (newPoints.length === 0) {
return;
}
chartDataset.data.push({ x: finalX, y: latestPoint.y });
sessionData.lastPushedXByDataset.set(datasetIndex, finalX);
pointsAdded++;
// Ordenar por x y garantizar monotonicidad con el último x
newPoints.sort((a, b) => a.x - b.x);
// Insertar NaN solo una vez al reanudar, antes del primer punto nuevo
if (sessionData.insertNaNOnNextIngest) {
const firstX = Math.max(newPoints[0].x, lastPushedX + 1);
let gapX = firstX - 1;
if (gapX <= lastPushedX) {
gapX = firstX - 0.001; // asegurar x estrictamente mayor que lastPushedX
}
chartDataset.data.push({ x: gapX, y: NaN });
}
// Empujar todos los puntos nuevos ajustando x para ser estrictamente creciente
let lastX = lastPushedX;
for (const p of newPoints) {
const finalX = Math.max(p.x, lastX + 1);
chartDataset.data.push({ x: finalX, y: p.y });
lastX = finalX;
pointsAdded++;
}
sessionData.lastPushedXByDataset.set(datasetIndex, lastX);
});
// En modo fallback (sin realtime), limpieza y update manual
@ -551,6 +624,11 @@ class PlotManager {
// Referencia: guía oficial (Pull Model - Asynchronous)
chart.update('quiet');
}
// Si se insertaron puntos en este ciclo, limpiar la marca para no seguir insertando NaN
if (pointsAdded > 0 && sessionData.insertNaNOnNextIngest) {
sessionData.insertNaNOnNextIngest = false;
}
}
/**
@ -667,16 +745,27 @@ class PlotManager {
if (sessionData.isRealTimeMode) {
// Modo realtime - pausar escala
const xScale = sessionData.chart.scales.x;
const chart = sessionData.chart;
const xScale = chart.scales.x;
if (xScale && xScale.realtime) {
xScale.realtime.pause = true;
}
if (chart.options && chart.options.scales && chart.options.scales.x && chart.options.scales.x.realtime) {
chart.options.scales.x.realtime.pause = true;
}
chart.update('quiet');
// Marcar pausa de ingesta para crear hueco (NaN) en reanudación
sessionData.ingestPaused = true;
sessionData.isPaused = true;
} else {
// Modo fallback - pausar intervalo manual
if (sessionData.manualInterval) {
clearInterval(sessionData.manualInterval);
sessionData.manualInterval = null;
}
sessionData.ingestPaused = true;
sessionData.isPaused = true;
}
}
@ -689,16 +778,25 @@ class PlotManager {
if (sessionData.isRealTimeMode) {
// Modo realtime - reanudar escala
const xScale = sessionData.chart.scales.x;
const chart = sessionData.chart;
const xScale = chart.scales.x;
if (xScale && xScale.realtime) {
xScale.realtime.pause = false;
}
if (chart.options && chart.options.scales && chart.options.scales.x && chart.options.scales.x.realtime) {
chart.options.scales.x.realtime.pause = false;
}
chart.update('quiet');
} else {
// Modo fallback - reanudar intervalo manual
if (!sessionData.manualInterval) {
this.startManualRefresh(sessionId);
}
}
// Marcar para insertar un único NaN en la próxima ingesta
sessionData.insertNaNOnNextIngest = true;
sessionData.ingestPaused = false;
sessionData.isPaused = false;
}
/**
@ -1449,42 +1547,37 @@ class PlotManager {
let sessionId = null;
if (this.currentEditingSession) {
// Modo edición: ELIMINAR el plot existente y crear uno nuevo desde cero
// 1. Eliminar el plot existente
const deleteResponse = await fetch(`/api/plots/${this.currentEditingSession}`, {
method: 'DELETE'
});
if (!deleteResponse.ok) {
throw new Error('Failed to delete existing plot');
}
// 2. Remover de la UI
if (typeof tabManager !== 'undefined') {
tabManager.removePlotTab(this.currentEditingSession);
}
// 3. Remover del PlotManager
if (this.sessions.has(this.currentEditingSession)) {
const chart = this.sessions.get(this.currentEditingSession);
if (chart) {
chart.destroy();
}
this.sessions.delete(this.currentEditingSession);
// Limpiar refresh rate
this.refreshRates.delete(this.currentEditingSession);
}
// 4. Crear nuevo plot desde cero
response = await fetch('/api/plots', {
method: 'POST',
// Modo edición: actualizar configuración vía PUT sin eliminar la sesión
const sessionIdToUpdate = this.currentEditingSession;
response = await fetch(`/api/plots/${sessionIdToUpdate}/config`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const deleteResult = await deleteResponse.json();
const result = await response.json();
if (result && result.success) {
// Actualizar estado local y aplicar cambios al chart existente
const sessionData = this.sessions.get(sessionIdToUpdate);
if (sessionData) {
sessionData.config = { ...(sessionData.config || {}), ...config };
this.applyConfigToChart(sessionIdToUpdate, config);
}
// Actualizar nombre del tab si cambió
if (typeof tabManager !== 'undefined' && config.name) {
tabManager.updatePlotTabName(sessionIdToUpdate, config.name);
tabManager.switchSubTab(`plot-${sessionIdToUpdate}`);
}
this.updatePlotStats(sessionIdToUpdate, sessionData ? sessionData.config : config);
this.hidePlotForm();
showNotification(result.message || 'Plot updated', 'success');
return;
} else {
showNotification((result && result.error) || 'Failed to update plot', 'error');
return;
}
} else {
// Modo creación normal
response = await fetch('/api/plots', {
@ -1492,46 +1585,36 @@ class PlotManager {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
}
const result = await response.json();
if (result.success) {
sessionId = result.session_id;
if (this.currentEditingSession) {
showNotification(`Plot recreated successfully: ${config.name}`, 'success');
} else {
const result = await response.json();
if (result.success) {
sessionId = result.session_id;
showNotification(result.message, 'success');
// Crear nuevo tab para la sesión
await this.createPlotSessionTab(sessionId, {
name: config.name,
variables_count: config.variables.length,
trigger_enabled: config.trigger_enabled,
trigger_variable: config.trigger_variable,
trigger_on_true: config.trigger_on_true,
is_active: false,
is_paused: false
});
// Cambiar al sub-tab del nuevo plot
if (typeof tabManager !== 'undefined') {
tabManager.switchSubTab(`plot-${sessionId}`);
}
this.hidePlotForm();
} else {
showNotification(result.error, 'error');
}
// Crear nuevo tab para la sesión (tanto en modo edición como creación)
await this.createPlotSessionTab(sessionId, {
name: config.name,
variables_count: config.variables.length,
trigger_enabled: config.trigger_enabled,
trigger_variable: config.trigger_variable,
trigger_on_true: config.trigger_on_true,
is_active: false,
is_paused: false
});
// Cambiar al sub-tab del nuevo plot
if (typeof tabManager !== 'undefined') {
tabManager.switchSubTab(`plot-${sessionId}`);
}
this.hidePlotForm();
} else {
showNotification(result.error, 'error');
}
} catch (error) {
console.error('Error submitting plot form:', error);
if (this.currentEditingSession) {
showNotification('Error recreating plot. Please try again.', 'error');
} else {
showNotification('Error creating plot', 'error');
}
showNotification(this.currentEditingSession ? 'Error updating plot' : 'Error creating plot', 'error');
}
}
@ -1676,65 +1759,113 @@ class PlotManager {
const sessionData = this.sessions.get(sessionId);
if (!sessionData || !sessionData.chart) return;
// Guardar config local
const chart = sessionData.chart;
const wasPaused = sessionData.isPaused === true;
// Pausar ingesta/render en modo realtime para evitar condiciones de carrera
try {
if (sessionData.isRealTimeMode && chart.scales && chart.scales.x && chart.scales.x.realtime) {
chart.scales.x.realtime.pause = true;
}
sessionData.ingestPaused = true;
} catch (_) { /* noop */ }
// Guardar config local combinada
sessionData.config = { ...(sessionData.config || {}), ...config };
// 1) Actualizar ventana de tiempo
// 1) Detectar si cambian las variables/datasets (antes de tocar el chart)
if (Array.isArray(config.variables)) {
const previousLabels = (chart.data.datasets || []).map(ds => ds && ds.label).filter(Boolean);
const changed = previousLabels.length !== config.variables.length ||
previousLabels.some((lbl, idx) => lbl !== config.variables[idx]);
if (changed) {
// Preservar refresh rate para la sesión
const previousRefresh = this.refreshRates.get(sessionId) || 1000;
// Destruir chart actual para evitar estados inconsistentes del plugin
try { chart.destroy(); } catch (_) { }
// Re-crear el chart con la nueva configuración sobre el mismo canvas
this.createStreamingChart(sessionId, sessionData.config);
// Restaurar refresh rate y timers
this.updateRefreshRate(sessionId, previousRefresh);
// Reaplicar estado de pausa/ingesta sobre el nuevo chart
const newSessionData = this.sessions.get(sessionId);
if (newSessionData) {
newSessionData.isPaused = wasPaused;
newSessionData.ingestPaused = wasPaused;
try {
if (newSessionData.isRealTimeMode && newSessionData.chart && newSessionData.chart.scales && newSessionData.chart.scales.x && newSessionData.chart.scales.x.realtime) {
newSessionData.chart.scales.x.realtime.pause = wasPaused;
} else if (!newSessionData.isRealTimeMode && !wasPaused) {
this.startManualRefresh(sessionId);
}
} catch (_) { /* noop */ }
}
// Y-axis range ya fue aplicado por createStreamingChart a partir de config
return;
} else {
// Si no cambió el conjunto de variables, solo refrescar estilos
const newDatasets = config.variables.map((variable, index) => {
const color = this.getColor(variable, index);
const existing = chart.data.datasets[index];
if (existing) {
existing.label = variable;
existing.borderColor = color;
existing.backgroundColor = color + '20';
existing.borderWidth = 2;
existing.fill = false;
existing.spanGaps = true;
existing.pointRadius = 0;
existing.pointHoverRadius = 3;
existing.tension = 0.1;
return existing;
}
return {
label: variable,
data: [],
borderColor: color,
backgroundColor: color + '20',
borderWidth: 2,
fill: false,
spanGaps: true,
pointRadius: 0,
pointHoverRadius: 3,
tension: 0.1
};
});
chart.data.datasets = newDatasets;
// Recalcular índices y timestamps de empuje
sessionData.datasetIndex = new Map();
sessionData.lastPushedXByDataset = new Map();
config.variables.forEach((variable, idx) => sessionData.datasetIndex.set(variable, idx));
}
}
// 2) Actualizar ventana de tiempo y rango Y cuando no hubo recreación
if (typeof config.time_window !== 'undefined') {
this.updateTimeWindow(sessionId, config.time_window);
}
// 2) Actualizar rango Y
const chart = sessionData.chart;
if (chart.options && chart.options.scales && chart.options.scales.y) {
chart.options.scales.y.min = config.y_min ?? undefined;
chart.options.scales.y.max = config.y_max ?? undefined;
}
// 3) Actualizar datasets según variables
if (Array.isArray(config.variables)) {
const oldDatasets = chart.data.datasets || [];
const oldByLabel = new Map();
oldDatasets.forEach(ds => {
if (ds && typeof ds.label === 'string') oldByLabel.set(ds.label, ds);
});
try { chart.update('quiet'); } catch (_) { }
const newDatasets = config.variables.map((variable, index) => {
const existing = oldByLabel.get(variable);
const color = this.getColor(variable, index);
if (existing) {
existing.label = variable;
existing.borderColor = color;
existing.backgroundColor = color + '20';
existing.borderWidth = 2;
existing.fill = false;
existing.pointRadius = 0;
existing.pointHoverRadius = 3;
existing.tension = 0.1;
return existing;
}
return {
label: variable,
data: [],
borderColor: color,
backgroundColor: color + '20',
borderWidth: 2,
fill: false,
pointRadius: 0,
pointHoverRadius: 3,
tension: 0.1
};
});
chart.data.datasets = newDatasets;
// Recalcular índices y timestamps de empuje
sessionData.datasetIndex = new Map();
sessionData.lastPushedXByDataset = new Map();
config.variables.forEach((variable, idx) => sessionData.datasetIndex.set(variable, idx));
}
chart.update('quiet');
// 3) Reanudar ingesta/render si no estaba en pausa antes
try {
sessionData.ingestPaused = wasPaused;
sessionData.isPaused = wasPaused;
if (sessionData.isRealTimeMode && chart.scales && chart.scales.x && chart.scales.x.realtime) {
chart.scales.x.realtime.pause = wasPaused;
}
} catch (_) { /* noop */ }
}
}

View File

@ -3,9 +3,10 @@
"should_connect": true,
"should_stream": false,
"active_datasets": [
"DAR"
"DAR",
"Fast"
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-09T01:04:33.338067"
"last_update": "2025-08-10T00:37:18.130615"
}