From 0c11ee3ae2db57144b060751ea0ee484a3ca8289 Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 11 Aug 2025 16:26:20 +0200 Subject: [PATCH] =?UTF-8?q?Actualizaci=C3=B3n=20de=20application=5Fevents.?= =?UTF-8?q?json=20para=20incluir=20nuevos=20eventos=20de=20inicio=20de=20a?= =?UTF-8?q?plicaci=C3=B3n=20y=20ajustes=20en=20las=20fechas=20de=20=C3=BAl?= =?UTF-8?q?tima=20actualizaci=C3=B3n.=20Se=20eliminaron=20archivos=20obsol?= =?UTF-8?q?etos=20relacionados=20con=20la=20integraci=C3=B3n=20de=20Chart.?= =?UTF-8?q?js=20y=20se=20reorganizaron=20las=20rutas=20de=20configuraci?= =?UTF-8?q?=C3=B3n=20en=20el=20c=C3=B3digo.=20Se=20implementaron=20mejoras?= =?UTF-8?q?=20en=20la=20gesti=C3=B3n=20de=20esquemas=20y=20se=20optimiz?= =?UTF-8?q?=C3=B3=20la=20carga=20de=20recursos=20est=C3=A1ticos,=20incluye?= =?UTF-8?q?ndo=20la=20favicon=20y=20logos=20en=20la=20interfaz.=20Adem?= =?UTF-8?q?=C3=A1s,=20se=20realizaron=20ajustes=20en=20el=20manejo=20de=20?= =?UTF-8?q?errores=20y=20se=20mejor=C3=B3=20la=20estructura=20de=20directo?= =?UTF-8?q?rios=20para=20una=20mejor=20organizaci=C3=B3n=20del=20proyecto.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CHARTJS_STREAMING_INTEGRATION.md | 0 .../CONSOLE_CLEANUP_SUMMARY.md | 0 .../EJEMPLO_USO_PLOTTING.md | 0 .doc/MemoriaDeEvolucion.md | 16 ++ .../PLOTTING_FINALIZATION_SUMMARY.md | 0 .../PLOTTING_FIXES_FINAL.md | 0 .../PLOTTING_FIXES_RESUMEN.md | 0 .../PLOTTING_STATUS_FINAL.md | 0 PLOTTING_SYSTEM.md => .doc/PLOTTING_SYSTEM.md | 0 README.md => .doc/README.md | 0 .../REAL_TIME_STREAMING.md | 0 .../STREAMING_TROUBLESHOOTING.md | 0 application_events.json | 18 +- config/data/plc_config.json | 21 ++ config/data/plc_datasets.json | 69 +++++++ config/data/plot_sessions.json | 23 +++ config/schema/datasets.schema.json | 183 ++++++++++++++++++ config/schema/plc.schema.json | 154 +++++++++++++++ config/schema/plots.schema.json | 100 ++++++++++ config/schema/ui/datasets.uischema.json | 42 ++++ config/schema/ui/plc.uischema.json | 44 +++++ config/schema/ui/plots.uischema.json | 21 ++ core/config_manager.py | 8 +- core/plot_manager.py | 6 +- core/schema_manager.py | 24 ++- frontend/index.html | 25 +-- frontend/public/favicon.ico | Bin 0 -> 15983 bytes frontend/public/record.png | Bin 0 -> 15983 bytes frontend/src/App.jsx | 5 +- frontend/src/assets/logo/record.png | Bin 0 -> 15983 bytes frontend/src/pages/Config.jsx | 5 +- frontend/src/pages/Dashboard.jsx | 4 +- frontend/vite.config.js | 8 + main.py | 61 +++++- static/icons/favicon.ico | Bin 0 -> 15983 bytes 35 files changed, 809 insertions(+), 28 deletions(-) rename CHARTJS_STREAMING_INTEGRATION.md => .doc/CHARTJS_STREAMING_INTEGRATION.md (100%) rename CONSOLE_CLEANUP_SUMMARY.md => .doc/CONSOLE_CLEANUP_SUMMARY.md (100%) rename EJEMPLO_USO_PLOTTING.md => .doc/EJEMPLO_USO_PLOTTING.md (100%) rename PLOTTING_FINALIZATION_SUMMARY.md => .doc/PLOTTING_FINALIZATION_SUMMARY.md (100%) rename PLOTTING_FIXES_FINAL.md => .doc/PLOTTING_FIXES_FINAL.md (100%) rename PLOTTING_FIXES_RESUMEN.md => .doc/PLOTTING_FIXES_RESUMEN.md (100%) rename PLOTTING_STATUS_FINAL.md => .doc/PLOTTING_STATUS_FINAL.md (100%) rename PLOTTING_SYSTEM.md => .doc/PLOTTING_SYSTEM.md (100%) rename README.md => .doc/README.md (100%) rename REAL_TIME_STREAMING.md => .doc/REAL_TIME_STREAMING.md (100%) rename STREAMING_TROUBLESHOOTING.md => .doc/STREAMING_TROUBLESHOOTING.md (100%) create mode 100644 config/data/plc_config.json create mode 100644 config/data/plc_datasets.json create mode 100644 config/data/plot_sessions.json create mode 100644 config/schema/datasets.schema.json create mode 100644 config/schema/plc.schema.json create mode 100644 config/schema/plots.schema.json create mode 100644 config/schema/ui/datasets.uischema.json create mode 100644 config/schema/ui/plc.uischema.json create mode 100644 config/schema/ui/plots.uischema.json create mode 100644 frontend/public/favicon.ico create mode 100644 frontend/public/record.png create mode 100644 frontend/src/assets/logo/record.png create mode 100644 static/icons/favicon.ico diff --git a/CHARTJS_STREAMING_INTEGRATION.md b/.doc/CHARTJS_STREAMING_INTEGRATION.md similarity index 100% rename from CHARTJS_STREAMING_INTEGRATION.md rename to .doc/CHARTJS_STREAMING_INTEGRATION.md diff --git a/CONSOLE_CLEANUP_SUMMARY.md b/.doc/CONSOLE_CLEANUP_SUMMARY.md similarity index 100% rename from CONSOLE_CLEANUP_SUMMARY.md rename to .doc/CONSOLE_CLEANUP_SUMMARY.md diff --git a/EJEMPLO_USO_PLOTTING.md b/.doc/EJEMPLO_USO_PLOTTING.md similarity index 100% rename from EJEMPLO_USO_PLOTTING.md rename to .doc/EJEMPLO_USO_PLOTTING.md diff --git a/.doc/MemoriaDeEvolucion.md b/.doc/MemoriaDeEvolucion.md index 1993869..4ce6700 100644 --- a/.doc/MemoriaDeEvolucion.md +++ b/.doc/MemoriaDeEvolucion.md @@ -1,3 +1,19 @@ +2025-08-11 - Favicon y logos + +- Resumen petición usuario: Cambiar el logo por `record.png` y que también se vea en la pestaña del navegador (favicon). + +- Cambios clave: + - `frontend/index.html`: `` y `shortcut icon` para forzar recarga. + - `main.py`: Ruta `/favicon.ico` que sirve `static/icons/record.png` para cubrir la petición automática del navegador. + - `frontend/src/App.jsx`: Reemplazo de logos visuales por `/static/icons/record.png` en header y navbar. + +- Decisiones/Notas: + - Estándar adoptado para estáticos en React: + - `frontend/public/`: archivos públicos sin import (favicon, robots.txt, imágenes públicas). Servidos desde la raíz: `/favicon.ico`, `/record.png`. + - `frontend/src/assets/`: assets usados por componentes React, importados con `import img from '...';` para que Vite haga hashing y optimización. + - Para favicon no usar archivos bajo `frontend/src/…`. Debe estar en `frontend/public` y enlazarse como `/favicon.ico`. + - Se añadió `?v=2` para evitar caché del navegador. + # Project Evolution Memory ## PLC S7-315 Streamer & Logger diff --git a/PLOTTING_FINALIZATION_SUMMARY.md b/.doc/PLOTTING_FINALIZATION_SUMMARY.md similarity index 100% rename from PLOTTING_FINALIZATION_SUMMARY.md rename to .doc/PLOTTING_FINALIZATION_SUMMARY.md diff --git a/PLOTTING_FIXES_FINAL.md b/.doc/PLOTTING_FIXES_FINAL.md similarity index 100% rename from PLOTTING_FIXES_FINAL.md rename to .doc/PLOTTING_FIXES_FINAL.md diff --git a/PLOTTING_FIXES_RESUMEN.md b/.doc/PLOTTING_FIXES_RESUMEN.md similarity index 100% rename from PLOTTING_FIXES_RESUMEN.md rename to .doc/PLOTTING_FIXES_RESUMEN.md diff --git a/PLOTTING_STATUS_FINAL.md b/.doc/PLOTTING_STATUS_FINAL.md similarity index 100% rename from PLOTTING_STATUS_FINAL.md rename to .doc/PLOTTING_STATUS_FINAL.md diff --git a/PLOTTING_SYSTEM.md b/.doc/PLOTTING_SYSTEM.md similarity index 100% rename from PLOTTING_SYSTEM.md rename to .doc/PLOTTING_SYSTEM.md diff --git a/README.md b/.doc/README.md similarity index 100% rename from README.md rename to .doc/README.md diff --git a/REAL_TIME_STREAMING.md b/.doc/REAL_TIME_STREAMING.md similarity index 100% rename from REAL_TIME_STREAMING.md rename to .doc/REAL_TIME_STREAMING.md diff --git a/STREAMING_TROUBLESHOOTING.md b/.doc/STREAMING_TROUBLESHOOTING.md similarity index 100% rename from STREAMING_TROUBLESHOOTING.md rename to .doc/STREAMING_TROUBLESHOOTING.md diff --git a/application_events.json b/application_events.json index d5df825..515df5a 100644 --- a/application_events.json +++ b/application_events.json @@ -9238,8 +9238,22 @@ "slot": 2, "error": "b' ISO : An error occurred during recv TCP : Connection timed out'" } + }, + { + "timestamp": "2025-08-11T15:17:54.090498", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} + }, + { + "timestamp": "2025-08-11T16:21:42.173167", + "level": "info", + "event_type": "application_started", + "message": "Application initialization completed successfully", + "details": {} } ], - "last_updated": "2025-08-11T13:53:59.849319", - "total_entries": 859 + "last_updated": "2025-08-11T16:21:42.173167", + "total_entries": 861 } \ No newline at end of file diff --git a/config/data/plc_config.json b/config/data/plc_config.json new file mode 100644 index 0000000..d18666f --- /dev/null +++ b/config/data/plc_config.json @@ -0,0 +1,21 @@ +{ + "plc_config": { + "ip": "10.1.33.12", + "rack": 0, + "slot": 2 + }, + "udp_config": { + "host": "127.0.0.1", + "port": 9870 + }, + "sampling_interval": 0.1, + "csv_config": { + "records_directory": "records", + "rotation_enabled": true, + "max_size_mb": 1000, + "max_days": 30, + "max_hours": null, + "cleanup_interval_hours": 24, + "last_cleanup": "2025-08-09T22:43:54.224975" + } +} \ No newline at end of file diff --git a/config/data/plc_datasets.json b/config/data/plc_datasets.json new file mode 100644 index 0000000..a39f71e --- /dev/null +++ b/config/data/plc_datasets.json @@ -0,0 +1,69 @@ +{ + "datasets": { + "DAR": { + "name": "DAR", + "prefix": "gateway_phoenix", + "variables": { + "UR29_Brix": { + "area": "db", + "offset": 1322, + "type": "real", + "streaming": true, + "db": 1011 + }, + "UR29_ma": { + "area": "db", + "offset": 1296, + "type": "real", + "streaming": true, + "db": 1011 + }, + "fUR29_Brix": { + "area": "db", + "offset": 1322, + "type": "real", + "streaming": false, + "db": 1011 + } + }, + "streaming_variables": [ + "UR29_Brix", + "UR29_ma" + ], + "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": [ + "Fast", + "DAR" + ], + "current_dataset_id": "Fast", + "version": "1.0", + "last_update": "2025-08-10T01:45:12.551768" +} \ No newline at end of file diff --git a/config/data/plot_sessions.json b/config/data/plot_sessions.json new file mode 100644 index 0000000..6077cf6 --- /dev/null +++ b/config/data/plot_sessions.json @@ -0,0 +1,23 @@ +{ + "plots": { + "plot_1": { + "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, + "session_id": "plot_1" + } + }, + "session_counter": 2, + "last_saved": "2025-08-10T00:37:46.525175", + "version": "1.0" + } \ No newline at end of file diff --git a/config/schema/datasets.schema.json b/config/schema/datasets.schema.json new file mode 100644 index 0000000..1629d96 --- /dev/null +++ b/config/schema/datasets.schema.json @@ -0,0 +1,183 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "datasets.schema.json", + "title": "Datasets Configuration", + "description": "Esquema para editar plc_datasets.json (múltiples datasets y variables)", + "type": "object", + "additionalProperties": false, + "properties": { + "datasets": { + "type": "object", + "title": "Datasets", + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Dataset Name", + "description": "Nombre legible del dataset", + "minLength": 1, + "maxLength": 60 + }, + "prefix": { + "type": "string", + "title": "CSV Prefix", + "description": "Prefijo para archivos CSV", + "pattern": "^[a-zA-Z0-9_-]+$", + "minLength": 1, + "maxLength": 20 + }, + "variables": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "area": { + "type": "string", + "title": "Memory Area", + "enum": [ + "db", + "mw", + "m", + "pew", + "pe", + "paw", + "pa", + "e", + "a", + "mb" + ] + }, + "db": { + "type": [ + "integer", + "null" + ], + "title": "DB Number", + "minimum": 1, + "maximum": 9999 + }, + "offset": { + "type": "integer", + "title": "Offset", + "minimum": 0, + "maximum": 8191 + }, + "bit": { + "type": [ + "integer", + "null" + ], + "title": "Bit Position", + "minimum": 0, + "maximum": 7 + }, + "type": { + "type": "string", + "title": "Data Type", + "enum": [ + "real", + "int", + "bool", + "dint", + "word", + "byte", + "uint", + "udint", + "sint", + "usint" + ] + }, + "streaming": { + "type": "boolean", + "title": "Stream to PlotJuggler", + "default": false + } + }, + "required": [ + "area", + "offset", + "type" + ] + } + }, + "streaming_variables": { + "type": "array", + "title": "Streaming Variables", + "items": { + "type": "string" + }, + "default": [] + }, + "sampling_interval": { + "type": [ + "number", + "null" + ], + "title": "Sampling Interval (s)", + "description": "Vacío para usar el intervalo global", + "minimum": 0.01, + "maximum": 10 + }, + "enabled": { + "type": "boolean", + "title": "Dataset Enabled", + "default": false, + "enum": [ + true, + false + ], + "options": { + "enum_titles": [ + "Activate", + "Deactivate" + ] + } + }, + "created": { + "type": [ + "string", + "null" + ], + "title": "Created" + } + }, + "required": [ + "name", + "prefix", + "variables", + "streaming_variables" + ] + } + }, + "active_datasets": { + "type": "array", + "title": "Active Datasets", + "items": { + "type": "string" + }, + "default": [] + }, + "current_dataset_id": { + "type": [ + "string", + "null" + ], + "title": "Current Dataset Id" + }, + "version": { + "type": "string", + "title": "Version" + }, + "last_update": { + "type": [ + "string", + "null" + ], + "title": "Last Update" + } + }, + "required": [ + "datasets" + ] +} \ No newline at end of file diff --git a/config/schema/plc.schema.json b/config/schema/plc.schema.json new file mode 100644 index 0000000..ab4b575 --- /dev/null +++ b/config/schema/plc.schema.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "plc.schema.json", + "title": "PLC & UDP Configuration", + "description": "Esquema para editar plc_config.json", + "type": "object", + "additionalProperties": false, + "properties": { + "plc_config": { + "type": "object", + "title": "PLC Configuration", + "additionalProperties": false, + "properties": { + "ip": { + "type": "string", + "title": "PLC IP", + "description": "Dirección IP del PLC (S7-31x)", + "format": "ipv4", + "pattern": "^.+$" + }, + "rack": { + "type": "integer", + "title": "Rack", + "description": "Número de rack (0-7)", + "minimum": 0, + "maximum": 7, + "default": 0 + }, + "slot": { + "type": "integer", + "title": "Slot", + "description": "Número de slot (generalmente 2)", + "minimum": 0, + "maximum": 31, + "default": 2 + } + }, + "required": [ + "ip", + "rack", + "slot" + ] + }, + "udp_config": { + "type": "object", + "title": "UDP Configuration", + "additionalProperties": false, + "properties": { + "host": { + "type": "string", + "title": "UDP Host", + "pattern": "^.+$", + "default": "127.0.0.1" + }, + "port": { + "type": "integer", + "minimum": 1, + "maximum": 65535, + "default": 9870 + } + }, + "required": [ + "host", + "port" + ] + }, + "sampling_interval": { + "type": "number", + "minimum": 0.01, + "maximum": 10, + "title": "Sampling Interval (s)", + "description": "Intervalo global de muestreo en segundos", + "default": 0.1 + }, + "csv_config": { + "type": "object", + "title": "CSV Recording", + "additionalProperties": false, + "properties": { + "records_directory": { + "type": "string", + "title": "Records Directory", + "default": "records" + }, + "rotation_enabled": { + "type": "boolean", + "title": "Rotation", + "default": true, + "enum": [ + true, + false + ], + "options": { + "enum_titles": [ + "Activate", + "Deactivate" + ] + } + }, + "max_size_mb": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "title": "Max Size (MB)", + "default": 1000 + }, + "max_days": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "title": "Max Days", + "default": 30 + }, + "max_hours": { + "type": [ + "integer", + "null" + ], + "minimum": 1, + "title": "Max Hours", + "default": null + }, + "cleanup_interval_hours": { + "type": "integer", + "minimum": 1, + "title": "Cleanup Interval (h)", + "default": 24 + }, + "last_cleanup": { + "type": [ + "string", + "null" + ], + "title": "Last Cleanup" + } + }, + "required": [ + "records_directory", + "rotation_enabled", + "cleanup_interval_hours" + ] + } + }, + "required": [ + "plc_config", + "udp_config", + "sampling_interval", + "csv_config" + ] +} \ No newline at end of file diff --git a/config/schema/plots.schema.json b/config/schema/plots.schema.json new file mode 100644 index 0000000..35a8c57 --- /dev/null +++ b/config/schema/plots.schema.json @@ -0,0 +1,100 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "plots.schema.json", + "title": "Plot Sessions", + "description": "Esquema para editar plot_sessions.json (sesiones de gráfica)", + "type": "object", + "additionalProperties": false, + "properties": { + "plots": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "name": { + "type": "string", + "title": "Plot Name", + "description": "Nombre de la sesión de gráfica" + }, + "variables": { + "type": "array", + "title": "Variables", + "description": "Variables a graficar", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "time_window": { + "type": "integer", + "title": "Time Window (s)", + "description": "Ventana temporal en segundos", + "minimum": 5, + "maximum": 3600, + "default": 60 + }, + "y_min": { + "type": [ + "number", + "null" + ], + "title": "Y Min", + "description": "Vacío para auto" + }, + "y_max": { + "type": [ + "number", + "null" + ], + "title": "Y Max", + "description": "Vacío para auto" + }, + "trigger_variable": { + "type": [ + "string", + "null" + ], + "title": "Trigger Variable" + }, + "trigger_enabled": { + "type": "boolean", + "title": "Enable Trigger", + "default": false + }, + "trigger_on_true": { + "type": "boolean", + "title": "Trigger on True", + "default": true + }, + "session_id": { + "type": "string", + "title": "Session Id" + } + }, + "required": [ + "name", + "variables", + "time_window" + ] + } + }, + "session_counter": { + "type": "integer", + "minimum": 0, + "default": 0 + }, + "last_saved": { + "type": [ + "string", + "null" + ] + }, + "version": { + "type": "string", + "default": "1.0" + } + }, + "required": [ + "plots" + ] +} \ No newline at end of file diff --git a/config/schema/ui/datasets.uischema.json b/config/schema/ui/datasets.uischema.json new file mode 100644 index 0000000..b4fd928 --- /dev/null +++ b/config/schema/ui/datasets.uischema.json @@ -0,0 +1,42 @@ +{ + "datasets": { + "ui:description": "Define datasets, their variables and streaming flags.", + "items": { + "name": { + "ui:placeholder": "Temperature Sensors" + }, + "prefix": { + "ui:placeholder": "temp" + }, + "sampling_interval": { + "ui:widget": "UpDownWidget" + }, + "enabled": { + "ui:widget": "CheckboxWidget" + }, + "variables": { + "ui:description": "Variables inside this dataset", + "items": { + "area": { + "ui:widget": "SelectWidget" + }, + "db": { + "ui:widget": "UpDownWidget" + }, + "offset": { + "ui:widget": "UpDownWidget" + }, + "bit": { + "ui:widget": "UpDownWidget" + }, + "type": { + "ui:widget": "SelectWidget" + }, + "streaming": { + "ui:widget": "CheckboxWidget" + } + } + } + } + } +} \ No newline at end of file diff --git a/config/schema/ui/plc.uischema.json b/config/schema/ui/plc.uischema.json new file mode 100644 index 0000000..5da2348 --- /dev/null +++ b/config/schema/ui/plc.uischema.json @@ -0,0 +1,44 @@ +{ + "plc_config": { + "ip": { + "ui:placeholder": "192.168.1.100" + }, + "rack": { + "ui:widget": "UpDownWidget" + }, + "slot": { + "ui:widget": "UpDownWidget" + } + }, + "udp_config": { + "host": { + "ui:placeholder": "127.0.0.1" + }, + "port": { + "ui:widget": "UpDownWidget" + } + }, + "sampling_interval": { + "ui:widget": "UpDownWidget" + }, + "csv_config": { + "records_directory": { + "ui:placeholder": "records" + }, + "rotation_enabled": { + "ui:widget": "CheckboxWidget" + }, + "max_size_mb": { + "ui:widget": "UpDownWidget" + }, + "max_days": { + "ui:widget": "UpDownWidget" + }, + "max_hours": { + "ui:widget": "UpDownWidget" + }, + "cleanup_interval_hours": { + "ui:widget": "UpDownWidget" + } + } +} \ No newline at end of file diff --git a/config/schema/ui/plots.uischema.json b/config/schema/ui/plots.uischema.json new file mode 100644 index 0000000..5b9765c --- /dev/null +++ b/config/schema/ui/plots.uischema.json @@ -0,0 +1,21 @@ +{ + "plots": { + "items": { + "time_window": { + "ui:widget": "UpDownWidget" + }, + "y_min": { + "ui:widget": "UpDownWidget" + }, + "y_max": { + "ui:widget": "UpDownWidget" + }, + "trigger_enabled": { + "ui:widget": "CheckboxWidget" + }, + "trigger_on_true": { + "ui:widget": "CheckboxWidget" + } + } + } +} \ No newline at end of file diff --git a/core/config_manager.py b/core/config_manager.py index fa135c9..6f3d079 100644 --- a/core/config_manager.py +++ b/core/config_manager.py @@ -24,9 +24,11 @@ class ConfigManager: """Initialize configuration manager""" self.logger = logger - # Configuration file paths - self.config_file = resource_path("plc_config.json") - self.datasets_file = resource_path("plc_datasets.json") + # Configuration file paths (reorganized under config/data) + data_dir = os.path.join("config", "data") + os.makedirs(resource_path(data_dir), exist_ok=True) + self.config_file = resource_path(os.path.join(data_dir, "plc_config.json")) + self.datasets_file = resource_path(os.path.join(data_dir, "plc_datasets.json")) self.state_file = resource_path("system_state.json") # Default configurations diff --git a/core/plot_manager.py b/core/plot_manager.py index deb1c39..0580487 100644 --- a/core/plot_manager.py +++ b/core/plot_manager.py @@ -227,8 +227,10 @@ class PlotManager: self.event_logger = event_logger self.logger = logger - # Persistent storage - self.plots_file = resource_path("plot_sessions.json") + # Persistent storage (reorganized under config/data) + self.plots_file = resource_path( + os.path.join("config", "data", "plot_sessions.json") + ) # Load existing plots from disk self.load_plots() diff --git a/core/schema_manager.py b/core/schema_manager.py index 6806a34..cf0f99f 100644 --- a/core/schema_manager.py +++ b/core/schema_manager.py @@ -36,14 +36,18 @@ class ConfigSchemaManager: self.plot_manager = plot_manager self.logger = logger - self.schemas_dir = resource_path("schemas") + # Reorganized schema directories + self.schemas_dir = resource_path(os.path.join("config", "schema")) + self.ui_schemas_dir = resource_path(os.path.join("config", "schema", "ui")) self.schemas_index: Dict[str, Dict[str, Any]] = {} # Mapa de id -> ruta de archivo real de configuración + data_dir = resource_path(os.path.join("config", "data")) + os.makedirs(data_dir, exist_ok=True) self.config_files: Dict[str, str] = { - "plc": resource_path("plc_config.json"), - "datasets": resource_path("plc_datasets.json"), - "plots": resource_path("plot_sessions.json"), + "plc": os.path.join(data_dir, "plc_config.json"), + "datasets": os.path.join(data_dir, "plc_datasets.json"), + "plots": os.path.join(data_dir, "plot_sessions.json"), } self._load_all_schemas() @@ -83,6 +87,18 @@ class ConfigSchemaManager: raise ValueError(f"Schema '{schema_id}' not found") return self.schemas_index[schema_id] + def get_ui_schema(self, schema_id: str) -> Optional[Dict[str, Any]]: + """Load optional RJSF UI schema if present.""" + try: + path = os.path.join(self.ui_schemas_dir, f"{schema_id}.uischema.json") + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + if self.logger: + self.logger.warning(f"Error loading UI schema for '{schema_id}': {e}") + return None + def read_config(self, config_id: str) -> Dict[str, Any]: if config_id == "plc": # Construir desde ConfigManager para asegurar consistencia diff --git a/frontend/index.html b/frontend/index.html index a686afe..59d7206 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,14 +1,17 @@ - - - - PLC S7-31x Streamer & Logger - React - - - -
- - - + + + + PLC S7-31x Streamer & Logger - React + + + + + +
+ + + + \ No newline at end of file diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..3f5ce654de5fb8925ca85753e7d5170e22fd0089 GIT binary patch literal 15983 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

- SIDEL + REC PLC S7-31x Streamer & Logger (React)

React base ready. We will migrate views incrementally.

@@ -39,7 +40,7 @@ function NavBar() {