Refactor plot definitions and variable configurations

- Updated plot definition for DAR_Brix to disable stacking and increase time window from 60 to 120 seconds.
- Changed variable name from "Blink" to "HMI_Instrument.CTS306.PVFiltered" in plot variables configuration.
- Removed console logs related to pending time changes in PlotHistoricalSession component for cleaner code.
- Enhanced VariableSelectorWidget with debug options and improved SSE connection handling.
- Removed unnecessary debug logs from various components including Tooltip, AllWidgets, and TestWidget.
- Implemented automatic reconnection logic for SSE in useCoordinatedSSE hook.
- Added periodic ping messages in stream_variables function to maintain SSE connection.
- Updated system state to enable connection and include active datasets.
This commit is contained in:
Miguel 2025-08-29 20:31:17 +02:00
parent a26a1c7ace
commit 4d41b7b9b3
17 changed files with 3403 additions and 569 deletions

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,9 @@
"name": "DAR_Brix",
"point_hover_radius": 4,
"point_radius": 1,
"stacked": true,
"stacked": false,
"stepped": true,
"time_window": 60,
"time_window": 120,
"trigger_enabled": false,
"trigger_on_true": true
},

View File

@ -72,7 +72,7 @@
"enabled": true
},
{
"variable_name": "Blink",
"variable_name": "HMI_Instrument.CTS306.PVFiltered",
"color": "#3498db",
"line_width": 2,
"y_axis": "left",

View File

@ -91,14 +91,11 @@ export default function PlotHistoricalSession({
// Apply pending changes after cooldown
const applyPendingTimeChanges = useCallback(() => {
const pending = pendingUpdatesRef.current
console.log('📊 Applying pending time changes:', pending)
if (pending.centralTime !== null) {
console.log('📊 Setting central time to:', pending.centralTime)
setCentralTime(pending.centralTime)
}
if (pending.rangeSeconds !== null) {
console.log('📊 Setting time range seconds to:', pending.rangeSeconds)
setTimeRangeSeconds(pending.rangeSeconds)
}
@ -109,8 +106,6 @@ export default function PlotHistoricalSession({
// Debounced handler for time changes (pan/zoom)
const debouncedTimeChange = useCallback((newCentralTime, newRangeSeconds = null) => {
console.log('📊 Debounced time change requested:', { newCentralTime, newRangeSeconds })
// Update pending values using refs
pendingUpdatesRef.current.centralTime = newCentralTime
if (newRangeSeconds !== null) {
@ -123,9 +118,7 @@ export default function PlotHistoricalSession({
}
// Set new timer
console.log('📊 Setting timer for 1000ms...')
cooldownTimerRef.current = setTimeout(() => {
console.log('📊 Timer fired, calling applyPendingTimeChanges')
applyPendingTimeChanges()
}, 1000)
}, [applyPendingTimeChanges])

View File

@ -1147,7 +1147,11 @@ export default function PlotManager() {
variable_name: {
"ui:widget": "variableSelector",
"ui:placeholder": "Search and select variable from datasets...",
"ui:description": "🔍 Search and select a variable from the configured datasets (includes live values and metadata)"
"ui:description": "🔍 Search and select a variable from the configured datasets (includes live values and metadata)",
"ui:options": {
debug: false, // Enable to debug live value issues
refreshTrigger: Date.now() // Force refresh when schema changes
}
},
label: {
"ui:widget": "text",

View File

@ -339,30 +339,20 @@ export default function PlotRealtimeSession({
const wasPaused = session.is_paused
try {
console.log(`🔄 Applying configuration changes for plot ${plotDefinition.id}...`)
console.log(`📊 Plot was active: ${wasActive}, paused: ${wasPaused}`)
console.log(`📋 Local config to apply:`, localConfig)
// First, update backend configuration and wait for it to complete
console.log(`💾 Saving configuration to backend...`)
await onConfigUpdate?.(plotDefinition.id, localConfig)
console.log(`✅ Configuration saved to backend successfully`)
// Wait a moment for the configuration to be fully persisted
await new Promise(resolve => setTimeout(resolve, 200))
// Then reload configuration from backend (same as Refresh button)
if (onReloadConfig) {
console.log(`📥 Reloading plot configuration from backend...`)
await onReloadConfig()
console.log(`✅ Configuration reloaded from backend`)
}
// Finally refresh the chart configuration (same as Refresh button)
if (chartControlsRef.current?.refreshConfiguration) {
console.log(`🔄 Refreshing chart configuration...`)
await chartControlsRef.current.refreshConfiguration()
console.log(`✅ Chart configuration refreshed successfully`)
} else {
console.warn(`⚠️ chartControlsRef.current.refreshConfiguration not available`)
}
@ -372,7 +362,6 @@ export default function PlotRealtimeSession({
// If the plot was active before, restart it
if (wasActive && !wasPaused) {
console.log(`🔄 Restarting plot session that was active before Apply...`)
await handleControlClick('start')
}
@ -426,13 +415,9 @@ export default function PlotRealtimeSession({
const wasPaused = session.is_paused
try {
console.log(`🔄 Refreshing configuration for plot ${plotDefinition.id}...`)
// First, reload configuration from backend if the function is available
if (onReloadConfig) {
console.log(`📥 Reloading plot configuration from backend...`)
await onReloadConfig()
console.log(`✅ Configuration reloaded from backend`)
}
// Trigger chart configuration refresh if available
@ -445,7 +430,6 @@ export default function PlotRealtimeSession({
// If the plot was active before refresh, try to restart it
if (wasActive && !wasPaused) {
console.log(`🔄 Plot was active before refresh, attempting to restart...`)
try {
// Wait a bit to ensure the session status has been updated
await new Promise(resolve => setTimeout(resolve, 500))

View File

@ -116,7 +116,6 @@ export default function TimePointSelector({
// Establecer nuevo timer
cooldownRef.current = setTimeout(() => {
if (onTimeChange && lastCallbackValueRef.current) {
console.log('📊 TimeSelector: Calling onChange after cooldown', lastCallbackValueRef.current);
onTimeChange(lastCallbackValueRef.current);
}
cooldownRef.current = null;
@ -142,7 +141,6 @@ export default function TimePointSelector({
setHasPendingChanges(false);
if (onTimeChange) {
console.log('📊 TimeSelector: Applying changes', { time: newValue, range: tempRangeSeconds });
onTimeChange(newValue, tempRangeSeconds);
}
}, [sliderValue, tempRangeSeconds, hasPendingChanges, onTimeChange]);
@ -158,7 +156,6 @@ export default function TimePointSelector({
// DatePicker es cambio directo - usar range actual
if (onTimeChange) {
console.log('📊 TimeSelector: DatePicker change (immediate)', newValue);
onTimeChange(newValue, rangeSeconds);
}
};

View File

@ -12,17 +12,14 @@ import { useCoordinatedSSE } from '../../hooks/useCoordinatedConnection'
export function VariableSelectorWidget(props) {
const { id, value, required, disabled, readonly, label, onChange, onBlur, onFocus, rawErrors = [], options = {} } = props
// Extract refresh trigger from options if provided
// Extract options at the top to maintain hook order
const refreshTrigger = options?.refreshTrigger || 0
const debugMode = options?.debug || false
// Use variable context if available, fallback to local state
let contextTrigger = 0
try {
const variableContext = useVariableContext()
contextTrigger = variableContext?.variableRefreshTrigger || 0
} catch {
// Context not available, use local trigger only
}
// Always call useVariableContext to maintain hook order
const variableContext = useVariableContext()
const contextTrigger = variableContext?.variableRefreshTrigger || 0
const [datasetVariables, setDatasetVariables] = useState({})
const [loading, setLoading] = useState(true)
@ -86,7 +83,12 @@ export function VariableSelectorWidget(props) {
// Load dataset variables on mount and when refresh trigger changes
useEffect(() => {
loadDatasetVariables()
// Small delay to ensure context is properly initialized
const timer = setTimeout(() => {
loadDatasetVariables()
}, 100)
return () => clearTimeout(timer)
}, [loadDatasetVariables, refreshTrigger, contextTrigger])
// Auto-refresh when component gains focus (optional behavior)
@ -128,22 +130,57 @@ export function VariableSelectorWidget(props) {
}, [datasetVariables])
// Determinar dataset y variable para SSE coordinado
const variable = value && allVariables.find(v => v.name === value)
const variable = useMemo(() => {
if (!value || !allVariables.length) return null
return allVariables.find(v => v.name === value)
}, [value, allVariables])
const datasetId = variable?.dataset
// Usar SSE coordinado para valor en vivo de la variable seleccionada
const sseUrl = datasetId && value ?
`/api/stream/variables?dataset_id=${encodeURIComponent(datasetId)}&interval=1.0` :
null
const sseUrl = useMemo(() => {
if (!datasetId || !value || !variable) return null
return `/api/stream/variables?dataset_id=${encodeURIComponent(datasetId)}&interval=1.0`
}, [datasetId, value, variable])
const { data: liveData } = useCoordinatedSSE(
`variable_live_${datasetId}_${value}`,
const { data: liveData, isConnected: sseConnected } = useCoordinatedSSE(
value && datasetId ? `variable_live_${datasetId}_${value}` : null,
sseUrl,
[datasetId, value]
[datasetId || '', value || '', (variable?.name || '')]
)
// Procesar datos SSE recibidos
// Debug logging for SSE connection
useEffect(() => {
if (debugMode) {
console.log(`🔍 VariableSelectorWidget Debug:`, {
value,
datasetId,
variable,
sseUrl,
liveStatus,
liveValue,
liveDataType: liveData?.type,
sseConnected,
timestamp: new Date().toLocaleTimeString()
})
}
}, [value, datasetId, variable, sseUrl, liveStatus, liveValue, liveData, sseConnected, debugMode])
// Procesar datos SSE recibidos y estado de conexión
useEffect(() => {
if (!value || !datasetId) {
setLiveValue(undefined)
setLiveStatus('idle')
return
}
// Si no hay conexión SSE, mostrar estado de desconexión
if (!sseConnected) {
setLiveStatus('disconnected')
// No limpiar liveValue inmediatamente para mostrar último valor conocido
return
}
if (!liveData) {
setLiveValue(undefined)
setLiveStatus('idle')
@ -151,17 +188,26 @@ export function VariableSelectorWidget(props) {
}
if (liveData?.type === 'values' && liveData.values) {
setLiveValue(liveData.values[value])
const currentValue = liveData.values[value]
setLiveValue(currentValue)
setLiveStatus('ok')
} else if (liveData?.type === 'ping') {
// Ignore ping messages, they're just for keeping connection alive
return
} else if (liveData?.type === 'no_cache') {
setLiveStatus('waiting')
setLiveValue(undefined)
} else if (liveData?.type === 'plc_disconnected' || liveData?.type === 'dataset_inactive') {
setLiveValue(undefined)
setLiveStatus('offline')
} else if (liveData?.type === 'connected') {
setLiveStatus('connecting')
setLiveValue(undefined)
} else if (liveData?.type === 'cache_error') {
setLiveStatus('error')
setLiveValue(undefined)
}
}, [liveData, value])
}, [liveData, value, datasetId, sseConnected])
// Filter variables based on search term and selected dataset
const filteredVariables = useMemo(() => {
@ -348,9 +394,33 @@ export function VariableSelectorWidget(props) {
{selectedVariable.streaming ? ' • Real-time streaming enabled' : ' • Static logging only'}
</Text>
<Text fontSize="sm">
Live value: {liveStatus === 'ok' && liveValue !== undefined ? (
<Text as="span" fontWeight="semibold">{String(liveValue)}</Text>
) : liveStatus === 'waiting' ? 'waiting…' : liveStatus === 'offline' ? 'offline' : liveStatus === 'error' ? 'error' : '—'}
Live value: {(() => {
if (!value || !datasetId) {
return <Text as="span" color="gray.500">select variable</Text>
}
switch (liveStatus) {
case 'ok':
return liveValue !== undefined ? (
<Text as="span" fontWeight="semibold" color="green.600">{String(liveValue)}</Text>
) : (
<Text as="span" color="orange.500">no data</Text>
)
case 'waiting':
return <Text as="span" color="blue.500">waiting for cache...</Text>
case 'offline':
return <Text as="span" color="red.500">PLC offline</Text>
case 'connecting':
return <Text as="span" color="blue.500">connecting...</Text>
case 'disconnected':
return <Text as="span" color="orange.500">reconnecting...</Text>
case 'error':
return <Text as="span" color="red.500">cache error</Text>
case 'idle':
default:
return <Text as="span" color="gray.500"></Text>
}
})()}
</Text>
</VStack>
</Box>

View File

@ -9,9 +9,6 @@ export const Tooltip = React.forwardRef(function Tooltip(props, ref) {
...rest
} = props
// Debug logging
console.log('Tooltip render:', { label, disabled, hasChildren: !!children })
if (disabled || !label) return children
return (

View File

@ -86,13 +86,6 @@ export const allWidgets = {
TestWidget: TestWidget,
}
// Debug log to verify widget registration
console.log('🎯 Widget Registry:', {
hasPlcVariableObject: !!allWidgets.plcVariableObject,
hasPlcVariableObjectDash: !!allWidgets['plc-variable-object'],
totalWidgets: Object.keys(allWidgets).length
})
// Export both widgets and fields (fields can handle complete objects)
export const allFields = {
'test-widget': TestWidget,

View File

@ -62,16 +62,6 @@ const PlcVariableObjectWidget = (props) => {
...otherProps
} = props
// Log all props received when used as a field
console.log('🔧 PlcVariableObjectWidget PROPS:', {
id,
value,
hasOnChange: !!onChange,
schema: schema?.title || schema?.type,
uiSchema: Object.keys(uiSchema || {}),
allPropsKeys: Object.keys(props),
otherProps: Object.keys(otherProps)
})
// Parse value - handle both object and string cases
let parsedValue = value
if (typeof value === 'string') {
@ -88,21 +78,6 @@ const PlcVariableObjectWidget = (props) => {
parsedValue = {}
}
// Debug log to understand how data is being passed
console.log('🔧 PlcVariableObjectWidget DEBUG:', {
id,
rawValue: value,
rawValueJSON: JSON.stringify(value, null, 2),
valueType: typeof value,
valueIsEmpty: !value || value === '{}',
parsedValue,
parsedValueJSON: JSON.stringify(parsedValue, null, 2),
parsedValueKeys: parsedValue ? Object.keys(parsedValue) : [],
schema: schema?.title,
formContext,
registry: !!registry
})
// Initialize state from parsed value object
const name = parsedValue?.name || ''
const address = parsedValue?.address || ''
@ -434,16 +409,6 @@ const PlcVariableObjectWidget = (props) => {
const status = getValidationStatus()
const showValidateButton = address.trim() || symbol.trim()
console.log('🔧 Current state values:', {
name: `"${name}"`,
address: `"${address}"`,
symbol: `"${symbol}"`,
format: `"${format}"`,
showValidateButton,
hasValue: !!value,
valueType: typeof value
})
return (
<VStack align="stretch" spacing={4}>
{/* Variable Name Field */}

View File

@ -4,8 +4,6 @@ import React from 'react'
* Simple Test Widget para verificar que el sistema de widgets funciona
*/
const TestWidget = (props) => {
console.log('🧪 TestWidget loaded!', props)
return (
<div style={{
border: '3px solid red',

View File

@ -26,7 +26,11 @@ export function VariableProvider({ children }) {
export function useVariableContext() {
const context = useContext(VariableContext)
if (context === undefined) {
throw new Error('useVariableContext must be used within a VariableProvider')
// Return a default context instead of throwing error
return {
variableRefreshTrigger: 0,
triggerVariableRefresh: () => {}
}
}
return context
}

View File

@ -13,6 +13,9 @@ export function useCoordinatedConnection(source, connectionFactory, dependencies
const coordinatorRef = useRef(null)
const subscriptionRef = useRef(null)
// Ensure dependencies is always an array to prevent undefined issues
const safeDependencies = Array.isArray(dependencies) ? dependencies : []
// Obtener el coordinador
useEffect(() => {
coordinatorRef.current = getTabCoordinator()
@ -30,7 +33,7 @@ export function useCoordinatedConnection(source, connectionFactory, dependencies
// Crear/recrear conexión cuando cambia el liderazgo o dependencias
useEffect(() => {
if (!coordinatorRef.current) return
if (!coordinatorRef.current || source === 'null_source') return
// Limpiar conexión anterior
if (connectionRef.current && typeof connectionRef.current.close === 'function') {
@ -83,7 +86,7 @@ export function useCoordinatedConnection(source, connectionFactory, dependencies
subscriptionRef.current()
}
}
}, [source, isLeader, ...dependencies])
}, [source, isLeader, ...safeDependencies])
// Cleanup final
useEffect(() => {
@ -106,6 +109,9 @@ export function useCoordinatedConnection(source, connectionFactory, dependencies
export function useCoordinatedPolling(source, fetchFunction, interval = 5000, dependencies = []) {
const [connectionError, setConnectionError] = useState(null)
// Ensure dependencies is always an array
const safeDependencies = Array.isArray(dependencies) ? dependencies : []
const result = useCoordinatedConnection(
source,
useCallback((onData) => {
@ -169,45 +175,114 @@ export function useCoordinatedPolling(source, fetchFunction, interval = 5000, de
}
}
}, [fetchFunction, interval]),
dependencies
safeDependencies
)
return { ...result, connectionError }
}
/**
* Hook para SSE coordinado
* Hook para SSE coordinado con reconexión automática
*/
export function useCoordinatedSSE(source, url, dependencies = []) {
return useCoordinatedConnection(
source,
// Ensure dependencies is always an array
const safeDependencies = Array.isArray(dependencies) ? dependencies : []
// Always call useCoordinatedConnection to maintain hook order
const result = useCoordinatedConnection(
source || 'null_source', // Use a placeholder when source is null
useCallback((onData) => {
// Don't create EventSource if URL is null or undefined
if (!url) {
// console.log(`Skipping SSE connection - URL is ${url}`)
if (!url || !source) {
// console.log(`Skipping SSE connection - URL is ${url}, source is ${source}`)
return {
close: () => {} // Return mock connection with close method
}
}
// console.log(`Creating SSE connection to ${url}`)
const eventSource = new EventSource(url)
let eventSource = null
let isActive = true
let reconnectAttempts = 0
const maxReconnectAttempts = 10
const baseReconnectDelay = 1000 // 1 second
eventSource.onmessage = (event) => {
const createConnection = () => {
if (!isActive) return
try {
const data = JSON.parse(event.data)
onData(data)
// console.log(`Creating SSE connection to ${url} (attempt ${reconnectAttempts + 1})`)
eventSource = new EventSource(url)
eventSource.onopen = () => {
// console.log(`SSE connection opened for ${source}`)
reconnectAttempts = 0 // Reset attempts on successful connection
}
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
onData(data)
} catch (error) {
console.error('SSE data parse error:', error)
}
}
eventSource.onerror = (error) => {
console.warn(`SSE connection error for ${source}:`, error)
if (eventSource.readyState === EventSource.CLOSED) {
// Connection is closed, attempt to reconnect
if (isActive && reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++
const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts - 1), 30000)
console.log(`Reconnecting SSE in ${delay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`)
setTimeout(() => {
if (isActive) {
createConnection()
}
}, delay)
} else {
console.error(`Max reconnection attempts reached for ${source}`)
}
}
}
} catch (error) {
console.error('SSE data parse error:', error)
console.error(`Failed to create SSE connection for ${source}:`, error)
// Retry with exponential backoff
if (isActive && reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++
const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempts - 1), 30000)
setTimeout(() => {
if (isActive) {
createConnection()
}
}, delay)
}
}
}
// Create initial connection
createConnection()
eventSource.onerror = (error) => {
console.error('SSE error:', error)
return {
close: () => {
isActive = false
if (eventSource) {
eventSource.close()
eventSource = null
}
}
}
return eventSource
}, [url]),
dependencies
}, [url, source]),
safeDependencies
)
// If source is null or undefined, return null data but maintain hook call consistency
if (!source) {
return { data: null, isLeader: false, isConnected: false }
}
return result
}

View File

@ -1320,9 +1320,6 @@ function DatasetManager() {
api.getSchema('dataset-variables')
])
console.log('🔧 Dashboard loaded datasets data:', JSON.stringify(datasetsData, null, 2))
console.log('🔧 Dashboard loaded variables data:', JSON.stringify(variablesData, null, 2))
setDatasetsConfig(datasetsData)
setVariablesConfig(variablesData)
setDatasetsSchemaData(datasetsSchemaResponse)
@ -1414,7 +1411,6 @@ function DatasetManager() {
// Get filtered variables for selected dataset (memoized)
const selectedDatasetVariables = useMemo(() => {
console.log('🔧 Recalculating selected dataset variables...')
if (!variablesConfig?.variables || !selectedDatasetId) {
return { dataset_id: selectedDatasetId, variables: [] }
}
@ -1494,7 +1490,6 @@ function DatasetManager() {
onChange={(e) => {
const newDatasetId = e.target.value
setSelectedDatasetId(newDatasetId)
// console.log(`🎯 Dataset selection changed to: ${newDatasetId}`)
}}
placeholder="Choose a dataset to configure..."
size="lg"
@ -1555,14 +1550,6 @@ function DatasetManager() {
{(() => {
const selectedDatasetVars = selectedDatasetVariables
// Debug log to understand what data we're working with
console.log('🔧 Form rendering with data:', {
selectedDatasetId,
selectedDatasetVars,
variablesArray: selectedDatasetVars?.variables,
variablesCount: selectedDatasetVars?.variables?.length || 0
})
// Create simplified schema from external schema for single dataset variables
let singleDatasetSchema = null
let singleDatasetUiSchema = null
@ -1573,13 +1560,6 @@ function DatasetManager() {
const datasetItemSchema = variablesSchemaData.schema.properties?.variables?.items
const variablesArraySchema = datasetItemSchema?.properties?.variables
console.log('🔧 Schema extraction:', {
datasetItemSchema: !!datasetItemSchema,
variablesArraySchema: !!variablesArraySchema,
variablesArraySchemaKeys: variablesArraySchema ? Object.keys(variablesArraySchema) : null,
itemsSchema: variablesArraySchema?.items
})
if (variablesArraySchema) {
singleDatasetSchema = {
type: "object",
@ -1600,13 +1580,6 @@ function DatasetManager() {
const datasetItemUiSchema = variablesSchemaData.uiSchema.variables?.items
const variablesUiSchema = datasetItemUiSchema?.variables
console.log('🔧 UI Schema extraction:', {
datasetItemUiSchema: !!datasetItemUiSchema,
variablesUiSchema: !!variablesUiSchema,
variablesUiSchemaKeys: variablesUiSchema ? Object.keys(variablesUiSchema) : null,
itemsUiField: variablesUiSchema?.items?.['ui:field']
})
if (variablesUiSchema) {
singleDatasetUiSchema = {
variables: variablesUiSchema
@ -1635,22 +1608,6 @@ function DatasetManager() {
variables: selectedDatasetVars.variables || []
}
console.log('🔧 Passing formData to Form:', {
formDataToPass,
variablesLength: formDataToPass.variables.length,
firstVariable: formDataToPass.variables[0] || 'no variables',
allVariables: formDataToPass.variables,
formDataJSON: JSON.stringify(formDataToPass, null, 2)
})
console.log('🔧 Final schemas for Form:', {
singleDatasetSchema,
singleDatasetUiSchema,
schemaVariablesType: singleDatasetSchema?.properties?.variables?.type,
schemaVariablesItemsProps: singleDatasetSchema?.properties?.variables?.items ? Object.keys(singleDatasetSchema.properties.variables.items.properties || {}) : null,
uiSchemaVariablesItemsField: singleDatasetUiSchema?.variables?.items?.['ui:field']
})
return (
<Form
schema={singleDatasetSchema}

10
main.py
View File

@ -3127,9 +3127,18 @@ def stream_variables():
yield f"data: {json.dumps({'type': 'connected', 'message': 'SSE connection established - monitoring cache'})}\n\n"
last_values = {}
last_ping_time = time.time()
ping_interval = 30 # Send ping every 30 seconds to keep connection alive
while True:
try:
current_time = time.time()
# Send periodic ping to keep connection alive
if current_time - last_ping_time >= ping_interval:
yield f"data: {json.dumps({'type': 'ping', 'timestamp': datetime.now().isoformat()})}\n\n"
last_ping_time = current_time
# Check basic preconditions for cache availability
if not streamer.plc_client.is_connected():
# PLC not connected - cache won't be populated
@ -3277,6 +3286,7 @@ def stream_variables():
"Connection": "keep-alive",
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Cache-Control",
"X-Accel-Buffering": "no", # Disable nginx buffering
},
)

View File

@ -1,10 +1,13 @@
{
"last_state": {
"should_connect": false,
"should_connect": true,
"should_stream": false,
"active_datasets": []
"active_datasets": [
"DAR",
"Test"
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-29T12:20:04.122435",
"last_update": "2025-08-29T20:13:41.644405",
"plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe"
}