Actualización de application_events.json con nuevos eventos para la gestión de datasets y sesiones de plot. Se mejoró el manejo de errores en la conexión al PLC en main.py, proporcionando detalles adicionales en caso de fallo. Se ajustaron las fechas de última actualización en varios archivos de configuración, incluyendo plc_config.json y plc_datasets.json. Se realizaron mejoras en la interfaz de usuario para reflejar el estado de conexión y se optimizó el código en plotting.js para una mejor gestión de datos en tiempo real.

This commit is contained in:
Miguel 2025-08-08 18:30:27 +02:00
parent df07451079
commit cf5a169cce
12 changed files with 1301 additions and 108 deletions

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@ class PLCClient:
self.logger = logger
self.plc = None
self.connected = False
self.last_error = None
# Connection configuration for reconnection
self.connection_config = {"ip": None, "rack": None, "slot": None}
@ -43,6 +44,7 @@ class PLCClient:
self.plc = snap7.client.Client()
self.plc.connect(ip, rack, slot)
self.connected = True
self.last_error = None
# Store connection configuration for reconnection
self.connection_config = {"ip": ip, "rack": rack, "slot": slot}
@ -58,6 +60,7 @@ class PLCClient:
except Exception as e:
self.connected = False
self.last_error = str(e)
if self.logger:
self.logger.error(
f"Failed to connect to PLC {ip}:{rack}/{slot}: {str(e)}"

View File

@ -152,7 +152,10 @@ class PLCDataStreamer:
"error",
"plc_connection_failed",
f"Failed to connect to PLC {self.config_manager.plc_config['ip']}",
self.config_manager.plc_config,
{
**self.config_manager.plc_config,
"error": getattr(self.plc_client, "last_error", None),
},
)
return success

18
main.py
View File

@ -298,7 +298,23 @@ def connect_plc():
if streamer.connect_plc():
return jsonify({"success": True, "message": "Connected to PLC"})
else:
return jsonify({"success": False, "message": "Error connecting to PLC"}), 500
# Extraer detalle de error si disponible
last_error = None
try:
last_error = streamer.plc_client.last_error
except Exception:
last_error = None
return (
jsonify(
{
"success": False,
"message": "Error connecting to PLC",
"error": last_error,
"plc_config": streamer.config_manager.plc_config,
}
),
500,
)
@app.route("/api/plc/disconnect", methods=["POST"])

View File

@ -16,6 +16,6 @@
"max_days": 30,
"max_hours": null,
"cleanup_interval_hours": 24,
"last_cleanup": "2025-08-04T15:29:50.501222"
"last_cleanup": "2025-08-08T15:50:27.922821"
}
}

View File

