Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/backend/editor/compiler/compiler-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,23 @@
originalCppPous?: CppPouDataCode[]
}

/**
* Keep generated C++ metadata aligned with the POU interfaces used to build
* the ST bridge. The sidecar carries the original C++ source, while the
* preprocessed POU is the authoritative source for its current variables.
*/
const getCppPousForGeneration = (projectData: ProjectDataWithCppPous): CppPouDataCode[] => {
return (projectData.originalCppPous ?? []).map((cppPou) => {
const processedPou = projectData.pous.find((pou) => pou.data.name === cppPou.name) as
| { data?: { variables?: PLCVariable[] } }
| undefined
return {
...cppPou,
variables: processedPou?.data?.variables ?? cppPou.variables,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
})
}

/**
* Post-build PLC start retry loop bounds. Why these numbers:
* - 5000 ms total: longer than the slowest STOP transition observed
Expand Down Expand Up @@ -117,7 +134,7 @@
import type { MessagePortMain } from 'electron/main'
import JSZip from 'jszip'

import type { PlatformOption } from '../../../middleware/shared/ports/types'
import type { PlatformOption, PLCVariable } from '../../../middleware/shared/ports/types'
import { BoardInfoResolver } from '../../shared/hardware/board-info-resolver'
import type { PackageManifest } from '../package-manager'
import { PackageManagerModule } from '../package-manager'
Expand Down Expand Up @@ -487,8 +504,8 @@

checkStrucppAvailability(): MethodsResult<string> {
try {
const { getVersion } = loadStrucpp()

Check warning on line 507 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe array destructuring of a tuple element with an error typed value
return { success: true, data: getVersion() }

Check warning on line 508 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe call of a(n) `error` type typed value

Check warning on line 508 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe assignment of an error typed value
} catch {
throw new Error('STruC++ not available. Run "npm run setup:binaries" to install it.')
}
Expand Down Expand Up @@ -1184,7 +1201,7 @@
sourceTargetFolderPath: string,
handleOutputData: HandleOutputDataCallback,
) {
const originalCppPous = projectData.originalCppPous || []
const originalCppPous = getCppPousForGeneration(projectData)

if (originalCppPous.length === 0) {
handleOutputData('No C/C++ blocks found, skipping c_blocks.h generation', 'info')
Expand Down Expand Up @@ -1217,7 +1234,7 @@
_boardRuntime: string,
handleOutputData: HandleOutputDataCallback,
) {
const originalCppPous = projectData.originalCppPous || []
const originalCppPous = getCppPousForGeneration(projectData)

if (originalCppPous.length === 0) {
handleOutputData('No C/C++ blocks found, skipping c_blocks_code.cpp generation', 'info')
Expand Down Expand Up @@ -2844,7 +2861,7 @@
_mainProcessPort.postMessage({
logLevel,
message: data,
...(compileError ? { compileError } : {}),

Check warning on line 2864 in src/backend/editor/compiler/compiler-module.ts

View workflow job for this annotation

GitHub Actions / lint / Lint Check

Unsafe assignment of an error typed value
})
},
{ hasCBlocks, pous: knownPous, libraries, missingLibraries },
Expand Down
17 changes: 17 additions & 0 deletions src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,23 @@ describe('preprocessPous — C++', () => {
expect(vars.some((v) => v.name === 'hasBeenInitialized')).toBe(true)
})

it('keeps C++ sidecar variables aligned with the generated ST bridge', () => {
const variables = [
makeVariable('Enable', 'input', 'BOOL'),
makeVariable('PrevSeq', 'local', 'USINT'),
makeVariable('NewData', 'output', 'BOOL'),
]
const project = makeProjectData([makeCppPou('can_rx', validCppCode, variables)])
const logger = collectLog()
const { projectData } = preprocessPous(project, false, logger.log)

const body = projectData.pous[0].body.value as string
expect(body).toContain('vars.PREVSEQ = &PREVSEQ;')
expect(projectData.originalCppPous?.[0].variables.map((v) => v.name)).toEqual(
expect.arrayContaining(['PrevSeq', 'hasBeenInitialized']),
)
})

it('skips C++ processing when no C++ POUs exist', () => {
const project = makeProjectData([makeStPou('Main', 'x := 1;')])
const logger = collectLog()
Expand Down
26 changes: 17 additions & 9 deletions src/backend/shared/utils/PLC/preprocess-pous.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,25 @@ function preprocessPous(projectData: PLCProjectData, isSimulator: boolean, log:
return { projectData: processedProjectData as ProjectDataWithCpp, validationFailed: true }
}

processedProjectData = addCppLocalVariables(processedProjectData)

const originalCppPousData = cppPous.map((pou) => ({
name: pou.name,
code:
const originalCppCodeByName = new Map(
cppPous.map((pou) => [
pou.name,
/* istanbul ignore next -- defensive: cppPous filter guarantees language === 'cpp' */
pou.body.language === 'cpp' ? (pou.body as { language: string; value: string }).value : '',
variables:
/* istanbul ignore next -- defensive: interface may be undefined */
pou.interface?.variables ?? [],
}))
]),
)

processedProjectData = addCppLocalVariables(processedProjectData)

const originalCppPousData = processedProjectData.pous
.filter((pou: PLCPou) => pou.body.language === 'cpp')
.map((pou) => ({
name: pou.name,
code: originalCppCodeByName.get(pou.name) ?? '',
variables:
/* istanbul ignore next -- defensive: interface may be undefined */
pou.interface?.variables ?? [],
}))

processedProjectData.pous = processedProjectData.pous.map((pou: PLCPou) => {
if (pou.body.language === 'cpp') {
Expand Down
35 changes: 28 additions & 7 deletions src/backend/shared/utils/cpp/__tests__/generateCBlocksCode.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PLCVariable } from '../../../../../middleware/shared/ports/types'
import { generateCBlocksCode } from '../generateCBlocksCode'

const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string): PLCVariable => ({
const makeScalarVar = (name: string, cls: PLCVariable['class'], baseType: string): PLCVariable => ({
name,
class: cls,
type: { definition: 'base-type', value: baseType },
Expand All @@ -10,7 +10,7 @@ const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string):
debug: false,
})

const makeArrayVar = (name: string, cls: 'input' | 'output', baseType: string, dimension: string): PLCVariable => ({
const makeArrayVar = (name: string, cls: PLCVariable['class'], baseType: string, dimension: string): PLCVariable => ({
name,
class: cls,
type: {
Expand Down Expand Up @@ -169,13 +169,13 @@ describe('generateCBlocksCode', () => {
expect(result).toContain('// comment about setup')
})

it('filters variables by class (only input and output)', () => {
it('generates struct fields and macros for local state variables', () => {
const variables: PLCVariable[] = [
makeScalarVar('inVar', 'input', 'INT'),
{
name: 'localVar',
name: 'PrevSeq',
class: 'local',
type: { definition: 'base-type', value: 'INT' },
type: { definition: 'base-type', value: 'USINT' },
location: '',
documentation: '',
debug: false,
Expand All @@ -188,9 +188,30 @@ describe('generateCBlocksCode', () => {

expect(result).toContain('#define inVar')
expect(result).toContain('#define outVar')
expect(result).not.toContain('#define localVar')
expect(result).toContain('strucpp::IEC_USINT *PREVSEQ;')
expect(result).toContain('#define PrevSeq (*(vars->PREVSEQ))')
expect(result).toContain('#undef inVar')
expect(result).toContain('#undef outVar')
expect(result).not.toContain('#undef localVar')
expect(result).toContain('#undef PrevSeq')
})

it('does not expose the generated setup guard as a user C++ macro', () => {
const variables: PLCVariable[] = [
makeScalarVar('x', 'input', 'INT'),
{
name: 'hasBeenInitialized',
class: 'local',
type: { definition: 'base-type', value: 'BOOL' },
location: '',
documentation: '',
debug: false,
},
]
const code = 'void setup() { }\nvoid loop() { }'

const result = generateCBlocksCode([{ name: 'test', code, variables }])

expect(result).not.toContain('HASBEENINITIALIZED')
expect(result).not.toContain('#define hasBeenInitialized')
})
})
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PLCVariable } from '../../../../../middleware/shared/ports/types'
import { generateCBlocksHeader } from '../generateCBlocksHeader'

const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string): PLCVariable => ({
const makeScalarVar = (name: string, cls: PLCVariable['class'], baseType: string): PLCVariable => ({
name,
class: cls,
type: { definition: 'base-type', value: baseType },
Expand All @@ -10,7 +10,7 @@ const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string):
debug: false,
})

const makeArrayVar = (name: string, cls: 'input' | 'output', baseType: string, dimension: string): PLCVariable => ({
const makeArrayVar = (name: string, cls: PLCVariable['class'], baseType: string, dimension: string): PLCVariable => ({
name,
class: cls,
type: {
Expand Down Expand Up @@ -59,13 +59,13 @@ describe('generateCBlocksHeader', () => {
expect(result).toContain('extern "C" void myblock_loop(MYBLOCK_VARS *vars);')
})

it('includes only input and output variables in the struct', () => {
it('includes local variables in the C++ POU instance struct', () => {
const variables: PLCVariable[] = [
makeScalarVar('inVar', 'input', 'INT'),
{
name: 'localVar',
name: 'PrevSeq',
class: 'local',
type: { definition: 'base-type', value: 'BOOL' },
type: { definition: 'base-type', value: 'USINT' },
location: '',
documentation: '',
debug: false,
Expand All @@ -76,8 +76,27 @@ describe('generateCBlocksHeader', () => {
const result = generateCBlocksHeader([{ name: 'test', variables }])

expect(result).toContain('strucpp::IEC_INT *INVAR;')
expect(result).toContain('strucpp::IEC_USINT *PREVSEQ;')
expect(result).toContain('strucpp::IEC_BOOL *OUTVAR;')
expect(result).not.toContain('LOCALVAR')
})

it('does not include the generated setup guard in the C++ POU instance struct', () => {
const variables: PLCVariable[] = [
makeScalarVar('inVar', 'input', 'INT'),
{
name: 'hasBeenInitialized',
class: 'local',
type: { definition: 'base-type', value: 'BOOL' },
location: '',
documentation: '',
debug: false,
},
]

const result = generateCBlocksHeader([{ name: 'test', variables }])

expect(result).toContain('strucpp::IEC_INT *INVAR;')
expect(result).not.toContain('HASBEENINITIALIZED')
})

it('generates declarations for multiple pous', () => {
Expand Down
21 changes: 5 additions & 16 deletions src/backend/shared/utils/cpp/generateCBlocksCode.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getCppPouStateVariables } from '../../../../frontend/utils/cpp/cppPouVariables'
import { generateStructMember, isArrayVariable } from '../../../../frontend/utils/PLC/array-codegen-helpers'
import type { PLCVariable } from '../../../../middleware/shared/ports/types'

Expand Down Expand Up @@ -112,17 +113,12 @@ const processUserCode = (pou: CppPouData): string => {
const setupFunctionName = `${pou.name.toLowerCase()}_setup`
const loopFunctionName = `${pou.name.toLowerCase()}_loop`

const inputVariables = pou.variables.filter((v) => v.class === 'input')
const outputVariables = pou.variables.filter((v) => v.class === 'output')
const stateVariables = getCppPouStateVariables(pou.variables)

let processedCode = `//definition of external blocks - ${pou.name.toUpperCase()}\n`
processedCode += `typedef struct {\n`

inputVariables.forEach((variable) => {
processedCode += generateStructMember(variable)
})

outputVariables.forEach((variable) => {
stateVariables.forEach((variable) => {
processedCode += generateStructMember(variable)
})

Expand All @@ -131,11 +127,7 @@ const processUserCode = (pou: CppPouData): string => {
processedCode += `extern "C" void ${setupFunctionName}(${structName} *vars);\n`
processedCode += `extern "C" void ${loopFunctionName}(${structName} *vars);\n\n`

inputVariables.forEach((variable) => {
processedCode += generateDefine(variable)
})

outputVariables.forEach((variable) => {
stateVariables.forEach((variable) => {
processedCode += generateDefine(variable)
})

Expand All @@ -153,10 +145,7 @@ const processUserCode = (pou: CppPouData): string => {
processedCode += modifiedUserCode
processedCode += '\n'

inputVariables.forEach((variable) => {
processedCode += generateUndef(variable)
})
outputVariables.forEach((variable) => {
stateVariables.forEach((variable) => {
processedCode += generateUndef(variable)
})
processedCode += '\n'
Expand Down
10 changes: 3 additions & 7 deletions src/backend/shared/utils/cpp/generateCBlocksHeader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { getCppPouStateVariables } from '../../../../frontend/utils/cpp/cppPouVariables'
import { generateStructMember } from '../../../../frontend/utils/PLC/array-codegen-helpers'
import type { PLCVariable } from '../../../../middleware/shared/ports/types'

Expand Down Expand Up @@ -25,17 +26,12 @@ const generateCBlocksHeader = (cppPous: CppPouData[]): string => {
const setupFunctionName = `${pou.name.toLowerCase()}_setup`
const loopFunctionName = `${pou.name.toLowerCase()}_loop`

const inputVariables = pou.variables.filter((v) => v.class === 'input')
const outputVariables = pou.variables.filter((v) => v.class === 'output')
const stateVariables = getCppPouStateVariables(pou.variables)

headerContent += `//definition of external blocks - ${pou.name.toUpperCase()}\n`
headerContent += `typedef struct {\n`

inputVariables.forEach((variable) => {
headerContent += generateStructMember(variable)
})

outputVariables.forEach((variable) => {
stateVariables.forEach((variable) => {
headerContent += generateStructMember(variable)
})

Expand Down
32 changes: 26 additions & 6 deletions src/frontend/utils/cpp/__tests__/generateSTCode.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { PLCVariable } from '../../../../middleware/shared/ports/types'
import { generateSTCode } from '../generateSTCode'

const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string): PLCVariable => ({
const makeScalarVar = (name: string, cls: PLCVariable['class'], baseType: string): PLCVariable => ({
name,
class: cls,
type: { definition: 'base-type', value: baseType },
Expand All @@ -10,7 +10,7 @@ const makeScalarVar = (name: string, cls: 'input' | 'output', baseType: string):
debug: false,
})

const makeArrayVar = (name: string, cls: 'input' | 'output', baseType: string, dimension: string): PLCVariable => ({
const makeArrayVar = (name: string, cls: PLCVariable['class'], baseType: string, dimension: string): PLCVariable => ({
name,
class: cls,
type: {
Expand Down Expand Up @@ -153,11 +153,11 @@ describe('generateSTCode (cpp)', () => {
expect(result).toContain('vars.D = &D[0] - 0;')
})

it('filters out local variables', () => {
it('passes C++ POU local variables as per-instance state', () => {
const localVar: PLCVariable = {
name: 'localVal',
name: 'PrevSeq',
class: 'local',
type: { definition: 'base-type', value: 'INT' },
type: { definition: 'base-type', value: 'USINT' },
location: '',
documentation: '',
debug: false,
Expand All @@ -168,6 +168,26 @@ describe('generateSTCode (cpp)', () => {
allVariables: [makeScalarVar('x', 'input', 'INT'), localVar],
})

expect(result).not.toContain('LOCALVAL')
expect(result).toContain('vars.PREVSEQ = &PREVSEQ;')
})

it('does not pass the runtime setup guard to the C++ POU struct', () => {
const result = generateSTCode({
pouName: 'test',
allVariables: [
makeScalarVar('x', 'input', 'INT'),
{
name: 'hasBeenInitialized',
class: 'local',
type: { definition: 'base-type', value: 'BOOL' },
location: '',
documentation: '',
debug: false,
},
],
})

expect(result).toContain('if hasBeenInitialized = False then')
expect(result).not.toContain('vars.HASBEENINITIALIZED')
})
})
Loading
Loading