From 5e2149b9d4a827c90cad239e6d5fd95f6ffb9f1e Mon Sep 17 00:00:00 2001 From: Miguel Date: Sun, 10 Aug 2025 00:38:08 +0200 Subject: [PATCH] =?UTF-8?q?Actualizaci=C3=B3n=20de=20application=5Fevents.?= =?UTF-8?q?json=20con=20nuevos=20eventos=20para=20la=20creaci=C3=B3n=20y?= =?UTF-8?q?=20activaci=C3=B3n=20de=20datasets,=20as=C3=AD=20como=20la=20ge?= =?UTF-8?q?sti=C3=B3n=20de=20sesiones=20de=20plot.=20Se=20ajustaron=20las?= =?UTF-8?q?=20fechas=20de=20=C3=BAltima=20actualizaci=C3=B3n=20en=20varios?= =?UTF-8?q?=20archivos=20de=20configuraci=C3=B3n,=20incluyendo=20plc=5Fcon?= =?UTF-8?q?fig.json,=20plc=5Fdatasets.json=20y=20system=5Fstate.json.=20Se?= =?UTF-8?q?=20implementaron=20mejoras=20en=20la=20interfaz=20de=20usuario?= =?UTF-8?q?=20para=20la=20visualizaci=C3=B3n=20de=20datos=20en=20tiempo=20?= =?UTF-8?q?real=20y=20se=20optimiz=C3=B3=20el=20c=C3=B3digo=20en=20plottin?= =?UTF-8?q?g.js=20para=20una=20mejor=20gesti=C3=B3n=20de=20gr=C3=A1ficos.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application_events.json | 878 +++++++++++++++++++++++++++++++++++++++- core/plc_client.py | 139 ++++--- core/streamer.py | 8 +- plc_config.json | 2 +- plc_datasets.json | 38 +- plot_sessions.json | 14 +- static/css/styles.css | 7 +- static/js/plotting.js | 397 ++++++++++++------ system_state.json | 5 +- 9 files changed, 1282 insertions(+), 206 deletions(-) diff --git a/application_events.json b/application_events.json index 0fc7e0a..4dad8c6 100644 --- a/application_events.json +++ b/application_events.json @@ -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 } \ No newline at end of file diff --git a/core/plc_client.py b/core/plc_client.py index db7e3fd..f815444 100644 --- a/core/plc_client.py +++ b/core/plc_client.py @@ -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, diff --git a/core/streamer.py b/core/streamer.py index c5d1cb9..6902dc7 100644 --- a/core/streamer.py +++ b/core/streamer.py @@ -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 diff --git a/plc_config.json b/plc_config.json index 0a5af50..755cc45 100644 --- a/plc_config.json +++ b/plc_config.json @@ -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" } } \ No newline at end of file diff --git a/plc_datasets.json b/plc_datasets.json index 6dc3ee1..fe4e0a9 100644 --- a/plc_datasets.json +++ b/plc_datasets.json @@ -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" } \ No newline at end of file diff --git a/plot_sessions.json b/plot_sessions.json index 0215368..177c2b7 100644 --- a/plot_sessions.json +++ b/plot_sessions.json @@ -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" } \ No newline at end of file diff --git a/static/css/styles.css b/static/css/styles.css index 1ae5283..50bfd09 100644 --- a/static/css/styles.css +++ b/static/css/styles.css @@ -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%; } diff --git a/static/js/plotting.js b/static/js/plotting.js index 3165126..bd76fcb 100644 --- a/static/js/plotting.js +++ b/static/js/plotting.js @@ -166,7 +166,7 @@ class PlotManager {
- +
`; @@ -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 */ } } } diff --git a/system_state.json b/system_state.json index 729b631..5de8ec4 100644 --- a/system_state.json +++ b/system_state.json @@ -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" } \ No newline at end of file