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/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 17f74a21a..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) @@ -164,7 +142,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 +158,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..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) @@ -131,7 +109,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 +125,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..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' @@ -65,6 +66,7 @@ const VariableElement = (block: VariableProps) => { workspace: { isDebuggerVisible, debugVariableIndexes, debugForcedVariables }, workspaceActions: { setDebugForcedVariables }, } = useOpenPLCStore() + const getCompositeKey = useDebugCompositeKey() const inputVariableRef = useRef< HTMLTextAreaElement & { @@ -280,7 +282,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 +304,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 +326,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 +354,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 +448,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/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 +} 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 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/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 } 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)