From 4ced4f684eafa1bc45bbb06398ae325af847da3b Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Wed, 18 Feb 2026 09:38:46 -0500 Subject: [PATCH 1/3] fix: show online debug values for structured data types and FB instances (#591) Fix three issues preventing debug values from displaying for struct and FB instance variables in textual (ST/IL) programs: 1. Extend variable reclassification to all POUs on project load, not just graphical (LD/FBD). The text parser lacks project context to correctly classify FB instances as 'derived', so re-parsing with full context is needed for ST/IL programs too. 2. Expand top-level user-data-type variables in debug polling so structs and any unresolved FB instances get their leaf fields registered for value polling. 3. Use fallback path resolution for struct fields in the debug tree builder. The xml2st compiler converts structs to FBs, so debug.c may use FB-style paths (no .value.) instead of struct-style paths (.value.). Try both. Co-Authored-By: Claude Opus 4.6 --- src/renderer/screens/workspace-screen.tsx | 55 +++++++++++++++++++++++ src/renderer/store/slices/shared/index.ts | 28 ++++++++---- src/utils/debug-tree-traversal.ts | 30 ++++++++++--- 3 files changed, 100 insertions(+), 13 deletions(-) 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..842cb6b6d 100644 --- a/src/renderer/store/slices/shared/index.ts +++ b/src/renderer/store/slices/shared/index.ts @@ -1220,26 +1220,38 @@ 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 + libraries: reclassLibraries, + } = reclassState - graphicalPous.forEach((pou) => { + pous.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) + const reparsedVariables: PLCVariable[] = parseIecStringToVariables( + iecString, + pous, + reclassDataTypes, + reclassLibraries, + ) getState().projectActions.setPouVariables({ pouName: pou.data.name, variables: reparsedVariables, }) }) + } + // 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, From 474adc70b875f6214144fbd568a9548fcc30b8ae Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Wed, 18 Feb 2026 09:54:15 -0500 Subject: [PATCH 2/3] fix: enable Force Value context menu for struct field nodes in debug tree The `canForceVariable` function used nullish coalescing (`??`) instead of logical OR (`||`) for its fallback checks. Since `Map.has()` returns a boolean (not null/undefined), `false ?? nextCheck` returns `false` without evaluating `nextCheck`. This prevented the compositeKey lookup from ever running when the fullPath lookup failed. For struct field nodes in the debug tree, `debugIndex` may not be set by the tree builder, so the function needs the compositeKey fallback to find the variable in `debugVariableIndexes` (populated by the polling setup). Co-Authored-By: Claude Opus 4.6 --- src/renderer/components/_molecules/variables-panel/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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], ) From 613906d465d624df14bb466c103adbcb1a8f4f4f Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Wed, 18 Feb 2026 09:58:23 -0500 Subject: [PATCH 3/3] fix: add try/catch around per-POU variable reclassification Wrap the reclassification loop in a try/catch so that a single malformed POU doesn't abort the entire project load. If reclassification fails for one POU, it keeps its original type classifications and the rest of the project loads normally. Co-Authored-By: Claude Opus 4.6 --- src/renderer/store/slices/shared/index.ts | 28 +++++++++++++---------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/renderer/store/slices/shared/index.ts b/src/renderer/store/slices/shared/index.ts index 842cb6b6d..d6f25ce45 100644 --- a/src/renderer/store/slices/shared/index.ts +++ b/src/renderer/store/slices/shared/index.ts @@ -1234,18 +1234,22 @@ export const createSharedSlice: StateCreator< } = reclassState pous.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, - reclassDataTypes, - reclassLibraries, - ) - getState().projectActions.setPouVariables({ - pouName: pou.data.name, - variables: reparsedVariables, - }) + 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) + } }) }