From 0aa9dfcd7c2b67c1f558f2f45f4c0641a6538b78 Mon Sep 17 00:00:00 2001 From: Daniel Coutinho <60111446+dcoutinho1328@users.noreply.github.com> Date: Mon, 8 Jun 2026 09:49:20 -0300 Subject: [PATCH 01/14] =?UTF-8?q?feat(compile):=20embed=20JSON=E2=86=92ST?= =?UTF-8?q?=20transpiler,=20retire=20xml2st=20binary=20(#843)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(compile): embed JSON→ST transpiler, retire xml2st binary path Ports the in-process JSON-fed transpiler (`generate-st-from-json/`) from openplc-web into the editor's backend and wires it through the shared `CompilerPlatformPort`/`LibraryBuildPort` surface. The bundled `xml2st` subprocess hop and `XmlGenerator` pre-step are no longer invoked during compile or library-build — both run entirely in-process against the renderer's project IR. - Renames `transpileXmlToSt` → `transpileToSt` across both ports. - Editor port impls (`editor-compiler-platform-port`, `desktop-library-build-port`) project the editor's IPC schema-shape payload via the new `fromSchemaShape` adapter. - Pipeline + library-build orchestrator cast the schema-shape payload to `never` at the port call site (matches openplc-web's pattern; each port impl knows the actual shape it receives). - Adds `@xmldom/xmldom@^0.9.10` for the transitional DOM fallback the graphical-POU walker still uses. Co-Authored-By: Claude Opus 4.7 (1M context) * chore(lint): autofix simple-import-sort + drop redundant cast in sfc walker CI lint failed with 5 import-sort errors (autofixable) and one unnecessary `as string` cast in `sfc/walker.ts:388` flagged by `@typescript-eslint/no-unnecessary-type-assertion`. Both fixed by `eslint --fix`. Co-Authored-By: Claude Opus 4.7 (1M context) * chore(format): prettier sweep on the ported transpiler subtree + new call sites CI's format check flagged 38 transpiler files (carried over from openplc-web with subtly different prettier config) plus three pipeline call sites touched in the previous commit. Pure formatting — no behavioural change. Co-Authored-By: Claude Opus 4.7 (1M context) * fix(transpiler): swap replaceAll → split/join + final newline on catalog Two byte-identity nits the cross-repo Shared Surface Sync flagged: - `plcopen.ts:normalizePlcOpenXml` used `String.prototype.replaceAll`, which is es2021 and unavailable under web's tsconfig target lib. `split/join` is identical in behaviour for non-regex inputs (`TC6_OLD_NS` has no metacharacters). - `data/std_block_catalog.json` was missing the trailing newline that the web copy has. Both file pairs are now byte-identical across repos. Co-Authored-By: Claude Opus 4.7 (1M context) * chore: drop xml2st from bundle, migrate remaining call sites in-process The shared compile pipeline already routed through the in-process JSON → ST transpiler (`generate-st-from-json/`), but two editor-only paths still spawned the bundled `xml2st` binary: - `CompilerModule.compileForDebugger` ran `handleGenerateXMLfromJSON` + `handleTranspileXMLtoST` instead of calling the JSON transpiler directly. Replaced with the same `fromSchemaShape + runJsonTranspiler` pattern the program-compile platform port uses. - `HardwareModule.getAvailableSerialPorts` shelled out to `xml2st --list-ports` to enumerate USB serial devices. Replaced with `SerialPort.list()` from the `serialport` npm package that was already a project dep (used by `modbus-rtu-client`). With nothing spawning xml2st anymore, the bundle no longer needs to ship it: - `binary-versions.json` no longer pins xml2st. - `scripts/download-binaries.ts` no longer downloads / caches it. - `resources/bin///xml2st` local copies removed + gitignore entries dropped. Dead-code cleanup: `CompilerModule.handleTranspileXMLtoST`, `#executeXml2st`, `#constructXml2stBinaryPath`, `xml2stBinaryPath` field, `EditorCompilerHandlers.handleTranspileXMLtoST`, and the matching tests. Co-Authored-By: Claude Opus 4.7 (1M context) * chore: address valid CodeRabbit nits (no transpiler-output changes) Two safe, output-neutral cleanups from the CodeRabbit review pass: - `from-schema.ts`: swap the three deep relative imports for the `@root/*` alias (per the project's coding-guidelines convention). - `plcopen/accessors.ts`: fix grammar in the two `body don't have instances` error messages — singular subject, so `doesn't have`. Skipped every CodeRabbit suggestion that would alter what the transpiler emits (regex character-class fix, ANY→BOOL fallback, quote-wrapping logic, kindKeyword default, dimensions fallback, isOfType vs strict equality, etc.) — those need separate review against the golden-fixture corpus. Co-Authored-By: Claude Opus 4.7 (1M context) * feat(transpiler): reject SFC at projection time SFC support is still under development on the JSON-native walker. The previous fallback ("passthrough as ST body") silently produced broken output because `body.value` for SFC is React Flow data, not ST text — strucpp would either reject the bogus payload or compile into incorrect logic. Fail-fast with a clear message instead. When the JSON-native SFC walker lands, this branch goes away. Co-Authored-By: Claude Opus 4.7 (1M context) * refactor(transpiler): consolidate into st-transpiler/, drive LD/FBD via React Flow walker - Drop the React Flow walker subtree (canonical at xml2st, validated byte-identical against the python oracle on 169 fixtures) into src/backend/.../st-transpiler/walker/. - Rewire from-schema.ts so LD/FBD bodies skip the PLCOpen XML hop entirely and feed the walker directly. TranspileBody.value now carries the raw React Flow shape; the walker returns { bodySt, syntheticVars }, the wrap composes header + VAR sections + body + END. - Merge generate-st-from-json/ and generate-st-from-react-flow/ into one st-transpiler/ folder with index/types/from-schema/ pou-emission-order at the root and emit/, walker/, core/, helpers/, data/ subfolders. - Prune ~2000 LOC of dead DOM-walking infrastructure (src/plcopen/, src/xmlclass/, body_emit/graph_primitives/ pou_assembly/gen_state and other DOM-only PLCGenerator files); trim surviving helpers to just the live exports. * fix(transpiler): treat block inOut params as call-site inputs `asBlockData` only dispatched on `'input'` / `'output'`, dropping the `'inOut'` class entirely. Result: FB calls whose declared signature had VAR_IN_OUT parameters emitted the call without binding them — e.g. `IRRIGATION_MAIN_CONTROLLER0(Moisture := …, T_Max := …)` instead of the oracle's `IRRIGATION_MAIN_CONTROLLER0 (State := State, Moisture := …, T_Max := …)`. Treat `inOut` (also `'inout'`) the same as `input` so the call emission sweeps the inOut formal in source order. Source order puts VAR_IN_OUT before VAR_INPUT, which matches the python oracle's argument-list order for FB calls. Caught by the new `edge__staging__autonomy_factory_fork` fixture (harvested from editor-staging) — `Irrigation_Main_Controller` declares `State : Irrigation_State` as `VAR_IN_OUT`, and `main` calls it as `(State := State, Moisture := Low_moisture_sensor, T_Max := T#5s)`. All 170 walker fixtures green. * chore: fix import-sort lint errors eslint --fix on the four st-transpiler files CI flagged for simple-import-sort/imports. No behavioural change. * chore: prettier-fix comma spacing in core/modifiers.ts import eslint --fix's import-sort pass collapsed the comma without a space; prettier requires the space. Local lint passed but the CI prettier --check pass caught it. --------- Co-authored-by: Claude Opus 4.7 (1M context) --- .gitignore | 5 - binary-versions.json | 6 +- package-lock.json | 20 +- package.json | 1 + scripts/download-binaries.ts | 237 +- .../editor-compiler-platform-port.test.ts | 59 - .../editor/compiler/compiler-module.spec.ts | 1 - .../editor/compiler/compiler-module.ts | 111 +- .../compiler/desktop-library-build-port.ts | 79 +- .../compiler/editor-compiler-platform-port.ts | 71 +- .../editor/hardware/hardware-module.ts | 55 +- src/backend/shared/compile/pipeline.ts | 47 +- src/backend/shared/library/build-pipeline.ts | 33 +- .../library/library-build-orchestrator.ts | 42 +- .../st-transpiler/core/modifier-types.ts | 16 + .../st-transpiler/core/modifiers.ts | 97 + .../st-transpiler/core/path-tree.ts | 253 + .../st-transpiler/core/trigger-var.ts | 12 + .../st-transpiler/data/std_block_catalog.json | 15855 ++++++++++++++++ .../st-transpiler/emit/configuration.ts | 245 + .../st-transpiler/emit/data-types.ts | 161 + .../st-transpiler/emit/pou-graphical.ts | 174 + .../st-transpiler/emit/pou-textual.ts | 185 + .../st-transpiler/emit/type-text.ts | 48 + .../transpilers/st-transpiler/emit/value.ts | 66 + .../transpilers/st-transpiler/from-schema.ts | 249 + .../st-transpiler/helpers/block-library.ts | 88 + .../st-transpiler/helpers/ctn-globals.ts | 56 + .../st-transpiler/helpers/data-type.ts | 10 + .../st-transpiler/helpers/program.ts | 13 + .../st-transpiler/helpers/text-helpers.ts | 93 + .../st-transpiler/helpers/type-hierarchy.ts | 96 + .../st-transpiler/helpers/type-text.ts | 17 + .../shared/transpilers/st-transpiler/index.ts | 118 + .../st-transpiler/pou-emission-order.ts | 127 + .../shared/transpilers/st-transpiler/types.ts | 163 + .../st-transpiler/walker/README.md | 44 + .../transpilers/st-transpiler/walker/fbd.ts | 29 + .../transpilers/st-transpiler/walker/ld.ts | 813 + .../st-transpiler/walker/narrow.ts | 254 + .../transpilers/st-transpiler/walker/types.ts | 51 + .../shared/ports/compiler-platform-port.ts | 40 +- .../shared/ports/library-build-port.ts | 18 +- 43 files changed, 19561 insertions(+), 597 deletions(-) create mode 100644 src/backend/shared/transpilers/st-transpiler/core/modifier-types.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/core/modifiers.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/core/path-tree.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/core/trigger-var.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/data/std_block_catalog.json create mode 100644 src/backend/shared/transpilers/st-transpiler/emit/configuration.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/emit/data-types.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/emit/pou-textual.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/emit/type-text.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/emit/value.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/from-schema.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/helpers/block-library.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/helpers/ctn-globals.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/helpers/data-type.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/helpers/program.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/helpers/type-text.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/index.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/pou-emission-order.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/types.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/walker/README.md create mode 100644 src/backend/shared/transpilers/st-transpiler/walker/fbd.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/walker/ld.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/walker/narrow.ts create mode 100644 src/backend/shared/transpilers/st-transpiler/walker/types.ts 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/binary-versions.json b/binary-versions.json index eebbe5bfa..46c0e540d 100644 --- a/binary-versions.json +++ b/binary-versions.json @@ -1,10 +1,6 @@ { - "xml2st": { - "version": "v4.0.5", - "repository": "Autonomy-Logic/xml2st" - }, "strucpp": { "version": "v0.5.5", "repository": "Autonomy-Logic/STruCpp" } -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index baa953c61..a943f91e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "@tailwindcss/forms": "^0.5.10", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.2", + "@xmldom/xmldom": "^0.9.10", "@xyflow/react": "^12.0.1", "auto-zustand-selectors-hook": "^2.0.0", "avr8js": "0.20.0", @@ -11037,13 +11038,12 @@ } }, "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, + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", + "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.6" } }, "node_modules/@xtuc/ieee754": { @@ -24901,6 +24901,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..9d58123f7 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@tailwindcss/forms": "^0.5.10", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.2", + "@xmldom/xmldom": "^0.9.10", "@xyflow/react": "^12.0.1", "auto-zustand-selectors-hook": "^2.0.0", "avr8js": "0.20.0", diff --git a/scripts/download-binaries.ts b/scripts/download-binaries.ts index b0318c183..f9504d48d 100644 --- a/scripts/download-binaries.ts +++ b/scripts/download-binaries.ts @@ -1,115 +1,35 @@ /** - * Download external tool binaries (xml2st, strucpp) from GitHub Releases. + * Download external tool binaries (strucpp) from GitHub Releases. * * Usage: - * ts-node scripts/download-binaries.ts [--platform ] [--arch ] [--force] + * ts-node scripts/download-binaries.ts [--force] * - * Defaults to the current platform/arch. Use --force to re-download even if cached. + * Use --force to re-install even if cached. + * + * The legacy `xml2st` binary download path was removed when the + * editor migrated to the in-process JSON → ST transpiler + * (`backend/shared/transpilers/generate-st-from-json/`). */ import { execSync } from 'child_process' import fs from 'fs' import path from 'path' -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - interface ToolEntry { version: string repository: string } interface BinaryVersions { - xml2st: ToolEntry strucpp: ToolEntry } -interface CacheMetadata { - xml2st: string - platform: string - arch: string -} - -type Platform = 'darwin' | 'linux' | 'win32' -type Arch = 'x64' | 'arm64' - -// --------------------------------------------------------------------------- -// Paths -// --------------------------------------------------------------------------- - const ROOT_DIR = path.resolve(__dirname, '..') const VERSIONS_FILE = path.join(ROOT_DIR, 'binary-versions.json') const RESOURCES_DIR = path.join(ROOT_DIR, 'resources') -function binDir(platform: Platform, arch: Arch): string { - return path.join(RESOURCES_DIR, 'bin', platform, arch) -} - -function cacheFile(platform: Platform, arch: Arch): string { - return path.join(binDir(platform, arch), '.binary-metadata.json') -} - -// --------------------------------------------------------------------------- -// CLI argument parsing -// --------------------------------------------------------------------------- - -function parseArgs(): { platform: Platform; arch: Arch; force: boolean } { - const args = process.argv.slice(2) - let platform = process.platform as string - let arch = process.arch as string - let force = false - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--platform' && args[i + 1]) { - platform = args[++i] - } else if (args[i] === '--arch' && args[i + 1]) { - arch = args[++i] - } else if (args[i] === '--force') { - force = true - } - } - - if (!['darwin', 'linux', 'win32'].includes(platform)) { - console.error(`Unsupported platform: ${platform}`) - process.exit(1) - } - if (!['x64', 'arm64'].includes(arch)) { - console.error(`Unsupported architecture: ${arch}`) - process.exit(1) - } - - return { platform: platform as Platform, arch: arch as Arch, force } -} - -// --------------------------------------------------------------------------- -// Cache check -// --------------------------------------------------------------------------- - -function getCachedMetadata(platform: Platform, arch: Arch): CacheMetadata | null { - const file = cacheFile(platform, arch) - if (!fs.existsSync(file)) return null - - try { - return JSON.parse(fs.readFileSync(file, 'utf-8')) as CacheMetadata - } catch { - return null - } -} - -function needsXml2st(versions: BinaryVersions, cached: CacheMetadata | null, platform: Platform, arch: Arch): boolean { - const dir = binDir(platform, arch) - const isWindows = platform === 'win32' - const isDarwin = platform === 'darwin' - - const xml2stPath = isDarwin - ? path.join(dir, 'xml2st', 'xml2st') - : path.join(dir, isWindows ? 'xml2st.exe' : 'xml2st') - - if (!fs.existsSync(xml2stPath)) return true - if (!cached || cached.xml2st !== versions.xml2st.version) return true - - return false +function parseArgs(): { force: boolean } { + return { force: process.argv.slice(2).includes('--force') } } function needsStrucpp(versions: BinaryVersions): boolean { @@ -126,24 +46,9 @@ function needsStrucpp(versions: BinaryVersions): boolean { } catch { return true } - return false } -function writeCache(versions: BinaryVersions, platform: Platform, arch: Arch): void { - const data: CacheMetadata = { - xml2st: versions.xml2st.version, - platform, - arch, - } - fs.mkdirSync(path.dirname(cacheFile(platform, arch)), { recursive: true }) - fs.writeFileSync(cacheFile(platform, arch), JSON.stringify(data, null, 2) + '\n') -} - -// --------------------------------------------------------------------------- -// Download helpers -// --------------------------------------------------------------------------- - async function downloadToFile(url: string, dest: string): Promise { const response = await fetch(url, { redirect: 'follow' }) if (!response.ok) { @@ -153,101 +58,12 @@ async function downloadToFile(url: string, dest: string): Promise { fs.writeFileSync(dest, new Uint8Array(arrayBuffer)) } -function extractTarGz(archive: string, destDir: string): void { - fs.mkdirSync(destDir, { recursive: true }) - execSync(`tar xzf "${archive}" -C "${destDir}"`, { stdio: 'pipe' }) -} - -function extractZip(archive: string, destDir: string): void { - fs.mkdirSync(destDir, { recursive: true }) - execSync(`tar xf "${archive}" -C "${destDir}"`, { stdio: 'pipe' }) -} - function rmrf(p: string): void { if (fs.existsSync(p)) { fs.rmSync(p, { recursive: true, force: true }) } } -function copyRecursive(src: string, dest: string): void { - fs.mkdirSync(dest, { recursive: true }) - for (const entry of fs.readdirSync(src, { withFileTypes: true })) { - const srcPath = path.join(src, entry.name) - const destPath = path.join(dest, entry.name) - if (entry.isSymbolicLink()) { - const linkTarget = fs.readlinkSync(srcPath) - if (fs.existsSync(destPath)) fs.rmSync(destPath, { force: true }) - fs.symlinkSync(linkTarget, destPath) - } else if (entry.isDirectory()) { - copyRecursive(srcPath, destPath) - } else { - fs.copyFileSync(srcPath, destPath) - } - } -} - -// --------------------------------------------------------------------------- -// xml2st download and extraction -// --------------------------------------------------------------------------- - -async function downloadXml2st( - tool: ToolEntry, - platform: Platform, - arch: Arch, - targetBinDir: string, -): Promise { - const isWindows = platform === 'win32' - const isDarwin = platform === 'darwin' - const ext = isWindows ? 'zip' : 'tar.gz' - const url = `https://github.com/${tool.repository}/releases/download/${tool.version}/xml2st-${platform}-${arch}.${ext}` - - console.log(` Downloading xml2st ${tool.version} for ${platform}-${arch}...`) - const tmpDir = fs.mkdtempSync(path.join(RESOURCES_DIR, '.tmp-xml2st-')) - - try { - const archivePath = path.join(tmpDir, `xml2st.${ext}`) - await downloadToFile(url, archivePath) - - const extractDir = path.join(tmpDir, 'extracted') - if (isWindows) { - extractZip(archivePath, extractDir) - } else { - extractTarGz(archivePath, extractDir) - } - - // Archive contains xml2st/ directory - const extractedToolDir = path.join(extractDir, 'xml2st') - - if (isDarwin) { - // macOS: xml2st is a directory with _internal/ — copy as-is - const destDir = path.join(targetBinDir, 'xml2st') - rmrf(destDir) - copyRecursive(extractedToolDir, destDir) - fs.chmodSync(path.join(destDir, 'xml2st'), 0o755) - } else { - // Linux/Windows: single executable - const exeName = isWindows ? 'xml2st.exe' : 'xml2st' - const srcFile = path.join(extractedToolDir, exeName) - const destFile = path.join(targetBinDir, exeName) - rmrf(destFile) - // Also remove any leftover directory from a previous macOS-style install - rmrf(path.join(targetBinDir, 'xml2st')) - fs.copyFileSync(srcFile, destFile) - if (!isWindows) { - fs.chmodSync(destFile, 0o755) - } - } - - console.log(` xml2st ${tool.version} installed.`) - } finally { - rmrf(tmpDir) - } -} - -// --------------------------------------------------------------------------- -// strucpp download and extraction -// --------------------------------------------------------------------------- - async function downloadStrucpp(tool: ToolEntry): Promise { // The npm tarball is platform-independent (pure TypeScript + C++ headers) const version = tool.version.replace(/^v/, '') @@ -284,12 +100,8 @@ async function downloadStrucpp(tool: ToolEntry): Promise { } } -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - async function main(): Promise { - const { platform, arch, force } = parseArgs() + const { force } = parseArgs() if (!fs.existsSync(VERSIONS_FILE)) { console.error(`binary-versions.json not found at ${VERSIONS_FILE}`) @@ -298,34 +110,15 @@ async function main(): Promise { const versions: BinaryVersions = JSON.parse(fs.readFileSync(VERSIONS_FILE, 'utf-8')) - console.log(`[download-binaries] platform=${platform} arch=${arch} force=${force}`) + console.log(`[download-binaries] force=${force}`) - const targetBinDir = binDir(platform, arch) - fs.mkdirSync(targetBinDir, { recursive: true }) - - const cached = force ? null : getCachedMetadata(platform, arch) - const downloadXml2stNeeded = force || needsXml2st(versions, cached, platform, arch) - const downloadStrucppNeeded = force || needsStrucpp(versions) - - if (!downloadXml2stNeeded && !downloadStrucppNeeded) { - console.log(`[download-binaries] All tools up to date, skipping.`) - return - } - - if (downloadXml2stNeeded) { - await downloadXml2st(versions.xml2st, platform, arch, targetBinDir) - } else { - console.log(` xml2st ${versions.xml2st.version} already installed, skipping.`) - } - - // strucpp is platform-independent — only download once regardless of platform/arch - if (downloadStrucppNeeded) { - await downloadStrucpp(versions.strucpp) - } else { + if (!force && !needsStrucpp(versions)) { console.log(` strucpp ${versions.strucpp.version} already installed, skipping.`) + console.log(`[download-binaries] Done.`) + return } - writeCache(versions, platform, arch) + await downloadStrucpp(versions.strucpp) console.log(`[download-binaries] Done.`) } 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..8793d3045 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' /** @@ -159,8 +164,6 @@ class CompilerModule { arduinoCliConfigurationFilePath: string arduinoCliBaseParameters: string[] - xml2stBinaryPath: string - strucppRuntimeDir: string // Memoised arduino-cli `--show-properties=expanded` output keyed by FQBN. @@ -215,8 +218,6 @@ class CompilerModule { // INFO: We use this approach because some commands can receive additional parameters as a string array. this.arduinoCliBaseParameters = ['--config-file', this.arduinoCliConfigurationFilePath] - this.xml2stBinaryPath = this.#constructXml2stBinaryPath() - this.strucppRuntimeDir = this.#constructStrucppRuntimeDir() } @@ -343,10 +344,6 @@ class CompilerModule { return join(this.binaryDirectoryPath, 'arduino-cli') } - #constructXml2stBinaryPath(): string { - return join(this.binaryDirectoryPath, 'xml2st', CompilerModule.HOST_PLATFORM === 'darwin' ? 'xml2st' : '') - } - #constructStrucppRuntimeDir(): string { // strucpp's runtime headers (`src/runtime/include/`) live in two // places depending on whether we're running dev or a packaged app: @@ -417,14 +414,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') { @@ -852,46 +841,6 @@ class CompilerModule { }) } - async handleTranspileXMLtoST( - generatedXMLFilePath: string, - handleOutputData: (chunk: Buffer | string, logLevel?: 'info' | 'error') => void, - extraXml2stArgs: readonly string[], - ) { - return new Promise>((resolve, reject) => { - // `extraXml2stArgs` comes from the shared pipeline's - // `TranspileXmlToStArgs.xml2stArgs` — the single source of truth - // for xml2st flag semantics across editor and web. Editor passes - // them through verbatim (trusted local binary); web's adapter - // filters against its known-args allowlist before sending to the - // compile-service. Strucpp targets currently pass - // `['--keep-structs']` (native STRUCT declarations vs matiec's - // legacy struct→FB rewrite); future flags appear here as the - // pipeline opts into them. - const executeCommand = this.#executeXml2st(['--generate-st', generatedXMLFilePath, ...extraXml2stArgs]) - - let stderrData = '' - - // INFO: We use the xml2st command to transpile the XML file to ST. - executeCommand.stdout?.on('data', (data: Buffer) => { - handleOutputData(data) - }) - executeCommand.stderr?.on('data', (data: Buffer) => { - stderrData += data.toString() - }) - - executeCommand.on('close', (code) => { - if (code === 0) { - handleOutputData(`ST file generated at: ${generatedXMLFilePath.replace('plc.xml', 'program.st')}`, 'info') - resolve({ - success: true, - }) - } else { - reject(new Error(`xml2st process exited with code ${code}\n${stderrData}`)) - } - }) - }) - } - async handleCompileSTtoCpp( sourceTargetFolderPath: string, handleOutputData: (chunk: Buffer | string, logLevel?: 'info' | 'error', compileError?: StrucppCompileError) => void, @@ -2596,7 +2545,6 @@ class CompilerModule { // --- Build the editor's CompilerPlatformPort implementation --- const platformPort = createEditorCompilerPlatformPort( { - handleTranspileXMLtoST: this.handleTranspileXMLtoST.bind(this), handleCompileArduinoProgram: this.handleCompileArduinoProgram.bind(this), handleUploadProgram: this.handleUploadProgram.bind(this), handleCoreInstallation: this.handleCoreInstallation.bind(this), @@ -2794,34 +2742,34 @@ class CompilerModule { return } + // JSON → ST in-process via `st-transpiler` — replaces the + // legacy XmlGenerator + xml2st-subprocess hop the program-compile + // pipeline already retired. Mirrors what + // `editor-compiler-platform-port.transpileToSt` does for the + // shared pipeline path, scoped down to the debug compile here. 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'], - ) + 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 XML to ST: ${error as string}\nStopping debug compilation process.`, + message: `Error transpiling JSON to ST: ${getErrorMessage(error)}\nStopping debug compilation process.`, }) _mainProcessPort.close() return @@ -2977,7 +2925,6 @@ class CompilerModule { // glue the library build needs — every stage decision lives in // the shared orchestrator from here on. const libraryPort = createDesktopLibraryBuildPort({ - transpileXmlToSt: (xmlPath, log, extraArgs) => this.handleTranspileXMLtoST(xmlPath, log, extraArgs), loadEnabledArchives: (names) => mainProcessBridge.loadEnabledArchives(names), runVerificationCompile: ({ projectPath: p, verifyProjectData: v, emit }) => this.runVerificationCompile(p, v as PLCProjectData, mainProcessBridge, (message, logLevel) => diff --git a/src/backend/editor/compiler/desktop-library-build-port.ts b/src/backend/editor/compiler/desktop-library-build-port.ts index f368967d8..442ae0e59 100644 --- a/src/backend/editor/compiler/desktop-library-build-port.ts +++ b/src/backend/editor/compiler/desktop-library-build-port.ts @@ -7,7 +7,8 @@ * `runLibraryBuildPipeline` cannot perform itself: * * - MD5 hashing (Node `crypto`) - * - xml2st subprocess invocation (the desktop binary) + * - in-process JSON → ST transpilation via + * `backend/shared/transpilers/st-transpiler/` * - 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,16 +18,20 @@ * 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' +import { createHash } from 'node:crypto' import * as fs from 'node:fs/promises' -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 { + fromSchemaShape, + type SchemaProjectData, + transpileToSt as runJsonTranspiler, +} from '@root/backend/shared/transpilers/st-transpiler' +import type { TranspileToStArgs, TranspileToStResult } from '@root/middleware/shared/ports/compiler-platform-port' import type { LibraryBuildPort } from '@root/middleware/shared/ports/library-build-port' /** @@ -35,19 +40,6 @@ import type { LibraryBuildPort } from '@root/middleware/shared/ports/library-bui * surface stays narrow + unit-testable. */ 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. - */ - transpileXmlToSt( - xmlPath: string, - log: (chunk: Buffer | string, level?: 'info' | 'error') => void, - extraArgs: readonly string[], - ): Promise - /** * Resolve the names of project-enabled libraries to their parsed * `.stlib` archives. Bundled IEC standard set is included @@ -80,42 +72,31 @@ 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 { - // 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` - // is written separately by the orchestrator via - // writeBuildFile — the intermediate here exists only for the - // subprocess. - const sessionDir = path.join(os.tmpdir(), `openplc-lib-xml2st-${randomUUID()}`) + ): Promise { try { - 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 deps.transpileXmlToSt( - xmlPath, - (chunk, level) => log(typeof chunk === 'string' ? chunk : chunk.toString(), level ?? 'info'), - args.xml2stArgs, - ) - - const programSt = await fs.readFile(programStPath, 'utf-8') - return { ok: true, programSt } + // 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(`xml2st failed: ${message}`, 'error') + log(`transpile-from-json failed: ${message}`, 'error') return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } - } finally { - // Best-effort cleanup. Leaks aren't fatal (os.tmpdir is - // the OS's responsibility) but tidying after ourselves - // keeps the dev disk clean. - await fs.rm(sessionDir, { recursive: true, force: true }).catch(() => { - /* swallow — the temp dir is the OS's to GC */ - }) } }, diff --git a/src/backend/editor/compiler/editor-compiler-platform-port.ts b/src/backend/editor/compiler/editor-compiler-platform-port.ts index 7419391e0..b95cde697 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,10 +16,12 @@ * - 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` no longer needs disk materialisation or a + * subprocess: it projects the project IR via `fromSchemaShape` + * (editor IPC delivers schema-shape data) and runs the in-process + * JSON-fed transpiler + * (`backend/shared/transpilers/st-transpiler/`). The old + * `xml2st` binary path has been retired. * * This module is editor-only (lives under `backend/editor/`); the * web platform implements the same port interface separately under @@ -28,6 +30,11 @@ 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 type { CheckRuntimeVersionArgs, CheckRuntimeVersionResult, @@ -40,8 +47,8 @@ import type { PackageVppPluginResult, PlatformDeviceContext, PlatformLog, - TranspileXmlToStArgs, - TranspileXmlToStResult, + TranspileToStArgs, + TranspileToStResult, UploadArduinoBoardArgs, UploadResult, UploadRuntimeV3Args, @@ -60,7 +67,6 @@ import type { CompilerModule } from './compiler-module' * file-watching, etc.). */ export interface EditorCompilerHandlers { - handleTranspileXMLtoST: CompilerModule['handleTranspileXMLtoST'] handleCompileArduinoProgram: CompilerModule['handleCompileArduinoProgram'] handleUploadProgram: CompilerModule['handleUploadProgram'] handleCoreInstallation: CompilerModule['handleCoreInstallation'] @@ -152,33 +158,34 @@ 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. + * Project the editor's port-shape data straight to Structured + * Text via the in-process JSON-fed transpiler. No subprocess, + * no disk round-trip — Electron's main process drives the + * walker directly. */ - async transpileXmlToSt(args: TranspileXmlToStArgs, log: PlatformLog): Promise { - const xmlPath = join(context.sourceTargetFolderPath, 'plc.xml') + async transpileToSt(args: TranspileToStArgs, log: PlatformLog): Promise { try { - await fs.mkdir(dirname(xmlPath), { recursive: true }) - await fs.writeFile(xmlPath, args.xml, 'utf-8') - - await handlers.handleTranspileXMLtoST( - xmlPath, - (chunk, level) => { - const message = typeof chunk === 'string' ? chunk : chunk.toString() - log(message, level ?? 'info') - }, - args.xml2stArgs, - ) - - const programStPath = join(context.sourceTargetFolderPath, 'program.st') - const programSt = await fs.readFile(programStPath, 'utf-8') - return { ok: true, programSt } + // 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(`xml2st failed: ${message}`, 'error') + log(`st-transpiler failed: ${message}`, 'error') return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } } }, 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/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..74a63278d --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts @@ -0,0 +1,245 @@ +/** + * IR-native `CONFIGURATION … END_CONFIGURATION` block emitter. + * + * JSON-direct port of `src/PLCGenerator/configuration.ts` — walks + * `TranspileProject.configuration` instead of the parsed DOM, but + * emits byte-identical chunks. Mirrors Python's + * `ProgramGenerator.GenerateConfiguration` + `GenerateResource` + * (PLCGenerator.py:334-628). + * + * The IR carries exactly one configuration with one resource, named + * `Config0` / `Res0` — same hardcoded names the XML-based emitter + * produced. Global vars are emitted under the configuration block + * (not under the resource), matching `irToPlcOpenXml`'s layout. + * + * CTN-globals provider is honoured: `kind: 'variable'` entries + * (tuple-shape globals) are appended after the IR's + * `globalVariables`. `kind: 'varlist'` entries (raw DOM `` + * elements) are silently ignored on the IR-native path — they only + * come from the legacy DOM-injection flow which has no caller after + * Phase 1. + */ + +import type { ConfigurationExtraVariablesProvider } from '../helpers/ctn-globals' +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' + +export interface GenerateConfigurationOptions { + /** + * Beremiz CTN-injected globals provider. Mirrors + * `Controler.GetConfigurationExtraVariables` (PLCControler.py:1248). + */ + extraVarsProvider?: ConfigurationExtraVariablesProvider | null +} + +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, + options: GenerateConfigurationOptions = {}, +): 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. IR-shape globals first, + // then any CTN-injected tuple-shape globals. + emitGlobalVarList( + out, + collectConfigGlobals(project, options.extraVarsProvider), + 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 collectConfigGlobals( + project: TranspileProject, + provider: ConfigurationExtraVariablesProvider | null | undefined, +): TranspileVariable[] { + const out: TranspileVariable[] = [...project.configuration.globalVariables] + if (!provider) return out + const entries = provider() + for (const entry of entries) { + if (entry.kind !== 'variable') continue + const tuple = entry.variable + out.push({ + name: tuple.name, + type: tupleTypeToIr(tuple.type), + ...(tuple.initial ? { initialValue: tuple.initial } : {}), + }) + } + return out +} + +function tupleTypeToIr(typeName: string): TranspileVariable['type'] { + // CTN-globals tuples carry the type as a bare string. IEC base + // types resolve to `base-type`; anything else becomes a derived + // reference — mirrors PLCControler.py:1258-1273. + const upper = typeName.toUpperCase() + const elementaryTypes = new Set([ + 'BOOL', + 'SINT', + 'INT', + 'DINT', + 'LINT', + 'USINT', + 'UINT', + 'UDINT', + 'ULINT', + 'REAL', + 'LREAL', + 'TIME', + 'DATE', + 'TOD', + 'DT', + 'STRING', + 'WSTRING', + 'BYTE', + 'WORD', + 'DWORD', + 'LWORD', + ]) + if (elementaryTypes.has(upper)) { + return { definition: 'base-type', value: upper } + } + return { definition: 'derived', value: typeName } +} + +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..a6fca3957 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts @@ -0,0 +1,174 @@ +/** + * 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 { resolveBlockType } from '../helpers/block-library' +import { PLC_BASE_TYPES } from '../helpers/ctn-globals' +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[] +} + +/* ─────────────────────────── 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. + 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 } + } + } + 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..b57700d27 --- /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/ctn-globals' +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/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/ctn-globals.ts b/src/backend/shared/transpilers/st-transpiler/helpers/ctn-globals.ts new file mode 100644 index 000000000..90539a74a --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/ctn-globals.ts @@ -0,0 +1,56 @@ +/** + * Configuration-level globals injected by host plugins (Beremiz CTN + * mechanism). Mirrors `Controler.GetConfigurationExtraVariables` + * (PLCControler.py:1248-1285): a provider callback returns either + * pre-built `varlist` entries or `(name, type, initial)` tuples that + * the configuration emitter folds into the resource's `VAR_GLOBAL` + * sections. + * + * In a standalone xml2st build the provider is stubbed to `[]`, so + * observable behaviour against the python oracle is unchanged — the + * shape is exported so editor/web hosts wiring plugins can plug in. + */ + +/** + * IEC 61131-3 elementary base types — mirrors `Controler.GetBaseTypes()` + * (derived from `TypeHierarchy_list` in `plcopen/definitions.py:84`). + * Used to decide whether a synthesised global's type is elementary + * (emit verbatim) or derived (emit as a type reference). + * + * `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', +]) + +export interface CtnGlobalVarTuple { + name: string + /** Elementary type name (`'BOOL'`, `'INT'`, …) or a derived-type name. */ + type: string + /** ST source for the initial value (`''` omits `:= …`). */ + initial?: string +} + +export type CtnGlobalEntry = { kind: 'variable'; variable: CtnGlobalVarTuple } + +export type ConfigurationExtraVariablesProvider = () => CtnGlobalEntry[] 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..1125d5fea --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts @@ -0,0 +1,93 @@ +/** + * Text-manipulation helpers used by the body dispatcher and downstream phases. + * + * Mirrors: + * - `ReIndentText` (PLCGenerator.py:66) + * - `ComputePouName` (plcopen/types_enums.py:112) + * - `ComputePouTransitionName` (plcopen/types_enums.py:117) + * - `ComputePouActionName` (plcopen/types_enums.py:122) + * - `ComputeConfigurationName` (plcopen/types_enums.py:127) + */ + +/** + * 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}` +} + +/** `"T::" + pou + "::" + transition`. */ +export function computePouTransitionName(pou: string, transition: string): string { + return `T::${pou}::${transition}` +} + +/** `"A::" + pou + "::" + action`. */ +export function computePouActionName(pou: string, action: string): string { + return `A::${pou}::${action}` +} + +/** `"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..87c3c017b --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts @@ -0,0 +1,96 @@ +/** + * 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. +} + +/** + * Returns `true` iff `type` is `reference` or any of its subtypes (walking + * the parent chain). `null` `reference` returns `true` ("matches anything"), + * mirroring Python's `if reference is None: return True`. + * + * Unknown types (not in the hierarchy) return `false` against any + * reference except themselves. Same behavior as Python where the unknown + * type raises `KeyError` on the hierarchy lookup — we soften to `false` so + * derived user types (e.g. `Irrigation_State`) don't crash overload checks. + */ +export function isOfType(type: string, reference: string | null): boolean { + if (reference === null) return true + if (type === reference) return true + const parent = TypeHierarchy[type] + if (parent === undefined) return false // user-defined type, not in hierarchy + if (parent === null) return false // reached ANY without matching + return isOfType(parent, reference) +} + +/** + * Concrete (non-ANY-prefixed) types that are subtypes of `metaType`. + * Used by `get_standard_funtions` to expand polymorphic CSV signatures into + * concrete overload entries (e.g. `(ANY_NUM, ANY_NUM)` becomes one entry per + * `INT`, `SINT`, `DINT`, …). Catalog already pre-expanded; this stays in TS + * for completeness and future use. + */ +export function getSubTypes(metaType: string): string[] { + const out: string[] = [] + for (const typename of Object.keys(TypeHierarchy)) { + if (typename.startsWith('ANY')) continue + if (isOfType(typename, metaType)) out.push(typename) + } + return out +} 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..489d493a5 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/index.ts @@ -0,0 +1,118 @@ +/** + * 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 { type GenerateConfigurationOptions, 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 { ConfigurationExtraVariablesProvider, CtnGlobalEntry, CtnGlobalVarTuple } from './helpers/ctn-globals' +export type { + TranspileBody, + TranspileBodyLanguage, + TranspileDataType, + TranspileInstance, + TranspilePou, + TranspilePouInterface, + TranspilePouKind, + TranspileProject, + TranspileTask, + TranspileVariable, + TranspileVariableClass, + TranspileVariableType, +} from './types' + +export type TranspileOptions = GenerateConfigurationOptions + +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, options: TranspileOptions = {}): 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, options)) { + 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..18dba4d1d --- /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/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 From 2ae973adf0291fc320562d23f63b1b13d9d0a11a Mon Sep 17 00:00:00 2001 From: Daniel Coutinho <60111446+dcoutinho1328@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:48:28 -0300 Subject: [PATCH 02/14] chore(transpiler): prune dead code from Phase 2 cleanup (#851) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Phase 2 transpiler consolidation left a few unreached surfaces that this commit removes: - Drop `@xmldom/xmldom` dependency. Was added for the DOM walker that Phase 2d retired; zero `src/` consumers remain. - Delete the CTN-globals plugin API (`CtnGlobalEntry`, `CtnGlobalVarTuple`, `ConfigurationExtraVariablesProvider`). No caller wires up a provider — the API was forward-looking only. `emit/configuration.ts` loses the `extraVarsProvider` option and the `collectConfigGlobals` / `tupleTypeToIr` helpers along with it; `transpileToSt`'s `options` parameter disappears as a consequence. Rename the surviving constant's home from `helpers/ctn-globals.ts` to `helpers/base-types.ts` (the file was named after the API surface that just went away). - Delete `isOfType` and `getSubTypes` from `helpers/type-hierarchy.ts`. Defined as a forward-looking overload-resolution helper, never called; `getSubTypes` was the only `isOfType` consumer. The `TypeHierarchy` constant itself stays — `emit/value.ts` reads it. - Delete `computePouTransitionName` and `computePouActionName` from `helpers/text-helpers.ts`. SFC-only naming helpers; SFC support was dropped in Phase 2d. `tsc --build`, `eslint`, and `prettier --check` clean. Editor and web `st-transpiler/` trees remain byte-identical for Shared Surface Sync. --- package-lock.json | 10 -- package.json | 1 - .../st-transpiler/emit/configuration.ts | 93 ++----------------- .../st-transpiler/emit/pou-graphical.ts | 2 +- .../st-transpiler/emit/pou-textual.ts | 2 +- .../st-transpiler/helpers/base-types.ts | 33 +++++++ .../st-transpiler/helpers/ctn-globals.ts | 56 ----------- .../st-transpiler/helpers/text-helpers.ts | 21 +---- .../st-transpiler/helpers/type-hierarchy.ts | 35 ------- .../shared/transpilers/st-transpiler/index.ts | 9 +- .../st-transpiler/walker/README.md | 12 +-- 11 files changed, 56 insertions(+), 218 deletions(-) create mode 100644 src/backend/shared/transpilers/st-transpiler/helpers/base-types.ts delete mode 100644 src/backend/shared/transpilers/st-transpiler/helpers/ctn-globals.ts diff --git a/package-lock.json b/package-lock.json index a943f91e2..5ae957ff8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,7 +33,6 @@ "@tailwindcss/forms": "^0.5.10", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.2", - "@xmldom/xmldom": "^0.9.10", "@xyflow/react": "^12.0.1", "auto-zustand-selectors-hook": "^2.0.0", "avr8js": "0.20.0", @@ -11037,15 +11036,6 @@ } } }, - "node_modules/@xmldom/xmldom": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.10.tgz", - "integrity": "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==", - "license": "MIT", - "engines": { - "node": ">=14.6" - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", diff --git a/package.json b/package.json index 9d58123f7..1c7274e2d 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,6 @@ "@tailwindcss/forms": "^0.5.10", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.2", - "@xmldom/xmldom": "^0.9.10", "@xyflow/react": "^12.0.1", "auto-zustand-selectors-hook": "^2.0.0", "avr8js": "0.20.0", diff --git a/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts b/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts index 74a63278d..f2e89f665 100644 --- a/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts +++ b/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts @@ -1,40 +1,22 @@ /** * IR-native `CONFIGURATION … END_CONFIGURATION` block emitter. * - * JSON-direct port of `src/PLCGenerator/configuration.ts` — walks - * `TranspileProject.configuration` instead of the parsed DOM, but - * emits byte-identical chunks. Mirrors Python's - * `ProgramGenerator.GenerateConfiguration` + `GenerateResource` - * (PLCGenerator.py:334-628). + * 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 XML-based emitter - * produced. Global vars are emitted under the configuration block - * (not under the resource), matching `irToPlcOpenXml`'s layout. - * - * CTN-globals provider is honoured: `kind: 'variable'` entries - * (tuple-shape globals) are appended after the IR's - * `globalVariables`. `kind: 'varlist'` entries (raw DOM `` - * elements) are silently ignored on the IR-native path — they only - * come from the legacy DOM-injection flow which has no caller after - * Phase 1. + * `Config0` / `Res0` — same hardcoded names the python oracle + * produces. Global vars are emitted under the configuration block + * (not under the resource). */ -import type { ConfigurationExtraVariablesProvider } from '../helpers/ctn-globals' 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' -export interface GenerateConfigurationOptions { - /** - * Beremiz CTN-injected globals provider. Mirrors - * `Controler.GetConfigurationExtraVariables` (PLCControler.py:1248). - */ - extraVarsProvider?: ConfigurationExtraVariablesProvider | null -} - const CONFIG_NAME = 'Config0' const RESOURCE_NAME = 'Res0' @@ -44,10 +26,7 @@ const RESOURCE_NAME = 'Res0' * globals (caller may still want the keyword shell — Python always * emits the block; we mirror that). */ -export function generateConfigurations( - project: TranspileProject, - options: GenerateConfigurationOptions = {}, -): ProgramChunk[] { +export function generateConfigurations(project: TranspileProject): ProgramChunk[] { const configTagname = computeConfigurationName(CONFIG_NAME) const resourceTagname = computeConfigurationResourceName(CONFIG_NAME, RESOURCE_NAME) @@ -57,11 +36,10 @@ export function generateConfigurations( out.push([CONFIG_NAME, [configTagname, 'name']]) out.push(['\n', []]) - // Configuration-level global variables. IR-shape globals first, - // then any CTN-injected tuple-shape globals. + // Configuration-level global variables. emitGlobalVarList( out, - collectConfigGlobals(project, options.extraVarsProvider), + project.configuration.globalVariables, configTagname, /*indent=*/ ' ', /*varIndent=*/ ' ', @@ -142,59 +120,6 @@ export function generateConfigurations( /* ────────────────────── helpers ─────────────────────────────────────────── */ -function collectConfigGlobals( - project: TranspileProject, - provider: ConfigurationExtraVariablesProvider | null | undefined, -): TranspileVariable[] { - const out: TranspileVariable[] = [...project.configuration.globalVariables] - if (!provider) return out - const entries = provider() - for (const entry of entries) { - if (entry.kind !== 'variable') continue - const tuple = entry.variable - out.push({ - name: tuple.name, - type: tupleTypeToIr(tuple.type), - ...(tuple.initial ? { initialValue: tuple.initial } : {}), - }) - } - return out -} - -function tupleTypeToIr(typeName: string): TranspileVariable['type'] { - // CTN-globals tuples carry the type as a bare string. IEC base - // types resolve to `base-type`; anything else becomes a derived - // reference — mirrors PLCControler.py:1258-1273. - const upper = typeName.toUpperCase() - const elementaryTypes = new Set([ - 'BOOL', - 'SINT', - 'INT', - 'DINT', - 'LINT', - 'USINT', - 'UINT', - 'UDINT', - 'ULINT', - 'REAL', - 'LREAL', - 'TIME', - 'DATE', - 'TOD', - 'DT', - 'STRING', - 'WSTRING', - 'BYTE', - 'WORD', - 'DWORD', - 'LWORD', - ]) - if (elementaryTypes.has(upper)) { - return { definition: 'base-type', value: upper } - } - return { definition: 'derived', value: typeName } -} - function emitGlobalVarList( out: ProgramChunk[], variables: TranspileVariable[], diff --git a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts index a6fca3957..5f68bd18c 100644 --- a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts @@ -9,8 +9,8 @@ * declaration order matches what the python oracle produces. */ +import { PLC_BASE_TYPES } from '../helpers/base-types' import { resolveBlockType } from '../helpers/block-library' -import { PLC_BASE_TYPES } from '../helpers/ctn-globals' import type { ProgramChunk } from '../helpers/program' import { computePouName } from '../helpers/text-helpers' import { varTypeNames } from '../helpers/type-text' diff --git a/src/backend/shared/transpilers/st-transpiler/emit/pou-textual.ts b/src/backend/shared/transpilers/st-transpiler/emit/pou-textual.ts index b57700d27..37bfd1ae6 100644 --- a/src/backend/shared/transpilers/st-transpiler/emit/pou-textual.ts +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-textual.ts @@ -11,7 +11,7 @@ * dispatch. */ -import { PLC_BASE_TYPES } from '../helpers/ctn-globals' +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' 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/ctn-globals.ts b/src/backend/shared/transpilers/st-transpiler/helpers/ctn-globals.ts deleted file mode 100644 index 90539a74a..000000000 --- a/src/backend/shared/transpilers/st-transpiler/helpers/ctn-globals.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Configuration-level globals injected by host plugins (Beremiz CTN - * mechanism). Mirrors `Controler.GetConfigurationExtraVariables` - * (PLCControler.py:1248-1285): a provider callback returns either - * pre-built `varlist` entries or `(name, type, initial)` tuples that - * the configuration emitter folds into the resource's `VAR_GLOBAL` - * sections. - * - * In a standalone xml2st build the provider is stubbed to `[]`, so - * observable behaviour against the python oracle is unchanged — the - * shape is exported so editor/web hosts wiring plugins can plug in. - */ - -/** - * IEC 61131-3 elementary base types — mirrors `Controler.GetBaseTypes()` - * (derived from `TypeHierarchy_list` in `plcopen/definitions.py:84`). - * Used to decide whether a synthesised global's type is elementary - * (emit verbatim) or derived (emit as a type reference). - * - * `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', -]) - -export interface CtnGlobalVarTuple { - name: string - /** Elementary type name (`'BOOL'`, `'INT'`, …) or a derived-type name. */ - type: string - /** ST source for the initial value (`''` omits `:= …`). */ - initial?: string -} - -export type CtnGlobalEntry = { kind: 'variable'; variable: CtnGlobalVarTuple } - -export type ConfigurationExtraVariablesProvider = () => CtnGlobalEntry[] diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts b/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts index 1125d5fea..256758f01 100644 --- a/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts +++ b/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts @@ -1,12 +1,7 @@ /** - * Text-manipulation helpers used by the body dispatcher and downstream phases. - * - * Mirrors: - * - `ReIndentText` (PLCGenerator.py:66) - * - `ComputePouName` (plcopen/types_enums.py:112) - * - `ComputePouTransitionName` (plcopen/types_enums.py:117) - * - `ComputePouActionName` (plcopen/types_enums.py:122) - * - `ComputeConfigurationName` (plcopen/types_enums.py:127) + * Text-manipulation helpers — mirrors python's `ReIndentText` + * (`PLCGenerator.py:66`) and the `Compute*Name` family in + * `plcopen/types_enums.py:112-132`. */ /** @@ -71,16 +66,6 @@ export function computePouName(name: string): string { return `P::${name}` } -/** `"T::" + pou + "::" + transition`. */ -export function computePouTransitionName(pou: string, transition: string): string { - return `T::${pou}::${transition}` -} - -/** `"A::" + pou + "::" + action`. */ -export function computePouActionName(pou: string, action: string): string { - return `A::${pou}::${action}` -} - /** `"C::" + name`. */ export function computeConfigurationName(name: string): string { return `C::${name}` diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts b/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts index 87c3c017b..7f93cbc32 100644 --- a/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts +++ b/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts @@ -59,38 +59,3 @@ export const TypeHierarchy: Readonly> = { LWORD: 'ANY_NBIT', // WSTRING intentionally absent — matches Python's `# TODO` comment. } - -/** - * Returns `true` iff `type` is `reference` or any of its subtypes (walking - * the parent chain). `null` `reference` returns `true` ("matches anything"), - * mirroring Python's `if reference is None: return True`. - * - * Unknown types (not in the hierarchy) return `false` against any - * reference except themselves. Same behavior as Python where the unknown - * type raises `KeyError` on the hierarchy lookup — we soften to `false` so - * derived user types (e.g. `Irrigation_State`) don't crash overload checks. - */ -export function isOfType(type: string, reference: string | null): boolean { - if (reference === null) return true - if (type === reference) return true - const parent = TypeHierarchy[type] - if (parent === undefined) return false // user-defined type, not in hierarchy - if (parent === null) return false // reached ANY without matching - return isOfType(parent, reference) -} - -/** - * Concrete (non-ANY-prefixed) types that are subtypes of `metaType`. - * Used by `get_standard_funtions` to expand polymorphic CSV signatures into - * concrete overload entries (e.g. `(ANY_NUM, ANY_NUM)` becomes one entry per - * `INT`, `SINT`, `DINT`, …). Catalog already pre-expanded; this stays in TS - * for completeness and future use. - */ -export function getSubTypes(metaType: string): string[] { - const out: string[] = [] - for (const typename of Object.keys(TypeHierarchy)) { - if (typename.startsWith('ANY')) continue - if (isOfType(typename, metaType)) out.push(typename) - } - return out -} diff --git a/src/backend/shared/transpilers/st-transpiler/index.ts b/src/backend/shared/transpilers/st-transpiler/index.ts index 489d493a5..2e65d3878 100644 --- a/src/backend/shared/transpilers/st-transpiler/index.ts +++ b/src/backend/shared/transpilers/st-transpiler/index.ts @@ -14,7 +14,7 @@ * `PLCProjectData` the editor's IPC delivers) — see `from-schema.ts`. */ -import { type GenerateConfigurationOptions, generateConfigurations } from './emit/configuration' +import { generateConfigurations } from './emit/configuration' import { generateDataTypes } from './emit/data-types' import { generateGraphicalPou } from './emit/pou-graphical' import { generateTextualPou } from './emit/pou-textual' @@ -22,7 +22,6 @@ import { buildPouEmissionOrder } from './pou-emission-order' import type { TranspileProject } from './types' export { fromSchemaShape, type SchemaProjectData } from './from-schema' -export type { ConfigurationExtraVariablesProvider, CtnGlobalEntry, CtnGlobalVarTuple } from './helpers/ctn-globals' export type { TranspileBody, TranspileBodyLanguage, @@ -38,8 +37,6 @@ export type { TranspileVariableType, } from './types' -export type TranspileOptions = GenerateConfigurationOptions - export interface TranspileResult { /** Concatenated Structured Text, or `null` if no POU compiled. */ programSt: string | null @@ -65,7 +62,7 @@ const TEXTUAL_LANGUAGES = new Set(['st', 'il', 'python', 'cpp']) * compile errors land in `result.errors` and the rest of the * program still emits. */ -export function transpileToSt(project: TranspileProject, options: TranspileOptions = {}): TranspileResult { +export function transpileToSt(project: TranspileProject): TranspileResult { const errors: string[] = [] const warnings: string[] = [] const pouNames: string[] = [] @@ -106,7 +103,7 @@ export function transpileToSt(project: TranspileProject, options: TranspileOptio } // Trailing CONFIGURATION block — emit IR-native. - for (const [text] of generateConfigurations(project, options)) { + for (const [text] of generateConfigurations(project)) { pieces.push(text) } diff --git a/src/backend/shared/transpilers/st-transpiler/walker/README.md b/src/backend/shared/transpilers/st-transpiler/walker/README.md index 18dba4d1d..d6f932e55 100644 --- a/src/backend/shared/transpilers/st-transpiler/walker/README.md +++ b/src/backend/shared/transpilers/st-transpiler/walker/README.md @@ -1,6 +1,6 @@ # walker -React Flow → Structured Text walker. Consumes the **React Flow** +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 @@ -13,16 +13,16 @@ VAR sections, the body bytes returned here, and the closing ## Files -- `ld.ts` — `emitLdBody(body: RFBody): EmitResult`. Handles both +- `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` + `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 + `RFNode`, `RFEdge`). Loose enough that the schema boundary in `../from-schema.ts` can project the editor's Zod-inferred shapes without typecasts. @@ -31,13 +31,13 @@ VAR sections, the body bytes returned here, and the closing 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 +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 +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/` From 6614f02dcb4be1672cec9979f37323ca6b6b5e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20GS=20Pereira?= Date: Mon, 8 Jun 2026 10:53:43 -0300 Subject: [PATCH 03/14] feat(versioning): shared app version + per-app product name Mirror of the openplc-web PR (same branch). Shared, byte-identical with web: - src/frontend/data/constants/app-version.ts: single source of truth for APP_VERSION (4.1.4), mirror-gated so web/editor never diverge. - about-modal: import APP_VERSION; title uses a per-app APP_NAME global. Editor-only: - webpack.app-info.ts: inject APP_NAME='OpenPLC Editor'; stop injecting APP_VERSION (about-modal imports it now). - globals.d.ts: declare APP_NAME, drop the unused APP_VERSION global. Co-Authored-By: Claude Opus 4.8 (1M context) --- configs/webpack/webpack.app-info.ts | 17 +++++++-------- .../_organisms/about-modal/index.tsx | 14 +++++++++---- src/frontend/data/constants/app-version.ts | 21 +++++++++++++++++++ src/globals.d.ts | 7 ++++--- 4 files changed, 43 insertions(+), 16 deletions(-) create mode 100644 src/frontend/data/constants/app-version.ts 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/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/data/constants/app-version.ts b/src/frontend/data/constants/app-version.ts new file mode 100644 index 000000000..7d5ffb984 --- /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.1.4' diff --git a/src/globals.d.ts b/src/globals.d.ts index a198f820c..b032eecce 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 From 5422420a0e138d9987dca2a0c87ac1ad8a8cb1e3 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Sun, 7 Jun 2026 21:03:10 -0400 Subject: [PATCH 04/14] fix(alias): enforce unique aliases at write time + cascade rename + clear orphaned locations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User-visible bugs in v4.2.0 (forum thread "openplc-420-teething-bugs"): #1 Variable picker shows the wrong alias label for the picked channel ("`btn_user (%IX0.1)`" appears after the user picks "`switch_1 (%IX0.1)`"). #5 After compile, every output BOOL collapses to `%QX0.0` in both the variables table and the emitted ST `AT %…` clauses. Both turned out to be the same family of bug rooted in three gaps in the alias architecture: Gap A — write-time alias-uniqueness was not enforced. `isAliasNameAvailable()` existed in `iec-address/alias-registry.ts` but neither IO-mapping screen (`module-slots-layout`, `io-table-layout`), the pin-mapping table, the EtherCAT channel editor, nor `updateIOPointAlias` (Modbus TCP) called it before persisting an alias edit. The address pool's silent first-wins on `byAlias` therefore silently dropped duplicate-alias claims; any variable later bound to the losing entry was collapsed to the winner's address by `syncVariableAliases`'s refresh path. Gap B — alias rename did not cascade to bound variables. `handleAliasChange` callsites wrote the new alias onto the channel and immediately called `syncVariableAliases()`. With the OLD alias gone from `byAlias`, every variable still carrying it took the orphan path instead of the rename path, silently dropping their address binding. Gap C — orphaned variables emitted stale `AT %…` to the compiler. `sync-variable-aliases.ts`'s orphan branch preserved both `variable.alias` and `variable.location`. The ST emitter (`generate-iec-variables-to-string.ts:55-57`) writes `AT ${v.location}` whenever non-empty, so orphans baked a no-longer-valid IEC address into the compiled program. ## Phase 1 — write-time uniqueness gate - New shared helpers in `middleware/shared/utils/iec-address`: * `validateAliasEdit(registry, alias, ignoring)` — wraps `isAliasNameAvailable` and returns the conflicting `AliasEntry` so callers can build a precise error message. Empty alias = OK (user clearing). No-op self-rename = OK. Cross-channel collision = reject + conflict entry. * `describeSource(source)` — human-readable rendering of a `SourceRef`: "VPP slot 2 channel O3", "Modbus device d1 point 17", etc. Centralised so every screen shows the same wording. - Wired into every producer's alias-edit path: * `module-slots-layout.tsx::handleAliasChange` * `io-table-layout.tsx::handleAliasChange` * `pin-mapping-table.tsx::handleUpdateDataRequest` (gates the cross-producer case; the in-board `checkIfPinAliasIsValid` in the slice still covers same-board uniqueness) * `use-device-configuration.ts::handleAliasChange` (EtherCAT) * `slice.ts::updateIOPointAlias` (Modbus TCP) Each builds a fresh pool + registry from the live store state, calls `validateAliasEdit` with the channel's `SourceRef`, and surfaces a `toast` (or a `ProjectResponse.ok: false` for `updateIOPointAlias`) on collision. - On project load, `syncVariableAliases` now also logs `registry.duplicateAliases` to the console (same shape as the existing `pool.conflicts` warning) so legacy projects that drifted into duplicate-alias state get a loud signal instead of silent collapse. ## Phase 2 — alias rename cascades to bound variables - New `projectActions.renameAlias(oldAlias, newAlias)` walks every POU's local + global variables and rewrites matching `variable.alias` (case-insensitive). Empty `newAlias` clears the alias on bound variables (so the subsequent sync re- evaluates them against the live registry). - Every `handleAliasChange` callsite now captures the OLD alias on the channel before writing, then calls `renameAlias(old, new)` BEFORE the channel mutation + `syncVariableAliases()`. The downstream sync sees variables already pointing at the new alias and takes the refresh path (location follows) instead of the orphan path (location cleared, warning glyph). ## Phase 3 — orphan clears `.location` - `sync-variable-aliases.ts`'s orphan branch now returns `{ ...variable, location: '' }`. `alias` is retained so the variable cell's `isOrphaned` predicate keeps firing and the amber warning glyph + tooltip still render. `report.orphaned[] .lastKnownAddress` carries the cleared address for the tooltip + an eventual undo affordance. - Now the ST emitter (`AT ${v.location}` is gated on `if (v.location)`) and the XML generator (same gating) cleanly omit the `AT %…` clause for orphans instead of baking a stale address into the compile. ## Phase 4 — pin-mapping vs VPP dropdown asymmetry documented - `location-dropdown-options.ts` and `remote-device-options.ts` now carry doc blocks explaining why pin-mapping options list every pin (stable hardware-fact addresses) while VPP / remote- device options list only aliased entries (allocator-assigned addresses can shift; alias is the only stable handle). Future devs shouldn't "fix" the inconsistency by filtering pins to aliased only — that would regress the "bind by raw %IX0.0" workflow. ## Phase 5 — dropdown defensive dedupe by address - `buildVendorIoOptionGroups` and `buildRemoteDeviceOptionGroups` now track an in-builder `seenAddresses` set and skip later entries that share an `iecAddress`. No-op for projects authored after Phase 1 (the pool enforces uniqueness at write time); guarantees that legacy projects that drifted before that gate existed can never surface two same-address options. Matches the pool's first-wins reservation order so the dropdown and the registry agree on which entry is canonical. ## Shared-surface compatibility Every code path that moves lives under `middleware/shared/` (the alias-registry helpers, the sync logic, the dropdown utilities) or in `frontend/` which is already part of the byte-identical shared surface between openplc-editor and openplc-web. The Modbus TCP gate lives inside `frontend/store/slices/project/slice.ts` which is likewise shared. A follow-up mirror PR on openplc-web will land the same diff byte-identically; no editor-only logic was introduced. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/pin-mapping-table.tsx | 56 ++++++++ .../vendor-screen/layouts/io-table-layout.tsx | 53 +++++++- .../layouts/module-slots-layout.tsx | 59 +++++++- .../hooks/use-device-configuration.ts | 66 ++++++++- .../sync-variable-aliases-action.test.ts | 9 +- src/frontend/store/slices/project/slice.ts | 128 ++++++++++++++++++ src/frontend/store/slices/project/types.ts | 23 ++++ .../__tests__/remote-device-options.test.ts | 33 ++++- .../utils/location-dropdown-options.ts | 23 ++++ src/frontend/utils/remote-device-options.ts | 30 ++++ .../__tests__/alias-registry.test.ts | 49 ++++++- .../__tests__/sync-variable-aliases.test.ts | 15 +- .../utils/iec-address/alias-registry.ts | 96 +++++++++++++ .../shared/utils/iec-address/index.ts | 3 + .../iec-address/sync-variable-aliases.ts | 20 ++- 15 files changed, 638 insertions(+), 25 deletions(-) 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/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__/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..718658309 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' @@ -765,6 +767,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 +822,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 +1497,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/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/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) { From fa11a1dcf6d1302618e4ea42493c8e49cffd4cc5 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 8 Jun 2026 07:10:16 -0400 Subject: [PATCH 05/14] =?UTF-8?q?fix(alias):=20enforce=20alias-=E2=86=94-l?= =?UTF-8?q?ocation=20invariant=20on=20createVariable=20+=20same-address=20?= =?UTF-8?q?re-pick?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to 8001308c9. User-visible symptom (forum thread, v4.2.0): in the Opta variables table, adding a new variable via the "+" button auto-increments the IEC location (e.g. %IX0.0 → %IX0.1) but carries the previous variable's alias forward. The new variable then has `.alias = "I1"` while `.location = "%IX0.1"` — the alias points at a different channel than the location. The next `syncVariableAliases` refresh sees the stored alias "I1" in `byAlias` and rewrites every such variable's location back to %IX0.0, producing the duplicate-address compile errors. The user can SEE the right alias for the new address in the cell's display (the auto-adopt path on `updateVariable` reads `byAddress`), but clicking that alias in the dropdown silently fails because the short-circuit in `editable-cell.tsx::onBlur` skips when the picked value matches the cell's current address — even when the variable's stored alias points at a different one. The workaround they discovered (pick a different alias, then pick the correct one) is this short-circuit's symptom: the second pick changes the address back, which is enough to satisfy the comparison and let `updateVariable` re-run. Three pieces of the fix: 1. **`createVariable` now auto-adopts the alias** for the post- increment location, just like `updateVariable` already does. Both actions delegate to a new `resolveAliasForLocation` helper in `slice.ts` that builds a fresh pool + registry from the live store state and returns the alias attached to the address. The helper is private to the slice (no public API surface change); it replaces the inline pool-build block previously embedded in `updateVariable`'s auto-adopt path, so the two actions stay in lockstep. 2. **Both `+` button callsites drop the stale alias explicitly** when building the new-row template. `variables-editor` and `global-variables-editor` both spread the previous variable to seed the new row's fields; now they explicitly set `alias: undefined` in the spread so the slice's auto-adopt path re-resolves against the new address. Defense-in-depth on top of piece (1) and a clearer signal of intent at the UI layer. 3. **`editable-cell` short-circuit grows a second exception.** Previously the location-column short-circuit was bypassed only when the variable's alias was *orphaned* (alias gone from the registry). Now it's also bypassed when the alias is *mismatched* — i.e. the alias still exists in the registry but points at a different address than the variable's current location. This lets users repair legacy projects (authored before piece (1) landed) with a single click instead of the alias-bounce workaround. Mirrored in `global-variables-table/editable-cell.tsx` so the global variables table behaves the same way. `isMismatched` is computed from the same `useAliasRegistry()` already imported in both cells — no additional dependencies. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../global-variables-table/editable-cell.tsx | 25 ++-- .../variables-table/editable-cell.tsx | 39 +++--- .../global-variables-editor/index.tsx | 6 +- .../_organisms/variables-editor/index.tsx | 9 ++ src/frontend/store/slices/project/slice.ts | 115 ++++++++++++------ 5 files changed, 133 insertions(+), 61 deletions(-) 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/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/store/slices/project/slice.ts b/src/frontend/store/slices/project/slice.ts index 718658309..56ad3c76f 100644 --- a/src/frontend/store/slices/project/slice.ts +++ b/src/frontend/store/slices/project/slice.ts @@ -171,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 // --------------------------------------------------------------------------- @@ -496,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) => { @@ -512,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 @@ -557,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) => { From 623a91cf7ac798539511dae46b4bf09c626e83f2 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 8 Jun 2026 08:02:03 -0400 Subject: [PATCH 06/14] fix(alias): walk past intervening claims when auto-incrementing IEC location MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to cbd87180c. Reported on the same v4.2.0 forum thread. With `I1..I5` aliases bound to `%IX0.0..%IX0.4` and an unrelated variable already at `%IX0.5`, the user keeps clicking the "+" button on the row at `%IX0.0`. Rows are created at `%IX0.1..%IX0.4` correctly (the alias propagation fix from cbd87180c holds), but the next click produces a fresh row at `%IX0.5` — colliding with the existing variable on that address and breaking compile. The cause is `createVariableValidation`'s auto-increment: when the incoming location collides with an existing variable, it parses that one variable's location, adds one (with the .7 → next-byte .0 wrap for BOOL), and returns. It never re-checks the *new* candidate against the rest of the table. Single-step increment is fine when only the seed row collides, but when contiguous rows of existing variables are in the way, the increment lands on the *next* taken slot and the collision is just shifted by one. Two changes in `validation/variables.ts`: 1. **Extract `incrementLocationByOne(location, typeValue)`** — pure helper that owns the per-type "increment by one slot" logic (BOOL bit/byte wrap, WORD/DWORD/LWORD numeric bump). Returns `null` for unrecognised IEC types so the loop can bail safely if some future type slips through the switch. The four duplicated case blocks in the previous implementation collapse into a single switch in the helper. 2. **`createVariableValidation` now loops the increment.** Builds an O(1) `Set` of existing variable locations, then walks `candidate = incrementLocationByOne(candidate, typeValue)` until the candidate is free. A `MAX_AUTO_INCREMENT_ITERATIONS = 8192` safety bound prevents runaway loops on pathologically dense tables or if `incrementLocationByOne` ever returns the same value (no-progress guard); 8192 is BOOLs × 1024 bytes which is well above any realistic IEC project size. Behaviour is unchanged when only the seed row collides (one iteration → one increment → free) so every existing test still passes. Tests in `project-validation-variables.test.ts`: - **walks past intervening claimed locations until it finds a free slot (BOOL)** — exact forum scenario. Rows at `%IX0.0..%IX0.4` plus an existing `%IX0.5`; "+" click at `%IX0.4` must land on `%IX0.6`. - **walks past multiple consecutive claimed locations (WORD)** — `%QW5`/`%QW6`/`%QW7` taken, click at `%QW5` must land on `%QW8`. - **wraps past a full BOOL byte when every bit and the next byte are taken** — `%QX0.0..%QX0.7` + `%QX1.0` claimed, click at `%QX0.0` must land on `%QX1.1`. Exercises the .7 → next-byte .0 wrap inside the walk loop. All 83 tests in the validator suite pass; the existing 80 cases still produce the same single-step output (one collision → one increment), so no behaviour change for the original single-conflict path. `tsc --noEmit` clean. ## Shared-surface note The change is confined to `frontend/store/slices/project/validation/ variables.ts` which is part of the byte-identical shared surface between openplc-editor and openplc-web. Follow-up mirror PR on openplc-web will land the same diff. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../project-validation-variables.test.ts | 48 +++++ .../slices/project/validation/variables.ts | 181 ++++++++++-------- 2 files changed, 152 insertions(+), 77 deletions(-) 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/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 } From 9c3b1cf7faea552fd862915826505e6e2c3e4c1c Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 8 Jun 2026 08:02:25 -0400 Subject: [PATCH 07/14] fix(graphical-editor): bind to exact-match variable on Enter instead of creating duplicate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported on the same v4.2.0 forum thread. In both the ladder and FBD editors: typing the exact name of an existing variable on a contact / coil / input-variable / output-variable element and hitting Return creates a *new* variable (auto-renamed by the validator to dodge the name collision) instead of binding the element to the variable the user clearly already had in mind. `GraphicalEditorAutocomplete`'s Enter / Tab handler had a hard rule: when no row was arrow-selected (`positionInArray === -1`), submit the synthetic "Add variable" option with `id: 'add'`. Both wrappers' `submit` callbacks treat `id === 'add'` as "create a new variable with `valueToSearch` as the name", so the wrappers had no opportunity to recover — by the time the wrapper runs, the "resolve to existing variable" intent is already lost. The fix lives in the atom so both wrappers benefit unchanged: - New `resolveImplicitSubmitOption()` runs first whenever Enter / Tab fires with no row selected (and from the imperative `triggerSubmit` used by parent components on blur, where the bug also reproduces). It checks `searchValue.trim()` against every name in the visible `variables` list, case-insensitive. On a match it returns the real variable's `{ id, name }`; only on no match does it fall through to the "Add variable" selectable. - The match runs against the parent's already-filtered list, so type-incompatible variables (e.g. an INT variable named `Var1` when the user is editing a BOOL contact) are correctly NOT auto-bound — they aren't in `variables` to begin with, so the search falls through to "Add variable" as it should and the validator's name-collision toast still signals "no, that variable doesn't fit here". Wrappers don't need changes: - **Ladder** (`_atoms/graphical-editor/ladder/autocomplete/ index.tsx`): `submit` sees `id !== 'add'` for the real match, falls past the `'add'` branch into `expandedVariables.find(... name.toLowerCase() === ...)` at line 268, finds it, calls `submitVariableToBlock(selectedVariable)`. Literal path (`getLiteralType('2.0')` etc.) still hits the `'add'` branch because numeric / boolean literals don't appear in `variables`, so the implicit-resolver falls through to "Add variable" and the literal handling is preserved. - **FBD** (`_atoms/graphical-editor/fbd/autocomplete/index.tsx`): same trace. Bonus: connector / continuation blocks (`canCreateNewVariable=false`) previously closed silently on Enter without a row selected; they now bind to an existing connector/continuation if the typed label matches one exactly. No regression — the `'newBlock'` "Add continuation" path stays gated to explicit arrow-selection, matching prior behaviour. `tsc --noEmit` clean. All 217 Jest suites / 4875 tests pass. The autocomplete atom has no existing test file in the codebase, so coverage continues through manual ladder + FBD testing. ## Shared-surface note The change is confined to `frontend/components/_atoms/graphical- editor/autocomplete/index.tsx`, part of the byte-identical shared surface between openplc-editor and openplc-web. Follow-up mirror PR on openplc-web will land the same diff. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../graphical-editor/autocomplete/index.tsx | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx b/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx index b402cc585..2d3d72291 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,7 @@ export const GraphicalEditorAutocomplete = forwardRef { switch (keyDown) { @@ -152,13 +179,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 { From 9570fe53086a01eac93d2e0632187dcdb18f23cc Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 8 Jun 2026 09:42:46 -0400 Subject: [PATCH 08/14] style(graphical-editor): prettier reflow of useImperativeHandle deps array CI Format Check on #850 caught the long useImperativeHandle deps array as a one-line breach of Prettier's printWidth. `prettier --write` reflows it across multiple lines. No semantic change. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../_atoms/graphical-editor/autocomplete/index.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx b/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx index 2d3d72291..b2cc87886 100644 --- a/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx +++ b/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx @@ -149,7 +149,16 @@ export const GraphicalEditorAutocomplete = forwardRef { switch (keyDown) { From df273e9d8fb0f4a98f27e5e77af9e71e2603bbba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20GS=20Pereira?= Date: Mon, 8 Jun 2026 11:19:05 -0300 Subject: [PATCH 09/14] chore(release): bump version to 4.2.1 Mirror of openplc-web. Match the latest release tag (v4.2.1): shared APP_VERSION + package.json (root and release/app) so electron-builder installer metadata and the displayed version agree. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- release/app/package.json | 2 +- src/frontend/data/constants/app-version.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1c7274e2d..1ace8e9a8 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.1", "license": "GPL-3.0", "author": { "name": "Autonomy Logic" diff --git a/release/app/package.json b/release/app/package.json index 76076e78d..64042f388 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.1", "description": "OpenPLC Editor - IDE capable of creating programs for the OpenPLC Runtime", "license": "MIT", "author": { diff --git a/src/frontend/data/constants/app-version.ts b/src/frontend/data/constants/app-version.ts index 7d5ffb984..f7ba5eecf 100644 --- a/src/frontend/data/constants/app-version.ts +++ b/src/frontend/data/constants/app-version.ts @@ -18,4 +18,4 @@ * 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.1.4' +export const APP_VERSION = '4.2.1' From cdb61e35b34eb6df0fefa6b1f0510d20bc6710ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20GS=20Pereira?= Date: Mon, 8 Jun 2026 11:35:32 -0300 Subject: [PATCH 10/14] fix(versioning): import APP_VERSION in catalog-browser, not the global Mirror of openplc-web. catalog-browser.tsx read APP_VERSION as a global injected by webpack DefinePlugin; this PR stopped injecting it (the shared About modal imports it now), so the bare runtime reads would throw ReferenceError when the package-manager compatibility check runs. Import APP_VERSION from the shared constant. Reported by CodeRabbit. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../editor/package-manager/catalog-browser.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 /** From 59c45283387bdcc8f21ee37b254a0a941ad80c16 Mon Sep 17 00:00:00 2001 From: Daniel Coutinho <60111446+dcoutinho1328@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:16:55 -0300 Subject: [PATCH 11/14] feat(transpiler): add OPENPLC_USE_NEW_TRANSPILER toggle, ship with legacy xml2st Restores the bundled xml2st subprocess transpile path so packaged builds keep using it while devs can opt into the new in-process JSON transpiler. Default is OFF (legacy xml2st). - binary-versions.json: re-pin xml2st@v4.0.5. - scripts/download-binaries.ts: restore the xml2st download + cache logic byte-for-byte from the pre-removal state. - compiler-module.ts: restore xml2stBinaryPath field, #constructXml2stBinaryPath, #executeXml2st, and handleTranspileXMLtoST verbatim; compileForDebugger branches on the toggle (legacy: handleGenerateXMLfromJSON + handleTranspileXMLtoST); rebind the handler on createEditorCompilerPlatformPort + createDesktopLibraryBuildPort. - editor-compiler-platform-port.transpileToSt: branches on the toggle; legacy serialises via XmlGenerator, materialises to plc.xml, runs handleTranspileXMLtoST, reads program.st back. - desktop-library-build-port.transpileToSt: same toggle; legacy reproduces the prior os.tmpdir() materialise / spawn / read sequence verbatim. - backend/editor/utils/transpiler-mode.ts: isNewTranspilerEnabled() reads OPENPLC_USE_NEW_TRANSPILER from process.env. Co-Authored-By: Claude Opus 4.7 (1M context) --- binary-versions.json | 6 +- scripts/download-binaries.ts | 237 ++++++++++++++++-- .../editor/compiler/compiler-module.ts | 140 +++++++++-- .../compiler/desktop-library-build-port.ts | 105 ++++++-- .../compiler/editor-compiler-platform-port.ts | 97 ++++--- src/backend/editor/utils/transpiler-mode.ts | 19 ++ 6 files changed, 517 insertions(+), 87 deletions(-) create mode 100644 src/backend/editor/utils/transpiler-mode.ts diff --git a/binary-versions.json b/binary-versions.json index 46c0e540d..eebbe5bfa 100644 --- a/binary-versions.json +++ b/binary-versions.json @@ -1,6 +1,10 @@ { + "xml2st": { + "version": "v4.0.5", + "repository": "Autonomy-Logic/xml2st" + }, "strucpp": { "version": "v0.5.5", "repository": "Autonomy-Logic/STruCpp" } -} +} \ No newline at end of file diff --git a/scripts/download-binaries.ts b/scripts/download-binaries.ts index f9504d48d..b0318c183 100644 --- a/scripts/download-binaries.ts +++ b/scripts/download-binaries.ts @@ -1,35 +1,115 @@ /** - * Download external tool binaries (strucpp) from GitHub Releases. + * Download external tool binaries (xml2st, strucpp) from GitHub Releases. * * Usage: - * ts-node scripts/download-binaries.ts [--force] + * ts-node scripts/download-binaries.ts [--platform ] [--arch ] [--force] * - * Use --force to re-install even if cached. - * - * The legacy `xml2st` binary download path was removed when the - * editor migrated to the in-process JSON → ST transpiler - * (`backend/shared/transpilers/generate-st-from-json/`). + * Defaults to the current platform/arch. Use --force to re-download even if cached. */ import { execSync } from 'child_process' import fs from 'fs' import path from 'path' +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + interface ToolEntry { version: string repository: string } interface BinaryVersions { + xml2st: ToolEntry strucpp: ToolEntry } +interface CacheMetadata { + xml2st: string + platform: string + arch: string +} + +type Platform = 'darwin' | 'linux' | 'win32' +type Arch = 'x64' | 'arm64' + +// --------------------------------------------------------------------------- +// Paths +// --------------------------------------------------------------------------- + const ROOT_DIR = path.resolve(__dirname, '..') const VERSIONS_FILE = path.join(ROOT_DIR, 'binary-versions.json') const RESOURCES_DIR = path.join(ROOT_DIR, 'resources') -function parseArgs(): { force: boolean } { - return { force: process.argv.slice(2).includes('--force') } +function binDir(platform: Platform, arch: Arch): string { + return path.join(RESOURCES_DIR, 'bin', platform, arch) +} + +function cacheFile(platform: Platform, arch: Arch): string { + return path.join(binDir(platform, arch), '.binary-metadata.json') +} + +// --------------------------------------------------------------------------- +// CLI argument parsing +// --------------------------------------------------------------------------- + +function parseArgs(): { platform: Platform; arch: Arch; force: boolean } { + const args = process.argv.slice(2) + let platform = process.platform as string + let arch = process.arch as string + let force = false + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--platform' && args[i + 1]) { + platform = args[++i] + } else if (args[i] === '--arch' && args[i + 1]) { + arch = args[++i] + } else if (args[i] === '--force') { + force = true + } + } + + if (!['darwin', 'linux', 'win32'].includes(platform)) { + console.error(`Unsupported platform: ${platform}`) + process.exit(1) + } + if (!['x64', 'arm64'].includes(arch)) { + console.error(`Unsupported architecture: ${arch}`) + process.exit(1) + } + + return { platform: platform as Platform, arch: arch as Arch, force } +} + +// --------------------------------------------------------------------------- +// Cache check +// --------------------------------------------------------------------------- + +function getCachedMetadata(platform: Platform, arch: Arch): CacheMetadata | null { + const file = cacheFile(platform, arch) + if (!fs.existsSync(file)) return null + + try { + return JSON.parse(fs.readFileSync(file, 'utf-8')) as CacheMetadata + } catch { + return null + } +} + +function needsXml2st(versions: BinaryVersions, cached: CacheMetadata | null, platform: Platform, arch: Arch): boolean { + const dir = binDir(platform, arch) + const isWindows = platform === 'win32' + const isDarwin = platform === 'darwin' + + const xml2stPath = isDarwin + ? path.join(dir, 'xml2st', 'xml2st') + : path.join(dir, isWindows ? 'xml2st.exe' : 'xml2st') + + if (!fs.existsSync(xml2stPath)) return true + if (!cached || cached.xml2st !== versions.xml2st.version) return true + + return false } function needsStrucpp(versions: BinaryVersions): boolean { @@ -46,9 +126,24 @@ function needsStrucpp(versions: BinaryVersions): boolean { } catch { return true } + return false } +function writeCache(versions: BinaryVersions, platform: Platform, arch: Arch): void { + const data: CacheMetadata = { + xml2st: versions.xml2st.version, + platform, + arch, + } + fs.mkdirSync(path.dirname(cacheFile(platform, arch)), { recursive: true }) + fs.writeFileSync(cacheFile(platform, arch), JSON.stringify(data, null, 2) + '\n') +} + +// --------------------------------------------------------------------------- +// Download helpers +// --------------------------------------------------------------------------- + async function downloadToFile(url: string, dest: string): Promise { const response = await fetch(url, { redirect: 'follow' }) if (!response.ok) { @@ -58,12 +153,101 @@ async function downloadToFile(url: string, dest: string): Promise { fs.writeFileSync(dest, new Uint8Array(arrayBuffer)) } +function extractTarGz(archive: string, destDir: string): void { + fs.mkdirSync(destDir, { recursive: true }) + execSync(`tar xzf "${archive}" -C "${destDir}"`, { stdio: 'pipe' }) +} + +function extractZip(archive: string, destDir: string): void { + fs.mkdirSync(destDir, { recursive: true }) + execSync(`tar xf "${archive}" -C "${destDir}"`, { stdio: 'pipe' }) +} + function rmrf(p: string): void { if (fs.existsSync(p)) { fs.rmSync(p, { recursive: true, force: true }) } } +function copyRecursive(src: string, dest: string): void { + fs.mkdirSync(dest, { recursive: true }) + for (const entry of fs.readdirSync(src, { withFileTypes: true })) { + const srcPath = path.join(src, entry.name) + const destPath = path.join(dest, entry.name) + if (entry.isSymbolicLink()) { + const linkTarget = fs.readlinkSync(srcPath) + if (fs.existsSync(destPath)) fs.rmSync(destPath, { force: true }) + fs.symlinkSync(linkTarget, destPath) + } else if (entry.isDirectory()) { + copyRecursive(srcPath, destPath) + } else { + fs.copyFileSync(srcPath, destPath) + } + } +} + +// --------------------------------------------------------------------------- +// xml2st download and extraction +// --------------------------------------------------------------------------- + +async function downloadXml2st( + tool: ToolEntry, + platform: Platform, + arch: Arch, + targetBinDir: string, +): Promise { + const isWindows = platform === 'win32' + const isDarwin = platform === 'darwin' + const ext = isWindows ? 'zip' : 'tar.gz' + const url = `https://github.com/${tool.repository}/releases/download/${tool.version}/xml2st-${platform}-${arch}.${ext}` + + console.log(` Downloading xml2st ${tool.version} for ${platform}-${arch}...`) + const tmpDir = fs.mkdtempSync(path.join(RESOURCES_DIR, '.tmp-xml2st-')) + + try { + const archivePath = path.join(tmpDir, `xml2st.${ext}`) + await downloadToFile(url, archivePath) + + const extractDir = path.join(tmpDir, 'extracted') + if (isWindows) { + extractZip(archivePath, extractDir) + } else { + extractTarGz(archivePath, extractDir) + } + + // Archive contains xml2st/ directory + const extractedToolDir = path.join(extractDir, 'xml2st') + + if (isDarwin) { + // macOS: xml2st is a directory with _internal/ — copy as-is + const destDir = path.join(targetBinDir, 'xml2st') + rmrf(destDir) + copyRecursive(extractedToolDir, destDir) + fs.chmodSync(path.join(destDir, 'xml2st'), 0o755) + } else { + // Linux/Windows: single executable + const exeName = isWindows ? 'xml2st.exe' : 'xml2st' + const srcFile = path.join(extractedToolDir, exeName) + const destFile = path.join(targetBinDir, exeName) + rmrf(destFile) + // Also remove any leftover directory from a previous macOS-style install + rmrf(path.join(targetBinDir, 'xml2st')) + fs.copyFileSync(srcFile, destFile) + if (!isWindows) { + fs.chmodSync(destFile, 0o755) + } + } + + console.log(` xml2st ${tool.version} installed.`) + } finally { + rmrf(tmpDir) + } +} + +// --------------------------------------------------------------------------- +// strucpp download and extraction +// --------------------------------------------------------------------------- + async function downloadStrucpp(tool: ToolEntry): Promise { // The npm tarball is platform-independent (pure TypeScript + C++ headers) const version = tool.version.replace(/^v/, '') @@ -100,8 +284,12 @@ async function downloadStrucpp(tool: ToolEntry): Promise { } } +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + async function main(): Promise { - const { force } = parseArgs() + const { platform, arch, force } = parseArgs() if (!fs.existsSync(VERSIONS_FILE)) { console.error(`binary-versions.json not found at ${VERSIONS_FILE}`) @@ -110,15 +298,34 @@ async function main(): Promise { const versions: BinaryVersions = JSON.parse(fs.readFileSync(VERSIONS_FILE, 'utf-8')) - console.log(`[download-binaries] force=${force}`) + console.log(`[download-binaries] platform=${platform} arch=${arch} force=${force}`) - if (!force && !needsStrucpp(versions)) { - console.log(` strucpp ${versions.strucpp.version} already installed, skipping.`) - console.log(`[download-binaries] Done.`) + const targetBinDir = binDir(platform, arch) + fs.mkdirSync(targetBinDir, { recursive: true }) + + const cached = force ? null : getCachedMetadata(platform, arch) + const downloadXml2stNeeded = force || needsXml2st(versions, cached, platform, arch) + const downloadStrucppNeeded = force || needsStrucpp(versions) + + if (!downloadXml2stNeeded && !downloadStrucppNeeded) { + console.log(`[download-binaries] All tools up to date, skipping.`) return } - await downloadStrucpp(versions.strucpp) + if (downloadXml2stNeeded) { + await downloadXml2st(versions.xml2st, platform, arch, targetBinDir) + } else { + console.log(` xml2st ${versions.xml2st.version} already installed, skipping.`) + } + + // strucpp is platform-independent — only download once regardless of platform/arch + if (downloadStrucppNeeded) { + await downloadStrucpp(versions.strucpp) + } else { + console.log(` strucpp ${versions.strucpp.version} already installed, skipping.`) + } + + writeCache(versions, platform, arch) console.log(`[download-binaries] Done.`) } diff --git a/src/backend/editor/compiler/compiler-module.ts b/src/backend/editor/compiler/compiler-module.ts index 8793d3045..3c55649b5 100644 --- a/src/backend/editor/compiler/compiler-module.ts +++ b/src/backend/editor/compiler/compiler-module.ts @@ -84,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' @@ -164,6 +165,8 @@ class CompilerModule { arduinoCliConfigurationFilePath: string arduinoCliBaseParameters: string[] + xml2stBinaryPath: string + strucppRuntimeDir: string // Memoised arduino-cli `--show-properties=expanded` output keyed by FQBN. @@ -218,6 +221,8 @@ class CompilerModule { // INFO: We use this approach because some commands can receive additional parameters as a string array. this.arduinoCliBaseParameters = ['--config-file', this.arduinoCliConfigurationFilePath] + this.xml2stBinaryPath = this.#constructXml2stBinaryPath() + this.strucppRuntimeDir = this.#constructStrucppRuntimeDir() } @@ -344,6 +349,10 @@ class CompilerModule { return join(this.binaryDirectoryPath, 'arduino-cli') } + #constructXml2stBinaryPath(): string { + return join(this.binaryDirectoryPath, 'xml2st', CompilerModule.HOST_PLATFORM === 'darwin' ? 'xml2st' : '') + } + #constructStrucppRuntimeDir(): string { // strucpp's runtime headers (`src/runtime/include/`) live in two // places depending on whether we're running dev or a packaged app: @@ -422,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 ================================= // ############################################################################ @@ -841,6 +858,46 @@ class CompilerModule { }) } + async handleTranspileXMLtoST( + generatedXMLFilePath: string, + handleOutputData: (chunk: Buffer | string, logLevel?: 'info' | 'error') => void, + extraXml2stArgs: readonly string[], + ) { + return new Promise>((resolve, reject) => { + // `extraXml2stArgs` comes from the shared pipeline's + // `TranspileXmlToStArgs.xml2stArgs` — the single source of truth + // for xml2st flag semantics across editor and web. Editor passes + // them through verbatim (trusted local binary); web's adapter + // filters against its known-args allowlist before sending to the + // compile-service. Strucpp targets currently pass + // `['--keep-structs']` (native STRUCT declarations vs matiec's + // legacy struct→FB rewrite); future flags appear here as the + // pipeline opts into them. + const executeCommand = this.#executeXml2st(['--generate-st', generatedXMLFilePath, ...extraXml2stArgs]) + + let stderrData = '' + + // INFO: We use the xml2st command to transpile the XML file to ST. + executeCommand.stdout?.on('data', (data: Buffer) => { + handleOutputData(data) + }) + executeCommand.stderr?.on('data', (data: Buffer) => { + stderrData += data.toString() + }) + + executeCommand.on('close', (code) => { + if (code === 0) { + handleOutputData(`ST file generated at: ${generatedXMLFilePath.replace('plc.xml', 'program.st')}`, 'info') + resolve({ + success: true, + }) + } else { + reject(new Error(`xml2st process exited with code ${code}\n${stderrData}`)) + } + }) + }) + } + async handleCompileSTtoCpp( sourceTargetFolderPath: string, handleOutputData: (chunk: Buffer | string, logLevel?: 'info' | 'error', compileError?: StrucppCompileError) => void, @@ -2545,6 +2602,7 @@ class CompilerModule { // --- Build the editor's CompilerPlatformPort implementation --- const platformPort = createEditorCompilerPlatformPort( { + handleTranspileXMLtoST: this.handleTranspileXMLtoST.bind(this), handleCompileArduinoProgram: this.handleCompileArduinoProgram.bind(this), handleUploadProgram: this.handleUploadProgram.bind(this), handleCoreInstallation: this.handleCoreInstallation.bind(this), @@ -2742,37 +2800,70 @@ class CompilerModule { return } - // JSON → ST in-process via `st-transpiler` — replaces the - // legacy XmlGenerator + xml2st-subprocess hop the program-compile - // pipeline already retired. 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' + 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: `${message}\nStopping debug compilation process.`, + message: `Error generating XML from JSON: ${error as string}\nStopping debug compilation process.`, }) _mainProcessPort.close() return } - for (const warning of result.warnings) { - _mainProcessPort.postMessage({ logLevel: 'info', message: warning }) + + 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 } - 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 } try { @@ -2925,6 +3016,7 @@ class CompilerModule { // glue the library build needs — every stage decision lives in // the shared orchestrator from here on. const libraryPort = createDesktopLibraryBuildPort({ + transpileXmlToSt: (xmlPath, log, extraArgs) => this.handleTranspileXMLtoST(xmlPath, log, extraArgs), loadEnabledArchives: (names) => mainProcessBridge.loadEnabledArchives(names), runVerificationCompile: ({ projectPath: p, verifyProjectData: v, emit }) => this.runVerificationCompile(p, v as PLCProjectData, mainProcessBridge, (message, logLevel) => diff --git a/src/backend/editor/compiler/desktop-library-build-port.ts b/src/backend/editor/compiler/desktop-library-build-port.ts index 442ae0e59..6e074e9d7 100644 --- a/src/backend/editor/compiler/desktop-library-build-port.ts +++ b/src/backend/editor/compiler/desktop-library-build-port.ts @@ -7,8 +7,10 @@ * `runLibraryBuildPipeline` cannot perform itself: * * - MD5 hashing (Node `crypto`) - * - in-process JSON → ST transpilation via - * `backend/shared/transpilers/st-transpiler/` + * - 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 @@ -21,16 +23,19 @@ * shared with the web port impl. */ -import { createHash } from 'node:crypto' +import { createHash, randomUUID } from 'node:crypto' import * as fs from 'node:fs/promises' +import * as os from 'node:os' import * as path from 'node:path' import { assertPathContained } from '@root/backend/editor/utils/path-containment' +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' @@ -40,6 +45,22 @@ import type { LibraryBuildPort } from '@root/middleware/shared/ports/library-bui * surface stays narrow + unit-testable. */ 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. + * + * Only invoked by the legacy transpile path (selected when + * `isNewTranspilerEnabled()` returns false). + */ + transpileXmlToSt( + xmlPath: string, + log: (chunk: Buffer | string, level?: 'info' | 'error') => void, + extraArgs: readonly string[], + ): Promise + /** * Resolve the names of project-enabled libraries to their parsed * `.stlib` archives. Bundled IEC standard set is included @@ -76,27 +97,73 @@ export function createDesktopLibraryBuildPort(deps: DesktopLibraryBuildPortDeps) args: TranspileToStArgs, log: (message: string, level: 'info' | 'warning' | 'error') => void, ): Promise { - 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') + 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' }] } } - for (const warning of result.warnings) { - log(warning, 'info') - } - return { ok: true, programSt: result.programSt } + } + + // 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` + // is written separately by the orchestrator via + // writeBuildFile — the intermediate here exists only for the + // subprocess. + const sessionDir = path.join(os.tmpdir(), `openplc-lib-xml2st-${randomUUID()}`) + try { + await fs.mkdir(sessionDir, { recursive: true }) + const xmlPath = path.join(sessionDir, 'plc.xml') + const programStPath = path.join(sessionDir, 'program.st') + await fs.writeFile(xmlPath, xmlResult.data, 'utf-8') + + await deps.transpileXmlToSt( + xmlPath, + (chunk, level) => log(typeof chunk === 'string' ? chunk : chunk.toString(), level ?? 'info'), + ['--keep-structs'], + ) + + const programSt = await fs.readFile(programStPath, 'utf-8') + return { ok: true, programSt } } catch (error) { const message = error instanceof Error ? error.message : String(error) - log(`transpile-from-json failed: ${message}`, 'error') + log(`xml2st failed: ${message}`, 'error') return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } + } finally { + // Best-effort cleanup. Leaks aren't fatal (os.tmpdir is + // the OS's responsibility) but tidying after ourselves + // keeps the dev disk clean. + await fs.rm(sessionDir, { recursive: true, force: true }).catch(() => { + /* swallow — the temp dir is the OS's to GC */ + }) } }, diff --git a/src/backend/editor/compiler/editor-compiler-platform-port.ts b/src/backend/editor/compiler/editor-compiler-platform-port.ts index b95cde697..fe2b39aeb 100644 --- a/src/backend/editor/compiler/editor-compiler-platform-port.ts +++ b/src/backend/editor/compiler/editor-compiler-platform-port.ts @@ -16,18 +16,21 @@ * - Translates the handler's return value back into the port's * canonical result shape * - * `transpileToSt` no longer needs disk materialisation or a - * subprocess: it projects the project IR via `fromSchemaShape` - * (editor IPC delivers schema-shape data) and runs the in-process - * JSON-fed transpiler - * (`backend/shared/transpilers/st-transpiler/`). The old - * `xml2st` binary path has been retired. + * `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 { @@ -35,6 +38,7 @@ import { 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, @@ -67,6 +71,7 @@ import type { CompilerModule } from './compiler-module' * file-watching, etc.). */ export interface EditorCompilerHandlers { + handleTranspileXMLtoST: CompilerModule['handleTranspileXMLtoST'] handleCompileArduinoProgram: CompilerModule['handleCompileArduinoProgram'] handleUploadProgram: CompilerModule['handleUploadProgram'] handleCoreInstallation: CompilerModule['handleCoreInstallation'] @@ -158,34 +163,70 @@ export function createEditorCompilerPlatformPort( }, /** - * Project the editor's port-shape data straight to Structured - * Text via the in-process JSON-fed transpiler. No subprocess, - * no disk round-trip — Electron's main process drives the - * walker directly. + * 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 transpileToSt(args: TranspileToStArgs, log: PlatformLog): Promise { - 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') + 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' }] } } - for (const warning of result.warnings) { - log(warning, 'info') - } - return { ok: true, programSt: result.programSt } + } + + 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, xmlResult.data, 'utf-8') + + await handlers.handleTranspileXMLtoST( + xmlPath, + (chunk, level) => { + const message = typeof chunk === 'string' ? chunk : chunk.toString() + log(message, level ?? 'info') + }, + ['--keep-structs'], + ) + + const programStPath = join(context.sourceTargetFolderPath, 'program.st') + const programSt = await fs.readFile(programStPath, 'utf-8') + return { ok: true, programSt } } catch (error) { const message = error instanceof Error ? error.message : String(error) - log(`st-transpiler failed: ${message}`, 'error') + log(`xml2st failed: ${message}`, 'error') return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } } }, 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' +} From 0838ddda3124dfd1496009a2659ef7e429c5f72d Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 8 Jun 2026 13:36:18 -0400 Subject: [PATCH 12/14] fix(transpiler): resolve polymorphic TO_ conversion functions to concrete output types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported by user on the desktop editor compiling a project that worked on web. STruC++ rejected the program with: main.st:15:30: error: Undefined type 'ANY' in PROGRAM 'MAIN' 15 | _TMP_TO_UINT874561_OUT : ANY; `ANY` is a generic placeholder in IEC 61131-3 — valid as a function argument type but never as a variable declaration type. STruC++ is correct to reject it. The bug is in the editor's JSON→ST transpiler (PR #843, recently landed): it was leaving synthetic output temps for polymorphic IEC type-conversion functions (`TO_INT`, `TO_UINT`, `TO_REAL`, …) at `ANY` instead of resolving them to their concrete destination types. The web editor still compiles this project because it uses xml2st in a worker (PR #482), not the new JSON→ST transpiler — different compiler, different code path. Once the web side also migrates to JSON→ST, this fix needs to be mirrored. ## Root cause `generateGraphicalPou` (emit/pou-graphical.ts:71) resolves `ANY` in synthetic vars by: 1. Looking for a user-defined project function with that name → use its `interface.returnType`. 2. Looking the name up in `data/std_block_catalog.json` via `resolveBlockType` → use the formal output port's type. 3. (Was missing) Fallback: leave as `ANY`. The catalog has entries for every *source-typed* conversion (`BOOL_TO_UINT`, `INT_TO_UINT`, `REAL_TO_UINT`, …) — but NOT for the polymorphic-input shortcuts (`TO_UINT`, `TO_INT`, …). IEC 61131-3 defines `TO_(in: ANY) → ` as a generic conversion whose output type is fixed by the function name, with the input type inferred from the connected source. The catalog's enumerate-every- combination approach doesn't capture this family; resolveBlockType returns null, the fallback kept `ANY`, strucpp rejected. ## Fix Adds a third resolution case after the catalog lookup fails: if `originBlockTypeName` matches `^TO_([A-Z]+)$` and the suffix is one of the 21 known IEC destination types (BOOL, INT, UINT, REAL, DINT, LREAL, BYTE, WORD, DWORD, LWORD, SINT, USINT, LINT, ULINT, UDINT, BCD, TIME, DATE, TOD, DT, STRING), use the suffix as the output type. Otherwise the existing fallback keeps `ANY` and strucpp reports the clear "Undefined type" error like before — no regression on truly-unknown names. The 21-entry allowlist is hard-coded in pou-graphical.ts with a comment pinning it to the catalog's `*_TO_` set; a comment notes that future catalog additions need to be reflected here too. This is intentionally more visible than deriving the set at runtime — the allowlist serves as documentation of which polymorphic conversions the transpiler claims to support. ## Why not a deeper fix A more principled solution would have the walker carry the block's own declared output type (which IS present in the project JSON — the FBD node's `variant.variables` lists OUT as `UINT` for this exact case) all the way through to synthetic-var emit, replacing the catalog round-trip entirely. That's a walker-side refactor (walker/ld.ts:687-700 currently only forwards typeName + numericId + formal parameter name). Out of scope for a targeted bug fix. ## Verification `tsc --noEmit` clean. No existing unit tests in the transpiler subtree — verification is via the user retesting the project that hit the bug (`/Users/thiagoralves/Downloads/Blink Demo Multilanguage`, which has two `TO_UINT` blocks at numericId 874561 and 9788614). Expected result: ST compiles cleanly, both temps now declared as `UINT` instead of `ANY`. ## Shared-surface mirror This file is part of the byte-identical shared surface between openplc-editor and openplc-web. A matching commit on the web side is required before either branch is merged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../st-transpiler/emit/pou-graphical.ts | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts index 5f68bd18c..a167781c2 100644 --- a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts @@ -26,6 +26,20 @@ interface InterfaceEntry { 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 ───────────────────────────────── */ /** @@ -68,6 +82,15 @@ export function generateGraphicalPou(pou: TranspilePou, project: TranspileProjec // …) 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) @@ -82,6 +105,10 @@ export function generateGraphicalPou(pou: TranspilePou, project: TranspileProjec 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 }) From 87d5ba58accaf8e40b679a4bd37ef40b55b3ef44 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 8 Jun 2026 15:23:35 -0400 Subject: [PATCH 13/14] style(transpiler): prettier reflow of TO_CONVERSION_TARGETS allowlist CI Format Check rejected the compact 3-per-line layout. `prettier --write` reflows the 21-element Set to one-per-line. No semantic change. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../st-transpiler/emit/pou-graphical.ts | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts index a167781c2..0540334e7 100644 --- a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts @@ -35,9 +35,27 @@ interface InterfaceEntry { * 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', + 'BCD', + 'BOOL', + 'BYTE', + 'DATE', + 'DINT', + 'DT', + 'DWORD', + 'INT', + 'LINT', + 'LREAL', + 'LWORD', + 'REAL', + 'SINT', + 'STRING', + 'TIME', + 'TOD', + 'UDINT', + 'UINT', + 'ULINT', + 'USINT', + 'WORD', ]) /* ─────────────────────────── public entry ───────────────────────────────── */ From 27f6387668c3595f1024fa93bbbfaf81f2201961 Mon Sep 17 00:00:00 2001 From: Thiago Alves Date: Mon, 8 Jun 2026 16:40:12 -0400 Subject: [PATCH 14/14] chore(release): bump version to 4.2.2 Mirror of openplc-web. Match the upcoming release tag (v4.2.2): shared APP_VERSION + package.json (root and release/app) so electron-builder installer metadata and the displayed version agree. Co-Authored-By: Claude Opus 4.7 (1M context) --- package.json | 2 +- release/app/package.json | 2 +- src/frontend/data/constants/app-version.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1ace8e9a8..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.2.1", + "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 64042f388..e2f51913f 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,6 +1,6 @@ { "name": "open-plc-editor", - "version": "4.2.1", + "version": "4.2.2", "description": "OpenPLC Editor - IDE capable of creating programs for the OpenPLC Runtime", "license": "MIT", "author": { diff --git a/src/frontend/data/constants/app-version.ts b/src/frontend/data/constants/app-version.ts index f7ba5eecf..4aa14620e 100644 --- a/src/frontend/data/constants/app-version.ts +++ b/src/frontend/data/constants/app-version.ts @@ -18,4 +18,4 @@ * 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.1' +export const APP_VERSION = '4.2.2'