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:
parent
a26a1c7ace
commit
4d41b7b9b3
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
},
|
||||
|
|
|
@ -72,7 +72,7 @@
|
|||
"enabled": true
|
||||
},
|
||||
{
|
||||
"variable_name": "Blink",
|
||||
"variable_name": "HMI_Instrument.CTS306.PVFiltered",
|
||||
"color": "#3498db",
|
||||
"line_width": 2,
|
||||
"y_axis": "left",
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
10
main.py
|
@ -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
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
Loading…
Reference in New Issue