@ -1,74 +1,37 @@
{
"datasets": {
"dar": {
"DAR": {
"name": "DAR",
"prefix": "dar",
"prefix": "gateway_phoenix",
"variables": {
"UR29_Brix_Digital": {
"UR29_Brix": {
"area": "db",
"offset": 40,
"type": "real",
"streaming": true,
"db": 2120
"db": 2123
},
"UR62_PEW": {
"area": "pew",
"offset": 300,
"type": "word",
"streaming": false
},
"UR29_PEW": {
"area": "pew",
"offset": 304,
"type": "word",
"streaming": false
},
"UR62_Brix": {
"UR29_ma": {
"area": "db",
"offset": 1296,
"offset": 36,
"type": "real",
"streaming": true,
"db": 1011
},
"UR29_Brix": {
"area": "db",
"offset": 1322,
"type": "real",
"streaming": true,
"db": 1011
},
"CTS306_PV": {
"area": "db",
"offset": 1328,
"type": "real",
"streaming": true,
"db": 1011
"db": 2123
}
},
"streaming_variables": [
"UR29_Brix_Digital",
"UR62_Brix",
"CTS306_PV",
"UR29_Brix"
"UR29_Brix",
"UR29_ma"
],
"sampling_interval": 0.2,
"enabled": true,
"created": "2025-07-17T16:47:56.698652"
},
"mixer": {
"name": "mixer",
"prefix": "mixer",
"variables": {},
"streaming_variables": [],
"sampling_interval": 1.0,
"enabled": false,
"created": "2025-07-17T17:43:27.745292"
"enabled": true,
"created": "2025-08-08T15:47:18.566053"
}
},
"active_datasets": [
"dar"
"DAR"
],
"current_dataset_id": "dar",
"current_dataset_id": "DAR",
"version": "1.0",
"last_update": "2025-08-04T18:22:22.744860"
"last_update": "2025-08-08T17:13:42.321147"
}

View File

@ -1,21 +1,21 @@
{
"plots": {
"plot_18": {
"name": "Brix",
"plot_20": {
"name": "UR29",
"variables": [
"UR29_Brix",
"UR62_Brix"
"UR29_ma"
],
"time_window": 60,
"time_window": 10,
"y_min": null,
"y_max": null,
"trigger_variable": null,
"trigger_enabled": false,
"trigger_on_true": true,
"session_id": "plot_18"
"session_id": "plot_20"
}
},
"session_counter": 19,
"last_saved": "2025-08-04T00:49:55.221304",
"session_counter": 21,
"last_saved": "2025-08-08T16:24:02.832199",
"version": "1.0"
}

View File

@ -97,18 +97,32 @@
hasOnRefresh: typeof me.realtime.onRefresh === 'function'
});
// Configurar intervalo de actualización
if (!streaming.intervalId && me.realtime.refresh > 0) {
// Configurar intervalo de obtención de datos (refresh)
if (me.realtime.refresh > 0) {
if (streaming.intervalId) {
clearInterval(streaming.intervalId);
}
streaming.intervalId = setInterval(() => {
if (!me.realtime.pause && typeof me.realtime.onRefresh === 'function') {
me.realtime.onRefresh(chart);
}
}, me.realtime.refresh);
console.log('📈 RealTimeScale data interval started:', me.realtime.refresh + 'ms');
}
// Configurar intervalo de render (frameRate)
const fps = Math.max(1, me.realtime.frameRate || 30);
const frameIntervalMs = Math.round(1000 / fps);
if (streaming.frameIntervalId) {
clearInterval(streaming.frameIntervalId);
}
streaming.frameIntervalId = setInterval(() => {
if (!me.realtime.pause) {
me.updateRealTimeData();
chart.update('quiet');
}, me.realtime.refresh);
console.log('📈 RealTimeScale interval started:', me.realtime.refresh + 'ms');
}
}
}, frameIntervalMs);
console.log('🎞️ RealTimeScale render interval started:', frameIntervalMs + 'ms (' + fps + ' fps)');
}
updateRealTimeData() {
@ -151,10 +165,17 @@
const chart = me.chart;
const streaming = chart.$streaming;
if (streaming && streaming.intervalId) {
clearInterval(streaming.intervalId);
delete streaming.intervalId;
console.log('📈 RealTimeScale interval cleared');
if (streaming) {
if (streaming.intervalId) {
clearInterval(streaming.intervalId);
delete streaming.intervalId;
console.log('📈 RealTimeScale data interval cleared');
}
if (streaming.frameIntervalId) {
clearInterval(streaming.frameIntervalId);
delete streaming.frameIntervalId;
console.log('🎞️ RealTimeScale render interval cleared');
}
}
super.destroy();

View File

@ -194,6 +194,26 @@ class PlotManager {
chartConfig = this.createFallbackChartConfig(sessionId, config);
}
// Pre-poblar datasets en config antes de crear el Chart para evitar metas undefined
if (Array.isArray(config.variables) && config.variables.length > 0) {
const datasets = [];
config.variables.forEach((variable, index) => {
const color = this.getColor(variable, index);
datasets.push({
label: variable,
data: [],
borderColor: color,
backgroundColor: color + '20',
borderWidth: 2,
fill: false,
pointRadius: 0,
pointHoverRadius: 3,
tension: 0.1
});
});
chartConfig.data.datasets = datasets;
}
// Crear chart
const chart = new Chart(ctx, chartConfig);
@ -203,7 +223,8 @@ class PlotManager {
config: config,
lastDataFetch: 0,
datasetIndex: new Map(), // Mapeo de variable -> índice de dataset
isRealTimeMode: hasRealTimeScale
isRealTimeMode: hasRealTimeScale,
lastPushedXByDataset: new Map() // índice de dataset -> último timestamp x añadido
});
// Inicializar refresh rate por defecto
@ -212,8 +233,12 @@ class PlotManager {
console.log(`⏱️ Plot ${sessionId}: Default refresh rate set to 1000ms`);
}
// Inicializar datasets para las variables
this.initializeChartDatasets(sessionId, config);
// Inicializar índice de datasets (sin mutar chart aún)
if (Array.isArray(config.variables)) {
config.variables.forEach((variable, index) => {
this.sessions.get(sessionId).datasetIndex.set(variable, index);
});
}
// Si no es modo realtime, iniciar intervalo manual
if (!hasRealTimeScale) {
@ -243,7 +268,7 @@ class PlotManager {
realtime: {
duration: (config.time_window || 60) * 1000,
refresh: this.refreshRates.get(sessionId) || 1000, // Actualizar según configuración dinámica
delay: 0,
delay: ((this.refreshRates.get(sessionId) || 1000) * 2), // Margen temporal para evitar saltos
frameRate: 30,
pause: !config.is_active, // Pausar si no está activo
onRefresh: (chart) => {
@ -443,14 +468,10 @@ class PlotManager {
const timeSinceLastUpdate = now - sessionData.lastDataFetch;
sessionData.lastDataFetch = now;
console.log(`🔄 Plot ${sessionId}: Updating data (${timeSinceLastUpdate}ms since last update, threshold: ${minInterval}ms)`);
// Obtener datos del backend (que usa solo cache)
const response = await fetch(`/api/plots/${sessionId}/data`);
if (!response.ok) {
console.error(`📈 Plot ${sessionId}: API response not ok:`, response.status);
return;
}
if (!response.ok) return;
const plotData = await response.json();
@ -497,12 +518,12 @@ class PlotManager {
}
// Agregar el punto con timestamp correcto
const newPoint = {
x: latestPoint.x || timestamp,
y: latestPoint.y
};
const lastPushedX = sessionData.lastPushedXByDataset.get(datasetIndex) || 0;
const candidateX = latestPoint.x || timestamp;
const finalX = candidateX > lastPushedX ? candidateX : lastPushedX + 1; // garantizar x strictly increasing
chart.data.datasets[datasetIndex].data.push(newPoint);
chart.data.datasets[datasetIndex].data.push({ x: finalX, y: latestPoint.y });
sessionData.lastPushedXByDataset.set(datasetIndex, finalX);
pointsAdded++;
});
@ -1539,23 +1560,30 @@ class PlotManager {
// Actualizar la configuración de refresh rate
chart.scales.x.realtime.refresh = finalRefreshRateMs;
// Forzar reinicio del intervalo interno del plugin
// Forzar reinicio del intervalo de obtención de datos (onRefresh)
const streaming = chart.$streaming;
if (streaming && streaming.intervalId) {
clearInterval(streaming.intervalId);
// Recrear el intervalo con el nuevo refresh rate
if (streaming) {
if (streaming.intervalId) {
clearInterval(streaming.intervalId);
}
// Recrear el intervalo SOLO para onRefresh; el render lo gestiona el plugin oficial
streaming.intervalId = setInterval(() => {
if (!chart.scales.x.realtime.pause && typeof chart.scales.x.realtime.onRefresh === 'function') {
chart.scales.x.realtime.onRefresh(chart);
}
if (chart.scales.x.updateRealTimeData) {
chart.scales.x.updateRealTimeData();
}
chart.update('quiet');
}, finalRefreshRateMs);
console.log(`🔄 Streaming interval restarted with ${finalRefreshRateMs}ms`);
console.log(`🔄 Data refresh interval restarted with ${finalRefreshRateMs}ms`);
}
// Ajustar dinámicamente el delay a 2x refresh para evitar saltos visuales
if (chart.scales && chart.scales.x && chart.scales.x.realtime) {
const newDelay = Math.max(finalRefreshRateMs * 2, 0);
chart.scales.x.realtime.delay = newDelay;
if (chart.options && chart.options.scales && chart.options.scales.x && chart.options.scales.x.realtime) {
chart.options.scales.x.realtime.delay = newDelay;
}
console.log(`⏳ Realtime delay adjusted to ${newDelay}ms`);
}
// También actualizar la configuración de opciones para futuros reinicios

View File

@ -76,8 +76,12 @@ function updateStatus() {
fetch('/api/plc/connect', { method: 'POST' })
.then(response => response.json())
.then(data => {
showMessage(data.message, data.success ? 'success' : 'error');
const msg = data.error ? `${data.message}: ${data.error}` : data.message;
showMessage(msg, data.success ? 'success' : 'error');
updateStatus();
})
.catch(err => {
showMessage(`Error connecting to PLC: ${err}`, 'error');
});
});
}
@ -228,8 +232,12 @@ function updateStatusFromStream(status) {
fetch('/api/plc/connect', { method: 'POST' })
.then(response => response.json())
.then(data => {
showMessage(data.message, data.success ? 'success' : 'error');
const msg = data.error ? `${data.message}: ${data.error}` : data.message;
showMessage(msg, data.success ? 'success' : 'error');
updateStatus();
})
.catch(err => {
showMessage(`Error connecting to PLC: ${err}`, 'error');
});
});
}

View File

@ -1,11 +1,11 @@
{
"last_state": {
"should_connect": true,
"should_stream": false,
"should_stream": true,
"active_datasets": [
"dar"
"DAR"
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-04T18:22:22.763087"
"last_update": "2025-08-08T17:13:44.673911"
}

View File

@ -754,12 +754,11 @@
</article>
</div>
<!-- Chart.js Libraries - Cargar en orden estricto -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns"></script>
<!-- Chart.js Streaming Plugin - Después de Chart.js -->
<script src="/static/js/chartjs-streaming/chartjs-plugin-streaming.js"></script>
<!-- Chart.js Libraries - Cargar en orden estricto (versiones compatibles) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@2"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@2.0.0"></script>
<!-- JavaScript Modules -->
<script src="/static/js/utils.js"></script>