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:
parent
df07451079
commit
cf5a169cce
File diff suppressed because it is too large
Load Diff
|
@ -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)}"
|
||||
|
|
|
@ -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
18
main.py
|
@ -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"])
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue