feat: Update PLC configuration to support absolute and relative paths for records and symbols

- Changed records_directory in plc_config.json to an absolute path.
- Enhanced plc.schema.json to allow absolute and relative paths for records_directory and symbols_path with path-browser widget.
- Updated ui schema to reflect changes in records_directory and symbols_path with appropriate help texts.
- Modified ConfigManager to handle symbols_path in PLC configuration updates.
- Added functionality in PLCDataStreamer and schema_manager to manage symbols_path.
- Implemented Load Symbols button in PLCConfigManager and Dashboard components to load symbols from ASC files.
- Created new PathBrowserWidget for browsing files and directories, supporting both absolute and relative paths.
- Added SimpleFilePathWidget for simplified file path selection.
- Introduced DirectoryBrowserWidget for directory selection.
- Updated main.py to handle loading symbols from ASC files and browsing directories.
This commit is contained in:
Miguel 2025-08-27 09:24:41 +02:00
parent c251c76072
commit 1b6528977a
17 changed files with 1927 additions and 20980 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@
"csv_config": {
"max_days": 30,
"max_size_mb": 1000,
"records_directory": "records",
"records_directory": "C:/Trabajo/SIDEL/09 - SAE452 - Diet as Regular - San Giorgio in Bosco/Reporte/LogRecords",
"rotation_enabled": true
},
"plc_config": {

View File

@ -28,9 +28,14 @@
},
"records_directory": {
"default": "records",
"description": "Directory to save *.csv files",
"description": "Directory to save *.csv files. Use absolute path (e.g. C:\\data) or relative path (e.g. records)",
"title": "Records Directory",
"type": "string"
"type": "string",
"options": {
"widget": "path-browser",
"mode": "directory",
"title": "Select Records Directory"
}
},
"rotation_enabled": {
"default": true,
@ -72,10 +77,13 @@
},
"symbols_path": {
"title": "Symbols File Path",
"description": "Path to the ASC symbol file for this PLC",
"description": "Path to the ASC symbol file for this PLC. Use absolute path or relative path",
"type": "string",
"options": {
"widget": "file-path"
"widget": "path-browser",
"mode": "file",
"title": "Select ASC Symbol File",
"filetypes": [["ASC Files", "*.asc"], ["All Files", "*.*"]]
}
}
},

View File

