diff --git a/src/renderer/components/_atoms/graphical-editor/block-output-debug-badges.tsx b/src/renderer/components/_atoms/graphical-editor/block-output-debug-badges.tsx new file mode 100644 index 000000000..b91db721d --- /dev/null +++ b/src/renderer/components/_atoms/graphical-editor/block-output-debug-badges.tsx @@ -0,0 +1,80 @@ +import { useDebugCompositeKey } from '@root/renderer/hooks/use-debug-composite-key' +import { useOpenPLCStore } from '@root/renderer/store' + +import { DebugValueBadge } from './debug-value-badge' + +type BlockOutputDebugBadgesProps = { + blockType: string + blockName: string + blockVariableName: string + numericId: string + outputVariables: Array<{ name: string; class: string; type: { definition: string; value: string } }> + connectorStartY: number + connectorOffsetY: number + blockWidth: number + /** Output names that already have a variable node connected showing its own badge. */ + connectedOutputNames?: Set +} + +/** + * Renders debug value badges next to each output connector of a block node. + * Works for both function blocks (using instance-qualified names) and + * functions (using _TMP_ variable names). Shared between FBD and LD. + * + * Outputs that are connected to a variable node are skipped since the + * variable node already displays its own badge (avoids double badges). + */ +const BlockOutputDebugBadges = ({ + blockType, + blockName, + blockVariableName, + numericId, + outputVariables, + connectorStartY, + connectorOffsetY, + blockWidth, + connectedOutputNames, +}: BlockOutputDebugBadgesProps) => { + const { + workspace: { isDebuggerVisible }, + } = useOpenPLCStore() + const getCompositeKey = useDebugCompositeKey() + + if (!isDebuggerVisible || blockType === 'generic') { + return null + } + + const outputs = outputVariables.filter((v) => v.class === 'output' || v.class === 'inOut') + + return ( + <> + {outputs.map((outputVar, index) => { + if (connectedOutputNames?.has(outputVar.name)) { + return null + } + + let compositeKey: string + if (blockType === 'function-block') { + compositeKey = getCompositeKey(`${blockVariableName}.${outputVar.name}`) + } else { + compositeKey = getCompositeKey(`_TMP_${blockName.toUpperCase()}${numericId}_${outputVar.name}`) + } + + return ( +
+ +
+ ) + })} + + ) +} + +export { BlockOutputDebugBadges } diff --git a/src/renderer/components/_atoms/graphical-editor/debug-value-badge.tsx b/src/renderer/components/_atoms/graphical-editor/debug-value-badge.tsx new file mode 100644 index 000000000..5d4b08d9f --- /dev/null +++ b/src/renderer/components/_atoms/graphical-editor/debug-value-badge.tsx @@ -0,0 +1,52 @@ +import { useOpenPLCStore } from '@root/renderer/store' +import { cn } from '@root/utils' + +type DebugValueBadgeProps = { + compositeKey: string + variableType: string | undefined + position?: 'right' | 'left' | 'below' +} + +/** + * Displays a real-time debug value badge next to graphical editor nodes. + * Shows the current polled value for non-BOOL variables when the debugger is active. + * BOOL variables are skipped since they already have dedicated color indicators. + * + * Designed to be used by both FBD and LD variable/block nodes. + */ +const DebugValueBadge = ({ compositeKey, variableType, position = 'right' }: DebugValueBadgeProps) => { + const { + workspace: { debugVariableValues }, + } = useOpenPLCStore() + + if (!variableType || variableType.toUpperCase() === 'BOOL') { + return null + } + + const value = debugVariableValues.get(compositeKey) + if (value === undefined) { + return null + } + + const positionClasses: Record = { + right: 'left-full ml-1 top-1/2 -translate-y-1/2', + left: 'right-full mr-1 top-1/2 -translate-y-1/2', + below: 'top-full mt-0.5 left-1/2 -translate-x-1/2', + } + + return ( +
+ {value} +
+ ) +} + +export { DebugValueBadge } +export type { DebugValueBadgeProps } diff --git a/src/renderer/components/_atoms/graphical-editor/fbd/block.tsx b/src/renderer/components/_atoms/graphical-editor/fbd/block.tsx index e3fda6b51..ac2e66baa 100644 --- a/src/renderer/components/_atoms/graphical-editor/fbd/block.tsx +++ b/src/renderer/components/_atoms/graphical-editor/fbd/block.tsx @@ -6,11 +6,12 @@ import type { PLCVariable } from '@root/types/PLC' import { cn, generateNumericUUID } from '@root/utils' import { newGraphicalEditorNodeID } from '@root/utils/new-graphical-editor-node-id' import { Node, NodeProps, Position } from '@xyflow/react' -import { FocusEvent, useEffect, useRef, useState } from 'react' +import { FocusEvent, useEffect, useMemo, useRef, useState } from 'react' import { HighlightedTextArea } from '../../highlighted-textarea' import { InputWithRef } from '../../input' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../tooltip' +import { BlockOutputDebugBadges } from '../block-output-debug-badges' import { BlockVariant } from '../types/block' import { getBlockDocumentation, getVariableRestrictionType } from '../utils' import { buildHandle, CustomHandle } from './handle' @@ -352,10 +353,24 @@ export const Block = (block: BlockProps) => { const [wrongVariable, setWrongVariable] = useState(false) const [hoveringBlock, setHoveringBlock] = useState(false) - const { variables } = getFBDPouVariablesRungNodeAndEdges(editor, pous, fbdFlows, { + const { variables, rung } = getFBDPouVariablesRungNodeAndEdges(editor, pous, fbdFlows, { nodeId: id ?? '', }) + // Outputs connected to variable nodes already show their own badge — skip those + const connectedOutputNames = useMemo(() => { + const names = new Set() + if (!rung) return names + const outgoingEdges = rung.edges.filter((e) => e.source === id) + for (const edge of outgoingEdges) { + const targetNode = rung.nodes.find((n) => n.id === edge.target) + if (targetNode && typeof targetNode.type === 'string' && targetNode.type.includes('variable')) { + if (edge.sourceHandle) names.add(edge.sourceHandle) + } + } + return names + }, [rung, id]) + const inputVariableRef = useRef< HTMLTextAreaElement & { blur: ({ submit }: { submit?: boolean }) => void @@ -807,6 +822,17 @@ export const Block = (block: BlockProps) => { {data.handles.map((handle, index) => ( ))} + ) } diff --git a/src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx b/src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx index 32533906c..8ed36568d 100644 --- a/src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx +++ b/src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx @@ -20,6 +20,7 @@ import { Modal, ModalContent, ModalTitle } from '../../../_molecules/modal' import { HighlightedTextArea } from '../../highlighted-textarea' import { Label } from '../../label' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../tooltip' +import { DebugValueBadge } from '../debug-value-badge' import { BlockVariant } from '../types/block' import { validateVariableType } from '../utils' import { FBDBlockAutoComplete } from './autocomplete' @@ -656,6 +657,16 @@ const VariableElement = (block: VariableProps) => { )} + {isDebuggerVisible && isAVariable && ( + + )} + {isDebuggerVisible && contextMenuPosition && ( diff --git a/src/renderer/components/_atoms/graphical-editor/ladder/block.tsx b/src/renderer/components/_atoms/graphical-editor/ladder/block.tsx index fcdc30480..90319c45c 100644 --- a/src/renderer/components/_atoms/graphical-editor/ladder/block.tsx +++ b/src/renderer/components/_atoms/graphical-editor/ladder/block.tsx @@ -11,11 +11,12 @@ import type { VariableReference } from '@root/types/PLC/variable-reference' import { cn, generateNumericUUID } from '@root/utils' import { newGraphicalEditorNodeID } from '@root/utils/new-graphical-editor-node-id' import { Node, NodeProps, Position } from '@xyflow/react' -import { FocusEvent, useEffect, useRef, useState } from 'react' +import { FocusEvent, useEffect, useMemo, useRef, useState } from 'react' import { HighlightedTextArea } from '../../highlighted-textarea' import { InputWithRef } from '../../input' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../tooltip' +import { BlockOutputDebugBadges } from '../block-output-debug-badges' import { BlockVariant as newBlockVariant } from '../types/block' import { getBlockDocumentation, getVariableRestrictionType, validateVariableType } from '../utils' import { buildHandle, CustomHandle } from './handle' @@ -433,6 +434,18 @@ export const Block = (block: BlockProps) => { nodeId: id, }) + const connectedOutputNames = useMemo(() => { + const names = new Set() + if (data.connectedVariables) { + for (const cv of data.connectedVariables) { + if (cv.type === 'output' && cv.variable) { + names.add(cv.handleId) + } + } + } + return names + }, [data.connectedVariables]) + const inputVariableRef = useRef< HTMLTextAreaElement & { blur: ({ submit }: { submit?: boolean }) => void @@ -896,6 +909,17 @@ export const Block = (block: BlockProps) => { {data.handles.map((handle, index) => ( ))} + ) } diff --git a/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx b/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx index 94c547070..c4b5cbf6d 100644 --- a/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx +++ b/src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx @@ -20,6 +20,7 @@ import { useEffect, useRef, useState } from 'react' import { Modal, ModalContent, ModalTitle } from '../../../_molecules/modal' import { HighlightedTextArea } from '../../highlighted-textarea' import { Label } from '../../label' +import { DebugValueBadge } from '../debug-value-badge' import { getVariableByName, validateVariableType } from '../utils' import { VariablesBlockAutoComplete } from './autocomplete' import { BlockNodeData, BlockVariant, LadderBlockConnectedVariables } from './block' @@ -455,6 +456,7 @@ const VariableElement = (block: VariableProps) => { return ( <>
@@ -519,6 +521,14 @@ const VariableElement = (block: VariableProps) => {
)} + {isDebuggerVisible && isAVariable && ( + + )} + {isDebuggerVisible && contextMenuPosition && ( diff --git a/src/renderer/screens/workspace-screen.tsx b/src/renderer/screens/workspace-screen.tsx index 34cd1d8d4..33fdcac97 100644 --- a/src/renderer/screens/workspace-screen.tsx +++ b/src/renderer/screens/workspace-screen.tsx @@ -4,6 +4,7 @@ import { PlcLogsFilters } from '@components/_organisms/plc-logs/filters' import * as Tabs from '@radix-ui/react-tabs' import { useRuntimePolling } from '@root/renderer/hooks/use-runtime-polling' import { DebugTreeNode } from '@root/types/debugger' +import type { PLCBaseTypesLowercase } from '@root/types/PLC/units/base-types' // Note: Logs polling is now handled by useRuntimePolling hook import { cn, isOpenPLCRuntimeTarget, isSimulatorTarget } from '@root/utils' import { @@ -640,7 +641,7 @@ const WorkspaceScreen = () => { } }) - // Register _TMP_ variables for function BOOL outputs so they get polled + // Register _TMP_ variables for function base-type outputs so they get polled const registerFunctionTempOutputs = (nodes: Array<{ type?: string; data: object }>) => { nodes.forEach((node) => { if (node.type !== 'block') return @@ -662,25 +663,22 @@ const WorkspaceScreen = () => { const numericId = blockData.numericId if (!numericId) return - let boolOutputs = blockData.variant.variables.filter( - (v) => - (v.class === 'output' || v.class === 'inOut') && - v.type.definition === 'base-type' && - v.type.value.toUpperCase() === 'BOOL', + let baseTypeOutputs = blockData.variant.variables.filter( + (v) => (v.class === 'output' || v.class === 'inOut') && v.type.definition === 'base-type', ) const hasExecutionControl = blockData.executionControl || false if (hasExecutionControl) { - const hasENO = boolOutputs.some((v) => v.name.toUpperCase() === 'ENO') + const hasENO = baseTypeOutputs.some((v) => v.name.toUpperCase() === 'ENO') if (!hasENO) { - boolOutputs = [ - ...boolOutputs, + baseTypeOutputs = [ + ...baseTypeOutputs, { name: 'ENO', class: 'output', type: { definition: 'base-type', value: 'BOOL' } }, ] } } - boolOutputs.forEach((outputVar) => { + baseTypeOutputs.forEach((outputVar) => { const index = getIndexFromMapWithFallback( debugVariableIndexes, programInstance.name, @@ -693,7 +691,10 @@ const WorkspaceScreen = () => { pouName: pou.data.name, variable: { name: tempVarName, - type: { definition: 'base-type', value: 'bool' }, + type: { + definition: 'base-type', + value: outputVar.type.value.toLowerCase() as PLCBaseTypesLowercase, + }, class: 'local', location: '', documentation: '', @@ -835,26 +836,23 @@ const WorkspaceScreen = () => { const numericId = blockData.numericId if (!numericId) return - let boolOutputs = blockData.variant.variables.filter( - (v) => - (v.class === 'output' || v.class === 'inOut') && - v.type.definition === 'base-type' && - v.type.value.toUpperCase() === 'BOOL', + let baseTypeOutputs = blockData.variant.variables.filter( + (v) => (v.class === 'output' || v.class === 'inOut') && v.type.definition === 'base-type', ) // Add ENO if execution control is enabled const hasExecutionControl = blockData.executionControl || false if (hasExecutionControl) { - const hasENO = boolOutputs.some((v) => v.name.toUpperCase() === 'ENO') + const hasENO = baseTypeOutputs.some((v) => v.name.toUpperCase() === 'ENO') if (!hasENO) { - boolOutputs = [ - ...boolOutputs, + baseTypeOutputs = [ + ...baseTypeOutputs, { name: 'ENO', class: 'output', type: { definition: 'base-type', value: 'BOOL' } }, ] } } - boolOutputs.forEach((outputVar) => { + baseTypeOutputs.forEach((outputVar) => { // Debug path uses the full nested path: // RES0__INSTANCE0.FB_B0.FB_A0._TMP_EQ_STATE7415072_ENO // Use fallback to try both FB-style and struct-style paths @@ -871,7 +869,10 @@ const WorkspaceScreen = () => { pouName: programPouName, variable: { name: tempVarName, - type: { definition: 'base-type', value: 'bool' }, + type: { + definition: 'base-type', + value: outputVar.type.value.toLowerCase() as PLCBaseTypesLowercase, + }, class: 'local', location: '', documentation: '', @@ -1386,6 +1387,44 @@ const WorkspaceScreen = () => { } } + // Poll all variables of the active POU so non-BOOL values can be displayed on the diagram. + // This adds every variable registered in variableInfoMapRef that belongs to the current POU, + // enabling the DebugValueBadge components to show real-time values for INT, REAL, etc. + if (currentPou) { + if (currentPou.type === 'function-block') { + // For FB POUs, resolve the selected instance context and match variables by + // program name + instance path prefix (same pattern used for BOOL polling above). + 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) { + const instancePrefix = `${selectedInstance.fbVariableName}.` + Array.from(variableInfoMapRef.current.values()).forEach((varInfos) => { + for (const varInfo of varInfos) { + if ( + varInfo.pouName === selectedInstance.programName && + varInfo.variable.name.startsWith(instancePrefix) + ) { + debugVariableKeys.add(`${varInfo.pouName}:${varInfo.variable.name}`) + } + } + }) + } + } + } else { + const activePouName = currentPou.data.name + Array.from(variableInfoMapRef.current.values()).forEach((varInfos) => { + for (const varInfo of varInfos) { + if (varInfo.pouName === activePouName) { + debugVariableKeys.add(`${varInfo.pouName}:${varInfo.variable.name}`) + } + } + }) + } + } + // Forced variables must also be polled so their current value appears in the debugger panel const { workspace: { debugForcedVariables: currentForcedVars }, diff --git a/src/types/PLC/units/base-types.ts b/src/types/PLC/units/base-types.ts index bf3144ca6..e3f0b7cee 100644 --- a/src/types/PLC/units/base-types.ts +++ b/src/types/PLC/units/base-types.ts @@ -4,5 +4,6 @@ import { z } from 'zod' const PLCBaseTypesSchema = z.enum(baseTypes) type PLCBaseTypes = z.infer +type PLCBaseTypesLowercase = Lowercase -export { PLCBaseTypes, PLCBaseTypesSchema } +export { PLCBaseTypes, PLCBaseTypesLowercase, PLCBaseTypesSchema }