From 11d5fe9551737a82331272449801268d878098e8 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Fri, 27 Feb 2026 10:20:41 -0500 Subject: [PATCH] fix: resolve three debugger issues with simulator and FBD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Simulator endianness: the simulator debug flow skipped the getMd5Hash() call that triggers set_endianness() on the AVR runtime, causing multi-byte forced values to be byte-swapped (e.g. 1 → 256). 2. FBD BOOL edge coloring: _TMP_ function output variables were only registered for Ladder flows but not FBD, so function outputs like GT.OUT never got polled and edges were never painted green. Extracted a shared registerFunctionTempOutputs helper used by both editors. Also added missing getCompositeKey to the FBD styledEdges memo deps. 3. Forced variable polling: variables forced from the diagram were added to debugForcedVariables but never to the polling key set, so their values never appeared in the debugger panel. Now forced variable keys are included in debugVariableKeys before building the poll list. Co-Authored-By: Claude Opus 4.6 --- src/main/modules/ipc/main.ts | 7 + .../_molecules/graphical-editor/fbd/index.tsx | 1 + src/renderer/screens/workspace-screen.tsx | 147 +++++++++++------- 3 files changed, 96 insertions(+), 59 deletions(-) diff --git a/src/main/modules/ipc/main.ts b/src/main/modules/ipc/main.ts index 38804d401..ae73dde85 100644 --- a/src/main/modules/ipc/main.ts +++ b/src/main/modules/ipc/main.ts @@ -1187,6 +1187,13 @@ class MainProcessBridge implements MainIpcModule { serialPort: virtualPort, }) await this.debuggerModbusClient.connect() + + // Trigger endianness detection on the emulated runtime. + // getMd5Hash sends 0xDEAD which the runtime uses to detect byte order + // and call set_endianness(). Without this, the default SAME_ENDIANNESS + // causes multi-byte values to be stored with swapped bytes on the + // little-endian AVR emulator. + await this.debuggerModbusClient.getMd5Hash() } else if (connectionType === 'websocket') { if (this.debuggerModbusClient) { this.debuggerModbusClient.disconnect() diff --git a/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx b/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx index 62998a0de..70fe2ac1c 100644 --- a/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx +++ b/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx @@ -222,6 +222,7 @@ export const FBDBody = ({ rung, nodeDivergences = [], isDebuggerActive = false } debugForcedVariables, editor.meta.name, project, + getCompositeKey, ]) const styledNodes = useMemo(() => { diff --git a/src/renderer/screens/workspace-screen.tsx b/src/renderer/screens/workspace-screen.tsx index 2ad35c224..34cd1d8d4 100644 --- a/src/renderer/screens/workspace-screen.tsx +++ b/src/renderer/screens/workspace-screen.tsx @@ -447,7 +447,7 @@ const WorkspaceScreen = () => { }) }) - const { ladderFlows } = useOpenPLCStore.getState() + const { ladderFlows, fbdFlows } = useOpenPLCStore.getState() project.data.pous.forEach((pou) => { if (pou.type !== 'program') return @@ -473,6 +473,18 @@ const WorkspaceScreen = () => { }) }) } + } else if (pou.data.body.language === 'fbd') { + const currentFbdFlow = fbdFlows.find((flow) => flow.name === pou.data.name) + if (currentFbdFlow) { + currentFbdFlow.rung.nodes.forEach((node) => { + if (node.type === 'block') { + const blockData = node.data as { variable?: { name: string }; executionControl?: boolean } + if (blockData.variable?.name && blockData.executionControl) { + blockExecutionControlMap.set(blockData.variable.name, true) + } + } + }) + } } functionBlockInstances.forEach((fbInstance) => { @@ -628,74 +640,83 @@ const WorkspaceScreen = () => { } }) - if (pou.data.body.language === 'ld') { - const currentLadderFlow = ladderFlows.find((flow) => flow.name === pou.data.name) - if (currentLadderFlow) { - currentLadderFlow.rungs.forEach((rung) => { - rung.nodes.forEach((node) => { - if (node.type !== 'block') return + // Register _TMP_ variables for function BOOL outputs so they get polled + const registerFunctionTempOutputs = (nodes: Array<{ type?: string; data: object }>) => { + nodes.forEach((node) => { + if (node.type !== 'block') return - const blockData = node.data as { - variable?: { name: string } - variant?: { - name: string - type: string - variables: Array<{ name: string; class: string; type: { definition: string; value: string } }> - } - numericId?: string - executionControl?: boolean - } + const blockData = node.data as { + variable?: { name: string } + variant?: { + name: string + type: string + variables: Array<{ name: string; class: string; type: { definition: string; value: string } }> + } + numericId?: string + executionControl?: boolean + } - if (!blockData.variant || blockData.variant.type !== 'function') return + if (!blockData.variant || blockData.variant.type !== 'function') return - const blockName = blockData.variant.name.toUpperCase() - const numericId = blockData.numericId - if (!numericId) return + const blockName = blockData.variant.name.toUpperCase() + 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 boolOutputs = blockData.variant.variables.filter( + (v) => + (v.class === 'output' || v.class === 'inOut') && + v.type.definition === 'base-type' && + v.type.value.toUpperCase() === 'BOOL', + ) - const hasExecutionControl = blockData.executionControl || false - if (hasExecutionControl) { - const hasENO = boolOutputs.some((v) => v.name.toUpperCase() === 'ENO') - if (!hasENO) { - boolOutputs = [ - ...boolOutputs, - { name: 'ENO', class: 'output', type: { definition: 'base-type', value: 'BOOL' } }, - ] - } - } + const hasExecutionControl = blockData.executionControl || false + if (hasExecutionControl) { + const hasENO = boolOutputs.some((v) => v.name.toUpperCase() === 'ENO') + if (!hasENO) { + boolOutputs = [ + ...boolOutputs, + { name: 'ENO', class: 'output', type: { definition: 'base-type', value: 'BOOL' } }, + ] + } + } - boolOutputs.forEach((outputVar) => { - // Use fallback to try both FB-style and struct-style paths - const index = getIndexFromMapWithFallback( - debugVariableIndexes, - programInstance.name, - `_TMP_${blockName}${numericId}_${outputVar.name}`, - ) + boolOutputs.forEach((outputVar) => { + const index = getIndexFromMapWithFallback( + debugVariableIndexes, + programInstance.name, + `_TMP_${blockName}${numericId}_${outputVar.name}`, + ) - if (index !== undefined) { - const tempVarName = `_TMP_${blockName}${numericId}_${outputVar.name}` - addVariableInfo(index, { - pouName: pou.data.name, - variable: { - name: tempVarName, - type: { definition: 'base-type', value: 'bool' }, - class: 'local', - location: '', - documentation: '', - debug: false, - }, - }) - } + if (index !== undefined) { + const tempVarName = `_TMP_${blockName}${numericId}_${outputVar.name}` + addVariableInfo(index, { + pouName: pou.data.name, + variable: { + name: tempVarName, + type: { definition: 'base-type', value: 'bool' }, + class: 'local', + location: '', + documentation: '', + debug: false, + }, }) - }) + } + }) + }) + } + + if (pou.data.body.language === 'ld') { + const currentLadderFlow = ladderFlows.find((flow) => flow.name === pou.data.name) + if (currentLadderFlow) { + currentLadderFlow.rungs.forEach((rung) => { + registerFunctionTempOutputs(rung.nodes) }) } + } else if (pou.data.body.language === 'fbd') { + const currentFbdFlow = fbdFlows.find((flow) => flow.name === pou.data.name) + if (currentFbdFlow) { + registerFunctionTempOutputs(currentFbdFlow.rung.nodes) + } } } }) @@ -1365,6 +1386,14 @@ const WorkspaceScreen = () => { } } + // Forced variables must also be polled so their current value appears in the debugger panel + const { + workspace: { debugForcedVariables: currentForcedVars }, + } = useOpenPLCStore.getState() + currentForcedVars.forEach((_value, compositeKey) => { + debugVariableKeys.add(compositeKey) + }) + const allIndexes = Array.from(variableInfoMapRef.current.entries()) .filter(([_, varInfos]) => varInfos.some((varInfo) => {