@ -14,7 +14,13 @@
},
"records_directory": {
"ui:column": 3,
"ui:placeholder": "records"
"ui:placeholder": "records or C:\\data",
"ui:widget": "path-browser",
"ui:options": {
"mode": "directory",
"title": "Select Records Directory"
},
"ui:help": "💾 Directory for CSV files. Relative paths based on app directory, absolute paths (C:\\folder) used as-is."
},
"rotation_enabled": {
"ui:column": 3,
@ -69,8 +75,14 @@
},
"symbols_path": {
"ui:column": 12,
"ui:widget": "file-path",
"ui:placeholder": "Select ASC symbol file..."
"ui:widget": "path-browser",
"ui:placeholder": "Select ASC symbol file...",
"ui:options": {
"mode": "file",
"title": "Select ASC Symbol File",
"filetypes": [["ASC Files", "*.asc"], ["All Files", "*.*"]]
},
"ui:help": "📁 Select the ASC symbol file from TIA Portal export. Use Load Symbols button to process."
},
"ui:column": 12,
"ui:layout": [

View File

@ -23,7 +23,20 @@ def resource_path(relative_path):
def external_path(relative_path):
"""Get path external to PyInstaller bundle (for records, logs, etc.)"""
"""Get path external to PyInstaller bundle (for records, logs, etc.)
Handles both absolute and relative paths:
- If path starts with drive letter (Windows) or / (Unix), treat as absolute
- Otherwise treat as relative to executable/script directory
"""
if not relative_path:
return relative_path
# Check if path is absolute
if os.path.isabs(relative_path):
return relative_path
# Handle relative paths
if getattr(sys, "frozen", False):
# Running as PyInstaller executable - use directory next to exe
executable_dir = os.path.dirname(sys.executable)
@ -58,7 +71,12 @@ class ConfigManager:
self.state_file = external_path("system_state.json")
# Default configurations
self.plc_config = {"ip": "192.168.1.100", "rack": 0, "slot": 2}
self.plc_config = {
"ip": "192.168.1.100",
"rack": 0,
"slot": 2,
"symbols_path": "",
}
self.udp_config = {"host": "127.0.0.1", "port": 9870, "sampling_interval": 1.0}
self.sampling_interval = 0.1 # Legacy fallback
@ -435,10 +453,22 @@ class ConfigManager:
return external_path(base)
# PLC Configuration Methods
def update_plc_config(self, ip: str, rack: int, slot: int):
def update_plc_config(
self, ip: str, rack: int, slot: int, symbols_path: str = None
):
"""Update PLC configuration"""
old_config = self.plc_config.copy()
self.plc_config = {"ip": ip, "rack": rack, "slot": slot}
# Preserve existing symbols_path if not provided
if symbols_path is None:
symbols_path = self.plc_config.get("symbols_path", "")
self.plc_config = {
"ip": ip,
"rack": rack,
"slot": slot,
"symbols_path": symbols_path,
}
self.save_configuration()
return {"old_config": old_config, "new_config": self.plc_config}

View File

@ -359,9 +359,13 @@ class PLCDataStreamer:
)
# Configuration Methods
def update_plc_config(self, ip: str, rack: int, slot: int):
def update_plc_config(
self, ip: str, rack: int, slot: int, symbols_path: str = None
):
"""Update PLC configuration"""
config_details = self.config_manager.update_plc_config(ip, rack, slot)
config_details = self.config_manager.update_plc_config(
ip, rack, slot, symbols_path
)
self.event_logger.log_event(
"info",
"config_change",

View File

@ -230,6 +230,10 @@ class ConfigSchemaManager:
"slot", self.config_manager.plc_config.get("slot", 2)
)
),
plc_cfg.get(
"symbols_path",
self.config_manager.plc_config.get("symbols_path", ""),
),
)
if udp_cfg:
self.config_manager.update_udp_config(

View File

@ -13,9 +13,11 @@ import {
AlertIcon,
useColorModeValue,
Badge,
IconButton
IconButton,
useToast
} from '@chakra-ui/react'
import { EditIcon } from '@chakra-ui/icons'
import { FiUpload } from 'react-icons/fi'
import Form from '@rjsf/chakra-ui'
import validator from '@rjsf/validator-ajv8'
import LayoutObjectFieldTemplate from './rjsf/LayoutObjectFieldTemplate.jsx'
@ -35,10 +37,12 @@ export default function PLCConfigManager() {
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [editing, setEditing] = useState(false)
const [loadingSymbols, setLoadingSymbols] = useState(false)
const [message, setMessage] = useState('')
const muted = useColorModeValue('gray.600', 'gray.300')
const borderColor = useColorModeValue('gray.200', 'gray.600')
const toast = useToast()
useEffect(() => {
loadData()
@ -97,6 +101,61 @@ export default function PLCConfigManager() {
setCurrentData(formData)
}
const handleLoadSymbols = async () => {
const symbolsPath = currentData?.plc_config?.symbols_path
if (!symbolsPath) {
toast({
title: 'No File Selected',
description: 'Please select an ASC file first',
status: 'warning',
duration: 3000,
isClosable: true,
})
return
}
try {
setLoadingSymbols(true)
const response = await fetch('/api/symbols/load', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
asc_file_path: symbolsPath
})
})
const data = await response.json()
if (data.success) {
toast({
title: 'Symbols Loaded',
description: `Successfully loaded ${data.symbols_count} symbols`,
status: 'success',
duration: 3000,
isClosable: true,
})
setMessage(`✅ Successfully loaded ${data.symbols_count} symbols from ASC file`)
} else {
throw new Error(data.error || 'Failed to load symbols')
}
} catch (error) {
toast({
title: 'Error',
description: `Failed to load symbols: ${error.message}`,
status: 'error',
duration: 5000,
isClosable: true,
})
setMessage(`❌ Error loading symbols: ${error.message}`)
} finally {
setLoadingSymbols(false)
}
}
if (loading) {
return <Text>Loading PLC configuration...</Text>
}
@ -156,6 +215,18 @@ export default function PLCConfigManager() {
onClick={handleEdit}
/>
)}
{/* Load Symbols button - always available when symbols_path is set */}
<Button
leftIcon={<FiUpload />}
size="sm"
colorScheme="green"
variant="outline"
onClick={handleLoadSymbols}
isLoading={loadingSymbols}
isDisabled={!currentData?.plc_config?.symbols_path || loadingSymbols}
>
Load Symbols
</Button>
</HStack>
</HStack>
<Text fontSize="sm" color={muted}>

