Actualización de application_events.json para incluir nuevos eventos de inicio de aplicación y ajustes en las fechas de última actualización. Se eliminaron archivos obsoletos relacionados con la integración de Chart.js y se reorganizaron las rutas de configuración en el código. Se implementaron mejoras en la gestión de esquemas y se optimizó la carga de recursos estáticos, incluyendo la favicon y logos en la interfaz. Además, se realizaron ajustes en el manejo de errores y se mejoró la estructura de directorios para una mejor organización del proyecto.

This commit is contained in:
Miguel 2025-08-11 16:26:20 +02:00
parent 593487e52f
commit 0c11ee3ae2
35 changed files with 809 additions and 28 deletions

View File

@ -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`: `<link rel="icon" type="image/png" href="/static/icons/record.png?v=2" />` 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

View File

@ -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
}

View File

@ -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"
}
}

View File

@ -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"
}

View File

@ -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"
}

View File

@ -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"
]
}

View File

@ -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"
]
}

View File

@ -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"
]
}

View File

@ -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"
}
}
}
}
}
}

View File

@ -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"
}
}
}

View File

@ -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"
}
}
}
}

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -1,14 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PLC S7-31x Streamer & Logger - React</title>
<link rel="icon" href="/images/SIDEL.png" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PLC S7-31x Streamer & Logger - React</title>
<link rel="icon" type="image/png" href="/favicon.ico?v=5" />
<link rel="shortcut icon" type="image/png" href="/favicon.ico?v=5" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
frontend/public/record.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -1,4 +1,5 @@
import React from 'react'
import recLogo from './assets/logo/record.png'
import { Routes, Route, Link } from 'react-router-dom'
import StatusPage from './pages/Status.jsx'
import EventsPage from './pages/Events.jsx'
@ -12,7 +13,7 @@ function Home() {
<div className="container py-3">
<header className="mb-3">
<h1 className="h3 d-flex align-items-center gap-2">
<img src="/images/SIDEL.png" alt="SIDEL" style={{ height: 28 }} />
<img src={recLogo} alt="REC" style={{ height: 28 }} />
PLC S7-31x Streamer & Logger (React)
</h1>
<p className="text-muted mb-0">React base ready. We will migrate views incrementally.</p>
@ -39,7 +40,7 @@ function NavBar() {
<nav className="navbar navbar-expand-lg bg-light border-bottom">
<div className="container">
<Link to="/" className="navbar-brand d-flex align-items-center gap-2">
<img src="/images/SIDEL.png" alt="SIDEL" style={{ height: 24 }} />
<img src={recLogo} alt="REC" style={{ height: 24 }} />
<span className="fw-semibold">PLC Streamer</span>
</Link>
<div className="d-flex gap-2">

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -44,6 +44,9 @@ export default function ConfigPage() {
const [message, setMessage] = useState('')
const available = useMemo(() => {
if (!schemas) return []
if (Array.isArray(schemas.schemas)) return schemas.schemas.map(s => s.id)
if (schemas.schemas && typeof schemas.schemas === 'object') return Object.keys(schemas.schemas)
const ids = []
if (schemas?.plc) ids.push('plc')
if (schemas?.datasets) ids.push('datasets')
@ -60,7 +63,7 @@ export default function ConfigPage() {
readConfig(id),
])
setSchema(schemaResp.schema)
setUiSchema(buildUiSchema(schemaResp.schema))
setUiSchema(schemaResp.ui_schema || buildUiSchema(schemaResp.schema))
setFormData(dataResp.data)
} catch (e) {
setMessage(e.message || 'Error loading schema/config')

View File

@ -108,7 +108,7 @@ export default function DashboardPage() {
const available = useMemo(() => {
if (!schemas) return []
// Accept multiple shapes from API
if (Array.isArray(schemas.schemas)) return schemas.schemas
if (Array.isArray(schemas.schemas)) return schemas.schemas.map(s => s.id || s)
if (schemas.schemas && typeof schemas.schemas === 'object') return Object.keys(schemas.schemas)
const ids = []
if (schemas?.plc) ids.push('plc')
@ -170,7 +170,7 @@ export default function DashboardPage() {
readConfig(id),
])
setSchema(schemaResp.schema)
setUiSchema(buildUiSchema(schemaResp.schema))
setUiSchema(schemaResp.ui_schema || buildUiSchema(schemaResp.schema))
setFormData(dataResp.data)
} catch (e) {
setMessage(e.message || 'Error loading config')

View File

@ -15,6 +15,14 @@ export default defineConfig({
'/images': {
target: 'http://localhost:5050',
changeOrigin: true,
},
'/static': {
target: 'http://localhost:5050',
changeOrigin: true,
},
'/favicon.ico': {
target: 'http://localhost:5050',
changeOrigin: true,
}
}
},

61
main.py
View File

@ -44,6 +44,12 @@ def resource_path(relative_path):
return os.path.join(base_path, relative_path)
def project_path(*parts: str) -> str:
"""Build absolute path from the project root (based on this file)."""
base_dir = os.path.abspath(os.path.dirname(__file__))
return os.path.join(base_dir, *parts)
# Global streamer instance (will be initialized in main)
streamer = None
@ -67,6 +73,50 @@ def serve_static(filename):
return send_from_directory("static", filename)
@app.route("/favicon.ico")
def serve_favicon():
"""Serve application favicon from robust locations.
Priority:
1) frontend/public/favicon.ico
2) frontend/public/record.png
3) static/icons/record.png
"""
# Use absolute paths for reliability (works in dev and bundled)
public_dir = project_path("frontend", "public")
public_favicon = os.path.join(public_dir, "favicon.ico")
public_record = os.path.join(public_dir, "record.png")
if os.path.exists(public_favicon):
return send_from_directory(public_dir, "favicon.ico")
if os.path.exists(public_record):
return send_from_directory(public_dir, "record.png")
# Fallback: static/icons
static_icons_dir = project_path("static", "icons")
if os.path.exists(os.path.join(static_icons_dir, "record.png")):
return send_from_directory(static_icons_dir, "record.png")
# Final fallback: 404
return Response("Favicon not found", status=404, mimetype="text/plain")
@app.route("/record.png")
def serve_public_record_png():
"""Serve /record.png from the React public folder with fallbacks."""
public_dir = project_path("frontend", "public")
public_record = os.path.join(public_dir, "record.png")
if os.path.exists(public_record):
return send_from_directory(public_dir, "record.png")
static_icons_dir = project_path("static", "icons")
if os.path.exists(os.path.join(static_icons_dir, "record.png")):
return send_from_directory(static_icons_dir, "record.png")
return Response("record.png not found", status=404, mimetype="text/plain")
# ==============================
# Frontend (React SPA)
# ==============================
@ -125,7 +175,16 @@ def get_config_schema(schema_id):
try:
schema = streamer.schema_manager.get_schema(schema_id)
return jsonify({"success": True, "schema": schema})
ui_schema = None
# Try load optional UI schema
try:
ui_schema = streamer.schema_manager.get_ui_schema(schema_id)
except Exception:
ui_schema = None
resp = {"success": True, "schema": schema}
if ui_schema is not None:
resp["ui_schema"] = ui_schema
return jsonify(resp)
except ValueError as e:
return jsonify({"success": False, "error": str(e)}), 404
except Exception as e:

BIN
static/icons/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB