Actualización de application_events.json con nuevos eventos para la gestión de sesiones de plot y el sistema de streaming. Se ajustaron las fechas de última actualización en plc_config.json, plc_datasets.json y system_state.json. Se implementó un control dinámico de la tasa de refresco en la interfaz de usuario, permitiendo a los usuarios establecer valores personalizados para la actualización de gráficos en tiempo real. Se realizaron mejoras en el código de plotting.js y tabs.js para soportar esta nueva funcionalidad.

This commit is contained in:
Miguel 2025-08-04 17:23:25 +02:00
parent bcef19e3f0
commit d1ca6f6ed6
8 changed files with 552 additions and 12 deletions

View File

@ -716,4 +716,107 @@ This represents a fundamental architectural improvement that transforms the appl
- **Cache Integration**: `DataStreamer.get_cached_dataset_values()` provides cached data access
- **Source Tracking**: Response includes source information (`cache` vs `plc_direct`)
- **Error Preservation**: Cached errors from streaming process are preserved and displayed
- **Automatic Cleanup**: Cache is cleared when streaming stops or datasets are deactivated
- **Automatic Cleanup**: Cache is cleared when streaming stops or datasets are deactivated
---
## 📊 Dynamic Refresh Rate Control for Real-Time Charts
### User Request Summary:
Usuario quería poder seleccionar el tiempo de refresco de las gráficas en tiempo real en vez de que sea un valor fijo, sin impacto en el PLC ya que siempre se leen valores del cache.
### Implementation Details:
**UI Changes**:
- **Refresh Rate Input**: Changed from dropdown to editable number input field for custom millisecond values
- **Flexible Input**: Users can enter any value between 100ms and 60,000ms with automatic validation
- **Dual Location Support**: Input field appears both in main tab and sub-tabs for consistency
- **Visual Integration**: Uses clock emoji (⏱️) as label, "ms" unit indicator, and compact styling matching existing controls
- **User Experience**: Enter key support for immediate application, auto-clamping to valid ranges
**Technical Implementation**:
- **Dynamic Configuration**: Modified `createStreamingChartConfig()` to use dynamic refresh rate instead of hardcoded 1000ms
- **Session Tracking**: Added `refreshRates` Map to track individual session refresh rates
- **Real-time Updates**: `updateRefreshRate()` function dynamically changes chart refresh without recreation
- **Interval Management**: Properly handles both ChartJS streaming intervals and manual fallback intervals
- **Synchronization**: Both selectors (main/tab) stay synchronized when changed
- **Memory Management**: Refresh rates are properly cleaned up when sessions are removed
**Key Features**:
- **No PLC Impact**: Only affects visualization refresh rate, not data collection from PLC cache
- **Instant Changes**: Refresh rate changes take effect immediately without chart recreation
- **Persistence**: Each plot session maintains its own independent refresh rate
- **Fallback Support**: Works with both streaming mode and manual refresh fallback
- **User Experience**: Intuitive controls integrated seamlessly with existing UI
**Code Changes**:
- **plotting.js**: Added refreshRates Map, updateRefreshRate() function, dynamic config
- **tabs.js**: Added refresh rate selector to sub-tab controls
- **styles.css**: Added styling for refresh-rate-control and refresh-rate-selector
- **Memory cleanup**: Ensured refresh rates are deleted when sessions are removed
**Benefits**:
- **Flexible Visualization**: Users can optimize refresh rate based on their monitoring needs
- **Performance Control**: Slower refresh rates reduce CPU usage for long-term monitoring
- **Independent Operation**: Each chart can have different refresh rates as needed
- **Cache-Based**: No additional load on PLC communication system
---
### 📝 Update: Changed to Editable Input Field
**User Request**: Cambiar el combo box por un campo editable con el tiempo en ms
**Changes Made**:
- **Replaced dropdown**: `<select>` changed to `<input type="number">` with millisecond values
- **Direct value entry**: Users can now enter exact millisecond values instead of preset options
- **Enhanced validation**: Auto-clamping to valid range (100-60,000ms) with console warnings
- **Improved UX**: Enter key support for immediate application, "ms" unit label for clarity
- **Consistent styling**: Updated CSS classes from `.refresh-rate-selector` to `.refresh-rate-input`
**Technical Benefits**:
- **Precision Control**: Users can set exact refresh rates like 1500ms, 750ms, etc.
- **Wide Range**: Supports from 100ms (high-frequency) to 60s (long-term monitoring)
- **Input Validation**: Automatic range enforcement prevents invalid values
- **Real-time Feedback**: Immediate visual and console feedback for out-of-range values
---
### 🐛 Bug Fix: Refresh Rate Not Working
**Issue**: El refresh rate no se aplicaba correctamente debido a throttles y intervalos hardcodeados.
**Problems Found**:
1. **Fixed Throttle**: `onStreamingRefresh` tenía un límite fijo de 800ms que impedía refresh rates más rápidos
2. **Fixed Manual Interval**: Modo fallback usaba 400ms fijo en lugar del refresh rate configurado
3. **Plugin Integration**: El modo streaming no reiniciaba correctamente los intervalos internos
**Corrections Made**:
- **Dynamic Throttle**: Cambiado a usar 80% del refresh rate configurado (mínimo 100ms)
- **Dynamic Manual Refresh**: `startManualRefresh` ahora usa el refresh rate configurado por sesión
- **Improved Streaming**: Mejor reinicio de intervalos para el plugin chartjs-streaming
- **Enhanced Debugging**: Logs detallados para diagnosticar problemas de refresh rate
**Technical Implementation**:
```javascript
// Throttle dinámico basado en refresh rate
const minInterval = Math.max(refreshRate * 0.8, 100);
// Intervalo manual con refresh rate dinámico
sessionData.manualInterval = setInterval(() => {
this.onStreamingRefresh(sessionId, sessionData.chart);
}, refreshRate);
// Reinicio mejorado del plugin streaming
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);
```
**Result**: El refresh rate ahora funciona correctamente tanto en modo streaming como fallback, con logs detallados para monitoreo.