View File

@ -2,6 +2,8 @@ import { customWidgets } from './CustomWidgets'
import { widgets } from '../rjsf/widgets'
import VariableSelectorWidget from '../rjsf/VariableSelectorWidget'
import FilePathWidget from './FilePathWidget'
import SimpleFilePathWidget from './SimpleFilePathWidget'
import PathBrowserWidget from './PathBrowserWidget'
import SymbolSelectorWidget from './SymbolSelectorWidget'
import DatasetVariableSymbolWidget from './DatasetVariableSymbolWidget'
@ -27,11 +29,21 @@ export const allWidgets = {
'variable-selector': VariableSelectorWidget,
VariableSelectorWidget: VariableSelectorWidget,
// File path widget for ASC symbol files
// File path widget for ASC symbol files (with symbol loading)
filePath: FilePathWidget,
'file-path': FilePathWidget,
FilePathWidget: FilePathWidget,
// Simple file path widget (just browse, no extra actions)
simpleFilePath: SimpleFilePathWidget,
'simple-file-path': SimpleFilePathWidget,
SimpleFilePathWidget: SimpleFilePathWidget,
// Generic path browser widget for files and directories
pathBrowser: PathBrowserWidget,
'path-browser': PathBrowserWidget,
PathBrowserWidget: PathBrowserWidget,
// Symbol selector widget for PLC symbols
symbolSelector: SymbolSelectorWidget,
'symbol-selector': SymbolSelectorWidget,

View File

@ -0,0 +1,69 @@
import { customWidgets } from './CustomWidgets'
import { widgets } from '../rjsf/widgets'
import VariableSelectorWidget from '../rjsf/VariableSelectorWidget'
import FilePathWidget from './FilePathWidget'
import SimpleFilePathWidget from './SimpleFilePathWidget'
import PathBrowserWidget from './PathBrowserWidget'
import SymbolSelectorWidget from './SymbolSelectorWidget'
import DatasetVariableSymbolWidget from './DatasetVariableSymbolWidget'
// Comprehensive widget collection that merges all available widgets
// for full UI schema support with layouts
export const allWidgets = {
// Custom application-specific widgets
...customWidgets,
// Enhanced RJSF widgets with proper styling
...widgets,
// Additional widget aliases for UI schema compatibility
updown: widgets.UpDownWidget,
text: widgets.TextWidget,
textarea: widgets.TextareaWidget,
select: widgets.SelectWidget,
checkbox: widgets.CheckboxWidget,
switch: widgets.SwitchWidget,
// Variable selector aliases - use the advanced version with search and metadata
variableSelector: VariableSelectorWidget,
'variable-selector': VariableSelectorWidget,
VariableSelectorWidget: VariableSelectorWidget,
// File path widget for ASC symbol files (with symbol loading)
filePath: FilePathWidget,
'file-path': FilePathWidget,
FilePathWidget: FilePathWidget,
// Simple file path widget (just browse, no extra actions)
simpleFilePath: SimpleFilePathWidget,
'simple-file-path': SimpleFilePathWidget,
SimpleFilePathWidget: SimpleFilePathWidget,
// Generic path browser widget for files and directories
pathBrowser: PathBrowserWidget,
'path-browser': PathBrowserWidget,
PathBrowserWidget: PathBrowserWidget,
// Directory browser widget alias
directoryBrowser: PathBrowserWidget,
'directory-browser': PathBrowserWidget,
// Symbol selector widget for PLC symbols
symbolSelector: SymbolSelectorWidget,
'symbol-selector': SymbolSelectorWidget,
SymbolSelectorWidget: SymbolSelectorWidget,
// Dataset variable symbol widget with auto-fill
datasetVariableSymbol: DatasetVariableSymbolWidget,
'dataset-variable-symbol': DatasetVariableSymbolWidget,
DatasetVariableSymbolWidget: DatasetVariableSymbolWidget,
// PLC-specific widget aliases (if available)
plcArea: widgets.PlcAreaWidget,
plcDataType: widgets.PlcDataTypeWidget,
plcNumber: widgets.PlcNumberWidget,
plcStreaming: widgets.PlcStreamingWidget,
plcVariableName: widgets.PlcVariableNameWidget,
}
export default allWidgets

View File

@ -0,0 +1,23 @@
import React from 'react'
import PathBrowserWidget from './PathBrowserWidget'
/**
* Directory browser widget - preconfigured PathBrowserWidget for directories
*/
const DirectoryBrowserWidget = (props) => {
const directoryOptions = {
mode: 'directory',
title: 'Select Directory',
helpText: 'Relative paths are based on application directory. Absolute paths (C:\\folder) are used as-is.',
showPathInfo: true
}
return (
<PathBrowserWidget
{...props}
options={{ ...directoryOptions, ...props.options }}
/>
)
}
export default DirectoryBrowserWidget

View File

@ -0,0 +1,165 @@
import React, { useState } from 'react'
import {
Box,
Button,
Input,
HStack,
Text,
useToast,
Icon,
Tooltip
} from '@chakra-ui/react'
import { FiFolder, FiFile } from 'react-icons/fi'
/**
* Generic path browser widget for files and directories
* Supports both absolute and relative paths
* Can be configured for file or directory selection via schema options
*/
const PathBrowserWidget = ({
value,
onChange,
label,
disabled,
readonly,
required,
placeholder,
schema = {},
uiSchema = {}
}) => {
const [isLoading, setIsLoading] = useState(false)
const toast = useToast()
// Configuration options from schema or uiSchema
const schemaOptions = schema.options || {}
const uiOptions = uiSchema['ui:options'] || {}
const options = { ...schemaOptions, ...uiOptions }
const {
mode = 'file', // 'file' or 'directory'
title = mode === 'file' ? 'Select File' : 'Select Directory',
filetypes = mode === 'file' ? [['All Files', '*.*']] : undefined,
showPathInfo = true
} = options
const helpText = uiSchema['ui:help'] || schema.description
const handleBrowse = async () => {
try {
setIsLoading(true)
const endpoint = mode === 'file' ? '/api/utils/browse-file' : '/api/utils/browse-directory'
const body = mode === 'file'
? { title, filetypes }
: { title }
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
})
const data = await response.json()
const pathKey = mode === 'file' ? 'file_path' : 'directory_path'
if (data.success && data[pathKey]) {
onChange(data[pathKey])
toast({
title: `${mode === 'file' ? 'File' : 'Directory'} Selected`,
description: `Selected: ${data[pathKey]}`,
status: 'success',
duration: 3000,
isClosable: true,
})
} else if (data.cancelled) {
// User cancelled - no action needed
} else {
throw new Error(data.error || `Failed to select ${mode}`)
}
} catch (error) {
toast({
title: 'Error',
description: `Failed to browse ${mode}: ${error.message}`,
status: 'error',
duration: 5000,
isClosable: true,
})
} finally {
setIsLoading(false)
}
}
const getPathInfo = (path) => {
if (!path) return null
// Check if path is absolute (starts with drive letter on Windows or / on Unix)
const isAbsolute = /^([a-zA-Z]:|\/)/.test(path)
const pathType = isAbsolute ? 'Absolute' : 'Relative'
const fileName = path.split(/[\\\/]/).pop()
return { isAbsolute, pathType, fileName }
}
const pathInfo = getPathInfo(value)
return (
<Box>
{label && (
<Text fontSize="sm" fontWeight="medium" mb={2}>
{label} {required && <Text as="span" color="red.500">*</Text>}
</Text>
)}
<HStack spacing={2} mb={showPathInfo && pathInfo ? 2 : 0}>
<Input
value={value || ''}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder || `Enter ${mode} path or browse...`}
disabled={disabled}
readOnly={readonly}
flex={1}
/>
<Tooltip label={`Browse for ${mode}`}>
<Button
leftIcon={<Icon as={mode === 'file' ? FiFile : FiFolder} />}
onClick={handleBrowse}
isLoading={isLoading}
disabled={disabled || readonly}
variant="outline"
size="md"
>
Browse
</Button>
</Tooltip>
</HStack>
{/* Path information display */}
{showPathInfo && pathInfo && (
<Box>
<HStack spacing={4} fontSize="xs" color="gray.500">
<Text>
<Icon as={mode === 'file' ? FiFile : FiFolder} mr={1} />
{pathInfo.fileName}
</Text>
<Text>
Path Type: <Text as="span" fontWeight="medium">{pathInfo.pathType}</Text>
</Text>
</HStack>
</Box>
)}
{/* Help text */}
{helpText && (
<Text fontSize="xs" color="gray.500" mt={1}>
{helpText}
</Text>
)}
</Box>
)
}
export default PathBrowserWidget

