Update backend manager status, dataset variables, and configuration schemas

- Updated backend manager status with new timestamps and process IDs.
- Modified dataset variables JSON to ensure consistent configuration types and added a new symbolic variable.
- Enhanced dataset variables schema to enforce configuration type and prevent additional properties.
- Adjusted UI schema for dataset variables to improve layout and visibility of fields.
- Refactored config manager to handle expanded dataset variables for PLC communication.
- Updated PLC data streamer and core streamer to utilize expanded dataset variables.
- Simplified DatasetVariableSymbolWidget and SymbolSelectorWidget components, improving toast notifications and layout.
- Removed unnecessary symbol expansion logic from Dashboard component.
- Updated system state JSON with new last update timestamp and added PlotJuggler path.
This commit is contained in:
Miguel 2025-08-28 11:33:09 +02:00
parent 81e5ddec57
commit a0a65f563d
12 changed files with 12827 additions and 11949 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
{
"timestamp": "2025-08-22T15:14:03.883875",
"timestamp": "2025-08-28T10:48:11.316409",
"status": "stopped",
"restart_count": 0,
"last_restart": 0,
"backend_pid": 33676,
"manager_pid": 25004,
"backend_pid": null,
"manager_pid": 13520,
"details": {}
}

View File

@ -4,8 +4,8 @@
"dataset_id": "DAR",
"variables": [
{
"configType": "manual",
"area": "DB",
"configType": "manual",
"db": 1011,
"name": "HMI_Instrument.QTM307.PVFiltered",
"offset": 1322,
@ -13,8 +13,8 @@
"type": "real"
},
{
"configType": "manual",
"area": "DB",
"configType": "manual",
"db": 1011,
"name": "HMI_Instrument.QTM306.PVFiltered",
"offset": 1296,
@ -22,8 +22,8 @@
"type": "real"
},
{
"configType": "manual",
"area": "DB",
"configType": "manual",
"db": 1011,
"name": "HMI_Instrument.CTS306.PVFiltered",
"offset": 1348,
@ -31,12 +31,17 @@
"type": "real"
},
{
"configType": "manual",
"area": "PEW",
"configType": "manual",
"name": "CTS306_PEW",
"offset": 256,
"streaming": true,
"type": "word"
},
{
"configType": "symbol",
"streaming": false,
"symbol": "AUX Blink_2.0S"
}
]
},
@ -44,13 +49,14 @@
"dataset_id": "Fast",
"variables": [
{
"name": "AUX Blink_2.0S",
"configType": "symbol",
"streaming": true,
"symbol": "AUX Blink_2.0S"
},
{
"area": "M",
"bit": 1,
"configType": "manual",
"name": "M50.1",
"offset": 50,
"streaming": false,
@ -59,6 +65,7 @@
{
"area": "M",
"bit": 2,
"configType": "manual",
"name": "M50.2",
"offset": 50,
"streaming": false,

View File

@ -157,10 +157,11 @@
},
"then": {
"properties": {
"name": {
"configType": {
"type": "string",
"title": "Variable Name",
"description": "Human-readable name for the variable (auto-filled from symbol)"
"title": "Configuration Type",
"enum": ["manual", "symbol"],
"default": "manual"
},
"symbol": {
"type": "string",
@ -177,7 +178,8 @@
"required": [
"configType",
"symbol"
]
],
"additionalProperties": false
}
}
]

View File

@ -26,8 +26,8 @@
"items": {
"ui:order": [
"configType",
"name",
"symbol",
"name",
"area",
"db",
"offset",
@ -39,17 +39,23 @@
[
{
"name": "configType",
"width": 3
"width": 6
},
{
"name": "streaming",
"width": 6
}
],
[
{
"name": "symbol",
"width": 12
}
],
[
{
"name": "name",
"width": 6
},
{
"name": "symbol",
"width": 6
"width": 12
}
],
[
@ -71,11 +77,7 @@
},
{
"name": "type",
"width": 2
},
{
"name": "streaming",
"width": 2
"width": 4
}
]
],

View File

@ -263,18 +263,21 @@ class ConfigManager:
streaming_variables = []
for var in variables_list:
# Handle symbolic variables by expanding them first
if var.get("configType") == "symbol":
var = self._expand_symbolic_variable(var)
if var is None:
# Skip if symbol expansion failed
continue
# Keep symbolic variables as they are for configuration storage
# They will be expanded when needed for PLC communication
var_name = var.get("name")
# For symbolic variables, use symbol name if no explicit name
if var.get("configType") == "symbol" and not var_name:
var_name = var.get("symbol")
var = var.copy() # Create copy to avoid modifying original
var["name"] = var_name
if not var_name:
if self.logger:
self.logger.warning(
f"Skipping variable without name in dataset {dataset_id}: {var}"
f"Skipping variable without name in dataset "
f"{dataset_id}: {var}"
)
continue
@ -537,8 +540,6 @@ class ConfigManager:
self.save_configuration()
return {"old_config": old_config, "new_config": self.csv_config}
def get_csv_file_directory_path(self) -> str:
"""Get the directory path for current day's CSV files"""
now = datetime.now()
@ -612,6 +613,39 @@ class ConfigManager:
return self.datasets[self.current_dataset_id]
return None
def get_expanded_dataset_variables(self, dataset_id: str):
"""Get variables for a dataset with symbolic variables expanded for PLC communication"""
if dataset_id not in self.datasets:
return {}
variables = self.datasets[dataset_id].get("variables", {})
expanded_variables = {}
for var_name, var_config in variables.items():
if var_config.get("configType") == "symbol":
# Expand symbolic variable for PLC communication
expanded_var = self._expand_symbolic_variable(var_config)
if expanded_var:
expanded_variables[var_name] = expanded_var
else:
# If expansion fails, keep the original symbolic config for counting
# but mark it as non-functional for PLC communication
fallback_var = var_config.copy()
fallback_var["_expansion_failed"] = True
expanded_variables[var_name] = fallback_var
if self.logger:
symbol = var_config.get("symbol", "unknown")
self.logger.warning(
f"Failed to expand symbol '{symbol}' for "
f"variable '{var_name}' in dataset '{dataset_id}'"
)
else:
# Keep manual variables as they are
expanded_variables[var_name] = var_config
return expanded_variables
def get_dataset_variables(self, dataset_id: str):
"""Get variables for a specific dataset"""
if dataset_id in self.datasets:

View File

@ -274,7 +274,7 @@ class PLCDataStreamer:
continue
# Get expected headers based on current configuration
dataset_variables = self.config_manager.get_dataset_variables(
dataset_variables = self.config_manager.get_expanded_dataset_variables(
dataset_id
)
expected_headers = ["timestamp"] + list(dataset_variables.keys())
@ -537,7 +537,7 @@ class PLCDataStreamer:
self.config_manager.active_datasets
), # Convert set to list for JSON
"total_variables": sum(
len(self.config_manager.get_dataset_variables(dataset_id))
len(self.config_manager.get_expanded_dataset_variables(dataset_id))
for dataset_id in self.config_manager.datasets.keys()
),
"streaming_variables_count": sum(
@ -774,6 +774,10 @@ class PLCDataStreamer:
"""Get variables for a specific dataset"""
return self.config_manager.get_dataset_variables(dataset_id)
def get_expanded_dataset_variables(self, dataset_id: str):
"""Get variables for a dataset with symbolic variables expanded for PLC communication"""
return self.config_manager.get_expanded_dataset_variables(dataset_id)
def get_recent_events(self, limit: int = 50):
"""Get recent events from the log"""
return self.event_logger.get_recent_events(limit)

View File

@ -252,7 +252,9 @@ class DataStreamer:
csv_path = self.get_dataset_csv_file_path(dataset_id)
# Get current dataset variables and create expected headers
dataset_variables = self.config_manager.get_dataset_variables(dataset_id)
dataset_variables = self.config_manager.get_expanded_dataset_variables(
dataset_id
)
expected_headers = ["timestamp"] + list(dataset_variables.keys())
# Check if file exists and validate headers
@ -356,7 +358,7 @@ class DataStreamer:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
# Create row with all variables for this dataset
dataset_variables = self.config_manager.get_dataset_variables(
dataset_variables = self.config_manager.get_expanded_dataset_variables(
dataset_id
)
row = [timestamp]
@ -417,8 +419,10 @@ class DataStreamer:
self.setup_dataset_csv_file(dataset_id)
if dataset_id in self.dataset_csv_writers:
dataset_variables = self.config_manager.get_dataset_variables(
dataset_id
dataset_variables = (
self.config_manager.get_expanded_dataset_variables(
dataset_id
)
)
for entry in buffer_data:
@ -562,7 +566,9 @@ class DataStreamer:
self.dataset_csv_hours[dataset_id] = datetime.now().hour
# Write headers with new variable configuration
dataset_variables = self.config_manager.get_dataset_variables(dataset_id)
dataset_variables = self.config_manager.get_expanded_dataset_variables(
dataset_id
)
if dataset_variables:
headers = ["timestamp"] + list(dataset_variables.keys())
self.dataset_csv_writers[dataset_id].writerow(headers)
@ -629,10 +635,14 @@ class DataStreamer:
try:
# <20> Get dataset configuration to determine reading method
dataset_config = self.config_manager.datasets.get(dataset_id, {})
use_optimized_reading = dataset_config.get("use_optimized_reading", True) # Default to True
use_optimized_reading = dataset_config.get(
"use_optimized_reading", True
) # Default to True
# 🚀 NEW: Use batch reading with dataset-specific optimization setting
batch_results = self.plc_client.read_variables_batch(variables, use_optimized_reading)
batch_results = self.plc_client.read_variables_batch(
variables, use_optimized_reading
)
for var_name, value in batch_results.items():
if value is not None:
@ -842,7 +852,7 @@ class DataStreamer:
try:
# 📋 CRITICAL SECTION: PLC READ with timing and error tracking
dataset_variables = self.config_manager.get_dataset_variables(
dataset_variables = self.config_manager.get_expanded_dataset_variables(
dataset_id
)
variables_count = len(dataset_variables)
@ -1138,7 +1148,7 @@ class DataStreamer:
{
"dataset_id": dataset_id,
"variables_count": len(
self.config_manager.get_dataset_variables(dataset_id)
self.config_manager.get_expanded_dataset_variables(dataset_id)
),
"streaming_count": len(dataset_info["streaming_variables"]),
"prefix": dataset_info["prefix"],

View File

@ -1,45 +1,18 @@
import React, { useState, useEffect } from 'react'
import React from 'react'
import {
Box,
VStack,
Text,
Badge,
useToast
} from '@chakra-ui/react'
import SymbolSelectorWidget from './SymbolSelectorWidget'
const DatasetVariableSymbolWidget = ({ value, onChange, label, disabled, readonly, required, placeholder, formContext }) => {
const [selectedSymbol, setSelectedSymbol] = useState(null)
const toast = useToast()
// Load symbol details when value changes
useEffect(() => {
if (value && !selectedSymbol) {
loadSymbolDetails(value)
}
}, [value])
const loadSymbolDetails = async (symbolName) => {
try {
const response = await fetch('/api/symbols')
const data = await response.json()
if (data.success && data.symbols) {
const symbol = data.symbols.find(s => s.name === symbolName)
if (symbol) {
setSelectedSymbol(symbol)
}
}
} catch (error) {
console.error('Error loading symbol details:', error)
}
}
const handleSymbolSelect = (symbolName) => {
// Update the symbol field
onChange(symbolName)
// Show success message
// Show success message only once
toast({
title: 'Symbol Selected',
description: `Selected: ${symbolName}`,
@ -50,10 +23,7 @@ const DatasetVariableSymbolWidget = ({ value, onChange, label, disabled, readonl
}
const symbolOptions = {
onSymbolSelect: (symbol) => {
setSelectedSymbol(symbol)
handleSymbolSelect(symbol.name)
}
skipToast: true, // Prevent duplicate toasts
}
return (
@ -69,30 +39,6 @@ const DatasetVariableSymbolWidget = ({ value, onChange, label, disabled, readonl
formContext={formContext}
options={symbolOptions}
/>
{selectedSymbol && (
<Box mt={2} p={2} border="1px" borderColor="green.200" borderRadius="md" bg="green.50">
<Text fontSize="xs" color="green.700" fontWeight="medium">
Symbol Information:
</Text>
<VStack align="start" spacing={1} mt={1}>
<Text fontSize="xs" color="green.600">
📝 Name: {selectedSymbol.description || selectedSymbol.name}
</Text>
<Text fontSize="xs" color="green.600" fontFamily="mono">
📍 Address: {selectedSymbol.plc_address}
</Text>
<Text fontSize="xs" color="green.600">
🔧 Area: {selectedSymbol.area?.toUpperCase()}, Offset: {selectedSymbol.offset}
{selectedSymbol.db && `, DB: ${selectedSymbol.db}`}
{selectedSymbol.bit !== null && selectedSymbol.bit !== undefined && `, Bit: ${selectedSymbol.bit}`}
</Text>
<Badge colorScheme="green" fontSize="xs">
{selectedSymbol.data_type}
</Badge>
</VStack>
</Box>
)}
</Box>
)
}

View File

@ -22,7 +22,9 @@ import {
ModalFooter,
ModalBody,
ModalCloseButton,
useDisclosure
useDisclosure,
SimpleGrid,
useColorModeValue
} from '@chakra-ui/react'
import { FiSearch, FiX, FiList, FiInfo } from 'react-icons/fi'
@ -33,6 +35,10 @@ const SymbolSelectorWidget = ({ value, onChange, label, disabled, readonly, requ
const [selectedSymbol, setSelectedSymbol] = useState(null)
const { isOpen, onOpen, onClose } = useDisclosure()
const toast = useToast()
// Theme values - must be called at component level, not conditionally
const nameColor = useColorModeValue("blue.700", "blue.300")
const addressColor = useColorModeValue("gray.600", "gray.400")
// Find the selected symbol from the loaded symbols
useEffect(() => {
@ -100,26 +106,32 @@ const SymbolSelectorWidget = ({ value, onChange, label, disabled, readonly, requ
onClose()
toast({
title: 'Symbol Selected',
description: `Selected: ${symbol.name}`,
status: 'success',
duration: 2000,
isClosable: true,
})
// Only show toast if not being called from a parent widget
if (!options || !options.skipToast) {
toast({
title: 'Symbol Selected',
description: `Selected: ${symbol.name}`,
status: 'success',
duration: 2000,
isClosable: true,
})
}
}
const handleClearSelection = () => {
setSelectedSymbol(null)
onChange('')
toast({
title: 'Selection Cleared',
description: 'Symbol selection has been cleared',
status: 'info',
duration: 2000,
isClosable: true,
})
// Only show toast if not being called from a parent widget
if (!options || !options.skipToast) {
toast({
title: 'Selection Cleared',
description: 'Symbol selection has been cleared',
status: 'info',
duration: 2000,
isClosable: true,
})
}
}
const SymbolCard = ({ symbol, onClick }) => (
@ -132,28 +144,31 @@ const SymbolSelectorWidget = ({ value, onChange, label, disabled, readonly, requ
_hover={{ bg: 'gray.50', borderColor: 'blue.300' }}
_active={{ bg: 'gray.100' }}
onClick={() => onClick(symbol)}
minH="140px"
display="flex"
flexDirection="column"
>
<VStack align="start" spacing={1}>
<VStack align="start" spacing={1} flex={1}>
<HStack justify="space-between" w="full">
<Text fontWeight="semibold" fontSize="sm" color="blue.600">
<Text fontWeight="semibold" fontSize="sm" color="blue.600" noOfLines={1}>
{symbol.name}
</Text>
<Badge colorScheme="gray" fontSize="xs">
<Badge colorScheme="gray" fontSize="xs" flexShrink={0}>
{symbol.data_type}
</Badge>
</HStack>
<Text fontSize="xs" color="gray.600" fontFamily="mono">
<Text fontSize="xs" color="gray.600" fontFamily="mono" noOfLines={1}>
{symbol.plc_address}
</Text>
{symbol.description && (
<Text fontSize="xs" color="gray.500" noOfLines={2}>
<Text fontSize="xs" color="gray.500" noOfLines={2} flex={1}>
{symbol.description}
</Text>
)}
<HStack spacing={2} fontSize="xs">
<HStack spacing={1} fontSize="xs" flexWrap="wrap" mt="auto">
<Badge colorScheme="blue" variant="subtle">
{symbol.area?.toUpperCase()}
</Badge>
@ -219,53 +234,35 @@ const SymbolSelectorWidget = ({ value, onChange, label, disabled, readonly, requ
</HStack>
{selectedSymbol && (
<Box
border="1px"
borderColor="blue.200"
borderRadius="md"
p={3}
bg="blue.50"
>
<VStack align="start" spacing={1}>
<HStack justify="space-between" w="full">
<Text fontWeight="semibold" fontSize="sm" color="blue.700">
{selectedSymbol.name}
</Text>
<Badge colorScheme="blue">
{selectedSymbol.data_type}
</Badge>
</HStack>
<Text fontSize="xs" color="blue.600" fontFamily="mono">
{selectedSymbol.plc_address}
<VStack spacing={1} align="start">
<HStack justify="space-between" w="full">
<Text fontWeight="semibold" fontSize="sm" color={nameColor}>
📝 {selectedSymbol.description || selectedSymbol.name}
</Text>
{selectedSymbol.description && (
<Text fontSize="xs" color="blue.500">
{selectedSymbol.description}
</Text>
)}
<HStack spacing={2} fontSize="xs">
<Badge colorScheme="blue">
<Badge colorScheme="blue" variant="subtle">
{selectedSymbol.data_type}
</Badge>
</HStack>
<HStack justify="space-between" w="full">
<Text fontSize="xs" color={addressColor} fontFamily="mono">
📍 {selectedSymbol.plc_address}
</Text>
<HStack spacing={1} fontSize="xs">
<Badge colorScheme="green" variant="subtle">
{selectedSymbol.area?.toUpperCase()}
</Badge>
{selectedSymbol.db && (
<Badge colorScheme="green">
DB{selectedSymbol.db}
</Badge>
)}
<Badge colorScheme="purple">
<Badge colorScheme="purple" variant="subtle">
@{selectedSymbol.offset}
</Badge>
{selectedSymbol.bit !== null && selectedSymbol.bit !== undefined && (
<Badge colorScheme="orange">
<Badge colorScheme="orange" variant="subtle">
.{selectedSymbol.bit}
</Badge>
)}
</HStack>
</VStack>
</Box>
</HStack>
</VStack>
)}
{symbols.length === 0 && !isLoading && (
@ -276,9 +273,9 @@ const SymbolSelectorWidget = ({ value, onChange, label, disabled, readonly, requ
</VStack>
{/* Symbol Selection Modal */}
<Modal isOpen={isOpen} onClose={onClose} size="xl" scrollBehavior="inside">
<Modal isOpen={isOpen} onClose={onClose} size="6xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent maxH="80vh">
<ModalContent maxH="85vh">
<ModalHeader>
<VStack align="start" spacing={2}>
<Text>Select PLC Symbol</Text>
@ -322,13 +319,15 @@ const SymbolSelectorWidget = ({ value, onChange, label, disabled, readonly, requ
{searchQuery && ` for "${searchQuery}"`}
</Text>
{filteredSymbols.map((symbol, index) => (
<SymbolCard
key={`${symbol.name}-${index}`}
symbol={symbol}
onClick={handleSymbolSelect}
/>
))}
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={3}>
{filteredSymbols.map((symbol, index) => (
<SymbolCard
key={`${symbol.name}-${index}`}
symbol={symbol}
onClick={handleSymbolSelect}
/>
))}
</SimpleGrid>
{searchQuery && filteredSymbols.length >= 50 && (
<Text fontSize="sm" color="orange.500" textAlign="center" fontStyle="italic">

View File

@ -1505,99 +1505,10 @@ function DatasetManager() {
)
}
// Function to expand symbol data using backend API
const expandSymbolToManualConfig = async (symbolName, currentVariable = {}) => {
try {
// Create a temporary variable array with just this symbol
const tempVariables = [{
symbol: symbolName,
streaming: currentVariable.streaming || false
}]
// Call backend API to process the symbol
const response = await fetch('/api/symbols/process-variables', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
variables: tempVariables
})
})
const result = await response.json()
if (result.success && result.processed_variables.length > 0) {
const processedVar = result.processed_variables[0]
// Build the configuration object, only including relevant fields
const config = {
name: processedVar.name || symbolName,
area: processedVar.area || "DB",
offset: processedVar.offset !== undefined && processedVar.offset !== null ? processedVar.offset : 0,
type: processedVar.type || "real",
streaming: currentVariable.streaming || false
}
// Only include db field if it's actually present and area requires it
if (processedVar.db !== undefined && processedVar.db !== null) {
config.db = processedVar.db
} else if (config.area === "DB") {
// Default to 1 only for DB area if no DB number was provided
config.db = 1
}
// Only include bit field if it's actually present
if (processedVar.bit !== undefined && processedVar.bit !== null) {
config.bit = processedVar.bit
} else {
// Default to 0 for bit position when not specified
config.bit = 0
}
return config
} else {
// If backend processing failed, return basic defaults
const fallbackConfig = {
name: currentVariable.name || symbolName,
area: "DB", // Default to DB area
offset: 0,
type: "real",
bit: 0,
streaming: currentVariable.streaming || false
}
// Only add db field for DB area
if (fallbackConfig.area === "DB") {
fallbackConfig.db = 1
}
return fallbackConfig
}
} catch (error) {
console.error('Error expanding symbol:', error)
// Return basic defaults on error
const errorConfig = {
name: currentVariable.name || symbolName,
area: "DB", // Default to DB area
offset: 0,
type: "real",
bit: 0,
streaming: currentVariable.streaming || false
}
// Only add db field for DB area
if (errorConfig.area === "DB") {
errorConfig.db = 1
}
return errorConfig
}
}
// Standard form change handler for external schema compatibility
const handleFormChange = ({ formData }) => {
// Direct update without special processing for external schema compatibility
// For symbol-based configuration, don't auto-expand to manual fields
// The schema should handle field visibility based on configType
updateSelectedDatasetVariables(formData)
}

View File

@ -7,5 +7,6 @@
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-27T15:19:56.923648"
"last_update": "2025-08-28T11:31:29.749311",
"plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe"
}