View File

@ -5069,8 +5069,230 @@
"udp_port": 9870,
"datasets_available": 1
}
},
{
"timestamp": "2025-08-04T15:29:49.877657",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-04T15:29:49.924676",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 6,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-08-04T15:29:49.930199",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 2
}
},
{
"timestamp": "2025-08-04T15:29:49.937198",
"level": "info",
"event_type": "udp_streaming_started",
"message": "UDP streaming to PlotJuggler started",
"details": {
"udp_host": "127.0.0.1",
"udp_port": 9870,
"datasets_available": 1
}
},
{
"timestamp": "2025-08-04T15:40:22.692868",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-04T15:51:39.771093",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 6,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-08-04T15:51:39.784757",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 2
}
},
{
"timestamp": "2025-08-04T15:51:39.799407",
"level": "info",
"event_type": "plc_connection",
"message": "Successfully connected to PLC 10.1.33.11 and auto-started CSV recording for 1 datasets",
"details": {
"ip": "10.1.33.11",
"rack": 0,
"slot": 2,
"auto_started_recording": true,
"recording_datasets": 1,
"dataset_names": [
"DAR"
]
}
},
{
"timestamp": "2025-08-04T17:09:13.079757",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-04T17:09:13.108754",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 6,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-08-04T17:09:13.114169",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 2
}
},
{
"timestamp": "2025-08-04T17:11:46.936399",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-04T17:11:46.966671",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 6,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-08-04T17:11:46.972612",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 2
}
},
{
"timestamp": "2025-08-04T17:15:26.951106",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-04T17:15:26.981971",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 6,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-08-04T17:15:26.989697",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 2
}
},
{
"timestamp": "2025-08-04T17:16:46.742716",
"level": "info",
"event_type": "dataset_deactivated",
"message": "Dataset deactivated: DAR",
"details": {
"dataset_id": "dar"
}
},
{
"timestamp": "2025-08-04T17:16:49.813456",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 6,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-08-04T17:21:53.978435",
"level": "info",
"event_type": "application_started",
"message": "Application initialization completed successfully",
"details": {}
},
{
"timestamp": "2025-08-04T17:21:54.010004",
"level": "info",
"event_type": "dataset_activated",
"message": "Dataset activated: DAR",
"details": {
"dataset_id": "dar",
"variables_count": 6,
"streaming_count": 4,
"prefix": "dar"
}
},
{
"timestamp": "2025-08-04T17:21:54.015891",
"level": "info",
"event_type": "csv_recording_started",
"message": "CSV recording started: 1 datasets activated",
"details": {
"activated_datasets": 1,
"total_datasets": 2
}
}
],
"last_updated": "2025-08-04T01:01:57.946011",
"total_entries": 484
"last_updated": "2025-08-04T17:21:54.015891",
"total_entries": 506
}