View File

@ -0,0 +1,165 @@
import React, { useState } from 'react'
import {
Box,
Button,
Input,
HStack,
Text,
useToast,
Icon,
Tooltip
} from '@chakra-ui/react'
import { FiFolder, FiFile } from 'react-icons/fi'
/**
* Generic path browser widget for files and directories
* Supports both absolute and relative paths
* Can be configured for file or directory selection via schema options
*/
const PathBrowserWidget = ({
value,
onChange,
label,
disabled,
readonly,
required,
placeholder,
schema = {},
uiSchema = {}
}) => {
const [isLoading, setIsLoading] = useState(false)
const toast = useToast()
// Configuration options from schema or uiSchema
const schemaOptions = schema.options || {}
const uiOptions = uiSchema['ui:options'] || {}
const options = { ...schemaOptions, ...uiOptions }
const {
mode = 'file', // 'file' or 'directory'
title = mode === 'file' ? 'Select File' : 'Select Directory',
filetypes = mode === 'file' ? [['All Files', '*.*']] : undefined,
showPathInfo = true
} = options
const helpText = uiSchema['ui:help'] || schema.description
const handleBrowse = async () => {
try {
setIsLoading(true)
const endpoint = mode === 'file' ? '/api/utils/browse-file' : '/api/utils/browse-directory'
const body = mode === 'file'
? { title, filetypes }
: { title }
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body)
})
const data = await response.json()
const pathKey = mode === 'file' ? 'file_path' : 'directory_path'
if (data.success && data[pathKey]) {
onChange(data[pathKey])
toast({
title: `${mode === 'file' ? 'File' : 'Directory'} Selected`,
description: `Selected: ${data[pathKey]}`,
status: 'success',
duration: 3000,
isClosable: true,
})
} else if (data.cancelled) {
// User cancelled - no action needed
} else {
throw new Error(data.error || `Failed to select ${mode}`)
}
} catch (error) {
toast({
title: 'Error',
description: `Failed to browse ${mode}: ${error.message}`,
status: 'error',
duration: 5000,
isClosable: true,
})
} finally {
setIsLoading(false)
}
}
const getPathInfo = (path) => {
if (!path) return null
// Check if path is absolute (starts with drive letter on Windows or / on Unix)
const isAbsolute = /^([a-zA-Z]:|\/)/.test(path)
const pathType = isAbsolute ? 'Absolute' : 'Relative'
const fileName = path.split(/[\\\/]/).pop()
return { isAbsolute, pathType, fileName }
}
const pathInfo = getPathInfo(value)
return (
<Box>
{label && (
<Text fontSize="sm" fontWeight="medium" mb={2}>
{label} {required && <Text as="span" color="red.500">*</Text>}
</Text>
)}
<HStack spacing={2} mb={showPathInfo && pathInfo ? 2 : 0}>
<Input
value={value || ''}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder || `Enter ${mode} path or browse...`}
disabled={disabled}
readOnly={readonly}
flex={1}
/>
<Tooltip label={`Browse for ${mode}`}>
<Button
leftIcon={<Icon as={mode === 'file' ? FiFile : FiFolder} />}
onClick={handleBrowse}
isLoading={isLoading}
disabled={disabled || readonly}
variant="outline"
size="md"
>
Browse
</Button>
</Tooltip>
</HStack>
{/* Path information display */}
{showPathInfo && pathInfo && (
<Box>
<HStack spacing={4} fontSize="xs" color="gray.500">
<Text>
<Icon as={mode === 'file' ? FiFile : FiFolder} mr={1} />
{pathInfo.fileName}
</Text>
<Text>
Path Type: <Text as="span" fontWeight="medium">{pathInfo.pathType}</Text>
</Text>
</HStack>
</Box>
)}
{/* Help text */}
{helpText && (
<Text fontSize="xs" color="gray.500" mt={1}>
{helpText}
</Text>
)}
</Box>
)
}
export default PathBrowserWidget

