diff --git a/.gitignore b/.gitignore index a33d9088b..ff41e9cd4 100644 --- a/.gitignore +++ b/.gitignore @@ -51,11 +51,6 @@ resources/st-compiler/**.spec # External tool binaries (downloaded by scripts/download-binaries.ts) # arduino-cli stays committed since we don't own its releases -resources/bin/**/xml2st -resources/bin/**/xml2st.exe -resources/bin/**/xml2st/ -resources/bin/.binary-metadata.json -resources/bin/**/.binary-metadata.json resources/strucpp/ # Playwright diff --git a/configs/webpack/webpack.app-info.ts b/configs/webpack/webpack.app-info.ts index 048d5c4b6..49de3c94c 100644 --- a/configs/webpack/webpack.app-info.ts +++ b/configs/webpack/webpack.app-info.ts @@ -1,13 +1,13 @@ /** - * App info configuration for webpack DefinePlugin - * Provides version from package.json and build date + * App info configuration for webpack DefinePlugin. + * + * APP_VERSION is no longer injected here: the shared About modal imports it + * from src/frontend/data/constants/app-version.ts — the single source of + * truth shared byte-for-byte with openplc-web, so the two IDEs always show + * the same version. This file now only provides the per-app product name + * (APP_NAME, "OpenPLC Editor") and the build date. */ -import { resolve } from 'path' - -// Read version from package.json -const packageJson = require(resolve(__dirname, '../../package.json')) - /** * Get current date in YYYY-MM-DD format */ @@ -24,11 +24,10 @@ function getCurrentDate(): string { * In CI builds, BUILD_DATE can be set via environment variable */ export function getAppInfoDefines() { - const version = packageJson.version as string const buildDate = process.env.BUILD_DATE || getCurrentDate() return { - APP_VERSION: JSON.stringify(version), + APP_NAME: JSON.stringify('OpenPLC Editor'), BUILD_DATE: JSON.stringify(buildDate), } } diff --git a/package-lock.json b/package-lock.json index baa953c61..5ae957ff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11036,16 +11036,6 @@ } } }, - "node_modules/@xmldom/xmldom": { - "version": "0.8.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", - "integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -24901,6 +24891,16 @@ "node": ">=10.4.0" } }, + "node_modules/plist/node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", diff --git a/package.json b/package.json index 1c7274e2d..19de4c005 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "open-plc-editor", "description": "OpenPLC Editor - IDE capable of creating programs for the OpenPLC Runtime", - "version": "4.1.4", + "version": "4.2.2", "license": "GPL-3.0", "author": { "name": "Autonomy Logic" diff --git a/release/app/package.json b/release/app/package.json index 76076e78d..e2f51913f 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,6 +1,6 @@ { "name": "open-plc-editor", - "version": "4.1.4", + "version": "4.2.2", "description": "OpenPLC Editor - IDE capable of creating programs for the OpenPLC Runtime", "license": "MIT", "author": { diff --git a/src/backend/editor/compiler/__tests__/editor-compiler-platform-port.test.ts b/src/backend/editor/compiler/__tests__/editor-compiler-platform-port.test.ts index ab4b96b40..590b5a0a7 100644 --- a/src/backend/editor/compiler/__tests__/editor-compiler-platform-port.test.ts +++ b/src/backend/editor/compiler/__tests__/editor-compiler-platform-port.test.ts @@ -129,7 +129,6 @@ describe('findHexInCompilationPath', () => { describe('createEditorCompilerPlatformPort', () => { function makeHandlers(overrides?: Partial): EditorCompilerHandlers { return { - handleTranspileXMLtoST: jest.fn(), handleCompileArduinoProgram: jest.fn(), handleUploadProgram: jest.fn(), handleCoreInstallation: jest.fn(), @@ -229,64 +228,6 @@ describe('createEditorCompilerPlatformPort', () => { expect(log).toHaveBeenCalledWith(expect.stringContaining('lib install failed'), 'warning') }) - // ---- transpileXmlToSt — xml2stArgs forwarding (STRUCT drift regression) ---- - - it('transpileXmlToSt forwards args.xml2stArgs to handleTranspileXMLtoST verbatim', async () => { - // Regression guard for the editor/web STRUCT drift bug: the - // shared pipeline owns the xml2st flag set as an array of CLI - // tokens, and the editor adapter must thread that array into - // handleTranspileXMLtoST as the third positional arg — the - // handler then splices it straight into the spawned xml2st argv. - // Editor's local xml2st is trusted, so the adapter passes the - // array through verbatim (no filtering). - const handleTranspileXMLtoST = jest - .fn< - ReturnType, - Parameters - >() - .mockResolvedValue({ success: true, data: '' }) - const tmp = mkdtempSync(join(tmpdir(), 'xml2st-args-')) - try { - const port = createEditorCompilerPlatformPort( - makeHandlers({ handleTranspileXMLtoST }), - makeContext({ sourceTargetFolderPath: tmp }), - ) - // The handler stub never produces a program.st, so the readFile - // after the spawn-equivalent step throws — that's fine, we only - // care about the xml2stArgs argument forwarded to the handler. - await port.transpileXmlToSt({ xml: '', xml2stArgs: ['--keep-structs'] }, () => undefined) - expect(handleTranspileXMLtoST).toHaveBeenCalledTimes(1) - const callArgs = handleTranspileXMLtoST.mock.calls[0]! - expect(callArgs[2]).toEqual(['--keep-structs']) - } finally { - rmSync(tmp, { recursive: true, force: true }) - } - }) - - it('transpileXmlToSt forwards an empty xml2stArgs array verbatim', async () => { - // The adapter must not "helpfully" inject defaults when the - // pipeline asked for nothing — that would be the exact kind of - // silent drift the shared port contract exists to prevent. - const handleTranspileXMLtoST = jest - .fn< - ReturnType, - Parameters - >() - .mockResolvedValue({ success: true, data: '' }) - const tmp = mkdtempSync(join(tmpdir(), 'xml2st-empty-args-')) - try { - const port = createEditorCompilerPlatformPort( - makeHandlers({ handleTranspileXMLtoST }), - makeContext({ sourceTargetFolderPath: tmp }), - ) - await port.transpileXmlToSt({ xml: '', xml2stArgs: [] }, () => undefined) - const callArgs = handleTranspileXMLtoST.mock.calls[0]! - expect(callArgs[2]).toEqual([]) - } finally { - rmSync(tmp, { recursive: true, force: true }) - } - }) - // ---- uploadArduinoBoard — port wiring (regression for issue #5) ---- it('uploadArduinoBoard forwards args.port to the handler as communicationPort', async () => { diff --git a/src/backend/editor/compiler/compiler-module.spec.ts b/src/backend/editor/compiler/compiler-module.spec.ts index 6340249b1..7cea25be6 100644 --- a/src/backend/editor/compiler/compiler-module.spec.ts +++ b/src/backend/editor/compiler/compiler-module.spec.ts @@ -113,7 +113,6 @@ describe('CompilerModule', () => { expect(typeof compilerModule.arduinoCliBinaryPath).toBe('string') expect(typeof compilerModule.arduinoCliConfigurationFilePath).toBe('string') expect(Array.isArray(compilerModule.arduinoCliBaseParameters)).toBe(true) - expect(typeof compilerModule.xml2stBinaryPath).toBe('string') expect(typeof compilerModule.strucppRuntimeDir).toBe('string') }) diff --git a/src/backend/editor/compiler/compiler-module.ts b/src/backend/editor/compiler/compiler-module.ts index c6ccd7bfb..3c55649b5 100644 --- a/src/backend/editor/compiler/compiler-module.ts +++ b/src/backend/editor/compiler/compiler-module.ts @@ -26,6 +26,11 @@ import { runLibraryBuildPipeline } from '@root/backend/shared/library/library-bu import { buildKnownPous, emitCompileErrorEvents } from '@root/backend/shared/library/program-build-helpers' import { runProgramBuildPipeline } from '@root/backend/shared/library/program-build-pipeline' import { loadStrucpp } from '@root/backend/shared/library/strucpp-runtime' +import { + fromSchemaShape, + type SchemaProjectData, + transpileToSt as runJsonTranspiler, +} from '@root/backend/shared/transpilers/st-transpiler' import type { KnownPou } from '@root/backend/shared/utils/PLC/split-program-st' /** @@ -79,6 +84,7 @@ const POST_BUILD_START_POLL_INTERVAL_MS = 150 import { assertPathContained } from '@root/backend/editor/utils/path-containment' import { getRuntimeHttpsOptions } from '@root/backend/editor/utils/runtime-https-config' +import { isNewTranspilerEnabled } from '@root/backend/editor/utils/transpiler-mode' import { runCompilePipeline } from '@root/backend/shared/compile/pipeline' import { mergeStrucppRuntimeIntoSkeleton } from '@root/backend/shared/compile/steps/merge-strucpp-runtime-into-skeleton' import { readHalsFile } from '@root/backend/shared/firmware/hals-loader' @@ -417,14 +423,6 @@ class CompilerModule { } } - #executeXml2st(args: string[]) { - let xml2stBinaryPath = this.xml2stBinaryPath - if (CompilerModule.HOST_PLATFORM === 'win32') { - xml2stBinaryPath += '.exe' - } - return spawn(xml2stBinaryPath, args) - } - #executeArduinoCliCommand(args: string[]) { let arduinoCliBinaryPath = this.arduinoCliBinaryPath if (CompilerModule.HOST_PLATFORM === 'win32') { @@ -433,6 +431,14 @@ class CompilerModule { return spawn(arduinoCliBinaryPath, args) } + #executeXml2st(args: string[]) { + let xml2stBinaryPath = this.xml2stBinaryPath + if (CompilerModule.HOST_PLATFORM === 'win32') { + xml2stBinaryPath += '.exe' + } + return spawn(xml2stBinaryPath, args) + } + // ############################################################################ // =========================== Public methods ================================= // ############################################################################ @@ -2794,37 +2800,70 @@ class CompilerModule { return } - try { - const generateXMLResult = await this.handleGenerateXMLfromJSON(sourceTargetFolderPath, projectData) - _mainProcessPort.postMessage({ - logLevel: 'info', - message: `Generated XML from JSON at: ${generateXMLResult.data?.xmlPath as string}`, - }) - } catch (error) { - _mainProcessPort.postMessage({ - logLevel: 'error', - message: `Error generating XML from JSON: ${error as string}\nStopping debug compilation process.`, - }) - _mainProcessPort.close() - return - } + if (isNewTranspilerEnabled()) { + // JSON → ST in-process via `st-transpiler`. Mirrors what + // `editor-compiler-platform-port.transpileToSt` does for the + // shared pipeline path, scoped down to the debug compile here. + try { + const ir = fromSchemaShape(projectData as unknown as SchemaProjectData) + const result = runJsonTranspiler(ir) + if (result.programSt === null || result.errors.length > 0) { + const message = result.errors.join('\n') || 'Failed to generate Structured Text' + _mainProcessPort.postMessage({ + logLevel: 'error', + message: `${message}\nStopping debug compilation process.`, + }) + _mainProcessPort.close() + return + } + for (const warning of result.warnings) { + _mainProcessPort.postMessage({ logLevel: 'info', message: warning }) + } + await mkdir(sourceTargetFolderPath, { recursive: true }) + const programStPath = join(sourceTargetFolderPath, 'program.st') + await writeFile(programStPath, result.programSt, 'utf-8') + _mainProcessPort.postMessage({ logLevel: 'info', message: `ST file generated at: ${programStPath}` }) + } catch (error) { + _mainProcessPort.postMessage({ + logLevel: 'error', + message: `Error transpiling JSON to ST: ${getErrorMessage(error)}\nStopping debug compilation process.`, + }) + _mainProcessPort.close() + return + } + } else { + try { + const generateXMLResult = await this.handleGenerateXMLfromJSON(sourceTargetFolderPath, projectData) + _mainProcessPort.postMessage({ + logLevel: 'info', + message: `Generated XML from JSON at: ${generateXMLResult.data?.xmlPath as string}`, + }) + } catch (error) { + _mainProcessPort.postMessage({ + logLevel: 'error', + message: `Error generating XML from JSON: ${error as string}\nStopping debug compilation process.`, + }) + _mainProcessPort.close() + return + } - const generatedXMLFilePath = join(sourceTargetFolderPath, 'plc.xml') - try { - await this.handleTranspileXMLtoST( - generatedXMLFilePath, - (data, logLevel) => { - _mainProcessPort.postMessage({ logLevel, message: data }) - }, - ['--keep-structs'], - ) - } catch (error) { - _mainProcessPort.postMessage({ - logLevel: 'error', - message: `Error transpiling XML to ST: ${error as string}\nStopping debug compilation process.`, - }) - _mainProcessPort.close() - return + const generatedXMLFilePath = join(sourceTargetFolderPath, 'plc.xml') + try { + await this.handleTranspileXMLtoST( + generatedXMLFilePath, + (data, logLevel) => { + _mainProcessPort.postMessage({ logLevel, message: data }) + }, + ['--keep-structs'], + ) + } catch (error) { + _mainProcessPort.postMessage({ + logLevel: 'error', + message: `Error transpiling XML to ST: ${error as string}\nStopping debug compilation process.`, + }) + _mainProcessPort.close() + return + } } try { diff --git a/src/backend/editor/compiler/desktop-library-build-port.ts b/src/backend/editor/compiler/desktop-library-build-port.ts index f368967d8..6e074e9d7 100644 --- a/src/backend/editor/compiler/desktop-library-build-port.ts +++ b/src/backend/editor/compiler/desktop-library-build-port.ts @@ -7,7 +7,10 @@ * `runLibraryBuildPipeline` cannot perform itself: * * - MD5 hashing (Node `crypto`) - * - xml2st subprocess invocation (the desktop binary) + * - ST transpilation through either backend, selected via + * `isNewTranspilerEnabled()` — the in-process JSON-fed + * transpiler when on, the bundled `xml2st` subprocess (default) + * when off * - read / write / delete project files on the local disk * - resolve library-name → `.stlib` archive via the main-process bridge * - drive a verification compile through the editor's existing @@ -17,7 +20,7 @@ * No business logic lives here. The orchestrator owns the build * sequence, cache decisions, error formatting, and the stable * `build/library/*` file names — every byte of that surface is - * shared with the web port impl that lands in a follow-up PR. + * shared with the web port impl. */ import { createHash, randomUUID } from 'node:crypto' @@ -26,7 +29,14 @@ import * as os from 'node:os' import * as path from 'node:path' import { assertPathContained } from '@root/backend/editor/utils/path-containment' -import type { TranspileXmlToStArgs, TranspileXmlToStResult } from '@root/middleware/shared/ports/compiler-platform-port' +import { isNewTranspilerEnabled } from '@root/backend/editor/utils/transpiler-mode' +import { + fromSchemaShape, + type SchemaProjectData, + transpileToSt as runJsonTranspiler, +} from '@root/backend/shared/transpilers/st-transpiler' +import { XmlGenerator } from '@root/backend/shared/utils/PLC/xml-generator' +import type { TranspileToStArgs, TranspileToStResult } from '@root/middleware/shared/ports/compiler-platform-port' import type { LibraryBuildPort } from '@root/middleware/shared/ports/library-build-port' /** @@ -39,8 +49,11 @@ export interface DesktopLibraryBuildPortDeps { * Spawn the bundled `xml2st` binary on the given input path. The * binary writes `program.st` next to its input; the port reads it * back from disk after the spawn resolves. Matches the existing - * `CompilerModule.handleTranspileXMLtoST` signature so the - * adapter can pass it through verbatim without a wrapper. + * `CompilerModule.handleTranspileXMLtoST` signature so the adapter + * can pass it through verbatim without a wrapper. + * + * Only invoked by the legacy transpile path (selected when + * `isNewTranspilerEnabled()` returns false). */ transpileXmlToSt( xmlPath: string, @@ -80,10 +93,45 @@ export function createDesktopLibraryBuildPort(deps: DesktopLibraryBuildPortDeps) return Promise.resolve(createHash('md5').update(input).digest('hex')) }, - async transpileXmlToSt( - args: TranspileXmlToStArgs, + async transpileToSt( + args: TranspileToStArgs, log: (message: string, level: 'info' | 'warning' | 'error') => void, - ): Promise { + ): Promise { + if (isNewTranspilerEnabled()) { + try { + // Editor library builds receive the same schema-shape + // project data as `compileProgram` (see `transpileToSt` on + // `editor-compiler-platform-port` for the IPC-shape note). + // The double cast bridges the port's declared port-shape type + // and the actual schema-shape payload at the boundary. + const ir = fromSchemaShape(args.projectData as unknown as SchemaProjectData) + const result = runJsonTranspiler(ir) + if (result.programSt === null || result.errors.length > 0) { + const message = result.errors.join('\n') || 'transpile-from-json failed' + log(message, 'error') + return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } + } + for (const warning of result.warnings) { + log(warning, 'info') + } + return { ok: true, programSt: result.programSt } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + log(`transpile-from-json failed: ${message}`, 'error') + return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } + } + } + + // Legacy path: serialise the project to PLCOpen XML and spawn + // the bundled `xml2st` binary on a temp file. Lifts the + // pre-Phase-2 implementation byte-for-byte; the only delta is + // the leading `XmlGenerator` call because the shared pipeline + // no longer hands the port a pre-built XML payload. + const xmlResult = XmlGenerator(args.projectData as never, 'old-editor') + if (!xmlResult.ok || !xmlResult.data) { + log(`XML generation failed: ${xmlResult.message}`, 'error') + return { ok: false, errors: [{ message: xmlResult.message, line: 0, column: 0, severity: 'error' }] } + } // xml2st takes a file path on stdin, so materialise the // in-memory XML to a unique temp file before spawning. // Lives in `os.tmpdir()` because the user-visible `plc.xml` @@ -95,12 +143,12 @@ export function createDesktopLibraryBuildPort(deps: DesktopLibraryBuildPortDeps) await fs.mkdir(sessionDir, { recursive: true }) const xmlPath = path.join(sessionDir, 'plc.xml') const programStPath = path.join(sessionDir, 'program.st') - await fs.writeFile(xmlPath, args.xml, 'utf-8') + await fs.writeFile(xmlPath, xmlResult.data, 'utf-8') await deps.transpileXmlToSt( xmlPath, (chunk, level) => log(typeof chunk === 'string' ? chunk : chunk.toString(), level ?? 'info'), - args.xml2stArgs, + ['--keep-structs'], ) const programSt = await fs.readFile(programStPath, 'utf-8') diff --git a/src/backend/editor/compiler/editor-compiler-platform-port.ts b/src/backend/editor/compiler/editor-compiler-platform-port.ts index 7419391e0..fe2b39aeb 100644 --- a/src/backend/editor/compiler/editor-compiler-platform-port.ts +++ b/src/backend/editor/compiler/editor-compiler-platform-port.ts @@ -1,8 +1,8 @@ /** * Editor-side implementation of `CompilerPlatformPort`. * - * Wraps the existing editor handlers (`handleTranspileXMLtoST`, - * `handleCompileArduinoProgram`, etc.) so the shared compile pipeline + * Wraps the existing editor handlers (`handleCompileArduinoProgram`, + * etc.) so the shared compile pipeline * (`backend/shared/compile/pipeline.ts`) can drive editor's compile * flow through the canonical platform-port contract. * @@ -16,18 +16,29 @@ * - Translates the handler's return value back into the port's * canonical result shape * - * No new pipeline logic lives here — only the platform-specific glue - * the editor needs to materialise the in-memory inputs to disk so - * `xml2st` / `arduino-cli` subprocesses can consume them, and to - * read the resulting artefacts back into memory for the pipeline. + * `transpileToSt` selects between two backends at runtime via + * `isNewTranspilerEnabled()` (env: `OPENPLC_USE_NEW_TRANSPILER`): + * the in-process JSON-fed transpiler + * (`backend/shared/transpilers/st-transpiler/`) when the flag is on, + * or the bundled `xml2st` subprocess (default) — serialising the + * project IR via `XmlGenerator` and running it through + * `handleTranspileXMLtoST` on disk, the same way the editor handled + * compilation before Phase 2. * * This module is editor-only (lives under `backend/editor/`); the * web platform implements the same port interface separately under * `middleware/adapters/web/`. */ +import { isNewTranspilerEnabled } from '@root/backend/editor/utils/transpiler-mode' import { deployRuntimeProgram } from '@root/backend/shared/library/deploy-runtime-program' import { probeRuntimeVersion } from '@root/backend/shared/library/probe-runtime-version' +import { + fromSchemaShape, + type SchemaProjectData, + transpileToSt as runJsonTranspiler, +} from '@root/backend/shared/transpilers/st-transpiler' +import { XmlGenerator } from '@root/backend/shared/utils/PLC/xml-generator' import type { CheckRuntimeVersionArgs, CheckRuntimeVersionResult, @@ -40,8 +51,8 @@ import type { PackageVppPluginResult, PlatformDeviceContext, PlatformLog, - TranspileXmlToStArgs, - TranspileXmlToStResult, + TranspileToStArgs, + TranspileToStResult, UploadArduinoBoardArgs, UploadResult, UploadRuntimeV3Args, @@ -152,17 +163,54 @@ export function createEditorCompilerPlatformPort( }, /** - * Spawn the bundled `xml2st` binary to transpile IEC 61131-3 - * XML to ST. The existing `handleTranspileXMLtoST` expects a - * file path (it opens the file via the subprocess's stdin), so - * we materialise the in-memory XML to a temp file first and - * read the produced `program.st` back from disk. + * Transpile the project IR to Structured Text. The toggle — + * `OPENPLC_USE_NEW_TRANSPILER` via `isNewTranspilerEnabled()` — selects + * between the in-process JSON-fed transpiler (new path, opt-in) + * and the bundled `xml2st` subprocess (legacy path, default). + * + * The legacy branch reproduces the pre-Phase-2 behaviour: + * serialise the project to IEC 61131-3 XML via the shared + * `XmlGenerator`, materialise it to `/plc.xml`, + * run `handleTranspileXMLtoST` (which spawns the bundled `xml2st` + * binary), then read `program.st` back from disk. */ - async transpileXmlToSt(args: TranspileXmlToStArgs, log: PlatformLog): Promise { + async transpileToSt(args: TranspileToStArgs, log: PlatformLog): Promise { + if (isNewTranspilerEnabled()) { + try { + // Editor IPC delivers the schema-shape project data + // (discriminated-union POUs + singular `configuration`). + // The port's declared `projectData` type is port-shape, but + // the pipeline reaches us with the editor's schema-shape IPC + // payload (matching `compileProgram`'s actual contract). The + // double cast bridges the static mismatch without serialising + // through `unknown` at runtime. + const ir = fromSchemaShape(args.projectData as unknown as SchemaProjectData) + const result = runJsonTranspiler(ir) + if (result.programSt === null || result.errors.length > 0) { + const message = result.errors.join('\n') || 'Failed to generate Structured Text' + log(message, 'error') + return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } + } + for (const warning of result.warnings) { + log(warning, 'info') + } + return { ok: true, programSt: result.programSt } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + log(`st-transpiler failed: ${message}`, 'error') + return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } + } + } + + const xmlResult = XmlGenerator(args.projectData as never, 'old-editor') + if (!xmlResult.ok || !xmlResult.data) { + log(`XML generation failed: ${xmlResult.message}`, 'error') + return { ok: false, errors: [{ message: xmlResult.message, line: 0, column: 0, severity: 'error' }] } + } const xmlPath = join(context.sourceTargetFolderPath, 'plc.xml') try { await fs.mkdir(dirname(xmlPath), { recursive: true }) - await fs.writeFile(xmlPath, args.xml, 'utf-8') + await fs.writeFile(xmlPath, xmlResult.data, 'utf-8') await handlers.handleTranspileXMLtoST( xmlPath, @@ -170,7 +218,7 @@ export function createEditorCompilerPlatformPort( const message = typeof chunk === 'string' ? chunk : chunk.toString() log(message, level ?? 'info') }, - args.xml2stArgs, + ['--keep-structs'], ) const programStPath = join(context.sourceTargetFolderPath, 'program.st') diff --git a/src/backend/editor/hardware/hardware-module.ts b/src/backend/editor/hardware/hardware-module.ts index d5072bec3..c51b361d2 100644 --- a/src/backend/editor/hardware/hardware-module.ts +++ b/src/backend/editor/hardware/hardware-module.ts @@ -1,11 +1,10 @@ -import { exec } from 'node:child_process' import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises' import { join, resolve as pathResolve, sep as pathSep } from 'node:path' -import { promisify } from 'node:util' import { app as electronApp } from 'electron' import { produce } from 'immer' +import { SerialPort as NodeSerialPort } from 'serialport' import { readHalsFile } from '../../shared/firmware/hals-loader' import { type BoardBuildInfo, BoardInfoResolver } from '../../shared/hardware/board-info-resolver' @@ -95,46 +94,20 @@ class HardwareModule { // ++ ============================= Getters ================================ ++ async getAvailableSerialPorts(): Promise { - let xml2stBinaryPath = join( - this.binaryDirectoryPath, - 'xml2st', - HardwareModule.HOST_PLATFORM === 'darwin' ? 'xml2st' : '', - ) - if (HardwareModule.HOST_PLATFORM === 'win32') { - xml2stBinaryPath += '.exe' - } - const executeCommand = promisify(exec) - + // Native `serialport` package replaces the legacy `xml2st + // --list-ports` subprocess (xml2st was retired when the JSON + // transpiler landed in-process; see + // `editor-compiler-platform-port.transpileToSt`). `NodeSerialPort.list()` + // returns each port's `path` plus optional vendor metadata; map + // it onto the `{name, address}` shape the renderer expects. try { - const { stdout, stderr } = await executeCommand(`"${xml2stBinaryPath}" --list-ports`) - - if (stderr) { - logger.warn(`xml2st stderr output: ${stderr}`) - } - - let normalizedOutputString: SerialPort[] = [{ name: '', address: 'fallback' }] - - if (stdout) { - try { - const parsedOutput = JSON.parse(stdout) as { - ports: { - name: string - address: string - }[] - } - normalizedOutputString = parsedOutput.ports.map((port) => ({ - name: port.name ?? port.address, - address: port.address, - })) - } catch (parseError: unknown) { - logger.error(`Failed to parse xml2st output: ${String(parseError)}`) - return [] - } - } - - return normalizedOutputString - } catch (execError: unknown) { - logger.error(`Failed to execute xml2st: ${String(execError)}`) + const ports = await NodeSerialPort.list() + return ports.map((port) => ({ + name: port.manufacturer ?? port.path, + address: port.path, + })) + } catch (error: unknown) { + logger.error(`Failed to enumerate serial ports: ${String(error)}`) return [] } } diff --git a/src/backend/editor/utils/transpiler-mode.ts b/src/backend/editor/utils/transpiler-mode.ts new file mode 100644 index 000000000..172425990 --- /dev/null +++ b/src/backend/editor/utils/transpiler-mode.ts @@ -0,0 +1,19 @@ +/** + * Build-time toggle that selects between the new in-process JSON → + * Structured Text transpiler and the legacy bundled `xml2st` + * subprocess. Default is `false` (legacy xml2st) — flip with + * `OPENPLC_USE_NEW_TRANSPILER=1` to opt into the JSON-fed transpiler. + * + * Lives in `backend/editor/utils/` so every editor-side transpilation + * call site (`editor-compiler-platform-port.transpileToSt`, + * `desktop-library-build-port.transpileToSt`, + * `CompilerModule.compileForDebugger`) reads the same flag. + * + * Named without the `use` prefix on purpose: `react-hooks/rules-of-hooks` + * treats any `use*` call inside a non-component / non-hook function as + * a violation. + */ +export function isNewTranspilerEnabled(): boolean { + const v = process.env.OPENPLC_USE_NEW_TRANSPILER + return v === '1' || v === 'true' +} diff --git a/src/backend/shared/compile/pipeline.ts b/src/backend/shared/compile/pipeline.ts index 9cdf36d1b..c38ac949e 100644 --- a/src/backend/shared/compile/pipeline.ts +++ b/src/backend/shared/compile/pipeline.ts @@ -40,7 +40,6 @@ import type { DevicePin } from '../types/PLC/devices' // (plural `configurations`) and converts at the pipeline entry — see C1 // in the architectural plan. import type { PLCProjectData } from '../types/PLC/open-plc' -import { XmlGenerator } from '../utils/PLC/xml-generator' import { buildCBlocksFromPous, composeFirmwareBundle } from './steps/compose-firmware-bundle' import { generateRuntimeConfs } from './steps/generate-confs' import { generateDefinesContent } from './steps/generate-defines' @@ -398,37 +397,25 @@ async function runCompilePipelineInner( } // --------------------------------------------------------------------- - // Step 1: Generate IEC 61131-3 XML from the project JSON. - // --------------------------------------------------------------------- - emit({ stage: 'xml', message: 'Generating IEC 61131-3 XML...', level: 'info' }) - // XmlGenerator accepts the schema-shape PLCProjectData (singular - // `configuration`). We pass through the preprocessor's output - // which is structurally compatible at runtime — see Step 0's type - // note. - const xmlResult = XmlGenerator(processedData as never, 'old-editor') - if (!xmlResult.ok || !xmlResult.data) { - return bailError(emit, 'xml', `Error generating XML from JSON: ${xmlResult.message}`) - } - const plcXml = xmlResult.data - - // --------------------------------------------------------------------- - // Step 2: Transpile XML to ST via the platform port (xml2st binary - // on editor, HTTP /generate-st on web). + // Step 1: Transpile the project IR straight to Structured Text via + // the platform port. Both adapters (editor + web) route through + // the in-process JSON-fed transpiler (`st-transpiler/`), + // so this hop never builds PLCOpen XML. Native STRUCT declarations + // are the only emission mode the transpiler supports — the legacy + // matiec struct→FB rewrite isn't ported, so there are no + // equivalents of the old `xml2stArgs` flags. // --------------------------------------------------------------------- emit({ stage: 'st', message: 'Generating Structured Text...', level: 'info' }) - // `['--keep-structs']` — strucpp parses native `STRUCT` declarations - // and rejects matiec's legacy struct→FB rewrite as a type-vs-instance - // mismatch. Editor's local xml2st always passed `--keep-structs`; - // pre-pipeline this was hardcoded inside its compiler-module while - // the web's compile-service `/generate-st` endpoint ran without it, - // causing structs to compile on the desktop and fail on the web. - // The flag set is now part of the port contract so both adapters - // observe the same tokens — future xml2st flags get appended here - // at this single call site. - const stResult = await port.transpileXmlToSt( - { xml: plcXml, xml2stArgs: ['--keep-structs'] }, - makePlatformLog(emit, 'st'), - ) + // The pipeline carries the editor's schema-shape `PLCProjectData`, + // but the port's `transpileToSt` is typed against the renderer's + // port-shape (`middleware/shared/ports/types`). The two diverge in + // POU layout (discriminated union vs. flat record) and configuration + // field name (`configuration` vs. `configurations`). Each platform + // port impl knows which shape it actually receives — desktop/editor + // routes through `fromSchemaShape`; web routes through `fromPortShape` + // after converting at the adapter boundary. Casting to `never` here + // erases the structural mismatch without losing runtime fidelity. + const stResult = await port.transpileToSt({ projectData: processedData as never }, makePlatformLog(emit, 'st')) if (!stResult.ok || !stResult.programSt) { if (stResult.errors && stResult.errors.length > 0) { emitCompileErrorEvents( diff --git a/src/backend/shared/library/build-pipeline.ts b/src/backend/shared/library/build-pipeline.ts index 5369053e3..622eff878 100644 --- a/src/backend/shared/library/build-pipeline.ts +++ b/src/backend/shared/library/build-pipeline.ts @@ -38,7 +38,6 @@ import type { PLCProject, PLCProjectData } from '@root/backend/shared/types/PLC/open-plc' import { checkPathId } from '@root/backend/shared/utils/path-safety' import { type KnownPou, splitProgramSt } from '@root/backend/shared/utils/PLC/split-program-st' -import { XmlGenerator } from '@root/backend/shared/utils/PLC/xml-generator' import { compileStlib, type CompileStlibError, type CompileStlibSource } from './compile-stlib' @@ -216,24 +215,29 @@ const STUB_SPLIT_FILENAME = `${STUB_PROGRAM_NAME}.st` // --------------------------------------------------------------------------- export interface PrepareXmlResult { - /** Plc.xml content the caller passes to xml2st. */ - xml: string - /** POU list the splitter needs to slice the xml2st output. + /** Stubbed project data — passes directly into the JSON-fed + * transpiler via `port.transpileToSt({ projectData })`. The + * stub adds a synthesised `main` program so the transpiler's + * "requires a main POU" check passes. */ + projectData: PLCProject['data'] + /** POU list the splitter needs to slice the transpiler output. * Includes the stub so the splitter recognises and emits a slice * for it — caller then drops that slice. */ knownPous: KnownPou[] /** Manifest the second stage reads, parsed here so a malformed - * manifest bails BEFORE xml2st runs. */ + * manifest bails BEFORE the transpile step. */ manifest: LibraryBuildManifest } export type PrepareXmlOutcome = PrepareXmlResult | { error: string } /** - * Stage 1. Validates the manifest and produces the XML xml2st - * consumes. Returns `{error}` when the manifest fails validation - * — caller surfaces that as a build error and bails before - * spawning xml2st. + * Stage 1. Validates the manifest and returns the stubbed project + * data the JSON transpiler consumes (plus the POU list the splitter + * needs). The "stub" adds a synthesised `main` program so the + * transpiler's "requires a main POU" guard passes — the caller drops + * the stub's slice from the splitter output downstream. Returns + * `{error}` when the manifest fails validation. */ export function prepareXmlForLibraryBuild(project: PLCProject, manifestJson: string): PrepareXmlOutcome { const parsed = parseLibraryManifest(manifestJson) @@ -242,15 +246,6 @@ export function prepareXmlForLibraryBuild(project: PLCProject, manifestJson: str } const stubbed = stubProgramFor(project) - // `'old-editor'` keeps the XML shape compatible with the bundled - // xml2st binary (the MatIEC-era flavor), which is the same pipeline - // every other library/program build in this repo speaks to. The - // `'codesys'` flavor exists for export-only paths; using it here - // would leave xml2st unable to find the program block. - const xmlRes = XmlGenerator(stubbed.data, 'old-editor') - if (!xmlRes.ok || !xmlRes.data) { - return { error: `XML generation failed: ${xmlRes.message ?? 'unknown error'}` } - } const knownPous: KnownPou[] = stubbed.data.pous.map((p) => ({ name: p.data.name, @@ -258,7 +253,7 @@ export function prepareXmlForLibraryBuild(project: PLCProject, manifestJson: str language: p.data.language, })) - return { xml: xmlRes.data, knownPous, manifest: parsed.manifest } + return { projectData: stubbed.data, knownPous, manifest: parsed.manifest } } // --------------------------------------------------------------------------- diff --git a/src/backend/shared/library/library-build-orchestrator.ts b/src/backend/shared/library/library-build-orchestrator.ts index 44b539383..f7ffa991a 100644 --- a/src/backend/shared/library/library-build-orchestrator.ts +++ b/src/backend/shared/library/library-build-orchestrator.ts @@ -15,11 +15,11 @@ * Stages: * * 0. Read `library.json` manifest. - * 1. Validate manifest + generate plc.xml (via shared - * `prepareXmlForLibraryBuild`). - * 2. xml2st → program.st (via `LibraryBuildPort.transpileXmlToSt`). - * The XML and ST live in memory only — no intermediate file - * persistence (see path-constants comment). + * 1. Validate manifest + stub the project for the transpiler + * (via shared `prepareXmlForLibraryBuild`). + * 2. Project IR → program.st (via `LibraryBuildPort.transpileToSt`). + * The ST lives in memory only — no intermediate file persistence + * (see path-constants comment). * 3. Resolve project-enabled library archives + fail on missing * names (one place — feeds BOTH verification and strucpp). * 4. Verification compile against the OpenPLC Simulator target @@ -128,29 +128,29 @@ export async function runLibraryBuildPipeline( if ('error' in stage1) { return fail(emit, stage1.error) } - const { xml, knownPous, manifest } = stage1 + const { projectData: stubbedData, knownPous, manifest } = stage1 emit({ message: `Manifest OK — building "${manifest.name}" v${manifest.version}.`, level: 'info' }) // ------------------------------------------------------------------------- - // Stage 2: xml2st via the shared compiler platform port + // Stage 2: project → ST via the shared compiler platform port. // - // The shared port abstracts the spawn vs HTTP divergence; the - // `--keep-structs` arg mirrors what `runCompilePipeline` passes - // for program builds so manifest type compatibility carries over. - // The XML and the resulting ST live in memory only — no - // intermediate-file persistence, see the path-constants comment. + // The port routes through the in-process JSON-fed transpiler + // (`st-transpiler/`). Native STRUCT emission is the only + // mode — no equivalents of the old `--keep-structs` flag exist. + // The resulting ST lives in memory only. // ------------------------------------------------------------------------- - emit({ message: 'Compiling file plc.xml', level: 'info' }) - const transpile = await port.transpileXmlToSt( - { xml, xml2stArgs: ['--keep-structs'] }, - // Forward xml2st's stdout / stderr to the caller — same channel - // the desktop pre-refactor compileLibrary used (`post(message, - // logLevel)`), so the console output stays unchanged. - (message, level) => emit({ message, level }), + emit({ message: 'Transpiling project to Structured Text', level: 'info' }) + // `stubbedData` is editor schema-shape; the port's signature is + // port-shape. Each platform port impl knows the actual shape it + // receives (desktop → `fromSchemaShape`; web → `fromPortShape` after + // its adapter converts). See the matching cast site in + // `pipeline.ts` (Step 1) for the same comment. + const transpile = await port.transpileToSt({ projectData: stubbedData as never }, (message, level) => + emit({ message, level }), ) if (!transpile.ok || !transpile.programSt) { - const firstError = transpile.errors?.[0]?.message ?? 'xml2st failed' - return fail(emit, `xml2st failed: ${firstError}`, { libraryName: manifest.name }) + const firstError = transpile.errors?.[0]?.message ?? 'transpile-from-json failed' + return fail(emit, `transpile-from-json failed: ${firstError}`, { libraryName: manifest.name }) } const programSt = transpile.programSt diff --git a/src/backend/shared/transpilers/st-transpiler/core/modifier-types.ts b/src/backend/shared/transpilers/st-transpiler/core/modifier-types.ts new file mode 100644 index 000000000..572cc54f0 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/core/modifier-types.ts @@ -0,0 +1,16 @@ +/** + * Contact / coil modifier shapes. Shared by the walker (which derives + * them from React Flow `data.variant` via `narrow.ts`) and by + * `extractModifier` (which translates them into ST emission steps). + */ + +export interface ContactModifier { + negated?: boolean + edge?: 'rising' | 'falling' +} + +export interface CoilModifier { + negated?: boolean + storage?: 'set' | 'reset' + edge?: 'rising' | 'falling' +} diff --git a/src/backend/shared/transpilers/st-transpiler/core/modifiers.ts b/src/backend/shared/transpilers/st-transpiler/core/modifiers.ts new file mode 100644 index 000000000..c38db6d2b --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/core/modifiers.ts @@ -0,0 +1,97 @@ +/** + * Modifier extraction for contacts and coils. + * + * Mirrors `src/PLCGenerator/modifiers.ts` exactly — three modifier + * kinds, byte-faithful emission: + * + * - `negated` → wrap expression in `NOT(...)` + * - `storage` (`set` / `reset`) → side-effect on the trailing + * program chunks; return a short "TRUE; END_IF" / "FALSE; END_IF" + * reference the caller substitutes + * - `edge` (`rising` / `falling`) → synthesize an `R_TRIG_N` / + * `F_TRIG_N` instance variable, emit its `(CLK := expr);` + * invocation, return `.Q` reference + * + * State carried via `WalkerState` (defined in `walker.ts`). + */ + +import type { CoilModifier, ContactModifier } from './modifier-types' +import type { Location, ProgramChunk } from './path-tree' +import type { TriggerVar } from './trigger-var' + +/** + * Subset of `WalkerState` that the modifier helpers actually read or + * mutate. Lets the React Flow walker (which carries its own state + * shape without `body` / `connectorExprs`) share this module without + * type casts. `WalkerState` itself satisfies `ModifierState` + * structurally — no migration needed for the legacy walker. + */ +export interface ModifierState { + program: ProgramChunk[] + currentIndent: string + declaredVars: Set + triggerVars: TriggerVar[] +} + +export function extractModifier( + state: ModifierState, + modifier: ContactModifier | CoilModifier, + expression: ProgramChunk[], + varInfo: Location, +): ProgramChunk[] { + if (modifier.negated) { + return [['NOT(', [...varInfo, 'negated']], ...expression, [')', []]] + } + if ('storage' in modifier) { + const storage = modifier.storage + if (storage === 'set' || storage === 'reset') { + state.program.push([`${state.currentIndent}IF `, [...varInfo, storage]]) + state.program.push(...expression) + state.program.push([' THEN\n ', []]) + const value = storage === 'set' ? 'TRUE' : 'FALSE' + return [[`${value}; (*${storage}*)\n${state.currentIndent}END_IF`, []]] + } + } + if (modifier.edge === 'rising') { + return addTrigger(state, 'R_TRIG', expression, [...varInfo, 'rising']) + } + if (modifier.edge === 'falling') { + return addTrigger(state, 'F_TRIG', expression, [...varInfo, 'falling']) + } + return expression +} + +/** Inject a `R_TRIG`/`F_TRIG` instance var into the POU's interface + * and emit its `(CLK := expr);` invocation. Returns `[`.Q`]` + * for the caller to substitute. */ +function addTrigger( + state: ModifierState, + edge: 'R_TRIG' | 'F_TRIG', + expression: ProgramChunk[], + varInfo: Location, +): ProgramChunk[] { + ensureTriggerVarSection(state) + let i = 1 + let name = `${edge}${i}` + while (state.declaredVars.has(name)) { + i++ + name = `${edge}${i}` + } + state.declaredVars.add(name) + state.triggerVars.push({ name, type: edge }) + + state.program.push([state.currentIndent, []]) + state.program.push([name, varInfo]) + state.program.push(['(CLK := ', []]) + state.program.push(...expression) + state.program.push([');\n', []]) + + return [[`${name}.Q`, varInfo]] +} + +function ensureTriggerVarSection(state: ModifierState): void { + // Trigger vars accumulate into `state.triggerVars`; the caller + // flushes them into the POU's `VAR` block before final assembly. + // No-op here beyond ensuring the array exists (always does). + void state +} diff --git a/src/backend/shared/transpilers/st-transpiler/core/path-tree.ts b/src/backend/shared/transpilers/st-transpiler/core/path-tree.ts new file mode 100644 index 000000000..f76337e7f --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/core/path-tree.ts @@ -0,0 +1,253 @@ +/** + * Path-tree data model + algorithms — the LD/FBD core that turns a + * graph walk into ST chunks. + * + * Mirrors `src/PLCGenerator/path_tree.ts` line-for-line but reads + * from the JSON `LdBody` IR instead of `@xmldom` `Element`s. The + * algorithm itself (factorization, ComputePaths, pythonRepr-stable + * keying) is pure JS and is preserved verbatim from the DOM walker. + * + * Chunks emitted here are the same `[text, location]` tuples the + * existing transpiler produces, so downstream assembly stays + * compatible. + */ + +/* ─────────────────────────── chunk model ────────────────────────────────── */ + +export type LocationAtom = string | number | readonly (string | number)[] +export type Location = readonly LocationAtom[] +export type ProgramChunk = readonly [text: string, location: Location] + +/* ─────────────────────────── path nodes ─────────────────────────────────── */ + +/** + * One node in the path tree. Same shape as the DOM walker's + * `PathNode`; algorithms below are byte-faithful ports. + */ +export type PathNode = + | { readonly kind: 'true' } + | { readonly kind: 'leaf'; readonly chunks: readonly ProgramChunk[] } + | { readonly kind: 'and'; readonly children: readonly PathNode[] } + | { readonly kind: 'or'; readonly children: readonly PathNode[] } + +export const TRUE_NODE: PathNode = { kind: 'true' } + +export function leafNode(chunks: ProgramChunk[]): PathNode { + return { kind: 'leaf', chunks } +} + +/* ─────────────────────────── pythonRepr ─────────────────────────────────── */ + +/** + * Stable structural key for a PathNode — used by `factorizePaths` to + * detect common terms. Identical to the Python `repr` output the + * original xml2st pipeline keys on. + */ +export function pythonReprNode(node: PathNode): string { + switch (node.kind) { + case 'true': + return 'None' + case 'leaf': + return pythonReprChunks(node.chunks) + case 'and': + return `[${node.children.map(pythonReprNode).join(', ')}]` + case 'or': + if (node.children.length === 0) return '()' + if (node.children.length === 1) return `(${pythonReprNode(node.children[0])},)` + return `(${node.children.map(pythonReprNode).join(', ')})` + } +} + +export function pythonReprChunks(chunks: readonly ProgramChunk[]): string { + return `[${chunks.map(pythonReprChunk).join(', ')}]` +} + +function pythonReprChunk(chunk: ProgramChunk): string { + const [text, location] = chunk + return `(${pythonReprString(text)}, ${pythonReprLocation(location)})` +} + +function pythonReprLocation(loc: Location): string { + if (loc.length === 0) return '()' + if (loc.length === 1) return `(${pythonReprPrimitive(loc[0])},)` + return `(${loc.map(pythonReprPrimitive).join(', ')})` +} + +function pythonReprPrimitive(v: LocationAtom): string { + if (Array.isArray(v)) { + if (v.length === 0) return '()' + if (v.length === 1) return `(${pythonReprPrimitive(v[0])},)` + return `(${v.map(pythonReprPrimitive).join(', ')})` + } + if (typeof v === 'number') { + return Number.isInteger(v) ? v.toString(10) : v.toString() + } + return pythonReprString(v as string) +} + +export function pythonReprString(s: string): string { + // Python prefers single quotes; switches to double quotes if the + // string contains a `'` and not a `"`. + const hasSingle = s.includes("'") + const hasDouble = s.includes('"') + const quote = hasSingle && !hasDouble ? '"' : "'" + let out = '' + for (const ch of s) { + const code = ch.codePointAt(0) ?? 0 + if (ch === quote) out += `\\${ch}` + else if (ch === '\\') out += '\\\\' + else if (ch === '\n') out += '\\n' + else if (ch === '\r') out += '\\r' + else if (ch === '\t') out += '\\t' + else if (code < 0x20 || code === 0x7f) out += `\\x${code.toString(16).padStart(2, '0')}` + else out += ch + } + return `${quote}${out}${quote}` +} + +/* ─────────────────────────── factorizePaths ─────────────────────────────── */ + +/** + * Boolean simplification — `(A AND B) OR (A AND C)` → `A AND (B OR C)`. + * + * Strategy mirrors `FactorizePaths` (PLCGenerator.py:1429): + * 1. Sort the paths by their Python `repr`. + * 2. For each pair, find common head/tail prefixes/suffixes between + * adjacent AND nodes. + * 3. Replace with `[common_head, OR(unique_middles), common_tail]`. + */ +export function factorizePaths(paths: readonly PathNode[]): PathNode[] { + if (paths.length <= 1) return [...paths] + + // Sort by stable repr key — same order Python produces. + const sorted = pythonStableSort(paths) + + // Walk sorted list, group adjacent paths sharing head AND. + const out: PathNode[] = [] + let i = 0 + while (i < sorted.length) { + let j = i + 1 + // Find run of paths that share the same head/tail with sorted[i]. + while (j < sorted.length && canFactor(sorted[i], sorted[j])) j++ + if (j - i === 1) { + out.push(sorted[i]) + i = j + continue + } + // Build the factored shape. + const group = sorted.slice(i, j) + out.push(factorGroup(group)) + i = j + } + return out +} + +/** Stable Python-style sort by `pythonReprNode` key. */ +export function pythonStableSort(nodes: readonly PathNode[]): PathNode[] { + return [...nodes].sort((a, b) => { + const ka = pythonReprNode(a) + const kb = pythonReprNode(b) + return ka < kb ? -1 : ka > kb ? 1 : 0 + }) +} + +/** Two paths can be factored if they share at least one head OR tail leaf. */ +function canFactor(a: PathNode, b: PathNode): boolean { + const aList = andChildren(a) + const bList = andChildren(b) + if (aList.length === 0 || bList.length === 0) return false + return ( + pythonReprNode(aList[0]) === pythonReprNode(bList[0]) || + pythonReprNode(aList[aList.length - 1]) === pythonReprNode(bList[bList.length - 1]) + ) +} + +function andChildren(node: PathNode): readonly PathNode[] { + return node.kind === 'and' ? node.children : [node] +} + +function factorGroup(group: readonly PathNode[]): PathNode { + // Find common head length and common tail length. + const lists = group.map(andChildren) + let headLen = 0 + outerHead: while (true) { + const ref = lists[0][headLen] + if (ref === undefined) break + const refKey = pythonReprNode(ref) + for (let k = 1; k < lists.length; k++) { + if (lists[k][headLen] === undefined) break outerHead + if (pythonReprNode(lists[k][headLen]) !== refKey) break outerHead + } + headLen++ + } + let tailLen = 0 + outerTail: while (true) { + const minLeft = Math.min(...lists.map((l) => l.length - headLen)) + if (tailLen >= minLeft) break + const ref = lists[0][lists[0].length - 1 - tailLen] + const refKey = pythonReprNode(ref) + for (let k = 1; k < lists.length; k++) { + if (pythonReprNode(lists[k][lists[k].length - 1 - tailLen]) !== refKey) break outerTail + } + tailLen++ + } + + const head = lists[0].slice(0, headLen) + const tail = headLen + tailLen >= lists[0].length ? [] : lists[0].slice(lists[0].length - tailLen) + const middles: PathNode[] = lists.map((l) => { + const mid = l.slice(headLen, l.length - tailLen) + if (mid.length === 0) return TRUE_NODE + if (mid.length === 1) return mid[0] + return { kind: 'and', children: mid } + }) + + const orNode: PathNode = { kind: 'or', children: pythonStableSort(middles) } + const allChildren: PathNode[] = [...head, orNode, ...tail] + if (allChildren.length === 1) return allChildren[0] + return { kind: 'and', children: allChildren } +} + +/* ─────────────────────────── computePaths ───────────────────────────────── */ + +/** + * Turn a `PathNode` into its ST expression chunks. + * + * `first=true` at the outermost call suppresses the parenthesis + * around the top-level OR — matches Python's `first` flag. + */ +export function computePaths(node: PathNode, first = false): ProgramChunk[] { + switch (node.kind) { + case 'true': + return [['TRUE', []]] + case 'leaf': + return [...node.chunks] + case 'and': + return joinChunkLists( + node.children.map((c) => computePaths(c)), + [[' AND ', []]], + ) + case 'or': { + const inner = joinChunkLists( + node.children.map((c) => computePaths(c)), + [[' OR ', []]], + ) + if (first) return inner + const out: ProgramChunk[] = [['(', []]] + out.push(...inner) + out.push([')', []]) + return out + } + } +} + +function joinChunkLists( + groups: readonly (readonly ProgramChunk[])[], + separator: readonly ProgramChunk[], +): ProgramChunk[] { + const out: ProgramChunk[] = [] + for (let i = 0; i < groups.length; i++) { + if (i > 0) out.push(...separator) + out.push(...groups[i]) + } + return out +} diff --git a/src/backend/shared/transpilers/st-transpiler/core/trigger-var.ts b/src/backend/shared/transpilers/st-transpiler/core/trigger-var.ts new file mode 100644 index 000000000..b09329aa1 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/core/trigger-var.ts @@ -0,0 +1,12 @@ +/** + * Edge-trigger instance variable (`R_TRIG1`, `F_TRIG1`, …) synthesised + * by `extractModifier` when a contact/coil carries a rising/falling + * edge modifier. The walker accumulates these in its state, the wrap + * declares them in the trailing local `VAR` section. + */ + +export interface TriggerVar { + /** `R_TRIG1`, `R_TRIG2`, … or `F_TRIG1`, `F_TRIG2`, … */ + name: string + type: 'R_TRIG' | 'F_TRIG' +} diff --git a/src/backend/shared/transpilers/st-transpiler/data/std_block_catalog.json b/src/backend/shared/transpilers/st-transpiler/data/std_block_catalog.json new file mode 100644 index 000000000..daf0e5474 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/data/std_block_catalog.json @@ -0,0 +1,15855 @@ +{ + "SR": [ + { + "section": "Standard function blocks", + "infos": { + "name": "SR", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "S1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q1", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "The SR bistable is a latch where the Set dominates.", + "usage": "\n (BOOL:S1, BOOL:R) => (BOOL:Q1)" + } + } + ], + "RS": [ + { + "section": "Standard function blocks", + "infos": { + "name": "RS", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "S", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "R1", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q1", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "The RS bistable is a latch where the Reset dominates.", + "usage": "\n (BOOL:S, BOOL:R1) => (BOOL:Q1)" + } + } + ], + "SEMA": [ + { + "section": "Standard function blocks", + "infos": { + "name": "SEMA", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CLAIM", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELEASE", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "BUSY", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "The semaphore provides a mechanism to allow software elements mutually exclusive access to certain resources.", + "usage": "\n (BOOL:CLAIM, BOOL:RELEASE) => (BOOL:BUSY)" + } + } + ], + "R_TRIG": [ + { + "section": "Standard function blocks", + "infos": { + "name": "R_TRIG", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CLK", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "The output produces a single pulse when a rising edge is detected.", + "usage": "\n (BOOL:CLK) => (BOOL:Q)" + } + } + ], + "F_TRIG": [ + { + "section": "Standard function blocks", + "infos": { + "name": "F_TRIG", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CLK", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "The output produces a single pulse when a falling edge is detected.", + "usage": "\n (BOOL:CLK) => (BOOL:Q)" + } + } + ], + "CTU": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTU", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "The up-counter can be used to signal when a count has reached a maximum value.", + "usage": "\n (BOOL:CU, BOOL:R, INT:PV) => (BOOL:Q, INT:CV)" + } + } + ], + "CTU_DINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTU_DINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "The up-counter can be used to signal when a count has reached a maximum value.", + "usage": "\n (BOOL:CU, BOOL:R, DINT:PV) => (BOOL:Q, DINT:CV)" + } + } + ], + "CTU_LINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTU_LINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "The up-counter can be used to signal when a count has reached a maximum value.", + "usage": "\n (BOOL:CU, BOOL:R, LINT:PV) => (BOOL:Q, LINT:CV)" + } + } + ], + "CTU_UDINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTU_UDINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "The up-counter can be used to signal when a count has reached a maximum value.", + "usage": "\n (BOOL:CU, BOOL:R, UDINT:PV) => (BOOL:Q, UDINT:CV)" + } + } + ], + "CTU_ULINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTU_ULINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "The up-counter can be used to signal when a count has reached a maximum value.", + "usage": "\n (BOOL:CU, BOOL:R, ULINT:PV) => (BOOL:Q, ULINT:CV)" + } + } + ], + "CTD": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTD", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "The down-counter can be used to signal when a count has reached zero, on counting down from a preset value.", + "usage": "\n (BOOL:CD, BOOL:LD, INT:PV) => (BOOL:Q, INT:CV)" + } + } + ], + "CTD_DINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTD_DINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "The down-counter can be used to signal when a count has reached zero, on counting down from a preset value.", + "usage": "\n (BOOL:CD, BOOL:LD, DINT:PV) => (BOOL:Q, DINT:CV)" + } + } + ], + "CTD_LINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTD_LINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "The down-counter can be used to signal when a count has reached zero, on counting down from a preset value.", + "usage": "\n (BOOL:CD, BOOL:LD, LINT:PV) => (BOOL:Q, LINT:CV)" + } + } + ], + "CTD_UDINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTD_UDINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "The down-counter can be used to signal when a count has reached zero, on counting down from a preset value.", + "usage": "\n (BOOL:CD, BOOL:LD, UDINT:PV) => (BOOL:Q, UDINT:CV)" + } + } + ], + "CTD_ULINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTD_ULINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "The down-counter can be used to signal when a count has reached zero, on counting down from a preset value.", + "usage": "\n (BOOL:CD, BOOL:LD, ULINT:PV) => (BOOL:Q, ULINT:CV)" + } + } + ], + "CTUD": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTUD", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "QU", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "QD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "INT", + "qualifier": "none" + }, + { + "name": "CD_T", + "type": "R_TRIG", + "qualifier": "none" + }, + { + "name": "CU_T", + "type": "R_TRIG", + "qualifier": "none" + } + ], + "comment": "The up-down counter has two inputs CU and CD. It can be used to both count up on one input and down on the other.", + "usage": "\n (BOOL:CU, BOOL:CD, BOOL:R, BOOL:LD, INT:PV) => (BOOL:QU, BOOL:QD, INT:CV, R_TRIG:CD_T, R_TRIG:CU_T)" + } + } + ], + "CTUD_DINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTUD_DINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "QU", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "QD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "DINT", + "qualifier": "none" + }, + { + "name": "CD_T", + "type": "R_TRIG", + "qualifier": "none" + }, + { + "name": "CU_T", + "type": "R_TRIG", + "qualifier": "none" + } + ], + "comment": "The up-down counter has two inputs CU and CD. It can be used to both count up on one input and down on the other.", + "usage": "\n (BOOL:CU, BOOL:CD, BOOL:R, BOOL:LD, DINT:PV) => (BOOL:QU, BOOL:QD, DINT:CV, R_TRIG:CD_T, R_TRIG:CU_T)" + } + } + ], + "CTUD_LINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTUD_LINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "QU", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "QD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "LINT", + "qualifier": "none" + }, + { + "name": "CD_T", + "type": "R_TRIG", + "qualifier": "none" + }, + { + "name": "CU_T", + "type": "R_TRIG", + "qualifier": "none" + } + ], + "comment": "The up-down counter has two inputs CU and CD. It can be used to both count up on one input and down on the other.", + "usage": "\n (BOOL:CU, BOOL:CD, BOOL:R, BOOL:LD, LINT:PV) => (BOOL:QU, BOOL:QD, LINT:CV, R_TRIG:CD_T, R_TRIG:CU_T)" + } + } + ], + "CTUD_UDINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTUD_UDINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "QU", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "QD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "UDINT", + "qualifier": "none" + }, + { + "name": "CD_T", + "type": "R_TRIG", + "qualifier": "none" + }, + { + "name": "CU_T", + "type": "R_TRIG", + "qualifier": "none" + } + ], + "comment": "The up-down counter has two inputs CU and CD. It can be used to both count up on one input and down on the other.", + "usage": "\n (BOOL:CU, BOOL:CD, BOOL:R, BOOL:LD, UDINT:PV) => (BOOL:QU, BOOL:QD, UDINT:CV, R_TRIG:CD_T, R_TRIG:CU_T)" + } + } + ], + "CTUD_ULINT": [ + { + "section": "Standard function blocks", + "infos": { + "name": "CTUD_ULINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CU", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "CD", + "type": "BOOL", + "qualifier": "rising" + }, + { + "name": "R", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "QU", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "QD", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CV", + "type": "ULINT", + "qualifier": "none" + }, + { + "name": "CD_T", + "type": "R_TRIG", + "qualifier": "none" + }, + { + "name": "CU_T", + "type": "R_TRIG", + "qualifier": "none" + } + ], + "comment": "The up-down counter has two inputs CU and CD. It can be used to both count up on one input and down on the other.", + "usage": "\n (BOOL:CU, BOOL:CD, BOOL:R, BOOL:LD, ULINT:PV) => (BOOL:QU, BOOL:QD, ULINT:CV, R_TRIG:CD_T, R_TRIG:CU_T)" + } + } + ], + "TP": [ + { + "section": "Standard function blocks", + "infos": { + "name": "TP", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PT", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "ET", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "The pulse timer can be used to generate output pulses of a given time duration.", + "usage": "\n (BOOL:IN, TIME:PT) => (BOOL:Q, TIME:ET)" + } + } + ], + "TON": [ + { + "section": "Standard function blocks", + "infos": { + "name": "TON", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PT", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "ET", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "The on-delay timer can be used to delay setting an output true, for fixed period after an input becomes true.", + "usage": "\n (BOOL:IN, TIME:PT) => (BOOL:Q, TIME:ET)" + } + } + ], + "TOF": [ + { + "section": "Standard function blocks", + "infos": { + "name": "TOF", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PT", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "ET", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "The off-delay timer can be used to delay setting an output false, for fixed period after input goes false.", + "usage": "\n (BOOL:IN, TIME:PT) => (BOOL:Q, TIME:ET)" + } + } + ], + "RTC": [ + { + "section": "Additional function blocks", + "infos": { + "name": "RTC", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PDT", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CDT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "The real time clock has many uses including time stamping, setting dates and times of day in batch reports, in alarm messages and so on.", + "usage": "\n (BOOL:IN, DT:PDT) => (BOOL:Q, DT:CDT)" + } + } + ], + "INTEGRAL": [ + { + "section": "Additional function blocks", + "infos": { + "name": "INTEGRAL", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "RUN", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "R1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "XIN", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "X0", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "CYCLE", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "XOUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "The integral function block integrates the value of input XIN over time.", + "usage": "\n (BOOL:RUN, BOOL:R1, REAL:XIN, REAL:X0, TIME:CYCLE) => (BOOL:Q, REAL:XOUT)" + } + } + ], + "DERIVATIVE": [ + { + "section": "Additional function blocks", + "infos": { + "name": "DERIVATIVE", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "RUN", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "XIN", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "CYCLE", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "XOUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "The derivative function block produces an output XOUT proportional to the rate of change of the input XIN.", + "usage": "\n (BOOL:RUN, REAL:XIN, TIME:CYCLE) => (REAL:XOUT)" + } + } + ], + "PID": [ + { + "section": "Additional function blocks", + "infos": { + "name": "PID", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "AUTO", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PV", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "SP", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "X0", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "KP", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TR", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TD", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "CYCLE", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "XOUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "The PID (proportional, Integral, Derivative) function block provides the classical three term controller for closed loop control.", + "usage": "\n (BOOL:AUTO, REAL:PV, REAL:SP, REAL:X0, REAL:KP, REAL:TR, REAL:TD, TIME:CYCLE) => (REAL:XOUT)" + } + } + ], + "RAMP": [ + { + "section": "Additional function blocks", + "infos": { + "name": "RAMP", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "RUN", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "X0", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "X1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TR", + "type": "TIME", + "qualifier": "none" + }, + { + "name": "CYCLE", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "BUSY", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "XOUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "The RAMP function block is modelled on example given in the standard.", + "usage": "\n (BOOL:RUN, REAL:X0, REAL:X1, TIME:TR, TIME:CYCLE) => (BOOL:BUSY, REAL:XOUT)" + } + } + ], + "HYSTERESIS": [ + { + "section": "Additional function blocks", + "infos": { + "name": "HYSTERESIS", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "XIN1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "XIN2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "EPS", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "Q", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "The hysteresis function block provides a hysteresis boolean output driven by the difference of two floating point (REAL) inputs XIN1 and XIN2.", + "usage": "\n (REAL:XIN1, REAL:XIN2, REAL:EPS) => (BOOL:Q)" + } + } + ], + "DS18B20": [ + { + "section": "Arduino", + "infos": { + "name": "DS18B20", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "PIN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Reads temperature from one DS18B20 one-wire sensor connected to the pin specified in PIN", + "usage": "\n (SINT:PIN) => (REAL:OUT)" + } + } + ], + "DS18B20_2_OUT": [ + { + "section": "Arduino", + "infos": { + "name": "DS18B20_2_OUT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "PIN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT_0", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_1", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Reads temperature from two DS18B20 one-wire sensors. Both sensors must be on the same bus connected to the pin specified in PIN", + "usage": "\n (SINT:PIN) => (REAL:OUT_0, REAL:OUT_1)" + } + } + ], + "DS18B20_3_OUT": [ + { + "section": "Arduino", + "infos": { + "name": "DS18B20_3_OUT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "PIN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT_0", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_2", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Reads temperature from three DS18B20 one-wire sensors. All sensors must be on the same bus connected to the pin specified in PIN", + "usage": "\n (SINT:PIN) => (REAL:OUT_0, REAL:OUT_1, REAL:OUT_2)" + } + } + ], + "DS18B20_4_OUT": [ + { + "section": "Arduino", + "infos": { + "name": "DS18B20_4_OUT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "PIN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT_0", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_3", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Reads temperature from four DS18B20 one-wire sensors. All sensors must be on the same bus connected to the pin specified in PIN", + "usage": "\n (SINT:PIN) => (REAL:OUT_0, REAL:OUT_1, REAL:OUT_2, REAL:OUT_3)" + } + } + ], + "DS18B20_5_OUT": [ + { + "section": "Arduino", + "infos": { + "name": "DS18B20_5_OUT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "PIN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT_0", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OUT_4", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Reads temperature from five DS18B20 one-wire sensors. All sensors must be on the same bus connected to the pin specified in PIN", + "usage": "\n (SINT:PIN) => (REAL:OUT_0, REAL:OUT_1, REAL:OUT_2, REAL:OUT_3, REAL:OUT_4)" + } + } + ], + "CLOUD_ADD_BOOL": [ + { + "section": "Arduino", + "infos": { + "name": "CLOUD_ADD_BOOL", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "VAR_NAME", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "BOOL_VAR", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [], + "comment": "Add a BOOL variable to sync with the Arduino Cloud. VAR_NAME must have the same name as the variable set up in the Arduino IoT Cloud", + "usage": "\n (STRING:VAR_NAME, BOOL:BOOL_VAR) => ()" + } + } + ], + "CLOUD_ADD_DINT": [ + { + "section": "Arduino", + "infos": { + "name": "CLOUD_ADD_DINT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "VAR_NAME", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "DINT_VAR", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [], + "comment": "Add an DINT variable (Arduino int) to sync with the Arduino Cloud. VAR_NAME must have the same name as the variable set up in the Arduino IoT Cloud", + "usage": "\n (STRING:VAR_NAME, DINT:DINT_VAR) => ()" + } + } + ], + "CLOUD_ADD_REAL": [ + { + "section": "Arduino", + "infos": { + "name": "CLOUD_ADD_REAL", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "VAR_NAME", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "REAL_VAR", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [], + "comment": "Add a REAL variable (Arduino float) to sync with the Arduino Cloud. VAR_NAME must have the same name as the variable set up in the Arduino IoT Cloud", + "usage": "\n (STRING:VAR_NAME, REAL:REAL_VAR) => ()" + } + } + ], + "CLOUD_BEGIN": [ + { + "section": "Arduino", + "infos": { + "name": "CLOUD_BEGIN", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "THING_ID", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "SSID", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "PASS", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [], + "comment": "Setup and initialize Arduino Cloud communication. Must be called before adding any variables (properties).", + "usage": "\n (STRING:THING_ID, STRING:SSID, STRING:PASS) => ()" + } + } + ], + "PWM_CONTROLLER": [ + { + "section": "Arduino", + "infos": { + "name": "PWM_CONTROLLER", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CHANNEL", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "FREQ", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "DUTY", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Configures the CPU internal PWM peripheral to generate a PWM signal through hardware. If the CPU does not have a PWM peripheral, compiling this block will result in a compilation error. CHANNEL is the PWM channel number. For most Arduino boards that number is the pin number for the PWM capable pin. FREQ is the desired PWM frequency in Hz. DUTY is the PWM duty cycle (between 0 and 100).", + "usage": "\n (SINT:CHANNEL, REAL:FREQ, REAL:DUTY) => (BOOL:SUCCESS)" + } + } + ], + "ARDUINOCAN_CONF": [ + { + "section": "Arduino", + "infos": { + "name": "ARDUINOCAN_CONF", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "EN_PIN", + "type": "WORD", + "qualifier": "none" + }, + { + "name": "BR", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "DONE", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "", + "usage": "\n (WORD:EN_PIN, LINT:BR) => (BOOL:DONE)" + } + } + ], + "ARDUINOCAN_WRITE": [ + { + "section": "Arduino", + "infos": { + "name": "ARDUINOCAN_WRITE", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "ID", + "type": "DWORD", + "qualifier": "none" + }, + { + "name": "D0", + "type": "USINT", + "qualifier": "none" + }, + { + "name": "D1", + "type": "USINT", + "qualifier": "none" + }, + { + "name": "D2", + "type": "USINT", + "qualifier": "none" + }, + { + "name": "D3", + "type": "USINT", + "qualifier": "none" + }, + { + "name": "D4", + "type": "USINT", + "qualifier": "none" + }, + { + "name": "D5", + "type": "USINT", + "qualifier": "none" + }, + { + "name": "D6", + "type": "USINT", + "qualifier": "none" + }, + { + "name": "D7", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "DONE", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "", + "usage": "\n (DWORD:ID, USINT:D0, USINT:D1, USINT:D2, USINT:D3, USINT:D4, USINT:D5, USINT:D6, USINT:D7) => (BOOL:DONE)" + } + } + ], + "ARDUINOCAN_WRITE_WORD": [ + { + "section": "Arduino", + "infos": { + "name": "ARDUINOCAN_WRITE_WORD", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "ID", + "type": "DWORD", + "qualifier": "none" + }, + { + "name": "DATA", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "DONE", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "", + "usage": "\n (DWORD:ID, LWORD:DATA) => (BOOL:DONE)" + } + } + ], + "ARDUINOCAN_READ": [ + { + "section": "Arduino", + "infos": { + "name": "ARDUINOCAN_READ", + "type": "functionBlock", + "extensible": false, + "inputs": [], + "outputs": [ + { + "name": "DATA", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "CAN READ", + "usage": "\n () => (LWORD:DATA)" + } + } + ], + "STM32CAN_CONF": [ + { + "section": "Microver", + "infos": { + "name": "STM32CAN_CONF", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CONF", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "BR", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "DONE", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "", + "usage": "\n (BOOL:CONF, LINT:BR) => (BOOL:DONE)" + } + } + ], + "STM32CAN_WRITE": [ + { + "section": "Microver", + "infos": { + "name": "STM32CAN_WRITE", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "EN_PIN", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "CH", + "type": "USINT", + "qualifier": "none" + }, + { + "name": "ID", + "type": "DWORD", + "qualifier": "none" + }, + { + "name": "D0", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D1", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D2", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D3", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D4", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D5", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D6", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D7", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "DONE", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "", + "usage": "\n (BOOL:EN_PIN, USINT:CH, DWORD:ID, BYTE:D0, BYTE:D1, BYTE:D2, BYTE:D3, BYTE:D4, BYTE:D5, BYTE:D6, BYTE:D7) => (BOOL:DONE)" + } + } + ], + "STM32CAN_READ": [ + { + "section": "Microver", + "infos": { + "name": "STM32CAN_READ", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "EN_PIN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "DONE", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "ID", + "type": "DWORD", + "qualifier": "none" + }, + { + "name": "D0", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D1", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D2", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D3", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D4", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D5", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D6", + "type": "BYTE", + "qualifier": "none" + }, + { + "name": "D7", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "CAN READ", + "usage": "\n (BOOL:EN_PIN) => (BOOL:DONE, DWORD:ID, BYTE:D0, BYTE:D1, BYTE:D2, BYTE:D3, BYTE:D4, BYTE:D5, BYTE:D6, BYTE:D7)" + } + } + ], + "TCP_CONNECT": [ + { + "section": "Communication", + "infos": { + "name": "TCP_CONNECT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CONNECT", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "IP_ADDRESS", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "PORT", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SOCKET_ID", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Connect to a remote TCP server when CONNECT is TRUE. Upon success, this block returns the connection ID on SOCKET_ID. If SOCKET_ID is less than zero, then the connection was not successfull", + "usage": "\n (BOOL:CONNECT, STRING:IP_ADDRESS, INT:PORT) => (INT:SOCKET_ID)" + } + } + ], + "TCP_SEND": [ + { + "section": "Communication", + "infos": { + "name": "TCP_SEND", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "SEND", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "SOCKET_ID", + "type": "INT", + "qualifier": "none" + }, + { + "name": "MSG", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "BYTES_SENT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Send a message to a remote device using TCP/IP when SEND is TRUE. SOCKET_ID must receive a connection ID from a successfull connection using the TCP_Connect block. BYTES_SENT returns the number of bytes sent to the remote device. If BYTES_SENT is less than zero then an error occurred while trying to send the message", + "usage": "\n (BOOL:SEND, INT:SOCKET_ID, STRING:MSG) => (INT:BYTES_SENT)" + } + } + ], + "TCP_RECEIVE": [ + { + "section": "Communication", + "infos": { + "name": "TCP_RECEIVE", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "RECEIVE", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "SOCKET_ID", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "BYTES_RECEIVED", + "type": "INT", + "qualifier": "none" + }, + { + "name": "MSG", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Send a message to a remote device using TCP/IP when SEND is TRUE. SOCKET_ID must receive a connection ID from a successfull connection using the TCP_Connect block. BYTES_RECEIVED returns the number of bytes received from the remote device. MSG is a String containing the message received", + "usage": "\n (BOOL:RECEIVE, INT:SOCKET_ID) => (INT:BYTES_RECEIVED, STRING:MSG)" + } + } + ], + "TCP_CLOSE": [ + { + "section": "Communication", + "infos": { + "name": "TCP_CLOSE", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CLOSE", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "SOCKET_ID", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Close the TCP connection with the remote server. If SUCCESS is less than zero, then the connection was not successfully closed, or the connection does not exist anymore.", + "usage": "\n (BOOL:CLOSE, INT:SOCKET_ID) => (INT:SUCCESS)" + } + } + ], + "P1AM_INIT": [ + { + "section": "P1AM Modules", + "infos": { + "name": "P1AM_INIT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "INIT", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Initialize P1AM Modules and return the number of initialized modules on SUCCESS. If SUCCESS is zero, an error has occurred, or there aren't any modules on the bus", + "usage": "\n (BOOL:INIT) => (SINT:SUCCESS)" + } + } + ], + "P1_16CDR": [ + { + "section": "P1AM Modules", + "infos": { + "name": "P1_16CDR", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "SLOT", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "O1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O8", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "I1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I8", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Get all inputs and update all outputs from P1-16CDR module. Also works with P1-15CDD1 and P1-15CDD2", + "usage": "\n (SINT:SLOT, BOOL:O1, BOOL:O2, BOOL:O3, BOOL:O4, BOOL:O5, BOOL:O6, BOOL:O7, BOOL:O8) => (BOOL:I1, BOOL:I2, BOOL:I3, BOOL:I4, BOOL:I5, BOOL:I6, BOOL:I7, BOOL:I8)" + } + } + ], + "P1_08N": [ + { + "section": "P1AM Modules", + "infos": { + "name": "P1_08N", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "SLOT", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "I1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I8", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Get all inputs from P1-08Nxx modules. Compatible with P1-08NA, P1-08ND3, P1-08NE3 and P1-08SIM", + "usage": "\n (SINT:SLOT) => (BOOL:I1, BOOL:I2, BOOL:I3, BOOL:I4, BOOL:I5, BOOL:I6, BOOL:I7, BOOL:I8)" + } + } + ], + "P1_16N": [ + { + "section": "P1AM Modules", + "infos": { + "name": "P1_16N", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "SLOT", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "I1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I8", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I9", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I10", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I11", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I12", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I13", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I14", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I15", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I16", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Get all inputs from P1-16Nxx modules. Compatible with P1-16ND3 and P1-16NE3", + "usage": "\n (SINT:SLOT) => (BOOL:I1, BOOL:I2, BOOL:I3, BOOL:I4, BOOL:I5, BOOL:I6, BOOL:I7, BOOL:I8, BOOL:I9, BOOL:I10, BOOL:I11, BOOL:I12, BOOL:I13, BOOL:I14, BOOL:I15, BOOL:I16)" + } + } + ], + "P1_08T": [ + { + "section": "P1AM Modules", + "infos": { + "name": "P1_08T", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "SLOT", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "O1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O8", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [], + "comment": "Set all outputs on P1-08Txx modules. Compatible with P1-08TA, P1-08TD1, P1-08TD2 and P1-08TRS", + "usage": "\n (SINT:SLOT, BOOL:O1, BOOL:O2, BOOL:O3, BOOL:O4, BOOL:O5, BOOL:O6, BOOL:O7, BOOL:O8) => ()" + } + } + ], + "P1_16TR": [ + { + "section": "P1AM Modules", + "infos": { + "name": "P1_16TR", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "SLOT", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "O1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O8", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O9", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O10", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O11", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O12", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O13", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O14", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O15", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O16", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [], + "comment": "Set all outputs on P1-16TR modules. Also compatible with P1-15TD1 and P1-15TD2", + "usage": "\n (SINT:SLOT, BOOL:O1, BOOL:O2, BOOL:O3, BOOL:O4, BOOL:O5, BOOL:O6, BOOL:O7, BOOL:O8, BOOL:O9, BOOL:O10, BOOL:O11, BOOL:O12, BOOL:O13, BOOL:O14, BOOL:O15, BOOL:O16) => ()" + } + } + ], + "P1_04AD": [ + { + "section": "P1AM Modules", + "infos": { + "name": "P1_04AD", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "SLOT", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "I1", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "I2", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "I3", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "I4", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Get all analog inputs from P1-04ADxx modules. Compatible with P1-04AD, P1-04ADL-1 and P1-04ADL-2", + "usage": "\n (SINT:SLOT) => (UINT:I1, UINT:I2, UINT:I3, UINT:I4)" + } + } + ], + "MQTT_RECEIVE": [ + { + "section": "MQTT", + "infos": { + "name": "MQTT_RECEIVE", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "RECEIVE", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "TOPIC", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "RECEIVED", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "MESSAGE", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Receive MQTT messages for a particular TOPIC when RECEIVE is active. You must subscribe to a topic first before you can start receiving messages for that particular topic. Once a message is received, RECEIVED output is triggered, and MESSAGE will contain the received message as a STRING.", + "usage": "\n (BOOL:RECEIVE, STRING:TOPIC) => (BOOL:RECEIVED, STRING:MESSAGE)" + } + } + ], + "MQTT_SEND": [ + { + "section": "MQTT", + "infos": { + "name": "MQTT_SEND", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "SEND", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "TOPIC", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "MESSAGE", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Sends a MESSAGE to a particular TOPIC when SEND input is triggered. Keep in mind that SEND is not configured as a rising edge input, which means that MQTT_SEND will continuously send messages every scan cycle while SEND is TRUE. If the message was sent without errors, SUCCESS will be TRUE.", + "usage": "\n (BOOL:SEND, STRING:TOPIC, STRING:MESSAGE) => (BOOL:SUCCESS)" + } + } + ], + "MQTT_CONNECT": [ + { + "section": "MQTT", + "infos": { + "name": "MQTT_CONNECT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CONNECT", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "BROKER", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "PORT", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Connect to a BROKER at a given PORT when CONNECT is triggered. If a successfull connection is made, SUCCESS is set to TRUE", + "usage": "\n (BOOL:CONNECT, STRING:BROKER, UINT:PORT) => (BOOL:SUCCESS)" + } + } + ], + "MQTT_CONNECT_AUTH": [ + { + "section": "MQTT", + "infos": { + "name": "MQTT_CONNECT_AUTH", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "CONNECT", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "BROKER", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "PORT", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "USER", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "PASSWORD", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Connect to an authenticated BROKER at a given PORT using the credentials from USER and PASSWORD when CONNECT is triggered. If a successfull connection is made, SUCCESS is set to TRUE", + "usage": "\n (BOOL:CONNECT, STRING:BROKER, UINT:PORT, STRING:USER, STRING:PASSWORD) => (BOOL:SUCCESS)" + } + } + ], + "MQTT_SUBSCRIBE": [ + { + "section": "MQTT", + "infos": { + "name": "MQTT_SUBSCRIBE", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "SUBSCRIBE", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "TOPIC", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Subscribe to a given TOPIC when SUBSCRIBE input is triggered. Upon a successfull subscription, SUCCESS is set to TRUE. Keep in mind that once you subscribe to a topic, OpenPLC will start receiving messages sent to that topic and storing them in a message pool. You must use the MQTT_RECEIVE block to retrieve messages from the pool and free up space to receive more messages. The maximum pool size is currently limited to 10 messages. If you let messages accumulate in the pool you will start loosing messages once the pool is full.", + "usage": "\n (BOOL:SUBSCRIBE, STRING:TOPIC) => (BOOL:SUCCESS)" + } + } + ], + "MQTT_UNSUBSCRIBE": [ + { + "section": "MQTT", + "infos": { + "name": "MQTT_UNSUBSCRIBE", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "UNSUBSCRIBE", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "TOPIC", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Unsubscribe to a given TOPIC when UNSUBSCRIBE input is triggered. Upon a successfull unsubscription, SUCCESS is set to TRUE. Keep in mind that once you unsubscribe to a topic, OpenPLC will stop storing messages sent to that topic in the message pool. However, messages received previously and not captured with a MQTT_RECEIVE block will remain in the pool using up pool space.", + "usage": "\n (BOOL:UNSUBSCRIBE, STRING:TOPIC) => (BOOL:SUCCESS)" + } + } + ], + "MQTT_DISCONNECT": [ + { + "section": "MQTT", + "infos": { + "name": "MQTT_DISCONNECT", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "DISCONNECT", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Disconnects from the current broker when DISCONNECT is set to TRUE. Upon a successfull disconnection, SUCCESS is set to TRUE.", + "usage": "\n (BOOL:DISCONNECT) => (BOOL:SUCCESS)" + } + } + ], + "SM_8RELAY": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_8RELAY", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "O1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O8", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [], + "comment": "Update all outputs from 8-relays card", + "usage": "\n (SINT:STACK, BOOL:O1, BOOL:O2, BOOL:O3, BOOL:O4, BOOL:O5, BOOL:O6, BOOL:O7, BOOL:O8) => ()" + } + } + ], + "SM_16RELAY": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_16RELAY", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "O1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O8", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O9", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O10", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O11", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O12", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O13", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O14", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O15", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "O16", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [], + "comment": "Update all outputs from 16-relays card", + "usage": "\n (SINT:STACK, BOOL:O1, BOOL:O2, BOOL:O3, BOOL:O4, BOOL:O5, BOOL:O6, BOOL:O7, BOOL:O8, BOOL:O9, BOOL:O10, BOOL:O11, BOOL:O12, BOOL:O13, BOOL:O14, BOOL:O15, BOOL:O16) => ()" + } + } + ], + "SM_8DIN": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_8DIN", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "I1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I8", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Get all inputs from Sequent microsystems 8 HV Inputs modules", + "usage": "\n (SINT:STACK) => (BOOL:I1, BOOL:I2, BOOL:I3, BOOL:I4, BOOL:I5, BOOL:I6, BOOL:I7, BOOL:I8)" + } + } + ], + "SM_16DIN": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_16DIN", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "I1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I8", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I9", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I10", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I11", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I12", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I13", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I14", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I15", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I16", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Get all inputs from Sequent Microsystems 16 digital inputs modules.", + "usage": "\n (SINT:STACK) => (BOOL:I1, BOOL:I2, BOOL:I3, BOOL:I4, BOOL:I5, BOOL:I6, BOOL:I7, BOOL:I8, BOOL:I9, BOOL:I10, BOOL:I11, BOOL:I12, BOOL:I13, BOOL:I14, BOOL:I15, BOOL:I16)" + } + } + ], + "SM_4REL4IN": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_4REL4IN", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "RELAY1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY4", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OPTO1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "AC_OPTO1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "AC_OPTO2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "AC_OPTO3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "AC_OPTO4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "PWM1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "PWM2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "PWM3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "PWM4", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "FREQ1", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "FREQ2", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "FREQ3", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "FREQ4", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "BUTTON", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Get all inputs from and set all outputs to SM_4REL4IN modules", + "usage": "\n (SINT:STACK, BOOL:RELAY1, BOOL:RELAY2, BOOL:RELAY3, BOOL:RELAY4) => (BOOL:OPTO1, BOOL:OPTO2, BOOL:OPTO3, BOOL:OPTO4, BOOL:AC_OPTO1, BOOL:AC_OPTO2, BOOL:AC_OPTO3, BOOL:AC_OPTO4, REAL:PWM1, REAL:PWM2, REAL:PWM3, REAL:PWM4, UINT:FREQ1, UINT:FREQ2, UINT:FREQ3, UINT:FREQ4, BOOL:BUTTON)" + } + } + ], + "SM_INDUSTRIAL": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_INDUSTRIAL", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "LED1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LED2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LED3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LED4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "Q0_10V1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q0_10V2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q0_10V3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q0_10V4", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q4_20MA1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q4_20MA2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q4_20MA3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q4_20MA4", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "QOD1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "QOD2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "QOD3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "QOD4", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OPTO1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "I0_10V1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "I0_10V2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "I0_10V3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "I0_10V4", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "I4_20MA1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "I4_20MA2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "I4_20MA3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "I4_20MA4", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T4", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Get all inpust and set all outputs of a Sequent Microsystems Industrial Automation card", + "usage": "\n (SINT:STACK, BOOL:LED1, BOOL:LED2, BOOL:LED3, BOOL:LED4, REAL:Q0_10V1, REAL:Q0_10V2, REAL:Q0_10V3, REAL:Q0_10V4, REAL:Q4_20MA1, REAL:Q4_20MA2, REAL:Q4_20MA3, REAL:Q4_20MA4, REAL:QOD1, REAL:QOD2, REAL:QOD3, REAL:QOD4) => (BOOL:OPTO1, BOOL:OPTO2, BOOL:OPTO3, BOOL:OPTO4, REAL:I0_10V1, REAL:I0_10V2, REAL:I0_10V3, REAL:I0_10V4, REAL:I4_20MA1, REAL:I4_20MA2, REAL:I4_20MA3, REAL:I4_20MA4, REAL:OWB_T1, REAL:OWB_T2, REAL:OWB_T3, REAL:OWB_T4)" + } + } + ], + "SM_RTD": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_RTD", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "TEMP1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TEMP2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TEMP3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TEMP4", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TEMP5", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TEMP6", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TEMP7", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "TEMP8", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Get all temperature inputs from SM_RTD module as REAL values in deg Celsious", + "usage": "\n (SINT:STACK) => (REAL:TEMP1, REAL:TEMP2, REAL:TEMP3, REAL:TEMP4, REAL:TEMP5, REAL:TEMP6, REAL:TEMP7, REAL:TEMP8)" + } + } + ], + "SM_BAS": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_BAS", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "TRIAC1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "TRIAC2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "TRIAC3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "TRIAC4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LED1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LED2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LED3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "LED4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "IN1_T", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "IN2_T", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "IN3_T", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "IN4_T", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "IN5_T", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "IN6_T", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "IN7_T", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "IN8_T", + "type": "UINT", + "qualifier": "none" + }, + { + "name": "Q0_10V1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q0_10V2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q0_10V3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q0_10V4", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "UNIV1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "UNIV2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "UNIV3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "UNIV4", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "UNIV5", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "UNIV6", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "UNIV7", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "UNIV8", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "DRY_C1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "DRY_C2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "DRY_C3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "DRY_C4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "DRY_C5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "DRY_C6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "DRY_C7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "DRY_C8", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OWB_T1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T4", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Get all inpust and set all outputs of a Sequent Microsystems Building Automation card", + "usage": "\n (SINT:STACK, BOOL:TRIAC1, BOOL:TRIAC2, BOOL:TRIAC3, BOOL:TRIAC4, BOOL:LED1, BOOL:LED2, BOOL:LED3, BOOL:LED4, UINT:IN1_T, UINT:IN2_T, UINT:IN3_T, UINT:IN4_T, UINT:IN5_T, UINT:IN6_T, UINT:IN7_T, UINT:IN8_T, REAL:Q0_10V1, REAL:Q0_10V2, REAL:Q0_10V3, REAL:Q0_10V4) => (REAL:UNIV1, REAL:UNIV2, REAL:UNIV3, REAL:UNIV4, REAL:UNIV5, REAL:UNIV6, REAL:UNIV7, REAL:UNIV8, BOOL:DRY_C1, BOOL:DRY_C2, BOOL:DRY_C3, BOOL:DRY_C4, BOOL:DRY_C5, BOOL:DRY_C6, BOOL:DRY_C7, BOOL:DRY_C8, REAL:OWB_T1, REAL:OWB_T2, REAL:OWB_T3, REAL:OWB_T4)" + } + } + ], + "SM_HOME": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_HOME", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "RELAY1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "RELAY8", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "Q0_10V1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q0_10V2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q0_10V3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "Q0_10V4", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "QOD1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "QOD2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "QOD3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "QOD4", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OPTO1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OPTO8", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "ADC1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "ADC2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "ADC3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "ADC4", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "ADC5", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "ADC6", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "ADC7", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "ADC8", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T1", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T2", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T3", + "type": "REAL", + "qualifier": "none" + }, + { + "name": "OWB_T4", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Get all inpust and set all outputs of a Sequent Microsystems Home Automation card", + "usage": "\n (SINT:STACK, BOOL:RELAY1, BOOL:RELAY2, BOOL:RELAY3, BOOL:RELAY4, BOOL:RELAY5, BOOL:RELAY6, BOOL:RELAY7, BOOL:RELAY8, REAL:Q0_10V1, REAL:Q0_10V2, REAL:Q0_10V3, REAL:Q0_10V4, REAL:QOD1, REAL:QOD2, REAL:QOD3, REAL:QOD4) => (BOOL:OPTO1, BOOL:OPTO2, BOOL:OPTO3, BOOL:OPTO4, BOOL:OPTO5, BOOL:OPTO6, BOOL:OPTO7, BOOL:OPTO8, REAL:ADC1, REAL:ADC2, REAL:ADC3, REAL:ADC4, REAL:ADC5, REAL:ADC6, REAL:ADC7, REAL:ADC8, REAL:OWB_T1, REAL:OWB_T2, REAL:OWB_T3, REAL:OWB_T4)" + } + } + ], + "SM_8MOSFET": [ + { + "section": "Sequent Microsystems Modules", + "infos": { + "name": "SM_8MOSFET", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "STACK", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "MOS1", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "MOS2", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "MOS3", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "MOS4", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "MOS5", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "MOS6", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "MOS7", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "MOS8", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [], + "comment": "Update all outputs from 8-mosfets card", + "usage": "\n (SINT:STACK, BOOL:MOS1, BOOL:MOS2, BOOL:MOS3, BOOL:MOS4, BOOL:MOS5, BOOL:MOS6, BOOL:MOS7, BOOL:MOS8) => ()" + } + } + ], + "ADC_CONFIG": [ + { + "section": "Jaguar", + "infos": { + "name": "ADC_CONFIG", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "ADC_CH", + "type": "SINT", + "qualifier": "none" + }, + { + "name": "ADC_TYPE", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "SUCCESS", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Configures the analog channel inputs on the Jaguar board. ADC_CH must be beween 0 - 3. ADC_TYPE must be between 0 - 3, where 0 = unipolar 10V, 1 = bipolar 10V, 2 = unipolar 5V, and 3 = bipolar 5V. Upon successfull configuration of the ADC, SUCCESS is set to TRUE.", + "usage": "\n (SINT:ADC_CH, SINT:ADC_TYPE) => (BOOL:SUCCESS)" + } + } + ], + "ROTARY_SWITCH": [ + { + "section": "SL-RP4", + "infos": { + "name": "ROTARY_SWITCH", + "type": "functionBlock", + "extensible": false, + "inputs": [ + { + "name": "READ", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "ERROR", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Reads the rotary switch position on SL-RP4 when the READ input is triggered. If ERROR is TRUE then an error occurred while trying to read the rotary switch. If ERROR is FALSE, the switch position value will be available on output OUT", + "usage": "\n (BOOL:READ) => (BOOL:ERROR, INT:OUT)" + } + } + ], + "BOOL_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (SINT:OUT)" + } + } + ], + "BOOL_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (INT:OUT)" + } + } + ], + "BOOL_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (DINT:OUT)" + } + } + ], + "BOOL_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (LINT:OUT)" + } + } + ], + "BOOL_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (USINT:OUT)" + } + } + ], + "BOOL_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (UINT:OUT)" + } + } + ], + "BOOL_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (UDINT:OUT)" + } + } + ], + "BOOL_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (ULINT:OUT)" + } + } + ], + "BOOL_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (REAL:OUT)" + } + } + ], + "BOOL_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (LREAL:OUT)" + } + } + ], + "BOOL_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (TIME:OUT)" + } + } + ], + "BOOL_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (DATE:OUT)" + } + } + ], + "BOOL_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (TOD:OUT)" + } + } + ], + "BOOL_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (DT:OUT)" + } + } + ], + "BOOL_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (STRING:OUT)" + } + } + ], + "BOOL_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (BYTE:OUT)" + } + } + ], + "BOOL_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (WORD:OUT)" + } + } + ], + "BOOL_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (DWORD:OUT)" + } + } + ], + "BOOL_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "BOOL_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BOOL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BOOL:IN) => (LWORD:OUT)" + } + } + ], + "SINT_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (BOOL:OUT)" + } + } + ], + "SINT_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (INT:OUT)" + } + } + ], + "SINT_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (DINT:OUT)" + } + } + ], + "SINT_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (LINT:OUT)" + } + } + ], + "SINT_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (USINT:OUT)" + } + } + ], + "SINT_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (UINT:OUT)" + } + } + ], + "SINT_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (UDINT:OUT)" + } + } + ], + "SINT_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (ULINT:OUT)" + } + } + ], + "SINT_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (REAL:OUT)" + } + } + ], + "SINT_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (LREAL:OUT)" + } + } + ], + "SINT_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (TIME:OUT)" + } + } + ], + "SINT_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (DATE:OUT)" + } + } + ], + "SINT_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (TOD:OUT)" + } + } + ], + "SINT_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (DT:OUT)" + } + } + ], + "SINT_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (STRING:OUT)" + } + } + ], + "SINT_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (BYTE:OUT)" + } + } + ], + "SINT_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (WORD:OUT)" + } + } + ], + "SINT_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (DWORD:OUT)" + } + } + ], + "SINT_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "SINT_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "SINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (SINT:IN) => (LWORD:OUT)" + } + } + ], + "INT_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (BOOL:OUT)" + } + } + ], + "INT_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (SINT:OUT)" + } + } + ], + "INT_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (DINT:OUT)" + } + } + ], + "INT_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (LINT:OUT)" + } + } + ], + "INT_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (USINT:OUT)" + } + } + ], + "INT_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (UINT:OUT)" + } + } + ], + "INT_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (UDINT:OUT)" + } + } + ], + "INT_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (ULINT:OUT)" + } + } + ], + "INT_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (REAL:OUT)" + } + } + ], + "INT_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (LREAL:OUT)" + } + } + ], + "INT_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (TIME:OUT)" + } + } + ], + "INT_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (DATE:OUT)" + } + } + ], + "INT_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (TOD:OUT)" + } + } + ], + "INT_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (DT:OUT)" + } + } + ], + "INT_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (STRING:OUT)" + } + } + ], + "INT_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (BYTE:OUT)" + } + } + ], + "INT_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (WORD:OUT)" + } + } + ], + "INT_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (DWORD:OUT)" + } + } + ], + "INT_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "INT_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (INT:IN) => (LWORD:OUT)" + } + } + ], + "DINT_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (BOOL:OUT)" + } + } + ], + "DINT_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (SINT:OUT)" + } + } + ], + "DINT_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (INT:OUT)" + } + } + ], + "DINT_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (LINT:OUT)" + } + } + ], + "DINT_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (USINT:OUT)" + } + } + ], + "DINT_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (UINT:OUT)" + } + } + ], + "DINT_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (UDINT:OUT)" + } + } + ], + "DINT_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (ULINT:OUT)" + } + } + ], + "DINT_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (REAL:OUT)" + } + } + ], + "DINT_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (LREAL:OUT)" + } + } + ], + "DINT_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (TIME:OUT)" + } + } + ], + "DINT_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (DATE:OUT)" + } + } + ], + "DINT_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (TOD:OUT)" + } + } + ], + "DINT_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (DT:OUT)" + } + } + ], + "DINT_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (STRING:OUT)" + } + } + ], + "DINT_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (BYTE:OUT)" + } + } + ], + "DINT_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (WORD:OUT)" + } + } + ], + "DINT_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (DWORD:OUT)" + } + } + ], + "DINT_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DINT_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DINT:IN) => (LWORD:OUT)" + } + } + ], + "LINT_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (BOOL:OUT)" + } + } + ], + "LINT_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (SINT:OUT)" + } + } + ], + "LINT_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (INT:OUT)" + } + } + ], + "LINT_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (DINT:OUT)" + } + } + ], + "LINT_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (USINT:OUT)" + } + } + ], + "LINT_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (UINT:OUT)" + } + } + ], + "LINT_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (UDINT:OUT)" + } + } + ], + "LINT_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (ULINT:OUT)" + } + } + ], + "LINT_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (REAL:OUT)" + } + } + ], + "LINT_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (LREAL:OUT)" + } + } + ], + "LINT_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (TIME:OUT)" + } + } + ], + "LINT_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (DATE:OUT)" + } + } + ], + "LINT_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (TOD:OUT)" + } + } + ], + "LINT_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (DT:OUT)" + } + } + ], + "LINT_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (STRING:OUT)" + } + } + ], + "LINT_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (BYTE:OUT)" + } + } + ], + "LINT_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (WORD:OUT)" + } + } + ], + "LINT_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (DWORD:OUT)" + } + } + ], + "LINT_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "LINT_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LINT:IN) => (LWORD:OUT)" + } + } + ], + "USINT_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (BOOL:OUT)" + } + } + ], + "USINT_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (SINT:OUT)" + } + } + ], + "USINT_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (INT:OUT)" + } + } + ], + "USINT_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (DINT:OUT)" + } + } + ], + "USINT_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (LINT:OUT)" + } + } + ], + "USINT_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (UINT:OUT)" + } + } + ], + "USINT_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (UDINT:OUT)" + } + } + ], + "USINT_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (ULINT:OUT)" + } + } + ], + "USINT_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (REAL:OUT)" + } + } + ], + "USINT_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (LREAL:OUT)" + } + } + ], + "USINT_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (TIME:OUT)" + } + } + ], + "USINT_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (DATE:OUT)" + } + } + ], + "USINT_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (TOD:OUT)" + } + } + ], + "USINT_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (DT:OUT)" + } + } + ], + "USINT_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (STRING:OUT)" + } + } + ], + "USINT_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (BYTE:OUT)" + } + } + ], + "USINT_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (WORD:OUT)" + } + } + ], + "USINT_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (DWORD:OUT)" + } + } + ], + "USINT_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (USINT:IN) => (LWORD:OUT)" + } + } + ], + "UINT_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (BOOL:OUT)" + } + } + ], + "UINT_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (SINT:OUT)" + } + } + ], + "UINT_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (INT:OUT)" + } + } + ], + "UINT_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (DINT:OUT)" + } + } + ], + "UINT_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (LINT:OUT)" + } + } + ], + "UINT_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (USINT:OUT)" + } + } + ], + "UINT_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (UDINT:OUT)" + } + } + ], + "UINT_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (ULINT:OUT)" + } + } + ], + "UINT_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (REAL:OUT)" + } + } + ], + "UINT_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (LREAL:OUT)" + } + } + ], + "UINT_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (TIME:OUT)" + } + } + ], + "UINT_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (DATE:OUT)" + } + } + ], + "UINT_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (TOD:OUT)" + } + } + ], + "UINT_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (DT:OUT)" + } + } + ], + "UINT_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (STRING:OUT)" + } + } + ], + "UINT_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (BYTE:OUT)" + } + } + ], + "UINT_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (WORD:OUT)" + } + } + ], + "UINT_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (DWORD:OUT)" + } + } + ], + "UINT_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UINT:IN) => (LWORD:OUT)" + } + } + ], + "UDINT_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (BOOL:OUT)" + } + } + ], + "UDINT_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (SINT:OUT)" + } + } + ], + "UDINT_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (INT:OUT)" + } + } + ], + "UDINT_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (DINT:OUT)" + } + } + ], + "UDINT_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (LINT:OUT)" + } + } + ], + "UDINT_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (USINT:OUT)" + } + } + ], + "UDINT_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (UINT:OUT)" + } + } + ], + "UDINT_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (ULINT:OUT)" + } + } + ], + "UDINT_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (REAL:OUT)" + } + } + ], + "UDINT_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (LREAL:OUT)" + } + } + ], + "UDINT_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (TIME:OUT)" + } + } + ], + "UDINT_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (DATE:OUT)" + } + } + ], + "UDINT_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (TOD:OUT)" + } + } + ], + "UDINT_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (DT:OUT)" + } + } + ], + "UDINT_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (STRING:OUT)" + } + } + ], + "UDINT_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (BYTE:OUT)" + } + } + ], + "UDINT_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (WORD:OUT)" + } + } + ], + "UDINT_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (DWORD:OUT)" + } + } + ], + "UDINT_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (UDINT:IN) => (LWORD:OUT)" + } + } + ], + "ULINT_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (BOOL:OUT)" + } + } + ], + "ULINT_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (SINT:OUT)" + } + } + ], + "ULINT_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (INT:OUT)" + } + } + ], + "ULINT_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (DINT:OUT)" + } + } + ], + "ULINT_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (LINT:OUT)" + } + } + ], + "ULINT_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (USINT:OUT)" + } + } + ], + "ULINT_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (UINT:OUT)" + } + } + ], + "ULINT_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (UDINT:OUT)" + } + } + ], + "ULINT_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (REAL:OUT)" + } + } + ], + "ULINT_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (LREAL:OUT)" + } + } + ], + "ULINT_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (TIME:OUT)" + } + } + ], + "ULINT_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (DATE:OUT)" + } + } + ], + "ULINT_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (TOD:OUT)" + } + } + ], + "ULINT_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (DT:OUT)" + } + } + ], + "ULINT_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (STRING:OUT)" + } + } + ], + "ULINT_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (BYTE:OUT)" + } + } + ], + "ULINT_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (WORD:OUT)" + } + } + ], + "ULINT_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (DWORD:OUT)" + } + } + ], + "ULINT_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (ULINT:IN) => (LWORD:OUT)" + } + } + ], + "REAL_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (BOOL:OUT)" + } + } + ], + "REAL_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (SINT:OUT)" + } + } + ], + "REAL_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (INT:OUT)" + } + } + ], + "REAL_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (DINT:OUT)" + } + } + ], + "REAL_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (LINT:OUT)" + } + } + ], + "REAL_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (USINT:OUT)" + } + } + ], + "REAL_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (UINT:OUT)" + } + } + ], + "REAL_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (UDINT:OUT)" + } + } + ], + "REAL_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (ULINT:OUT)" + } + } + ], + "REAL_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (LREAL:OUT)" + } + } + ], + "REAL_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (TIME:OUT)" + } + } + ], + "REAL_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (DATE:OUT)" + } + } + ], + "REAL_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (TOD:OUT)" + } + } + ], + "REAL_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (DT:OUT)" + } + } + ], + "REAL_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (STRING:OUT)" + } + } + ], + "REAL_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (BYTE:OUT)" + } + } + ], + "REAL_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (WORD:OUT)" + } + } + ], + "REAL_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (DWORD:OUT)" + } + } + ], + "REAL_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "REAL_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (REAL:IN) => (LWORD:OUT)" + } + } + ], + "LREAL_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (BOOL:OUT)" + } + } + ], + "LREAL_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (SINT:OUT)" + } + } + ], + "LREAL_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (INT:OUT)" + } + } + ], + "LREAL_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (DINT:OUT)" + } + } + ], + "LREAL_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (LINT:OUT)" + } + } + ], + "LREAL_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (USINT:OUT)" + } + } + ], + "LREAL_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (UINT:OUT)" + } + } + ], + "LREAL_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (UDINT:OUT)" + } + } + ], + "LREAL_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (ULINT:OUT)" + } + } + ], + "LREAL_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (REAL:OUT)" + } + } + ], + "LREAL_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (TIME:OUT)" + } + } + ], + "LREAL_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (DATE:OUT)" + } + } + ], + "LREAL_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (TOD:OUT)" + } + } + ], + "LREAL_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (DT:OUT)" + } + } + ], + "LREAL_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (STRING:OUT)" + } + } + ], + "LREAL_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (BYTE:OUT)" + } + } + ], + "LREAL_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (WORD:OUT)" + } + } + ], + "LREAL_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (DWORD:OUT)" + } + } + ], + "LREAL_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "LREAL_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LREAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LREAL:IN) => (LWORD:OUT)" + } + } + ], + "TIME_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (SINT:OUT)" + } + } + ], + "TIME_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (INT:OUT)" + } + } + ], + "TIME_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (DINT:OUT)" + } + } + ], + "TIME_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (LINT:OUT)" + } + } + ], + "TIME_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (USINT:OUT)" + } + } + ], + "TIME_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (UINT:OUT)" + } + } + ], + "TIME_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (UDINT:OUT)" + } + } + ], + "TIME_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (ULINT:OUT)" + } + } + ], + "TIME_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (REAL:OUT)" + } + } + ], + "TIME_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (LREAL:OUT)" + } + } + ], + "TIME_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (STRING:OUT)" + } + } + ], + "TIME_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (BYTE:OUT)" + } + } + ], + "TIME_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (WORD:OUT)" + } + } + ], + "TIME_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (DWORD:OUT)" + } + } + ], + "TIME_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "TIME_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TIME:IN) => (LWORD:OUT)" + } + } + ], + "DATE_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (SINT:OUT)" + } + } + ], + "DATE_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (INT:OUT)" + } + } + ], + "DATE_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (DINT:OUT)" + } + } + ], + "DATE_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (LINT:OUT)" + } + } + ], + "DATE_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (USINT:OUT)" + } + } + ], + "DATE_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (UINT:OUT)" + } + } + ], + "DATE_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (UDINT:OUT)" + } + } + ], + "DATE_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (ULINT:OUT)" + } + } + ], + "DATE_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (REAL:OUT)" + } + } + ], + "DATE_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (LREAL:OUT)" + } + } + ], + "DATE_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (STRING:OUT)" + } + } + ], + "DATE_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (BYTE:OUT)" + } + } + ], + "DATE_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (WORD:OUT)" + } + } + ], + "DATE_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (DWORD:OUT)" + } + } + ], + "DATE_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DATE:IN) => (LWORD:OUT)" + } + } + ], + "TOD_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (SINT:OUT)" + } + } + ], + "TOD_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (INT:OUT)" + } + } + ], + "TOD_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (DINT:OUT)" + } + } + ], + "TOD_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (LINT:OUT)" + } + } + ], + "TOD_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (USINT:OUT)" + } + } + ], + "TOD_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (UINT:OUT)" + } + } + ], + "TOD_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (UDINT:OUT)" + } + } + ], + "TOD_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (ULINT:OUT)" + } + } + ], + "TOD_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (REAL:OUT)" + } + } + ], + "TOD_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (LREAL:OUT)" + } + } + ], + "TOD_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (STRING:OUT)" + } + } + ], + "TOD_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (BYTE:OUT)" + } + } + ], + "TOD_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (WORD:OUT)" + } + } + ], + "TOD_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (DWORD:OUT)" + } + } + ], + "TOD_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "TOD_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (TOD:IN) => (LWORD:OUT)" + } + } + ], + "DT_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (SINT:OUT)" + } + } + ], + "DT_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (INT:OUT)" + } + } + ], + "DT_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (DINT:OUT)" + } + } + ], + "DT_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (LINT:OUT)" + } + } + ], + "DT_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (USINT:OUT)" + } + } + ], + "DT_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (UINT:OUT)" + } + } + ], + "DT_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (UDINT:OUT)" + } + } + ], + "DT_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (ULINT:OUT)" + } + } + ], + "DT_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (REAL:OUT)" + } + } + ], + "DT_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (LREAL:OUT)" + } + } + ], + "DT_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (STRING:OUT)" + } + } + ], + "DT_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (BYTE:OUT)" + } + } + ], + "DT_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (WORD:OUT)" + } + } + ], + "DT_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (DWORD:OUT)" + } + } + ], + "DT_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DT_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DT:IN) => (LWORD:OUT)" + } + } + ], + "STRING_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (BOOL:OUT)" + } + } + ], + "STRING_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (SINT:OUT)" + } + } + ], + "STRING_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (INT:OUT)" + } + } + ], + "STRING_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (DINT:OUT)" + } + } + ], + "STRING_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (LINT:OUT)" + } + } + ], + "STRING_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (USINT:OUT)" + } + } + ], + "STRING_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (UINT:OUT)" + } + } + ], + "STRING_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (UDINT:OUT)" + } + } + ], + "STRING_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (ULINT:OUT)" + } + } + ], + "STRING_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (REAL:OUT)" + } + } + ], + "STRING_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (LREAL:OUT)" + } + } + ], + "STRING_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (TIME:OUT)" + } + } + ], + "STRING_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (DATE:OUT)" + } + } + ], + "STRING_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (TOD:OUT)" + } + } + ], + "STRING_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (DT:OUT)" + } + } + ], + "STRING_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (BYTE:OUT)" + } + } + ], + "STRING_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (WORD:OUT)" + } + } + ], + "STRING_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (DWORD:OUT)" + } + } + ], + "STRING_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "STRING_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (STRING:IN) => (LWORD:OUT)" + } + } + ], + "BYTE_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (BOOL:OUT)" + } + } + ], + "BYTE_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (SINT:OUT)" + } + } + ], + "BYTE_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (INT:OUT)" + } + } + ], + "BYTE_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (DINT:OUT)" + } + } + ], + "BYTE_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (LINT:OUT)" + } + } + ], + "BYTE_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (USINT:OUT)" + } + } + ], + "BYTE_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (UINT:OUT)" + } + } + ], + "BYTE_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (UDINT:OUT)" + } + } + ], + "BYTE_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (ULINT:OUT)" + } + } + ], + "BYTE_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (REAL:OUT)" + } + } + ], + "BYTE_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (LREAL:OUT)" + } + } + ], + "BYTE_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (TIME:OUT)" + } + } + ], + "BYTE_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (DATE:OUT)" + } + } + ], + "BYTE_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (TOD:OUT)" + } + } + ], + "BYTE_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (DT:OUT)" + } + } + ], + "BYTE_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (STRING:OUT)" + } + } + ], + "BYTE_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (WORD:OUT)" + } + } + ], + "BYTE_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (DWORD:OUT)" + } + } + ], + "BYTE_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "BYTE_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (BYTE:IN) => (LWORD:OUT)" + } + } + ], + "WORD_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (BOOL:OUT)" + } + } + ], + "WORD_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (SINT:OUT)" + } + } + ], + "WORD_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (INT:OUT)" + } + } + ], + "WORD_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (DINT:OUT)" + } + } + ], + "WORD_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (LINT:OUT)" + } + } + ], + "WORD_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (USINT:OUT)" + } + } + ], + "WORD_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (UINT:OUT)" + } + } + ], + "WORD_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (UDINT:OUT)" + } + } + ], + "WORD_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (ULINT:OUT)" + } + } + ], + "WORD_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (REAL:OUT)" + } + } + ], + "WORD_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (LREAL:OUT)" + } + } + ], + "WORD_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (TIME:OUT)" + } + } + ], + "WORD_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (DATE:OUT)" + } + } + ], + "WORD_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (TOD:OUT)" + } + } + ], + "WORD_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (DT:OUT)" + } + } + ], + "WORD_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (STRING:OUT)" + } + } + ], + "WORD_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (BYTE:OUT)" + } + } + ], + "WORD_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (DWORD:OUT)" + } + } + ], + "WORD_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "WORD_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (WORD:IN) => (LWORD:OUT)" + } + } + ], + "DWORD_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (BOOL:OUT)" + } + } + ], + "DWORD_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (SINT:OUT)" + } + } + ], + "DWORD_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (INT:OUT)" + } + } + ], + "DWORD_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (DINT:OUT)" + } + } + ], + "DWORD_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (LINT:OUT)" + } + } + ], + "DWORD_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (USINT:OUT)" + } + } + ], + "DWORD_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (UINT:OUT)" + } + } + ], + "DWORD_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (UDINT:OUT)" + } + } + ], + "DWORD_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (ULINT:OUT)" + } + } + ], + "DWORD_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (REAL:OUT)" + } + } + ], + "DWORD_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (LREAL:OUT)" + } + } + ], + "DWORD_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (TIME:OUT)" + } + } + ], + "DWORD_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (DATE:OUT)" + } + } + ], + "DWORD_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (TOD:OUT)" + } + } + ], + "DWORD_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (DT:OUT)" + } + } + ], + "DWORD_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (STRING:OUT)" + } + } + ], + "DWORD_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (BYTE:OUT)" + } + } + ], + "DWORD_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (WORD:OUT)" + } + } + ], + "DWORD_TO_LWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "DWORD_TO_LWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (DWORD:IN) => (LWORD:OUT)" + } + } + ], + "LWORD_TO_BOOL": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_BOOL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (BOOL:OUT)" + } + } + ], + "LWORD_TO_SINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_SINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "SINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (SINT:OUT)" + } + } + ], + "LWORD_TO_INT": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_INT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (INT:OUT)" + } + } + ], + "LWORD_TO_DINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_DINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (DINT:OUT)" + } + } + ], + "LWORD_TO_LINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_LINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (LINT:OUT)" + } + } + ], + "LWORD_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (USINT:OUT)" + } + } + ], + "LWORD_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (UINT:OUT)" + } + } + ], + "LWORD_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (UDINT:OUT)" + } + } + ], + "LWORD_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (ULINT:OUT)" + } + } + ], + "LWORD_TO_REAL": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_REAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "REAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (REAL:OUT)" + } + } + ], + "LWORD_TO_LREAL": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_LREAL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LREAL", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (LREAL:OUT)" + } + } + ], + "LWORD_TO_TIME": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (TIME:OUT)" + } + } + ], + "LWORD_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (DATE:OUT)" + } + } + ], + "LWORD_TO_TOD": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (TOD:OUT)" + } + } + ], + "LWORD_TO_DT": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (DT:OUT)" + } + } + ], + "LWORD_TO_STRING": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_STRING", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (STRING:OUT)" + } + } + ], + "LWORD_TO_BYTE": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_BYTE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (BYTE:OUT)" + } + } + ], + "LWORD_TO_WORD": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_WORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (WORD:OUT)" + } + } + ], + "LWORD_TO_DWORD": [ + { + "section": "Type conversion", + "infos": { + "name": "LWORD_TO_DWORD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Data type conversion", + "usage": "\n (LWORD:IN) => (DWORD:OUT)" + } + } + ], + "TRUNC": [ + { + "section": "Type conversion", + "infos": { + "name": "TRUNC", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "comment": "Rounding up/down", + "usage": "\n (ANY_REAL:IN) => (ANY_INT:OUT)" + } + } + ], + "BCD_TO_USINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BCD_TO_USINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "BYTE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "USINT", + "qualifier": "none" + } + ], + "comment": "Conversion from BCD", + "usage": "\n (BYTE:IN) => (USINT:OUT)" + } + } + ], + "BCD_TO_UINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BCD_TO_UINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "WORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UINT", + "qualifier": "none" + } + ], + "comment": "Conversion from BCD", + "usage": "\n (WORD:IN) => (UINT:OUT)" + } + } + ], + "BCD_TO_UDINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BCD_TO_UDINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "UDINT", + "qualifier": "none" + } + ], + "comment": "Conversion from BCD", + "usage": "\n (DWORD:IN) => (UDINT:OUT)" + } + } + ], + "BCD_TO_ULINT": [ + { + "section": "Type conversion", + "infos": { + "name": "BCD_TO_ULINT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "LWORD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ULINT", + "qualifier": "none" + } + ], + "comment": "Conversion from BCD", + "usage": "\n (LWORD:IN) => (ULINT:OUT)" + } + } + ], + "USINT_TO_BCD": [ + { + "section": "Type conversion", + "infos": { + "name": "USINT_TO_BCD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "USINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BYTE", + "qualifier": "none" + } + ], + "comment": "Conversion to BCD", + "usage": "\n (USINT:IN) => (BYTE:OUT)" + } + } + ], + "UINT_TO_BCD": [ + { + "section": "Type conversion", + "infos": { + "name": "UINT_TO_BCD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "WORD", + "qualifier": "none" + } + ], + "comment": "Conversion to BCD", + "usage": "\n (UINT:IN) => (WORD:OUT)" + } + } + ], + "UDINT_TO_BCD": [ + { + "section": "Type conversion", + "infos": { + "name": "UDINT_TO_BCD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "UDINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DWORD", + "qualifier": "none" + } + ], + "comment": "Conversion to BCD", + "usage": "\n (UDINT:IN) => (DWORD:OUT)" + } + } + ], + "ULINT_TO_BCD": [ + { + "section": "Type conversion", + "infos": { + "name": "ULINT_TO_BCD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ULINT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "LWORD", + "qualifier": "none" + } + ], + "comment": "Conversion to BCD", + "usage": "\n (ULINT:IN) => (LWORD:OUT)" + } + } + ], + "DATE_AND_TIME_TO_TIME_OF_DAY": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_AND_TIME_TO_TIME_OF_DAY", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Conversion to time-of-day", + "usage": "\n (DT:IN) => (TOD:OUT)" + } + } + ], + "DATE_AND_TIME_TO_DATE": [ + { + "section": "Type conversion", + "infos": { + "name": "DATE_AND_TIME_TO_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DATE", + "qualifier": "none" + } + ], + "comment": "Conversion to date", + "usage": "\n (DT:IN) => (DATE:OUT)" + } + } + ], + "ABS": [ + { + "section": "Numerical", + "infos": { + "name": "ABS", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "comment": "Absolute number", + "usage": "\n (ANY_NUM:IN) => (ANY_NUM:OUT)" + } + } + ], + "SQRT": [ + { + "section": "Numerical", + "infos": { + "name": "SQRT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Square root (base 2)", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "LN": [ + { + "section": "Numerical", + "infos": { + "name": "LN", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Natural logarithm", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "LOG": [ + { + "section": "Numerical", + "infos": { + "name": "LOG", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Logarithm to base 10", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "EXP": [ + { + "section": "Numerical", + "infos": { + "name": "EXP", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Exponentiation", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "SIN": [ + { + "section": "Numerical", + "infos": { + "name": "SIN", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Sine", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "COS": [ + { + "section": "Numerical", + "infos": { + "name": "COS", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Cosine", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "TAN": [ + { + "section": "Numerical", + "infos": { + "name": "TAN", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Tangent", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "ASIN": [ + { + "section": "Numerical", + "infos": { + "name": "ASIN", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Arc sine", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "ACOS": [ + { + "section": "Numerical", + "infos": { + "name": "ACOS", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Arc cosine", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "ATAN": [ + { + "section": "Numerical", + "infos": { + "name": "ATAN", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Arc tangent", + "usage": "\n (ANY_REAL:IN) => (ANY_REAL:OUT)" + } + } + ], + "ADD": [ + { + "section": "Arithmetic", + "infos": { + "name": "ADD", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY_NUM", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "comment": "Addition", + "usage": "\n (ANY_NUM:IN1, ANY_NUM:IN2) => (ANY_NUM:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "ADD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TIME", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time addition", + "usage": "\n (TIME:IN1, TIME:IN2) => (TIME:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "ADD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TOD", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Time-of-day addition", + "usage": "\n (TOD:IN1, TIME:IN2) => (TOD:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "ADD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "DT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Date addition", + "usage": "\n (DT:IN1, TIME:IN2) => (DT:OUT)" + } + } + ], + "MUL": [ + { + "section": "Arithmetic", + "infos": { + "name": "MUL", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY_NUM", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "comment": "Multiplication", + "usage": "\n (ANY_NUM:IN1, ANY_NUM:IN2) => (ANY_NUM:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "MUL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TIME", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time multiplication", + "usage": "\n (TIME:IN1, ANY_NUM:IN2) => (TIME:OUT)" + } + } + ], + "SUB": [ + { + "section": "Arithmetic", + "infos": { + "name": "SUB", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "ANY_NUM", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "comment": "Subtraction", + "usage": "\n (ANY_NUM:IN1, ANY_NUM:IN2) => (ANY_NUM:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "SUB", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TIME", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time subtraction", + "usage": "\n (TIME:IN1, TIME:IN2) => (TIME:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "SUB", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "DATE", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Date subtraction", + "usage": "\n (DATE:IN1, DATE:IN2) => (TIME:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "SUB", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TOD", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Time-of-day subtraction", + "usage": "\n (TOD:IN1, TIME:IN2) => (TOD:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "SUB", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TOD", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time-of-day subtraction", + "usage": "\n (TOD:IN1, TOD:IN2) => (TIME:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "SUB", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "DT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Date and time subtraction", + "usage": "\n (DT:IN1, TIME:IN2) => (DT:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "SUB", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "DT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Date and time subtraction", + "usage": "\n (DT:IN1, DT:IN2) => (TIME:OUT)" + } + } + ], + "DIV": [ + { + "section": "Arithmetic", + "infos": { + "name": "DIV", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "ANY_NUM", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "comment": "Division", + "usage": "\n (ANY_NUM:IN1, ANY_NUM:IN2) => (ANY_NUM:OUT)" + } + }, + { + "section": "Time", + "infos": { + "name": "DIV", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TIME", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time division", + "usage": "\n (TIME:IN1, ANY_NUM:IN2) => (TIME:OUT)" + } + } + ], + "MOD": [ + { + "section": "Arithmetic", + "infos": { + "name": "MOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "ANY_INT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "comment": "Remainder (modulo)", + "usage": "\n (ANY_INT:IN1, ANY_INT:IN2) => (ANY_INT:OUT)" + } + } + ], + "EXPT": [ + { + "section": "Arithmetic", + "infos": { + "name": "EXPT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "ANY_REAL", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_REAL", + "qualifier": "none" + } + ], + "comment": "Exponent", + "usage": "\n (ANY_REAL:IN1, ANY_NUM:IN2) => (ANY_REAL:OUT)" + } + } + ], + "MOVE": [ + { + "section": "Arithmetic", + "infos": { + "name": "MOVE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY", + "qualifier": "none" + } + ], + "comment": "Assignment", + "usage": "\n (ANY:IN) => (ANY:OUT)" + } + } + ], + "ADD_TIME": [ + { + "section": "Time", + "infos": { + "name": "ADD_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TIME", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time addition", + "usage": "\n (TIME:IN1, TIME:IN2) => (TIME:OUT)" + } + } + ], + "ADD_TOD_TIME": [ + { + "section": "Time", + "infos": { + "name": "ADD_TOD_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TOD", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Time-of-day addition", + "usage": "\n (TOD:IN1, TIME:IN2) => (TOD:OUT)" + } + } + ], + "ADD_DT_TIME": [ + { + "section": "Time", + "infos": { + "name": "ADD_DT_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "DT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Date addition", + "usage": "\n (DT:IN1, TIME:IN2) => (DT:OUT)" + } + } + ], + "MULTIME": [ + { + "section": "Time", + "infos": { + "name": "MULTIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TIME", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time multiplication", + "usage": "\n (TIME:IN1, ANY_NUM:IN2) => (TIME:OUT)" + } + } + ], + "SUB_TIME": [ + { + "section": "Time", + "infos": { + "name": "SUB_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TIME", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time subtraction", + "usage": "\n (TIME:IN1, TIME:IN2) => (TIME:OUT)" + } + } + ], + "SUB_DATE_DATE": [ + { + "section": "Time", + "infos": { + "name": "SUB_DATE_DATE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "DATE", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "DATE", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Date subtraction", + "usage": "\n (DATE:IN1, DATE:IN2) => (TIME:OUT)" + } + } + ], + "SUB_TOD_TIME": [ + { + "section": "Time", + "infos": { + "name": "SUB_TOD_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TOD", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TOD", + "qualifier": "none" + } + ], + "comment": "Time-of-day subtraction", + "usage": "\n (TOD:IN1, TIME:IN2) => (TOD:OUT)" + } + } + ], + "SUB_TOD_TOD": [ + { + "section": "Time", + "infos": { + "name": "SUB_TOD_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TOD", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time-of-day subtraction", + "usage": "\n (TOD:IN1, TOD:IN2) => (TIME:OUT)" + } + } + ], + "SUB_DT_TIME": [ + { + "section": "Time", + "infos": { + "name": "SUB_DT_TIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "DT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TIME", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Date and time subtraction", + "usage": "\n (DT:IN1, TIME:IN2) => (DT:OUT)" + } + } + ], + "SUB_DT_DT": [ + { + "section": "Time", + "infos": { + "name": "SUB_DT_DT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "DT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "DT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Date and time subtraction", + "usage": "\n (DT:IN1, DT:IN2) => (TIME:OUT)" + } + } + ], + "DIVTIME": [ + { + "section": "Time", + "infos": { + "name": "DIVTIME", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "TIME", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_NUM", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "TIME", + "qualifier": "none" + } + ], + "comment": "Time division", + "usage": "\n (TIME:IN1, ANY_NUM:IN2) => (TIME:OUT)" + } + } + ], + "SHL": [ + { + "section": "Bit-shift", + "infos": { + "name": "SHL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_BIT", + "qualifier": "none" + }, + { + "name": "N", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "comment": "Shift left", + "usage": "\n (ANY_BIT:IN, ANY_INT:N) => (ANY_BIT:OUT)" + } + } + ], + "SHR": [ + { + "section": "Bit-shift", + "infos": { + "name": "SHR", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_BIT", + "qualifier": "none" + }, + { + "name": "N", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "comment": "Shift right", + "usage": "\n (ANY_BIT:IN, ANY_INT:N) => (ANY_BIT:OUT)" + } + } + ], + "ROR": [ + { + "section": "Bit-shift", + "infos": { + "name": "ROR", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_NBIT", + "qualifier": "none" + }, + { + "name": "N", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_NBIT", + "qualifier": "none" + } + ], + "comment": "Rotate right", + "usage": "\n (ANY_NBIT:IN, ANY_INT:N) => (ANY_NBIT:OUT)" + } + } + ], + "ROL": [ + { + "section": "Bit-shift", + "infos": { + "name": "ROL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_NBIT", + "qualifier": "none" + }, + { + "name": "N", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_NBIT", + "qualifier": "none" + } + ], + "comment": "Rotate left", + "usage": "\n (ANY_NBIT:IN, ANY_INT:N) => (ANY_NBIT:OUT)" + } + } + ], + "AND": [ + { + "section": "Bitwise", + "infos": { + "name": "AND", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY_BIT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "comment": "Bitwise AND", + "usage": "\n (ANY_BIT:IN1, ANY_BIT:IN2) => (ANY_BIT:OUT)" + } + } + ], + "OR": [ + { + "section": "Bitwise", + "infos": { + "name": "OR", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY_BIT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "comment": "Bitwise OR", + "usage": "\n (ANY_BIT:IN1, ANY_BIT:IN2) => (ANY_BIT:OUT)" + } + } + ], + "XOR": [ + { + "section": "Bitwise", + "infos": { + "name": "XOR", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY_BIT", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "comment": "Bitwise XOR", + "usage": "\n (ANY_BIT:IN1, ANY_BIT:IN2) => (ANY_BIT:OUT)" + } + } + ], + "NOT": [ + { + "section": "Bitwise", + "infos": { + "name": "NOT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY_BIT", + "qualifier": "none" + } + ], + "comment": "Bitwise inverting", + "usage": "\n (ANY_BIT:IN) => (ANY_BIT:OUT)" + } + } + ], + "SEL": [ + { + "section": "Selection", + "infos": { + "name": "SEL", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "G", + "type": "BOOL", + "qualifier": "none" + }, + { + "name": "IN0", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY", + "qualifier": "none" + } + ], + "comment": "Binary selection (1 of 2)", + "usage": "\n (BOOL:G, ANY:IN0, ANY:IN1) => (ANY:OUT)" + } + } + ], + "MAX": [ + { + "section": "Selection", + "infos": { + "name": "MAX", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY", + "qualifier": "none" + } + ], + "comment": "Maximum", + "usage": "\n (ANY:IN1, ANY:IN2) => (ANY:OUT)" + } + } + ], + "MIN": [ + { + "section": "Selection", + "infos": { + "name": "MIN", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY", + "qualifier": "none" + } + ], + "comment": "Minimum", + "usage": "\n (ANY:IN1, ANY:IN2) => (ANY:OUT)" + } + } + ], + "LIMIT": [ + { + "section": "Selection", + "infos": { + "name": "LIMIT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "MN", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "MX", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY", + "qualifier": "none" + } + ], + "comment": "Limitation", + "usage": "\n (ANY:MN, ANY:IN, ANY:MX) => (ANY:OUT)" + } + } + ], + "MUX": [ + { + "section": "Selection", + "infos": { + "name": "MUX", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "K", + "type": "ANY_INT", + "qualifier": "none" + }, + { + "name": "IN0", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "ANY", + "qualifier": "none" + } + ], + "comment": "Multiplexer (select 1 of N)", + "usage": "\n (ANY_INT:K, ANY:IN0, ANY:IN1) => (ANY:OUT)" + } + } + ], + "GT": [ + { + "section": "Comparison", + "infos": { + "name": "GT", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Greater than", + "usage": "\n (ANY:IN1, ANY:IN2) => (BOOL:OUT)" + } + } + ], + "GE": [ + { + "section": "Comparison", + "infos": { + "name": "GE", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Greater than or equal to", + "usage": "\n (ANY:IN1, ANY:IN2) => (BOOL:OUT)" + } + } + ], + "EQ": [ + { + "section": "Comparison", + "infos": { + "name": "EQ", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Equal to", + "usage": "\n (ANY:IN1, ANY:IN2) => (BOOL:OUT)" + } + } + ], + "LT": [ + { + "section": "Comparison", + "infos": { + "name": "LT", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Less than", + "usage": "\n (ANY:IN1, ANY:IN2) => (BOOL:OUT)" + } + } + ], + "LE": [ + { + "section": "Comparison", + "infos": { + "name": "LE", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Less than or equal to", + "usage": "\n (ANY:IN1, ANY:IN2) => (BOOL:OUT)" + } + } + ], + "NE": [ + { + "section": "Comparison", + "infos": { + "name": "NE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "ANY", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "ANY", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "BOOL", + "qualifier": "none" + } + ], + "comment": "Not equal to", + "usage": "\n (ANY:IN1, ANY:IN2) => (BOOL:OUT)" + } + } + ], + "LEN": [ + { + "section": "Character string", + "infos": { + "name": "LEN", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Length of string", + "usage": "\n (STRING:IN) => (INT:OUT)" + } + } + ], + "LEFT": [ + { + "section": "Character string", + "infos": { + "name": "LEFT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "L", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "string left of", + "usage": "\n (STRING:IN, ANY_INT:L) => (STRING:OUT)" + } + } + ], + "RIGHT": [ + { + "section": "Character string", + "infos": { + "name": "RIGHT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "L", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "string right of", + "usage": "\n (STRING:IN, ANY_INT:L) => (STRING:OUT)" + } + } + ], + "MID": [ + { + "section": "Character string", + "infos": { + "name": "MID", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "L", + "type": "ANY_INT", + "qualifier": "none" + }, + { + "name": "P", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "string from the middle", + "usage": "\n (STRING:IN, ANY_INT:L, ANY_INT:P) => (STRING:OUT)" + } + } + ], + "CONCAT": [ + { + "section": "Character string", + "infos": { + "name": "CONCAT", + "type": "function", + "extensible": true, + "inputs": [ + { + "name": "IN1", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Concatenation", + "usage": "\n (STRING:IN1, STRING:IN2) => (STRING:OUT)" + } + } + ], + "CONCAT_DATE_TOD": [ + { + "section": "Character string", + "infos": { + "name": "CONCAT_DATE_TOD", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "DATE", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "TOD", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "DT", + "qualifier": "none" + } + ], + "comment": "Time concatenation", + "usage": "\n (DATE:IN1, TOD:IN2) => (DT:OUT)" + } + } + ], + "INSERT": [ + { + "section": "Character string", + "infos": { + "name": "INSERT", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "P", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Insertion (into)", + "usage": "\n (STRING:IN1, STRING:IN2, ANY_INT:P) => (STRING:OUT)" + } + } + ], + "DELETE": [ + { + "section": "Character string", + "infos": { + "name": "DELETE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "L", + "type": "ANY_INT", + "qualifier": "none" + }, + { + "name": "P", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Deletion (within)", + "usage": "\n (STRING:IN, ANY_INT:L, ANY_INT:P) => (STRING:OUT)" + } + } + ], + "REPLACE": [ + { + "section": "Character string", + "infos": { + "name": "REPLACE", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "L", + "type": "ANY_INT", + "qualifier": "none" + }, + { + "name": "P", + "type": "ANY_INT", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "STRING", + "qualifier": "none" + } + ], + "comment": "Replacement (within)", + "usage": "\n (STRING:IN1, STRING:IN2, ANY_INT:L, ANY_INT:P) => (STRING:OUT)" + } + } + ], + "FIND": [ + { + "section": "Character string", + "infos": { + "name": "FIND", + "type": "function", + "extensible": false, + "inputs": [ + { + "name": "IN1", + "type": "STRING", + "qualifier": "none" + }, + { + "name": "IN2", + "type": "STRING", + "qualifier": "none" + } + ], + "outputs": [ + { + "name": "OUT", + "type": "INT", + "qualifier": "none" + } + ], + "comment": "Find position", + "usage": "\n (STRING:IN1, STRING:IN2) => (INT:OUT)" + } + } + ] +} diff --git a/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts b/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts new file mode 100644 index 000000000..f2e89f665 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts @@ -0,0 +1,170 @@ +/** + * IR-native `CONFIGURATION … END_CONFIGURATION` block emitter. + * + * Walks `TranspileProject.configuration` and emits byte-identical + * chunks against the python oracle's `ProgramGenerator.GenerateConfiguration` + * + `GenerateResource` (PLCGenerator.py:334-628). + * + * The IR carries exactly one configuration with one resource, named + * `Config0` / `Res0` — same hardcoded names the python oracle + * produces. Global vars are emitted under the configuration block + * (not under the resource). + */ + +import type { ProgramChunk } from '../helpers/program' +import { computeConfigurationName, computeConfigurationResourceName } from '../helpers/text-helpers' +import type { TranspileProject, TranspileVariable } from '../types' +import { declaredTypeName, getTypeAsText } from './type-text' +import { computeValue } from './value' + +const CONFIG_NAME = 'Config0' +const RESOURCE_NAME = 'Res0' + +/** + * Emit the trailing `\nCONFIGURATION … END_CONFIGURATION\n` block. + * Returns an empty array when the IR carries no tasks / instances / + * globals (caller may still want the keyword shell — Python always + * emits the block; we mirror that). + */ +export function generateConfigurations(project: TranspileProject): ProgramChunk[] { + const configTagname = computeConfigurationName(CONFIG_NAME) + const resourceTagname = computeConfigurationResourceName(CONFIG_NAME, RESOURCE_NAME) + + const out: ProgramChunk[] = [] + + out.push(['\nCONFIGURATION ', []]) + out.push([CONFIG_NAME, [configTagname, 'name']]) + out.push(['\n', []]) + + // Configuration-level global variables. + emitGlobalVarList( + out, + project.configuration.globalVariables, + configTagname, + /*indent=*/ ' ', + /*varIndent=*/ ' ', + project, + ) + + // RESOURCE block. The IR has exactly one resource (`Res0`). + out.push(['\n RESOURCE ', []]) + out.push([RESOURCE_NAME, [resourceTagname, 'name']]) + out.push([' ON PLC\n', []]) + + // Resource-scope globals are not in the IR today (Python supports + // them; we don't surface a field for them yet). Skipping the + // resource-level VAR_GLOBAL emit matches `irToPlcOpenXml`'s + // current shape. + + // Tasks. + project.configuration.tasks.forEach((task, taskNumber) => { + out.push([' TASK ', []]) + out.push([task.name, [resourceTagname, 'task', taskNumber, 'name']]) + out.push(['(', []]) + + if (task.triggering !== 'Cyclic') { + const single = task.single ?? '' + if (single.length === 0) { + throw new Error( + `Source signal has to be defined for single task '${task.name}' in resource '${CONFIG_NAME}.${RESOURCE_NAME}'.`, + ) + } + const snglkw = single.startsWith('[') && single.endsWith(']') ? 'MULTI' : 'SINGLE' + out.push([`${snglkw} := `, []]) + out.push([single, [resourceTagname, 'task', taskNumber, 'single']]) + out.push([',', []]) + } + + if (task.interval !== undefined) { + out.push(['INTERVAL := ', []]) + out.push([task.interval, [resourceTagname, 'task', taskNumber, 'interval']]) + out.push([',', []]) + } + + out.push(['PRIORITY := ', []]) + out.push([`${task.priority}`, [resourceTagname, 'task', taskNumber, 'priority']]) + out.push([');\n', []]) + }) + + // PROGRAM bindings — first the task-bound instances (in task + // iteration order, then instance order within each task), then the + // task-less instances directly under the resource. + let instanceNumber = 0 + for (const task of project.configuration.tasks) { + for (const instance of project.configuration.instances) { + if (instance.task !== task.name) continue + out.push([' PROGRAM ', []]) + out.push([instance.name, [resourceTagname, 'instance', instanceNumber, 'name']]) + out.push([' WITH ', []]) + out.push([task.name, [resourceTagname, 'instance', instanceNumber, 'task']]) + out.push([' : ', []]) + out.push([instance.program, [resourceTagname, 'instance', instanceNumber, 'type']]) + out.push([';\n', []]) + instanceNumber++ + } + } + for (const instance of project.configuration.instances) { + if (instance.task !== undefined && instance.task !== '') continue + out.push([' PROGRAM ', []]) + out.push([instance.name, [resourceTagname, 'instance', instanceNumber, 'name']]) + out.push([' : ', []]) + out.push([instance.program, [resourceTagname, 'instance', instanceNumber, 'type']]) + out.push([';\n', []]) + instanceNumber++ + } + + out.push([' END_RESOURCE\n', []]) + out.push(['END_CONFIGURATION\n', []]) + return out +} + +/* ────────────────────── helpers ─────────────────────────────────────────── */ + +function emitGlobalVarList( + out: ProgramChunk[], + variables: TranspileVariable[], + tagname: string, + indent: string, + _varIndent: string, + project: TranspileProject, +): void { + if (variables.length === 0) return + + const variableType = 'var_local' + const range: [number, number] = [0, variables.length] + + out.push([`${indent}VAR_GLOBAL`, []]) + // CONSTANT / RETAIN / NON_RETAIN modifiers come from the + // wrapper in the DOM path; the IR doesn't surface + // per-list modifiers today (only the bare variable list). + void range + void tagname + out.push(['\n', []]) + + variables.forEach((variable, idx) => { + out.push([_varIndent, []]) + out.push([variable.name, [tagname, variableType, idx, 'name']]) + out.push([' ', []]) + + if (variable.location) { + out.push(['AT ', []]) + out.push([variable.location, [tagname, variableType, idx, 'location']]) + out.push([' ', []]) + } + + out.push([': ', []]) + out.push([getTypeAsText(variable), [tagname, variableType, idx, 'type']]) + + if (variable.initialValue !== undefined && variable.initialValue !== '') { + const declaredType = declaredTypeName(variable) + out.push([' := ', []]) + out.push([ + computeValue(project, variable.initialValue, declaredType), + [tagname, variableType, idx, 'initial value'], + ]) + } + out.push([';\n', []]) + }) + + out.push([`${indent}END_VAR\n`, []]) +} diff --git a/src/backend/shared/transpilers/st-transpiler/emit/data-types.ts b/src/backend/shared/transpilers/st-transpiler/emit/data-types.ts new file mode 100644 index 000000000..af3d2c122 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/emit/data-types.ts @@ -0,0 +1,161 @@ +/** + * IR-native `TYPE … END_TYPE` block emitter. + * + * JSON-direct port of `generate_data_type.ts` — walks + * `TranspileProject.dataTypes` instead of the parsed DOM, but emits + * byte-identical chunks (verified by the comparison helper). + * + * The recursive emission order (a child data type referencing + * another forces the dependency to emit first) mirrors + * `ProgramGenerator.GenerateDataType` (PLCGenerator.py:152-299) line + * for line. Only difference from the DOM version: no `subrange` + * branch — the IR's `TranspileDataType` union doesn't carry + * subranges (matches the schema at `backend/shared/types/PLC/open-plc.ts`). + */ + +import { ComputeDataTypeName } from '../helpers/data-type' +import type { ProgramChunk } from '../helpers/program' +import type { TranspileDataType, TranspileProject, TranspileVariable, TranspileVariableType } from '../types' +import { computeValue } from './value' + +interface DataTypeState { + project: TranspileProject + out: ProgramChunk[] + byName: Map + computed: Map +} + +export function generateDataTypes(project: TranspileProject): ProgramChunk[] { + if (project.dataTypes.length === 0) return [] + + const state: DataTypeState = { + project, + out: [], + byName: new Map(project.dataTypes.map((dt) => [dt.name, dt])), + computed: new Map(project.dataTypes.map((dt) => [dt.name, false])), + } + + const program: ProgramChunk[] = [] + program.push(['TYPE\n', []]) + for (const name of state.computed.keys()) { + generateDataType(state, name) + } + program.push(...state.out) + program.push(['END_TYPE\n\n', []]) + return program +} + +function generateDataType(state: DataTypeState, datatypeName: string): void { + // Mirror PLCGenerator.py:154 — dict.get(name, True). Unknown names + // (e.g. POU references) get True back, skipping emission. + if (state.computed.get(datatypeName) !== false) return + state.computed.set(datatypeName, true) + + const dt = state.byName.get(datatypeName) + if (!dt) return + + const tagname = ComputeDataTypeName(dt.name) + const chunks: ProgramChunk[] = [ + [' ', []], + [dt.name, [tagname, 'name']], + [' : ', []], + ] + + if (dt.derivation === 'directly-derived') { + // Branch on whether the base is elementary or another data type. + const base = dt.baseType + if (state.byName.has(base)) { + generateDataType(state, base) + chunks.push([base, [tagname, 'base']]) + } else { + // Elementary IEC base — emit uppercased to match the DOM + // walker (which does `baseTypeKind.toUpperCase()` for the + // elementary branch at PLCGenerator.py:286). + chunks.push([base.toUpperCase(), [tagname, 'base']]) + } + } else if (dt.derivation === 'enumerated') { + chunks.push(['(', []]) + dt.values.forEach((value, i) => { + if (i > 0) chunks.push([', ', []]) + chunks.push([value.description, [tagname, 'value', i]]) + }) + chunks.push([')', []]) + } else if (dt.derivation === 'array') { + const baseTypeName = resolveArrayBaseName(dt.baseType, state) + chunks.push(['ARRAY [', []]) + dt.dimensions.forEach((dimension, i) => { + if (i > 0) chunks.push([',', []]) + const [lower, upper] = dimension.dimension.split('..') + chunks.push( + [`${lower}`, [tagname, 'range', i, 'lower']], + ['..', []], + [`${upper}`, [tagname, 'range', i, 'upper']], + ) + }) + chunks.push(['] OF ', []], [baseTypeName, [tagname, 'base']]) + } else if (dt.derivation === 'structure') { + chunks.push(['STRUCT', []]) + dt.variable.forEach((variable, i) => { + const elementtypeName = resolveStructFieldType(variable, state) + chunks.push( + ['\n ', []], + [variable.name, [tagname, 'struct', i, 'name']], + [' : ', []], + [elementtypeName, [tagname, 'struct', i, 'type']], + ) + if (variable.initialValue !== undefined && variable.initialValue !== '') { + chunks.push( + [' := ', []], + [ + computeValue(state.project, variable.initialValue, elementtypeName), + [tagname, 'struct', i, 'initial value'], + ], + ) + } + chunks.push([';', []]) + }) + chunks.push(['\n END_STRUCT', []]) + } + + if (dt.initialValue !== undefined && dt.initialValue !== '') { + chunks.push([' := ', []], [computeValue(state.project, dt.initialValue, datatypeName), [tagname, 'initial value']]) + } + chunks.push([';\n', []]) + + state.out.push(...chunks) +} + +function resolveArrayBaseName(baseType: string | { value: string }, state: DataTypeState): string { + const name = typeof baseType === 'string' ? baseType : baseType.value + if (state.byName.has(name)) { + generateDataType(state, name) + return name + } + return name.toUpperCase() +} + +function resolveStructFieldType(variable: TranspileVariable, state: DataTypeState): string { + const type = variable.type + if (type.definition === 'derived' || type.definition === 'user-data-type') { + if (state.byName.has(type.value)) generateDataType(state, type.value) + return type.value + } + if (type.definition === 'array') { + const baseName = resolveArrayBaseName(type.data.baseType, state) + const dimensions = type.data.dimensions.map((d) => d.dimension).join(',') + return `ARRAY [${dimensions}] OF ${baseName}` + } + return formatBaseType(type) +} + +function formatBaseType(type: TranspileVariableType): string { + if (type.definition === 'base-type') { + return type.value.toUpperCase() + } + if (type.definition === 'array') { + const baseName = typeof type.data.baseType === 'string' ? type.data.baseType : type.data.baseType.value + const dimensions = type.data.dimensions.map((d) => d.dimension).join(',') + return `ARRAY [${dimensions}] OF ${baseName.toUpperCase()}` + } + return type.value +} diff --git a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts new file mode 100644 index 000000000..0540334e7 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts @@ -0,0 +1,219 @@ +/** + * IR-native graphical-POU emitter — LD / FBD bodies. + * + * Drives the React Flow walker (`../walker/`) for + * the body content, then wraps it with the POU's signature + VAR + * sections + END. Trigger variables and function-call output temps + * synthesised during the walk (`R_TRIG1`, `_TMP__OUT`, …) + * get appended to the trailing `VAR` section before assembly so the + * declaration order matches what the python oracle produces. + */ + +import { PLC_BASE_TYPES } from '../helpers/base-types' +import { resolveBlockType } from '../helpers/block-library' +import type { ProgramChunk } from '../helpers/program' +import { computePouName } from '../helpers/text-helpers' +import { varTypeNames } from '../helpers/type-text' +import type { TranspilePou, TranspileProject, TranspileVariable, TranspileVariableClass } from '../types' +import { emitFbdBody } from '../walker/fbd' +import type { SyntheticVar } from '../walker/ld' +import { emitLdBody } from '../walker/ld' +import { declaredTypeName, getTypeAsText } from './type-text' +import { computeValue } from './value' + +interface InterfaceEntry { + keyword: string + vars: TranspileVariable[] +} + +/** + * Destination types of the IEC 61131-3 polymorphic conversion family + * (`TO_BOOL`, `TO_INT`, `TO_UINT`, …). Hard-coded here rather than + * derived at runtime from the catalog so any future addition is visible + * in code review. Kept in sync with `data/std_block_catalog.json` — any + * `_TO_` entry in the catalog implies `TO_` is a valid + * polymorphic conversion target. + */ +const TO_CONVERSION_TARGETS: ReadonlySet = new Set([ + 'BCD', + 'BOOL', + 'BYTE', + 'DATE', + 'DINT', + 'DT', + 'DWORD', + 'INT', + 'LINT', + 'LREAL', + 'LWORD', + 'REAL', + 'SINT', + 'STRING', + 'TIME', + 'TOD', + 'UDINT', + 'UINT', + 'ULINT', + 'USINT', + 'WORD', +]) + +/* ─────────────────────────── public entry ───────────────────────────────── */ + +/** + * Emit a complete LD/FBD POU (signature → VAR sections → body → + * closing keyword). Mirrors `pou-textual.generateTextualPou` for + * the wrapping, with the body coming from the React Flow walker. + */ +export function generateGraphicalPou(pou: TranspilePou, project: TranspileProject): ProgramChunk[] { + const tagName = computePouName(pou.name) + const kindKeyword = ( + { + program: 'PROGRAM', + function: 'FUNCTION', + 'function-block': 'FUNCTION_BLOCK', + } as Record + )[pou.pouType] + + if (pou.body.language !== 'ld' && pou.body.language !== 'fbd') { + throw new Error(`generateGraphicalPou called with non-graphical body: ${pou.body.language}`) + } + const emitted = pou.body.language === 'ld' ? emitLdBody(pou.body.value) : emitFbdBody(pou.body.value) + + // Compose the final POU chunk stream now that the walker has + // emitted the body bytes + any synthetic vars. + const program: ProgramChunk[] = [] + program.push([`${kindKeyword} `, []]) + program.push([pou.name, [tagName, 'name']]) + if (pou.pouType === 'function') { + const returnType = (pou.interface.returnType ?? 'BOOL').toUpperCase() + program.push([' : ', []]) + program.push([returnType, [tagName, 'return']]) + } + program.push(['\n', []]) + + // Resolve `ANY` placeholders in the synthesised function-output + // temps: + // 1. User-defined project functions → declared `interface.returnType`. + // 2. Standard catalog functions (ADD, MUL, NOT, AND, …) → catalog's + // formal output `type`. Generic groups (`ANY_BIT`, `ANY_NUM`, + // …) collapse to `BOOL`, which matches the corpus where these + // operators are always Boolean rung logic. A future + // computeConnectionTypes port will narrow these properly. + // 3. Polymorphic IEC 61131-3 type-conversion functions of the + // form `TO_` (TO_INT, TO_UINT, TO_REAL, …) — the + // catalog enumerates the source-specific variants + // (`BOOL_TO_UINT`, `INT_TO_UINT`, …) but NOT the generic + // `TO_` family, so resolveBlockType returns null for + // them. Without this case the synthetic var stayed at + // `ANY` and strucpp rejected the program with + // "Undefined type 'ANY' in PROGRAM" — fixed here by reading + // the destination type directly from the function name. + const resolvedSyntheticVars = emitted.syntheticVars.map((sv) => { + if (sv.type !== 'ANY' || sv.originBlockTypeName === undefined) return sv + const referenced = project.pous.find((p) => p.name === sv.originBlockTypeName) + if (referenced && referenced.pouType === 'function' && referenced.interface.returnType) { + return { ...sv, type: referenced.interface.returnType } + } + const stdResolved = resolveBlockType(sv.originBlockTypeName) + if (stdResolved) { + const outPort = stdResolved.infos.outputs.find((o) => o.name === sv.originFormalParameter) + if (outPort) { + const collapsed = outPort.type.startsWith('ANY') ? 'BOOL' : outPort.type + return { ...sv, type: collapsed } + } + } + const polymorphicMatch = sv.originBlockTypeName.match(/^TO_([A-Z]+)$/) + if (polymorphicMatch && TO_CONVERSION_TARGETS.has(polymorphicMatch[1])) { + return { ...sv, type: polymorphicMatch[1] } + } + return sv + }) + + const iface = computeInterface(pou.interface?.variables ?? [], resolvedSyntheticVars) + for (const entry of iface) { + const variableType = locationCategory(entry.keyword) + program.push([` ${entry.keyword}`, []]) + program.push(['\n', []]) + entry.vars.forEach((v, varNumber) => { + program.push([' ', []]) + program.push([v.name, [tagName, variableType, varNumber, 'name']]) + program.push([' ', []]) + if (v.location) { + program.push(['AT ', []]) + program.push([v.location, [tagName, variableType, varNumber, 'location']]) + program.push([' ', []]) + } + const typeText = getTypeAsText(v) + program.push([': ', []]) + program.push([typeText, [tagName, variableType, varNumber, 'type']]) + if (v.initialValue !== undefined && v.initialValue !== '') { + const declared = declaredTypeName(v) + program.push([' := ', []]) + program.push([ + computeValue(project, v.initialValue, declared), + [tagName, variableType, varNumber, 'initial value'], + ]) + } + program.push([';\n', []]) + }) + program.push([' END_VAR\n', []]) + } + program.push([emitted.bodySt, []]) + program.push([`END_${kindKeyword}\n\n`, []]) + return program +} + +/* ────────────────────────── helpers ─────────────────────────────────────── */ + +function computeInterface(variables: TranspileVariable[], syntheticVars: SyntheticVar[]): InterfaceEntry[] { + const classToKeyword: Record = { + input: varTypeNames.inputVars, + output: varTypeNames.outputVars, + inOut: varTypeNames.inOutVars, + external: varTypeNames.externalVars, + local: varTypeNames.localVars, + temp: varTypeNames.tempVars, + } + // Group by keyword, preserving IR insertion order. + const grouped = new Map() + for (const v of variables) { + const keyword = classToKeyword[v.class ?? 'local'] ?? varTypeNames.localVars + const bucket = grouped.get(keyword) ?? [] + bucket.push(v) + grouped.set(keyword, bucket) + } + // Append synthesised trigger vars + function-call output temps to + // the trailing VAR (local) bucket so they appear after the user's + // declared locals — same order the python oracle produces. + if (syntheticVars.length > 0) { + const localKeyword = varTypeNames.localVars + const localBucket = grouped.get(localKeyword) ?? [] + for (const sv of syntheticVars) { + const isElementary = PLC_BASE_TYPES.has(sv.type.toUpperCase()) + localBucket.push({ + name: sv.name, + type: isElementary + ? { definition: 'base-type', value: sv.type.toUpperCase() } + : { definition: 'derived', value: sv.type }, + class: 'local', + }) + } + grouped.set(localKeyword, localBucket) + } + const out: InterfaceEntry[] = [] + for (const [keyword, vars] of grouped) { + out.push({ keyword, vars }) + } + return out +} + +const ERROR_VAR_TYPES: Record = { + VAR_INPUT: 'var_input', + VAR_OUTPUT: 'var_output', + VAR_INOUT: 'var_inout', +} + +function locationCategory(keyword: string): string { + return ERROR_VAR_TYPES[keyword] ?? 'var_local' +} diff --git a/src/backend/shared/transpilers/st-transpiler/emit/pou-textual.ts b/src/backend/shared/transpilers/st-transpiler/emit/pou-textual.ts new file mode 100644 index 000000000..37bfd1ae6 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-textual.ts @@ -0,0 +1,185 @@ +/** + * IR-native textual-POU emitter — ST / IL / Python / C++. + * + * Skips the DOM round-trip entirely: walks `TranspilePou` directly + * and emits the same chunk shape `pou_assembly.generateProgram` + * produces for textual bodies. Byte-identical with the XML-fed + * transpiler (verified by the comparison helper). + * + * Graphical POUs (LD / FBD / SFC) still go through the DOM-based + * walker in `src/PLCGenerator/pou_assembly.ts` — see `index.ts`'s + * dispatch. + */ + +import { PLC_BASE_TYPES } from '../helpers/base-types' +import type { ProgramChunk } from '../helpers/program' +import { reIndentText } from '../helpers/text-helpers' +import { computePouName } from '../helpers/text-helpers' +import { varTypeNames } from '../helpers/type-text' +import type { TranspilePou, TranspileProject, TranspileVariable, TranspileVariableClass } from '../types' +import { declaredTypeName, getTypeAsText } from './type-text' +import { computeValue } from './value' + +/** + * Mirror python's `` rendering rule: elementary IEC + * types come out uppercased (`BOOL`, `INT`, …), user-defined + * derived types keep their declared case (`Irrigation_State`). + * `interface.computeReturnType` in the XML-fed transpiler does the + * equivalent by branching on the `` local tag. + */ +function formatReturnType(returnType: string | undefined): string { + if (returnType === undefined || returnType === '') return 'BOOL' + return PLC_BASE_TYPES.has(returnType.toUpperCase()) ? returnType.toUpperCase() : returnType +} + +interface InterfaceEntry { + keyword: string + located: boolean + vars: TranspileVariable[] +} + +/* ─────────────────────────── public entry ───────────────────────────────── */ + +/** + * Emit a complete textual-body POU (signature → VAR sections → body + * text → closing keyword). Mirrors + * `PouProgramGenerator.GenerateProgram` (PLCGenerator.py:2414) for the + * textual body path. + * + * Throws `Error` when the POU has no interface or no body — same + * guards Python uses. + */ +export function generateTextualPou(pou: TranspilePou, project: TranspileProject, indent = 2): ProgramChunk[] { + const tagName = computePouName(pou.name) + const kindKeyword = ( + { + program: 'PROGRAM', + function: 'FUNCTION', + 'function-block': 'FUNCTION_BLOCK', + } as Record + )[pou.pouType] + + const program: ProgramChunk[] = [] + program.push([`${kindKeyword} `, []]) + program.push([pou.name, [tagName, 'name']]) + + if (pou.pouType === 'function') { + program.push([' : ', []]) + program.push([formatReturnType(pou.interface.returnType), [tagName, 'return']]) + } + program.push(['\n', []]) + + const iface = computeInterface(pou.interface.variables) + if (iface.length === 0) { + throw new Error(`No variable defined in "${pou.name}" POU`) + } + + let varNumber = 0 + for (const entry of iface) { + const variableType = locationCategory(entry.keyword) + program.push([` ${entry.keyword}`, []]) + program.push(['\n', []]) + + for (const v of entry.vars) { + program.push([' ', []]) + program.push([v.name, [tagName, variableType, varNumber, 'name']]) + program.push([' ', []]) + + if (v.location) { + program.push(['AT ', []]) + program.push([v.location, [tagName, variableType, varNumber, 'location']]) + program.push([' ', []]) + } + + const typeText = getTypeAsText(v) + program.push([': ', []]) + program.push([typeText, [tagName, variableType, varNumber, 'type']]) + + if (v.initialValue !== undefined && v.initialValue !== '') { + const declared = declaredTypeName(v) + program.push([' := ', []]) + program.push([ + computeValue(project, v.initialValue, declared), + [tagName, variableType, varNumber, 'initial value'], + ]) + } + program.push([';\n', []]) + varNumber++ + } + program.push([' END_VAR\n', []]) + } + + program.push(['\n', []]) + + // Body: raw textual source, re-indented to `indent` spaces. For ST + // / IL / Python / C++ the IR's `value` is already a string. + if ( + pou.body.language !== 'st' && + pou.body.language !== 'il' && + pou.body.language !== 'python' && + pou.body.language !== 'cpp' + ) { + throw new Error(`generateTextualPou called with non-textual body "${pou.body.language}"`) + } + const bodyText = pou.body.value + if (bodyText.length === 0) { + throw new Error(`No body defined in "${pou.name}" POU`) + } + program.push([reIndentText(bodyText, indent), [tagName, 'body', indent]]) + + program.push([`END_${kindKeyword}\n\n`, []]) + return program +} + +/* ────────────────────── helpers ─────────────────────────────────────────── */ + +function computeInterface(variables: TranspileVariable[]): InterfaceEntry[] { + // Bucket variables by class, mirroring the order Python's varlist + // iteration produces. `classToKeyword` is keyed on IR class names + // (which match the PLCOpen ``/``/… local + // tags after lower-casing the leading section); look those up + // through `varTypeNames` so the keyword strings stay identical to + // the DOM emitter. + const classToKeyword: Record = { + input: varTypeNames.inputVars, + output: varTypeNames.outputVars, + inOut: varTypeNames.inOutVars, + external: varTypeNames.externalVars, + local: varTypeNames.localVars, + temp: varTypeNames.tempVars, + } + + const grouped = new Map() + // Maintain insertion order matching the IR's variable order. + for (const v of variables) { + const keyword = classToKeyword[v.class ?? 'local'] ?? varTypeNames.localVars + let bucket = grouped.get(keyword) + if (!bucket) { + bucket = { located: [], unlocated: [] } + grouped.set(keyword, bucket) + } + if (v.location) bucket.located.push(v) + else bucket.unlocated.push(v) + } + + const out: InterfaceEntry[] = [] + for (const [keyword, bucket] of grouped) { + if (bucket.unlocated.length > 0) { + out.push({ keyword, located: false, vars: bucket.unlocated }) + } + if (bucket.located.length > 0) { + out.push({ keyword, located: true, vars: bucket.located }) + } + } + return out +} + +const ERROR_VAR_TYPES: Record = { + VAR_INPUT: 'var_input', + VAR_OUTPUT: 'var_output', + VAR_INOUT: 'var_inout', +} + +function locationCategory(keyword: string): string { + return ERROR_VAR_TYPES[keyword] ?? 'var_local' +} diff --git a/src/backend/shared/transpilers/st-transpiler/emit/type-text.ts b/src/backend/shared/transpilers/st-transpiler/emit/type-text.ts new file mode 100644 index 000000000..d4b1f3645 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/emit/type-text.ts @@ -0,0 +1,48 @@ +/** + * IR-native `gettypeAsText` — mirrors the DOM helper + * (`plcopen.py:1100` and `src/PLCGenerator/type_text.ts`) but reads + * the variable's type definition directly off the IR. + */ + +import type { TranspileVariable, TranspileVariableType } from '../types' + +/** + * Textual representation of a `TranspileVariable`'s declared type: + * - `derived` / `user-data-type` → the referenced name as-is. + * - `base-type` → uppercased (`'BOOL'`, `'INT'`, …). Lowercase + * `string`/`wstring` are uppercased into `STRING`/`WSTRING`. + * - `array` → `ARRAY [a..b, …] OF basetype`. + */ +export function getTypeAsText(variable: TranspileVariable): string { + return formatType(variable.type) +} + +function formatType(type: TranspileVariableType): string { + if (type.definition === 'derived' || type.definition === 'user-data-type') { + return type.value + } + if (type.definition === 'base-type') { + return type.value.toUpperCase() + } + // array — explicit guard so editor's stricter narrowing keeps the + // `data` property in scope. Web's tsconfig accepted the fall- + // through; editor's doesn't. + if (type.definition !== 'array') return '' + const baseName = typeof type.data.baseType === 'string' ? type.data.baseType : type.data.baseType.value + const dims = type.data.dimensions.map((d) => d.dimension).join(',') + return `ARRAY [${dims}] OF ${baseName.toUpperCase()}` +} + +/** + * For `computeValue`'s quote-wrapping check we need the type name + * users actually wrote in the type field — but for the derived case + * the DOM helper returned the derived name directly (not "ARRAY […]" + * even if the derived type happens to be an array). Mirrors the + * `interface.ts:resolveDeclaredType` quirk. + */ +export function declaredTypeName(variable: TranspileVariable): string { + if (variable.type.definition === 'derived' || variable.type.definition === 'user-data-type') { + return variable.type.value + } + return getTypeAsText(variable) +} diff --git a/src/backend/shared/transpilers/st-transpiler/emit/value.ts b/src/backend/shared/transpilers/st-transpiler/emit/value.ts new file mode 100644 index 000000000..b936b9508 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/emit/value.ts @@ -0,0 +1,66 @@ +/** + * IR-native `computeValue` — wraps STRING/WSTRING initial values in + * quotes when the user didn't already. Mirrors + * `ProgramGenerator.ComputeValue` (PLCGenerator.py:135) without going + * through DOM accessors; the project's data-type alias chain is + * walked directly off the IR's `dataTypes` array. + */ + +import { TypeHierarchy } from '../helpers/type-hierarchy' +import type { TranspileDataType, TranspileProject } from '../types' + +/** + * Walk `typename` down through `project.dataTypes` aliases until an + * elementary IEC type is reached. Returns `null` if the chain + * dead-ends (unknown alias, struct/enum leaves where the elementary + * base is intentionally absent). + * + * Identical behaviour to `pou_assembly.getBaseType`, just reading the + * IR map instead of the DOM project. + */ +function getBaseType(typename: string, dataTypeIndex: Map): string | null { + // Elementary types short-circuit (the existing DOM helper does the same). + if (typename in TypeHierarchy) return typename + + const dt = dataTypeIndex.get(typename) + if (!dt) return null + + if (dt.derivation === 'array') { + return getBaseType(typeof dt.baseType === 'string' ? dt.baseType : dt.baseType.value, dataTypeIndex) + } + if (dt.derivation === 'directly-derived') { + return getBaseType(dt.baseType, dataTypeIndex) + } + // Struct / enum leaves: Python's GetDataTypeBaseType returns the + // type name itself. We return the same so STRING/WSTRING quote + // gating doesn't fire on them. + return typename +} + +/** + * Resolve a variable's underlying elementary base type from the + * IR. Used by initial-value quote wrapping + * (`'foo'` for STRING, `"foo"` for WSTRING). Returns `null` when + * the chain doesn't terminate at an elementary type. + */ +export function resolveBaseType(typename: string, project: TranspileProject): string | null { + const index = new Map() + for (const dt of project.dataTypes) index.set(dt.name, dt) + return getBaseType(typename, index) +} + +/** + * Same as the DOM helper at `pou_assembly.computeValue` — wrap + * STRING / WSTRING initial values in quotes when the source text + * doesn't already carry them. + */ +export function computeValue(project: TranspileProject, value: string, varType: string): string { + const baseType = resolveBaseType(varType, project) + if (baseType === 'STRING' && !value.startsWith("'") && !value.endsWith("'")) { + return `'${value}'` + } + if (baseType === 'WSTRING' && !value.startsWith('"') && !value.endsWith('"')) { + return `"${value}"` + } + return value +} diff --git a/src/backend/shared/transpilers/st-transpiler/from-schema.ts b/src/backend/shared/transpilers/st-transpiler/from-schema.ts new file mode 100644 index 000000000..5a78d6435 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/from-schema.ts @@ -0,0 +1,249 @@ +/** + * Project the editor's IPC schema-shape `PLCProjectData` + * (`backend/shared/types/PLC/open-plc.ts`, z.infer of + * `PLCProjectDataSchema`) into the transpiler's minimal IR. + * + * Lives alongside the transpiler because both types come from + * `backend/shared/` — no layer violation. The port-shape adapter + * lives outside (under `middleware/adapters/`) because port-shape + * types are middleware-scoped. + */ + +import type { PLCProjectData as SchemaPLCProjectData } from '@root/backend/shared/types/PLC/open-plc' + +import type { + TranspileBody, + TranspileDataType, + TranspileInstance, + TranspilePou, + TranspileProject, + TranspileTask, + TranspileVariable, + TranspileVariableType, +} from './types' +import type { RFFbdBody } from './walker/fbd' +import type { RFBody, RFEdge, RFNode, RFRung } from './walker/types' + +export type SchemaProjectData = SchemaPLCProjectData + +export function fromSchemaShape(data: SchemaPLCProjectData): TranspileProject { + return { + pous: data.pous.map(projectPou), + dataTypes: (data.dataTypes ?? []).map(projectDataType), + configuration: { + tasks: (data.configuration?.resource?.tasks ?? []).map(projectTask), + instances: (data.configuration?.resource?.instances ?? []).map(projectInstance), + globalVariables: (data.configuration?.resource?.globalVariables ?? []).map((v) => + projectVariable(v as SchemaVariable), + ), + }, + } +} + +type SchemaPou = SchemaPLCProjectData['pous'][number] +type SchemaVariable = NonNullable[number] +type SchemaDataType = NonNullable[number] +type SchemaStructureVariable = Extract['variable'][number] +type SchemaTask = SchemaPLCProjectData['configuration']['resource']['tasks'][number] +type SchemaInstance = SchemaPLCProjectData['configuration']['resource']['instances'][number] +type SchemaBody = SchemaPou['data']['body'] + +function projectPou(pou: SchemaPou): TranspilePou { + const variables = (pou.data.variables ?? []).map(projectVariable) + return { + name: pou.data.name, + pouType: pou.type, + documentation: pou.data.documentation ?? '', + interface: { + variables, + ...(pou.type === 'function' ? { returnType: stringifyReturnType(pou.data.returnType) } : {}), + }, + body: projectBody(pou.data.body), + } +} + +function projectBody(body: SchemaBody): TranspileBody { + switch (body.language) { + case 'st': + case 'il': + case 'python': + case 'cpp': + return { language: body.language, value: body.value } + case 'ld': + return { language: 'ld', value: projectLdBody(body.value.rungs) } + case 'fbd': + return { language: 'fbd', value: projectFbdBody(body.value.rung) } + case 'sfc': + throw new Error('SFC support is under development') + default: { + const unreachable: never = body + throw new Error(`Unknown body language: ${JSON.stringify(unreachable)}`) + } + } +} + +/* ─── graphical body projection ──────────────────────────────────── */ + +type SchemaLdValue = Extract['value'] +type SchemaFbdValue = Extract['value'] +type SchemaRung = SchemaLdValue['rungs'][number] +type SchemaFbdRung = SchemaFbdValue['rung'] +type SchemaNode = SchemaRung['nodes'][number] +type SchemaEdge = SchemaRung['edges'][number] + +function projectLdBody(rungs: readonly SchemaRung[]): RFBody { + return { rungs: rungs.map(projectRung) } +} + +function projectFbdBody(rung: SchemaFbdRung): RFFbdBody { + return { + rung: { + comment: rung.comment, + nodes: rung.nodes.map(projectNode), + edges: rung.edges.map(projectEdge), + }, + } +} + +function projectRung(rung: SchemaRung): RFRung { + return { + id: rung.id, + comment: rung.comment, + reactFlowViewport: rung.reactFlowViewport, + nodes: rung.nodes.map(projectNode), + edges: rung.edges.map(projectEdge), + } +} + +function projectNode(n: SchemaNode): RFNode { + return { + id: n.id, + type: n.type, + position: n.position, + data: isRecord(n.data) ? n.data : {}, + } +} + +function projectEdge(e: SchemaEdge): RFEdge { + return { + id: e.id, + source: e.source, + target: e.target, + sourceHandle: e.sourceHandle, + targetHandle: e.targetHandle, + } +} + +function isRecord(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v) +} + +function projectVariable(v: SchemaVariable): TranspileVariable { + return { + name: v.name, + type: projectVariableType(v.type), + ...(v.class !== undefined ? { class: normalizeVarClass(v.class) } : {}), + ...(v.location !== undefined && v.location !== '' ? { location: v.location } : {}), + ...(v.initialValue !== undefined && v.initialValue !== null && v.initialValue !== '' + ? { initialValue: v.initialValue } + : {}), + ...(v.documentation !== undefined && v.documentation !== '' ? { documentation: v.documentation } : {}), + } +} + +function normalizeVarClass(cls: NonNullable): TranspileVariable['class'] { + // Schema's variable class includes 'global'; the IR doesn't (global + // vars live under configuration.globalVariables, not in a POU + // interface). Collapse to 'local' for the projection — matches the + // port-shape adapter's normalisation. + if (cls === 'global') return 'local' + return cls +} + +function projectStructureVariable(v: SchemaStructureVariable): TranspileVariable { + const initial = v.initialValue?.simpleValue?.value + return { + name: v.name, + type: projectStructureVariableType(v.type), + ...(initial !== undefined && initial !== '' ? { initialValue: initial } : {}), + } +} + +function projectVariableType(type: SchemaVariable['type']): TranspileVariableType { + if (type.definition === 'array') { + return { + definition: 'array', + data: { + dimensions: type.data.dimensions.map((d) => ({ dimension: d.dimension })), + baseType: { value: type.data.baseType.value }, + }, + } + } + if (type.definition === 'derived' || type.definition === 'user-data-type') { + return { definition: type.definition, value: type.value } + } + return { definition: 'base-type', value: type.value } +} + +function projectStructureVariableType(type: SchemaStructureVariable['type']): TranspileVariableType { + if (type.definition === 'array') { + return { + definition: 'array', + data: { + dimensions: type.data.dimensions.map((d) => ({ dimension: d.dimension })), + baseType: { value: type.data.baseType.value }, + }, + } + } + if (type.definition === 'derived' || type.definition === 'user-data-type') { + return { definition: type.definition, value: type.value } + } + return { definition: 'base-type', value: type.value } +} + +function projectDataType(dt: SchemaDataType): TranspileDataType { + if (dt.derivation === 'array') { + return { + name: dt.name, + derivation: 'array', + dimensions: dt.dimensions.map((d) => ({ dimension: d.dimension })), + baseType: { value: dt.baseType.value }, + ...(dt.initialValue ? { initialValue: dt.initialValue } : {}), + } + } + if (dt.derivation === 'enumerated') { + return { + name: dt.name, + derivation: 'enumerated', + values: dt.values.map((v) => ({ description: v.description })), + ...(dt.initialValue ? { initialValue: dt.initialValue } : {}), + } + } + // structure + return { + name: dt.name, + derivation: 'structure', + variable: dt.variable.map(projectStructureVariable), + } +} + +function projectTask(task: SchemaTask): TranspileTask { + return { + name: task.name, + priority: task.priority, + triggering: task.triggering, + ...(task.triggering === 'Cyclic' ? { interval: task.interval } : { single: task.interval }), + } +} + +function projectInstance(inst: SchemaInstance): TranspileInstance { + return { + name: inst.name, + program: inst.program, + ...(inst.task ? { task: inst.task } : {}), + } +} + +function stringifyReturnType(returnType: unknown): string { + return typeof returnType === 'string' ? returnType : 'BOOL' +} diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/base-types.ts b/src/backend/shared/transpilers/st-transpiler/helpers/base-types.ts new file mode 100644 index 000000000..8f5aa670b --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/base-types.ts @@ -0,0 +1,33 @@ +/** + * IEC 61131-3 elementary base types — mirrors python's + * `Controler.GetBaseTypes()` (derived from `TypeHierarchy_list` in + * `plcopen/definitions.py:84`). Used by the emit pipeline to + * decide whether a type name resolves to an elementary `` + * element or a `` wrapper. + * + * `WSTRING` is intentionally absent — matches python's `# TODO` + * comment at `definitions.py:118`. + */ + +export const PLC_BASE_TYPES: ReadonlySet = new Set([ + 'BOOL', + 'SINT', + 'INT', + 'DINT', + 'LINT', + 'USINT', + 'UINT', + 'UDINT', + 'ULINT', + 'REAL', + 'LREAL', + 'TIME', + 'DATE', + 'TOD', + 'DT', + 'STRING', + 'BYTE', + 'WORD', + 'DWORD', + 'LWORD', +]) diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/block-library.ts b/src/backend/shared/transpilers/st-transpiler/helpers/block-library.ts new file mode 100644 index 000000000..e98d51ae6 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/block-library.ts @@ -0,0 +1,88 @@ +/** + * Standard block-library resolution against the pre-built catalog. + * + * The full python pipeline resolves block types from three sources — + * TC6 function-block library XMLs, `iec_std.csv` overloads, and + * project-local POUs. The first two are baked into + * `data/std_block_catalog.json` at build time + * (`tools/build_std_catalog.py`); the third is intentionally dropped + * here because the only caller (`emit/pou-graphical.ts`) resolves + * project POUs separately via `project.pous.find(...)`. + * + * Overload behaviour mirrors the python oracle's display mode: when + * a name has multiple catalog entries (ADD, GT, …), the result has + * all I/O collapsed to `'ANY'`. The wrap then narrows via its own + * type-resolution pass. + */ + +import stdCatalog from '../data/std_block_catalog.json' + +export interface BlockIO { + name: string + type: string + qualifier: 'none' | 'negated' | 'rising' | 'falling' +} + +export interface BlockInfos { + name: string + type: 'function' | 'functionBlock' | 'program' | string + extensible: boolean + inputs: BlockIO[] + outputs: BlockIO[] + comment: string + usage: string +} + +export interface BlockResolution { + source: 'standard' + infos: BlockInfos +} + +interface CatalogEntry { + section: string + infos: BlockInfos +} + +const CATALOG: ReadonlyMap = (() => { + const map = new Map() + for (const [name, entries] of Object.entries(stdCatalog as Record)) { + map.set(name, entries) + } + return map +})() + +/** + * Look the block name up in the standard catalog. Single match → + * return its infos. Multiple matches → return the first entry with + * all I/O types collapsed to `'ANY'` (the wrap re-narrows). No + * match → `null`. + */ +export function resolveBlockType(typename: string): BlockResolution | null { + const entries = CATALOG.get(typename) ?? [] + let result: BlockInfos | null = null + for (const entry of entries) { + if (result !== null) return { source: 'standard', infos: collapseToAny(result) } + result = cloneBlockInfos(entry.infos) + } + return result === null ? null : { source: 'standard', infos: result } +} + +function cloneBlockInfos(infos: BlockInfos): BlockInfos { + return { + name: infos.name, + type: infos.type, + extensible: infos.extensible, + inputs: infos.inputs.map((i) => ({ ...i })), + outputs: infos.outputs.map((o) => ({ ...o })), + comment: infos.comment, + usage: infos.usage, + } +} + +function collapseToAny(infos: BlockInfos): BlockInfos { + return { + ...infos, + inputs: infos.inputs.map((i) => ({ ...i, type: 'ANY' })), + outputs: infos.outputs.map((o) => ({ ...o, type: 'ANY' })), + } +} diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/data-type.ts b/src/backend/shared/transpilers/st-transpiler/helpers/data-type.ts new file mode 100644 index 000000000..3fa3bbf12 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/data-type.ts @@ -0,0 +1,10 @@ +/** + * `"D::" + name` — data-type-tagged identifier used as the first + * field of Program-chunk location tuples for `TYPE … END_TYPE` body + * fragments. Mirrors `ComputeDataTypeName` + * (`plcopen/types_enums.py:142`). + */ + +export function ComputeDataTypeName(datatype: string): string { + return `D::${datatype}` +} diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/program.ts b/src/backend/shared/transpilers/st-transpiler/helpers/program.ts new file mode 100644 index 000000000..fda860d82 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/program.ts @@ -0,0 +1,13 @@ +/** + * Chunk model — the unit the transpiler accumulates and then joins + * into the final ST source. + * + * A chunk is `[text, location]`: the text fragment to emit, plus a + * variable-arity tuple identifying where the fragment came from + * (POU tag, region, index, …) so the editor can navigate back to + * source from cursor positions in the generated ST. + */ + +export type LocationAtom = string | number | readonly (string | number)[] +export type Location = readonly LocationAtom[] +export type ProgramChunk = readonly [text: string, location: Location] diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts b/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts new file mode 100644 index 000000000..256758f01 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts @@ -0,0 +1,78 @@ +/** + * Text-manipulation helpers — mirrors python's `ReIndentText` + * (`PLCGenerator.py:66`) and the `Compute*Name` family in + * `plcopen/types_enums.py:112-132`. + */ + +/** + * Reindent every line of `text` to `nbSpaces` leading spaces. + * + * Behavior (exact port of `PLCGenerator.py:66-86`): + * 1. Split on newlines. + * 2. Find the first non-blank line (`.strip() != ""`). + * 3. Count its leading spaces. + * 4. Build an `indent` string of `max(nbSpaces - leadingSpaces, 0)` spaces. + * 5. For every line, prepend `indent` (but emit empty lines as just `"\n"`). + * 6. Always append `"\n"` after each non-empty line. + * + * The function returns `""` when given an entirely blank input. + */ +export function reIndentText(text: string, nbSpaces: number): string { + let compute = '' + const lines = pySplitLines(text) + if (lines.length === 0) return compute + + let lineNum = 0 + while (lineNum < lines.length && lines[lineNum].trim().length === 0) { + lineNum++ + } + if (lineNum >= lines.length) return compute + + let spaces = 0 + const firstNonBlank = lines[lineNum] + while (spaces < firstNonBlank.length && firstNonBlank.charAt(spaces) === ' ') { + spaces++ + } + let indent = '' + for (let i = spaces; i < nbSpaces; i++) indent += ' ' + + for (const line of lines) { + if (line !== '') { + compute += `${indent}${line}\n` + } else { + compute += '\n' + } + } + return compute +} + +/** + * Mirror of Python's `str.splitlines()` for the line separators we encounter. + * + * Key difference from `String.prototype.split('\n')`: Python drops the final + * empty element when the string ends with a separator. PLCOpen-loaded text + * is normalized to `\n` by lxml, so we only need to handle that form here. + */ +function pySplitLines(text: string): string[] { + if (text.length === 0) return [] + const lines = text.split('\n') + if (lines[lines.length - 1] === '') lines.pop() + return lines +} + +/** `"P::" + name` — POU-tagged identifier used as the first field of a + * Program-chunk location tuple. */ +export function computePouName(name: string): string { + return `P::${name}` +} + +/** `"C::" + name`. */ +export function computeConfigurationName(name: string): string { + return `C::${name}` +} + +/** `"R::" + config + "::" + resource` + * (`plcopen/types_enums.py:132`). */ +export function computeConfigurationResourceName(config: string, resource: string): string { + return `R::${config}::${resource}` +} diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts b/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts new file mode 100644 index 000000000..7f93cbc32 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts @@ -0,0 +1,61 @@ +/** + * IEC 61131-3 type hierarchy + compatibility predicate. + * + * Mirrors ``plcopen/definitions.py:84-119`` (`TypeHierarchy_list`), + * ``plcopen/structures.py:36-48`` (`IsOfType`), and + * ``plcopen/structures.py:51-59`` (`GetSubTypes`). + * + * The hierarchy is a tree rooted at `"ANY"` with each concrete IEC type + * (`"INT"`, `"BOOL"`, `"REAL"`, …) attached under its ANY-prefixed + * meta-parent. `isOfType(child, ancestor)` is true when walking parent + * pointers from `child` eventually reaches `ancestor`. + * + * This is used by `GetBlockType`'s overload resolution: given a call like + * `ADD(myInt, myInt)` with signature `(ANY_NUM, ANY_NUM) -> ANY_NUM`, each + * input is type-checked via `isOfType("INT", "ANY_NUM")`. + * + * Note on WSTRING: Python's hierarchy comments-out `("WSTRING", "ANY_STRING")` + * with a TODO. We preserve that — WSTRING returns false for any IsOfType + * lookup against ancestors except itself. + */ + +/** + * Parent pointer table. `null` parent means root (`"ANY"`). + * Insertion order matches `plcopen/definitions.py:TypeHierarchy_list`. + */ +export const TypeHierarchy: Readonly> = { + ANY: null, + ANY_DERIVED: 'ANY', + ANY_ELEMENTARY: 'ANY', + ANY_MAGNITUDE: 'ANY_ELEMENTARY', + ANY_BIT: 'ANY_ELEMENTARY', + ANY_NBIT: 'ANY_BIT', + ANY_STRING: 'ANY_ELEMENTARY', + ANY_DATE: 'ANY_ELEMENTARY', + ANY_NUM: 'ANY_MAGNITUDE', + ANY_REAL: 'ANY_NUM', + ANY_INT: 'ANY_NUM', + ANY_SINT: 'ANY_INT', + ANY_UINT: 'ANY_INT', + BOOL: 'ANY_BIT', + SINT: 'ANY_SINT', + INT: 'ANY_SINT', + DINT: 'ANY_SINT', + LINT: 'ANY_SINT', + USINT: 'ANY_UINT', + UINT: 'ANY_UINT', + UDINT: 'ANY_UINT', + ULINT: 'ANY_UINT', + REAL: 'ANY_REAL', + LREAL: 'ANY_REAL', + TIME: 'ANY_MAGNITUDE', + DATE: 'ANY_DATE', + TOD: 'ANY_DATE', + DT: 'ANY_DATE', + STRING: 'ANY_STRING', + BYTE: 'ANY_NBIT', + WORD: 'ANY_NBIT', + DWORD: 'ANY_NBIT', + LWORD: 'ANY_NBIT', + // WSTRING intentionally absent — matches Python's `# TODO` comment. +} diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/type-text.ts b/src/backend/shared/transpilers/st-transpiler/helpers/type-text.ts new file mode 100644 index 000000000..caa3862b0 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/type-text.ts @@ -0,0 +1,17 @@ +/** + * PLCOpen varlist localName → IEC 61131-3 declaration keyword. + * Identical to the python oracle's `varTypeNames` constant + * (`PLCGenerator.py:38`). Used by the POU wrap to choose the right + * `VAR_*` keyword per variable class. + */ + +export const varTypeNames: Readonly> = { + localVars: 'VAR', + tempVars: 'VAR_TEMP', + inputVars: 'VAR_INPUT', + outputVars: 'VAR_OUTPUT', + inOutVars: 'VAR_IN_OUT', + externalVars: 'VAR_EXTERNAL', + globalVars: 'VAR_GLOBAL', + accessVars: 'VAR_ACCESS', +} diff --git a/src/backend/shared/transpilers/st-transpiler/index.ts b/src/backend/shared/transpilers/st-transpiler/index.ts new file mode 100644 index 000000000..2e65d3878 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/index.ts @@ -0,0 +1,115 @@ +/** + * Project IR → Structured Text transpiler. + * + * Dispatch: + * - Data types, configuration, textual POUs (ST / IL / Python / C++) + * emit directly from the IR via `./emit/*`. + * - Graphical POUs (LD / FBD) emit via the React Flow walker + * (`./walker/`), wrapped by `./emit/pou-graphical.ts`. SFC is + * currently unsupported — `from-schema.ts` throws at projection + * time. + * + * Callers project their own project shape into `TranspileProject` + * via `fromSchemaShape` (defined here, against the schema-shape + * `PLCProjectData` the editor's IPC delivers) — see `from-schema.ts`. + */ + +import { generateConfigurations } from './emit/configuration' +import { generateDataTypes } from './emit/data-types' +import { generateGraphicalPou } from './emit/pou-graphical' +import { generateTextualPou } from './emit/pou-textual' +import { buildPouEmissionOrder } from './pou-emission-order' +import type { TranspileProject } from './types' + +export { fromSchemaShape, type SchemaProjectData } from './from-schema' +export type { + TranspileBody, + TranspileBodyLanguage, + TranspileDataType, + TranspileInstance, + TranspilePou, + TranspilePouInterface, + TranspilePouKind, + TranspileProject, + TranspileTask, + TranspileVariable, + TranspileVariableClass, + TranspileVariableType, +} from './types' + +export interface TranspileResult { + /** Concatenated Structured Text, or `null` if no POU compiled. */ + programSt: string | null + /** Names of POUs that compiled successfully, in emission order. */ + pouNames: string[] + /** Non-fatal diagnostics (skipped empty bodies, missing libraries, …). */ + warnings: string[] + /** + * Per-POU compile errors (and any project-level load error). Empty when + * every POU compiled cleanly. + */ + errors: string[] +} + +const TEXTUAL_LANGUAGES = new Set(['st', 'il', 'python', 'cpp']) + +/** + * Walk a `TranspileProject` and emit Structured Text for every data + * type, POU, and configuration. Pure function — no I/O, no + * network, safe to call in the browser / Web Worker. + * + * Throws synchronously only on internal invariant failures; per-POU + * compile errors land in `result.errors` and the rest of the + * program still emits. + */ +export function transpileToSt(project: TranspileProject): TranspileResult { + const errors: string[] = [] + const warnings: string[] = [] + const pouNames: string[] = [] + const pieces: string[] = [] + + // TYPE … END_TYPE — emit IR-native. + for (const [text] of generateDataTypes(project)) { + pieces.push(text) + } + + // Per-POU emission. + // + // Iteration order mirrors python's lazy / on-reference scheme + // (`PLCGenerator.GeneratePouProgram`, PLCGenerator.py:302): + // dependencies (POUs whose names appear inside another POU's body + // or as a derived-type variable declaration) are emitted BEFORE + // their dependents. See `pou_emission_order.ts`. + const orderedPous = buildPouEmissionOrder(project.pous) + for (const pou of orderedPous) { + try { + if (TEXTUAL_LANGUAGES.has(pou.body.language)) { + const chunks = generateTextualPou(pou, project) + pieces.push(chunks.map((c) => c[0]).join('')) + pouNames.push(pou.name) + continue + } + if (pou.body.language === 'ld' || pou.body.language === 'fbd') { + const chunks = generateGraphicalPou(pou, project) + pieces.push(chunks.map((c) => c[0]).join('')) + pouNames.push(pou.name) + continue + } + errors.push(`POU "${pou.name}" (${pou.body.language} body): language not supported`) + } catch (e) { + const msg = e instanceof Error ? e.message : String(e) + errors.push(`POU "${pou.name}" (${pou.body.language} body): ${msg}`) + } + } + + // Trailing CONFIGURATION block — emit IR-native. + for (const [text] of generateConfigurations(project)) { + pieces.push(text) + } + + if (pouNames.length === 0 && errors.length > 0) { + return { programSt: null, pouNames, warnings, errors } + } + + return { programSt: pieces.join(''), pouNames, warnings, errors } +} diff --git a/src/backend/shared/transpilers/st-transpiler/pou-emission-order.ts b/src/backend/shared/transpilers/st-transpiler/pou-emission-order.ts new file mode 100644 index 000000000..21b5ff73c --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/pou-emission-order.ts @@ -0,0 +1,127 @@ +/** + * Project-level POU emission order, mirroring python's lazy / + * on-reference scheme. + * + * Python's `PLCGenerator.GeneratePouProgram(pou_name)` is recursive + * (PLCGenerator.py:302): + * + * 1. Iterate `project.getpous()` in source order. + * 2. For each POU, mark it computed BEFORE descending so cycles + * don't infinite-loop. + * 3. While generating the POU's body, every block / variable + * reference that resolves to another project POU triggers + * `GeneratePouProgram(dep)` to emit that dependency to the + * shared `self.Program` list FIRST. + * + * Net effect: a depth-first post-order traversal where dependencies + * are emitted ahead of dependents. This module recreates that order + * up-front so the driver loop is a single linear walk — no callback + * threading required through the per-POU generator. + * + * Dependency extraction (mirrors PLCGenerator.py call sites): + * - `block.typeName` for blocks in `LdBody.instances` + * (PLCGenerator.py:1384, 1827). + * - Variable `type` of `derived` / `user-data-type` definitions + * matching a project POU name (FB instance variables, + * PLCGenerator.py:900). + * - Textual body text scanned with python's identifier regex + * `(?:^|[^0-9^A-Z])NAME(?:$|[^0-9^A-Z])` against UPPER(body) + * (PLCGenerator.py:327-331 `GeneratePouProgramInText`). + */ + +import type { TranspilePou, TranspileProject } from './types' + +export function buildPouEmissionOrder(pous: readonly TranspilePou[]): TranspilePou[] { + const byName = new Map() + for (const p of pous) byName.set(p.name, p) + const pouNames = new Set(byName.keys()) + + const emitted = new Set() + const order: TranspilePou[] = [] + + const visit = (name: string): void => { + if (emitted.has(name)) return + emitted.add(name) // mark BEFORE recursion (cycle-safe; matches python) + const pou = byName.get(name) + if (!pou) return + for (const dep of extractPouDeps(pou, pouNames)) visit(dep) + order.push(pou) + } + + for (const p of pous) visit(p.name) + return order +} + +function extractPouDeps(pou: TranspilePou, pouNames: ReadonlySet): string[] { + const deps = new Set() + + // Variable references: a `derived` / `user-data-type` variable + // whose type name matches another project POU is an FB instance + // declaration — emit the FB definition first. + for (const v of pou.interface?.variables ?? []) { + const def = v.type.definition + if (def === 'derived' || def === 'user-data-type') { + const tname = (v.type as { value: string }).value + if (tname !== pou.name && pouNames.has(tname)) deps.add(tname) + } + } + + // Body references — graphical bodies iterate React Flow nodes; + // textual bodies regex-match POU names in the source text. + if (pou.body.language === 'ld') { + for (const rung of pou.body.value.rungs) { + for (const node of rung.nodes) { + if (node.type !== 'block') continue + const typeName = readBlockTypeName(node.data) + if (typeName && typeName !== pou.name && pouNames.has(typeName)) deps.add(typeName) + } + } + } else if (pou.body.language === 'fbd') { + for (const node of pou.body.value.rung.nodes) { + if (node.type !== 'block') continue + const typeName = readBlockTypeName(node.data) + if (typeName && typeName !== pou.name && pouNames.has(typeName)) deps.add(typeName) + } + } else if ( + pou.body.language === 'st' || + pou.body.language === 'il' || + pou.body.language === 'python' || + pou.body.language === 'cpp' + ) { + // Textual body — port of python's `GeneratePouProgramInText`. + // Explicit narrowing because editor's tsconfig doesn't infer + // `body.value` from the negation of the graphical-language list + // above (web's tsconfig does). + const upper = pou.body.value.toUpperCase() + for (const name of pouNames) { + if (name === pou.name) continue + const re = new RegExp(`(?:^|[^0-9^A-Z])${escapeRegex(name.toUpperCase())}(?:$|[^0-9^A-Z])`) + if (re.test(upper)) deps.add(name) + } + } + + return [...deps] +} + +function escapeRegex(s: string): string { + return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') +} + +/** Read the block's referenced POU/function type name from the React + * Flow `data` payload. Editor stores this at `data.variant.name`. */ +function readBlockTypeName(data: Record): string | null { + const variant = data['variant'] + if (!isRecord(variant)) return null + const name = variant['name'] + return typeof name === 'string' ? name : null +} + +function isRecord(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v) +} + +/* Re-export for callers that want to query just the order, e.g. + * for diagnostics or alternative walk schemes. */ +export function emissionOrder(project: TranspileProject): string[] { + return buildPouEmissionOrder(project.pous).map((p) => p.name) +} diff --git a/src/backend/shared/transpilers/st-transpiler/types.ts b/src/backend/shared/transpilers/st-transpiler/types.ts new file mode 100644 index 000000000..02d50c2d3 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/types.ts @@ -0,0 +1,163 @@ +/** + * Minimal IR consumed by the JSON-fed transpiler. + * + * Decoupled from both `middleware/shared/ports/types.ts` (port shape, + * what the renderer store holds) and `backend/shared/types/PLC/open-plc.ts` + * (schema shape, what `project.json` persists). Callers project their + * own shape into `TranspileProject` via the adapter helpers + * (`from-schema.ts` here for the editor's IPC payload; openplc-web + * ships a `transpile-from-port.ts` under middleware for its port-shape + * renderer payload). + * + * Carries ONLY the fields the transpiler actually reads — no + * `servers`, no `remoteDevices`, no `libraries`, no `debugVariables`. + * That makes the surface small, the projections cheap, and the + * transpiler stable when the renderer eventually migrates between + * shapes (only the adapters change; this IR doesn't). + */ + +/* ─────────────────────────── project ────────────────────────────────────── */ + +export interface TranspileProject { + dataTypes: TranspileDataType[] + pous: TranspilePou[] + configuration: { + tasks: TranspileTask[] + instances: TranspileInstance[] + globalVariables: TranspileVariable[] + } +} + +/* ─────────────────────────── pou ────────────────────────────────────────── */ + +export type TranspilePouKind = 'program' | 'function' | 'function-block' +export type TranspileBodyLanguage = 'st' | 'il' | 'ld' | 'fbd' | 'sfc' | 'python' | 'cpp' + +export interface TranspilePou { + name: string + pouType: TranspilePouKind + documentation?: string + /** Empty for POUs with no parameters / locals. */ + interface: TranspilePouInterface + body: TranspileBody +} + +export interface TranspilePouInterface { + /** Only set on `function` POUs. */ + returnType?: string + variables: TranspileVariable[] +} + +/** + * Body payload — discriminated by `language`: + * + * - Textual ('st' | 'il' | 'python' | 'cpp') carry `value: string` + * (raw source) — the transpiler emits the IEC text directly. + * + * - Graphical ('ld' | 'fbd') carry the raw React Flow body + * (`{ rungs: [...] }` for LD, `{ rung: {...} }` for FBD) that the + * editor stores on disk under `pous/s/.{ld,fbd}`. + * The walker in `./walker/` consumes this shape directly — no + * PLCOpen XML intermediate. + * + * The plain-object shape is structured-clonable so the IR rides + * the worker boundary verbatim. + */ +export type TranspileBody = + | { + language: 'st' | 'il' | 'python' | 'cpp' + value: string + } + | { + language: 'ld' + value: import('./walker/types').RFBody + } + | { + language: 'fbd' + value: import('./walker/fbd').RFFbdBody + } + +/* ──────────────────────────── variable ──────────────────────────────────── */ + +export type TranspileVariableClass = 'input' | 'output' | 'inOut' | 'external' | 'local' | 'temp' + +export interface TranspileVariable { + name: string + type: TranspileVariableType + class?: TranspileVariableClass + /** IEC located address (`%QX0.0`, `%IW3`, …). */ + location?: string + /** Raw initial-value text — caller-supplied, no quote-wrapping. */ + initialValue?: string + documentation?: string +} + +/* ──────────────────────────── variable type ─────────────────────────────── */ + +/** + * - `base-type` — elementary IEC type (`BOOL`, `INT`, `STRING`, …) + * - `array` — `ARRAY [a..b, …] OF T` + * - `derived` / + * `user-data-type` — referenced data-type name (struct/enum/subrange) + */ +export type TranspileVariableType = + | { definition: 'base-type'; value: string } + | { definition: 'derived' | 'user-data-type'; value: string } + | { + definition: 'array' + data: { + dimensions: { dimension: string }[] + baseType: string | { value: string } + } + } + +/* ──────────────────────────── data type ─────────────────────────────────── */ + +export type TranspileDataType = + | { + name: string + derivation: 'array' + dimensions: { dimension: string }[] + baseType: string | { value: string } + initialValue?: string + } + | { + name: string + derivation: 'enumerated' + values: { description: string }[] + initialValue?: string + } + | { + name: string + derivation: 'structure' + variable: TranspileVariable[] + initialValue?: string + } + | { + name: string + derivation: 'directly-derived' + baseType: string + initialValue?: string + } + +/* ──────────────────────────── configuration ─────────────────────────────── */ + +export interface TranspileTask { + name: string + priority: number + /** Triggering mode: 'Cyclic' uses `interval`; 'Interrupt' uses `single`. */ + triggering: 'Cyclic' | 'Interrupt' + interval?: string + /** Source signal expression for non-Cyclic tasks (mapped to SINGLE/MULTI). */ + single?: string +} + +export interface TranspileInstance { + /** Instance name (`instance0`). */ + name: string + /** POU type the instance references (`main`). */ + program: string + /** Task this instance binds to. Empty / unset → no task assignment + * (instance lives directly under the resource). */ + task?: string +} diff --git a/src/backend/shared/transpilers/st-transpiler/walker/README.md b/src/backend/shared/transpilers/st-transpiler/walker/README.md new file mode 100644 index 000000000..d6f932e55 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/walker/README.md @@ -0,0 +1,44 @@ +# walker + +React Flow → Structured Text walker. Consumes the **React Flow** +body shape (the exact format `openplc-editor` / `openplc-web` store +in `pous/s/.{ld,fbd}` on disk) and emits the body bytes +that go between `END_VAR` and `END_PROGRAM` — byte-identical to the +python oracle (`xml2st.py`). + +The orchestrator at `../index.ts` calls these entry points via +`../emit/pou-graphical.ts`; the wrap there composes the POU header, +VAR sections, the body bytes returned here, and the closing +`END_PROGRAM` / `END_FUNCTION` / `END_FUNCTION_BLOCK`. + +## Files + +- `ld.ts` — `emitLdBody(body: RFBody): EmitResult`. Handles both + LD and FBD bodies (FBD is a strict subset of LD's vocabulary). +- `fbd.ts` — thin adapter that wraps `{ rung }` into the LD + `{ rungs: [rung] }` shape and delegates to `emitLdBody`. +- `narrow.ts` — type-safe accessors for the loosely-typed + `RFNode.data: Record` payloads. Each `as*Data` + helper returns `null` when the payload doesn't match the expected + shape, letting the walker decide whether to warn or skip. +- `types.ts` — minimal React Flow types (`RFBody`, `RFRung`, + `RFNode`, `RFEdge`). Loose enough that the schema boundary in + `../from-schema.ts` can project the editor's Zod-inferred shapes + without typecasts. + +## Where the algebra lives + +The walker's emission steps (contact/coil dispatch, block-call +emission, parallel-branch factoring) build up `PathNode` trees, then +hand them to `../core/path-tree.ts` for normalisation and chunk +serialisation. Contact / coil modifiers (negated, set, reset, edge +triggers) flow through `../core/modifiers.ts:extractModifier`. + +## Source of truth + +`xml2st`'s python `PLCGenerator.py` is the canonical reference for +every emission rule. Any divergence between this walker's output +and the oracle is a walker bug, never an oracle bug — see the +fixture corpus under `xml2st/fixtures/` and the test harness under +`xml2st/shared-backend/transpilers/generate-st-from-react-flow/tests/` +for the validation loop. diff --git a/src/backend/shared/transpilers/st-transpiler/walker/fbd.ts b/src/backend/shared/transpilers/st-transpiler/walker/fbd.ts new file mode 100644 index 000000000..82c9b481b --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/walker/fbd.ts @@ -0,0 +1,29 @@ +/** + * React Flow FBD body → Structured Text. + * + * FBD is a strict subset of LD's node vocabulary (no contacts, no + * coils, no power rails, no parallel-branch markers) plus connector + * / continuation pairs. Both subsets feed the same execution-order + * driven sink iteration and the same block-call / variable emission + * code paths, so the FBD entry point is a thin adapter that wraps + * the body's single `rung` into the `RFBody.rungs[]` shape the LD + * walker consumes. + * + * FBD-specific behaviour (ENO gating on outVariable assignments, + * connector caching, continuation lookup, executionOrderId-bucketed + * sink sort) all live in the LD walker — they're no-ops for LD + * bodies that don't exercise them. + */ +import { emitLdBody, type EmitResult } from './ld' +import type { RFRung } from './types' + +export type { EmitResult } from './ld' + +/** FBD body shape — a single `rung` container of nodes + edges. */ +export interface RFFbdBody { + rung: RFRung +} + +export function emitFbdBody(body: RFFbdBody): EmitResult { + return emitLdBody({ rungs: [body.rung] }) +} diff --git a/src/backend/shared/transpilers/st-transpiler/walker/ld.ts b/src/backend/shared/transpilers/st-transpiler/walker/ld.ts new file mode 100644 index 000000000..fe2c3a6b4 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/walker/ld.ts @@ -0,0 +1,813 @@ +/** + * React Flow LD body → Structured Text. + * + * Walks `RFBody.rungs[*].nodes/edges` directly — no PLCOpen + * intermediate. Output must match the python oracle + * (`xml2st.py --keep-structs --no-complex-parser`) byte-for-byte; + * `tests/per_case.test.ts` is the per-case validation loop, and + * `tests/golden_react_flow.test.ts` covers the larger harvested + * corpus once it lands. + * + * Reuses the legacy LD walker's algorithmic primitives + * (`path_tree`, `modifiers`) for boolean factorization and modifier + * emission — those modules are PLCOpen-independent and don't pull + * the `LdBody` IR in. Only the topology traversal is rewritten to + * read React Flow's `nodes`/`edges` directly. + */ + +import { extractModifier } from '../core/modifiers' +import { + computePaths, + factorizePaths, + leafNode, + type Location, + type PathNode, + type ProgramChunk, + TRUE_NODE, +} from '../core/path-tree' +import { + asBlockData, + asCoilData, + asConnectorData, + asContactData, + asContinuationData, + asParallelData, + asPowerRailData, + asVariableData, + asVariableExpressionData, + type BlockData, + coilModifierFromVariant, + contactModifierFromVariant, + type VariableData, +} from './narrow' +import type { RFBody, RFEdge, RFNode, RFRung } from './types' + +/** + * A variable the walker synthesises during emission that the caller + * must declare in the POU's trailing local `VAR` section. Two + * flavours flow through this shape: + * - Trigger instances (`R_TRIG1`, `F_TRIG1`, …) — `type` is + * already the resolved `'R_TRIG'`/`'F_TRIG'` name. `origin*` + * fields are absent. + * - Function-call output temps (`_TMP__`) + * — `type` is `'BOOL'` for `ENO`, otherwise the literal string + * `'ANY'`. When `'ANY'`, the caller resolves it against the + * standard block catalog or the project's POU table using + * `originBlockTypeName` + `originFormalParameter`. + */ +export interface SyntheticVar { + name: string + type: string + originBlockTypeName?: string + originFormalParameter?: string +} + +export interface EmitResult { + bodySt: string + syntheticVars: SyntheticVar[] + warnings: string[] +} + +interface WalkerState { + tagName: string + byId: Map + /** Edges keyed by their target node id. */ + incoming: Map + /** Outgoing edges keyed by source node id — used to detect + * standalone blocks (no consumer of any output). */ + outgoing: Map + program: ProgramChunk[] + currentIndent: string + declaredVars: Set + triggerVars: { name: string; type: 'R_TRIG' | 'F_TRIG' }[] + /** `_TMP__` temps synthesised by + * function-call emission. `originBlockTypeName` + + * `originFormalParameter` let the caller resolve `'ANY'` types + * against the standard block catalog or a project POU's declared + * `returnType` after the walk completes. */ + functionTempVars: { + name: string + type: string + originBlockTypeName: string + originFormalParameter: string + }[] + emittedBlocks: Set + /** Connector expressions cached by name, consumed by continuation + * visits later in the same rung (FBD-only). */ + connectorExprs: Map + /** Cumulative Y-offset per node id — mirrors how `ladder-xml.ts` + * globalises rung positions when it serialises React Flow into + * PLCOpen XML (each rung's `reactFlowViewport[1]` height adds to + * the running offset). Used to position-sort sinks across rungs + * in the same coordinate space the python oracle sees. */ + yOffset: Map + warnings: string[] +} + +function rungHeight(rung: RFRung): number { + const vp = rung.reactFlowViewport + if (Array.isArray(vp) && typeof vp[1] === 'number' && Number.isFinite(vp[1])) { + return vp[1] + } + return 0 +} + +/** + * Location-tuple identity for a React Flow node. Prefers the editor- + * assigned `data.numericId` (an integer, matching PLCOpen `@localId`) + * over the React Flow UUID — `factorizePaths` sorts paths by the + * stable `pythonReprNode` key, so the location bytes embedded in chunk + * reprs decide tie-break order between sibling AND-paths. The python + * oracle sees `int` localIds; using the same integers here keeps our + * sort byte-identical with it. Falls back to `node.id` when a fixture + * lacks `numericId` (per-case fixtures that don't need the integer + * round-trip). + */ +function locId(node: RFNode): number | string { + const raw = node.data['numericId'] + if (typeof raw === 'string') { + const parsed = Number.parseInt(raw, 10) + if (Number.isFinite(parsed) && String(parsed) === raw) return parsed + } + if (typeof raw === 'number' && Number.isInteger(raw)) return raw + return node.id +} + +/** + * True for any node that holds a variable reference — covers both + * vocabularies the editor produces: + * - 'variable' with `data.variant` of 'input'/'output'/'inout' (LD + * bodies + the older FBD shape used by per-case fixtures) + * - 'input-variable' / 'output-variable' / 'inout-variable' (modern + * FBD bodies as harvested from real projects) + */ +function isVariableNode(node: RFNode): boolean { + return ( + node.type === 'variable' || + node.type === 'input-variable' || + node.type === 'output-variable' || + node.type === 'inout-variable' + ) +} + +/* ─────────────────────────── public entry ───────────────────────────────── */ + +export function emitLdBody(body: RFBody): EmitResult { + // POU name doesn't influence body emission today (it's only the + // first element of the `Location` tuples used for source-map back- + // references). Hard-code a sentinel; the orchestrator can pass a + // real name when it wires this walker in. + const tagName = 'P::main' + const state: WalkerState = { + tagName, + byId: new Map(), + incoming: new Map(), + outgoing: new Map(), + program: [], + currentIndent: ' ', + declaredVars: new Set(), + triggerVars: [], + functionTempVars: [], + emittedBlocks: new Set(), + connectorExprs: new Map(), + yOffset: new Map(), + warnings: [], + } + + // Index every node + edge from every rung up front. Sinks are then + // collected and bucketed GLOBALLY (across all rungs) — the python + // oracle iterates a flat instance list, so per-rung bucketing would + // misorder cases where an ordered block (executionOrderId > 0) lives + // in a later rung but must emit before unordered work earlier in the + // body. Each rung's local Y coords are shifted by the cumulative + // viewport heights of earlier rungs (matches `ladder-xml.ts`:612). + let cumulativeYOffset = 0 + for (const rung of body.rungs) { + for (const node of rung.nodes) { + if (state.byId.has(node.id)) { + state.warnings.push(`duplicate node id "${node.id}" across rungs`) + } + state.byId.set(node.id, node) + state.incoming.set(node.id, []) + state.outgoing.set(node.id, []) + state.yOffset.set(node.id, cumulativeYOffset) + } + for (const edge of rung.edges) { + const targetBucket = state.incoming.get(edge.target) + if (targetBucket !== undefined) targetBucket.push(edge) + const sourceBucket = state.outgoing.get(edge.source) + if (sourceBucket !== undefined) sourceBucket.push(edge) + } + cumulativeYOffset += rungHeight(rung) + } + + // Collect sinks across the body. A sink is any instance that + // appears in the body iteration sweep (mirrors PLCGenerator.py's + // generate-instances loop): + // - coils, outVariables, inOutVariables — write targets + // - blocks (idempotent via `emittedBlocks`; ensures cascaded + // blocks get their call site regardless of consumer chain) + // - connectors — cache their upstream expression for later + // continuation visits. + // + // Empty-name variables are dropped to match `ladder-xml.ts`:602, which + // filters them out of the PLCOpen serialization the oracle consumes. + // Those nodes represent dangling block outputs the user never wired + // to a real variable. + const sinks: RFNode[] = [] + for (const rung of body.rungs) { + for (const node of rung.nodes) { + if (node.type === 'coil') { + if (asCoilData(node.data)?.variable) sinks.push(node) + } else if (isVariableNode(node)) { + const data = asVariableData(node.data) + if (data === null || data.variable === '') continue + if (data.variant === 'output' || data.variant === 'inout') sinks.push(node) + } else if (node.type === 'block') { + sinks.push(node) + } else if (node.type === 'connector') { + sinks.push(node) + } + } + } + + // Bucket by executionOrderId: explicit > 0 emits first (sorted + // ascending), zero/unset falls back to position sort (Y first + // 10-unit tol, then X). Mirrors `body_emit.ts` in the legacy + // walker. Walks launched from an ordered sink propagate + // `order=true` so upstream blocks don't emit eagerly — they emit + // at their own iteration step instead. + const ordered: RFNode[] = [] + const others: RFNode[] = [] + for (const node of sinks) { + if (nodeExecutionOrder(node) > 0) ordered.push(node) + else others.push(node) + } + ordered.sort((a, b) => nodeExecutionOrder(a) - nodeExecutionOrder(b)) + others.sort((a, b) => compareNodePosition(state, a, b)) + + for (const sink of ordered) emitSink(state, sink) + for (const sink of others) emitSink(state, sink) + + const bodySt = '\n' + state.program.map((c) => c[0]).join('') + const syntheticVars: SyntheticVar[] = [ + ...state.triggerVars.map((t) => ({ name: t.name, type: t.type })), + ...state.functionTempVars, + ] + return { bodySt, syntheticVars, warnings: state.warnings } +} + +function nodeExecutionOrder(node: RFNode): number { + if (node.type === 'coil') return asCoilData(node.data)?.executionOrder ?? 0 + if (node.type === 'block') return asBlockData(node.data)?.executionOrder ?? 0 + if (isVariableNode(node)) return asVariableData(node.data)?.executionOrder ?? 0 + return 0 +} + +function compareNodePosition(state: WalkerState, a: RFNode, b: RFNode): number { + const aOff = state.yOffset.get(a.id) ?? 0 + const bOff = state.yOffset.get(b.id) ?? 0 + const ax = Math.trunc(a.position.x) + const ay = Math.trunc(a.position.y + aOff) + const bx = Math.trunc(b.position.x) + const by = Math.trunc(b.position.y + bOff) + if (Math.abs(ay - by) >= 10) return ay - by + return ax - bx +} + +function emitSink(state: WalkerState, node: RFNode): void { + // Top-level sink walks ALWAYS use order=false — the sink's own + // executionOrderId doesn't propagate into its upstream walks + // (mirrors body_emit.emitCoil / emitOutVariable in the legacy + // walker). Order suppression is purely a block→upstream-block + // concern, applied inside `buildInputArgs` based on the emitting + // block's eoid. + if (node.type === 'coil') return emitCoilNode(state, node) + if (isVariableNode(node)) { + const data = asVariableData(node.data) + if (data?.variant === 'output') return emitOutVariableNode(state, node, data) + if (data?.variant === 'inout') return emitInOutVariableNode(state, node, data) + return + } + if (node.type === 'block') return emitStandaloneBlock(state, node) + if (node.type === 'connector') return emitConnectorNode(state, node) +} + +/* ─────────────────────────── coil emission ──────────────────────────────── */ + +function emitCoilNode(state: WalkerState, node: RFNode): void { + const data = asCoilData(node.data) + if (data === null) { + state.warnings.push(`coil node "${node.id}" has unrecognised data shape`) + return + } + + const paths = pathsFromIncoming(state, node.id, /*order=*/ false) + if (paths.length === 0) { + state.warnings.push(`Coil "${data.variable}" must be connected.`) + return + } + const expr = pathsToChunks(paths) + const coilInfo: Location = [state.tagName, 'coil', locId(node)] + const modifier = coilModifierFromVariant(data.variant) + const modified = extractModifier(state, modifier, expr, coilInfo) + + state.program.push([state.currentIndent, []]) + state.program.push([data.variable, [...coilInfo, 'reference']]) + state.program.push([' := ', []]) + for (const chunk of modified) state.program.push(chunk) + state.program.push([';\n', []]) +} + +/* ─────────────────────────── outVariable emission ───────────────────────── */ + +function emitOutVariableNode(state: WalkerState, node: RFNode, data: VariableData): void { + const paths = pathsFromIncoming(state, node.id, /*order=*/ false) + if (paths.length === 0) { + state.warnings.push(`outVariable "${data.variable}" must be connected.`) + return + } + const expr = pathsToChunks(paths) + const info: Location = [state.tagName, 'io_variable', locId(node), 'expression'] + + // ENO gating — when the single upstream connection points at a + // block with `EN` wired, wrap the assignment in + // `IF THEN ... END_IF;`. Mirrors PLCGenerator.py:1243 + // (`GetUsedEno`). + const enoVar = getUsedEnoForNode(state, node.id) + if (enoVar !== null) { + state.program.push([`${state.currentIndent}IF ${enoVar}`, []]) + state.program.push([' THEN\n ', []]) + state.currentIndent += ' ' + } + + state.program.push([state.currentIndent, []]) + state.program.push([data.variable, info]) + state.program.push([' := ', []]) + for (const chunk of expr) state.program.push(chunk) + state.program.push([';\n', []]) + + if (enoVar !== null) { + state.currentIndent = state.currentIndent.slice(0, -2) + state.program.push([`${state.currentIndent}END_IF;\n`, []]) + } +} + +/** + * If the only incoming edge to `nodeId` originates from a block that + * has its `EN` input wired, return the ST reference of the matching + * `ENO` output — `.ENO` for FB instances, + * `_TMP__ENO` for functions. Otherwise null. + */ +function getUsedEnoForNode(state: WalkerState, nodeId: string): string | null { + const edges = state.incoming.get(nodeId) ?? [] + if (edges.length !== 1) return null + const upstream = state.byId.get(edges[0].source) + if (upstream === undefined || upstream.type !== 'block') return null + const data = asBlockData(upstream.data) + if (data === null) return null + // Block has EN wired iff some incoming edge targets the EN handle. + const blockIncoming = state.incoming.get(upstream.id) ?? [] + const enWired = blockIncoming.some((e) => e.targetHandle === 'EN') + if (!enWired) return null + if (data.blockKind === 'function-block-instance') { + return `${data.instanceName}.ENO` + } + return `_TMP_${data.typeName}${data.numericId}_ENO` +} + +/* ─────────────────────────── inOutVariable emission ────────────────────── */ + +function emitInOutVariableNode(state: WalkerState, node: RFNode, data: VariableData): void { + const paths = pathsFromIncoming(state, node.id, /*order=*/ false) + if (paths.length === 0) { + state.warnings.push(`inOutVariable "${data.variable}" must be connected.`) + return + } + const expr = pathsToChunks(paths) + const info: Location = [state.tagName, 'io_variable', locId(node), 'expression'] + state.program.push([state.currentIndent, []]) + state.program.push([data.variable, info]) + state.program.push([' := ', []]) + for (const chunk of expr) state.program.push(chunk) + state.program.push([';\n', []]) +} + +/** + * Connector emission: walk the connector's single incoming edge, + * cache the resulting expression chunks under the connector's name + * for later continuation lookups. No bytes emitted into + * `state.program` — the continuation is what surfaces the cached + * expression at its consumer's call site. + */ +function emitConnectorNode(state: WalkerState, node: RFNode): void { + const data = asConnectorData(node.data) + if (data === null) { + state.warnings.push(`connector node "${node.id}" has unrecognised data shape`) + return + } + if (state.connectorExprs.has(data.name)) return + const paths = pathsFromIncoming(state, node.id, /*order=*/ false) + if (paths.length === 0) return + state.connectorExprs.set(data.name, pathsToChunks(paths)) +} + +/* ─────────────────────────── standalone block ───────────────────────────── */ + +function emitStandaloneBlock(state: WalkerState, node: RFNode): void { + const data = asBlockData(node.data) + if (data === null) { + state.warnings.push(`block node "${node.id}" has unrecognised data shape`) + return + } + if (data.blockKind === 'function-block-instance') { + emitFunctionBlockCall(state, node, data) + } else { + emitFunctionCall(state, node, data) + } +} + +/* ─────────────────────────── upstream walk ──────────────────────────────── */ + +function pathsFromIncoming(state: WalkerState, nodeId: string, order: boolean): PathNode[] { + const edges = state.incoming.get(nodeId) ?? [] + const out: PathNode[] = [] + for (const edge of edges) { + const upstream = state.byId.get(edge.source) + if (upstream === undefined) continue + const node = visitUpstream(state, upstream, edge, order) + if (node !== undefined) out.push(node) + } + return out +} + +function visitUpstream(state: WalkerState, node: RFNode, edge: RFEdge, order: boolean): PathNode | undefined { + switch (node.type) { + case 'powerRail': { + const data = asPowerRailData(node.data) + // A left-rail upstream contributes "always-on" energization. + // A right-rail upstream is never reached during a back-walk + // (rails sink wires; they don't source them). + if (data?.variant === 'left') return TRUE_NODE + return undefined + } + case 'contact': + return visitContact(state, node, order) + case 'parallel': + return visitParallel(state, node, order) + case 'variable': + case 'input-variable': + case 'output-variable': + case 'inout-variable': + return visitVariable(state, node) + case 'block': + return visitBlockOutput(state, node, edge, order) + case 'coil': + // Coil-as-passthrough: editor topologies sometimes route a wire + // past a coil to feed a downstream node. Mirror legacy walker + // behaviour: emit nothing for the coil, just propagate the + // upstream signal. + return visitCoilPassthrough(state, node, order) + case 'continuation': + return visitContinuation(state, node) + case 'connector': + // Connectors are sinks, never sources — skip when encountered + // as an upstream during a back-walk. + return undefined + default: + state.warnings.push(`unknown upstream node type "${node.type}"`) + return undefined + } +} + +function visitContact(state: WalkerState, node: RFNode, order: boolean): PathNode { + const data = asContactData(node.data) + if (data === null) { + state.warnings.push(`contact node "${node.id}" has unrecognised data shape`) + return TRUE_NODE + } + const contactInfo: Location = [state.tagName, 'contact', locId(node)] + const variableChunks: ProgramChunk[] = [[data.variable, [...contactInfo, 'reference']]] + const modifier = contactModifierFromVariant(data.variant) + const variableLeaf = leafNode(extractModifier(state, modifier, variableChunks, contactInfo)) + + const upstream = pathsFromIncoming(state, node.id, order) + if (upstream.length === 0) { + state.warnings.push(`Contact "${data.variable}" must be connected.`) + return variableLeaf + } + if (upstream.length === 1) { + const only = upstream[0] + if (only.kind === 'true') return variableLeaf + if (only.kind === 'and') { + return { kind: 'and', children: [variableLeaf, ...only.children] } + } + return { kind: 'and', children: [variableLeaf, only] } + } + const factored = factorizePaths(upstream) + if (factored.length > 1) { + return { + kind: 'and', + children: [variableLeaf, { kind: 'or', children: factored }], + } + } + const tail = factored[0] + const tailChildren = tail.kind === 'and' ? tail.children : [tail] + return { kind: 'and', children: [variableLeaf, ...tailChildren] } +} + +function visitCoilPassthrough(state: WalkerState, node: RFNode, order: boolean): PathNode | undefined { + const upstream = pathsFromIncoming(state, node.id, order) + if (upstream.length === 0) return undefined + if (upstream.length === 1) return upstream[0] + const factored = factorizePaths(upstream) + if (factored.length === 1) return factored[0] + return { kind: 'or', children: factored } +} + +/** + * Parallel-marker nodes (open/close) are zero-cost pass-throughs in + * the path-tree algebra. Their job is purely topological: they let + * React Flow express wire merges that the editor renders as parallel + * rails. + * + * - `close`: collect every incoming branch, factor common terms, + * return as a single `or` (or unwrapped child if only one). + * - `open`: collapse to its single upstream — the open marker + * itself doesn't contribute a term. + */ +function visitParallel(state: WalkerState, node: RFNode, order: boolean): PathNode | undefined { + const data = asParallelData(node.data) + if (data === null) { + state.warnings.push(`parallel node "${node.id}" has unrecognised data shape`) + return undefined + } + const paths = pathsFromIncoming(state, node.id, order) + if (paths.length === 0) return undefined + // Flatten nested OR children — when the editor chains close markers + // to render a 3+ way merge (each close takes 2 inputs), the naive + // recursion produces `((A OR B) OR C)`. The python oracle has no + // such intermediates and emits a flat `A OR B OR C`; the + // factorize-then-stable-sort below relies on the OR being flat. + const flat: PathNode[] = [] + for (const p of paths) { + if (p.kind === 'or') flat.push(...p.children) + else flat.push(p) + } + if (data.side === 'open') { + // Open markers should have exactly one upstream wire; if multiple + // arrive, fall back to OR-ing them so we surface the topology + // weirdness rather than silently dropping branches. + if (flat.length === 1) return flat[0] + const factored = factorizePaths(flat) + if (factored.length === 1) return factored[0] + return { kind: 'or', children: factored } + } + // close + if (flat.length === 1) return flat[0] + const factored = factorizePaths(flat) + if (factored.length === 1) return factored[0] + return { kind: 'or', children: factored } +} + +/** + * Continuation visit: surface the connector expression cached + * earlier under the same `name`. If the matching connector hasn't + * been visited yet (iteration order quirk), resolve it eagerly by + * walking the connector's incoming edge now. + */ +function visitContinuation(state: WalkerState, node: RFNode): PathNode | undefined { + const data = asContinuationData(node.data) + if (data === null) { + state.warnings.push(`continuation node "${node.id}" has unrecognised data shape`) + return undefined + } + const cached = state.connectorExprs.get(data.name) + if (cached !== undefined) return leafNode([...cached]) + const connector = findConnectorByName(state, data.name) + if (connector !== undefined) { + emitConnectorNode(state, connector) + const resolved = state.connectorExprs.get(data.name) + if (resolved !== undefined) return leafNode([...resolved]) + } + state.warnings.push(`continuation "${data.name}" has no matching connector`) + return undefined +} + +function findConnectorByName(state: WalkerState, name: string): RFNode | undefined { + for (const node of state.byId.values()) { + if (node.type !== 'connector') continue + if (asConnectorData(node.data)?.name === name) return node + } + return undefined +} + +/** + * A `variable` node — both inVariable (variant='input') and inOut- + * Variable when read as an upstream — contributes its `expression` + * (or fallback variable name) as a single leaf. + */ +function visitVariable(state: WalkerState, node: RFNode): PathNode | undefined { + // Editor stores `data.variable.name` as the expression text; when + // the body comes from a non-editor source (or older versions), a + // dedicated `data.expression` field may be present instead. + const expr = asVariableExpressionData(node.data)?.expression ?? asVariableData(node.data)?.variable + if (expr === undefined || expr === '') return undefined + return leafNode([[expr, [state.tagName, 'io_variable', locId(node), 'expression']]]) +} + +/* ─────────────────────────── block emission ─────────────────────────────── */ + +/** + * One block-output read. Emits the block-call statement on first + * visit (idempotent via `emittedBlocks`) and returns a leaf naming + * the requested output — `.` for FB instances or + * `_TMP__` for functions. + */ +function visitBlockOutput(state: WalkerState, node: RFNode, edge: RFEdge, order: boolean): PathNode | undefined { + const data = asBlockData(node.data) + if (data === null) { + state.warnings.push(`block node "${node.id}" has unrecognised data shape`) + return undefined + } + const requestedOutput = + typeof edge.sourceHandle === 'string' && edge.sourceHandle.length > 0 + ? edge.sourceHandle + : (data.outputs[0] ?? 'OUT') + + // `order=true` suppresses eager emission: the upstream block emits + // at its own iteration step instead. Only emit now when walking + // from an unordered sink (or the block hasn't been reached yet + // and the consumer will need its call site). + if (data.blockKind === 'function-block-instance') { + if (!order) emitFunctionBlockCall(state, node, data) + const out = requestedOutput + return leafNode([[`${data.instanceName}.${out}`, [state.tagName, 'block', locId(node), 'output', out]]]) + } + // function path + if (!order) emitFunctionCall(state, node, data) + const out = requestedOutput + const tempName = `_TMP_${data.typeName}${data.numericId}_${out}` + return leafNode([[tempName, [state.tagName, 'block', locId(node), 'output', out]]]) +} + +function emitFunctionBlockCall(state: WalkerState, node: RFNode, data: BlockData): void { + if (state.emittedBlocks.has(node.id)) return + state.emittedBlocks.add(node.id) + + // Block-input walks inherit the block's own ordering — when the + // block carries an explicit executionOrderId, upstream blocks + // emit at their own steps; otherwise they emit eagerly. + const recurseOrdered = data.executionOrder > 0 + const info: Location = [state.tagName, 'block', locId(node)] + const parts = buildInputArgs(state, node, data, /*useNamedArgs=*/ true, recurseOrdered) + + state.program.push([state.currentIndent, []]) + state.program.push([data.instanceName, [...info, 'instance']]) + state.program.push(['(', []]) + for (let i = 0; i < parts.length; i++) { + if (i > 0) state.program.push([', ', []]) + for (const chunk of parts[i]) state.program.push(chunk) + } + state.program.push([');\n', []]) +} + +function emitFunctionCall(state: WalkerState, node: RFNode, data: BlockData): void { + if (state.emittedBlocks.has(node.id)) return + state.emittedBlocks.add(node.id) + + const info: Location = [state.tagName, 'block', locId(node)] + const wiredInputs = data.inputs.filter((name) => firstIncomingForHandle(state, node.id, name) !== undefined) + const allInputConnected = wiredInputs.length === data.inputs.length + const useNamedArgs = data.outputs.length > 1 || !allInputConnected + + const recurseOrdered = data.executionOrder > 0 + const parts = buildInputArgs(state, node, data, useNamedArgs, recurseOrdered) + + // Pick the primary output — function emits `_TMP__ + // := (...)` on the LHS; secondary outputs (e.g. `ENO`) tail + // as `param => tempName` extras. + let primaryName: string | null = null + let primaryFormal = '' + for (let i = 0; i < data.outputs.length; i++) { + const out = data.outputs[i] + const tempName = `_TMP_${data.typeName}${data.numericId}_${out}` + const tempType = out === 'ENO' ? 'BOOL' : 'ANY' + state.functionTempVars.push({ + name: tempName, + type: tempType, + originBlockTypeName: data.typeName, + originFormalParameter: out, + }) + const isPrimary = data.outputs.length === 1 || out === '' || out === 'OUT' + if (isPrimary && primaryName === null) { + primaryName = tempName + primaryFormal = out + } else { + parts.push([ + [out, [...info, 'output', i]], + [` => ${tempName}`, []], + ]) + } + } + if (primaryName === null) return + + state.program.push([state.currentIndent, []]) + state.program.push([primaryName, [...info, 'output', 0]]) + state.program.push([' := ', []]) + state.program.push([data.typeName, [...info, 'type']]) + state.program.push(['(', []]) + for (let i = 0; i < parts.length; i++) { + if (i > 0) state.program.push([', ', []]) + for (const chunk of parts[i]) state.program.push(chunk) + } + state.program.push([');\n', []]) + void primaryFormal +} + +function buildInputArgs( + state: WalkerState, + node: RFNode, + data: BlockData, + useNamedArgs: boolean, + order: boolean, +): ProgramChunk[][] { + const parts: ProgramChunk[][] = [] + for (const inputName of data.inputs) { + if (data.extensible) { + // Extensible inputs: the editor's XML serializer (`fbd-xml.ts`:67) + // emits one `` per edge that + // targets this handle. The python oracle then puts the + // serialized list through a dict keyed on the formal parameter + // (`PLCGenerator.py`:1498) — duplicates overwrite, so all + // occurrences of the same name resolve to the LAST connected + // edge's upstream. Mimic that here: emit one arg per edge, all + // using the shared "last edge" upstream's expression. + const edges = edgesForBlockInput(state, node.id, inputName) + if (edges.length === 0) continue + const lastEdge = edges[edges.length - 1] + const lastUpstream = state.byId.get(lastEdge.source) + if (lastUpstream === undefined) continue + const sharedPath = visitUpstream(state, lastUpstream, lastEdge, order) + if (sharedPath === undefined) continue + const sharedExpr = pathsToChunks([sharedPath]) + for (let i = 0; i < edges.length; i++) { + if (useNamedArgs) { + const chunk: ProgramChunk[] = [[`${inputName} := `, []]] + for (const c of sharedExpr) chunk.push(c) + parts.push(chunk) + } else { + parts.push([...sharedExpr]) + } + } + continue + } + const upstreamPaths = pathsForBlockInput(state, node.id, inputName, order) + if (upstreamPaths.length === 0) continue + const inputExpr = pathsToChunks(upstreamPaths) + if (useNamedArgs) { + const chunk: ProgramChunk[] = [[`${inputName} := `, []]] + for (const c of inputExpr) chunk.push(c) + parts.push(chunk) + } else { + parts.push([...inputExpr]) + } + } + return parts +} + +function edgesForBlockInput(state: WalkerState, blockId: string, inputName: string): RFEdge[] { + const incoming = state.incoming.get(blockId) ?? [] + return incoming.filter((e) => e.targetHandle === inputName) +} + +function pathsForBlockInput(state: WalkerState, blockId: string, inputName: string, order: boolean): PathNode[] { + const incoming = state.incoming.get(blockId) ?? [] + const out: PathNode[] = [] + for (const edge of incoming) { + if (edge.targetHandle !== inputName) continue + const upstream = state.byId.get(edge.source) + if (upstream === undefined) continue + const node = visitUpstream(state, upstream, edge, order) + if (node !== undefined) out.push(node) + } + return out +} + +function firstIncomingForHandle(state: WalkerState, blockId: string, handle: string): RFEdge | undefined { + const incoming = state.incoming.get(blockId) ?? [] + for (const edge of incoming) { + if (edge.targetHandle === handle) return edge + } + return undefined +} + +/* ─────────────────────────── helpers ────────────────────────────────────── */ + +function pathsToChunks(paths: PathNode[]): ProgramChunk[] { + if (paths.length === 0) return [['TRUE', []]] + if (paths.length === 1) return computePaths(paths[0], /*first=*/ true) + const factored = factorizePaths(paths) + if (factored.length === 1) return computePaths(factored[0], /*first=*/ true) + return computePaths({ kind: 'or', children: factored }, /*first=*/ true) +} diff --git a/src/backend/shared/transpilers/st-transpiler/walker/narrow.ts b/src/backend/shared/transpilers/st-transpiler/walker/narrow.ts new file mode 100644 index 000000000..127740cc7 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/walker/narrow.ts @@ -0,0 +1,254 @@ +/** + * Narrowing helpers — read the loosely-typed `data: Record` + * payload on `RFNode` into a structured shape the walker can consume + * without `as` casts. + * + * Each helper returns `null` when the payload doesn't match the + * expected shape so the walker can decide whether to warn or skip. + * Forward-compatible: extra fields are ignored, missing optional + * fields default to safe values. + */ + +import type { CoilModifier, ContactModifier } from '../core/modifier-types' + +export type ContactVariant = 'default' | 'negated' | 'risingEdge' | 'fallingEdge' +export type CoilVariant = ContactVariant | 'set' | 'reset' +export type PowerRailSide = 'left' | 'right' +export type VariableVariant = 'input' | 'output' | 'inout' +export type BlockKind = 'function' | 'function-block' | 'function-block-instance' + +export interface ContactData { + variant: ContactVariant + variable: string +} + +export interface CoilData { + variant: CoilVariant + variable: string + executionOrder: number +} + +export interface PowerRailData { + variant: PowerRailSide +} + +export interface VariableData { + variant: VariableVariant + variable: string + executionOrder: number + block?: { id: string; handleId: string } +} + +export interface ConnectorData { + name: string +} + +export interface ContinuationData { + name: string +} + +export interface BlockData { + typeName: string + blockKind: BlockKind + instanceName: string + /** Editor-assigned numeric localId — string of digits. Feeds the + * `_TMP__` temp-var naming and the + * PLCOpen `@localId` round-trip. */ + numericId: string + executionOrder: number + executionControl: boolean + /** Extensible-input flag — when true, multiple edges may target a + * single declared input handle; the editor's XML serializer emits + * one `` per edge (`fbd-xml.ts:67`), and the python + * oracle then collapses them into "for each occurrence of the + * formal parameter in the variable list, use the LAST connected + * variable's expression" (`PLCGenerator.py:1498`). */ + extensible: boolean + /** Declared input formal-parameter names, in declaration order. */ + inputs: string[] + /** Declared output formal-parameter names, in declaration order. */ + outputs: string[] +} + +export interface VariableExpression { + expression: string +} + +export interface ParallelData { + side: 'open' | 'close' +} + +function isObject(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v) +} + +function asString(v: unknown): string | null { + return typeof v === 'string' ? v : null +} + +function asNumber(v: unknown): number | null { + return typeof v === 'number' && Number.isFinite(v) ? v : null +} + +function asBoolean(v: unknown): boolean { + return v === true +} + +function readVariableName(v: unknown): string { + if (!isObject(v)) return '' + const name = v['name'] + return typeof name === 'string' ? name : '' +} + +export function asContactData(data: Record): ContactData | null { + const variant = data['variant'] + if (variant !== 'default' && variant !== 'negated' && variant !== 'risingEdge' && variant !== 'fallingEdge') + return null + return { variant, variable: readVariableName(data['variable']) } +} + +export function asCoilData(data: Record): CoilData | null { + const variant = data['variant'] + if ( + variant !== 'default' && + variant !== 'negated' && + variant !== 'risingEdge' && + variant !== 'fallingEdge' && + variant !== 'set' && + variant !== 'reset' + ) + return null + const eo = asNumber(data['executionOrder']) + return { + variant, + variable: readVariableName(data['variable']), + executionOrder: eo ?? 0, + } +} + +export function asPowerRailData(data: Record): PowerRailData | null { + const variant = data['variant'] + if (variant !== 'left' && variant !== 'right') return null + return { variant } +} + +export function asVariableData(data: Record): VariableData | null { + // Two variant shapes coexist in real-world fixtures: + // - LD per-case + serialized LD bodies: 'input' / 'output' / 'inout' + // - Modern FBD bodies: 'input-variable' / 'output-variable' / 'inout-variable' + // Both serialize to the same PLCOpen XML element; map both to the + // short form so downstream code only sees one vocabulary. + const rawVariant = data['variant'] + let variant: VariableVariant + if (rawVariant === 'input' || rawVariant === 'input-variable') variant = 'input' + else if (rawVariant === 'output' || rawVariant === 'output-variable') variant = 'output' + else if (rawVariant === 'inout' || rawVariant === 'inout-variable') variant = 'inout' + else return null + const out: VariableData = { + variant, + variable: readVariableName(data['variable']), + executionOrder: asNumber(data['executionOrder']) ?? 0, + } + const block = data['block'] + if (isObject(block)) { + const id = asString(block['id']) + const handleId = asString(block['handleId']) + if (id !== null && handleId !== null) out.block = { id, handleId } + } + return out +} + +export function asConnectorData(data: Record): ConnectorData | null { + const name = asString(data['name']) + if (name === null) return null + return { name } +} + +export function asContinuationData(data: Record): ContinuationData | null { + const name = asString(data['name']) + if (name === null) return null + return { name } +} + +export function asParallelData(data: Record): ParallelData | null { + const t = data['type'] + if (t !== 'open' && t !== 'close') return null + return { side: t } +} + +export function asBlockData(data: Record): BlockData | null { + const variant = data['variant'] + if (!isObject(variant)) return null + const typeName = asString(variant['name']) + if (typeName === null) return null + const rawKind = variant['type'] + const blockKind: BlockKind = + rawKind === 'function-block' || rawKind === 'function-block-instance' ? 'function-block-instance' : 'function' + const inputs: string[] = [] + const outputs: string[] = [] + const variables = variant['variables'] + if (Array.isArray(variables)) { + for (const v of variables) { + if (!isObject(v)) continue + const name = asString(v['name']) + if (name === null) continue + const cls = v['class'] + if (cls === 'input') inputs.push(name) + else if (cls === 'output') outputs.push(name) + // `inOut` (also written `'inout'` in some shapes) is a single + // formal parameter that the call site binds as an input *and* + // the caller can read post-call as an output. For call-site + // emission both the python oracle and the editor's XML + // serializer treat it as an input slot (the binding appears in + // the argument list as ` := `), so we list it + // under `inputs` in source order — VAR_IN_OUT typically appears + // before VAR_INPUT in the declaration, which is the order the + // call site emits. + else if (cls === 'inOut' || cls === 'inout') inputs.push(name) + } + } + const rawNumericId = data['numericId'] + const numericId = + typeof rawNumericId === 'string' + ? rawNumericId + : typeof rawNumericId === 'number' && Number.isFinite(rawNumericId) + ? String(rawNumericId) + : '' + return { + typeName, + blockKind, + instanceName: readVariableName(data['variable']), + numericId, + executionOrder: asNumber(data['executionOrder']) ?? 0, + executionControl: asBoolean(data['executionControl']), + extensible: asBoolean(variant['extensible']), + inputs, + outputs, + } +} + +/** inVariable / outVariable / inOutVariable carry an `expression` + * string (variable name OR literal OR free-form expression). */ +export function asVariableExpressionData(data: Record): VariableExpression | null { + const expression = asString(data['expression']) + if (expression === null) return null + return { expression } +} + +export function contactModifierFromVariant(variant: ContactVariant): ContactModifier { + const mod: ContactModifier = {} + if (variant === 'negated') mod.negated = true + else if (variant === 'risingEdge') mod.edge = 'rising' + else if (variant === 'fallingEdge') mod.edge = 'falling' + return mod +} + +export function coilModifierFromVariant(variant: CoilVariant): CoilModifier { + const mod: CoilModifier = {} + if (variant === 'negated') mod.negated = true + else if (variant === 'risingEdge') mod.edge = 'rising' + else if (variant === 'fallingEdge') mod.edge = 'falling' + else if (variant === 'set') mod.storage = 'set' + else if (variant === 'reset') mod.storage = 'reset' + return mod +} diff --git a/src/backend/shared/transpilers/st-transpiler/walker/types.ts b/src/backend/shared/transpilers/st-transpiler/walker/types.ts new file mode 100644 index 000000000..a2182413f --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/walker/types.ts @@ -0,0 +1,51 @@ +/** + * Minimal types describing the React Flow LD body shape — exactly + * what the editor stores in `pous/s/.ld` after the + * declaration / variables header. + * + * Kept narrower than the renderer's `RungLadderState` (the editor + * type carries layout-only fields like `defaultBounds`, + * `reactFlowViewport`, `selectedNodes`). Only the walker-relevant + * fields are typed here; the rest passes through as `unknown` so + * harvested fixtures from different editor versions still load. + */ + +export interface RFBody { + rungs: RFRung[] +} + +export interface RFRung { + /** Layout-only; the walker doesn't read it. Optional so the FBD + * schema shape (`{ comment, nodes, edges }`, no `id`) flows in + * without a typecast at the projection boundary. */ + id?: string + comment?: string + nodes: RFNode[] + edges: RFEdge[] + /** Layout-only field. Stored as `[width, height]`; the walker reads + * the height to globalise per-rung Y coordinates the same way the + * editor's `ladder-xml.ts` does when it serialises to PLCOpen. */ + reactFlowViewport?: unknown +} + +export interface RFNode { + id: string + /** Kept as `string` (not narrowed to a literal union) so the schema + * boundary in `from-schema.ts` can pass the editor's `node.type: + * string` straight through without a typecast. Known values the + * walker dispatches on: `'powerRail' | 'contact' | 'coil' | 'block' + * | 'variable' | 'input-variable' | 'output-variable' | + * 'inout-variable' | 'parallel' | 'connector' | 'continuation'`. + * Anything else is treated as a no-op sink. */ + type: string + position: { x: number; y: number } + data: Record +} + +export interface RFEdge { + id: string + source: string + target: string + sourceHandle?: string | null + targetHandle?: string | null +} diff --git a/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx b/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx index b402cc585..b2cc87886 100644 --- a/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx +++ b/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx @@ -96,6 +96,33 @@ export const GraphicalEditorAutocomplete = forwardRef { + const trimmed = searchValue.trim().toLowerCase() + if (trimmed) { + const exactMatch = variables?.find((v) => v.name.toLowerCase() === trimmed) + if (exactMatch) { + return { + id: exactMatch.id ?? '', + name: exactMatch.name, + } + } + } + const addVariableOption = selectableValues.find((item) => item.type === 'add') + return addVariableOption ? addVariableOption.variable : null + } + // @ts-expect-error - not all properties are used useImperativeHandle(ref, () => { return { @@ -111,9 +138,9 @@ export const GraphicalEditorAutocomplete = forwardRef { if (selectedVariable.positionInArray === -1) { - const addVariableOption = selectableValues.find((item) => item.type === 'add') - if (addVariableOption) { - submitAutocompletion({ variable: addVariableOption.variable }) + const implicit = resolveImplicitSubmitOption() + if (implicit) { + submitAutocompletion({ variable: implicit }) } else { closeModal() } @@ -122,7 +149,16 @@ export const GraphicalEditorAutocomplete = forwardRef { switch (keyDown) { @@ -152,13 +188,13 @@ export const GraphicalEditorAutocomplete = forwardRef item.type === 'add') - if (addVariableOption) { - submitAutocompletion({ variable: addVariableOption.variable }) + const implicit = resolveImplicitSubmitOption() + if (implicit) { + submitAutocompletion({ variable: implicit }) } else { - // No 'add' option available; close the autocomplete to provide clear feedback + // Nothing matched and no 'add' option available; close + // the autocomplete to give the user clear feedback. closeModal() } } else { diff --git a/src/frontend/components/_features/[workspace]/editor/device/configuration/components/pin-mapping-table.tsx b/src/frontend/components/_features/[workspace]/editor/device/configuration/components/pin-mapping-table.tsx index 321d06419..bcdb5b45e 100644 --- a/src/frontend/components/_features/[workspace]/editor/device/configuration/components/pin-mapping-table.tsx +++ b/src/frontend/components/_features/[workspace]/editor/device/configuration/components/pin-mapping-table.tsx @@ -1,6 +1,14 @@ +import { toast } from '@root/frontend/components/_features/[app]/toast/use-toast' import { pinSelectors } from '@root/frontend/hooks/use-store-selectors' import { useOpenPLCStore } from '@root/frontend/store' import type { DevicePin } from '@root/middleware/shared/ports/types' +import { + buildAddressPool, + buildAliasRegistry, + describeSource, + validateAliasEdit, +} from '@root/middleware/shared/utils/iec-address' +import { resolveTargetCapabilities } from '@root/middleware/shared/utils/target-capabilities' import { createColumnHelper } from '@tanstack/react-table' import { GenericTable } from '../../../../../../_atoms/generic-table' @@ -39,6 +47,54 @@ const PinMappingTable = ({ pins, selectedRowId, handleRowClick }: PinMappingTabl const updatePin = pinSelectors.useUpdatePin() const handleUpdateDataRequest = (_rowIndex: number, columnId: string, value: unknown) => { + // Phase 1 — write-time alias-uniqueness gate (global, across all + // producers). `checkIfPinAliasIsValid` inside the slice's + // `updatePin` already enforces uniqueness *within* the active + // board's pin list; this additional check covers the cross- + // producer case where a pin alias collides with a VPP channel + // alias, a Modbus point alias, or an EtherCAT channel alias. + if (columnId === 'alias' && typeof value === 'string') { + const state = useOpenPLCStore.getState() + const board = state.deviceDefinitions.configuration.deviceBoard + const currentPins = state.deviceDefinitions.pinMapping.pinsByBoard[board] ?? [] + const currentPin = currentPins[state.deviceDefinitions.pinMapping.currentSelectedPinTableRow] + const sourceRef = { kind: 'pin-mapping' as const, ref: currentPin?.address ?? '' } + const boardInfo = state.deviceAvailableOptions.availableBoards.get(board ?? '') + const ioMapping = + ( + state.deviceDefinitions.configuration.vendorScreenData?.['io-mapping'] as + | { entries?: Array<{ iecAddress: string; alias?: string; slot: number; channelName: string }> } + | undefined + )?.entries ?? [] + const pool = buildAddressPool( + { + pinMapping: { pins: currentPins }, + vendorIoMapping: { entries: ioMapping }, + remoteDevices: state.project.data.remoteDevices, + }, + resolveTargetCapabilities(boardInfo), + ) + const registry = buildAliasRegistry(pool) + const validation = validateAliasEdit(registry, value, sourceRef) + if (!validation.ok) { + toast({ + title: 'Alias already in use', + description: `"${value}" is already assigned to ${describeSource(validation.conflict.source)} (${validation.conflict.address}). Alias names must be unique across all I/O channels.`, + variant: 'fail', + }) + return { ok: false, title: 'Alias already in use', message: 'Pin alias collides with another producer.' } + } + + // Phase 2 — cascade rename onto bound variables BEFORE + // mutating the pin, so the subsequent `syncVariableAliases()` + // sees variables pointing at the new alias and refreshes + // locations rather than orphaning them. + const oldAlias = currentPin?.alias ?? '' + if (oldAlias) { + useOpenPLCStore.getState().projectActions.renameAlias(oldAlias, value) + } + } + const res = updatePin({ [columnId as keyof DevicePin]: value, }) diff --git a/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/io-table-layout.tsx b/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/io-table-layout.tsx index 634da9df0..998b4fd85 100644 --- a/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/io-table-layout.tsx +++ b/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/io-table-layout.tsx @@ -1,8 +1,15 @@ +import { toast } from '@root/frontend/components/_features/[app]/toast/use-toast' import { useOpenPLCStore } from '@root/frontend/store' import { getSectionPersistenceKey } from '@root/frontend/utils/vpp/persistence-keys' import { resolveModuleChannels, type ResolverModuleDef } from '@root/frontend/utils/vpp/resolve-module-channels' import type { IoMappingEntry, VendorIoMapping } from '@root/middleware/shared/ports/types' -import { buildAddressPool, nextFreeAddress } from '@root/middleware/shared/utils/iec-address' +import { + buildAddressPool, + buildAliasRegistry, + describeSource, + nextFreeAddress, + validateAliasEdit, +} from '@root/middleware/shared/utils/iec-address' import { resolveTargetCapabilities } from '@root/middleware/shared/utils/target-capabilities' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -144,11 +151,53 @@ function IoTableLayout({ section, moduleSystem }: IoTableLayoutProps) { }, [slots, formatSelectionKey]) const handleAliasChange = (index: number, alias: string) => { + const target = entries[index] + if (!target) return + const sourceRef = { kind: 'vpp-io' as const, ref: `slot-${target.slot}:${target.channelName}` } + + // Phase 1 — write-time uniqueness gate. See + // `module-slots-layout.tsx::handleAliasChange` for the full + // rationale; same pattern, scoped to this layout's `entries` + // array as the VPP-IO source. + const state = useOpenPLCStore.getState() + const boardInfo = state.deviceAvailableOptions.availableBoards.get( + state.deviceDefinitions.configuration.deviceBoard ?? '', + ) + const pool = buildAddressPool( + { + pinMapping: { + pins: state.deviceDefinitions.pinMapping.pinsByBoard[state.deviceDefinitions.configuration.deviceBoard] ?? [], + }, + vendorIoMapping: { entries }, + remoteDevices: state.project.data.remoteDevices, + }, + resolveTargetCapabilities(boardInfo), + ) + const registry = buildAliasRegistry(pool) + const validation = validateAliasEdit(registry, alias, sourceRef) + if (!validation.ok) { + toast({ + title: 'Alias already in use', + description: `"${alias}" is already assigned to ${describeSource(validation.conflict.source)} (${validation.conflict.address}). Alias names must be unique across all I/O channels.`, + variant: 'fail', + }) + return + } + + // Phase 2 — cascade rename onto bound variables BEFORE writing + // so the subsequent `syncVariableAliases()` sees variables + // pointing at the new alias and takes the refresh path instead + // of orphan. + const oldAlias = target.alias ?? '' + if (oldAlias) { + useOpenPLCStore.getState().projectActions.renameAlias(oldAlias, alias) + } + const updated = [...entries] updated[index] = { ...updated[index], alias } setEntries(updated) setVendorScreenData(persistenceKey, { entries: updated }) - // Alias name changed on a single entry — refresh. + // Refresh variables against any allocator-driven address shifts. useOpenPLCStore.getState().projectActions.syncVariableAliases() } diff --git a/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/module-slots-layout.tsx b/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/module-slots-layout.tsx index 52dab851f..0b6c14cca 100644 --- a/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/module-slots-layout.tsx +++ b/src/frontend/components/_features/[workspace]/editor/device/configuration/vendor-screen/layouts/module-slots-layout.tsx @@ -6,6 +6,7 @@ import { Checkbox } from '@root/frontend/components/_atoms/checkbox' import { Label } from '@root/frontend/components/_atoms/label' import { Select, SelectContent, SelectItem, SelectTrigger } from '@root/frontend/components/_atoms/select' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@root/frontend/components/_atoms/tooltip' +import { toast } from '@root/frontend/components/_features/[app]/toast/use-toast' import { Modal, ModalContent, ModalTitle } from '@root/frontend/components/_molecules/modal' import { boardSelectors } from '@root/frontend/hooks/use-store-selectors' import { useOpenPLCStore } from '@root/frontend/store' @@ -14,7 +15,13 @@ import { getSectionPersistenceKey } from '@root/frontend/utils/vpp/persistence-k import { resolveModuleChannels, type ResolverModuleDef } from '@root/frontend/utils/vpp/resolve-module-channels' import type { IoMappingEntry } from '@root/middleware/shared/ports/types' import { useDevice } from '@root/middleware/shared/providers/platform-context' -import { buildAddressPool, nextFreeAddress } from '@root/middleware/shared/utils/iec-address' +import { + buildAddressPool, + buildAliasRegistry, + describeSource, + nextFreeAddress, + validateAliasEdit, +} from '@root/middleware/shared/utils/iec-address' import { resolveTargetCapabilities } from '@root/middleware/shared/utils/target-capabilities' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -569,11 +576,55 @@ function ModuleSlotsLayout({ section, moduleSystem }: ModuleSlotsLayoutProps) { const handleAliasChange = (slot: number, channelName: string, alias: string) => { const state = useOpenPLCStore.getState() const vsd = state.deviceDefinitions.configuration.vendorScreenData - const entries = ((vsd?.['io-mapping'] as { entries?: IoMappingEntry[] } | undefined)?.entries ?? []).map((e) => - e.slot === slot && e.channelName === channelName ? { ...e, alias } : e, + const sourceRef = { kind: 'vpp-io' as const, ref: `slot-${slot}:${channelName}` } + + // Phase 1 — write-time uniqueness gate. Build a fresh registry + // from the live state (including the entry being edited, scoped + // to the active board's capabilities) and reject the edit if the + // new alias is already claimed by a different channel. Without + // this gate, the pool's silent first-wins reservation would + // cause every variable that the user later binds to the losing + // entry to collapse to the winner's address through + // `syncVariableAliases`'s refresh path. + const boardInfo = state.deviceAvailableOptions.availableBoards.get( + state.deviceDefinitions.configuration.deviceBoard ?? '', + ) + const currentEntries = (vsd?.['io-mapping'] as { entries?: IoMappingEntry[] } | undefined)?.entries ?? [] + const pool = buildAddressPool( + { + pinMapping: { + pins: state.deviceDefinitions.pinMapping.pinsByBoard[state.deviceDefinitions.configuration.deviceBoard] ?? [], + }, + vendorIoMapping: { entries: currentEntries }, + remoteDevices: state.project.data.remoteDevices, + }, + resolveTargetCapabilities(boardInfo), ) + const registry = buildAliasRegistry(pool) + const validation = validateAliasEdit(registry, alias, sourceRef) + if (!validation.ok) { + toast({ + title: 'Alias already in use', + description: `"${alias}" is already assigned to ${describeSource(validation.conflict.source)} (${validation.conflict.address}). Alias names must be unique across all I/O channels.`, + variant: 'fail', + }) + return + } + + // Phase 2 — cascade rename onto bound variables BEFORE writing + // the new entries, so the subsequent `syncVariableAliases()` + // call sees variables already pointing at the new alias name and + // takes the refresh path (location follows alias) instead of the + // orphan path (location cleared, warning glyph rendered). + const oldAlias = currentEntries.find((e) => e.slot === slot && e.channelName === channelName)?.alias ?? '' + if (oldAlias) { + useOpenPLCStore.getState().projectActions.renameAlias(oldAlias, alias) + } + + const entries = currentEntries.map((e) => (e.slot === slot && e.channelName === channelName ? { ...e, alias } : e)) setVendorScreenData('io-mapping', { entries }) - // Alias name changed — refresh variables bound to the old name. + // Refresh variables bound to the (now-renamed) alias against + // any address shifts produced by the change. useOpenPLCStore.getState().projectActions.syncVariableAliases() } diff --git a/src/frontend/components/_features/[workspace]/editor/package-manager/catalog-browser.tsx b/src/frontend/components/_features/[workspace]/editor/package-manager/catalog-browser.tsx index 96319f6b1..276bb290e 100644 --- a/src/frontend/components/_features/[workspace]/editor/package-manager/catalog-browser.tsx +++ b/src/frontend/components/_features/[workspace]/editor/package-manager/catalog-browser.tsx @@ -27,20 +27,13 @@ import { DownloadIcon } from '@root/frontend/assets/icons/interface/Download' import { MagnifierIcon } from '@root/frontend/assets/icons/interface/Magnifier' import { RefreshIcon } from '@root/frontend/assets/icons/interface/Refresh' import { TrashCanIcon } from '@root/frontend/assets/icons/interface/TrashCan' +import { APP_VERSION } from '@root/frontend/data/constants/app-version' import { useOpenPLCStore } from '@root/frontend/store' import { compareSemver, isCompatibleEditorVersion } from '@root/frontend/utils/semver' import type { RemoteCatalogEntry, RemoteVersionEntry } from '@root/middleware/shared/ports/types' import { usePackages } from '@root/middleware/shared/providers/platform-context' import { useCallback, useEffect, useMemo, useState } from 'react' -// Build-time global injected by the bundler (webpack DefinePlugin on -// editor; Vite `define` on web). Declared module-locally here so the -// shared file resolves on web — editor also has a project-wide -// declaration in `src/globals.d.ts` with the same type, which this -// one duplicates (TypeScript merges identical `declare const` -// declarations within scopes, so there's no conflict on editor). -declare const APP_VERSION: string - interface CatalogBrowserProps { installedVersions: Map /** diff --git a/src/frontend/components/_molecules/global-variables-table/editable-cell.tsx b/src/frontend/components/_molecules/global-variables-table/editable-cell.tsx index e8bd7ec82..f25ee7761 100644 --- a/src/frontend/components/_molecules/global-variables-table/editable-cell.tsx +++ b/src/frontend/components/_molecules/global-variables-table/editable-cell.tsx @@ -291,22 +291,25 @@ const EditableLocationCell = ({ const [cellValue, setCellValue] = useState(initialValue ?? '') - // Alias staleness check. Lifted above `onBlur` so the short-circuit - // can take it into account — when the user's previously-bound alias - // has been renamed/removed upstream, re-picking the same address - // from the dropdown should still refresh the variable's stored - // alias. Mirrors the local-table variant of this cell. + // Alias staleness checks. See the local variables-table cell + // (`_molecules/variables-table/editable-cell.tsx`) for the longer + // explanation — same two flavours of staleness, same exception + // semantics on the location-column short-circuit. const variableAlias = original?.alias + const variableLocation = original?.location ?? '' const aliasRegistry = useAliasRegistry() const isOrphaned = !!variableAlias && !aliasRegistry.byAlias.has(variableAlias) + const isMismatched = + !!variableAlias && aliasRegistry.byAlias.has(variableAlias) + ? aliasRegistry.byAlias.get(variableAlias)?.address !== variableLocation + : false const onBlur = (value: string) => { - // Same short-circuit semantics as the local variables-table cell: - // skip unchanged-value blurs unless the variable's alias is - // orphaned, in which case the user re-picking the same address - // is their signal to refresh the alias. `updateVariable`'s - // auto-adopt path re-resolves against the live alias registry. - if (value === initialValue && !isOrphaned) return + // Short-circuit unchanged-value blurs unless the variable's alias + // is orphaned or mismatched (alias points at a different address + // than the variable's location) — in either case re-picking the + // same address is the user's signal to refresh the alias. + if (value === initialValue && !(isOrphaned || isMismatched)) return const res = table.options.meta?.updateData(index, id, value) if (res?.ok) { setCellValue(value) diff --git a/src/frontend/components/_molecules/variables-table/editable-cell.tsx b/src/frontend/components/_molecules/variables-table/editable-cell.tsx index cda865d47..9947e435c 100644 --- a/src/frontend/components/_molecules/variables-table/editable-cell.tsx +++ b/src/frontend/components/_molecules/variables-table/editable-cell.tsx @@ -471,26 +471,37 @@ const EditableLocationCell = ({ const isEditable = useCallback(isCellEditable, [id, variable, isDebuggerVisible]) - // Alias staleness check. Lifted above `onBlur` so the short-circuit - // can take it into account — when the user's previously-bound alias - // has been renamed/removed upstream (pin mapping, backplane, etc.), - // re-picking the same address from the dropdown should refresh the - // variable's stored alias. Without this hoist `isOrphaned` was - // only used for the rendered warning glyph below. + // Alias staleness checks. Lifted above `onBlur` so the short- + // circuit can take them into account. Two flavours of staleness + // both demand that the location-pick re-fires `updateVariable` + // even when the picked value matches the cell's current address: + // + // - `isOrphaned`: the variable's stored alias is no longer in the + // registry's `byAlias` (producer renamed/removed it). + // Re-picking the same address re-runs the auto-adopt and + // refreshes the alias against the live registry. + // - `isMismatched`: the variable carries an alias that points at + // a *different* address than its current location. Happens when + // legacy projects authored before `createVariable`'s auto-adopt + // fix carried a stale alias from one row to the next through the + // "+ button" template spread. Without this exception, clicking + // on the right alias for the current address would no-op because + // the address itself isn't changing — the user would have to + // pick a DIFFERENT alias first, then return to the desired one, + // to force the alias write through. const aliasRegistry = useAliasRegistry() const isOrphaned = !!variable?.alias && !aliasRegistry.byAlias.has(variable.alias) + const isMismatched = + !!variable?.alias && aliasRegistry.byAlias.has(variable.alias) + ? aliasRegistry.byAlias.get(variable.alias)?.address !== variable.location + : false // When the input is blurred, we'll call our table meta's updateData function const onBlur = (value: string) => { // Short-circuit unchanged-value blurs so re-focus doesn't fire a - // gratuitous state update. Exception: when the user re-picks the - // SAME location for a variable whose stored alias is now orphaned - // (the producer renamed it), force the update through so - // `updateVariable`'s auto-adopt path re-resolves the address - // against the live alias registry and refreshes the variable's - // alias field. Otherwise the orphan warning would persist - // forever and the only workaround would be Clear → re-pick. - if (value === initialValue && !(id === 'location' && isOrphaned)) return + // gratuitous state update. Exceptions for the location column, + // see the `isOrphaned` / `isMismatched` block above. + if (value === initialValue && !(id === 'location' && (isOrphaned || isMismatched))) return const res = table.options.meta?.updateData(index, id, value) if (res?.ok) { setCellValue(value) diff --git a/src/frontend/components/_organisms/about-modal/index.tsx b/src/frontend/components/_organisms/about-modal/index.tsx index c449f4207..729571f30 100644 --- a/src/frontend/components/_organisms/about-modal/index.tsx +++ b/src/frontend/components/_organisms/about-modal/index.tsx @@ -1,13 +1,19 @@ import { useEffect, useState } from 'react' import { useAccelerator, useCapabilities, useSystem } from '../../../../middleware/shared/providers' - -declare const APP_VERSION: string | undefined -declare const BUILD_DATE: string | undefined import openPlcLogo from '../../../assets/icons/about/logo.svg' +import { APP_VERSION } from '../../../data/constants/app-version' import { useOpenPLCStore } from '../../../store' import { Modal, ModalContent } from '../../_molecules/modal' +// Per-app product name, injected at build time (Vite `define` in web = +// 'OpenPLC Web'; webpack DefinePlugin in the editor = 'OpenPLC Editor'). This +// file is byte-identical across both repos — the displayed name differs only +// because the injected value differs. Declared defensively so an un-injected +// build (tests) falls back to 'OpenPLC' instead of throwing. +declare const APP_NAME: string | undefined +declare const BUILD_DATE: string | undefined + const AboutModal = () => { const { workspaceActions: { setModalOpen }, @@ -27,7 +33,7 @@ const AboutModal = () => { const closeModal = () => { setModalOpen('aboutOpenPlc', false) } - const title = `OpenPLC Editor ${typeof APP_VERSION !== 'undefined' ? APP_VERSION : ''}` + const title = `${typeof APP_NAME !== 'undefined' ? APP_NAME : 'OpenPLC'} ${APP_VERSION}` const releaseDate = `Release: ${typeof BUILD_DATE !== 'undefined' ? BUILD_DATE : ''}` const description = 'Open Source IDE for the OpenPLC Runtime, compliant with the IEC 61131-3 international standard.' const copyrightYear = new Date().getFullYear() diff --git a/src/frontend/components/_organisms/global-variables-editor/index.tsx b/src/frontend/components/_organisms/global-variables-editor/index.tsx index f04128b6e..ffc828e19 100644 --- a/src/frontend/components/_organisms/global-variables-editor/index.tsx +++ b/src/frontend/components/_organisms/global-variables-editor/index.tsx @@ -236,7 +236,11 @@ const GlobalVariablesEditor = () => { selectedRow === ROWS_NOT_SELECTED ? variables[variables.length - 1] : variables[selectedRow] ) as PLCGlobalVariable - const newVarData = { ...variable, documentation: '' } + // Don't carry the previous variable's `alias` into the new row. + // See variables-editor/index.tsx for the longer explanation — + // tl;dr: `createVariable` auto-increments `location`, and keeping + // the stale alias would break the alias-↔-location invariant. + const newVarData = { ...variable, alias: undefined, documentation: '' } if (selectedRow === ROWS_NOT_SELECTED) { createVariable({ scope: 'global', data: newVarData }) diff --git a/src/frontend/components/_organisms/variables-editor/index.tsx b/src/frontend/components/_organisms/variables-editor/index.tsx index 88e0d4470..33a713a52 100644 --- a/src/frontend/components/_organisms/variables-editor/index.tsx +++ b/src/frontend/components/_organisms/variables-editor/index.tsx @@ -424,8 +424,17 @@ const VariablesEditor = ({ name: propName, isActive: _isActive = true }: Variabl const variable: PLCVariable = selectedRow === ROWS_NOT_SELECTED ? variables[variables.length - 1] : variables[selectedRow] + // Don't carry the previous variable's `alias` into the new row. + // The slice's `createVariable` auto-increments `location`; if we + // kept the old alias attached, the new variable would claim the + // OLD channel's alias while pointing at the NEW address — + // breaking the alias-↔-location invariant. `createVariable`'s + // auto-adopt path resolves the right alias for the new location + // against the live registry (matching whichever producer-channel + // owns the auto-incremented address). const newVarData = { ...variable, + alias: undefined, class: defaultClass, type: variable.type.definition === 'derived' diff --git a/src/frontend/data/constants/app-version.ts b/src/frontend/data/constants/app-version.ts new file mode 100644 index 000000000..4aa14620e --- /dev/null +++ b/src/frontend/data/constants/app-version.ts @@ -0,0 +1,21 @@ +/** + * Single source of truth for the OpenPLC application version, SHARED + * byte-for-byte between openplc-web and openplc-editor. + * + * This file lives under `src/frontend/`, so the mirror gate + * (`scripts/compare-surfaces.py`, run by `ci-sync.yml`) byte-compares it + * across both repos — the two IDEs can never display different versions. + * Bumping the version is a single edit here, carried identically into both + * repos by the coordinated mirror PR. + * + * Consumers: + * - the About modal renders this directly (both apps); + * - the web build writes it into `version.json` (`version` field); + * - the editor's electron-builder reads `package.json.version`, kept equal + * to this value by `release.yml`. + * + * NOTE: this is the human-facing semver only. The web "force update" check + * compares a per-deploy `BUILD_ID` (git commit SHA), not this version, so a + * stale tab is detected on every deploy even without a version bump. + */ +export const APP_VERSION = '4.2.2' diff --git a/src/frontend/hooks/use-device-configuration.ts b/src/frontend/hooks/use-device-configuration.ts index 72b49239c..7039be1d6 100644 --- a/src/frontend/hooks/use-device-configuration.ts +++ b/src/frontend/hooks/use-device-configuration.ts @@ -1,6 +1,8 @@ import { enrichDeviceData } from '@root/backend/shared/ethercat/enrich-device-data' import { generateDefaultChannelMappings, pdoToChannels } from '@root/backend/shared/ethercat/esi-parser' import { extractDefaultSdoConfigurations } from '@root/backend/shared/ethercat/sdo-config-defaults' +import { toast } from '@root/frontend/components/_features/[app]/toast/use-toast' +import { useOpenPLCStore } from '@root/frontend/store' import type { ConfiguredEtherCATDevice, EnrichDeviceData, @@ -10,6 +12,13 @@ import type { EtherCATSlaveConfig, } from '@root/middleware/shared/ports/esi-types' import { useEsi } from '@root/middleware/shared/providers/platform-context' +import { + buildAddressPool, + buildAliasRegistry, + describeSource, + validateAliasEdit, +} from '@root/middleware/shared/utils/iec-address' +import { resolveTargetCapabilities } from '@root/middleware/shared/utils/target-capabilities' import { useCallback, useEffect, useRef, useState } from 'react' type UseDeviceConfigurationParams = { @@ -106,10 +115,65 @@ export function useDeviceConfiguration({ const handleAliasChange = useCallback( (channelId: string, alias: string) => { if (!device) return + + // Resolve the bus owning this slave so we can construct a + // `SourceRef` whose `ref` matches the format used by the + // address pool (`${busName}:${slaveName}:${channelId}` — see + // `address-pool.ts:243`). Without this, `validateAliasEdit`'s + // "ignoring" comparison wouldn't recognise a no-op self-rename + // and would spuriously reject it. + const state = useOpenPLCStore.getState() + const owningBus = state.project.data.remoteDevices?.find((d) => + d.ethercatConfig?.devices?.some((s) => s.name === device.name), + ) + const busName = owningBus?.name ?? '' + const sourceRef = { kind: 'ethercat' as const, ref: `${busName}:${device.name}:${channelId}` } + + // Phase 1 — write-time uniqueness gate (global across all + // producers). Build a fresh registry from the live state and + // reject the edit on collision. See + // `module-slots-layout.tsx::handleAliasChange` for the longer + // rationale. + const board = state.deviceDefinitions.configuration.deviceBoard ?? '' + const boardInfo = state.deviceAvailableOptions.availableBoards.get(board) + const ioMapping = + ( + state.deviceDefinitions.configuration.vendorScreenData?.['io-mapping'] as + | { entries?: Array<{ iecAddress: string; alias?: string; slot: number; channelName: string }> } + | undefined + )?.entries ?? [] + const pool = buildAddressPool( + { + pinMapping: { pins: state.deviceDefinitions.pinMapping.pinsByBoard[board] ?? [] }, + vendorIoMapping: { entries: ioMapping }, + remoteDevices: state.project.data.remoteDevices, + }, + resolveTargetCapabilities(boardInfo), + ) + const registry = buildAliasRegistry(pool) + const validation = validateAliasEdit(registry, alias, sourceRef) + if (!validation.ok) { + toast({ + title: 'Alias already in use', + description: `"${alias}" is already assigned to ${describeSource(validation.conflict.source)} (${validation.conflict.address}). Alias names must be unique across all I/O channels.`, + variant: 'fail', + }) + return + } + + // Phase 2 — cascade rename onto bound variables BEFORE + // writing the new alias so the downstream sync sees variables + // pointing at the new name and refreshes locations rather + // than orphaning them. + const oldAlias = device.channelMappings.find((m) => m.channelId === channelId)?.alias ?? '' + if (oldAlias) { + useOpenPLCStore.getState().projectActions.renameAlias(oldAlias, alias) + } + const updated = device.channelMappings.map((m) => (m.channelId === channelId ? { ...m, alias } : m)) onUpdateChannelMappingsRef.current(updated) }, - [device?.channelMappings], + [device?.channelMappings, device?.name], ) const updateConfig = useCallback( diff --git a/src/frontend/store/__tests__/project-validation-variables.test.ts b/src/frontend/store/__tests__/project-validation-variables.test.ts index dbc243a6a..090f9d3a9 100644 --- a/src/frontend/store/__tests__/project-validation-variables.test.ts +++ b/src/frontend/store/__tests__/project-validation-variables.test.ts @@ -340,6 +340,54 @@ describe('createVariableValidation', () => { expect(result.location).toBe('%MD0') }) + // -- Multi-collision walk (regression for forum bug: contiguous "+" clicks + // across a row with a variable already further down) -- + it('walks past intervening claimed locations until it finds a free slot (BOOL)', () => { + // Existing rows occupy %IX0.0..%IX0.4 and %IX0.5 — user "+"-clicks the row + // at %IX0.4. Single-step increment would land on %IX0.5 and collide; the + // validator must walk to %IX0.6. + const existing = [ + makeVariable('I1', 'BOOL', '%IX0.0'), + makeVariable('I1_0', 'BOOL', '%IX0.1'), + makeVariable('I1_1', 'BOOL', '%IX0.2'), + makeVariable('I1_2', 'BOOL', '%IX0.3'), + makeVariable('I1_3', 'BOOL', '%IX0.4'), + makeVariable('I2', 'BOOL', '%IX0.5'), + ] + const variable = makeVariable('NewVar', 'BOOL', '%IX0.4') + const result = createVariableValidation(existing, variable) + expect(result.location).toBe('%IX0.6') + }) + + it('walks past multiple consecutive claimed locations (WORD)', () => { + const existing = [ + makeVariable('A', 'INT', '%QW5'), + makeVariable('B', 'INT', '%QW6'), + makeVariable('C', 'INT', '%QW7'), + ] + const variable = makeVariable('NewVar', 'INT', '%QW5') + const result = createVariableValidation(existing, variable) + expect(result.location).toBe('%QW8') + }) + + it('wraps past a full BOOL byte when every bit and the next byte are taken', () => { + // %QX0.0..%QX0.7 claimed plus %QX1.0 — must land at %QX1.1. + const existing = [ + makeVariable('B0', 'BOOL', '%QX0.0'), + makeVariable('B1', 'BOOL', '%QX0.1'), + makeVariable('B2', 'BOOL', '%QX0.2'), + makeVariable('B3', 'BOOL', '%QX0.3'), + makeVariable('B4', 'BOOL', '%QX0.4'), + makeVariable('B5', 'BOOL', '%QX0.5'), + makeVariable('B6', 'BOOL', '%QX0.6'), + makeVariable('B7', 'BOOL', '%QX0.7'), + makeVariable('B8', 'BOOL', '%QX1.0'), + ] + const variable = makeVariable('NewVar', 'BOOL', '%QX0.0') + const result = createVariableValidation(existing, variable) + expect(result.location).toBe('%QX1.1') + }) + // -- Edge case: location exists but not found in variables (defensive) -- it('returns unchanged when location exists but variable not found in list', () => { // This covers the `if (!variableFound) return response` branch diff --git a/src/frontend/store/__tests__/sync-variable-aliases-action.test.ts b/src/frontend/store/__tests__/sync-variable-aliases-action.test.ts index e15f73014..bc9312229 100644 --- a/src/frontend/store/__tests__/sync-variable-aliases-action.test.ts +++ b/src/frontend/store/__tests__/sync-variable-aliases-action.test.ts @@ -163,7 +163,7 @@ describe('projectActions.syncVariableAliases (store integration)', () => { expect(vars[0].alias).toBe('conveyor_motor') }) - it('reports orphans when the producer no longer exposes the alias', () => { + it('reports orphans and clears their stale location when the producer no longer exposes the alias', () => { const store = makeStore() seedBoard(store, 'SLM-RP4', VPP_V4) seedVendorScreenData(store, withVppEntries([])) // no VPP entries at all @@ -172,10 +172,13 @@ describe('projectActions.syncVariableAliases (store integration)', () => { const report = store.getState().projectActions.syncVariableAliases() expect(report.orphaned).toBe(1) - // Variable kept as-is so the user can re-bind manually. + // Phase 3 contract: orphans keep the alias name (so the UI can + // render the warning glyph + tooltip) but their stale location + // is cleared so the ST / XML emitters don't bake an `AT %…` into + // the compile for an alias whose producer is no longer active. const vars = store.getState().project.data.pous[0].interface!.variables expect(vars[0].alias).toBe('conveyor_motor') - expect(vars[0].location).toBe('%QX0.0') + expect(vars[0].location).toBe('') }) it('honours target capabilities: switching to an arduino board orphans VPP-bound aliases', () => { diff --git a/src/frontend/store/slices/project/slice.ts b/src/frontend/store/slices/project/slice.ts index e69e97929..56ad3c76f 100644 --- a/src/frontend/store/slices/project/slice.ts +++ b/src/frontend/store/slices/project/slice.ts @@ -14,8 +14,10 @@ import type { import { buildAddressPool, buildAliasRegistry, + describeSource, nextFreeAddress, syncVariableAliases as syncVariablesPure, + validateAliasEdit, } from '../../../../middleware/shared/utils/iec-address' import { resolveTargetCapabilities } from '../../../../middleware/shared/utils/target-capabilities' import { parseIecStringToVariables } from '../../../utils/generate-iec-string-to-variables' @@ -169,6 +171,57 @@ function generateIOPoints( return points } +// --------------------------------------------------------------------------- +// Alias auto-adopt +// --------------------------------------------------------------------------- + +/** + * Resolve the canonical alias that a variable should carry for its + * current `location`. Builds a fresh pool + registry from the live + * store state, scoped to the active target's capabilities, then + * returns whatever alias the registry attaches to that address (or + * `undefined` when the address isn't aliased, or `location` is empty). + * + * Used by both `createVariable` and `updateVariable` to enforce the + * alias-↔-location invariant: a variable's `alias` field MUST point + * at the same producer-channel its `location` points at. Without + * this, the "+" button (which auto-increments the location of a + * spread-from-previous variable) leaves the OLD alias attached to a + * NEW address — `syncVariableAliases` then collapses every such + * variable back to the OLD alias's canonical address on the next + * refresh pass, producing the duplicate-address compile errors + * reported in v4.2.0. + * + * Returns `undefined` for an empty / missing location so callers can + * distinguish "no alias because the address is unmapped" from "no + * alias because we didn't bother to look". + */ +function resolveAliasForLocation(getState: ProjectGetState, location: string | undefined): string | undefined { + if (!location) return undefined + const live = getState() + const boardInfo = live.deviceAvailableOptions.availableBoards.get( + live.deviceDefinitions.configuration.deviceBoard ?? '', + ) + const ioMapping = + ( + live.deviceDefinitions.configuration.vendorScreenData?.['io-mapping'] as + | { entries?: Array<{ iecAddress: string; alias?: string; slot: number; channelName: string }> } + | undefined + )?.entries ?? [] + const pool = buildAddressPool( + { + pinMapping: { + pins: live.deviceDefinitions.pinMapping.pinsByBoard[live.deviceDefinitions.configuration.deviceBoard] ?? [], + }, + vendorIoMapping: { entries: ioMapping }, + remoteDevices: live.project.data.remoteDevices, + }, + resolveTargetCapabilities(boardInfo), + ) + const registry = buildAliasRegistry(pool) + return registry.byAddress.get(location)?.alias +} + // --------------------------------------------------------------------------- // Variables-text ⇄ variables-table reconcile helpers // --------------------------------------------------------------------------- @@ -494,6 +547,24 @@ const createProjectSlice: StateCreator = if (!reconcile.ok) return reconcile } + // Apply the validator's name + location auto-increment OUTSIDE + // produce so we can then re-resolve the alias against the live + // store state. The new variable's `alias` MUST point at the + // channel its post-increment `location` points at — otherwise + // the "+ button" UI flow (which spreads the previous variable + // as a template) carries a stale alias from the previous row + // forward, breaking the alias-↔-location invariant. The next + // `syncVariableAliases` refresh would then silently collapse + // the new variable back to the stale alias's canonical + // address, producing the duplicate-address compile errors + // reported in v4.2.0 (forum thread "openplc-420-teething-bugs"). + const sourceVariables = + scope === 'local' && associatedPou + ? (getState().project.data.pous.find((p) => p.name === associatedPou)?.interface?.variables ?? []) + : getState().project.data.configurations.resource.globalVariables + const validated = createVariableValidation(sourceVariables, data) + data = { ...data, ...validated, alias: resolveAliasForLocation(getState, validated.location) } + let response: ProjectResponse = { ok: true } setState( produce((slice: ProjectSlice) => { @@ -510,9 +581,6 @@ const createProjectSlice: StateCreator = variables = slice.project.data.configurations.resource.globalVariables } - // Validate and auto-increment name/location - data = { ...data, ...createVariableValidation(variables, data) } - // Insert or append if (rowToInsert !== undefined) { const filtered = scope === 'local' ? variables.filter((v) => v.name !== 'OUT') : variables @@ -555,40 +623,19 @@ const createProjectSlice: StateCreator = let response: ProjectResponse = { ok: true } - // Auto-adopt path: whenever the location changes, look up the - // alias registry and patch updates.alias to match the new - // address. If the address has an alias, the variable adopts it - // (cell shows the alias name, Phase 4 sync will keep the - // location current as the alias moves). If not, the alias + // Auto-adopt path: whenever the location changes, re-resolve + // the alias against the live registry so the variable's alias + // always points at the producer-channel its location points at. + // If the address has an alias, the variable adopts it (cell + // shows the alias name; `syncVariableAliases` will keep the + // location current as the alias moves). If not, the alias // clears — re-typing a now-orphaned location intentionally - // drops the stale alias label too. Done outside `produce` so + // drops the stale alias label too. Done outside `produce` so // we read the live store state including pinMapping + caps. - let aliasOverride: { alias: string | undefined } | undefined - if (typeof updates.location === 'string') { - const live = getState() - const boardInfo = live.deviceAvailableOptions.availableBoards.get( - live.deviceDefinitions.configuration.deviceBoard ?? '', - ) - const ioMapping = - ( - live.deviceDefinitions.configuration.vendorScreenData?.['io-mapping'] as - | { entries?: Array<{ iecAddress: string; alias?: string; slot: number; channelName: string }> } - | undefined - )?.entries ?? [] - const pool = buildAddressPool( - { - pinMapping: { - pins: - live.deviceDefinitions.pinMapping.pinsByBoard[live.deviceDefinitions.configuration.deviceBoard] ?? [], - }, - vendorIoMapping: { entries: ioMapping }, - remoteDevices: live.project.data.remoteDevices, - }, - resolveTargetCapabilities(boardInfo), - ) - const registry = buildAliasRegistry(pool) - aliasOverride = { alias: registry.byAddress.get(updates.location)?.alias } - } + const aliasOverride: { alias: string | undefined } | undefined = + typeof updates.location === 'string' + ? { alias: resolveAliasForLocation(getState, updates.location) } + : undefined setState( produce((slice: ProjectSlice) => { @@ -765,6 +812,23 @@ const createProjectSlice: StateCreator = message: `Address pool reports ${pool.conflicts.length} conflicting claim(s): ${sample}${overflow}. The first source wins; later ones lose their address binding.`, }) } + // Same migration warning, alias side: projects authored before + // the write-time `validateAliasEdit` gate landed may have + // duplicate alias names across producers. The registry + // first-wins on `byAlias`, but every variable bound to the + // losing entry gets quietly collapsed to the winner's address + // through the sync's refresh path. Surface this loudly so the + // user can resolve it (rename one of the duplicates) instead + // of silently inheriting a broken state. + if (registry.duplicateAliases.length > 0) { + const sample = registry.duplicateAliases.slice(0, 5).join(', ') + const overflow = registry.duplicateAliases.length > 5 ? ` (+${registry.duplicateAliases.length - 5} more)` : '' + live.consoleActions.addLog({ + id: crypto.randomUUID(), + level: 'warning', + message: `Alias registry reports ${registry.duplicateAliases.length} duplicate alias name(s): ${sample}${overflow}. Each alias must be unique across all I/O channels — rename the duplicates in the IO mapping screens. Until then, variables bound to the losing entries will resolve to the winning entry's address.`, + }) + } let adopted = 0 let refreshed = 0 @@ -803,6 +867,63 @@ const createProjectSlice: StateCreator = return { adopted, refreshed, orphaned } }, + /** + * Cascade-rename every variable's `.alias` from `oldAlias` to + * `newAlias`. See the type doc in `project/types.ts` for the + * full contract — short version: when the user renames the + * alias on a producer channel (pin mapping, VPP module, Modbus + * TCP, EtherCAT), the bound variables follow so they don't drop + * into the orphan path. Case-insensitive match. A subsequent + * `syncVariableAliases()` then refreshes the variables' + * `.location` against the now-renamed alias's address. + */ + renameAlias: (oldAlias, newAlias) => { + const trimmedOld = oldAlias?.trim() ?? '' + const trimmedNew = newAlias?.trim() ?? '' + // No-op when there's nothing to rename FROM. Caller is the IO + // mapping screen on first-time alias write where there's no + // prior text to cascade. + if (trimmedOld.length === 0) return { renamed: 0 } + // No-op when the rename is a pure case change or an actual + // no-op — saves a render pass and avoids spurious mutation. + if (trimmedOld.toLowerCase() === trimmedNew.toLowerCase()) return { renamed: 0 } + + let renamed = 0 + const cascade = (variable: PLCVariable): PLCVariable => { + if (!variable.alias) return variable + if (variable.alias.toLowerCase() !== trimmedOld.toLowerCase()) return variable + renamed += 1 + // When the user clears the alias on the producer side, the + // bound variables should also drop their alias — the next + // `syncVariableAliases()` will then re-evaluate them against + // the live registry (auto-adopt by raw location if the same + // address is still claimed by some other producer, or leave + // them alias-less otherwise). `undefined` rather than '' + // matches the rest of the codebase's "no alias" convention. + return { ...variable, alias: trimmedNew.length > 0 ? trimmedNew : undefined } + } + + setState( + produce((slice: ProjectSlice) => { + for (const pou of slice.project.data.pous) { + /* istanbul ignore if -- schema guarantees `interface.variables`; defensive */ + if (!pou.interface?.variables) continue + for (let i = 0; i < pou.interface.variables.length; i++) { + pou.interface.variables[i] = cascade(pou.interface.variables[i]) + } + } + const globals = slice.project.data.configurations.resource.globalVariables + if (globals) { + for (let i = 0; i < globals.length; i++) { + globals[i] = cascade(globals[i]) + } + } + }), + ) + + return { renamed } + }, + // ----------------------------------------------------------------------- // Data types // ----------------------------------------------------------------------- @@ -1421,6 +1542,58 @@ const createProjectSlice: StateCreator = return ok() }, updateIOPointAlias: (deviceName, groupId, pointId, alias) => { + // Phase 1 — write-time alias-uniqueness gate (global, across + // all producers). Build a fresh registry from the live state + // and reject the edit on collision. See + // `module-slots-layout.tsx::handleAliasChange` for the longer + // rationale. + const live = getState() + const sourceRef = { kind: 'modbus-tcp-remote' as const, ref: `${deviceName}:${pointId}` } + const boardInfo = live.deviceAvailableOptions?.availableBoards?.get( + live.deviceDefinitions?.configuration?.deviceBoard ?? '', + ) + const ioMapping = + ( + live.deviceDefinitions?.configuration?.vendorScreenData?.['io-mapping'] as + | { entries?: Array<{ iecAddress: string; alias?: string; slot: number; channelName: string }> } + | undefined + )?.entries ?? [] + const pool = buildAddressPool( + { + pinMapping: { + pins: + live.deviceDefinitions?.pinMapping?.pinsByBoard[ + live.deviceDefinitions?.configuration?.deviceBoard ?? '' + ] ?? [], + }, + vendorIoMapping: { entries: ioMapping }, + remoteDevices: live.project.data.remoteDevices, + }, + resolveTargetCapabilities(boardInfo), + ) + const registry = buildAliasRegistry(pool) + const validation = validateAliasEdit(registry, alias, sourceRef) + if (!validation.ok) { + return { + ok: false, + title: 'Alias already in use', + message: `"${alias}" is already assigned to ${describeSource(validation.conflict.source)} (${validation.conflict.address}). Alias names must be unique across all I/O channels.`, + } + } + + // Phase 2 — capture the old alias and cascade rename onto + // bound variables BEFORE writing the new alias so the + // downstream sync sees variables pointing at the new name and + // refreshes locations rather than orphaning them. + const oldAlias = + live.project.data.remoteDevices + ?.find((d) => d.name === deviceName) + ?.modbusTcpConfig?.ioGroups?.find((g) => g.id === groupId) + ?.ioPoints?.find((p) => p.id === pointId)?.alias ?? '' + if (oldAlias) { + getState().projectActions.renameAlias(oldAlias, alias) + } + setState( produce((slice: ProjectSlice) => { const device = slice.project.data.remoteDevices?.find((d) => d.name === deviceName) diff --git a/src/frontend/store/slices/project/types.ts b/src/frontend/store/slices/project/types.ts index 529e49c93..2746c6dc8 100644 --- a/src/frontend/store/slices/project/types.ts +++ b/src/frontend/store/slices/project/types.ts @@ -165,6 +165,29 @@ export type ProjectActions = { orphaned: number } + /** + * Cascade-rename every variable's `.alias` field from `oldAlias` to + * `newAlias` across all POU-local and global variables. Used by + * the IO-mapping screens (pin-mapping, VPP modules, VPP io-table, + * Modbus TCP remote, EtherCAT) when the user renames the alias on + * a producer channel — the rename cascades to bound variables so + * they don't become orphaned just because the alias text moved. + * + * Empty `oldAlias` (channel previously had no alias) is a no-op. + * Empty `newAlias` (user clearing the alias) causes the matching + * variables to drop their alias too; `syncVariableAliases` will + * then re-evaluate them against the live registry (auto-adopt by + * raw location when applicable, otherwise alias-less binding). + * + * Case-insensitive matching to align with the rest of the IEC + * identifier handling. Callers should follow this with a + * `syncVariableAliases()` to refresh `.location` against the now- + * renamed alias's address. + * + * Returns the number of variables actually mutated. + */ + renameAlias: (oldAlias: string, newAlias: string) => { renamed: number } + // Data types createDatatype: (dto: DataTypeDTO & { rowToInsert?: number }) => ProjectResponse deleteDatatype: (name: string) => void diff --git a/src/frontend/store/slices/project/validation/variables.ts b/src/frontend/store/slices/project/validation/variables.ts index a5de3db90..2f0524894 100644 --- a/src/frontend/store/slices/project/validation/variables.ts +++ b/src/frontend/store/slices/project/validation/variables.ts @@ -212,6 +212,92 @@ const checkVariableName = (variables: PLCVariable[], variableName: string) => { * This is a validation to check if it is needed changing the name of a variable at creation. * If the variable exists change the variable name. **/ +/** + * Increment an IEC 61131-3 address by one slot, respecting the + * width of the variable's underlying type. For BOOL addresses + * (`%IX/%QX.`) the bit field wraps from .7 back to .0 + * with the byte index bumping by one; for word / dword / lword + * forms the numeric index after the prefix increments by one. + * + * Returns `null` when the type isn't recognised — the caller stops + * the auto-increment loop and falls back to whatever location it + * currently holds, so an unknown future IEC type can't produce an + * infinite loop here. + */ +const incrementLocationByOne = (location: string, typeValue: string): string | null => { + switch (typeValue.toUpperCase()) { + case 'BOOL': { + const stringWithNoPrefix = location + .replace(PLC_ADDRESS_PREFIX.BOOL_OUTPUT, '') + .replace(PLC_ADDRESS_PREFIX.BOOL_INPUT, '') + const position = parseInt(stringWithNoPrefix.split('.')[0]) + const dotPosition = parseInt(stringWithNoPrefix.split('.')[1]) + const prefix = location.startsWith(PLC_ADDRESS_PREFIX.BOOL_OUTPUT) + ? PLC_ADDRESS_PREFIX.BOOL_OUTPUT + : PLC_ADDRESS_PREFIX.BOOL_INPUT + return `${prefix}${dotPosition === 7 ? position + 1 : position}.${dotPosition === 7 ? 0 : dotPosition + 1}` + } + case 'INT': + case 'UINT': + case 'WORD': { + const stringWithNoPrefix = location + .replace(PLC_ADDRESS_PREFIX.WORD_OUTPUT, '') + .replace(PLC_ADDRESS_PREFIX.WORD_INPUT, '') + .replace(PLC_ADDRESS_PREFIX.WORD_MEMORY, '') + const position = parseInt(stringWithNoPrefix) + const prefix = location.startsWith(PLC_ADDRESS_PREFIX.WORD_OUTPUT) + ? PLC_ADDRESS_PREFIX.WORD_OUTPUT + : location.startsWith(PLC_ADDRESS_PREFIX.WORD_INPUT) + ? PLC_ADDRESS_PREFIX.WORD_INPUT + : PLC_ADDRESS_PREFIX.WORD_MEMORY + return `${prefix}${position + 1}` + } + case 'DINT': + case 'UDINT': + case 'REAL': + case 'DWORD': { + const stringWithNoPrefix = location + .replace(PLC_ADDRESS_PREFIX.DWORD_OUTPUT, '') + .replace(PLC_ADDRESS_PREFIX.DWORD_INPUT, '') + .replace(PLC_ADDRESS_PREFIX.DWORD_MEMORY, '') + const position = parseInt(stringWithNoPrefix) + const prefix = location.startsWith(PLC_ADDRESS_PREFIX.DWORD_OUTPUT) + ? PLC_ADDRESS_PREFIX.DWORD_OUTPUT + : location.startsWith(PLC_ADDRESS_PREFIX.DWORD_INPUT) + ? PLC_ADDRESS_PREFIX.DWORD_INPUT + : PLC_ADDRESS_PREFIX.DWORD_MEMORY + return `${prefix}${position + 1}` + } + case 'LINT': + case 'ULINT': + case 'LREAL': + case 'LWORD': { + const stringWithNoPrefix = location + .replace(PLC_ADDRESS_PREFIX.LWORD_OUTPUT, '') + .replace(PLC_ADDRESS_PREFIX.LWORD_INPUT, '') + .replace(PLC_ADDRESS_PREFIX.LWORD_MEMORY, '') + const position = parseInt(stringWithNoPrefix) + const prefix = location.startsWith(PLC_ADDRESS_PREFIX.LWORD_OUTPUT) + ? PLC_ADDRESS_PREFIX.LWORD_OUTPUT + : location.startsWith(PLC_ADDRESS_PREFIX.LWORD_INPUT) + ? PLC_ADDRESS_PREFIX.LWORD_INPUT + : PLC_ADDRESS_PREFIX.LWORD_MEMORY + return `${prefix}${position + 1}` + } + default: + return null + } +} + +/** Safety bound on the auto-increment loop in `createVariableValidation`. + * Picked well above any realistic project size (8 bits × N bytes = + * N×8 BOOLs; this lets us scan ~1000 bytes / words / dwords / lwords + * before we give up). The loop normally terminates after at most + * a handful of iterations — the bound only matters if the table is + * pathologically dense or an unknown IEC type slipped past + * `incrementLocationByOne`'s switch. */ +const MAX_AUTO_INCREMENT_ITERATIONS = 8192 + const createVariableValidation = ( variables: PLCVariable[], variable: PLCVariable, @@ -224,84 +310,25 @@ const createVariableValidation = ( response.name = `${variableNameWithoutNumber}${number}` } - if (checkIfLocationExists(variables, variableLocation)) { - if (variableLocation === '') return response - - const variableFound = variables.find((variable) => variable.location === variableLocation) - /* istanbul ignore next -- defensive: find() uses same predicate as checkIfLocationExists above */ - if (!variableFound) return response - - switch (variable.type.value.toUpperCase()) { - case 'BOOL': { - const stringWithNoPrefix = variableFound.location - .replace(PLC_ADDRESS_PREFIX.BOOL_OUTPUT, '') - .replace(PLC_ADDRESS_PREFIX.BOOL_INPUT, '') - const position = parseInt(stringWithNoPrefix.split('.')[0]) - const dotPosition = parseInt(stringWithNoPrefix.split('.')[1]) - - const prefix = variableFound?.location.startsWith(PLC_ADDRESS_PREFIX.BOOL_OUTPUT) - ? PLC_ADDRESS_PREFIX.BOOL_OUTPUT - : PLC_ADDRESS_PREFIX.BOOL_INPUT - response.location = `${prefix}${dotPosition === 7 ? position + 1 : position}.${dotPosition === 7 ? 0 : dotPosition + 1}` - break - } - - case 'INT': - case 'UINT': - case 'WORD': { - const stringWithNoPrefix = variableFound.location - .replace(PLC_ADDRESS_PREFIX.WORD_OUTPUT, '') - .replace(PLC_ADDRESS_PREFIX.WORD_INPUT, '') - .replace(PLC_ADDRESS_PREFIX.WORD_MEMORY, '') - const position = parseInt(stringWithNoPrefix) - const prefix = variableFound?.location.startsWith(PLC_ADDRESS_PREFIX.WORD_OUTPUT) - ? PLC_ADDRESS_PREFIX.WORD_OUTPUT - : variableFound?.location.startsWith(PLC_ADDRESS_PREFIX.WORD_INPUT) - ? PLC_ADDRESS_PREFIX.WORD_INPUT - : PLC_ADDRESS_PREFIX.WORD_MEMORY - response.location = `${prefix}${position + 1}` - break - } - - case 'DINT': - case 'UDINT': - case 'REAL': - case 'DWORD': { - const stringWithNoPrefix = variableFound.location - .replace(PLC_ADDRESS_PREFIX.DWORD_OUTPUT, '') - .replace(PLC_ADDRESS_PREFIX.DWORD_INPUT, '') - .replace(PLC_ADDRESS_PREFIX.DWORD_MEMORY, '') - const position = parseInt(stringWithNoPrefix) - const prefix = variableFound?.location.startsWith(PLC_ADDRESS_PREFIX.DWORD_OUTPUT) - ? PLC_ADDRESS_PREFIX.DWORD_OUTPUT - : variableFound?.location.startsWith(PLC_ADDRESS_PREFIX.DWORD_INPUT) - ? PLC_ADDRESS_PREFIX.DWORD_INPUT - : PLC_ADDRESS_PREFIX.DWORD_MEMORY - response.location = `${prefix}${position + 1}` - break - } - - case 'LINT': - case 'ULINT': - case 'LREAL': - case 'LWORD': { - const stringWithNoPrefix = variableFound.location - .replace(PLC_ADDRESS_PREFIX.LWORD_OUTPUT, '') - .replace(PLC_ADDRESS_PREFIX.LWORD_INPUT, '') - .replace(PLC_ADDRESS_PREFIX.LWORD_MEMORY, '') - const position = parseInt(stringWithNoPrefix) - const prefix = variableFound?.location.startsWith(PLC_ADDRESS_PREFIX.LWORD_OUTPUT) - ? PLC_ADDRESS_PREFIX.LWORD_OUTPUT - : variableFound?.location.startsWith(PLC_ADDRESS_PREFIX.LWORD_INPUT) - ? PLC_ADDRESS_PREFIX.LWORD_INPUT - : PLC_ADDRESS_PREFIX.LWORD_MEMORY - response.location = `${prefix}${position + 1}` - break - } - - default: - break + if (checkIfLocationExists(variables, variableLocation) && variableLocation !== '') { + // Scan forward through the address space until we find a slot + // that no other variable in this table holds. Single-increment + // wasn't enough: when the user kept clicking "+" through a row + // of contiguous variables, the increment would eventually land + // ON another already-bound row and silently produce a duplicate- + // location collision that only the compiler caught (forum + // thread, v4.2.0 follow-up). An `inUse` set keeps the inner + // check O(1) so the loop is linear in the number of variables. + const inUse = new Set(variables.map((v) => v.location)) + let candidate = variableLocation + let iterations = 0 + while (inUse.has(candidate) && iterations < MAX_AUTO_INCREMENT_ITERATIONS) { + const next = incrementLocationByOne(candidate, variable.type.value) + if (!next || next === candidate) break // unknown type / no progress — bail + candidate = next + iterations += 1 } + response.location = candidate } return response } diff --git a/src/frontend/utils/__tests__/remote-device-options.test.ts b/src/frontend/utils/__tests__/remote-device-options.test.ts index 414f87c23..ec5443057 100644 --- a/src/frontend/utils/__tests__/remote-device-options.test.ts +++ b/src/frontend/utils/__tests__/remote-device-options.test.ts @@ -42,10 +42,14 @@ describe('buildRemoteDeviceOptionGroups', () => { }) it('groups points by device name', () => { + // Each address must be distinct — the production address pool + // enforces one-claim-per-address, and the builder dedupes + // defensively against legacy projects that drifted before that + // enforcement landed. const points = [ - makeIOPoint({ deviceName: 'DevA', ioPointId: 'a1', alias: 'A1' }), - makeIOPoint({ deviceName: 'DevB', ioPointId: 'b1', alias: 'B1' }), - makeIOPoint({ deviceName: 'DevA', ioPointId: 'a2', alias: 'A2' }), + makeIOPoint({ deviceName: 'DevA', ioPointId: 'a1', iecLocation: '%IX0.0', alias: 'A1' }), + makeIOPoint({ deviceName: 'DevB', ioPointId: 'b1', iecLocation: '%IX0.1', alias: 'B1' }), + makeIOPoint({ deviceName: 'DevA', ioPointId: 'a2', iecLocation: '%IX0.2', alias: 'A2' }), ] const result = buildRemoteDeviceOptionGroups('c', points) expect(result).toHaveLength(2) @@ -57,15 +61,32 @@ describe('buildRemoteDeviceOptionGroups', () => { it('mixes aliased and non-aliased points, keeping only aliased', () => { const points = [ - makeIOPoint({ ioPointId: 'p1', alias: 'Yes' }), - makeIOPoint({ ioPointId: 'p2', alias: undefined }), - makeIOPoint({ ioPointId: 'p3', alias: 'Also' }), + makeIOPoint({ ioPointId: 'p1', iecLocation: '%IX0.0', alias: 'Yes' }), + makeIOPoint({ ioPointId: 'p2', iecLocation: '%IX0.1', alias: undefined }), + makeIOPoint({ ioPointId: 'p3', iecLocation: '%IX0.2', alias: 'Also' }), ] const result = buildRemoteDeviceOptionGroups('x', points) expect(result).toHaveLength(1) expect(result[0].options).toHaveLength(2) }) + it('dedupes by IEC address (defensive against legacy projects with duplicate-address entries)', () => { + // Production never produces this state (the address pool enforces + // uniqueness at write time), but legacy projects may have drifted + // into it before the gate existed. First-iterated wins, matching + // the pool's reservation order. + const points = [ + makeIOPoint({ ioPointId: 'first', iecLocation: '%IX0.0', alias: 'A_first' }), + makeIOPoint({ ioPointId: 'second', iecLocation: '%IX0.0', alias: 'A_second' }), + makeIOPoint({ ioPointId: 'third', iecLocation: '%IX0.1', alias: 'B_unique' }), + ] + const result = buildRemoteDeviceOptionGroups('x', points) + expect(result).toHaveLength(1) + expect(result[0].options).toHaveLength(2) + expect(result[0].options[0].label).toBe('%IX0.0 (A_first)') + expect(result[0].options[1].label).toBe('%IX0.1 (B_unique)') + }) + it('uses cellId in option IDs', () => { const points = [makeIOPoint({ ioPointId: 'pt-7' })] const result = buildRemoteDeviceOptionGroups('my-cell', points) diff --git a/src/frontend/utils/location-dropdown-options.ts b/src/frontend/utils/location-dropdown-options.ts index 48a2a317b..2804104c0 100644 --- a/src/frontend/utils/location-dropdown-options.ts +++ b/src/frontend/utils/location-dropdown-options.ts @@ -11,6 +11,29 @@ * the dropdown would offer addresses from producers the active target * has deactivated — e.g. an SLM-RP4 slot 1 entry still showing while * the user is targeting an Arduino Mega. + * + * Pin-mapping vs. VPP/remote-device dropdown asymmetry — by design: + * - Pin-mapping options below (`pinGroup`) list **every pin**, + * aliased or not. Pin addresses on Arduino-style targets are + * stable hardware facts (pin 13 = `%QX0.3` on Arduino Uno, + * always), so addressing by raw IEC location stays meaningful + * even without a user-supplied alias. An empty `(alias)` suffix + * just means the user hasn't given the pin a friendly name yet. + * - VPP/module + remote-device options (`buildVendorIoOptionGroups` / + * `buildRemoteDeviceOptionGroups` in `remote-device-options.ts`) + * list only **aliased entries**. Their IEC addresses are + * allocator-assigned and can shift when the user changes slot + * layout, adds modules, etc. Variables that bind to an alias + * survive those shifts (`syncVariableAliases` refreshes their + * `location` via the registry); variables that bound to a raw + * address would silently break. Requiring an alias makes the + * rebind-on-shift contract explicit. + * + * Do not "fix" the inconsistency by filtering the pin-mapping branch + * to aliased pins only — it would force users to write an alias + * before they can bind any variable to a pin, regressing the + * Arduino-style "bind by raw `%IX0.0`" workflow that pre-dates the + * alias machinery. */ import type { DevicePin, IoMappingEntry } from '../../middleware/shared/ports/types' diff --git a/src/frontend/utils/remote-device-options.ts b/src/frontend/utils/remote-device-options.ts index db29dcd9e..b6cc91cdc 100644 --- a/src/frontend/utils/remote-device-options.ts +++ b/src/frontend/utils/remote-device-options.ts @@ -1,6 +1,30 @@ /** * Utility functions for building remote device and vendor module IO point options. * Used in location dropdowns for variable tables. + * + * Dropdown asymmetry — by design: + * - Pin-mapping options (see `location-dropdown-options.ts`) list + * **every pin**, aliased or not. Pin addresses on Arduino-style + * targets are stable (pin 13 = `%QX0.3` on Arduino Uno, always), + * so addressing by raw IEC location stays meaningful even without + * a user-supplied alias. + * - VPP module + remote-device options (this file) list **only + * aliased entries**. Their IEC addresses are allocator-assigned + * and can shift when the user changes slot layout, adds modules, + * etc. Variables that bind to an alias survive those shifts + * (the sync engine refreshes their `location` via the registry); + * variables that bound to a raw address would silently break. + * Requiring an alias makes the rebind-on-shift contract explicit. + * + * Dedupe contract: each address can appear at most once across all + * options this file produces. The address pool enforces one-claim- + * per-address at registry-build time, so authoring through the live + * UI never produces duplicates. Legacy projects authored before the + * write-time `validateAliasEdit` check existed may have drifted into + * a state where two entries claim the same `iecAddress` — for those, + * we skip later occurrences (first-iterated wins, matching the + * pool's first-wins reservation order) so the picker can never offer + * an ambiguous pick. */ import type { IoMappingEntry } from '../../middleware/shared/ports/types' @@ -36,9 +60,12 @@ type DropdownGroup = { */ export function buildRemoteDeviceOptionGroups(cellId: string, remoteIOPoints: RemoteDeviceIOPoint[]): DropdownGroup[] { const groupsByDevice = new Map() + const seenAddresses = new Set() // see file-level "Dedupe contract" for (const ioPoint of remoteIOPoints) { if (!ioPoint.alias) continue // Only show points with aliases + if (seenAddresses.has(ioPoint.iecLocation)) continue + seenAddresses.add(ioPoint.iecLocation) let deviceGroup = groupsByDevice.get(ioPoint.deviceName) if (!deviceGroup) { @@ -69,9 +96,12 @@ export function buildRemoteDeviceOptionGroups(cellId: string, remoteIOPoints: Re */ export function buildVendorIoOptionGroups(cellId: string, entries: IoMappingEntry[]): DropdownGroup[] { const groupsBySlot = new Map() + const seenAddresses = new Set() // see file-level "Dedupe contract" for (const entry of entries) { if (!entry.alias) continue + if (seenAddresses.has(entry.iecAddress)) continue + seenAddresses.add(entry.iecAddress) const groupKey = `Slot ${entry.slot}: ${entry.moduleName}` let group = groupsBySlot.get(groupKey) diff --git a/src/globals.d.ts b/src/globals.d.ts index ffc37cf1d..7c4e31712 100644 --- a/src/globals.d.ts +++ b/src/globals.d.ts @@ -3,10 +3,11 @@ */ /** - * Application version from package.json - * @example "4.1.1" + * Per-app product name (e.g. "OpenPLC Editor"), injected per build. The + * shared About modal reads this; the app version itself is now imported from + * src/frontend/data/constants/app-version.ts, not injected as a global. */ -declare const APP_VERSION: string +declare const APP_NAME: string /** * Build date in YYYY-MM-DD format diff --git a/src/middleware/shared/ports/compiler-platform-port.ts b/src/middleware/shared/ports/compiler-platform-port.ts index 05f2aa579..cb032097c 100644 --- a/src/middleware/shared/ports/compiler-platform-port.ts +++ b/src/middleware/shared/ports/compiler-platform-port.ts @@ -35,7 +35,7 @@ * server-side, but the pipeline never knows. */ -import type { StructuredCompileError } from './types' +import type { PLCProjectData, StructuredCompileError } from './types' /** * Canonical progress callback the pipeline passes to every port @@ -99,24 +99,30 @@ export type PlatformDeviceContext = * `/generate-st` endpoint, logging a warning for anything it * doesn't recognise (defence in depth — service has its own * allowlist too). */ -export interface TranspileXmlToStArgs { - xml: string - /** Extra CLI tokens to append after `--generate-st ` in the - * xml2st invocation. For strucpp targets the shared pipeline - * sets `['--keep-structs']`; future flags get added here at the - * single call site in `runCompilePipeline`. */ - xml2stArgs: readonly string[] +/** + * Input to the in-process JSON → ST transpiler. Each adapter + * projects this into the transpiler's `TranspileProject` IR with + * its own helper — editor: `fromSchemaShape` (IPC schema-shape); + * web: `fromPortShape` after a port→schema conversion at the + * adapter boundary. Replaces the legacy XML-fed surface that + * routed through a bundled `xml2st` binary; transpilation now + * runs in-process against the JSON IR. + * + * The declared type is port-shape because that's the renderer + * store's shape; pipeline callers that hold schema-shape data + * cast through `never` at the call site (see `pipeline.ts` Step 1 + * and `library-build-orchestrator.ts` Stage 2). */ +export interface TranspileToStArgs { + projectData: PLCProjectData } -export interface TranspileXmlToStResult { +export interface TranspileToStResult { ok: boolean - /** ST source emitted by xml2st when the transpile succeeded. - * Empty / undefined on failure. */ + /** ST source emitted by the JSON transpiler when transpilation + * succeeded. Empty / undefined on failure. */ programSt?: string - /** Structured diagnostics emitted by xml2st. Web maps the - * server's `output_stderr` parser output here; editor parses - * the binary's stderr stream. Carried over to the pipeline's - * caller via the `errors[]` return on `CompileResult`. */ + /** Structured diagnostics from the transpiler. Forwarded to the + * pipeline caller via `errors[]` on `CompileResult`. */ errors?: StructuredCompileError[] /** Same shape as `errors[]` but for non-fatal warnings. */ warnings?: StructuredCompileError[] @@ -304,8 +310,8 @@ export interface CompilerPlatformPort { * hash-impl dependency. */ computeMd5(input: string): Promise - /** Step 3 of the editor pipeline. Transpile IEC XML to ST. */ - transpileXmlToSt(args: TranspileXmlToStArgs, log: PlatformLog): Promise + /** Step 3 of the editor pipeline. Transpile project IR to ST. */ + transpileToSt(args: TranspileToStArgs, log: PlatformLog): Promise /** Step 5 of the editor pipeline. Arduino-CLI core install. * Web returns `{ ok: true }` immediately. */ diff --git a/src/middleware/shared/ports/library-build-port.ts b/src/middleware/shared/ports/library-build-port.ts index 7a9b06bfc..3134a05e5 100644 --- a/src/middleware/shared/ports/library-build-port.ts +++ b/src/middleware/shared/ports/library-build-port.ts @@ -28,7 +28,7 @@ * the exact pattern. */ -import type { TranspileXmlToStArgs, TranspileXmlToStResult } from './compiler-platform-port' +import type { TranspileToStArgs, TranspileToStResult } from './compiler-platform-port' /** * Outcome of an attempted verification compile against the OpenPLC @@ -99,16 +99,16 @@ export interface LibraryBuildPort { computeMd5(input: string): Promise /** - * Transpile IEC 61131-3 XML to ST. Editor spawns the bundled - * `xml2st` binary; web posts to its compile-service. Both honor - * the same `xml2stArgs` and surface the same diagnostic shape. - * The `log` callback is the orchestrator's emit channel — every - * line xml2st produces flows through here. + * Transpile the project IR directly to ST via the in-process + * JSON transpiler (`st-transpiler`). Both editor and + * web adapters project their port-shape input into the + * transpiler's minimal `TranspileProject` IR. The `log` + * callback is the orchestrator's emit channel. */ - transpileXmlToSt( - args: TranspileXmlToStArgs, + transpileToSt( + args: TranspileToStArgs, log: (message: string, level: 'info' | 'warning' | 'error') => void, - ): Promise + ): Promise // ------------------------------------------------------------------------- // Generic file IO over the project tree diff --git a/src/middleware/shared/utils/iec-address/__tests__/alias-registry.test.ts b/src/middleware/shared/utils/iec-address/__tests__/alias-registry.test.ts index 91ae92f31..ef8c0aa07 100644 --- a/src/middleware/shared/utils/iec-address/__tests__/alias-registry.test.ts +++ b/src/middleware/shared/utils/iec-address/__tests__/alias-registry.test.ts @@ -1,6 +1,12 @@ import { ARDUINO_CLI_CAPABILITIES, RUNTIME_V4_CAPABILITIES } from '../../target-capabilities' import { buildAddressPool } from '../address-pool' -import { aliasForAddress, buildAliasRegistry, isAliasNameAvailable, resolveAlias } from '../alias-registry' +import { + aliasForAddress, + buildAliasRegistry, + isAliasNameAvailable, + resolveAlias, + validateAliasEdit, +} from '../alias-registry' const v4 = RUNTIME_V4_CAPABILITIES const arduino = ARDUINO_CLI_CAPABILITIES @@ -192,3 +198,44 @@ describe('isAliasNameAvailable', () => { expect(isAliasNameAvailable(reg, 'tank', { kind: 'modbus-tcp-remote', ref: 'd:p' })).toBe(false) }) }) + +describe('validateAliasEdit', () => { + const pool = buildAddressPool( + { + vendorIoMapping: { + entries: [{ iecAddress: '%IW0', alias: 'tank', slot: 1, channelName: 'AI1' }], + }, + }, + v4WithVpp, + ) + const reg = buildAliasRegistry(pool) + + it('accepts an empty alias (user clearing the field)', () => { + expect(validateAliasEdit(reg, '', { kind: 'vpp-io', ref: 'slot-2:AI1' })).toEqual({ ok: true }) + expect(validateAliasEdit(reg, ' ', { kind: 'vpp-io', ref: 'slot-2:AI1' })).toEqual({ ok: true }) + expect(validateAliasEdit(reg, undefined, { kind: 'vpp-io', ref: 'slot-2:AI1' })).toEqual({ ok: true }) + }) + + it('accepts a brand-new alias name', () => { + expect(validateAliasEdit(reg, 'pressure', { kind: 'vpp-io', ref: 'slot-2:AI1' })).toEqual({ ok: true }) + }) + + it('accepts a no-op rename (same alias, same source)', () => { + expect(validateAliasEdit(reg, 'tank', { kind: 'vpp-io', ref: 'slot-1:AI1' })).toEqual({ ok: true }) + }) + + it('rejects a collision with another channel and returns the conflicting entry', () => { + const result = validateAliasEdit(reg, 'tank', { kind: 'vpp-io', ref: 'slot-2:AI1' }) + expect(result.ok).toBe(false) + if (!result.ok) { + expect(result.conflict.alias).toBe('tank') + expect(result.conflict.address).toBe('%IW0') + expect(result.conflict.source).toEqual({ kind: 'vpp-io', ref: 'slot-1:AI1' }) + } + }) + + it('rejects a collision across producers (pin-mapping vs VPP)', () => { + const result = validateAliasEdit(reg, 'tank', { kind: 'pin-mapping', ref: '%QX0.0' }) + expect(result.ok).toBe(false) + }) +}) diff --git a/src/middleware/shared/utils/iec-address/__tests__/sync-variable-aliases.test.ts b/src/middleware/shared/utils/iec-address/__tests__/sync-variable-aliases.test.ts index 8fe21b5b4..0ae264812 100644 --- a/src/middleware/shared/utils/iec-address/__tests__/sync-variable-aliases.test.ts +++ b/src/middleware/shared/utils/iec-address/__tests__/sync-variable-aliases.test.ts @@ -61,12 +61,16 @@ describe('syncVariableAliases', () => { expect(result.report.refreshed).toEqual([]) }) - it('reports orphaned variables when the alias no longer exists in the registry', () => { + it('orphans clear the stale location but keep the alias for the UI warning glyph', () => { const registry = buildRegistryFromVpp([]) const vars = [VAR({ name: 'ghost', location: '%QX2.0', alias: 'removed_module' })] const result = syncVariableAliases(vars, registry) - // Variable kept as-is so the user can re-bind manually. - expect(result.variables[0].location).toBe('%QX2.0') + // `location` is cleared so the ST / XML emitters don't bake an + // `AT %QX2.0` for an alias whose producer is no longer active. + // `alias` is kept so the variable cell renders the orphan glyph + // and tooltip; the previous address is preserved in the report's + // `lastKnownAddress` field for the tooltip + undo affordance. + expect(result.variables[0].location).toBe('') expect(result.variables[0].alias).toBe('removed_module') expect(result.report.orphaned).toEqual([{ varName: 'ghost', alias: 'removed_module', lastKnownAddress: '%QX2.0' }]) expect(result.report.adopted).toEqual([]) @@ -87,7 +91,10 @@ describe('syncVariableAliases', () => { const result = syncVariableAliases(vars, registry) expect(result.variables[0]).toMatchObject({ name: 'adopted', alias: 'pressure', location: '%IW1' }) expect(result.variables[1]).toMatchObject({ name: 'refreshed', alias: 'valve_open', location: '%QX3.2' }) - expect(result.variables[2]).toMatchObject({ name: 'orphaned', alias: 'gone', location: '%QW9' }) + // Orphaned variables retain their alias (for the UI warning) but + // get their location cleared — see the "orphans clear the stale + // location" case above for the standalone version of this rule. + expect(result.variables[2]).toMatchObject({ name: 'orphaned', alias: 'gone', location: '' }) expect(result.variables[3]).toMatchObject({ name: 'untouched', location: '%MW100' }) expect(result.report.adopted).toHaveLength(1) diff --git a/src/middleware/shared/utils/iec-address/alias-registry.ts b/src/middleware/shared/utils/iec-address/alias-registry.ts index f1ce66a4c..65d8716b1 100644 --- a/src/middleware/shared/utils/iec-address/alias-registry.ts +++ b/src/middleware/shared/utils/iec-address/alias-registry.ts @@ -90,3 +90,99 @@ export function isAliasNameAvailable(registry: AliasRegistry, alias: string, ign if (!ignoring) return false return entry.source.kind === ignoring.kind && entry.source.ref === ignoring.ref } + +/** Outcome of `validateAliasEdit`. When `ok` is false, `conflict` + * carries the in-registry entry that already owns the alias, so + * callers can render a precise error message (e.g. "alias 'relay_1' + * is already used by slot 2 channel O3"). + */ +export type AliasEditValidation = { ok: true } | { ok: false; conflict: AliasEntry } + +/** + * Human-readable description of a `SourceRef`, for use inside error + * toasts / inline validation messages. Centralised here so every + * producer's alias-edit screen renders the same wording when the + * uniqueness check rejects an edit. + * + * Format examples: + * - `pin-mapping` ref="%QX0.0" → "pin mapping (%QX0.0)" + * - `vpp-io` ref="slot-2:O3" → "VPP slot 2 channel O3" + * - `modbus-tcp-remote` ref="d1:point17" → "Modbus device d1 point point17" + * - `ethercat` ref="bus0:slave2:ch5" → "EtherCAT bus0 slave2 channel ch5" + * + * Unknown formats fall back to a verbatim `${kind} ${ref}` rendering + * so future producer kinds at least show something useful before this + * helper is updated. + */ +export function describeSource(source: SourceRef): string { + switch (source.kind) { + case 'pin-mapping': + return `pin mapping (${source.ref})` + case 'vpp-io': { + // ref shape "slot-:" — see address-pool.ts:218. + const match = /^slot-(\d+):(.+)$/.exec(source.ref) + if (match) return `VPP slot ${match[1]} channel ${match[2]}` + return `VPP I/O (${source.ref})` + } + case 'modbus-tcp-remote': { + // ref shape ":" — see address-pool.ts:228. + const idx = source.ref.indexOf(':') + if (idx > 0) { + const device = source.ref.slice(0, idx) + const point = source.ref.slice(idx + 1) + return `Modbus device ${device} point ${point}` + } + return `Modbus TCP (${source.ref})` + } + case 'ethercat': { + // ref shape "::" — see address-pool.ts:243. + const parts = source.ref.split(':') + if (parts.length === 3) return `EtherCAT ${parts[0]} slave ${parts[1]} channel ${parts[2]}` + return `EtherCAT (${source.ref})` + } + default: + // Exhaustive-fallback. TypeScript narrows `source` to `never` + // here when every `SourceKind` above is covered, so this branch + // only executes when a future `SourceKind` lands without + // updating this helper. We stringify defensively rather than + // dereferencing `source.kind` / `source.ref` (which TS would + // reject on the narrowed `never`). + return JSON.stringify(source) + } +} + +/** + * Validate an alias-edit at write time. Wraps `isAliasNameAvailable` + * and returns the conflicting entry so the calling UI can produce a + * helpful toast / inline error instead of a generic "already in use". + * + * Semantics: + * - Empty / whitespace-only `alias` is always OK (user clearing the + * alias is the normal way to detach a channel from its variables). + * - When the alias is already claimed by **the same channel** that's + * being edited (`ignoring` matches the in-registry source), the + * edit is OK — this lets the UI commit no-op writes without + * spuriously failing. + * - When the alias is already claimed by **a different channel**, + * the edit is rejected and `conflict` carries the surviving entry. + * + * Every IO-mapping screen / pin-mapping table / remote-device editor + * MUST call this before persisting a new alias, otherwise the registry + * silently first-wins on duplicates and the variables bound to the + * losing entry are subsequently collapsed by `syncVariableAliases` to + * the winner's address. See the architectural notes at the top of + * `address-pool.ts` for the full reservation chain. + */ +export function validateAliasEdit( + registry: AliasRegistry, + alias: string | undefined, + ignoring: SourceRef, +): AliasEditValidation { + if (!alias || alias.trim().length === 0) return { ok: true } + const entry = registry.byAlias.get(alias) + if (!entry) return { ok: true } + if (entry.source.kind === ignoring.kind && entry.source.ref === ignoring.ref) { + return { ok: true } + } + return { ok: false, conflict: entry } +} diff --git a/src/middleware/shared/utils/iec-address/index.ts b/src/middleware/shared/utils/iec-address/index.ts index a99b6432f..bfd91d716 100644 --- a/src/middleware/shared/utils/iec-address/index.ts +++ b/src/middleware/shared/utils/iec-address/index.ts @@ -15,12 +15,15 @@ export { type SourceRef, } from './address-pool' export { + type AliasEditValidation, type AliasEntry, aliasForAddress, type AliasRegistry, buildAliasRegistry, + describeSource, isAliasNameAvailable, resolveAlias, + validateAliasEdit, } from './alias-registry' export { type SyncableVariable, diff --git a/src/middleware/shared/utils/iec-address/sync-variable-aliases.ts b/src/middleware/shared/utils/iec-address/sync-variable-aliases.ts index b35e63da4..e054a3cf7 100644 --- a/src/middleware/shared/utils/iec-address/sync-variable-aliases.ts +++ b/src/middleware/shared/utils/iec-address/sync-variable-aliases.ts @@ -15,9 +15,14 @@ * reallocates its addresses. * - **orphaned**: variable has an alias the registry no longer * knows about (its producer was removed, or - * target-switched away). Keeps `location` and - * `alias` so the user can decide; reported so - * the UI can flag the cell. + * target-switched away). Keeps `alias` so the + * UI can show the orphan warning + tooltip, but + * **clears `location`** so the ST / XML emitters + * don't bake the stale `AT %…` into the compile. + * The user can re-bind via the picker; the + * previous address is preserved in the report's + * `lastKnownAddress` field for an undo / tooltip + * affordance. * * Pure function: same inputs always produce the same outputs. * Safe to call from any context — store actions, the pre-compile @@ -65,7 +70,14 @@ export function syncVariableAliases( alias: variable.alias, lastKnownAddress: variable.location, }) - next.push(variable) + // Clear the stale location so the compile step (ST + XML + // emitters) doesn't bake an `AT %…` for an address whose + // producer is no longer active. The alias name is retained + // so the variable cell can render the orphan warning and the + // tooltip can surface the last-known address from the report. + // When the user re-binds, `updateVariable`'s auto-adopt path + // re-attaches the alias against the live registry. + next.push({ ...variable, location: '' }) continue } if (entry.address !== variable.location) {