diff --git a/src/renderer/components/_molecules/variables-panel/index.tsx b/src/renderer/components/_molecules/variables-panel/index.tsx index a1a6282fb..72a8cdaff 100644 --- a/src/renderer/components/_molecules/variables-panel/index.tsx +++ b/src/renderer/components/_molecules/variables-panel/index.tsx @@ -127,7 +127,7 @@ const VariablesPanel = ({ (node: DebugTreeNode) => { if (!isDebuggerVisible || node.isComplex) return false if (node.debugIndex !== undefined) return true - return debugVariableIndexes?.has(node.fullPath) ?? debugVariableIndexes?.has(node.compositeKey) ?? false + return debugVariableIndexes?.has(node.fullPath) || debugVariableIndexes?.has(node.compositeKey) || false }, [isDebuggerVisible, debugVariableIndexes], ) diff --git a/src/renderer/screens/workspace-screen.tsx b/src/renderer/screens/workspace-screen.tsx index 6ac84c8d6..b3b7295b3 100644 --- a/src/renderer/screens/workspace-screen.tsx +++ b/src/renderer/screens/workspace-screen.tsx @@ -571,6 +571,61 @@ const WorkspaceScreen = () => { } }) + // Process top-level user-data-type variables (structs and any unresolved FBs) + const userDataTypeVars = pou.data.variables.filter((variable) => variable.type.definition === 'user-data-type') + userDataTypeVars.forEach((udtVar) => { + const typeNameUpper = udtVar.type.value.toUpperCase() + + const isStandardFB = StandardFunctionBlocks.pous.some( + (fb: { name: string; type: string }) => + fb.name.toUpperCase() === typeNameUpper && fb.type.toLowerCase().replace(/[-_]/g, '') === 'functionblock', + ) + const isCustomFB = project.data.pous.some( + (p) => p.type === 'function-block' && p.data.name.toUpperCase() === typeNameUpper, + ) + + let variablesToProcess: + | Array<{ name: string; class: string; type: { definition: string; value: string } }> + | undefined + + if (isStandardFB || isCustomFB) { + const standardFB = StandardFunctionBlocks.pous.find( + (fb: { name: string }) => fb.name.toUpperCase() === typeNameUpper, + ) + if (standardFB) { + variablesToProcess = ensureEnoVariable(standardFB.variables) + } else { + const customFB = project.data.pous.find( + (p) => p.type === 'function-block' && p.data.name.toUpperCase() === typeNameUpper, + ) + if (customFB && customFB.type === 'function-block') { + variablesToProcess = ensureEnoVariable( + customFB.data.variables as Array<{ + name: string + class: string + type: { definition: string; value: string } + }>, + ) + } + } + } else { + const structType = project.data.dataTypes.find((dt) => dt.name.toUpperCase() === typeNameUpper) + if (structType && structType.derivation === 'structure') { + variablesToProcess = structType.variable.map((field) => ({ + name: field.name, + class: 'local' as const, + type: { definition: field.type.definition, value: field.type.value }, + })) + } + } + + if (variablesToProcess) { + const debugPathPrefix = buildDebugPath(programInstance.name, udtVar.name) + const variableNamePrefix = udtVar.name + processNestedVariables(variablesToProcess, pou.data.name, debugPathPrefix, variableNamePrefix) + } + }) + if (pou.data.body.language === 'ld') { const currentLadderFlow = ladderFlows.find((flow) => flow.name === pou.data.name) if (currentLadderFlow) { diff --git a/src/renderer/store/slices/shared/index.ts b/src/renderer/store/slices/shared/index.ts index 098a580af..d6f25ce45 100644 --- a/src/renderer/store/slices/shared/index.ts +++ b/src/renderer/store/slices/shared/index.ts @@ -1220,26 +1220,42 @@ export const createSharedSlice: StateCreator< pous.map((pou) => pou.type !== 'program' && getState().libraryActions.addLibrary(pou.data.name, pou.type)) - const graphicalPous = [...ladderPous, ...fbdPous] - if (graphicalPous.length) { - const state = getState() + // Reclassify ALL POUs' variables with full context. + // The text parser can't determine type definitions accurately since it doesn't have + // the full project context. Re-parse with pous, dataTypes, and libraries to correctly + // classify FB instances as 'derived' vs structs as 'user-data-type'. + { + const reclassState = getState() const { project: { - data: { dataTypes }, + data: { dataTypes: reclassDataTypes }, }, - libraries, - } = state - - graphicalPous.forEach((pou) => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any - const iecString = generateIecVariablesToString(pou.data.variables as any) - const reparsedVariables: PLCVariable[] = parseIecStringToVariables(iecString, pous, dataTypes, libraries) - getState().projectActions.setPouVariables({ - pouName: pou.data.name, - variables: reparsedVariables, - }) + libraries: reclassLibraries, + } = reclassState + + pous.forEach((pou) => { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any + const iecString = generateIecVariablesToString(pou.data.variables as any) + const reparsedVariables: PLCVariable[] = parseIecStringToVariables( + iecString, + pous, + reclassDataTypes, + reclassLibraries, + ) + getState().projectActions.setPouVariables({ + pouName: pou.data.name, + variables: reparsedVariables, + }) + } catch (err) { + console.error(`[Reclassify] Failed to reclassify variables for POU "${pou.data.name}":`, err) + } }) + } + // Sync graphical POU nodes with reclassified variables + const graphicalPous = [...ladderPous, ...fbdPous] + if (graphicalPous.length) { const freshState = getState() const freshLadderFlows = freshState.ladderFlows const freshFBDFlows = freshState.fbdFlows diff --git a/src/utils/debug-tree-traversal.ts b/src/utils/debug-tree-traversal.ts index 11b31ba03..0c5257951 100644 --- a/src/utils/debug-tree-traversal.ts +++ b/src/utils/debug-tree-traversal.ts @@ -208,22 +208,32 @@ function traverseNestedNode( const children: T[] = [] for (const field of structVariables) { - // Structure fields use .value. prefix - const fieldFullPath = `${fullPath}.value.${field.name.toUpperCase()}` const fieldCompositeKey = `${compositeKey}.${field.name}` if (field.type.definition === 'base-type') { - const debugVar = findDebugVariable(debugVariables, fieldFullPath) + // Use fallback to try both struct-style (.value.) and FB-style paths. + // The xml2st compiler converts structs to FBs, so debug.c may use either path style. + const result = findDebugVariableForField(debugVariables, fullPath, field.name) children.push( visitor.visitLeaf( field.name, - fieldFullPath, + result.matchedPath, fieldCompositeKey, field.type.value.toUpperCase(), - debugVar?.index, + result.match?.index, ), ) } else if (field.type.definition === 'user-data-type') { + // Try FB-style path first (compiler may have converted struct to FB) + const fbStylePath = `${fullPath}.${field.name.toUpperCase()}` + const structStylePath = `${fullPath}.value.${field.name.toUpperCase()}` + const hasFbMatch = debugVariables.some( + (dv) => + dv.name.toUpperCase().startsWith(fbStylePath.toUpperCase() + '.') || + dv.name.toUpperCase() === fbStylePath.toUpperCase(), + ) + const fieldFullPath = hasFbMatch ? fbStylePath : structStylePath + const childTypeDef = isFunctionBlock(field.type.value, projectPous) ? 'derived' : 'user-data-type' children.push( traverseNestedNode( @@ -237,6 +247,16 @@ function traverseNestedNode( ), ) } else if (field.type.definition === 'array' && field.type.data) { + // Use fallback path for array fields too + const fbStylePath = `${fullPath}.${field.name.toUpperCase()}` + const structStylePath = `${fullPath}.value.${field.name.toUpperCase()}` + const hasFbMatch = debugVariables.some( + (dv) => + dv.name.toUpperCase().startsWith(fbStylePath.toUpperCase() + '.') || + dv.name.toUpperCase() === fbStylePath.toUpperCase(), + ) + const fieldFullPath = hasFbMatch ? fbStylePath : structStylePath + children.push( traverseNestedNode( field.name,