View File

@ -0,0 +1,159 @@
import React, { useState } from 'react'
import {
Box,
Button,
Input,
HStack,
Text,
useToast,
Icon,
Tooltip
} from '@chakra-ui/react'
import { FiFolder, FiFile } from 'react-icons/fi'
/**
* Simple file path widget with browse functionality
* Simplified version without specific actions like symbol loading
*/
const SimpleFilePathWidget = ({
value,
onChange,
label,
disabled,
readonly,
required,
placeholder,
schema = {},
uiSchema = {}
}) => {
const [isLoading, setIsLoading] = useState(false)
const toast = useToast()
// Configuration options from schema
const schemaOptions = schema.options || {}
const uiOptions = uiSchema['ui:options'] || {}
const options = { ...schemaOptions, ...uiOptions }
const {
title = 'Select File',
filetypes = [['All Files', '*.*']],
showPathInfo = true
} = options
const helpText = uiSchema['ui:help'] || schema.description
const handleBrowseFile = async () => {
try {
setIsLoading(true)
const response = await fetch('/api/utils/browse-file', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
filetypes
})
})
const data = await response.json()
if (data.success && data.file_path) {
onChange(data.file_path)
toast({
title: 'File Selected',
description: `Selected: ${data.file_path}`,
status: 'success',
duration: 3000,
isClosable: true,
})
} else if (data.cancelled) {
// User cancelled - no action needed
} else {
throw new Error(data.error || 'Failed to select file')
}
} catch (error) {
toast({
title: 'Error',
description: `Failed to browse file: ${error.message}`,
status: 'error',
duration: 5000,
isClosable: true,
})
} finally {
setIsLoading(false)
}
}
const getPathInfo = (path) => {
if (!path) return null
// Check if path is absolute (starts with drive letter on Windows or / on Unix)
const isAbsolute = /^([a-zA-Z]:|\/)/.test(path)
const pathType = isAbsolute ? 'Absolute' : 'Relative'
const fileName = path.split(/[\\\/]/).pop()
return { isAbsolute, pathType, fileName }
}
const pathInfo = getPathInfo(value)
return (
<Box>
{label && (
<Text fontSize="sm" fontWeight="medium" mb={2}>
{label} {required && <Text as="span" color="red.500">*</Text>}
</Text>
)}
<HStack spacing={2} mb={showPathInfo && pathInfo ? 2 : 0}>
<Input
value={value || ''}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder || 'Enter file path or browse...'}
disabled={disabled}
readOnly={readonly}
flex={1}
/>
<Tooltip label="Browse for file">
<Button
leftIcon={<Icon as={FiFolder} />}
onClick={handleBrowseFile}
isLoading={isLoading}
disabled={disabled || readonly}
variant="outline"
size="md"
>
Browse
</Button>
</Tooltip>
</HStack>
{/* Path information display */}
{showPathInfo && pathInfo && (
<Box>
<HStack spacing={4} fontSize="xs" color="gray.500">
<Text>
<Icon as={FiFile} mr={1} />
{pathInfo.fileName}
</Text>
<Text>
Path Type: <Text as="span" fontWeight="medium">{pathInfo.pathType}</Text>
</Text>
</HStack>
</Box>
)}
{/* Help text */}
{helpText && (
<Text fontSize="xs" color="gray.500" mt={1}>
{helpText}
</Text>
)}
</Box>
)
}
export default SimpleFilePathWidget

