From a356d2bed46c7b4b0b92e726325b3af595786fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20GS=20Pereira?= Date: Mon, 16 Feb 2026 09:00:56 -0300 Subject: [PATCH 1/4] fix: improve debug forcing for coils, contacts, and variables Use immediate forced values from debugForcedVariables instead of polled values for coils and contacts, eliminating the 200ms delay. Respect negated variant when displaying forced state colors. Fix composite key generation in variable element to support function block instance context. Add debug logging for unresolved variable indexes. Co-Authored-By: Claude Opus 4.6 --- package-lock.json | 9 ------ .../_atoms/graphical-editor/ladder/coil.tsx | 13 ++++++--- .../graphical-editor/ladder/contact.tsx | 13 ++++++--- .../graphical-editor/ladder/variable.tsx | 29 +++++++++++++++---- src/renderer/screens/workspace-screen.tsx | 17 ++++++++++- 5 files changed, 57 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ee3a7545..61ada1ebb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,7 +34,6 @@ "@tanstack/react-table": "^8.10.7", "@xyflow/react": "^12.0.1", "auto-zustand-selectors-hook": "^2.0.0", - "bcryptjs": "^3.0.3", "clsx": "^2.0.0", "cva": "npm:class-variance-authority@^0.7.0", "dompurify": "^3.2.4", @@ -11663,14 +11662,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bcryptjs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-3.0.3.tgz", - "integrity": "sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==", - "bin": { - "bcrypt": "bin/bcrypt" - } - }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", diff --git a/src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx b/src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx index 17f74a21a..aa429eb3c 100644 --- a/src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx +++ b/src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx @@ -164,7 +164,14 @@ export const Coil = (block: CoilProps) => { } const compositeKey = getCompositeKey(data.variable.name) - const value = debugVariableValues.get(compositeKey) + const isForced = debugForcedVariables.has(compositeKey) + + // When forced, use immediate value from debugForcedVariables (no 200ms poll delay) + const value = isForced + ? debugForcedVariables.get(compositeKey) + ? '1' + : '0' + : debugVariableValues.get(compositeKey) if (value === undefined) { return undefined @@ -173,10 +180,8 @@ export const Coil = (block: CoilProps) => { const isTrue = value === '1' || value.toUpperCase() === 'TRUE' const displayState = data.variant === 'negated' ? !isTrue : isTrue - const isForced = debugForcedVariables.has(compositeKey) if (isForced) { - const forcedValue = debugForcedVariables.get(compositeKey) - return forcedValue ? '#80C000' : '#4080FF' + return displayState ? '#80C000' : '#4080FF' } return displayState ? '#00FF00' : '#0464FB' diff --git a/src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx b/src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx index a3991d5fb..4aa98bb27 100644 --- a/src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx +++ b/src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx @@ -131,7 +131,14 @@ export const Contact = (block: ContactProps) => { } const compositeKey = getCompositeKey(data.variable.name) - const value = debugVariableValues.get(compositeKey) + const isForced = debugForcedVariables.has(compositeKey) + + // When forced, use immediate value from debugForcedVariables (no 200ms poll delay) + const value = isForced + ? debugForcedVariables.get(compositeKey) + ? '1' + : '0' + : debugVariableValues.get(compositeKey) if (value === undefined) { return undefined @@ -140,10 +147,8 @@ export const Contact = (block: ContactProps) => { const isTrue = value === '1' || value.toUpperCase() === 'TRUE' const displayState = data.variant === 'negated' ? !isTrue : isTrue - const isForced = debugForcedVariables.has(compositeKey) if (isForced) { - const forcedValue = debugForcedVariables.get(compositeKey) - return forcedValue ? '#80C000' : '#4080FF' + return displayState ? '#80C000' : '#4080FF' } return displayState ? '#00FF00' : '#0464FB' diff --git a/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx b/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx index 76ca9569d..e754ebeb2 100644 --- a/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx +++ b/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx @@ -62,10 +62,27 @@ const VariableElement = (block: VariableProps) => { }, ladderFlows, ladderFlowActions: { updateNode }, - workspace: { isDebuggerVisible, debugVariableIndexes, debugForcedVariables }, + workspace: { isDebuggerVisible, debugVariableIndexes, debugForcedVariables, fbSelectedInstance, fbDebugInstances }, workspaceActions: { setDebugForcedVariables }, } = useOpenPLCStore() + // Helper to get composite key with FB instance context + const getCompositeKey = (variableName: string): string => { + const currentPou = pous.find((p) => p.data.name === editor.meta.name) + if (currentPou?.type === 'function-block') { + const fbTypeKey = currentPou.data.name.toUpperCase() + const selectedKey = fbSelectedInstance.get(fbTypeKey) + if (selectedKey) { + const instances = fbDebugInstances.get(fbTypeKey) || [] + const selectedInstance = instances.find((inst) => inst.key === selectedKey) + if (selectedInstance) { + return `${selectedInstance.programName}:${selectedInstance.fbVariableName}.${variableName}` + } + } + } + return `${editor.meta.name}:${variableName}` + } + const inputVariableRef = useRef< HTMLTextAreaElement & { blur: ({ submit }: { submit?: boolean }) => void @@ -280,7 +297,7 @@ const VariableElement = (block: VariableProps) => { if (!data.variable.name) return - const compositeKey = `${editor.meta.name}:${data.variable.name}` + const compositeKey = getCompositeKey(data.variable.name) const variableIndex = debugVariableIndexes.get(compositeKey) if (variableIndex === undefined) return @@ -302,7 +319,7 @@ const VariableElement = (block: VariableProps) => { if (!data.variable.name) return - const compositeKey = `${editor.meta.name}:${data.variable.name}` + const compositeKey = getCompositeKey(data.variable.name) const variableIndex = debugVariableIndexes.get(compositeKey) if (variableIndex === undefined) return @@ -324,7 +341,7 @@ const VariableElement = (block: VariableProps) => { if (!data.variable.name) return - const compositeKey = `${editor.meta.name}:${data.variable.name}` + const compositeKey = getCompositeKey(data.variable.name) const variableIndex = debugVariableIndexes.get(compositeKey) if (variableIndex === undefined) return @@ -352,7 +369,7 @@ const VariableElement = (block: VariableProps) => { return } - const compositeKey = `${editor.meta.name}:${data.variable.name}` + const compositeKey = getCompositeKey(data.variable.name) const variableIndex = debugVariableIndexes.get(compositeKey) if (variableIndex === undefined) { @@ -446,7 +463,7 @@ const VariableElement = (block: VariableProps) => { const variableType = getVariableType() const isBoolVariable = variableType?.toUpperCase() === 'BOOL' - const compositeKey = `${editor.meta.name}:${data.variable.name}` + const compositeKey = getCompositeKey(data.variable.name) const isForced = debugForcedVariables.has(compositeKey) const forcedValue = debugForcedVariables.get(compositeKey) diff --git a/src/renderer/screens/workspace-screen.tsx b/src/renderer/screens/workspace-screen.tsx index bd8b124dc..6ac84c8d6 100644 --- a/src/renderer/screens/workspace-screen.tsx +++ b/src/renderer/screens/workspace-screen.tsx @@ -285,6 +285,12 @@ const WorkspaceScreen = () => { // Use fallback to try both FB-style and struct-style paths const index = getFieldIndexFromMapWithFallback(debugVariableIndexes, debugPathPrefix, fbVar.name) + if (index === undefined) { + console.warn( + `[Debugger] Could not resolve index for nested variable: ${debugPathPrefix}.${fbVar.name} (POU: ${pouName})`, + ) + } + if (index !== undefined) { const varName = `${variableNamePrefix}.${fbVar.name}` addVariableInfo(index, { @@ -431,6 +437,10 @@ const WorkspaceScreen = () => { const index = debugVariableIndexes.get(compositeKey) if (index !== undefined) { addVariableInfo(index, { pouName: pou.data.name, variable: v }) + } else { + console.warn( + `[Debugger] Could not resolve index for program variable: ${compositeKey} (type: ${v.type.value})`, + ) } }) }) @@ -1528,7 +1538,12 @@ const WorkspaceScreen = () => { ): Promise => { const keyForIndexLookup = lookupKey ?? compositeKey const variableIndex = debugVariableIndexes.get(keyForIndexLookup) - if (variableIndex === undefined) return + if (variableIndex === undefined) { + console.warn( + `[Debugger] Force variable failed: no index found for key "${keyForIndexLookup}" (compositeKey: "${compositeKey}")`, + ) + return + } if (value === undefined && valueBuffer === undefined) { // Release force From 3b3079d3717554a840871949ce3f7e620da12365 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 16 Feb 2026 22:26:30 -0500 Subject: [PATCH 2/4] refactor: extract shared useDebugCompositeKey hook Consolidate 6 duplicated getCompositeKey implementations (4 plain functions in atom components, 2 useCallback-wrapped in molecule components) into a single shared hook with proper memoization. Co-Authored-By: Claude Opus 4.6 --- .../_atoms/graphical-editor/fbd/variable.tsx | 28 ++----------- .../_atoms/graphical-editor/ladder/coil.tsx | 28 ++----------- .../graphical-editor/ladder/contact.tsx | 28 ++----------- .../graphical-editor/ladder/variable.tsx | 21 ++-------- .../_molecules/graphical-editor/fbd/index.tsx | 29 ++------------ .../graphical-editor/ladder/rung/body.tsx | 29 ++------------ src/renderer/hooks/index.ts | 1 + src/renderer/hooks/use-debug-composite-key.ts | 40 +++++++++++++++++++ 8 files changed, 61 insertions(+), 143 deletions(-) create mode 100644 src/renderer/hooks/use-debug-composite-key.ts diff --git a/src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx b/src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx index adb5c44b1..e28b8064e 100644 --- a/src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx +++ b/src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx @@ -1,3 +1,4 @@ +import { useDebugCompositeKey } from '@hooks/use-debug-composite-key' import * as Popover from '@radix-ui/react-popover' import { useOpenPLCStore } from '@root/renderer/store' import { PLCVariable } from '@root/types/PLC' @@ -58,33 +59,10 @@ const VariableElement = (block: VariableProps) => { project: { data: { pous, dataTypes }, }, - workspace: { - isDebuggerVisible, - debugVariableIndexes, - debugVariableValues, - debugForcedVariables, - fbSelectedInstance, - fbDebugInstances, - }, + workspace: { isDebuggerVisible, debugVariableIndexes, debugVariableValues, debugForcedVariables }, workspaceActions: { setDebugForcedVariables }, } = useOpenPLCStore() - - // Helper to get composite key with FB instance context - const getCompositeKey = (variableName: string): string => { - const currentPou = pous.find((p) => p.data.name === editor.meta.name) - if (currentPou?.type === 'function-block') { - const fbTypeKey = currentPou.data.name.toUpperCase() - const selectedKey = fbSelectedInstance.get(fbTypeKey) - if (selectedKey) { - const instances = fbDebugInstances.get(fbTypeKey) || [] - const selectedInstance = instances.find((inst) => inst.key === selectedKey) - if (selectedInstance) { - return `${selectedInstance.programName}:${selectedInstance.fbVariableName}.${variableName}` - } - } - } - return `${editor.meta.name}:${variableName}` - } + const getCompositeKey = useDebugCompositeKey() const inputVariableRef = useRef< HTMLTextAreaElement & { diff --git a/src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx b/src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx index aa429eb3c..68838595d 100644 --- a/src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx +++ b/src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx @@ -1,3 +1,4 @@ +import { useDebugCompositeKey } from '@hooks/use-debug-composite-key' import * as Popover from '@radix-ui/react-popover' import { DefaultCoil, @@ -126,33 +127,10 @@ export const Coil = (block: CoilProps) => { }, ladderFlows, ladderFlowActions: { updateNode }, - workspace: { - isDebuggerVisible, - debugVariableValues, - debugVariableIndexes, - debugForcedVariables, - fbSelectedInstance, - fbDebugInstances, - }, + workspace: { isDebuggerVisible, debugVariableValues, debugVariableIndexes, debugForcedVariables }, workspaceActions: { setDebugForcedVariables }, } = useOpenPLCStore() - - // Helper to get composite key with FB instance context - const getCompositeKey = (variableName: string): string => { - const currentPou = pous.find((p) => p.data.name === editor.meta.name) - if (currentPou?.type === 'function-block') { - const fbTypeKey = currentPou.data.name.toUpperCase() - const selectedKey = fbSelectedInstance.get(fbTypeKey) - if (selectedKey) { - const instances = fbDebugInstances.get(fbTypeKey) || [] - const selectedInstance = instances.find((inst) => inst.key === selectedKey) - if (selectedInstance) { - return `${selectedInstance.programName}:${selectedInstance.fbVariableName}.${variableName}` - } - } - } - return `${editor.meta.name}:${variableName}` - } + const getCompositeKey = useDebugCompositeKey() const coil = DEFAULT_COIL_TYPES[data.variant] const [coilVariableValue, setCoilVariableValue] = useState(data.variable.name) diff --git a/src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx b/src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx index 4aa98bb27..e58925c47 100644 --- a/src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx +++ b/src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx @@ -1,3 +1,4 @@ +import { useDebugCompositeKey } from '@hooks/use-debug-composite-key' import * as Popover from '@radix-ui/react-popover' import { DefaultContact, @@ -93,33 +94,10 @@ export const Contact = (block: ContactProps) => { }, ladderFlows, ladderFlowActions: { updateNode }, - workspace: { - isDebuggerVisible, - debugVariableValues, - debugVariableIndexes, - debugForcedVariables, - fbSelectedInstance, - fbDebugInstances, - }, + workspace: { isDebuggerVisible, debugVariableValues, debugVariableIndexes, debugForcedVariables }, workspaceActions: { setDebugForcedVariables }, } = useOpenPLCStore() - - // Helper to get composite key with FB instance context - const getCompositeKey = (variableName: string): string => { - const currentPou = pous.find((p) => p.data.name === editor.meta.name) - if (currentPou?.type === 'function-block') { - const fbTypeKey = currentPou.data.name.toUpperCase() - const selectedKey = fbSelectedInstance.get(fbTypeKey) - if (selectedKey) { - const instances = fbDebugInstances.get(fbTypeKey) || [] - const selectedInstance = instances.find((inst) => inst.key === selectedKey) - if (selectedInstance) { - return `${selectedInstance.programName}:${selectedInstance.fbVariableName}.${variableName}` - } - } - } - return `${editor.meta.name}:${variableName}` - } + const getCompositeKey = useDebugCompositeKey() const contact = DEFAULT_CONTACT_TYPES[data.variant] const [contactVariableValue, setContactVariableValue] = useState(data.variable.name) diff --git a/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx b/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx index e754ebeb2..94c547070 100644 --- a/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx +++ b/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx @@ -1,3 +1,4 @@ +import { useDebugCompositeKey } from '@hooks/use-debug-composite-key' import * as Popover from '@radix-ui/react-popover' import { useOpenPLCStore } from '@root/renderer/store' import { RungLadderState } from '@root/renderer/store/slices' @@ -62,26 +63,10 @@ const VariableElement = (block: VariableProps) => { }, ladderFlows, ladderFlowActions: { updateNode }, - workspace: { isDebuggerVisible, debugVariableIndexes, debugForcedVariables, fbSelectedInstance, fbDebugInstances }, + workspace: { isDebuggerVisible, debugVariableIndexes, debugForcedVariables }, workspaceActions: { setDebugForcedVariables }, } = useOpenPLCStore() - - // Helper to get composite key with FB instance context - const getCompositeKey = (variableName: string): string => { - const currentPou = pous.find((p) => p.data.name === editor.meta.name) - if (currentPou?.type === 'function-block') { - const fbTypeKey = currentPou.data.name.toUpperCase() - const selectedKey = fbSelectedInstance.get(fbTypeKey) - if (selectedKey) { - const instances = fbDebugInstances.get(fbTypeKey) || [] - const selectedInstance = instances.find((inst) => inst.key === selectedKey) - if (selectedInstance) { - return `${selectedInstance.programName}:${selectedInstance.fbVariableName}.${variableName}` - } - } - } - return `${editor.meta.name}:${variableName}` - } + const getCompositeKey = useDebugCompositeKey() const inputVariableRef = useRef< HTMLTextAreaElement & { diff --git a/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx b/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx index 5ec9af0fb..62998a0de 100644 --- a/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx +++ b/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx @@ -1,3 +1,4 @@ +import { useDebugCompositeKey } from '@hooks/use-debug-composite-key' import { CustomFbdNodeTypes, customNodeTypes } from '@root/renderer/components/_atoms/graphical-editor/fbd' import { BlockNode } from '@root/renderer/components/_atoms/graphical-editor/fbd/block' import { getVariableRestrictionType } from '@root/renderer/components/_atoms/graphical-editor/utils' @@ -47,36 +48,14 @@ export const FBDBody = ({ rung, nodeDivergences = [], isDebuggerActive = false } modals, modalActions: { closeModal, openModal }, snapshotActions: { addSnapshot }, - workspace: { isDebuggerVisible, debugVariableValues, debugForcedVariables, fbSelectedInstance, fbDebugInstances }, + workspace: { isDebuggerVisible, debugVariableValues, debugForcedVariables }, } = useOpenPLCStore() + const getCompositeKey = useDebugCompositeKey() const pous = project.data.pous const pouRef = pous.find((pou) => pou.data.name === editor.meta.name) - // Get FB instance context for function block POUs - const fbInstanceContext = useMemo(() => { - if (!pouRef || pouRef.type !== 'function-block') return null - const fbTypeKey = pouRef.data.name.toUpperCase() // Canonical key for map lookups - const selectedKey = fbSelectedInstance.get(fbTypeKey) - if (!selectedKey) return null - const instances = fbDebugInstances.get(fbTypeKey) || [] - return instances.find((inst) => inst.key === selectedKey) || null - }, [pouRef, fbSelectedInstance, fbDebugInstances]) - - // Helper to get composite key for variable lookup, handling FB instance context - const getCompositeKey = useCallback( - (variableName: string): string => { - if (fbInstanceContext) { - // For FB POUs, transform to instance context: main:MOTOR_SPEED0.varName - return `${fbInstanceContext.programName}:${fbInstanceContext.fbVariableName}.${variableName}` - } - // For programs, use standard format: pouName:varName - return `${editor.meta.name}:${variableName}` - }, - [fbInstanceContext, editor.meta.name], - ) - const [rungLocal, setRungLocal] = useState(rung) const [dragging, setDragging] = useState(false) @@ -132,7 +111,7 @@ export const FBDBody = ({ rung, nodeDivergences = [], isDebuggerActive = false } // For program POUs, verify the program instance exists // For FB POUs, skip this check since getCompositeKey already handles instance context - if (!fbInstanceContext) { + if (pouRef?.type !== 'function-block') { const instances = project.data.configuration.resource.instances const programInstance = instances.find((inst: { program: string }) => inst.program === editor.meta.name) if (!programInstance) return undefined diff --git a/src/renderer/components/_molecules/graphical-editor/ladder/rung/body.tsx b/src/renderer/components/_molecules/graphical-editor/ladder/rung/body.tsx index d9aebaa88..4c6fa0084 100644 --- a/src/renderer/components/_molecules/graphical-editor/ladder/rung/body.tsx +++ b/src/renderer/components/_molecules/graphical-editor/ladder/rung/body.tsx @@ -1,3 +1,4 @@ +import { useDebugCompositeKey } from '@hooks/use-debug-composite-key' import { getVariableRestrictionType } from '@root/renderer/components/_atoms/graphical-editor/utils' import { useOpenPLCStore } from '@root/renderer/store' import type { RungLadderState } from '@root/renderer/store/slices' @@ -76,35 +77,13 @@ export const RungBody = ({ rung, className, nodeDivergences = [], isDebuggerActi searchQuery, searchActions: { setSearchNodePosition }, snapshotActions: { addSnapshot }, - workspace: { isDebuggerVisible, debugVariableValues, fbSelectedInstance, fbDebugInstances }, + workspace: { isDebuggerVisible, debugVariableValues }, } = useOpenPLCStore() + const getCompositeKey = useDebugCompositeKey() const pouRef = project.data.pous.find((pou) => pou.data.name === editor.meta.name) const nodeTypes = useMemo(() => customNodeTypes, []) - // Get FB instance context for function block POUs - const fbInstanceContext = useMemo(() => { - if (!pouRef || pouRef.type !== 'function-block') return null - const fbTypeKey = pouRef.data.name.toUpperCase() // Canonical key for map lookups - const selectedKey = fbSelectedInstance.get(fbTypeKey) - if (!selectedKey) return null - const instances = fbDebugInstances.get(fbTypeKey) || [] - return instances.find((inst) => inst.key === selectedKey) || null - }, [pouRef, fbSelectedInstance, fbDebugInstances]) - - // Helper to get composite key for variable lookup, handling FB instance context - const getCompositeKey = useCallback( - (variableName: string): string => { - if (fbInstanceContext) { - // For FB POUs, transform to instance context: main:MOTOR_SPEED0.varName - return `${fbInstanceContext.programName}:${fbInstanceContext.fbVariableName}.${variableName}` - } - // For programs, use standard format: pouName:varName - return `${editor.meta.name}:${variableName}` - }, - [fbInstanceContext, editor.meta.name], - ) - const [rungLocal, setRungLocal] = useState(rung) const [dragging, setDragging] = useState(false) @@ -158,7 +137,7 @@ export const RungBody = ({ rung, className, nodeDivergences = [], isDebuggerActi // For program POUs, verify the program instance exists // For FB POUs, skip this check since getCompositeKey already handles instance context - if (!fbInstanceContext) { + if (pouRef?.type !== 'function-block') { const instances = project.data.configuration.resource.instances const programInstance = instances.find((inst) => inst.program === editor.meta.name) if (!programInstance) return undefined diff --git a/src/renderer/hooks/index.ts b/src/renderer/hooks/index.ts index bc25eb1ee..d87094af3 100644 --- a/src/renderer/hooks/index.ts +++ b/src/renderer/hooks/index.ts @@ -1,2 +1,3 @@ export * from './use-compiler' +export * from './use-debug-composite-key' export * from './use-store-selectors' diff --git a/src/renderer/hooks/use-debug-composite-key.ts b/src/renderer/hooks/use-debug-composite-key.ts new file mode 100644 index 000000000..6acaefce1 --- /dev/null +++ b/src/renderer/hooks/use-debug-composite-key.ts @@ -0,0 +1,40 @@ +import { useOpenPLCStore } from '@root/renderer/store' +import { useCallback, useMemo } from 'react' + +/** + * Hook that returns a memoized function to build composite keys for debug variable lookups. + * Handles both program POUs (simple `pouName:variableName` format) and function block POUs + * (resolved to `programName:fbVariableName.variableName` using the selected FB instance context). + */ +export const useDebugCompositeKey = () => { + const { + editor, + project: { + data: { pous }, + }, + workspace: { fbSelectedInstance, fbDebugInstances }, + } = useOpenPLCStore() + + const pouRef = pous.find((p) => p.data.name === editor.meta.name) + + const fbInstanceContext = useMemo(() => { + if (!pouRef || pouRef.type !== 'function-block') return null + const fbTypeKey = pouRef.data.name.toUpperCase() + const selectedKey = fbSelectedInstance.get(fbTypeKey) + if (!selectedKey) return null + const instances = fbDebugInstances.get(fbTypeKey) || [] + return instances.find((inst) => inst.key === selectedKey) || null + }, [pouRef, fbSelectedInstance, fbDebugInstances]) + + const getCompositeKey = useCallback( + (variableName: string): string => { + if (fbInstanceContext) { + return `${fbInstanceContext.programName}:${fbInstanceContext.fbVariableName}.${variableName}` + } + return `${editor.meta.name}:${variableName}` + }, + [fbInstanceContext, editor.meta.name], + ) + + return getCompositeKey +} From 2510ac49b40ad4c20a82137223dad6b4b450eb2b Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Tue, 17 Feb 2026 08:40:03 -0500 Subject: [PATCH 3/4] fix: handle web editor projects missing pous field in project.json Web editor exports project.json without data.pous (POUs are stored as separate files), causing Zod validation to fail and the entire project data to be replaced with defaults. This lost the project name and introduced null for debugVariables.pous (ZodRecord was unhandled). Co-Authored-By: Claude Opus 4.6 --- src/types/PLC/open-plc.ts | 2 +- src/utils/default-zod-schema-values.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/PLC/open-plc.ts b/src/types/PLC/open-plc.ts index 95f6e316d..2f4cdd4bc 100644 --- a/src/types/PLC/open-plc.ts +++ b/src/types/PLC/open-plc.ts @@ -633,7 +633,7 @@ type PLCDebugVariables = z.infer const PLCProjectDataSchema = z.object({ dataTypes: z.array(PLCDataTypeSchema), - pous: z.array(PLCPouSchema), + pous: z.array(PLCPouSchema).default([]), configuration: PLCConfigurationSchema, servers: z.array(PLCServerSchema).optional(), remoteDevices: z.array(PLCRemoteDeviceSchema).optional(), diff --git a/src/utils/default-zod-schema-values.ts b/src/utils/default-zod-schema-values.ts index 00e9807d6..d0b010b5e 100644 --- a/src/utils/default-zod-schema-values.ts +++ b/src/utils/default-zod-schema-values.ts @@ -12,6 +12,7 @@ import { ZodNumber, ZodObject, ZodOptional, + ZodRecord, ZodString, ZodTuple, ZodTypeAny, @@ -30,6 +31,7 @@ export const getDefaultSchemaValues = (schema: ZodTypeAny): unknown => { if (schema instanceof ZodBoolean) return false if (schema instanceof ZodEnum) return schema.options[0] if (schema instanceof ZodLiteral) return schema._def.value + if (schema instanceof ZodRecord) return {} if (schema instanceof ZodNullable) return null if (schema instanceof ZodOptional) return getDefaultSchemaValues(schema._def.innerType as ZodTypeAny) if (schema instanceof ZodUnion) return getDefaultSchemaValues(schema._def.options[0] as ZodTypeAny) From de5513d6f4a76382001f9b97e4917d897e3d0d1f Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Tue, 17 Feb 2026 13:32:01 -0500 Subject: [PATCH 4/4] Fix hybrid POU serializer/parser for correct END keyword handling The hybrid POU serializer (Python/C++) was not appending the END keyword (END_FUNCTION_BLOCK, END_PROGRAM, END_FUNCTION) to saved files, breaking compatibility with openplc-web which correctly includes it. The parser also did not strip the END keyword on load, causing it to appear in the code editor when opening projects saved by openplc-web. - Serializer: append END keyword for hybrid POUs, matching textual/graphical - Parser: strip trailing END keyword from body, matching textual/graphical Co-Authored-By: Claude Opus 4.6 --- src/utils/PLC/pou-text-parser.ts | 13 ++++++++++++- src/utils/PLC/pou-text-serializer.ts | 5 ++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/utils/PLC/pou-text-parser.ts b/src/utils/PLC/pou-text-parser.ts index 373557fcd..3f6981a47 100644 --- a/src/utils/PLC/pou-text-parser.ts +++ b/src/utils/PLC/pou-text-parser.ts @@ -241,7 +241,18 @@ export const parseHybridPouFromString = (content: string, language: string, type ? parseIecStringToVariables(variablesString).map((v) => ({ ...v, debug: false })) : [] - const bodyContent = remainingContent.slice(bodyStartIndex).trim() + // Strip the trailing END keyword from the body content, matching how textual/graphical parsers handle it + const endKeywords: Record = { + program: 'END_PROGRAM', + function: 'END_FUNCTION', + 'function-block': 'END_FUNCTION_BLOCK', + } + const endKeyword = endKeywords[type] + let bodyContent = remainingContent.slice(bodyStartIndex).trim() + if (endKeyword) { + const endKeywordRegex = new RegExp(`\\s*\\b${endKeyword}\\b\\s*$`, 'i') + bodyContent = bodyContent.replace(endKeywordRegex, '').trim() + } if (type === 'function') { return { diff --git a/src/utils/PLC/pou-text-serializer.ts b/src/utils/PLC/pou-text-serializer.ts index 419debf59..578f3cb40 100644 --- a/src/utils/PLC/pou-text-serializer.ts +++ b/src/utils/PLC/pou-text-serializer.ts @@ -82,7 +82,10 @@ export const serializeHybridPouToString = (pou: PLCPou): string => { : generateIecVariablesToString(variables as VariablePLC[]) result += variablesString + '\n' - result += body.value + result += body.value + '\n\n' + + const endKeyword = getEndKeyword(type) + result += endKeyword return result }