View File

@ -16,6 +16,6 @@
"max_days": 30,
"max_hours": null,
"cleanup_interval_hours": 24,
"last_cleanup": "2025-08-03T10:18:30.271685"
"last_cleanup": "2025-08-04T15:29:50.501222"
}
}

View File

@ -70,5 +70,5 @@
],
"current_dataset_id": "dar",
"version": "1.0",
"last_update": "2025-08-04T01:01:57.925555"
"last_update": "2025-08-04T17:21:54.008091"
}

View File

@ -555,6 +555,37 @@ textarea {
min-width: auto;
}
.refresh-rate-control {
display: flex;
align-items: center;
gap: 0.25rem;
margin-left: 0.5rem;
}
.refresh-rate-control label {
font-size: 0.9rem;
margin: 0;
cursor: pointer;
}
.refresh-rate-input {
padding: 0.15rem 0.3rem;
font-size: 0.75rem;
width: 80px;
height: auto;
border-radius: var(--pico-border-radius);
border: 1px solid var(--pico-border-color);
background: var(--pico-background-color);
color: var(--pico-color);
text-align: center;
}
.refresh-rate-unit {
font-size: 0.7rem;
color: var(--pico-muted-color);
margin-left: 0.2rem;
}
.plot-info {
padding: 0.5rem 1rem;
background: var(--pico-card-background-color);

View File

@ -19,6 +19,7 @@ class PlotManager {
this.updateInterval = null;
this.statusUpdateInterval = null; // 🔑 NUEVO: Para updates de status
this.isInitialized = false;
this.refreshRates = new Map(); // Para mantener el refresh rate de cada sesión
// Colores para las variables
this.colors = [
@ -140,6 +141,15 @@ class PlotManager {
<button class="btn btn-sm" onclick="plotManager.removePlot('${sessionId}')" title="Remove">
Remove
</button>
<div class="refresh-rate-control">
<label for="refresh-rate-${sessionId}" title="Chart Refresh Rate (ms)"></label>
<input type="number" id="refresh-rate-${sessionId}" class="refresh-rate-input"
value="1000" min="100" max="60000" step="100"
onchange="plotManager.updateRefreshRate('${sessionId}', this.value)"
onkeypress="if(event.key==='Enter') plotManager.updateRefreshRate('${sessionId}', this.value)"
title="Refresh rate in milliseconds (100-60000)">
<span class="refresh-rate-unit">ms</span>
</div>
</div>
</div>
<div class="plot-info">
@ -171,11 +181,16 @@ class PlotManager {
let chartConfig;
console.log(`🔍 Plot ${sessionId}: Checking streaming support...`);
console.log(`📊 Chart.registry.scales.realtime available:`, !!hasRealTimeScale);
if (hasRealTimeScale) {
// Configuración con chartjs-plugin-streaming
console.log(`✅ Plot ${sessionId}: Using Real-time Streaming mode`);
chartConfig = this.createStreamingChartConfig(sessionId, config);
} else {
// Configuración fallback con time scale normal
console.log(`⚠️ Plot ${sessionId}: Using Manual Fallback mode`);
chartConfig = this.createFallbackChartConfig(sessionId, config);
}
@ -191,11 +206,19 @@ class PlotManager {
isRealTimeMode: hasRealTimeScale
});
// Inicializar refresh rate por defecto
if (!this.refreshRates.has(sessionId)) {
this.refreshRates.set(sessionId, 1000); // 1 segundo por defecto
console.log(`⏱️ Plot ${sessionId}: Default refresh rate set to 1000ms`);
}
// Inicializar datasets para las variables
this.initializeChartDatasets(sessionId, config);
// Si no es modo realtime, iniciar intervalo manual
if (!hasRealTimeScale) {
const refreshRate = this.refreshRates.get(sessionId) || 1000;
console.log(`🔄 Plot ${sessionId}: Starting manual refresh with ${refreshRate}ms interval`);
this.startManualRefresh(sessionId);
}
}
@ -219,7 +242,7 @@ class PlotManager {
type: 'realtime',
realtime: {
duration: (config.time_window || 60) * 1000,
refresh: 1000, // Actualizar cada segundo
refresh: this.refreshRates.get(sessionId) || 1000, // Actualizar según configuración dinámica
delay: 0,
frameRate: 30,
pause: !config.is_active, // Pausar si no está activo
@ -349,10 +372,17 @@ class PlotManager {
const sessionData = this.sessions.get(sessionId);
if (!sessionData) return;
// Crear intervalo de actualización manual
// Limpiar intervalo existente si hay uno
if (sessionData.manualInterval) {
clearInterval(sessionData.manualInterval);
}
// Crear intervalo de actualización manual con refresh rate dinámico
const refreshRate = this.refreshRates.get(sessionId) || 1000;
console.log(`⏲️ Plot ${sessionId}: Manual interval created with ${refreshRate}ms`);
sessionData.manualInterval = setInterval(() => {
this.onStreamingRefresh(sessionId, sessionData.chart);
}, 400); // Cada 400ms para streaming más suave
}, refreshRate);
}
/**
@ -400,12 +430,20 @@ class PlotManager {
return;
}
// Evitar llamadas muy frecuentes
// Evitar llamadas muy frecuentes basado en el refresh rate dinámico
const now = Date.now();
if (now - sessionData.lastDataFetch < 800) {
const refreshRate = this.refreshRates.get(sessionId) || 1000;
const minInterval = Math.max(refreshRate * 0.8, 100); // 80% del refresh rate, mínimo 100ms
if (now - sessionData.lastDataFetch < minInterval) {
const timeSinceLastUpdate = now - sessionData.lastDataFetch;
console.log(`⏭️ Plot ${sessionId}: Skipping update (${timeSinceLastUpdate}ms < ${minInterval}ms threshold)`);
return;
}
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`);
@ -703,6 +741,9 @@ class PlotManager {
}
this.sessions.delete(sessionId);
// Limpiar refresh rate
this.refreshRates.delete(sessionId);
}
// Remover contenedor
@ -1375,6 +1416,9 @@ class PlotManager {
chart.destroy();
}
this.sessions.delete(this.currentEditingSession);
// Limpiar refresh rate
this.refreshRates.delete(this.currentEditingSession);
}
// 4. Crear nuevo plot desde cero
@ -1454,6 +1498,99 @@ class PlotManager {
}
this.sessions.clear();
}
/**
* Actualiza el refresh rate de una sesión de plot
*/
updateRefreshRate(sessionId, refreshRate) {
try {
const refreshRateMs = parseInt(refreshRate);
// Validación de rango
if (isNaN(refreshRateMs)) {
console.error('❌ Invalid refresh rate - not a number:', refreshRate);
return;
}
if (refreshRateMs < 100) {
console.warn('⚠️ Refresh rate too low, setting to minimum (100ms)');
refreshRate = '100';
} else if (refreshRateMs > 60000) {
console.warn('⚠️ Refresh rate too high, setting to maximum (60000ms)');
refreshRate = '60000';
}
const finalRefreshRateMs = parseInt(refreshRate);
// Actualizar el map de refresh rates
this.refreshRates.set(sessionId, finalRefreshRateMs);
const sessionData = this.sessions.get(sessionId);
if (!sessionData || !sessionData.chart) {
console.error('❌ Session not found:', sessionId);
return;
}
// Actualizar la configuración del chart
if (sessionData.isRealTimeMode) {
// Modo streaming con chartjs-plugin-streaming
const chart = sessionData.chart;
if (chart.scales && chart.scales.x && chart.scales.x.realtime) {
// Actualizar la configuración de refresh rate
chart.scales.x.realtime.refresh = finalRefreshRateMs;
// Forzar reinicio del intervalo interno del plugin
const streaming = chart.$streaming;
if (streaming && streaming.intervalId) {
clearInterval(streaming.intervalId);
// Recrear el intervalo con el nuevo refresh rate
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`);
}
// También actualizar la configuración de opciones para futuros reinicios
chart.options.scales.x.realtime.refresh = finalRefreshRateMs;
}
} else {
// Para modo manual, reiniciar el intervalo
console.log(`🔄 Manual refresh restarted with ${finalRefreshRateMs}ms`);
this.startManualRefresh(sessionId);
}
// Sincronizar ambos inputs (main y tab)
const mainInput = document.getElementById(`refresh-rate-${sessionId}`);
const tabInput = document.getElementById(`refresh-rate-tab-${sessionId}`);
if (mainInput && mainInput.value !== refreshRate) {
mainInput.value = refreshRate;
}
if (tabInput && tabInput.value !== refreshRate) {
tabInput.value = refreshRate;
}
// Debug information
console.log(`⏱️ Plot ${sessionId}: Refresh rate updated to ${finalRefreshRateMs}ms`);
console.log(`📊 Plot ${sessionId}: Mode: ${sessionData.isRealTimeMode ? 'Real-time Streaming' : 'Manual Fallback'}`);
console.log(`🔧 Plot ${sessionId}: Chart scales available:`, !!sessionData.chart.scales);
if (sessionData.chart.scales && sessionData.chart.scales.x) {
console.log(`⚙️ Plot ${sessionId}: X-scale type:`, sessionData.chart.scales.x.type);
console.log(`⚡ Plot ${sessionId}: Realtime config:`, sessionData.chart.scales.x.realtime);
}
} catch (error) {
console.error('❌ Error updating refresh rate:', error);
}
}
}
// Funciones de debug removidas para limpiar console.log
@ -1504,6 +1641,44 @@ window.removePlotSession = async function (sessionId) {
// Remover de PlotManager
if (plotManager && plotManager.sessions.has(sessionId)) {
plotManager.sessions.delete(sessionId);
// Limpiar refresh rate
plotManager.refreshRates.delete(sessionId);
}
showNotification(result.message, 'success');
} else {
showNotification(result.error, 'error');
}
} catch (error) {
console.error('Error removing plot:', error);
showNotification('Error removing plot session', 'error');
}
}
}
// Función global para remover sesiones de plot (movida fuera de la clase)
window.removePlotSession = async function (sessionId) {
if (confirm('¿Estás seguro de que quieres eliminar este plot?')) {
try {
const response = await fetch(`/api/plots/${sessionId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
// Remover tab dinámico
if (typeof tabManager !== 'undefined') {
tabManager.removePlotTab(sessionId);
}
// Remover de PlotManager
if (plotManager && plotManager.sessions.has(sessionId)) {
plotManager.sessions.delete(sessionId);
// Limpiar refresh rate
plotManager.refreshRates.delete(sessionId);
}
showNotification(result.message, 'success');

View File

@ -99,6 +99,15 @@ class TabManager {
<button class="btn btn-sm" onclick="plotManager.controlPlot('${sessionId}', 'stop')" title="Stop">
Stop
</button>
<div class="refresh-rate-control">
<label for="refresh-rate-tab-${sessionId}" title="Chart Refresh Rate (ms)"></label>
<input type="number" id="refresh-rate-tab-${sessionId}" class="refresh-rate-input"
value="1000" min="100" max="60000" step="100"
onchange="plotManager.updateRefreshRate('${sessionId}', this.value)"
onkeypress="if(event.key==='Enter') plotManager.updateRefreshRate('${sessionId}', this.value)"
title="Refresh rate in milliseconds (100-60000)">
<span class="refresh-rate-unit">ms</span>
</div>
</div>
</div>
<div class="plot-info">

View File

@ -1,11 +1,11 @@
{
"last_state": {
"should_connect": true,
"should_stream": true,
"should_stream": false,
"active_datasets": [
"dar"
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-04T01:01:57.951970"
"last_update": "2025-08-04T17:21:54.020081"
}