View File

@ -54,6 +54,7 @@ import {
ModalCloseButton
} from '@chakra-ui/react'
import { ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons'
import { FiUpload } from 'react-icons/fi'
import Form from '@rjsf/chakra-ui'
import validator from '@rjsf/validator-ajv8'
import PlotManager from '../components/PlotManager'
@ -1064,6 +1065,7 @@ function ConfigurationPanel({ schemaData, formData, onFormChange, onSave, saving
const cardBg = useColorModeValue('white', 'gray.700')
const borderColor = useColorModeValue('gray.200', 'gray.600')
const toast = useToast()
const [loadingSymbols, setLoadingSymbols] = useState(false)
const handleImportConfig = (importedData) => {
onFormChange(importedData)
@ -1075,6 +1077,59 @@ function ConfigurationPanel({ schemaData, formData, onFormChange, onSave, saving
})
}
const handleLoadSymbols = async () => {
const symbolsPath = formData?.plc_config?.symbols_path
if (!symbolsPath) {
toast({
title: 'No File Selected',
description: 'Please select an ASC file first in the configuration',
status: 'warning',
duration: 3000,
isClosable: true,
})
return
}
try {
setLoadingSymbols(true)
const response = await fetch('/api/symbols/load', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
asc_file_path: symbolsPath
})
})
const data = await response.json()
if (data.success) {
toast({
title: 'Symbols Loaded Successfully',
description: `Successfully loaded ${data.symbols_count} symbols from ASC file`,
status: 'success',
duration: 4000,
isClosable: true,
})
} else {
throw new Error(data.error || 'Failed to load symbols')
}
} catch (error) {
toast({
title: 'Error Loading Symbols',
description: `Failed to load symbols: ${error.message}`,
status: 'error',
duration: 5000,
isClosable: true,
})
} finally {
setLoadingSymbols(false)
}
}
if (!schemaData?.schema || !formData) {
return (
<Card bg={cardBg} borderColor={borderColor}>
@ -1095,12 +1150,27 @@ function ConfigurationPanel({ schemaData, formData, onFormChange, onSave, saving
{t('config.subtitle')}
</Text>
</Box>
<ImportExportButtons
data={formData}
onImport={handleImportConfig}
exportFilename="plc_config.json"
configType="object"
/>
<HStack spacing={3}>
<ImportExportButtons
data={formData}
onImport={handleImportConfig}
exportFilename="plc_config.json"
configType="object"
/>
{/* Load Symbols button */}
<Button
leftIcon={<FiUpload />}
size="sm"
colorScheme="green"
variant="outline"
onClick={handleLoadSymbols}
isLoading={loadingSymbols}
isDisabled={!formData?.plc_config?.symbols_path || loadingSymbols}
title="Load symbols from the configured ASC file"
>
🔄 Load Symbols
</Button>
</HStack>
</Flex>
{message && (
<Alert status="success" mt={2}>

54
main.py
View File

@ -475,8 +475,9 @@ def update_plc_config():
ip = data.get("ip", "10.1.33.11")
rack = int(data.get("rack", 0))
slot = int(data.get("slot", 2))
symbols_path = data.get("symbols_path", "")
streamer.update_plc_config(ip, rack, slot)
streamer.update_plc_config(ip, rack, slot, symbols_path)
return jsonify({"success": True, "message": "PLC configuration updated"})
except Exception as e:
@ -3548,6 +3549,43 @@ def browse_file():
return jsonify({"success": False, "error": str(e)}), 500
@app.route("/api/utils/browse-directory", methods=["POST"])
def browse_directory():
"""Open directory dialog to browse for directories."""
try:
if not TKINTER_AVAILABLE:
return (
jsonify(
{
"success": False,
"error": "Directory browser not available. Please enter the directory path manually.",
}
),
400,
)
data = request.get_json()
title = data.get("title", "Select Directory")
# Create a temporary tkinter root window
root = tk.Tk()
root.withdraw() # Hide the root window
root.attributes("-topmost", True) # Bring to front
# Open directory dialog
directory_path = filedialog.askdirectory(title=title)
root.destroy() # Clean up
if directory_path:
return jsonify({"success": True, "directory_path": directory_path})
else:
return jsonify({"success": True, "cancelled": True})
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500
@app.route("/api/symbols/load", methods=["POST"])
def load_symbols():
"""Load symbols from ASC file and save to JSON."""
@ -3555,12 +3593,26 @@ def load_symbols():
data = request.get_json()
asc_file_path = data.get("asc_file_path")
# If no explicit path provided, try to get from plc_config
if not asc_file_path and streamer:
symbols_path = streamer.config_manager.plc_config.get("symbols_path", "")
if symbols_path:
# Handle absolute vs relative paths
if os.path.isabs(symbols_path):
asc_file_path = symbols_path
else:
asc_file_path = external_path(symbols_path)
if not asc_file_path:
return (
jsonify({"success": False, "error": "ASC file path is required"}),
400,
)
# Handle absolute vs relative paths for provided asc_file_path
if not os.path.isabs(asc_file_path):
asc_file_path = external_path(asc_file_path)
if not os.path.exists(asc_file_path):
return (
jsonify(

View File

@ -1,12 +1,12 @@
{
"last_state": {
"should_connect": true,
"should_stream": true,
"should_stream": false,
"active_datasets": [
"DAR"
]
},
"auto_recovery_enabled": true,
"last_update": "2025-08-25T18:40:24.478882",
"last_update": "2025-08-27T09:24:15.915232",
"plotjuggler_path": "C:\\Program Files\\PlotJuggler\\plotjuggler.exe"
}