diff --git a/.claude/review-guidelines.md b/.claude/review-guidelines.md new file mode 100644 index 000000000..9cb8ce451 --- /dev/null +++ b/.claude/review-guidelines.md @@ -0,0 +1,11 @@ +# Review Guidelines + +Order of concern: correctness, architecture, tests, style. + +- Layer dependencies respect ports and adapters: frontend, backend, and middleware only communicate through the ports in `src/middleware/shared/ports/`. `npm run validate:arch` must pass. +- DTOs cross layer boundaries, never domain entities. +- 100%-coverage directories stay at 100%; new behavior comes with tests (Jest for logic, Playwright for user flows). +- TypeScript Best Practices in CLAUDE.md apply to every diff (no `any`, no `as`, no `!`, no floating promises, boundary validation over casting). +- Named exports over default exports. +- No emojis in code, comments, or docs. +- Docs updated when documented behavior changes (README, CLAUDE.md, docs/). diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..c19b49e15 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,19 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run:*)", + "Bash(npm test:*)", + "Bash(npx tsx:*)", + "Bash(npx playwright test:*)", + "Bash(git status:*)", + "Bash(git diff:*)", + "Bash(git log:*)", + "Bash(git show:*)", + "Bash(git branch:*)", + "Bash(gh pr view:*)", + "Bash(gh pr list:*)", + "Bash(gh issue view:*)", + "Bash(gh issue list:*)" + ] + } +} diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index eb3559a83..987214821 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -39,12 +39,6 @@ else echo " WARNING: matiec binary not found at $BIN_DIR/iec2c" fi -if [ -f "$BIN_DIR/xml2st" ]; then - echo " xml2st binary ($NODE_ARCH): OK" -else - echo " WARNING: xml2st binary not found at $BIN_DIR/xml2st" -fi - # Fix Electron sandbox for container (chrome-sandbox needs SUID root + mode 4755) CHROME_SANDBOX="node_modules/electron/dist/chrome-sandbox" if [ -f "$CHROME_SANDBOX" ]; then diff --git a/.gitattributes b/.gitattributes index a9cb934ad..1aa57117b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -15,5 +15,4 @@ # Identification for binary files used as compilers/transpilers ########################## iec2c binary -xml2st binary resources/bin/**/* binary \ No newline at end of file diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 63f9ce50b..4a8f9734d 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -20,7 +20,7 @@ jobs: - name: Install dependencies run: npm ci --ignore-scripts - - name: Download strucpp + xml2st binaries + - name: Download strucpp binaries # tsc needs the strucpp package to resolve the types imported # from `backend/shared/library/strucpp-runtime.ts` and friends. # Run the binary downloader directly (skipping the rest of diff --git a/.github/workflows/ci-unit-tests.yml b/.github/workflows/ci-unit-tests.yml index c19cebca2..ef15d6f76 100644 --- a/.github/workflows/ci-unit-tests.yml +++ b/.github/workflows/ci-unit-tests.yml @@ -23,7 +23,7 @@ jobs: # --ignore-scripts skips the postinstall, so strucpp (a GitHub-release # package, not an npm dep) is never fetched — yet the TS sources import # `strucpp/libs/iec-types.json` and its runtime headers. Install just - # strucpp (no xml2st platform binary, no native rebuild) so the suites + # strucpp (no native rebuild) so the suites # can load and the coverage gate actually runs. - name: Install strucpp run: npm run setup:strucpp diff --git a/CLAUDE.md b/CLAUDE.md index 516aa0764..3226b1aac 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,7 @@ src/ ├── main/ # Electron main process (Node.js) ├── frontend/ # React UI layer (renderer process) │ ├── components/ # Atomic Design: _atoms, _molecules, _organisms, _features, _templates -│ ├── store/ # Zustand store (19 slices) +│ ├── store/ # Zustand store (18 slices) │ ├── hooks/ # Custom React hooks │ ├── services/ # Business logic and side effects │ ├── utils/ # Domain utilities (PLC, graphical, debug, formatters) @@ -47,7 +47,7 @@ src/ │ ├── locales/ # i18next translations │ └── assets/ # Images, icons ├── backend/ -│ ├── editor/ # Main process modules (compiler, hardware, modbus, websocket, services) +│ ├── editor/ # Main process modules (compiler, hardware, modbus, ethercat, library-manager, services) │ └── shared/ # Platform-agnostic utilities (XML generation, project parsing, simulator) ├── middleware/ # Ports & Adapters layer │ ├── shared/ @@ -131,7 +131,7 @@ Main and renderer processes communicate through typed IPC bridges: ### State Management (Zustand) -Single store composed of 19 slices (`src/frontend/store/`), accessed via auto-generated selector hooks: +Single store composed of 18 slices (`src/frontend/store/`), accessed via auto-generated selector hooks: ```typescript import { useOpenPLCStore } from '@root/frontend/store' @@ -160,7 +160,7 @@ const createPou = useOpenPLCStore((s) => s.projectActions.createPou) | `console` | Log output | | `library` | System + user function block libraries | | `file` | File save states (dirty tracking) | -| `ai`, `clipboard`, `history`, `modal`, `search`, `shared`, `version-control`, `webrtc` | Supporting features | +| `ai`, `history`, `modal`, `readme`, `search`, `shared`, `version-control`, `webrtc` | Supporting features | **Conventions:** - Actions are grouped under a `*Actions` namespace (e.g., `projectActions`, `deviceActions`) @@ -205,14 +205,19 @@ Flow state is stored per-POU in dedicated slices (`ladder`, `fbd`). Flows must b Orchestrated by `CompilerModule` (`src/backend/editor/compiler/compiler-module.ts`): ``` -PLCProjectData -> Preprocess POUs -> XML Generation -> xml2st -> iec2c -> C code +PLCProjectData -> Preprocess POUs -> ST transpiler (in-process) -> strucpp -> C++ code | defines.h (pins, Modbus, MD5) | Arduino CLI / openplc-compiler -> firmware ``` -Platform-specific binaries in `/resources/bin/[platform]/[arch]/`. Board configs in `/resources/sources/boards/hals.json`. +Structured Text is generated in-process by the TS transpiler +(`src/backend/shared/transpilers/st-transpiler/`) on both editor and web — the +legacy `xml2st` binary path has been retired. `XmlGenerator` is kept only for +the "Export Project as XML" feature. + +Platform-specific binaries in `/resources/bin/[platform]/[arch]/`. Board configs in `src/backend/shared/firmware/hals.json`. ### Debugging @@ -241,9 +246,22 @@ When adding new code to covered directories, you must add corresponding tests to - ESLint flat config (`eslint.config.mjs`) with TypeScript strict type checking - Prettier: 120 char width, no semicolons, single quotes, trailing commas - Import sorting enforced via `simple-import-sort` plugin -- Pre-commit hooks via Husky run lint-staged on `./src/**/*` +- Pre-commit hooks via Husky run lint-staged on `./src/**/*.{ts,tsx}` - Path alias: `@root/*` -> `./src/*` +### TypeScript Best Practices + +- No type assertions: `as` hides real type errors — fix the type at the source or narrow with type guards. `as const` is fine; `as unknown as T` is forbidden. +- No non-null assertions (`!`): handle the undefined case or narrow explicitly. +- For truly unknown data use `unknown` and narrow before use — never `any`. +- No `@ts-ignore`/`@ts-expect-error` without a one-line justification. +- Validate external data at the boundary (IPC payloads, project files, downloaded binaries metadata) with schemas (zod) or type guards instead of casting. +- No floating promises: `await` or handle rejection explicitly — async errors must not disappear. +- Prefer `??` over `||` for defaults when `0`, `''`, or `false` are valid values. +- Model variant states as discriminated unions; make `switch` exhaustive with a `never` check. +- Named exports over default exports. +- Zustand state changes only through slice actions — never mutate store values from components. + ## Key Technologies - **Electron 35** / **React 18** / **TypeScript** (target ES2022) @@ -311,7 +329,7 @@ on its `main` push. (Ideally `package.json.version` should be derived from ### IEC address allocation + alias registry -Located in `src/backend/shared/utils/iec-address/` (byte-identical on +Located in `src/middleware/shared/utils/iec-address/` (byte-identical on openplc-web). Pure functions, no IPC, no electron coupling. - **Address pool** (`address-pool.ts`): producer-only, target-scoped @@ -353,7 +371,15 @@ new alias. ## Environment -- **Node.js:** >= 20.x < 24 +- **Node.js:** >= 22.x < 24 - **Dev server port:** 1313 - **Supported platforms:** macOS, Windows, Linux (x64 & ARM64) - **Binaries:** Auto-downloaded via `scripts/download-binaries.ts` during `npm install` + +## Git Workflow + +Follow the Workflow section in CONTRIBUTING.md (base branch, branch naming, Conventional Commits). `` maps from the Jira issue type: Story → `feature`, Bug → `bugfix`, Task → `task`, Improvement → `improvement`. + +## Issue Tracker + +Jira, project key `DOPE`. Fetch and update tickets via the Atlassian MCP tools. Reference the ticket key (`DOPE-`) in branch names and PR descriptions. GitHub Issues (`.github/ISSUE_TEMPLATE/`) receives external bug reports; planned work lives in Jira. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e69de29bb..95d7883fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing + +## Setup + +Requires Node.js >= 22 < 24. See README.md for the full step by step. + +```bash +npm install +npm run dev # dev server on port 1313 +``` + +## Workflow + +1. Internal work is tracked in Jira, project DOPE (internal tracker). External contributors: open a GitHub issue using the provided templates. +2. Branch from `development`, named `/DOPE--` (``: feature, bugfix, task, improvement). Maintenance without a ticket uses `chore/`, `ci/`, `docs/`. External contributors without Jira access: use the GitHub issue number instead (`/gh--`); a maintainer files the DOPE ticket when needed. +3. Commit style: Conventional Commits, concise, focused on why. +4. Open a PR targeting `development` and fill in the PR template. + +## Before pushing + +```bash +npm run test # unit tests (Jest, with coverage) +npm run test:e2e # end-to-end (Playwright, requires a build) +npm run validate:arch # architecture layer dependencies +``` + +Lint and format run on commit via Husky. + +## Docs + +If your change alters documented behavior (commands, endpoints, env vars, architecture, setup steps), update the affected docs (README, CLAUDE.md, docs/) in the same PR. + +## Review + +PRs are reviewed against `.claude/review-guidelines.md`. diff --git a/README.md b/README.md index 8861e05c6..fa6e00a9e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ In order to run the development version, clone the repository, and install depen You'll need the following tools: - [Git](https://git-scm.com/) -- [NodeJS](https://nodejs.org/en/download/), version `>=20 <24` +- [NodeJS](https://nodejs.org/en/download/), version `>=22 <24` ### Step by step @@ -38,6 +38,14 @@ npm install npm run dev ``` +### Running tests + +```bash +npm run test # Unit tests (Jest, with coverage) +npm run test:watch # Unit tests in watch mode (no coverage) +npm run test:e2e # End-to-end tests (Playwright, builds the app first) +``` + ## Releasing a New Version The project uses GitHub Actions to automatically build and release new versions for all supported platforms (macOS, Windows, and Linux) in both x64 and ARM64 architectures. diff --git a/binary-versions.json b/binary-versions.json index a0d6978b0..5539337ec 100644 --- a/binary-versions.json +++ b/binary-versions.json @@ -1,10 +1,6 @@ { - "xml2st": { - "version": "v4.0.7", - "repository": "Autonomy-Logic/xml2st" - }, "strucpp": { - "version": "v0.5.13", + "version": "v0.6.0", "repository": "Autonomy-Logic/STruCpp" } } diff --git a/docs/debugger-scalability-analysis.md b/docs/debugger-scalability-analysis.md index 5e7338c71..e668bccf1 100644 --- a/docs/debugger-scalability-analysis.md +++ b/docs/debugger-scalability-analysis.md @@ -1193,9 +1193,9 @@ Implement **Strategy F** (lazy registration) only if needed for extreme-scale pr ### openplc-editor | File | Changes | |------|---------| -| `src/main/modules/compiler/compiler-module.ts` | Pass target platform to xml2st | -| `src/utils/debug-parser.ts` | Support JSON manifest parsing | -| `src/renderer/utils/debugger-session.ts` | Array index handling, protocol v2 | -| `src/main/modules/modbus/modbus-client.ts` | Array element request method | -| `src/main/modules/websocket/websocket-debug-client.ts` | Array element request method | -| `src/renderer/screens/workspace-screen.tsx` | Array polling in debug loop | +| `src/backend/editor/compiler/compiler-module.ts` | Pass target platform to xml2st | +| `src/frontend/utils/debug-parser.ts` | Support JSON manifest parsing | +| `src/frontend/utils/debugger-session.ts` | Array index handling, protocol v2 | +| `src/backend/editor/modbus/modbus-client.ts` | Array element request method | +| `src/backend/shared/debug/websocket-debug-transport.ts` | Array element request method | +| `src/frontend/screens/workspace-screen.tsx` | Array polling in debug loop | diff --git a/docs/ethercat-architecture.md b/docs/ethercat-architecture.md index e9e66e4e2..dcd09d423 100644 --- a/docs/ethercat-architecture.md +++ b/docs/ethercat-architecture.md @@ -27,7 +27,7 @@ Network scan → Match against repo → Add to project → Configure → Generat ## 2. Type System -### Core Types (`src/types/ethercat/esi-types.ts`) +### Core Types (`src/middleware/shared/ports/esi-types.ts`) | Type | Purpose | |------|---------| @@ -42,7 +42,7 @@ Network scan → Match against repo → Add to project → Configure → Generat | `ESIDeviceRef` | Pointer to a device in the repository (`repositoryItemId` + `deviceIndex`) | | `ScannedDeviceMatch` | A scanned device paired with its ESI matches (exact/partial/none) | -### Discovery Types (`src/types/ethercat/index.ts`) +### Discovery Types (`src/middleware/shared/ports/ethercat-types.ts`) | Type | Purpose | |------|---------| @@ -51,7 +51,7 @@ Network scan → Match against repo → Add to project → Configure → Generat | `NetworkInterface` | Adapter for scanning (name, description) | | `EtherCATRuntimeStatusResponse` | Runtime state machine (masters/slaves with states, WKC) | -### Project Persistence (`src/types/PLC/open-plc.ts`) +### Project Persistence (`src/backend/shared/types/PLC/open-plc.ts`) ```typescript PLCRemoteDevice { @@ -61,7 +61,7 @@ PLCRemoteDevice { } EthercatConfig { - masterConfig?: EtherCATMasterConfig // interface, cycleTimeUs, watchdogTimeoutCycles + masterConfig?: EtherCATMasterConfig // networkInterface, cycleTimeUs, watchdogTimeoutCycles, taskPriority devices: ConfiguredEtherCATDevice[] } ``` @@ -82,7 +82,7 @@ Stored in `project.json` under `data.remoteDevices[]`. └── ... ``` -### ESI Service (`src/main/services/esi-service/index.ts`) +### ESI Service (`src/backend/editor/ethercat/esi-service.ts`) Runs in the **main process**. Key methods: @@ -95,7 +95,7 @@ Runs in the **main process**. Key methods: | `clearRepository(projectPath)` | Delete all ESI files and index | | `migrateRepositoryToV2(projectPath)` | Convert v1 (metadata-only) → v2 (with summaries) | -### ESI Parser (`src/main/services/esi-service/esi-parser-main.ts`) +### ESI Parser (`src/backend/shared/ethercat/esi-parser-main.ts`) Two parsing modes: @@ -132,36 +132,19 @@ Defined in `src/main/modules/ipc/main.ts` (handlers) and `src/main/modules/ipc/r ## 4. Remote Device (Bus) Creation -### Store Action (`src/renderer/store/slices/project/slice.ts`) +### Store Action (`src/frontend/store/slices/project/slice.ts`) `createRemoteDevice(device: PLCRemoteDevice)`: -1. Validates name doesn't conflict with POUs/datatypes +1. Validates the name is not already used by another remote device 2. Pushes to `project.data.remoteDevices[]` -3. **Auto-creates a system task** for EtherCAT: -```typescript -{ - name: "EtherCAT_", // ethercatTaskName() - triggering: 'Cyclic', - interval: "T#1ms", // cycleTimeUsToIecInterval(1000) - priority: 1, - isSystemTask: true, - associatedDevice: deviceName, -} -``` +No IEC task is created: the EtherCAT bus is driven by a dedicated thread inside the +runtime plugin. Bus timing lives entirely in `masterConfig` (`cycleTimeUs`, `taskPriority`). Related actions: -- `deleteRemoteDevice(name)` — removes device + associated system task -- `updateRemoteDeviceName(oldName, newName)` — renames both device and task -- `updateEthercatConfig(deviceName, ethercatConfig)` — updates config and **syncs cycle time to task interval** - -### Task Helpers (`src/utils/ethercat/ethercat-task-helpers.ts`) - -| Function | Output | -|----------|--------| -| `ethercatTaskName("Master1")` | `"EtherCAT_Master1"` | -| `cycleTimeUsToIecInterval(1000)` | `"T#1ms"` | -| `cycleTimeUsToIecInterval(500)` | `"T#500us"` | +- `deleteRemoteDevice(name)` — removes the device +- `updateRemoteDeviceName(oldName, newName)` — renames the device +- `updateEthercatConfig(deviceName, ethercatConfig)` — replaces the device's EtherCAT config --- @@ -209,7 +192,7 @@ window.bridge.etherCATScan(ipAddress, jwtToken, { interface, timeout_ms }) ## 6. Device Matching -**File:** `src/utils/ethercat/device-matcher.ts` +**File:** `src/backend/shared/ethercat/device-matcher.ts` `matchDevicesToRepository(scannedDevices, repository)` → `ScannedDeviceMatch[]` @@ -256,7 +239,7 @@ Two paths: - Assigns next available position - Calls `syncDevicesToStore()` -### Device Enrichment (`src/utils/ethercat/enrich-device-data.ts`) +### Device Enrichment (`src/backend/shared/ethercat/enrich-device-data.ts`) `enrichDeviceData(esiDevice)` extracts fields spread into `ConfiguredEtherCATDevice`: @@ -267,7 +250,7 @@ Two paths: | `slaveType: string` | `deriveSlaveType()` — heuristic: `digital_input`, `analog_output`, `coupler`, etc. | | `sdoConfigurations: SDOConfigurationEntry[]` | `extractDefaultSdoConfigurations()` — RW objects in 0x2000+ range | -### Default Slave Config (`src/utils/ethercat/device-config-defaults.ts`) +### Default Slave Config (`src/backend/shared/ethercat/device-config-defaults.ts`) `createDefaultSlaveConfig()` returns: ```typescript @@ -287,7 +270,7 @@ Two paths: ### Editor Components ``` -src/renderer/components/_features/[workspace]/editor/device/ethercat/ +src/frontend/components/_features/[workspace]/editor/device/ethercat/ ├── index.tsx # Bus-level editor (3 tabs: Network, Repository, Advanced) ├── ethercat-device-editor.tsx # Per-device editor (4 tabs below) └── components/ @@ -313,7 +296,7 @@ src/renderer/components/_features/[workspace]/editor/device/ethercat/ 3. **Startup Parameters** — `SdoParametersSection`: editable CoE SDO entries 4. **Channel Mappings** — `ChannelMappingsSection`: IEC 61131-3 located variable assignments -### Channel Mapping Utilities (`src/utils/ethercat/esi-parser.ts`) +### Channel Mapping Utilities (`src/backend/shared/ethercat/esi-parser.ts`) | Function | Purpose | |----------|---------| @@ -330,7 +313,7 @@ IEC location format: Example: BOOL input at byte 0, bit 2 → %IX0.2 ``` -### SDO Extraction (`src/utils/ethercat/sdo-config-defaults.ts`) +### SDO Extraction (`src/backend/shared/ethercat/sdo-config-defaults.ts`) `extractDefaultSdoConfigurations(coeObjects)` → `SDOConfigurationEntry[]` @@ -338,7 +321,7 @@ IEC location format: - Excludes system objects (0x0000–0x1FFF) - For complex objects: extracts RW sub-items with default values -### Device Configuration Hook (`src/renderer/hooks/use-device-configuration.ts`) +### Device Configuration Hook (`src/frontend/hooks/use-device-configuration.ts`) `useDeviceConfiguration({ device, projectPath, ... })` provides: - Lazy-loads full ESI device on first render @@ -352,12 +335,12 @@ IEC location format: ### Zustand Store Slices -The EtherCAT state lives primarily in the **Project Slice** (`src/renderer/store/slices/project/slice.ts`): +The EtherCAT state lives primarily in the **Project Slice** (`src/frontend/store/slices/project/slice.ts`): ``` project.data.remoteDevices[] → PLCRemoteDevice[] └── ethercatConfig - ├── masterConfig: { networkInterface, cycleTimeUs, watchdogTimeoutCycles } + ├── masterConfig: { networkInterface, cycleTimeUs, watchdogTimeoutCycles, taskPriority } └── devices: ConfiguredEtherCATDevice[] ``` @@ -365,16 +348,16 @@ Key actions on the project slice: | Action | Purpose | |--------|---------| -| `createRemoteDevice()` | Add bus + auto-create system task | -| `deleteRemoteDevice()` | Remove bus + delete system task | -| `updateEthercatConfig()` | Update master config and/or device list, sync task interval | -| `updateRemoteDeviceName()` | Rename bus + associated task | +| `createRemoteDevice()` | Add bus | +| `deleteRemoteDevice()` | Remove bus | +| `updateEthercatConfig()` | Update master config and/or device list | +| `updateRemoteDeviceName()` | Rename bus | The **Editor Slice** manages the active editor model: - Bus editor: `type: 'plc-remote-device'`, `meta: { name, protocol: 'ethercat' }` - Device editor: `type: 'plc-ethercat-device'`, `meta: { name, busName, deviceId }` -### EtherCAT Editor State (`src/renderer/components/.../ethercat/index.tsx`) +### EtherCAT Editor State (`src/frontend/components/.../ethercat/index.tsx`) Local component state in `EtherCATEditor`: @@ -395,7 +378,7 @@ masterConfig = remoteDevice.ethercatConfig.masterConfig ## 10. JSON Configuration Generation for Runtime -### Generator (`src/utils/ethercat/generate-ethercat-config.ts`) +### Generator (`src/backend/shared/ethercat/generate-ethercat-config.ts`) `generateEthercatConfig(remoteDevices[])` → JSON string or `null` @@ -410,8 +393,7 @@ interface RuntimeRootEntry { interface: string // "eth0" cycle_time_us: number watchdog_timeout_cycles: number - task_name?: string // "EtherCAT_Master1" - task_cycle_time_us?: number + task_priority?: number // SCHED_FIFO priority of the bus thread (default 90) } slaves: RuntimeSlave[] diagnostics: { @@ -452,14 +434,13 @@ Channel type derivation: `deriveChannelType(direction, bitLen)` → `"digital_in SDO value parsing: `parseNumericValue(str)` handles decimal, hex (`0xFF`, `#xFF`), float, negative. -### Compiler Integration (`src/main/modules/compiler/compiler-module.ts`) +### Compiler Integration (`src/backend/shared/compile/steps/generate-confs.ts`) -`handleGenerateEthercatConfig(sourceTargetFolderPath, projectData, handleOutputData)`: +`generateRuntimeConfs()`: -1. Calls `generateEthercatConfig(projectData.remoteDevices)` -2. Creates `conf/` directory in firmware build folder -3. Writes `conf/ethercat.json` -4. Part of the larger build pipeline alongside `modbus-master.json`, `s7comm.json`, `opcua.json` +1. Calls `generateEthercatConfig(remoteDevices)` and validates the result with `validateEthercatConfig()` +2. Returns the JSON alongside the other runtime configs (Modbus slave/master, S7comm, OPC UA) +3. The bundle composer (`src/middleware/shared/utils/library/compose-runtime-v4-bundle.ts`) writes it to `conf/ethercat.json`, next to `conf/modbus_master.json`, `conf/s7comm.json`, `conf/opcua.json` --- @@ -468,7 +449,7 @@ SDO value parsing: `parseNumericValue(str)` handles decimal, hex (`0xFF`, `#xFF` ``` 1. CREATE BUS UI: Add Remote Device → protocol: ethercat - Store: createRemoteDevice() → remoteDevices[] + system task + Store: createRemoteDevice() → remoteDevices[] 2. UPLOAD ESI FILES UI: Repository tab → drag & drop XML @@ -487,7 +468,7 @@ SDO value parsing: `parseNumericValue(str)` handles decimal, hex (`0xFF`, `#xFF` 4. ENRICH & STORE IPC: esiLoadDeviceFull() → parseESIDeviceFull() Util: enrichDeviceData() → channelInfo, PDOs, slaveType, SDOs - Store: updateEthercatConfig() → devices[] + sync task interval + Store: updateEthercatConfig() → devices[] 5. CONFIGURE DEVICE UI: Click device in tree → EtherCATDeviceEditor @@ -507,59 +488,58 @@ SDO value parsing: `parseNumericValue(str)` handles decimal, hex (`0xFF`, `#xFF` ### Types | File | Contents | |------|----------| -| `src/types/ethercat/esi-types.ts` | ESIDevice, ConfiguredEtherCATDevice, channels, PDOs, SDOs | -| `src/types/ethercat/index.ts` | EtherCATDevice, scan/status responses, NetworkInterface | -| `src/types/PLC/open-plc.ts` | Zod schemas: EthercatConfig, EtherCATMasterConfig, PLCRemoteDevice | +| `src/middleware/shared/ports/esi-types.ts` | ESIDevice, ConfiguredEtherCATDevice, channels, PDOs, SDOs | +| `src/middleware/shared/ports/ethercat-types.ts` | EtherCATDevice, scan/status responses, NetworkInterface | +| `src/backend/shared/types/PLC/open-plc.ts` | Zod schemas: EthercatConfig, EtherCATMasterConfig, PLCRemoteDevice | ### Main Process | File | Contents | |------|----------| -| `src/main/services/esi-service/index.ts` | ESI file persistence and repository management | -| `src/main/services/esi-service/esi-parser-main.ts` | XML parsing (light and full modes) | +| `src/backend/editor/ethercat/esi-service.ts` | ESI file persistence and repository management | +| `src/backend/shared/ethercat/esi-parser-main.ts` | XML parsing (light and full modes) | | `src/main/modules/ipc/main.ts` | IPC handlers for scan, status, ESI operations | | `src/main/modules/ipc/renderer.ts` | Renderer-side IPC bridge (`window.bridge.*`) | -| `src/main/modules/compiler/compiler-module.ts` | Firmware build: writes `conf/ethercat.json` | +| `src/backend/shared/compile/steps/generate-confs.ts` | Build pipeline: generates the `conf/ethercat.json` payload | -### Renderer — Utilities +### Shared Utilities | File | Contents | |------|----------| -| `src/utils/ethercat/device-matcher.ts` | Match scanned devices against ESI repository | -| `src/utils/ethercat/enrich-device-data.ts` | Extract persistable data from full ESI device | -| `src/utils/ethercat/esi-parser.ts` | pdoToChannels, generateIecLocation, default mappings | -| `src/utils/ethercat/device-config-defaults.ts` | DEFAULT_SLAVE_CONFIG | -| `src/utils/ethercat/sdo-config-defaults.ts` | Extract default SDO configurations from CoE | -| `src/utils/ethercat/ethercat-task-helpers.ts` | Task naming, cycle time conversion | -| `src/utils/ethercat/generate-ethercat-config.ts` | Generate runtime JSON from project state | +| `src/backend/shared/ethercat/device-matcher.ts` | Match scanned devices against ESI repository | +| `src/backend/shared/ethercat/enrich-device-data.ts` | Extract persistable data from full ESI device | +| `src/backend/shared/ethercat/esi-parser.ts` | pdoToChannels, generateIecLocation, default mappings | +| `src/backend/shared/ethercat/device-config-defaults.ts` | DEFAULT_SLAVE_CONFIG | +| `src/backend/shared/ethercat/sdo-config-defaults.ts` | Extract default SDO configurations from CoE | +| `src/backend/shared/ethercat/generate-ethercat-config.ts` | Generate runtime JSON from project state | +| `src/backend/shared/ethercat/validate-ethercat-config.ts` | Validate generated runtime JSON | ### Renderer — Components | File | Contents | |------|----------| -| `src/renderer/components/.../ethercat/index.tsx` | Bus-level editor (Network, Repository, Advanced tabs) | -| `src/renderer/components/.../ethercat/ethercat-device-editor.tsx` | Per-device editor (Info, Config, SDO, Channels tabs) | -| `src/renderer/components/.../ethercat/components/scan-bus-tab.tsx` | Scan UI + configured devices list with +/- | -| `src/renderer/components/.../ethercat/components/device-browser-modal.tsx` | Browse ESI repo to add devices | -| `src/renderer/components/.../ethercat/components/device-configuration-form.tsx` | Slave config forms | -| `src/renderer/components/.../ethercat/components/channel-mapping-table.tsx` | IEC variable mapping | -| `src/renderer/components/.../ethercat/components/sdo-parameters-table.tsx` | CoE SDO editing | +| `src/frontend/components/.../ethercat/index.tsx` | Bus-level editor (Network, Repository, Advanced tabs) | +| `src/frontend/components/.../ethercat/ethercat-device-editor.tsx` | Per-device editor (Info, Config, SDO, Channels tabs) | +| `src/frontend/components/.../ethercat/components/scan-bus-tab.tsx` | Scan UI + configured devices list with +/- | +| `src/frontend/components/.../ethercat/components/device-browser-modal.tsx` | Browse ESI repo to add devices | +| `src/frontend/components/.../ethercat/components/device-configuration-form.tsx` | Slave config forms | +| `src/frontend/components/.../ethercat/components/channel-mapping-table.tsx` | IEC variable mapping | +| `src/frontend/components/.../ethercat/components/sdo-parameters-table.tsx` | CoE SDO editing | ### Renderer — Store | File | Contents | |------|----------| -| `src/renderer/store/slices/project/slice.ts` | createRemoteDevice, updateEthercatConfig, delete, rename | -| `src/renderer/store/slices/editor/types.ts` | Editor model schema (plc-ethercat-device variant) | -| `src/renderer/store/slices/tabs/utils.ts` | CreateEtherCATDeviceEditor | -| `src/renderer/store/slices/shared/index.ts` | openFile, closeFile, forceCloseFile, deleteEthercatDevice | -| `src/renderer/hooks/use-device-configuration.ts` | Lazy-load full device, manage channels/SDOs | +| `src/frontend/store/slices/project/slice.ts` | createRemoteDevice, updateEthercatConfig, delete, rename | +| `src/frontend/store/slices/editor/types.ts` | Editor model schema (plc-ethercat-device variant) | +| `src/frontend/store/slices/tabs/utils.ts` | CreateEtherCATDeviceEditor | +| `src/frontend/store/slices/shared/slice.ts` | closeFile, forceCloseFile, ethercatDeviceActions.delete | +| `src/frontend/hooks/use-device-configuration.ts` | Lazy-load full device, manage channels/SDOs | --- ## 13. Constraints & Notes 1. **ESI parsing is CPU-bound** — runs in main process. Sequential uploads recommended for UI responsiveness. -2. **Cycle time ↔ task sync** — `updateEthercatConfig()` auto-updates the associated system task interval. +2. **No IEC task** — the EtherCAT bus is driven by a dedicated thread inside the runtime plugin; timing comes from `masterConfig` (`cycleTimeUs`, `taskPriority`). 3. **Address uniqueness** — IEC addresses must be unique across all remote devices (Modbus + EtherCAT). `usedAddresses` is tracked when generating mappings. 4. **CoE SDO range** — only objects in 0x2000+ are user-configurable. System objects (0x0000–0x1FFF) are runtime-managed. 5. **PDO padding** — entries with `index: "0x0000"` are padding: excluded from channel lists but preserved in persisted PDOs for correct byte offsets. -6. **System tasks** — auto-created on device creation, auto-deleted on removal. Marked with `isSystemTask: true`. -7. **Repository v2 migration** — old v1 (metadata-only) auto-migrates to v2 (with device summaries) on first load. -8. **Editor caching** — editor models are cached by `meta.name`. When removing a device, its tab/editor must be explicitly closed to avoid stale `deviceId` references on re-add. +6. **Repository v2 migration** — old v1 (metadata-only) auto-migrates to v2 (with device summaries) on first load. +7. **Editor caching** — editor models are cached by `meta.name`. When removing a device, its tab/editor must be explicitly closed to avoid stale `deviceId` references on re-add. diff --git a/docs/outdated/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md b/docs/outdated/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md deleted file mode 100644 index d2701cc9b..000000000 --- a/docs/outdated/ARDUINO_UNO_Q_BINARY_SIZE_FIX.md +++ /dev/null @@ -1,137 +0,0 @@ -# Arduino Uno Q Binary Size Optimization - -## Problem Discovery - -### Symptoms -OpenPLC programs compiled for Arduino Uno Q would upload successfully but fail to execute - no LED blinking, no serial output, despite the same code working on Arduino Mega and the same board working with Arduino IDE sketches. - -### Root Cause Analysis - -Through extensive debugging using OpenOCD via ADB shell, we traced the failure to the Zephyr LLEXT (Linkable Loadable Extensions) loader. The exact failure point was identified: - -``` -llext_load() returns 0xfffffff4 (-12 = ENOMEM) -``` - -The Arduino Uno Q uses a dual-processor architecture: -- **Qualcomm QRB2210** - Linux MPU (host) -- **STM32U585** - Zephyr MCU (runs Arduino sketches via LLEXT) - -The Zephyr firmware is configured with `CONFIG_LLEXT_HEAP_SIZE=128` (128KB). The OpenPLC-generated binary was **241KB** - nearly twice the heap limit. - -### Binary Size Comparison - -| Binary | Size | -|--------|------| -| Simple Arduino IDE sketch | ~25 KB | -| OpenPLC blink program | ~241 KB | -| LLEXT heap limit | 128 KB | - -## Root Cause: Code Duplication - -Analysis with `arm-zephyr-eabi-nm` revealed the problem: - -1. **582 IEC function blocks** were compiled into the binary -2. Each function appeared **4 times** at different addresses -3. Functions like `TON_init__`, `TON_body__`, `SM_16DIN_init__` were duplicated - -The duplication occurred because: -1. `iec_std_FB.h` defines all IEC function blocks as `static` functions -2. This header is included by 4 different `.c` files: `Res0.c`, `Config0.c`, `glueVars.c`, and `POUS.c` (via `POUS.h`) -3. Since `static` functions have internal linkage, each translation unit gets its own copy -4. The linker cannot deduplicate them - -Additionally, hardware-specific modules (P1AM, Click PLC SM_CARDS, etc.) were unconditionally included even when not needed. - -## Solution - -### 1. Conditional Module Includes (`iec_std_FB.h`) - -Added `#ifdef` guards around hardware-specific function block includes: - -```c -#ifdef USE_P1AM_BLOCKS -#include "p1am_FB.h" -#endif - -#ifdef USE_SM_BLOCKS -#include "sm_cards.h" -#endif - -#ifdef USE_MQTT_BLOCKS -#include "MQTT.h" -#endif -// etc. -``` - -### 2. Single-Compilation Function Definitions (`iec_std_FB.h`) - -Restructured the header to compile function bodies only once: - -```c -#ifndef IEC_STD_FB_IMPL -// Extern declarations for when implementations are not included -extern void TON_init__(TON *data__, BOOL retain); -extern void TON_body__(TON *data__); -// ... all other function declarations -#else -// Function definitions - compiled only once -void TON_init__(TON *data__, BOOL retain) { - // implementation -} -// ... all other function definitions -#endif -``` - -### 3. Template Update (`glueVars.c.j2`) - -Modified the glueVars.c template to define `IEC_STD_FB_IMPL`: - -```c -// Include IEC function block implementations in this file only. -// Other files get only type definitions to avoid code duplication. -#define IEC_STD_FB_IMPL -#include "iec_std_lib.h" -``` - -This ensures only `glueVars.c` compiles the function bodies, while other files get extern declarations. - -## Results - -| Stage | Total Size | .text Section | Function Count | -|-------|-----------|---------------|----------------| -| Original | 241 KB | 233 KB | 582 (4× duplicates) | -| After module guards | 140 KB | 132 KB | 374 | -| **After deduplication** | **56 KB** | **49 KB** | **98 (unique)** | - -**77% size reduction** - well under the 128KB LLEXT heap limit. - -## Files Modified - -### openplc-editor -- `resources/sources/MatIEC/lib/iec_std_FB.h` - Added conditional compilation guards and extern declarations - -### xml2st -- `templates/glueVars.c.j2` - Added `#define IEC_STD_FB_IMPL` - -## Why Arduino Mega Worked - -The Arduino Mega (8-bit AVR) uses a different build process: -- AVR-GCC performs more aggressive dead code elimination -- The `.hex` file contains only final linked code after unused symbols are stripped -- No dynamic loading - code is flashed directly to MCU - -The Zephyr LLEXT system loads the entire relocatable ELF into heap memory for dynamic linking, making binary size critical. - -## Testing - -After applying these fixes: -1. Binary size reduced to 56KB (well under 128KB limit) -2. LED blinks correctly on Arduino Uno Q -3. No regressions observed on other Arduino boards - -## Future Considerations - -1. Consider making the `USE_*_BLOCKS` macros automatically defined based on PLC program analysis -2. Further optimization could include dead code elimination for unused standard function blocks -3. The same optimization benefits all Arduino targets, not just Uno Q diff --git a/docs/outdated/HEADLESS_SETUP.md b/docs/outdated/HEADLESS_SETUP.md deleted file mode 100644 index 63383d2ef..000000000 --- a/docs/outdated/HEADLESS_SETUP.md +++ /dev/null @@ -1,357 +0,0 @@ -# OpenPLC Editor Headless Setup Guide - -This guide explains how to run the OpenPLC Editor GUI application in a headless Linux environment using the automated setup script. - -## Overview - -The `setup-headless-vnc.sh` script automates the complete setup of a headless environment for running the OpenPLC Editor with browser-based GUI access through noVNC. This is particularly useful for: - -- Running the OpenPLC Editor on headless servers -- Automating GUI testing in CI/CD pipelines -- Remote access to the editor through a web browser -- Development and testing in containerized environments - -## Architecture - -The setup creates the following service stack: - -``` -┌─────────────────────────────────────────────┐ -│ Web Browser (http://localhost:8080) │ -│ User interacts with GUI via noVNC │ -└────────────────┬────────────────────────────┘ - │ -┌────────────────▼────────────────────────────┐ -│ noVNC (WebSocket to VNC proxy) │ -│ Port 8080 │ -└────────────────┬────────────────────────────┘ - │ -┌────────────────▼────────────────────────────┐ -│ x11vnc (VNC Server) │ -│ Port 5900 │ -└────────────────┬────────────────────────────┘ - │ -┌────────────────▼────────────────────────────┐ -│ Xvfb (Virtual X11 Display) │ -│ Display :99 │ -│ ┌──────────────────────────────────────┐ │ -│ │ fluxbox (Window Manager) │ │ -│ │ ┌────────────────────────────────┐ │ │ -│ │ │ OpenPLC Editor (Electron App) │ │ │ -│ │ └────────────────────────────────┘ │ │ -│ └──────────────────────────────────────┘ │ -└─────────────────────────────────────────────┘ -``` - -## Prerequisites - -The script will verify that the following dependencies are installed: - -### System Packages -- `xvfb` - Virtual X11 display server -- `x11vnc` - VNC server for X11 -- `fluxbox` - Lightweight window manager -- `scrot` - Screenshot utility -- `libfuse2` - Required for running AppImage files -- `python3` - Python runtime - -### Node.js Dependencies -- `node` - Node.js runtime -- `npm` - Node package manager - -### Additional Tools -- `noVNC` - Web-based VNC client (cloned to `~/noVNC`) -- `websockify` - WebSocket to TCP proxy (in `~/noVNC/utils/websockify`) - -### Installing Dependencies - -On Ubuntu/Debian systems: - -```bash -# Install system packages -sudo apt-get update -sudo apt-get install -y xvfb x11vnc fluxbox scrot libfuse2 python3 - -# Install Node.js (if not already installed) -# Option 1: Using nvm (recommended) -curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash -source ~/.bashrc -nvm install --lts - -# Option 2: Using package manager -sudo apt-get install -y nodejs npm - -# Clone noVNC and websockify -git clone https://github.com/novnc/noVNC.git ~/noVNC -git clone https://github.com/novnc/websockify ~/noVNC/utils/websockify -``` - -## Usage - -### Basic Usage - -Run the script with the path to your openplc-editor repository: - -```bash -./setup-headless-vnc.sh /path/to/openplc-editor -``` - -### Advanced Options - -The script supports several command-line options to customize the setup: - -```bash -./setup-headless-vnc.sh [options] - -Options: - -d, --display NUM Display number (default: 99) - -v, --vnc-port PORT VNC port (default: 5900) - -n, --novnc-port PORT noVNC web port (default: 8080) - -r, --resolution RES Screen resolution (default: 1920x1080x24) - -h, --help Show help message - -Examples: - # Use custom display and ports - ./setup-headless-vnc.sh ~/repos/openplc-editor --display 98 --vnc-port 5901 - - # Use custom resolution - ./setup-headless-vnc.sh ~/repos/openplc-editor --resolution 1280x720x24 - - # Combine multiple options - ./setup-headless-vnc.sh ~/repos/openplc-editor \ - --display 100 \ - --vnc-port 5902 \ - --novnc-port 8081 -``` - -## Accessing the GUI - -After running the script, you can access the OpenPLC Editor GUI in two ways: - -### 1. Web Browser (Recommended) - -Open your web browser and navigate to: -``` -http://localhost:8080/vnc.html -``` - -Click the "Connect" button to start interacting with the GUI. This method works from any device that can access your server, including mobile devices (when properly exposed). - -### 2. VNC Client - -Use any VNC client to connect to: -``` -localhost:5900 -``` - -No password is required for the connection. - -## What the Script Does - -1. **Dependency Check**: Verifies all required dependencies are installed -2. **Repository Validation**: Confirms the provided path contains a valid openplc-editor repository -3. **Service Cleanup**: Stops any existing instances of the services -4. **Xvfb Startup**: Launches the virtual X11 display server -5. **Window Manager**: Starts fluxbox for window management -6. **VNC Server**: Starts x11vnc to expose the display via VNC protocol -7. **noVNC Proxy**: Launches the WebSocket-to-VNC proxy for browser access -8. **Build Check**: Verifies the application is built or builds it if needed -9. **AppImage Extraction**: Extracts the AppImage for direct execution -10. **Application Launch**: Starts the OpenPLC Editor in the virtual display -11. **Status Report**: Provides a summary of all running services and access methods - -## Service Management - -### Viewing Service Status - -After running the script, it will display PIDs for all services: - -``` -Service Status: - • Xvfb: Display :99 (PID: 1234) - • fluxbox: Window manager (PID: 1235) - • x11vnc: Port 5900 (PID: 1236) - • noVNC: Port 8080 (PID: 1237) - • OpenPLC Editor: Running (PID: 1238) -``` - -### Viewing Logs - -Logs are written to `/tmp/` for easy debugging: - -```bash -# View x11vnc logs -tail -f /tmp/x11vnc.log - -# View noVNC logs -tail -f /tmp/novnc.log - -# View OpenPLC Editor logs -tail -f /tmp/openplc-editor.log -``` - -### Stopping Services - -To stop all services, run the commands provided in the script output: - -```bash -pkill -f 'Xvfb :99' -pkill -f 'x11vnc.*-display :99' -pkill -f 'websockify.*8080' -pkill -f 'fluxbox.*-display :99' -pkill -f 'open-plc-editor' -``` - -Or stop individual services by their PIDs: - -```bash -kill -``` - -## Troubleshooting - -### Script Reports Missing Dependencies - -If the script reports missing dependencies, install them using the commands provided in the error messages. - -### Port Already in Use - -If you get errors about ports already being in use, either: -1. Stop the existing services using the commands above -2. Use different ports with the `--vnc-port` and `--novnc-port` options - -### Display Already in Use - -If display :99 is already in use, specify a different display number: -```bash -./setup-headless-vnc.sh /path/to/repo --display 100 -``` - -### Application Fails to Start - -Check the application logs: -```bash -tail -50 /tmp/openplc-editor.log -``` - -Common issues: -- **Missing node_modules**: The script will automatically run `npm install` if needed -- **Build artifacts missing**: The script will run `npm run package` to build the AppImage -- **Permission errors**: Ensure you have read/write access to the repository directory - -### noVNC Connection Issues - -1. Verify x11vnc is running: -```bash -ps aux | grep x11vnc -``` - -2. Check x11vnc logs: -```bash -cat /tmp/x11vnc.log -``` - -3. Verify noVNC is running: -```bash -ps aux | grep websockify -``` - -### Browser Shows Black Screen - -This usually means the OpenPLC Editor hasn't started yet. Wait a few seconds and refresh the browser. Check the application logs if the issue persists. - -## Remote Access - -### From Another Machine on the Same Network - -If you want to access the GUI from another machine on your local network: - -```bash -# On the server, expose the noVNC port -# Replace with your server's IP address -# Users can then access http://:8080/vnc.html -``` - -Note: This is insecure as noVNC runs without authentication. Only use on trusted networks. - -### Via SSH Tunnel (Secure) - -For secure remote access over the internet: - -```bash -# On your local machine -ssh -L 8080:localhost:8080 user@server-address - -# Then access http://localhost:8080/vnc.html in your local browser -``` - -## CI/CD Integration - -This setup is ideal for automated testing in CI/CD pipelines: - -```yaml -# Example GitHub Actions workflow -- name: Setup Headless Environment - run: | - sudo apt-get install -y xvfb x11vnc fluxbox scrot libfuse2 - git clone https://github.com/novnc/noVNC.git ~/noVNC - git clone https://github.com/novnc/websockify ~/noVNC/utils/websockify - -- name: Start OpenPLC Editor - run: | - chmod +x setup-headless-vnc.sh - ./setup-headless-vnc.sh $(pwd) - -- name: Run GUI Tests - run: | - # Your test commands here - python test_gui.py -``` - -## Performance Considerations - -- **Display Resolution**: Lower resolutions use less CPU and bandwidth. Use `--resolution 1280x720x24` for better performance if high resolution isn't needed. -- **Color Depth**: The 24 in `1920x1080x24` represents color depth. Reducing this can improve performance on slower connections. -- **Network Bandwidth**: noVNC streams the display over WebSockets. On slow connections, you may experience lag. - -## Security Considerations - -⚠️ **Important Security Notes**: - -1. **No Authentication**: The default setup has no authentication on the VNC server. Do not expose these ports to untrusted networks. -2. **Unencrypted Connection**: The VNC connection is not encrypted by default. Use SSH tunnels for remote access. -3. **Localhost Only**: x11vnc is configured to listen on localhost only (`-listen localhost`), which is secure by default. -4. **Production Use**: For production environments, consider adding authentication and SSL/TLS encryption. - -## Known Limitations - -### Mouse Click Automation - -Electron applications (including OpenPLC Editor) implement security features that block synthetic mouse input from tools like `xdotool` and `PyAutoGUI`. This is intentional security to prevent UI automation attacks. - -**Workaround**: Use the browser-based noVNC interface where genuine user input events are respected. For automated testing, consider: -- Keyboard navigation (`Tab`, `Enter`, arrow keys work reliably) -- Browser automation tools that can interact with the noVNC canvas -- Chrome DevTools Protocol (if the app is launched with `--remote-debugging-port`) - -### Mobile Device Limitations - -While noVNC works on mobile browsers, the touch interface may not be as responsive as on desktop browsers. For best experience, use a desktop or laptop computer. - -## Additional Resources - -- [noVNC Project](https://github.com/novnc/noVNC) -- [x11vnc Documentation](https://github.com/LibVNC/x11vnc) -- [Xvfb Manual](https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml) -- [OpenPLC Editor Repository](https://github.com/Autonomy-Logic/openplc-editor) - -## Support - -For issues with: -- **This script**: Open an issue in the openplc-editor repository -- **OpenPLC Editor**: Check the main repository documentation -- **noVNC/VNC**: Refer to the respective project documentation - -## License - -This script is part of the OpenPLC Editor project and follows the same license. diff --git a/docs/outdated/dead-code-inventory.md b/docs/outdated/dead-code-inventory.md deleted file mode 100644 index 803a9315d..000000000 --- a/docs/outdated/dead-code-inventory.md +++ /dev/null @@ -1,69 +0,0 @@ -# Dead Code Inventory - -Audit date: 2026-03-17 -Branch: `fix/step-31-final-adjustments` - -This document tracks dead code identified during a code quality audit. -Items here are documented for later cleanup — they are not blocking any work. - ---- - -## Unused Exported Function - -### `getVariableSize()` — `src/frontend/utils/variable-sizes.ts:101-146` - -Exported function that takes a full `PLCVariable` object and returns byte size. -Superseded by `getTypeSizeByName()` (line 152) which takes just a type name string. - -- Never imported by any other file in the codebase. -- Not called internally within the file. -- The sibling function `getTypeSizeByName` covers the same logic and is actively used. - -**Action:** Remove the function. No callers to update. - ---- - -## Unnecessary Export Keyword - -### `parseVariableValue()` — `src/frontend/utils/variable-sizes.ts:189-258` - -Exported but only called internally by `parseValueByTypeName()` (line 277). -No external file imports `parseVariableValue` directly. - -**Action:** Remove the `export` keyword. The function itself is live code (used by `parseValueByTypeName` which is consumed by `useDebugPolling.ts`). - ---- - -## Commented-Out Imports - -These are leftover commented imports that should be removed: - -| File | Line | Content | -|------|------|---------| -| `src/main/modules/preload/preload.ts` | 1 | `// import './splash-screen/index'` | -| `src/frontend/components/_molecules/graphical-editor/ladder/rung/ladder-utils/elements/diagram/index.ts` | 4 | `// import type { VariableNode } from '...'` | -| `src/types/common/project.ts` | 1, 3 | `// import { z } from 'zod'`, `// import { CONSTANTS, formatDate } from '@/utils'` | -| `src/frontend/utils/PLC/xml-generator/codesys/pou-xml.ts` | 1 | `// import { PLCVariable } from '@root/types/PLC'` | -| `src/frontend/components/_organisms/global-variables-editor/index.tsx` | 1 | `// import * as PrimitiveSwitch from '@radix-ui/react-switch'` | - -**Action:** Delete each commented line. - ---- - -## TODO/FIXME Comments (Incomplete Implementations) - -These are not dead code but flag missing validation logic: - -| File | Description | -|------|-------------| -| `src/types/PLC/open-plc.ts:165-168` | Task name uniqueness — needs homologation | -| `src/types/PLC/open-plc.ts:234-236` | Schema validation TODOs | -| `src/types/PLC/units/task.ts` | Task interval regex validation missing | -| `src/types/PLC/units/instance.ts` | Instance task/program validation missing | -| `src/types/PLC/units/library.ts` | Library validation TODOs | -| `src/backend/editor/hardware/hardware-module.ts` | TODO comment | -| `src/backend/editor/compiler/compiler-module.ts` | TODO comment | -| `src/backend/editor/services/project-service/utils/read-project.ts` | TODO comment | -| `src/main/main.ts` | Multiple TODO comments | - -**Action:** Track as separate backlog items, not dead code cleanup. diff --git a/docs/outdated/debugger-opcua-shared-utilities.md b/docs/outdated/debugger-opcua-shared-utilities.md deleted file mode 100644 index 285b589b6..000000000 --- a/docs/outdated/debugger-opcua-shared-utilities.md +++ /dev/null @@ -1,544 +0,0 @@ -# Debugger and OPC-UA Shared Utilities Refactoring - -## Overview - -This document outlines the plan to refactor the OpenPLC Editor codebase to eliminate code duplication between the **debugger** and **OPC-UA** subsystems. Both systems need to resolve debug variable indices from the `debug.c` file generated during compilation, but currently they use separate implementations with duplicated logic. - -## Problem Statement - -### Current State - -The debugger and OPC-UA configuration generator both need to: - -1. Parse `debug.c` to extract variable entries with their indices -2. Build debug paths in specific formats (`RES0__INSTANCE.VAR`, `CONFIG0__GLOBAL`, etc.) -3. Match PLC variables to debug entries to resolve indices -4. Handle complex types (function blocks, structures, arrays) - -However, these functionalities are implemented **twice**: - -| Functionality | Debugger Implementation | OPC-UA Implementation | -|--------------|------------------------|----------------------| -| Parse debug.c | `renderer/utils/parse-debug-file.ts` | `utils/debug-variable-finder.ts` | -| Build paths | `renderer/utils/debug-tree-builder.ts` | `utils/debug-variable-finder.ts` | -| Find indices | `parse-debug-file.ts:matchVariableWithDebugEntry()` | `debug-variable-finder.ts:findVariableIndex()` | -| FB/struct lookup | Manual in `debug-tree-builder.ts` | `utils/pou-helpers.ts` | -| Tree traversal | `debug-tree-builder.ts` | `opcua/resolve-indices.ts` | - -### Why This Is a Problem - -1. **Bug Duplication**: A fix in one system may not be applied to the other -2. **Inconsistent Behavior**: Subtle differences in path building can cause one system to work while the other fails -3. **Maintenance Burden**: Changes to debug.c format require updates in multiple places -4. **Testing Overhead**: Both implementations need separate test coverage - -### Recent Example - -During OPC-UA development, shared utilities were created (`debug-variable-finder.ts`, `pou-helpers.ts`) but the debugger was not updated to use them. This led to confusion when debugging issues because the systems used different code paths. - -## Architecture Analysis - -### Debug Path Formats in debug.c - -The IEC 61131-3 compiler generates `debug.c` with variable paths in these formats: - -```c -// Global variables -{ &(CONFIG0__GLOBAL_VAR), INT_ENUM } - -// Program variables (simple) -{ &(RES0__INSTANCE0.MOTOR_SPEED), INT_ENUM } - -// Function block instance variables -{ &(RES0__INSTANCE0.TON0.Q), BOOL_ENUM } -{ &(RES0__INSTANCE0.TON0.ET), TIME_ENUM } - -// Nested FB variables -{ &(RES0__INSTANCE0.CONTROLLER0.INNER_FB0.VALUE), REAL_ENUM } - -// Structure fields (note the .value. segment) -{ &(RES0__INSTANCE0.MY_STRUCT.value.FIELD1), INT_ENUM } - -// Array elements (note .value.table[i]) -{ &(RES0__INSTANCE0.MY_ARRAY.value.table[0]), INT_ENUM } -{ &(RES0__INSTANCE0.MY_ARRAY.value.table[1]), INT_ENUM } -``` - -Key observations: -- Instance names come from Resources configuration, NOT POU names -- FB variables use direct dot notation (no `.value.`) -- Structure fields require `.value.` before field name -- Arrays require `.value.table[i]` syntax - -### Current Debugger Files - -``` -src/renderer/ -├── utils/ -│ ├── parse-debug-file.ts # Parses debug.c, matchVariableWithDebugEntry() -│ └── debug-tree-builder.ts # Builds UI tree, has own path building -├── components/_organisms/ -│ └── workspace-activity-bar/ -│ └── default.tsx # Initializes debugger, calls parsing -└── screens/ - └── workspace-screen.tsx # Polling loop, builds paths inline -``` - -### Current OPC-UA Files - -``` -src/utils/ -├── debug-variable-finder.ts # Path building, variable lookup (SHARED) -├── pou-helpers.ts # FB/struct lookup (SHARED) -└── opcua/ - ├── resolve-indices.ts # Uses shared utilities - ├── generate-opcua-config.ts # Generates JSON config - └── types.ts # Type definitions -``` - -### Duplication Details - -#### 1. Debug File Parsing - -**Debugger** (`parse-debug-file.ts`): -```typescript -export function parseDebugFile(content: string): ParsedDebugData { - const variables: DebugVariable[] = [] - const debugVarsMatch = content.match(/debug_vars\[\]\s*=\s*\{([\s\S]*?)\};/) - // ... parsing logic - return { variables, totalCount } -} -``` - -**Shared** (`debug-variable-finder.ts`): -```typescript -export function parseDebugVariables(content: string): DebugVariableEntry[] { - const variables: DebugVariableEntry[] = [] - const debugVarsMatch = content.match(/debug_vars\[\]\s*=\s*\{([\s\S]*?)\};/) - // ... nearly identical parsing logic - return variables -} -``` - -#### 2. Path Building - -**Debugger** (`debug-tree-builder.ts`): -```typescript -function buildVariableBasePath(variableName: string, instanceName: string, variableClass?: string): string { - if (variableClass === 'external') { - return `CONFIG0__${variableNameUpper}` - } - return `RES0__${instanceNameUpper}.${variableNameUpper}` -} -``` - -**Shared** (`debug-variable-finder.ts`): -```typescript -export function buildDebugPath(instanceName: string, variablePath: string, options = {}): string { - // More comprehensive implementation handling all path formats -} - -export function buildGlobalDebugPath(variablePath: string): string { - return `CONFIG0__${variablePath.toUpperCase()}` -} -``` - -#### 3. Index Lookup - -**Debugger** (`parse-debug-file.ts`): -```typescript -export function matchVariableWithDebugEntry( - pouVariableName: string, - instanceName: string, - debugVariables: DebugVariable[], - variableClass?: string, -): number | null { - const expectedPath = `RES0__${instanceNameUpper}.${variableNameUpper}` - const match = debugVariables.find((dv) => dv.name === expectedPath) - return match ? match.index : null -} -``` - -**Shared** (`debug-variable-finder.ts`): -```typescript -export function findVariableIndex( - instanceName: string, - variablePath: string, - debugVariables: DebugVariableEntry[], - options = {}, -): number | null { - const debugPath = buildDebugPath(instanceName, variablePath, options) - const match = findDebugVariable(debugVariables, debugPath) - return match ? match.index : null -} -``` - -## Refactoring Plan - -The refactoring will be done in 6 phases, each building on the previous. This incremental approach minimizes risk and allows testing at each stage. - -### Phase 1: Consolidate Debug File Parsing - -**Goal**: Single source of truth for parsing `debug.c` - -**Rationale**: The parsing logic is nearly identical in both implementations. Having a single parser ensures consistent behavior and makes it easier to handle any future changes to the debug.c format. - -**Changes**: - -1. Create canonical parser in `src/utils/debug-parser.ts`: - ```typescript - // src/utils/debug-parser.ts - export interface DebugVariableEntry { - name: string // Full path (e.g., "RES0__INSTANCE0.VAR") - type: string // Type enum (e.g., "INT_ENUM") - index: number // Array index in debug_vars[] - } - - export function parseDebugVariables(content: string): DebugVariableEntry[] - ``` - -2. Update `renderer/utils/parse-debug-file.ts` to delegate: - ```typescript - // Re-export from shared module - export { parseDebugVariables as parseDebugFile } from '@root/utils/debug-parser' - - // Keep DebugVariable interface for backwards compatibility - export type DebugVariable = DebugVariableEntry - ``` - -3. Update `utils/debug-variable-finder.ts` to import from `debug-parser.ts` - -**Files Modified**: -- Create: `src/utils/debug-parser.ts` -- Modify: `src/renderer/utils/parse-debug-file.ts` -- Modify: `src/utils/debug-variable-finder.ts` - -**Testing**: -- Existing debugger tests should pass unchanged -- Existing OPC-UA tests should pass unchanged -- Add shared parser unit tests - ---- - -### Phase 2: Unify Path Building - -**Goal**: Single implementation for all debug path formats - -**Rationale**: Path building is where subtle bugs can occur. The shared `buildDebugPath()` already handles more cases than the debugger's `buildVariableBasePath()`. Unifying ensures both systems handle all path formats correctly. - -**Changes**: - -1. Ensure `buildDebugPath()` handles all cases: - - Simple variables: `RES0__INSTANCE.VAR` - - FB instance variables: `RES0__INSTANCE.FB.VAR` - - Nested FB variables: `RES0__INSTANCE.FB1.FB2.VAR` - - Structure fields: `RES0__INSTANCE.STRUCT.value.FIELD` - - Array elements: `RES0__INSTANCE.ARR.value.table[i]` - - Global variables: `CONFIG0__VAR` - -2. Update `debug-tree-builder.ts` to use shared path builder: - ```typescript - // Before - const fullPath = buildVariableBasePath(variable.name, instanceName, variable.class) - - // After - import { buildDebugPath, buildGlobalDebugPath } from '@root/utils/debug-variable-finder' - const fullPath = variable.class === 'external' - ? buildGlobalDebugPath(variable.name) - : buildDebugPath(instanceName, variable.name) - ``` - -3. Keep `buildVariableBasePath()` as deprecated wrapper during transition - -**Files Modified**: -- Modify: `src/utils/debug-variable-finder.ts` (ensure all path formats) -- Modify: `src/renderer/utils/debug-tree-builder.ts` - -**Testing**: -- Test all path format variations -- Verify debugger tree building still works -- Test with nested FBs, arrays of structs, etc. - ---- - -### Phase 3: Unify Variable Index Lookup - -**Goal**: Single function for finding variable indices - -**Rationale**: Index lookup depends on correct path building. By this phase, paths are unified, so lookup can also be unified. - -**Changes**: - -1. Update `workspace-activity-bar/default.tsx` to use shared utilities: - ```typescript - // Before - import { parseDebugFile, matchVariableWithDebugEntry } from '../utils/parse-debug-file' - const index = matchVariableWithDebugEntry(v.name, instance.name, parsed.variables, v.class) - - // After - import { parseDebugVariables } from '@root/utils/debug-parser' - import { findVariableIndex, findGlobalVariableIndex } from '@root/utils/debug-variable-finder' - const index = v.class === 'external' - ? findGlobalVariableIndex(v.name, debugVariables) - : findVariableIndex(instance.name, v.name, debugVariables) - ``` - -2. Deprecate `matchVariableWithDebugEntry()` and `matchGlobalVariableWithDebugEntry()` - -**Files Modified**: -- Modify: `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` -- Modify: `src/renderer/utils/parse-debug-file.ts` (mark functions deprecated) - -**Testing**: -- Verify debugger initialization works correctly -- Test with various variable types (local, external, FB instances) -- Ensure index map is populated correctly - ---- - -### Phase 4: Unify Tree Building - -**Goal**: Shared tree/leaf variable traversal for both debugger and OPC-UA - -**Rationale**: Both systems need to traverse complex types (FBs, structs, arrays) to find leaf variables. The traversal logic is complex and having it in one place reduces bugs. - -**Changes**: - -1. Create shared tree traversal with visitor pattern: - ```typescript - // src/utils/debug-tree-traversal.ts - - export interface DebugNodeVisitor { - visitLeaf(path: string, compositeKey: string, type: string, index: number | undefined): T - visitComplex(path: string, compositeKey: string, type: string, children: T[]): T - visitArray(path: string, compositeKey: string, elementType: string, indices: [number, number], children: T[]): T - } - - export function traverseVariable( - variable: PouVariable, - pouName: string, - instanceName: string, - debugVariables: DebugVariableEntry[], - projectPous: PLCPou[], - dataTypes: PLCDataType[], - visitor: DebugNodeVisitor - ): T - ``` - -2. Debugger implements visitor for `DebugTreeNode`: - ```typescript - const debuggerVisitor: DebugNodeVisitor = { - visitLeaf: (path, key, type, index) => ({ - name: path.split('.').pop()!, - fullPath: path, - compositeKey: key, - type, - isComplex: false, - debugIndex: index - }), - // ... other methods - } - ``` - -3. OPC-UA implements visitor for `ResolvedField[]`: - ```typescript - const opcuaVisitor: DebugNodeVisitor = { - visitLeaf: (path, key, type, index) => [{ - name: path, - datatype: type, - index: index!, - // ... - }], - // ... other methods - } - ``` - -**Files Created**: -- `src/utils/debug-tree-traversal.ts` - -**Files Modified**: -- Modify: `src/renderer/utils/debug-tree-builder.ts` (use shared traversal) -- Modify: `src/utils/opcua/resolve-indices.ts` (use shared traversal) - -**Testing**: -- Test with deeply nested FBs -- Test with arrays of structures -- Test with mixed complex types -- Verify both debugger and OPC-UA produce correct results - ---- - -### Phase 5: Update Debugger Polling - -**Goal**: Consistent index lookup in polling code - -**Rationale**: The polling loop in `workspace-screen.tsx` builds paths inline. Using shared utilities ensures consistency with initialization. - -**Changes**: - -1. Replace inline path construction: - ```typescript - // Before - const debugPath = `RES0__${programInstance.name.toUpperCase()}.${fbInstance.name.toUpperCase()}.${fbVar.name.toUpperCase()}` - - // After - import { buildDebugPath } from '@root/utils/debug-variable-finder' - const debugPath = buildDebugPath(programInstance.name, `${fbInstance.name}.${fbVar.name}`) - ``` - -2. Use `findInstanceName()` instead of manual instance lookup - -3. Centralize FB variable iteration logic - -**Files Modified**: -- Modify: `src/renderer/screens/workspace-screen.tsx` - -**Testing**: -- Test debugger polling with real hardware -- Verify all variable types update correctly -- Test force variable functionality - ---- - -### Phase 6: Remove Deprecated Code - -**Goal**: Clean up duplicate implementations after verification - -**Prerequisites**: -- All phases 1-5 completed -- Debugger verified working with real hardware -- OPC-UA verified working with real hardware -- No regressions in functionality -- All tests passing - -**Removals**: - -1. **`src/renderer/utils/parse-debug-file.ts`**: - - Remove `parseDebugFile()` implementation body (keep as re-export) - - Remove `matchVariableWithDebugEntry()` function - - Remove `matchGlobalVariableWithDebugEntry()` function - - Remove `parsePathComponents()` helper - - Keep file with re-exports for backwards compatibility - -2. **`src/renderer/utils/debug-tree-builder.ts`**: - - Remove `buildVariableBasePath()` function - - Remove `normalizeTypeString()` (use from `pou-helpers.ts`) - - Remove duplicate FB/struct lookup code - - Update to be thin wrapper around shared traversal - -3. **`src/renderer/screens/workspace-screen.tsx`**: - - Remove any remaining inline path building - -4. **Update all imports** to point to canonical shared locations - -**Final File Structure**: - -``` -src/ -├── utils/ -│ ├── debug-parser.ts # CANONICAL: Parse debug.c -│ ├── debug-variable-finder.ts # CANONICAL: Path building, index lookup -│ ├── debug-tree-traversal.ts # CANONICAL: Tree traversal (NEW) -│ ├── pou-helpers.ts # CANONICAL: FB/struct lookup -│ └── opcua/ -│ ├── resolve-indices.ts # Uses shared utilities -│ ├── generate-opcua-config.ts -│ └── types.ts -├── renderer/ -│ ├── utils/ -│ │ ├── parse-debug-file.ts # Re-exports only (backwards compat) -│ │ └── debug-tree-builder.ts # UI wrapper using shared traversal -│ ├── components/_organisms/ -│ │ └── workspace-activity-bar/ -│ │ └── default.tsx # Uses shared utilities -│ └── screens/ -│ └── workspace-screen.tsx # Uses shared utilities -└── types/ - └── debugger.ts # Shared debug node types -``` - -**Testing**: -- Full regression test suite -- Manual testing with various PLC programs -- Test edge cases (empty programs, complex nesting) - -## Migration Strategy - -### Backwards Compatibility - -During the transition, we maintain backwards compatibility by: - -1. **Re-exporting**: Old import paths continue to work -2. **Wrapper functions**: Deprecated functions delegate to new implementations -3. **Type aliases**: Old type names map to new types - -Example: -```typescript -// src/renderer/utils/parse-debug-file.ts (during transition) - -// Re-export new parser with old name -export { parseDebugVariables as parseDebugFile } from '@root/utils/debug-parser' - -// Type alias for backwards compatibility -export type { DebugVariableEntry as DebugVariable } from '@root/utils/debug-parser' - -// Deprecated wrapper (to be removed in Phase 6) -/** @deprecated Use findVariableIndex from debug-variable-finder instead */ -export function matchVariableWithDebugEntry(...) { - console.warn('matchVariableWithDebugEntry is deprecated') - return findVariableIndex(...) -} -``` - -### Testing Strategy - -Each phase includes specific testing requirements: - -1. **Unit Tests**: Test shared utilities in isolation -2. **Integration Tests**: Test debugger and OPC-UA with shared code -3. **Hardware Tests**: Verify with actual PLC hardware -4. **Regression Tests**: Ensure no existing functionality breaks - -### Rollback Plan - -If issues are discovered: - -1. Each phase can be reverted independently -2. Deprecated wrappers provide fallback during transition -3. Git branches allow easy rollback to previous state - -## Timeline Estimate - -| Phase | Complexity | Dependencies | -|-------|------------|--------------| -| Phase 1 | Low | None | -| Phase 2 | Medium | Phase 1 | -| Phase 3 | Medium | Phase 2 | -| Phase 4 | High | Phase 2, 3 | -| Phase 5 | Medium | Phase 2 | -| Phase 6 | Low | All above + verification | - -Recommended order: 1 → 2 → 3 → 5 → 4 → 6 - -(Phase 5 can be done before Phase 4 as it only requires path building) - -## Success Criteria - -The refactoring is complete when: - -1. ✅ Single debug.c parser used by both systems -2. ✅ Single path building implementation -3. ✅ Single index lookup implementation -4. ✅ Shared tree traversal logic -5. ✅ No duplicate implementations remain -6. ✅ All tests pass -7. ✅ Debugger works correctly with hardware -8. ✅ OPC-UA works correctly with hardware -9. ✅ Code coverage maintained or improved - -## References - -- `src/utils/debug-variable-finder.ts` - Current shared path building -- `src/utils/pou-helpers.ts` - Current shared POU helpers -- `src/renderer/utils/debug-tree-builder.ts` - Current debugger tree builder -- `src/utils/opcua/resolve-indices.ts` - Current OPC-UA index resolution diff --git a/docs/outdated/external-binaries-strategy.md b/docs/outdated/external-binaries-strategy.md deleted file mode 100644 index 3dc928216..000000000 --- a/docs/outdated/external-binaries-strategy.md +++ /dev/null @@ -1,383 +0,0 @@ -# External Binaries Strategy - -> **Purpose**: Guide the migration of pre-built binaries (xml2st, matiec, arduino-cli) out of the -> openplc-editor git repository, replacing them with versioned downloads from GitHub Releases. - ---- - -## Table of Contents - -1. [Problem Statement](#problem-statement) -2. [Current State](#current-state) -3. [Target Architecture](#target-architecture) -4. [Phase 1 — Tagged Releases on xml2st and matiec](#phase-1--tagged-releases-on-xml2st-and-matiec) -5. [Phase 2 — Download Script in openplc-editor](#phase-2--download-script-in-openplc-editor) -6. [Phase 3 — Update openplc-editor CI/CD](#phase-3--update-openplc-editor-cicd) -7. [Phase 4 — Remove Binaries from Git](#phase-4--remove-binaries-from-git) -8. [Alternatives Considered](#alternatives-considered) -9. [Key Design Decisions](#key-design-decisions) -10. [Implementation Checklist](#implementation-checklist) - ---- - -## Problem Statement - -The openplc-editor repository contains **~350 MB** of pre-built binaries committed directly to git -under `resources/bin/`. Every time xml2st or matiec is updated, new binaries are committed, bloating -the repository history. There is no version tracking — it is impossible to know which version of -xml2st or matiec is bundled at any given commit. - -## Current State - -### Repository layout - -``` -resources/ -├── bin/ -│ ├── darwin/ -│ │ ├── x64/ (68 MB) — arduino-cli, iec2c, xml2st/ -│ │ └── arm64/ (66 MB) — arduino-cli, iec2c, xml2st/ -│ ├── linux/ -│ │ ├── x64/ (57 MB) — arduino-cli, iec2c, xml2st -│ │ └── arm64/ (33 MB) — arduino-cli only (incomplete) -│ └── win32/ -│ ├── x64/ (63 MB) — arduino-cli.exe, iec2c.exe, xml2st.exe -│ └── arm64/ (63 MB) — arduino-cli.exe, iec2c.exe, xml2st.exe -└── sources/ - └── MatIEC/lib/ — matiec IEC standard library files (596 KB) -``` - -### Tool summary - -| Tool | Repository | Language | CI/CD | Tagged Releases | Extra Files | -|---|---|---|---|---|---| -| xml2st | Autonomy-Logic/xml2st | Python (PyInstaller) | Build workflow for 6 platforms | None | plcopen/, templates/ (bundled by PyInstaller) | -| matiec (iec2c) | Autonomy-Logic/matiec | C++ (Bison/Flex/Autotools) | None | None | lib/ dir (596 KB) with .txt + C headers | -| arduino-cli | arduino/arduino-cli | Go | Official | Yes (official releases) | None | - -### How binaries are consumed - -The `CompilerModule` class (`src/main/modules/compiler/compiler-module.ts`) resolves binary paths -at runtime: - -``` -Development: {cwd}/resources/bin/{platform}/{arch}/{binary} -Production: {resourcesPath}/bin/{binary} -``` - -`electron-builder.json` copies the platform-specific `resources/bin/{platform}/{arch}` directory -into the packaged app as `extraResources`. - -The MatIEC standard library is consumed from `resources/sources/MatIEC/lib/` by the compilation -pipeline. - -### Compilation pipeline - -``` -XML Project File → xml2st → ST File → iec2c → C/C++ Files → arduino-cli → Board Binary -``` - ---- - -## Target Architecture - -``` -openplc-editor/ -├── binary-versions.json ← Single source of truth for tool versions -├── scripts/ -│ └── download-binaries.ts ← Downloads release artifacts from GitHub -├── resources/ -│ ├── bin/ ← .gitignored, populated by download script -│ │ └── {platform}/{arch}/ -│ └── sources/ -│ └── MatIEC/lib/ ← .gitignored, populated by download script -``` - -Version bumps become a one-line diff in `binary-versions.json`. - ---- - -## Phase 1 — Tagged Releases on xml2st and matiec - -### 1.1 xml2st (Autonomy-Logic/xml2st) - -**Status**: Already has a CI build workflow for all 6 platforms. Needs a release workflow. - -**Tasks**: - -- [ ] Add a release workflow (`.github/workflows/release.yml`) triggered on tag push (`v*`) -- [ ] Standardize artifact naming: `xml2st-{platform}-{arch}.tar.gz` - - `xml2st-darwin-arm64.tar.gz` - - `xml2st-darwin-x64.tar.gz` - - `xml2st-linux-arm64.tar.gz` - - `xml2st-linux-x64.tar.gz` - - `xml2st-win32-arm64.zip` - - `xml2st-win32-x64.zip` -- [ ] On macOS, the artifact is a **directory** (xml2st/ with _internal/ containing Python - runtime). The tarball must preserve this directory structure. -- [ ] On Linux and Windows, the artifact is a **single executable** (`-F` flag in PyInstaller). -- [ ] Create a GitHub Release with all 6 artifacts attached -- [ ] Tag the current development branch as `v1.0.0` - -### 1.2 matiec (Autonomy-Logic/matiec) - -**Status**: Has no CI/CD at all. Needs a complete pipeline. - -**Tasks**: - -- [ ] Create `.github/workflows/release.yml` triggered on tag push (`v*`) -- [ ] Build matrix covering 6 platform/arch combinations: - - `macos-15` (arm64), `macos-13` (x64) - - `ubuntu-22.04` (x64), `ubuntu-22.04-arm` (arm64) - - `windows-latest` (x64), Windows ARM64 (cross-compile or native runner) -- [ ] Build dependencies per platform: - - **Linux**: `sudo apt-get install -y build-essential bison flex autoconf automake` - - **macOS**: `brew install bison flex autoconf automake` (or use Xcode tools) - - **Windows**: MinGW or MSYS2 with bison, flex, autoconf, automake -- [ ] Build steps: `autoreconf -i && ./configure && make` -- [ ] Package output as `matiec-{platform}-{arch}.tar.gz` containing: - - `iec2c` binary (or `iec2c.exe` on Windows) - - `lib/` directory (all .txt files + C/ subdirectory with headers) -- [ ] Create a GitHub Release with all 6 artifacts attached -- [ ] Tag the current development branch as `v1.0.0` - -**Note on Windows ARM64**: matiec uses autotools which may need MSYS2 or cross-compilation. If a -native Windows ARM64 runner is not available, cross-compile from x64 or use the x64 binary (runs -via emulation on Windows ARM64). Evaluate feasibility during implementation. - ---- - -## Phase 2 — Download Script in openplc-editor - -### 2.1 Version manifest: `binary-versions.json` - -Create at repository root: - -```json -{ - "xml2st": { - "version": "v1.0.0", - "repository": "Autonomy-Logic/xml2st" - }, - "matiec": { - "version": "v1.0.0", - "repository": "Autonomy-Logic/matiec" - }, - "arduino-cli": { - "version": "1.1.1", - "repository": "arduino/arduino-cli" - } -} -``` - -### 2.2 Download script: `scripts/download-binaries.ts` - -**Responsibilities**: - -1. Read `binary-versions.json` -2. Determine current platform and architecture (or accept `--platform` / `--arch` overrides) -3. Construct GitHub Release download URLs: - - xml2st: `https://github.com/Autonomy-Logic/xml2st/releases/download/{tag}/xml2st-{platform}-{arch}.tar.gz` - - matiec: `https://github.com/Autonomy-Logic/matiec/releases/download/{tag}/matiec-{platform}-{arch}.tar.gz` - - arduino-cli: `https://github.com/arduino/arduino-cli/releases/download/v{version}/arduino-cli_{version}_{os}_{arch}.tar.gz` -4. Download and extract to `resources/bin/{platform}/{arch}/` -5. Copy matiec `lib/` to `resources/sources/MatIEC/lib/` -6. Write a `.binary-metadata.json` inside `resources/bin/` recording downloaded versions (for cache - checking) - -**Behavioral rules**: - -- **Cache check**: If binaries already exist and `.binary-metadata.json` matches - `binary-versions.json`, skip download. Use `--force` to override. -- **Dev mode**: Downloads only the current platform's binaries (not all 6). -- **CI mode**: Accepts `--platform` and `--arch` flags for cross-platform builds. -- **Error handling**: Clear error messages if GitHub is unreachable or a release artifact is missing. - -### 2.3 npm integration - -```jsonc -// package.json -{ - "scripts": { - "setup:binaries": "ts-node scripts/download-binaries.ts", - "postinstall": "ts-node scripts/download-binaries.ts && ts-node scripts/check-native-dep.js && electron-builder install-app-deps" - } -} -``` - -Running `npm install` will automatically fetch the correct binaries for the developer's platform. - -### 2.4 Platform / architecture mapping - -The download script must map Node.js `process.platform` and `process.arch` values to artifact -naming conventions. Each tool may use slightly different naming: - -| Node.js | xml2st artifact | matiec artifact | arduino-cli artifact | -|---|---|---|---| -| darwin/arm64 | xml2st-darwin-arm64 | matiec-darwin-arm64 | arduino-cli_{v}_macOS_ARM64 | -| darwin/x64 | xml2st-darwin-x64 | matiec-darwin-x64 | arduino-cli_{v}_macOS_64bit | -| linux/arm64 | xml2st-linux-arm64 | matiec-linux-arm64 | arduino-cli_{v}_Linux_ARM64 | -| linux/x64 | xml2st-linux-x64 | matiec-linux-x64 | arduino-cli_{v}_Linux_64bit | -| win32/arm64 | xml2st-win32-arm64 | matiec-win32-arm64 | arduino-cli_{v}_Windows_ARM64 | -| win32/x64 | xml2st-win32-x64 | matiec-win32-x64 | arduino-cli_{v}_Windows_64bit | - ---- - -## Phase 3 — Update openplc-editor CI/CD - -### 3.1 Modify `release.yml` - -Add a binary download step **before** the build step in each platform job: - -```yaml -- name: Download tool binaries - run: npx ts-node scripts/download-binaries.ts -``` - -For cross-architecture builds (e.g., Windows ARM64 on x64 runner): - -```yaml -- name: Download tool binaries (ARM64) - run: npx ts-node scripts/download-binaries.ts --platform win32 --arch arm64 -``` - -### 3.2 macOS builds - -The macOS job builds **both** x64 and arm64 in a single run. The download script must be called -twice (once per architecture), or accept a `--all-arch` flag to download both: - -```yaml -- name: Download binaries for x64 - run: npx ts-node scripts/download-binaries.ts --arch x64 - -- name: Download binaries for arm64 - run: npx ts-node scripts/download-binaries.ts --arch arm64 -``` - -### 3.3 Caching (optional optimization) - -Use GitHub Actions cache to avoid re-downloading binaries across CI runs when versions haven't -changed: - -```yaml -- name: Cache tool binaries - uses: actions/cache@v4 - with: - path: resources/bin - key: binaries-${{ runner.os }}-${{ hashFiles('binary-versions.json') }} -``` - ---- - -## Phase 4 — Remove Binaries from Git - -### 4.1 Gitignore - -Add to `.gitignore`: - -```gitignore -# External tool binaries (downloaded by scripts/download-binaries.ts) -resources/bin/ -resources/sources/MatIEC/lib/ -``` - -### 4.2 Remove tracked files - -```bash -git rm -r --cached resources/bin/ -git rm -r --cached resources/sources/MatIEC/lib/ -git commit -m "chore: remove pre-built binaries from git tracking" -``` - -### 4.3 (Optional) Rewrite git history - -The binary blobs will remain in git history forever unless history is rewritten. This is a -**destructive operation** that requires coordination with all contributors: - -```bash -# Using git-filter-repo (recommended over BFG) -git filter-repo --path resources/bin/ --invert-paths -``` - -**Recommendation**: Do this in a separate effort after the new system is proven stable. It requires -force-pushing and all contributors to re-clone. - ---- - -## Alternatives Considered - -| Alternative | Why Not | -|---|---| -| **Git submodules** | Still requires building from source after clone. Doesn't solve the pre-built binary distribution problem. | -| **Git LFS** | Binaries still stored (in LFS server); versioning is no better; adds LFS dependency for all developers. | -| **npm packages** | xml2st and matiec aren't JavaScript. Publishing native binaries as npm packages is non-standard and awkward. | -| **Build from source on npm install** | Requires bison/flex/autoconf for matiec and Python/PyInstaller for xml2st on every dev machine. Too fragile, too many dependencies. | -| **Docker-based builds** | Overkill for a desktop Electron app's development workflow. | -| **Vendored tarballs** | Still bloats the repo, just with compressed files instead of raw binaries. | - ---- - -## Key Design Decisions - -### 1. `binary-versions.json` as single source of truth - -When upgrading xml2st, change one line in this file. The PR diff clearly shows "upgraded xml2st -from v1.2.0 to v1.3.0" — no binary blobs, clear audit trail. - -### 2. Download on `postinstall` - -Makes `npm install` self-sufficient — a new developer clones the repo, runs `npm install`, and -has everything needed to develop. Includes a cache check to avoid redundant downloads. - -### 3. arduino-cli from official releases - -Arduino CLI already has proper GitHub Releases with pre-built binaries for every platform. No -changes needed to the Arduino project — just download their official artifacts. - -### 4. matiec lib/ bundled in release tarball - -The matiec release artifact includes both the `iec2c` binary and the `lib/` directory. The download -script extracts `iec2c` to `resources/bin/` and `lib/` to `resources/sources/MatIEC/lib/`. This -keeps them versioned together (they must always match). - -### 5. Platform-specific downloads in development - -A developer on macOS ARM only downloads ~70 MB (darwin-arm64 binaries) instead of ~350 MB (all -platforms). CI downloads only what it needs for the target platform. - ---- - -## Implementation Checklist - -Phases should be executed in order. Each phase can be merged independently. - -### Phase 1: Release Pipelines - -- [ ] **1.1** Create xml2st release workflow with standardized artifact names -- [ ] **1.2** Tag xml2st `v1.0.0` -- [ ] **1.3** Create matiec CI/CD build workflow for all 6 platforms -- [ ] **1.4** Create matiec release workflow with standardized artifact names -- [ ] **1.5** Tag matiec `v1.0.0` -- [ ] **1.6** Verify all 12 release artifacts (6 per tool) download and work correctly - -### Phase 2: Download Script - -- [ ] **2.1** Create `binary-versions.json` in openplc-editor repo root -- [ ] **2.2** Implement `scripts/download-binaries.ts` -- [ ] **2.3** Test on all 3 platforms (macOS, Linux, Windows) -- [ ] **2.4** Hook into `postinstall` in package.json -- [ ] **2.5** Update CLAUDE.md / README with new setup instructions - -### Phase 3: CI/CD Integration - -- [ ] **3.1** Update `release.yml` to use download script instead of committed binaries -- [ ] **3.2** Handle cross-architecture builds (macOS dual-arch, Windows ARM64) -- [ ] **3.3** Add GitHub Actions caching for binaries -- [ ] **3.4** Run a full release build and verify all 6 distribution packages work - -### Phase 4: Cleanup - -- [ ] **4.1** Add `resources/bin/` and `resources/sources/MatIEC/lib/` to `.gitignore` -- [ ] **4.2** Remove binaries from git tracking (`git rm --cached`) -- [ ] **4.3** (Optional) Rewrite git history to purge old binary blobs -- [ ] **4.4** Update contributor documentation diff --git a/docs/outdated/name-type-linking-design.md b/docs/outdated/name-type-linking-design.md deleted file mode 100644 index 047332850..000000000 --- a/docs/outdated/name-type-linking-design.md +++ /dev/null @@ -1,1608 +0,0 @@ -# Name+Type-Based Variable Linking Design - -## Executive Summary - -This document defines the technical design for migrating from UUID-based variable linking to name+type-based linking in the OpenPLC Editor, following IEC 61131-3 standards. The design addresses all six core linking rules specified in the migration plan and provides a clear implementation path for Phase 2. - -**Design Principles:** -- Case-insensitive name matching (IEC 61131-3 standard) -- Type compatibility validation -- Automatic variable creation for Function Blocks -- User-controlled reference updates on rename -- Broken link detection on type changes -- Backward compatibility with existing projects - ---- - -## 1. Core Linking Rules - -The new system must satisfy these requirements: - -### Rule 1: Element-Variable Association -Elements (contacts, coils, function block instances, variable boxes) must be associated with a variable. - -### Rule 2: Name+Type Matching -Variables are linked to elements based on **case-insensitive name AND type matching** (IEC 61131-3 standard). - -### Rule 3: Function Block Instantiation -When a Function Block is added, a variable is automatically created (Function Blocks must be instantiated). - -### Rule 4: Automatic Cleanup -If a Function Block is deleted and no other Function Block uses its variable, the variable should be deleted. - -### Rule 5: Rename Propagation -Variable renaming should prompt user to update references: -- If declined: links break -- If accepted: find-and-replace all references - -### Rule 6: Type Change Validation -Variable type changes should recheck all elements using that variable - break links if type no longer matches. - ---- - -## 2. Composite Key Data Structure - -### 2.1 Variable Reference Type - -```typescript -/** - * Composite key for identifying a variable reference in the system. - * Replaces the previous UUID-based identification. - */ -export type VariableReference = { - /** POU name where the variable is defined (for scoping) */ - pouName: string - - /** Variable name (case-insensitive matching) */ - variableName: string - - /** Variable type for compatibility checking */ - variableType: PLCVariable['type'] -} - -/** - * Normalized form for lookups and comparisons. - * Names are converted to lowercase for case-insensitive matching. - */ -export type NormalizedVariableReference = { - pouName: string // lowercase - variableName: string // lowercase - variableType: PLCVariable['type'] -} -``` - -### 2.2 Variable Scope Handling - -```typescript -/** - * Scope determines where to search for variables. - */ -export type VariableScope = 'local' | 'global' - -/** - * Extended reference that includes scope information. - */ -export type ScopedVariableReference = VariableReference & { - scope: VariableScope -} -``` - -### 2.3 Block-Variable Connection - -```typescript -/** - * Replaces the current LadderBlockConnectedVariables type. - * Removes handleTableId (ID-based) and uses name+type instead. - */ -export type BlockConnectedVariable = { - /** Handle name on the block (e.g., "EN", "IN1", "OUT") */ - handleName: string - - /** Direction of the connection */ - type: 'input' | 'output' - - /** Reference to the connected variable (name+type) */ - variableRef: VariableReference | null - - /** Cached variable data for performance (updated on sync) */ - variable: PLCVariable | undefined -} - -export type BlockConnectedVariables = BlockConnectedVariable[] -``` - -### 2.4 Element Variable Association - -```typescript -/** - * Replaces the current variable field in node data. - * Used for contacts, coils, and block instances. - */ -export type ElementVariableAssociation = { - /** Variable reference (name+type) */ - ref: VariableReference - - /** Cached variable data (updated on sync) */ - variable: PLCVariable | undefined - - /** Validation state */ - isValid: boolean - validationError?: string -} -``` - ---- - -## 3. Type Compatibility System - -### 3.1 Type Comparison Logic - -```typescript -/** - * Type compatibility result. - */ -export type TypeCompatibility = { - isCompatible: boolean - reason?: string -} - -/** - * Checks if a variable type is compatible with an expected type. - * Handles base types, derived types, user-defined types, and arrays. - */ -export function checkTypeCompatibility( - variableType: PLCVariable['type'], - expectedType: PLCVariable['type'] | string, - dataTypes: PLCDataType[], -): TypeCompatibility { - // Case 1: Expected type is a string (simple type name) - if (typeof expectedType === 'string') { - return checkSimpleTypeCompatibility(variableType, expectedType, dataTypes) - } - - // Case 2: Expected type is a full type object - return checkComplexTypeCompatibility(variableType, expectedType, dataTypes) -} - -/** - * Simple type compatibility (e.g., "BOOL", "INT", "MyFunctionBlock") - */ -function checkSimpleTypeCompatibility( - variableType: PLCVariable['type'], - expectedTypeName: string, - dataTypes: PLCDataType[], -): TypeCompatibility { - const varTypeName = getTypeName(variableType) - const expectedNormalized = expectedTypeName.toUpperCase() - const varNormalized = varTypeName.toUpperCase() - - // Direct match - if (varNormalized === expectedNormalized) { - return { isCompatible: true } - } - - // Check if expected type is a user-defined type - const userType = dataTypes.find(dt => dt.name.toUpperCase() === expectedNormalized) - if (userType) { - // For structures and enums, check if variable type matches - if (variableType.definition === 'user-data-type' && - variableType.value.toUpperCase() === expectedNormalized) { - return { isCompatible: true } - } - } - - return { - isCompatible: false, - reason: `Type mismatch: expected ${expectedTypeName}, got ${varTypeName}` - } -} - -/** - * Complex type compatibility (full type objects) - */ -function checkComplexTypeCompatibility( - variableType: PLCVariable['type'], - expectedType: PLCVariable['type'], - dataTypes: PLCDataType[], -): TypeCompatibility { - // Definition must match - if (variableType.definition !== expectedType.definition) { - return { - isCompatible: false, - reason: `Definition mismatch: expected ${expectedType.definition}, got ${variableType.definition}` - } - } - - // Value must match (case-insensitive) - const varValue = variableType.value.toUpperCase() - const expectedValue = expectedType.value.toUpperCase() - - if (varValue !== expectedValue) { - return { - isCompatible: false, - reason: `Type mismatch: expected ${expectedType.value}, got ${variableType.value}` - } - } - - // For arrays, check dimensions - if (variableType.definition === 'array' && expectedType.definition === 'array') { - // Array dimension checking would go here - // For now, we consider arrays compatible if base types match - } - - return { isCompatible: true } -} - -/** - * Extract type name from a type object. - */ -function getTypeName(type: PLCVariable['type']): string { - return type.value -} -``` - -### 3.2 Contact/Coil Type Restrictions - -```typescript -/** - * Contacts and coils require BOOL type. - */ -export function validateContactCoilType(variableType: PLCVariable['type']): TypeCompatibility { - const typeName = getTypeName(variableType).toUpperCase() - - if (typeName === 'BOOL') { - return { isCompatible: true } - } - - return { - isCompatible: false, - reason: 'Contacts and coils require BOOL type variables' - } -} -``` - -### 3.3 Block Input/Output Type Validation - -```typescript -/** - * Validates that a variable matches a block's input/output type. - */ -export function validateBlockConnectionType( - variableType: PLCVariable['type'], - blockVariableType: BlockVariant['variables'][0]['type'], - dataTypes: PLCDataType[], -): TypeCompatibility { - const expectedType = { - definition: blockVariableType.definition as PLCVariable['type']['definition'], - value: blockVariableType.value, - } - - return checkComplexTypeCompatibility(variableType, expectedType, dataTypes) -} -``` - ---- - -## 4. Variable Lookup System - -### 4.1 Lookup by Name+Type - -```typescript -/** - * Finds a variable by name and type within a scope. - * Replaces getVariableBasedOnRowIdOrVariableId. - */ -export function findVariableByNameAndType( - variables: PLCVariable[], - variableName: string, - expectedType?: PLCVariable['type'] | string, - dataTypes?: PLCDataType[], -): PLCVariable | null { - const normalizedName = variableName.toLowerCase() - - // Find all variables with matching name (case-insensitive) - const matchingVariables = variables.filter( - v => v.name.toLowerCase() === normalizedName - ) - - // If no type specified, return first match - if (!expectedType) { - return matchingVariables[0] || null - } - - // If type specified, find compatible match - for (const variable of matchingVariables) { - const compatibility = checkTypeCompatibility( - variable.type, - expectedType, - dataTypes || [] - ) - - if (compatibility.isCompatible) { - return variable - } - } - - return null -} - -/** - * Finds a variable by reference (name+type+scope). - */ -export function findVariableByReference( - ref: VariableReference, - projectData: PLCProjectData, -): PLCVariable | null { - // Determine scope - const pou = projectData.pous.find(p => p.data.name === ref.pouName) - - if (!pou) { - return null - } - - // Search in local variables - const localVar = findVariableByNameAndType( - pou.data.variables, - ref.variableName, - ref.variableType, - projectData.dataTypes - ) - - if (localVar) { - return localVar - } - - // Search in global variables - return findVariableByNameAndType( - projectData.configuration.resource.globalVariables, - ref.variableName, - ref.variableType, - projectData.dataTypes - ) -} -``` - -### 4.2 Scoped Lookup - -```typescript -/** - * Finds a variable with explicit scope control. - */ -export function findVariableInScope( - variableName: string, - scope: VariableScope, - pouName: string, - projectData: PLCProjectData, - expectedType?: PLCVariable['type'] | string, -): PLCVariable | null { - if (scope === 'global') { - return findVariableByNameAndType( - projectData.configuration.resource.globalVariables, - variableName, - expectedType, - projectData.dataTypes - ) - } - - // Local scope - const pou = projectData.pous.find(p => p.data.name === pouName) - if (!pou) { - return null - } - - return findVariableByNameAndType( - pou.data.variables, - variableName, - expectedType, - projectData.dataTypes - ) -} -``` - -### 4.3 Uniqueness Validation - -```typescript -/** - * Checks if a variable name is unique within its scope. - * IEC 61131-3 requires case-insensitive uniqueness. - */ -export function isVariableNameUnique( - variableName: string, - scope: VariableScope, - pouName: string, - projectData: PLCProjectData, - excludeVariable?: PLCVariable, -): boolean { - const normalizedName = variableName.toLowerCase() - - const variables = scope === 'global' - ? projectData.configuration.resource.globalVariables - : projectData.pous.find(p => p.data.name === pouName)?.data.variables || [] - - const conflicts = variables.filter(v => - v.name.toLowerCase() === normalizedName && - v !== excludeVariable - ) - - return conflicts.length === 0 -} -``` - ---- - -## 5. Reference Validation and Broken Link Detection - -### 5.1 Validation Result Type - -```typescript -/** - * Result of validating a variable reference. - */ -export type ValidationResult = { - isValid: boolean - variable?: PLCVariable - error?: string - errorType?: 'not-found' | 'type-mismatch' | 'scope-error' -} -``` - -### 5.2 Reference Validation - -```typescript -/** - * Validates a variable reference and returns the variable if valid. - */ -export function validateVariableReference( - ref: VariableReference, - projectData: PLCProjectData, -): ValidationResult { - const variable = findVariableByReference(ref, projectData) - - if (!variable) { - return { - isValid: false, - error: `Variable '${ref.variableName}' not found in POU '${ref.pouName}'`, - errorType: 'not-found' - } - } - - // Check type compatibility - const compatibility = checkTypeCompatibility( - variable.type, - ref.variableType, - projectData.dataTypes - ) - - if (!compatibility.isCompatible) { - return { - isValid: false, - variable, - error: compatibility.reason, - errorType: 'type-mismatch' - } - } - - return { - isValid: true, - variable - } -} -``` - -### 5.3 Broken Link Detection - -```typescript -/** - * Finds all broken references in a POU's graphical diagram. - */ -export function findBrokenReferences( - pouName: string, - projectData: PLCProjectData, - ladderFlows: LadderFlowState['ladderFlows'], -): BrokenReference[] { - const brokenRefs: BrokenReference[] = [] - - const flow = ladderFlows.find(f => f.name === pouName) - if (!flow) return brokenRefs - - flow.rungs.forEach(rung => { - rung.nodes.forEach(node => { - // Check contacts and coils - if (node.type === 'contact' || node.type === 'coil') { - const data = node.data as { variable?: ElementVariableAssociation } - if (data.variable?.ref) { - const validation = validateVariableReference(data.variable.ref, projectData) - if (!validation.isValid) { - brokenRefs.push({ - nodeId: node.id, - rungId: rung.id, - elementType: node.type, - ref: data.variable.ref, - error: validation.error!, - errorType: validation.errorType! - }) - } - } - } - - // Check blocks - if (node.type === 'block') { - const data = node.data as BlockNodeData - - // Check instance variable - if (data.variable?.ref) { - const validation = validateVariableReference(data.variable.ref, projectData) - if (!validation.isValid) { - brokenRefs.push({ - nodeId: node.id, - rungId: rung.id, - elementType: 'block-instance', - ref: data.variable.ref, - error: validation.error!, - errorType: validation.errorType! - }) - } - } - - // Check connected variables - data.connectedVariables?.forEach(conn => { - if (conn.variableRef) { - const validation = validateVariableReference(conn.variableRef, projectData) - if (!validation.isValid) { - brokenRefs.push({ - nodeId: node.id, - rungId: rung.id, - elementType: 'block-connection', - handleName: conn.handleName, - ref: conn.variableRef, - error: validation.error!, - errorType: validation.errorType! - }) - } - } - }) - } - }) - }) - - return brokenRefs -} - -export type BrokenReference = { - nodeId: string - rungId: string - elementType: 'contact' | 'coil' | 'block-instance' | 'block-connection' - handleName?: string - ref: VariableReference - error: string - errorType: 'not-found' | 'type-mismatch' | 'scope-error' -} -``` - ---- - -## 6. Variable Lifecycle Management - -### 6.1 Automatic Function Block Variable Creation - -```typescript -/** - * Creates a variable for a function block instance. - * Implements Rule 3: Function Blocks must be instantiated. - */ -export function createFunctionBlockVariable( - blockName: string, - blockType: string, - pouName: string, - projectData: PLCProjectData, -): { variable: PLCVariable; name: string } { - // Generate unique name - const baseName = blockType.toUpperCase() - let counter = 0 - let variableName = baseName - - while (!isVariableNameUnique(variableName, 'local', pouName, projectData)) { - counter++ - variableName = `${baseName}${counter}` - } - - // Create variable - const variable: PLCVariable = { - name: variableName, - class: 'local', - type: { - definition: 'derived', - value: blockType - }, - location: '', - documentation: `Instance of ${blockType}`, - debug: false - } - - return { variable, name: variableName } -} -``` - -### 6.2 Automatic Cleanup on Function Block Deletion - -```typescript -/** - * Checks if a function block variable is still in use. - * Implements Rule 4: Delete unused FB variables. - */ -export function isFunctionBlockVariableInUse( - variableName: string, - pouName: string, - ladderFlows: LadderFlowState['ladderFlows'], - fbdFlows: FBDFlowState['fbdFlows'], -): boolean { - // Check ladder flows - const ladderFlow = ladderFlows.find(f => f.name === pouName) - if (ladderFlow) { - for (const rung of ladderFlow.rungs) { - for (const node of rung.nodes) { - if (node.type === 'block') { - const data = node.data as BlockNodeData - if (data.variable?.ref?.variableName.toLowerCase() === variableName.toLowerCase()) { - return true - } - } - } - } - } - - // Check FBD flows - const fbdFlow = fbdFlows.find(f => f.name === pouName) - if (fbdFlow) { - for (const node of fbdFlow.rung.nodes) { - if (node.type === 'block') { - const data = node.data as BlockNodeData - if (data.variable?.ref?.variableName.toLowerCase() === variableName.toLowerCase()) { - return true - } - } - } - } - - return false -} - -/** - * Deletes a function block variable if no longer in use. - */ -export function cleanupFunctionBlockVariable( - variableName: string, - pouName: string, - projectData: PLCProjectData, - ladderFlows: LadderFlowState['ladderFlows'], - fbdFlows: FBDFlowState['fbdFlows'], -): boolean { - if (isFunctionBlockVariableInUse(variableName, pouName, ladderFlows, fbdFlows)) { - return false - } - - // Delete the variable - const pou = projectData.pous.find(p => p.data.name === pouName) - if (!pou) return false - - const index = pou.data.variables.findIndex( - v => v.name.toLowerCase() === variableName.toLowerCase() - ) - - if (index !== -1) { - pou.data.variables.splice(index, 1) - return true - } - - return false -} -``` - ---- - -## 7. Variable Rename Handling - -### 7.1 Find References - -```typescript -/** - * Finds all references to a variable by name (case-insensitive). - * Implements Rule 5: Rename propagation. - */ -export function findVariableReferences( - variableName: string, - pouName: string, - ladderFlows: LadderFlowState['ladderFlows'], - fbdFlows: FBDFlowState['fbdFlows'], -): VariableReferenceLocation[] { - const normalizedName = variableName.toLowerCase() - const references: VariableReferenceLocation[] = [] - - // Search ladder flows - const ladderFlow = ladderFlows.find(f => f.name === pouName) - if (ladderFlow) { - ladderFlow.rungs.forEach(rung => { - rung.nodes.forEach(node => { - if (node.type === 'contact' || node.type === 'coil') { - const data = node.data as { variable?: ElementVariableAssociation } - if (data.variable?.ref?.variableName.toLowerCase() === normalizedName) { - references.push({ - type: 'ladder', - nodeId: node.id, - rungId: rung.id, - elementType: node.type, - currentRef: data.variable.ref - }) - } - } - - if (node.type === 'block') { - const data = node.data as BlockNodeData - - // Check instance variable - if (data.variable?.ref?.variableName.toLowerCase() === normalizedName) { - references.push({ - type: 'ladder', - nodeId: node.id, - rungId: rung.id, - elementType: 'block-instance', - currentRef: data.variable.ref - }) - } - - // Check connected variables - data.connectedVariables?.forEach((conn, index) => { - if (conn.variableRef?.variableName.toLowerCase() === normalizedName) { - references.push({ - type: 'ladder', - nodeId: node.id, - rungId: rung.id, - elementType: 'block-connection', - connectionIndex: index, - currentRef: conn.variableRef - }) - } - }) - } - }) - }) - } - - // Search FBD flows (similar pattern) - const fbdFlow = fbdFlows.find(f => f.name === pouName) - if (fbdFlow) { - // Similar logic for FBD nodes... - } - - return references -} - -export type VariableReferenceLocation = { - type: 'ladder' | 'fbd' - nodeId: string - rungId?: string // ladder only - elementType: 'contact' | 'coil' | 'block-instance' | 'block-connection' | 'variable' - connectionIndex?: number // for block connections - currentRef: VariableReference -} -``` - -### 7.2 Propagate Rename - -```typescript -/** - * Updates all references to use the new variable name. - */ -export function propagateVariableRename( - oldName: string, - newName: string, - pouName: string, - references: VariableReferenceLocation[], - ladderFlows: LadderFlowState['ladderFlows'], - fbdFlows: FBDFlowState['fbdFlows'], - ladderFlowActions: any, - fbdFlowActions: any, -): void { - references.forEach(ref => { - if (ref.type === 'ladder') { - const flow = ladderFlows.find(f => f.name === pouName) - if (!flow) return - - const rung = flow.rungs.find(r => r.id === ref.rungId) - if (!rung) return - - const node = rung.nodes.find(n => n.id === ref.nodeId) - if (!node) return - - // Update the reference - const updatedRef: VariableReference = { - ...ref.currentRef, - variableName: newName - } - - if (ref.elementType === 'contact' || ref.elementType === 'coil') { - const data = node.data as { variable?: ElementVariableAssociation } - if (data.variable) { - data.variable.ref = updatedRef - } - } else if (ref.elementType === 'block-instance') { - const data = node.data as BlockNodeData - if (data.variable) { - data.variable.ref = updatedRef - } - } else if (ref.elementType === 'block-connection' && ref.connectionIndex !== undefined) { - const data = node.data as BlockNodeData - if (data.connectedVariables[ref.connectionIndex]) { - data.connectedVariables[ref.connectionIndex].variableRef = updatedRef - } - } - - // Update the node in the flow - ladderFlowActions.updateNode({ - editorName: pouName, - rungId: ref.rungId!, - nodeId: ref.nodeId, - node - }) - } - - // Similar logic for FBD... - }) -} -``` - -### 7.3 Break References on Declined Rename - -```typescript -/** - * Marks references as broken when user declines rename propagation. - */ -export function breakVariableReferences( - references: VariableReferenceLocation[], - pouName: string, - ladderFlows: LadderFlowState['ladderFlows'], - fbdFlows: FBDFlowState['fbdFlows'], - ladderFlowActions: any, - fbdFlowActions: any, -): void { - references.forEach(ref => { - if (ref.type === 'ladder') { - const flow = ladderFlows.find(f => f.name === pouName) - if (!flow) return - - const rung = flow.rungs.find(r => r.id === ref.rungId) - if (!rung) return - - const node = rung.nodes.find(n => n.id === ref.nodeId) - if (!node) return - - // Mark as invalid - if (ref.elementType === 'contact' || ref.elementType === 'coil') { - const data = node.data as { variable?: ElementVariableAssociation } - if (data.variable) { - data.variable.isValid = false - data.variable.validationError = 'Variable renamed - reference not updated' - } - } else if (ref.elementType === 'block-instance') { - const data = node.data as BlockNodeData - if (data.variable) { - data.variable.isValid = false - data.variable.validationError = 'Variable renamed - reference not updated' - } - } else if (ref.elementType === 'block-connection' && ref.connectionIndex !== undefined) { - const data = node.data as BlockNodeData - const conn = data.connectedVariables[ref.connectionIndex] - if (conn) { - conn.variable = undefined - // Keep the ref but it will fail validation - } - } - - // Update the node in the flow - ladderFlowActions.updateNode({ - editorName: pouName, - rungId: ref.rungId!, - nodeId: ref.nodeId, - node - }) - } - - // Similar logic for FBD... - }) -} -``` - ---- - -## 8. Type Change Handling - -### 8.1 Detect Type Change Impact - -```typescript -/** - * Finds all references that will be broken by a type change. - * Implements Rule 6: Type change validation. - */ -export function findReferencesAffectedByTypeChange( - variableName: string, - oldType: PLCVariable['type'], - newType: PLCVariable['type'], - pouName: string, - projectData: PLCProjectData, - ladderFlows: LadderFlowState['ladderFlows'], - fbdFlows: FBDFlowState['fbdFlows'], -): VariableReferenceLocation[] { - const allReferences = findVariableReferences(variableName, pouName, ladderFlows, fbdFlows) - const affectedReferences: VariableReferenceLocation[] = [] - - allReferences.forEach(ref => { - // Check if new type is compatible with the reference's expected type - const compatibility = checkTypeCompatibility( - newType, - ref.currentRef.variableType, - projectData.dataTypes - ) - - if (!compatibility.isCompatible) { - affectedReferences.push(ref) - } - }) - - return affectedReferences -} -``` - -### 8.2 Break References on Type Change - -```typescript -/** - * Breaks references that are no longer type-compatible. - */ -export function breakReferencesOnTypeChange( - affectedReferences: VariableReferenceLocation[], - pouName: string, - ladderFlows: LadderFlowState['ladderFlows'], - fbdFlows: FBDFlowState['fbdFlows'], - ladderFlowActions: any, - fbdFlowActions: any, -): void { - affectedReferences.forEach(ref => { - if (ref.type === 'ladder') { - const flow = ladderFlows.find(f => f.name === pouName) - if (!flow) return - - const rung = flow.rungs.find(r => r.id === ref.rungId) - if (!rung) return - - const node = rung.nodes.find(n => n.id === ref.nodeId) - if (!node) return - - // Mark as type mismatch - if (ref.elementType === 'contact' || ref.elementType === 'coil') { - const data = node.data as { variable?: ElementVariableAssociation } - if (data.variable) { - data.variable.isValid = false - data.variable.validationError = 'Variable type changed - no longer compatible' - } - } else if (ref.elementType === 'block-instance') { - const data = node.data as BlockNodeData - if (data.variable) { - data.variable.isValid = false - data.variable.validationError = 'Variable type changed - no longer compatible' - } - } else if (ref.elementType === 'block-connection' && ref.connectionIndex !== undefined) { - const data = node.data as BlockNodeData - const conn = data.connectedVariables[ref.connectionIndex] - if (conn) { - conn.variable = undefined - // Ref remains but will fail validation - } - } - - // Update the node in the flow - ladderFlowActions.updateNode({ - editorName: pouName, - rungId: ref.rungId!, - nodeId: ref.nodeId, - node - }) - } - - // Similar logic for FBD... - }) -} -``` - ---- - -## 9. Migration Strategy - -### 9.1 Project Version Detection - -```typescript -/** - * Detects if a project uses the old ID-based system. - */ -export function isLegacyProject(projectData: PLCProjectData): boolean { - // Check if any variables have IDs - const hasVariableIds = projectData.pous.some(pou => - pou.data.variables.some(v => v.id !== undefined) - ) - - const hasGlobalIds = projectData.configuration.resource.globalVariables.some( - v => v.id !== undefined - ) - - return hasVariableIds || hasGlobalIds -} -``` - -### 9.2 Migration Algorithm - -```typescript -/** - * Migrates a project from ID-based to name+type-based linking. - */ -export function migrateProjectToNameTypeLinking( - projectData: PLCProjectData, - ladderFlows: LadderFlowState['ladderFlows'], - fbdFlows: FBDFlowState['fbdFlows'], -): MigrationResult { - const result: MigrationResult = { - success: true, - migratedNodes: 0, - errors: [] - } - - // Step 1: Migrate ladder flows - ladderFlows.forEach(flow => { - const pou = projectData.pous.find(p => p.data.name === flow.name) - if (!pou) return - - flow.rungs.forEach(rung => { - rung.nodes.forEach(node => { - try { - if (node.type === 'contact' || node.type === 'coil') { - migrateContactCoilNode(node, pou, projectData) - result.migratedNodes++ - } else if (node.type === 'block') { - migrateBlockNode(node, pou, projectData) - result.migratedNodes++ - } else if (node.type === 'variable') { - migrateVariableNode(node, pou, projectData) - result.migratedNodes++ - } - } catch (error) { - result.errors.push({ - nodeId: node.id, - rungId: rung.id, - error: error instanceof Error ? error.message : String(error) - }) - } - }) - }) - }) - - // Step 2: Migrate FBD flows (similar pattern) - fbdFlows.forEach(flow => { - // Similar logic... - }) - - // Step 3: Remove IDs from variables - projectData.pous.forEach(pou => { - pou.data.variables.forEach(v => { - delete v.id - }) - }) - - projectData.configuration.resource.globalVariables.forEach(v => { - delete v.id - }) - - result.success = result.errors.length === 0 - return result -} - -function migrateContactCoilNode( - node: Node, - pou: PLCPou, - projectData: PLCProjectData -): void { - const data = node.data as any - - // Old format: { variable: { id?: string, name: string } } - // New format: { variable: ElementVariableAssociation } - - if (!data.variable || !data.variable.name) { - throw new Error('Node has no variable') - } - - const variableName = data.variable.name - - // Find the variable by name - let variable = findVariableByNameAndType( - pou.data.variables, - variableName - ) - - if (!variable) { - variable = findVariableByNameAndType( - projectData.configuration.resource.globalVariables, - variableName - ) - } - - if (!variable) { - throw new Error(`Variable '${variableName}' not found`) - } - - // Create new reference - const ref: VariableReference = { - pouName: pou.data.name, - variableName: variable.name, - variableType: variable.type - } - - // Validate type (contacts/coils need BOOL) - const validation = validateContactCoilType(variable.type) - - // Update node data - data.variable = { - ref, - variable, - isValid: validation.isCompatible, - validationError: validation.reason - } -} - -function migrateBlockNode( - node: Node, - pou: PLCPou, - projectData: PLCProjectData -): void { - const data = node.data as any - - // Migrate instance variable - if (data.variable && data.variable.name) { - const variableName = data.variable.name - - let variable = findVariableByNameAndType( - pou.data.variables, - variableName - ) - - if (!variable) { - variable = findVariableByNameAndType( - projectData.configuration.resource.globalVariables, - variableName - ) - } - - if (variable) { - const ref: VariableReference = { - pouName: pou.data.name, - variableName: variable.name, - variableType: variable.type - } - - data.variable = { - ref, - variable, - isValid: true - } - } - } - - // Migrate connected variables - if (data.connectedVariables && Array.isArray(data.connectedVariables)) { - const newConnectedVariables: BlockConnectedVariable[] = [] - - data.connectedVariables.forEach((conn: any) => { - if (!conn.variable || !conn.variable.name) { - newConnectedVariables.push({ - handleName: conn.handleId || conn.handleName, - type: conn.type, - variableRef: null, - variable: undefined - }) - return - } - - const variableName = conn.variable.name - - let variable = findVariableByNameAndType( - pou.data.variables, - variableName - ) - - if (!variable) { - variable = findVariableByNameAndType( - projectData.configuration.resource.globalVariables, - variableName - ) - } - - if (variable) { - const ref: VariableReference = { - pouName: pou.data.name, - variableName: variable.name, - variableType: variable.type - } - - newConnectedVariables.push({ - handleName: conn.handleId || conn.handleName, - type: conn.type, - variableRef: ref, - variable - }) - } else { - newConnectedVariables.push({ - handleName: conn.handleId || conn.handleName, - type: conn.type, - variableRef: null, - variable: undefined - }) - } - }) - - data.connectedVariables = newConnectedVariables - } -} - -function migrateVariableNode( - node: Node, - pou: PLCPou, - projectData: PLCProjectData -): void { - // Similar to migrateContactCoilNode - migrateContactCoilNode(node, pou, projectData) -} - -export type MigrationResult = { - success: boolean - migratedNodes: number - errors: Array<{ - nodeId: string - rungId?: string - error: string - }> -} -``` - -### 9.3 Backward Compatibility - -```typescript -/** - * Ensures new projects work with old editor versions (graceful degradation). - */ -export function ensureBackwardCompatibility(projectData: PLCProjectData): void { - // Generate IDs for all variables that don't have them - // This allows old editor versions to open new projects - - projectData.pous.forEach(pou => { - pou.data.variables.forEach(v => { - if (!v.id) { - v.id = crypto.randomUUID() - } - }) - }) - - projectData.configuration.resource.globalVariables.forEach(v => { - if (!v.id) { - v.id = crypto.randomUUID() - } - }) -} -``` - ---- - -## 10. Implementation Phases - -### Phase 2A: Core Infrastructure (Week 1-2) -1. Implement new type definitions (VariableReference, ElementVariableAssociation, etc.) -2. Implement type compatibility system -3. Implement new lookup functions (findVariableByNameAndType, etc.) -4. Add unit tests for core functions - -### Phase 2B: Graphical Editor Updates (Week 3-4) -1. Update Ladder editor components (contact, coil, block, variable) -2. Update FBD editor components -3. Replace handleTableId with name+type references -4. Update autocomplete components - -### Phase 2C: Variable Management (Week 5) -1. Update variable creation/deletion logic -2. Implement automatic FB variable creation -3. Implement automatic FB variable cleanup -4. Update variable renaming with propagation - -### Phase 2D: Validation and Synchronization (Week 6) -1. Implement reference validation -2. Implement broken link detection -3. Implement type change handling -4. Add validation UI indicators - -### Phase 2E: Migration and Testing (Week 7-8) -1. Implement project migration algorithm -2. Add migration UI/prompts -3. Comprehensive testing (unit, integration, E2E) -4. Performance testing and optimization - -### Phase 2F: Documentation and Rollout (Week 9) -1. Update user documentation -2. Create migration guide -3. Beta testing with real projects -4. Final release - ---- - -## 11. Testing Strategy - -### 11.1 Unit Tests - -```typescript -describe('Type Compatibility', () => { - it('should match identical base types', () => { - const result = checkTypeCompatibility( - { definition: 'base-type', value: 'BOOL' }, - { definition: 'base-type', value: 'BOOL' }, - [] - ) - expect(result.isCompatible).toBe(true) - }) - - it('should be case-insensitive', () => { - const result = checkTypeCompatibility( - { definition: 'base-type', value: 'bool' }, - { definition: 'base-type', value: 'BOOL' }, - [] - ) - expect(result.isCompatible).toBe(true) - }) - - it('should reject type mismatches', () => { - const result = checkTypeCompatibility( - { definition: 'base-type', value: 'INT' }, - { definition: 'base-type', value: 'BOOL' }, - [] - ) - expect(result.isCompatible).toBe(false) - expect(result.reason).toContain('Type mismatch') - }) -}) - -describe('Variable Lookup', () => { - it('should find variable by name (case-insensitive)', () => { - const variables: PLCVariable[] = [ - { name: 'MyVar', type: { definition: 'base-type', value: 'INT' }, ... } - ] - - const result = findVariableByNameAndType(variables, 'myvar') - expect(result).toBeDefined() - expect(result?.name).toBe('MyVar') - }) - - it('should return null for non-existent variable', () => { - const result = findVariableByNameAndType([], 'NonExistent') - expect(result).toBeNull() - }) -}) -``` - -### 11.2 Integration Tests - -```typescript -describe('Variable Rename Propagation', () => { - it('should update all references when user accepts', async () => { - // Setup: Create project with variable and references - // Action: Rename variable with propagation - // Assert: All references updated - }) - - it('should break references when user declines', async () => { - // Setup: Create project with variable and references - // Action: Rename variable without propagation - // Assert: References marked as broken - }) -}) - -describe('Function Block Lifecycle', () => { - it('should create variable when FB added', () => { - // Setup: Add function block to diagram - // Assert: Variable created in table - }) - - it('should delete variable when last FB removed', () => { - // Setup: Add FB, then remove it - // Assert: Variable deleted from table - }) - - it('should keep variable when other FBs use it', () => { - // Setup: Add two FBs with same variable, remove one - // Assert: Variable still exists - }) -}) -``` - -### 11.3 Migration Tests - -```typescript -describe('Project Migration', () => { - it('should migrate ID-based project to name+type', () => { - // Setup: Load legacy project with IDs - // Action: Run migration - // Assert: All nodes migrated, IDs removed - }) - - it('should handle missing variables gracefully', () => { - // Setup: Project with broken ID references - // Action: Run migration - // Assert: Errors reported, other nodes migrated - }) -}) -``` - ---- - -## 12. Performance Considerations - -### 12.1 Caching Strategy - -```typescript -/** - * Cache for variable lookups to avoid repeated searches. - */ -class VariableLookupCache { - private cache: Map = new Map() - - getCacheKey(name: string, pouName: string, scope: VariableScope): string { - return `${scope}:${pouName}:${name.toLowerCase()}` - } - - get(name: string, pouName: string, scope: VariableScope): PLCVariable | null | undefined { - return this.cache.get(this.getCacheKey(name, pouName, scope)) - } - - set(name: string, pouName: string, scope: VariableScope, variable: PLCVariable | null): void { - this.cache.set(this.getCacheKey(name, pouName, scope), variable) - } - - invalidate(): void { - this.cache.clear() - } - - invalidateScope(pouName: string, scope: VariableScope): void { - const prefix = `${scope}:${pouName}:` - for (const key of this.cache.keys()) { - if (key.startsWith(prefix)) { - this.cache.delete(key) - } - } - } -} -``` - -### 12.2 Batch Validation - -```typescript -/** - * Validates multiple references in a single pass. - */ -export function batchValidateReferences( - refs: VariableReference[], - projectData: PLCProjectData, - cache?: VariableLookupCache -): Map { - const results = new Map() - - refs.forEach(ref => { - const result = validateVariableReference(ref, projectData) - results.set(ref, result) - }) - - return results -} -``` - -### 12.3 Lazy Validation - -```typescript -/** - * Only validate references when needed (on save, compile, or user request). - * Don't validate on every keystroke. - */ -export function scheduleValidation( - pouName: string, - debounceMs: number = 500 -): void { - // Debounce validation to avoid excessive checks - // Implementation would use a debounce utility -} -``` - ---- - -## 13. UI/UX Considerations - -### 13.1 Visual Indicators - -- **Valid reference:** Normal appearance -- **Broken reference (not found):** Red border, red text -- **Type mismatch:** Yellow border, warning icon -- **Pending validation:** Gray/dimmed appearance - -### 13.2 Error Messages - -```typescript -export const ERROR_MESSAGES = { - VARIABLE_NOT_FOUND: (name: string) => - `Variable '${name}' not found. Check the variable table.`, - - TYPE_MISMATCH: (expected: string, actual: string) => - `Type mismatch: expected ${expected}, got ${actual}`, - - CONTACT_COIL_BOOL_ONLY: () => - `Contacts and coils require BOOL type variables`, - - FB_MUST_BE_INSTANTIATED: (blockType: string) => - `Function block '${blockType}' must be instantiated with a variable`, -} -``` - -### 13.3 Rename Dialog - -``` -┌─────────────────────────────────────────────┐ -│ Rename Variable │ -├─────────────────────────────────────────────┤ -│ │ -│ You renamed 'OldName' to 'NewName'. │ -│ │ -│ This variable is used in 5 locations. │ -│ │ -│ Do you want to update all references? │ -│ │ -│ ○ Yes, rename all references │ -│ ○ No, keep old references (will break) │ -│ │ -│ [Cancel] [Apply] │ -└─────────────────────────────────────────────┘ -``` - ---- - -## 14. Summary - -This design provides a complete specification for migrating from ID-based to name+type-based variable linking. Key features: - -**Strengths:** -- ✅ Follows IEC 61131-3 standards (case-insensitive names) -- ✅ Type-safe with comprehensive validation -- ✅ Handles all 6 core linking rules -- ✅ Backward compatible with migration path -- ✅ Performance-optimized with caching -- ✅ Clear error handling and user feedback - -**Implementation Complexity:** -- **High:** Touches many parts of the codebase -- **Mitigated by:** Phased approach, comprehensive testing - -**Risk Mitigation:** -- Migration algorithm preserves data -- Validation catches broken references -- Backward compatibility maintained -- Extensive test coverage planned - -**Next Steps:** -- Review and approve design -- Begin Phase 2A implementation -- Set up CI/CD for automated testing -- Create feature branch for development - ---- - -**Document Version:** 1.0 -**Date:** 2025-10-22 -**Author:** Devin (AI Assistant) -**Status:** Complete - Ready for Review diff --git a/docs/outdated/opcua-server-configuration/01-design-overview.md b/docs/outdated/opcua-server-configuration/01-design-overview.md deleted file mode 100644 index 7edaa6da3..000000000 --- a/docs/outdated/opcua-server-configuration/01-design-overview.md +++ /dev/null @@ -1,414 +0,0 @@ -# OPC-UA Server Configuration - Design Overview - -## 1. Architecture Overview - -### 1.1 System Context - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ OpenPLC Editor │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ OPC-UA Configuration UI │ │ -│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │ │ -│ │ │ General │ │ Security │ │ Users │ │ Address Space │ │ │ -│ │ │ Settings │ │ Profiles │ │ │ │ (Variables) │ │ │ -│ │ └──────────┘ └──────────┘ └──────────┘ └──────────────────┘ │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Project State (Zustand Store) │ │ -│ │ - OPC-UA config stored WITHOUT indices │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ │ -│ ▼ (on compile) │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Compiler Module │ │ -│ │ 1. xml2st generates debug.c (with variable indices) │ │ -│ │ 2. Parse debug.c to get index mapping │ │ -│ │ 3. Resolve variable references → indices │ │ -│ │ 4. Generate opcua.json with resolved indices │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────┘ - │ - ▼ -┌─────────────────────────────────────────────────────────────────────────┐ -│ OpenPLC Runtime │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ OPC-UA Plugin │ │ -│ │ Reads opcua.json configuration │ │ -│ │ Exposes variables via OPC-UA protocol │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 1.2 Configuration Flow - -The OPC-UA configuration follows a two-phase approach: - -#### Phase 1: Configuration Time (No Compilation Required) - -1. User opens OPC-UA server configuration from the Servers panel -2. User configures server settings, security profiles, users -3. User selects variables from the project's POU tree structure -4. User configures OPC-UA properties for each selected variable (node ID, permissions, etc.) -5. Configuration is saved in the project file **without variable indices** - -#### Phase 2: Compilation Time (Index Resolution) - -1. User compiles the project -2. xml2st generates `debug.c` with all variable indices -3. Compiler parses `debug.c` using existing `parseDebugFile()` function -4. Compiler matches user's selected variables to their debug indices -5. Compiler generates `opcua.json` with fully resolved indices -6. `opcua.json` is included in the compiled project package - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Configuration Flow Diagram │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ┌────────────────────┐ ┌────────────────────┐ │ -│ │ Project POUs │ │ OPC-UA Config UI │ │ -│ │ (Programs, FBs, │ ───► │ (Select Variables │ │ -│ │ Global Variables) │ │ Configure Props) │ │ -│ └────────────────────┘ └─────────┬──────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────┐ │ -│ │ Project File │ │ -│ │ (Config WITHOUT │ │ -│ │ indices) │ │ -│ └─────────┬──────────┘ │ -│ │ │ -│ ▼ [User clicks Compile] │ -│ ┌────────────────────┐ │ -│ │ xml2st │ │ -│ │ (Generates debug.c)│ │ -│ └─────────┬──────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────┐ ┌────────────────────┐ │ -│ │ parseDebugFile() │ ◄─── │ debug.c │ │ -│ │ (Extract indices) │ │ (Variable indices) │ │ -│ └─────────┬──────────┘ └────────────────────┘ │ -│ │ │ -│ ▼ │ -│ ┌────────────────────┐ ┌────────────────────┐ │ -│ │ Resolve indices │ ───► │ opcua.json │ │ -│ │ for each variable │ │ (WITH indices) │ │ -│ └────────────────────┘ └────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -## 2. Integration with Existing Systems - -### 2.1 Debugger Integration - -The OPC-UA plugin uses the **same debug variable indices** as the OpenPLC debugger. This means: - -- We reuse `parseDebugFile()` from `src/renderer/utils/parse-debug-file.ts` -- We reuse the path matching logic from the debugger implementation -- Variable indices are consistent between debugger and OPC-UA access - -### 2.2 Project Structure Integration - -OPC-UA configuration follows the same patterns as Modbus and S7Comm: - -- Configuration stored in `project.data.servers[]` array -- Protocol discriminated by `server.protocol === 'opcua'` -- Configuration in `server.opcuaServerConfig` object - -### 2.3 Compiler Integration - -The compiler module generates `opcua.json` similar to how it generates `modbus_slave.json` and `s7comm.json`: - -```typescript -// In compiler-module.ts -async handleGenerateOpcUaConfig(sourceTargetFolderPath, projectData) { - const opcuaConfig = generateOpcUaConfig( - projectData.servers, - parsedDebugVariables // From debug.c parsing - ) - if (opcuaConfig) { - await writeFile( - join(sourceTargetFolderPath, 'conf', 'opcua.json'), - opcuaConfig - ) - } -} -``` - -## 3. Data Model - -### 3.1 Editor Data Model (Stored in Project - No Indices) - -```typescript -interface OpcUaServerConfig { - server: { - enabled: boolean - name: string - applicationUri: string - productUri: string - bindAddress: string - port: number - endpointPath: string - } - - securityProfiles: OpcUaSecurityProfile[] - - security: { - serverCertificateStrategy: 'auto_self_signed' | 'custom' - serverCertificateCustom: string | null - serverPrivateKeyCustom: string | null - trustedClientCertificates: OpcUaTrustedCertificate[] - } - - users: OpcUaUser[] - - cycleTimeMs: number - - addressSpace: { - namespaceUri: string - nodes: OpcUaNodeConfig[] // Hierarchical tree of selected nodes - } -} - -// Variable/Node configuration WITHOUT index -interface OpcUaNodeConfig { - id: string // UUID - - // Reference to PLC variable (resolved to index at compile time) - pouName: string // "main", "GVL", "FB_Motor" - variablePath: string // "MOTOR_SPEED", "TON0.ET", "SENSOR.temperature" - variableType: string // IEC type: "INT", "BOOL", "REAL", etc. - - // OPC-UA node configuration - nodeId: string // "PLC.Main.MotorSpeed" - browseName: string - displayName: string - description: string - initialValue: any - permissions: { - viewer: 'r' | 'w' | 'rw' - operator: 'r' | 'w' | 'rw' - engineer: 'r' | 'w' | 'rw' - } - - // For complex types - nodeType: 'variable' | 'structure' | 'array' - children?: OpcUaNodeConfig[] // For structures: field configs - arrayLength?: number // For arrays: element count -} -``` - -### 3.2 Runtime Data Model (Generated JSON - With Indices) - -The runtime expects a JSON array with the following structure: - -```typescript -interface OpcUaRuntimeConfig { - name: string // "opcua_server" - protocol: "OPC-UA" - config: { - server: { - name: string - application_uri: string - product_uri: string - endpoint_url: string // Full URL: opc.tcp://host:port/path - security_profiles: RuntimeSecurityProfile[] - } - security: { - server_certificate_strategy: string - server_certificate_custom: string | null - server_private_key_custom: string | null - trusted_client_certificates: { id: string; pem: string }[] - } - users: RuntimeUser[] - cycle_time_ms: number - address_space: { - namespace_uri: string - variables: RuntimeVariable[] // With resolved indices - structures: RuntimeStructure[] // With resolved field indices - arrays: RuntimeArray[] // With resolved start indices - } - } -} -``` - -## 4. Variable Hierarchy Support - -### 4.1 Hierarchical Variable Structure - -PLC programs can have deeply nested variable structures: - -``` -Program: main -├── MOTOR_SPEED (INT) → Simple variable -├── IS_RUNNING (BOOL) → Simple variable -├── MOTOR_CONTROLLER (FB_MotorControl) → Function Block instance -│ ├── ENABLE (BOOL) → FB input -│ ├── SPEED_SETPOINT (INT) → FB input -│ ├── ACTUAL_SPEED (INT) → FB output -│ └── PID_CTRL (FB_PID) → Nested FB -│ ├── KP (REAL) -│ ├── KI (REAL) -│ └── OUTPUT (REAL) -├── SENSOR_DATA (T_SensorData) → Structure -│ ├── temperature (REAL) → Struct field -│ ├── pressure (REAL) → Struct field -│ └── status (T_Status) → Nested structure -│ ├── is_valid (BOOL) -│ └── error_code (INT) -├── TEMPERATURES (ARRAY[1..10] OF REAL) → Array -└── SENSOR_ARRAY (ARRAY[1..5] OF T_SensorData) → Array of structures - └── [1] (T_SensorData) - ├── temperature (REAL) - └── ... -``` - -### 4.2 Debug Path Format - -The debugger uses specific path formats for each variable type: - -| Type | Debug Path Format | Example | -|------|-------------------|---------| -| Simple | `RES0__INSTANCE.VARNAME` | `RES0__INSTANCE0.MOTOR_SPEED` | -| FB Variable | `RES0__INSTANCE.FBVAR.FIELD` | `RES0__INSTANCE0.MOTOR_CONTROLLER.ENABLE` | -| Nested FB | `RES0__INSTANCE.FB1.FB2.FIELD` | `RES0__INSTANCE0.MOTOR_CONTROLLER.PID_CTRL.KP` | -| Struct Field | `RES0__INSTANCE.VAR.value.FIELD` | `RES0__INSTANCE0.SENSOR_DATA.value.temperature` | -| Nested Struct | `RES0__INSTANCE.VAR.value.NESTED.value.FIELD` | `RES0__INSTANCE0.SENSOR_DATA.value.status.value.is_valid` | -| Array Element | `RES0__INSTANCE.VAR.value.table[i]` | `RES0__INSTANCE0.TEMPERATURES.value.table[0]` | -| Global | `CONFIG0__VARNAME` | `CONFIG0__SYSTEM_STATE` | - -### 4.3 Index Resolution at Compile Time - -```typescript -const resolveVariableIndex = ( - pouName: string, - variablePath: string, - debugVariables: DebugVariable[] -): number => { - // Build the debug path based on variable type - const debugPath = buildDebugPath(pouName, variablePath) - - // Find matching entry in parsed debug.c - const debugVar = debugVariables.find(dv => - dv.name.toUpperCase() === debugPath.toUpperCase() - ) - - if (!debugVar) { - throw new Error(`Cannot resolve index for ${pouName}:${variablePath}`) - } - - return debugVar.index -} - -const buildDebugPath = (pouName: string, variablePath: string): string => { - // Handle global variables - if (pouName === 'GVL' || pouName === 'CONFIG') { - return `CONFIG0__${variablePath.toUpperCase()}` - } - - // Handle program/FB instance variables - // The instance name format depends on resource configuration - return `RES0__INSTANCE0.${variablePath.toUpperCase()}` -} -``` - -## 5. Security Model - -### 5.1 Security Profiles - -OPC-UA supports multiple security profiles that can be enabled simultaneously: - -| Profile | Security Policy | Security Mode | Auth Methods | -|---------|-----------------|---------------|--------------| -| Insecure | None | None | Anonymous | -| Signed | Basic256Sha256 | Sign | Username, Certificate | -| Encrypted | Basic256Sha256 | SignAndEncrypt | Username, Certificate | - -### 5.2 User Roles and Permissions - -Three user roles with different access levels: - -| Role | Description | Default Variable Access | -|------|-------------|------------------------| -| Viewer | Read-only monitoring | Read only | -| Operator | Can modify operational variables | Read/Write (configurable) | -| Engineer | Full administrative access | Read/Write | - -Each variable can have custom permissions per role. - -## 6. File Structure - -``` -src/ -├── types/PLC/ -│ └── open-plc.ts # Add OPC-UA type schemas -├── utils/ -│ └── opcua/ -│ ├── generate-opcua-config.ts # JSON generator with index resolution -│ ├── bcrypt-utils.ts # Password hashing utilities -│ └── index.ts -├── renderer/ -│ ├── utils/ -│ │ └── parse-debug-file.ts # REUSE for index resolution -│ ├── store/slices/project/ -│ │ └── slice.ts # Add OPC-UA actions -│ └── components/_features/[workspace]/editor/ -│ └── server/ -│ └── opcua-server/ -│ ├── index.tsx # Main tabbed component -│ ├── tabs/ -│ │ ├── general-settings.tsx -│ │ ├── security-profiles.tsx -│ │ ├── certificates.tsx -│ │ ├── users.tsx -│ │ └── address-space.tsx -│ └── components/ -│ ├── variable-tree.tsx -│ ├── variable-config-modal.tsx -│ └── ... -└── main/modules/compiler/ - └── compiler-module.ts # Add OPC-UA JSON generation -``` - -## 7. Key Design Decisions - -### 7.1 Index Resolution at Compile Time - -**Decision**: Store variable references (pouName + variablePath) in project, resolve indices only during compilation. - -**Rationale**: -- Users can configure OPC-UA without building first -- Indices may change when program is modified -- Consistent with how debugger works - -### 7.2 Reuse Debugger Infrastructure - -**Decision**: Reuse `parseDebugFile()` and path matching logic from debugger. - -**Rationale**: -- OPC-UA plugin uses same debug indices as debugger -- Proven, tested code -- Maintains consistency - -### 7.3 Tabbed UI Interface - -**Decision**: Use tabbed interface instead of single scrollable page. - -**Rationale**: -- OPC-UA configuration is significantly more complex than Modbus/S7Comm -- Tabs help organize related settings -- Reduces cognitive load on users - -### 7.4 Tree-Based Variable Selection - -**Decision**: Display all project variables in a tree structure, allowing selection of individual leaves or entire branches (structures/arrays). - -**Rationale**: -- Supports deeply nested hierarchies (FB inside FB inside Program) -- Intuitive selection of complex types -- Consistent with how POUs are structured diff --git a/docs/outdated/opcua-server-configuration/02-ui-screen-specifications.md b/docs/outdated/opcua-server-configuration/02-ui-screen-specifications.md deleted file mode 100644 index 353ed3fb8..000000000 --- a/docs/outdated/opcua-server-configuration/02-ui-screen-specifications.md +++ /dev/null @@ -1,818 +0,0 @@ -# OPC-UA Server Configuration - UI Screen Specifications - -## 1. Overall Layout - -The OPC-UA server configuration uses a **tabbed interface** with 5 main tabs: - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ OPC-UA Server Configuration [X] Close │ -├─────────────────────────────────────────────────────────────────────────┤ -│ ┌──────────┬──────────┬──────────┬──────────┬───────────────────────┐ │ -│ │ General │ Security │ Users │ Certifi- │ Address Space │ │ -│ │ Settings │ Profiles │ │ cates │ │ │ -│ └──────────┴──────────┴──────────┴──────────┴───────────────────────┘ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ │ │ -│ │ [Active Tab Content] │ │ -│ │ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 2. Tab 1: General Settings - -### 2.1 Layout - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ General Settings │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Enable OPC-UA Server [Toggle: Off] │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Server Identity │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Server Name │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ OpenPLC OPC UA Server │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Application URI │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ urn:openplc:opcua:server │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ (Unique identifier for this application instance) │ -│ │ -│ Product URI │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ urn:openplc:runtime │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ (Identifier for the product type) │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Network Configuration │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Bind Address Port │ -│ ┌─────────────────────────┐ ┌─────────────────────────┐ │ -│ │ 0.0.0.0 [▼]│ │ 4840 │ │ -│ └─────────────────────────┘ └─────────────────────────┘ │ -│ (0.0.0.0 = all interfaces) (Default: 4840) │ -│ │ -│ Endpoint Path │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ /openplc/opcua │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Full Endpoint URL: │ -│ opc.tcp://0.0.0.0:4840/openplc/opcua │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Performance │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Synchronization Cycle Time (ms) │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 100 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ (Lower values = faster updates, higher CPU usage. Range: 10-10000) │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 2.2 Fields Specification - -| Field | Type | Default | Validation | Description | -|-------|------|---------|------------|-------------| -| enabled | boolean | false | - | Master switch for OPC-UA server | -| name | string | "OpenPLC OPC UA Server" | max 128 chars | Human-readable server name | -| applicationUri | string | "urn:openplc:opcua:server" | valid URI | Unique application identifier | -| productUri | string | "urn:openplc:runtime" | valid URI | Product type identifier | -| bindAddress | string | "0.0.0.0" | valid IP | Network interface to bind | -| port | number | 4840 | 1-65535 | TCP port number | -| endpointPath | string | "/openplc/opcua" | starts with / | URL path component | -| cycleTimeMs | number | 100 | 10-10000 | Sync interval in milliseconds | - ---- - -## 3. Tab 2: Security Profiles - -### 3.1 Layout - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Security Profiles │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Configure which security profiles clients can use to connect. │ -│ At least one profile must be enabled. │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ [+] Add Security Profile │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ [✓] Insecure (No Security) [Edit] [Del] │ │ -│ │ Policy: None | Mode: None │ │ -│ │ Authentication: Anonymous │ │ -│ │ ⚠ Warning: No encryption or authentication. Use only for │ │ -│ │ development/testing. │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ [ ] Signed [Edit] [Del] │ │ -│ │ Policy: Basic256Sha256 | Mode: Sign │ │ -│ │ Authentication: Username, Certificate │ │ -│ │ Messages are signed but not encrypted. │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ [ ] Signed & Encrypted [Edit] [Del] │ │ -│ │ Policy: Basic256Sha256 | Mode: SignAndEncrypt │ │ -│ │ Authentication: Username, Certificate │ │ -│ │ Full security: messages are signed and encrypted. │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ Note: Security profiles with Certificate authentication require │ -│ trusted client certificates to be configured in the Certificates tab. │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 3.2 Add/Edit Security Profile Modal - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Add Security Profile [X] │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Profile Name │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ signed_encrypted │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ (Unique identifier for this profile) │ -│ │ -│ Enabled [Toggle: On] │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ Security Settings │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ Security Policy │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Basic256Sha256 [▼] │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ Options: None, Basic128Rsa15, Basic256, Basic256Sha256 │ -│ │ -│ Security Mode │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ SignAndEncrypt [▼] │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ Options: None, Sign, SignAndEncrypt │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ Authentication Methods │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ [ ] Anonymous │ -│ (Only available with Security Policy: None) │ -│ │ -│ [✓] Username / Password │ -│ (Users must be configured in the Users tab) │ -│ │ -│ [✓] Certificate │ -│ (Client certificates must be added to trusted list) │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ [Cancel] [Save Profile] │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 3.3 Validation Rules - -- At least one security profile must be enabled -- "Anonymous" authentication only available when Security Policy = "None" -- Security Mode "None" requires Security Policy "None" -- Profile names must be unique - ---- - -## 4. Tab 3: Users - -### 4.1 Layout - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ User Accounts │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Configure user accounts for OPC-UA client authentication. │ -│ At least one user is required when Username authentication is enabled. │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ [+] Add User │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 👤 viewer [Edit] [Del] │ │ -│ │ Type: Password Authentication │ │ -│ │ Role: Viewer (Read-only access) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 👤 operator [Edit] [Del] │ │ -│ │ Type: Password Authentication │ │ -│ │ Role: Operator (Read/Write per variable permissions) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 👤 engineer [Edit] [Del] │ │ -│ │ Type: Password Authentication │ │ -│ │ Role: Engineer (Full access) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 🔐 engineer_cert [Edit] [Del] │ │ -│ │ Type: Certificate Authentication │ │ -│ │ Certificate: engineer_client │ │ -│ │ Role: Engineer (Full access) │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ Role Descriptions: │ -│ • Viewer: Can only read variable values (monitoring) │ -│ • Operator: Can read/write based on per-variable permissions │ -│ • Engineer: Full administrative access to all variables │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 4.2 Add/Edit User Modal - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Add User [X] │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Authentication Type │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Password [▼] │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ Options: Password, Certificate │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Password Authentication (visible when type=Password)│ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Username │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ operator │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Password │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ •••••••• [👁]│ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Confirm Password │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ •••••••• [👁]│ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Certificate Authentication (visible when type=Certificate)│ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Client Certificate │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ engineer_client [▼] │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ (Select from trusted certificates configured in Certificates tab) │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ User Role │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Role │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Operator [▼] │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ Options: Viewer, Operator, Engineer │ -│ │ -│ Role Permissions: │ -│ • Viewer: Read-only access to all variables │ -│ • Operator: Read/Write based on per-variable permission settings │ -│ • Engineer: Full administrative access │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ [Cancel] [Save User] │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 4.3 Password Handling - -Passwords are hashed using bcrypt before storage: - -```typescript -import bcrypt from 'bcryptjs' - -const hashPassword = async (password: string): Promise => { - const salt = await bcrypt.genSalt(12) - return bcrypt.hash(password, salt) -} -``` - -The plain text password is **never** stored in the project file. - ---- - -## 5. Tab 4: Certificates - -### 5.1 Layout - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Certificate Management │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Server Certificate │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Certificate Strategy │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Auto-generate Self-Signed [▼] │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ Options: Auto-generate Self-Signed, Use Custom Certificate │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ Custom Certificate (visible when strategy=custom) │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ Server Certificate (PEM format) │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ -----BEGIN CERTIFICATE----- │ │ -│ │ MIIEpDCCAowCCQC7... │ │ -│ │ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ [Browse File...] │ -│ │ -│ Server Private Key (PEM format) │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ -----BEGIN PRIVATE KEY----- │ │ -│ │ MIIEvgIBADANBg... │ │ -│ │ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ [Browse File...] │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Trusted Client Certificates │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Client certificates that are trusted for certificate authentication. │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ [+] Add Trusted Certificate │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 📜 engineer_client [View] [Del] │ │ -│ │ Subject: CN=Engineer Client, O=MyCompany │ │ -│ │ Valid: 2024-01-01 to 2025-12-31 │ │ -│ │ Fingerprint: A1:B2:C3:D4:E5:F6:... │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 📜 scada_client [View] [Del] │ │ -│ │ Subject: CN=SCADA System, O=Industrial Corp │ │ -│ │ Valid: 2024-06-01 to 2026-06-01 │ │ -│ │ Fingerprint: 12:34:56:78:9A:BC:... │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 5.2 Add Trusted Certificate Modal - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Add Trusted Client Certificate [X] │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Certificate ID │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ engineer_client │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ (Unique identifier used to reference this certificate in user config) │ -│ │ -│ Certificate (PEM format) │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ -----BEGIN CERTIFICATE----- │ │ -│ │ MIIEpDCCAowCCQC7... │ │ -│ │ │ │ -│ │ │ │ -│ │ │ │ -│ │ │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ [Browse File...] │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ Certificate Details (parsed from PEM) │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ Subject: CN=Engineer Client, O=MyCompany │ -│ Issuer: CN=My CA, O=MyCompany │ -│ Valid From: 2024-01-01 00:00:00 UTC │ -│ Valid To: 2025-12-31 23:59:59 UTC │ -│ Fingerprint: A1:B2:C3:D4:E5:F6:78:90:AB:CD:EF:12:34:56:78:90 │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ [Cancel] [Add Certificate] │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - ---- - -## 6. Tab 5: Address Space (Variable Selection) - -### 6.1 Overview - -The Address Space tab allows users to select which PLC variables to expose via OPC-UA. It displays **all project variables in a hierarchical tree structure**, supporting: - -- Simple variables (INT, BOOL, REAL, etc.) -- Function Block instances with their internal variables -- Nested Function Blocks (FB inside FB inside Program) -- Structures with fields -- Nested Structures (struct inside struct) -- Arrays of any type -- Arrays of structures - -### 6.2 Main Layout - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Address Space Configuration │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Namespace URI │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ urn:openplc:opcua:namespace │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ Select variables to expose via OPC-UA. You can select individual │ -│ variables (leaves) or entire structures/arrays/function blocks │ -│ (branches). Variable indices are resolved automatically during │ -│ project compilation. │ -│ │ -│ ┌────────────────────────────────┬────────────────────────────────┐ │ -│ │ Available PLC Variables │ Selected for OPC-UA │ │ -│ │ (from project) │ │ │ -│ ├────────────────────────────────┼────────────────────────────────┤ │ -│ │ ▼ 📁 main (Program) │ ▼ PLC.Main.MotorSpeed │ │ -│ │ ├── ☐ MOTOR_SPEED (INT) │ │ main:MOTOR_SPEED │ │ -│ │ ├── ☐ IS_RUNNING (BOOL) │ │ Type: INT │ │ -│ │ ├── ▶ ☐ TON0 (TON) │ │ Perms: V:r O:rw E:rw │ │ -│ │ ├── ▶ ☐ MOTOR_CTRL (FB_Mot) │ ├────────────────────────────│ │ -│ │ ├── ▶ ☐ SENSOR (T_Sensor) │ ▼ PLC.Main.MOTOR_CTRL │ │ -│ │ └── ▶ ☐ TEMPS (ARRAY[1..10])│ │ main:MOTOR_CTRL (FB) │ │ -│ │ ▼ 📁 GVL (Global Variables) │ │ [Entire FB selected] │ │ -│ │ ├── ☐ SYSTEM_STATE (INT) │ ├── ENABLE (BOOL) │ │ -│ │ └── ☐ ERROR_CODE (DINT) │ ├── SPEED_SP (INT) │ │ -│ │ ► 📁 FB_MotorControl (FB) │ └── PID_CTRL.OUTPUT (REAL) │ │ -│ │ ► 📁 FB_PID (FB) │ ─ PLC.GVL.SystemState │ │ -│ │ │ │ GVL:SYSTEM_STATE │ │ -│ │ │ │ Type: INT │ │ -│ │ │ │ │ -│ ├────────────────────────────────┼────────────────────────────────┤ │ -│ │ [Filter: ________] │ │ │ -│ │ │ [↑] [↓] [Edit] [Remove] │ │ -│ │ [Add Selected →] │ │ │ -│ └────────────────────────────────┴────────────────────────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 6.3 Variable Tree Structure - -The left panel shows all project variables in a tree: - -``` -▼ 📁 main (Program) - ├── MOTOR_SPEED (INT) ← Simple variable (leaf) - ├── IS_RUNNING (BOOL) ← Simple variable (leaf) - ├── ▼ TON0 (TON) ← Standard FB instance - │ ├── IN (BOOL) ← FB input - │ ├── PT (TIME) ← FB input - │ ├── Q (BOOL) ← FB output - │ └── ET (TIME) ← FB output - ├── ▼ MOTOR_CTRL (FB_MotorControl) ← Custom FB instance - │ ├── ENABLE (BOOL) ← FB variable - │ ├── SPEED_SETPOINT (INT) ← FB variable - │ ├── ACTUAL_SPEED (INT) ← FB variable - │ └── ▼ PID_CTRL (FB_PID) ← Nested FB (2 levels deep) - │ ├── KP (REAL) - │ ├── KI (REAL) - │ ├── KD (REAL) - │ └── OUTPUT (REAL) - ├── ▼ SENSOR (T_SensorData) ← Structure instance - │ ├── temperature (REAL) ← Struct field - │ ├── pressure (REAL) ← Struct field - │ └── ▼ status (T_Status) ← Nested structure - │ ├── is_valid (BOOL) - │ └── error_code (INT) - ├── ▼ TEMPS (ARRAY[1..10] OF REAL) ← Array - │ ├── [1] (REAL) - │ ├── [2] (REAL) - │ └── ... (10 elements) - └── ▼ SENSORS (ARRAY[1..5] OF T_SensorData) ← Array of structures - ├── ▼ [1] (T_SensorData) - │ ├── temperature (REAL) - │ ├── pressure (REAL) - │ └── ▼ status (T_Status) - │ └── ... - └── ... (5 elements) - -▼ 📁 GVL (Global Variables) - ├── SYSTEM_STATE (INT) - ├── ERROR_CODE (DINT) - └── ▼ CONFIG (T_SystemConfig) - └── ... - -► 📁 FB_MotorControl (Function Block Definition) - └── (Shows FB type definition - variables not directly selectable) - -► 📁 FB_PID (Function Block Definition) - └── (Shows FB type definition) -``` - -### 6.4 Selection Behavior - -Users can select: - -1. **Individual Leaves**: Select a single simple variable -2. **Entire Branches**: Select a structure, array, or FB to include all children - -``` -Selection Examples: - -☐ MOTOR_SPEED (INT) → Selects only this variable -☑ MOTOR_CTRL (FB_MotorControl) → Selects entire FB with all nested variables - ☑ ENABLE (BOOL) (auto-selected as part of parent) - ☑ SPEED_SETPOINT (INT) (auto-selected as part of parent) - ☑ PID_CTRL (FB_PID) (auto-selected as part of parent) - ☑ KP (REAL) (auto-selected, nested) - ☑ OUTPUT (REAL) (auto-selected, nested) - -Partial selection is NOT allowed - either select the parent or individual children -``` - -### 6.5 Edit Variable/Node Modal - -When editing a selected variable/node: - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Configure OPC-UA Node [X] │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ PLC Variable: main:MOTOR_SPEED (INT) │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ OPC-UA Node Configuration │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Node ID │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ PLC.Main.MotorSpeed │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ (Unique identifier in OPC-UA address space) │ -│ │ -│ Browse Name │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ MotorSpeed │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Display Name │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Motor Speed │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Description │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Current motor speed in RPM │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Initial Value │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ 0 │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Access Permissions │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Role Access Level │ -│ ───────────────────────────────────────────────────────────────────── │ -│ Viewer [Read Only ▼] │ -│ Operator [Read/Write ▼] │ -│ Engineer [Read/Write ▼] │ -│ │ -│ Options: Read Only, Write Only, Read/Write │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ [Cancel] [Save] │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 6.6 Edit Structure/FB Modal - -When editing a structure or function block (branch selection): - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Configure OPC-UA Structure Node [X] │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ PLC Variable: main:SENSOR (T_SensorData) │ -│ Type: Structure │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Structure Node Configuration │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Node ID │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ PLC.Main.Sensor │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Browse Name │ Display Name │ -│ ┌────────────────────┼────────────────────────────────────────────┐ │ -│ │ Sensor │ Sensor Data │ │ -│ └────────────────────┴────────────────────────────────────────────┘ │ -│ │ -│ Description │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Main sensor data structure │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Field Configuration │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Field │ Type │ Initial │ Viewer │ Operator │ Engr │ │ -│ ├─────────────────┼───────┼─────────┼────────┼──────────┼───────┤ │ -│ │ temperature │ REAL │ 0.0 │ r │ r │ rw │ │ -│ │ pressure │ REAL │ 0.0 │ r │ r │ rw │ │ -│ │ status.is_valid │ BOOL │ false │ r │ r │ r │ │ -│ │ status.error_co │ INT │ 0 │ r │ r │ rw │ │ -│ └─────────────────┴───────┴─────────┴────────┴──────────┴───────┘ │ -│ │ -│ [Edit Field Permissions...] │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ [Cancel] [Save] │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 6.7 Edit Array Modal - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Configure OPC-UA Array Node [X] │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ PLC Variable: main:TEMPS (ARRAY[1..10] OF REAL) │ -│ Type: Array │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Array Node Configuration │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Node ID │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ PLC.Main.Temperatures │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ Browse Name │ Display Name │ -│ ┌────────────────────┼────────────────────────────────────────────┐ │ -│ │ Temperatures │ Temperature Readings │ │ -│ └────────────────────┴────────────────────────────────────────────┘ │ -│ │ -│ Description │ -│ ┌─────────────────────────────────────────────────────────────────┐ │ -│ │ Array of temperature sensor values │ │ -│ └─────────────────────────────────────────────────────────────────┘ │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ Array Properties │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ Element Type: REAL │ -│ Array Length: 10 (indices 1 to 10) │ -│ Initial Value: 0.0 (applied to all elements) │ -│ │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ Access Permissions (applies to entire array) │ -│ ═══════════════════════════════════════════════════════════════════ │ -│ │ -│ Viewer: [Read Only ▼] │ -│ Operator: [Read/Write ▼] │ -│ Engineer: [Read/Write ▼] │ -│ │ -│ ───────────────────────────────────────────────────────────────────── │ -│ │ -│ [Cancel] [Save] │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 6.8 Deep Nesting Example - -For deeply nested structures (FB inside FB, struct inside struct), the tree expands fully: - -``` -▼ 📁 main (Program) - └── ▼ PROCESS_CTRL (FB_ProcessController) - ├── ENABLE (BOOL) - ├── MODE (INT) - └── ▼ HEATER_CTRL (FB_HeaterController) - ├── SETPOINT (REAL) - ├── ACTUAL (REAL) - └── ▼ PID (FB_PID) - ├── KP (REAL) - ├── KI (REAL) - ├── KD (REAL) - ├── ERROR (REAL) - └── OUTPUT (REAL) - -When user selects PROCESS_CTRL, all nested items are included. -The variablePath for deepest item would be: - "PROCESS_CTRL.HEATER_CTRL.PID.OUTPUT" -``` - ---- - -## 7. Responsive Design Considerations - -### 7.1 Minimum Window Size - -- Minimum width: 800px -- Minimum height: 600px - -### 7.2 Panel Resizing - -The Address Space tab should support resizable panels: -- Left panel (Available Variables): min 250px, max 50% -- Right panel (Selected Variables): min 250px, remaining space - -### 7.3 Tree Virtualization - -For projects with many variables, implement virtualized tree rendering: -- Use react-window or similar for large lists -- Lazy-load children when expanding nodes -- Show loading indicator while expanding complex FBs - ---- - -## 8. Accessibility - -### 8.1 Keyboard Navigation - -- Tab: Move between panels and controls -- Arrow keys: Navigate tree structure -- Space: Toggle checkbox selection -- Enter: Expand/collapse tree node or open edit modal - -### 8.2 Screen Reader Support - -- Proper ARIA labels on all interactive elements -- Tree structure uses role="tree" and role="treeitem" -- Selection state announced: "Selected" / "Not selected" - -### 8.3 Color Contrast - -- All text meets WCAG 2.1 AA contrast requirements -- Icons have sufficient contrast or text alternatives -- Error states use both color and icon indicators diff --git a/docs/outdated/opcua-server-configuration/03-json-configuration-mapping.md b/docs/outdated/opcua-server-configuration/03-json-configuration-mapping.md deleted file mode 100644 index 23a3412b0..000000000 --- a/docs/outdated/opcua-server-configuration/03-json-configuration-mapping.md +++ /dev/null @@ -1,959 +0,0 @@ -# OPC-UA Server Configuration - JSON Configuration Mapping - -This document describes how the editor configuration maps to the runtime JSON format expected by the OPC-UA plugin. - -## 1. Overview - -The OPC-UA configuration undergoes transformation during compilation: - -``` -┌─────────────────────────┐ ┌─────────────────────────┐ -│ Editor Format │ │ Runtime Format │ -│ (Project File) │ ──► │ (opcua.json) │ -├─────────────────────────┤ ├─────────────────────────┤ -│ • camelCase properties │ │ • snake_case properties │ -│ • Variable references │ │ • Resolved indices │ -│ • No indices │ │ • Full variable info │ -│ • TypeScript types │ │ • JSON structure │ -└─────────────────────────┘ └─────────────────────────┘ -``` - -## 2. Editor Data Model (Stored in Project) - -### 2.1 Complete Type Definitions - -```typescript -// ═══════════════════════════════════════════════════════════════════════ -// Security Profile -// ═══════════════════════════════════════════════════════════════════════ - -interface OpcUaSecurityProfile { - id: string // UUID - name: string // Profile identifier (e.g., "insecure", "signed") - enabled: boolean - securityPolicy: 'None' | 'Basic128Rsa15' | 'Basic256' | 'Basic256Sha256' - securityMode: 'None' | 'Sign' | 'SignAndEncrypt' - authMethods: ('Anonymous' | 'Username' | 'Certificate')[] -} - -// ═══════════════════════════════════════════════════════════════════════ -// Trusted Certificate -// ═══════════════════════════════════════════════════════════════════════ - -interface OpcUaTrustedCertificate { - id: string // Certificate identifier (referenced by users) - pem: string // PEM-encoded certificate content - // Derived fields for display (parsed from PEM) - subject?: string - validFrom?: string - validTo?: string - fingerprint?: string -} - -// ═══════════════════════════════════════════════════════════════════════ -// User Account -// ═══════════════════════════════════════════════════════════════════════ - -interface OpcUaUser { - id: string // UUID - type: 'password' | 'certificate' - // For password auth - username: string | null - passwordHash: string | null // bcrypt hash (never plain text) - // For certificate auth - certificateId: string | null // References trustedCertificate.id - // Common - role: 'viewer' | 'operator' | 'engineer' -} - -// ═══════════════════════════════════════════════════════════════════════ -// Permissions -// ═══════════════════════════════════════════════════════════════════════ - -interface OpcUaPermissions { - viewer: 'r' | 'w' | 'rw' - operator: 'r' | 'w' | 'rw' - engineer: 'r' | 'w' | 'rw' -} - -// ═══════════════════════════════════════════════════════════════════════ -// Address Space Node (Variable/Structure/Array) -// ═══════════════════════════════════════════════════════════════════════ - -interface OpcUaNodeConfig { - id: string // UUID for tracking - - // PLC variable reference (resolved to index at compile time) - pouName: string // "main", "GVL", "FB_Motor" - variablePath: string // "MOTOR_SPEED", "CTRL.PID.OUTPUT" - variableType: string // IEC type - - // OPC-UA configuration - nodeId: string // OPC-UA node identifier - browseName: string - displayName: string - description: string - initialValue: boolean | number | string - permissions: OpcUaPermissions - - // Type classification - nodeType: 'variable' | 'structure' | 'array' - - // For structures: nested field configurations - fields?: OpcUaFieldConfig[] - - // For arrays: element count (derived from type) - arrayLength?: number - elementType?: string -} - -interface OpcUaFieldConfig { - fieldPath: string // "temperature", "status.is_valid" - displayName: string - initialValue: boolean | number | string - permissions: OpcUaPermissions -} - -// ═══════════════════════════════════════════════════════════════════════ -// Complete Server Configuration -// ═══════════════════════════════════════════════════════════════════════ - -interface OpcUaServerConfig { - server: { - enabled: boolean - name: string - applicationUri: string - productUri: string - bindAddress: string - port: number - endpointPath: string - } - - securityProfiles: OpcUaSecurityProfile[] - - security: { - serverCertificateStrategy: 'auto_self_signed' | 'custom' - serverCertificateCustom: string | null - serverPrivateKeyCustom: string | null - trustedClientCertificates: OpcUaTrustedCertificate[] - } - - users: OpcUaUser[] - - cycleTimeMs: number - - addressSpace: { - namespaceUri: string - nodes: OpcUaNodeConfig[] - } -} -``` - -## 3. Runtime JSON Format (Generated) - -### 3.1 Complete Structure - -The runtime expects a JSON array containing plugin configurations: - -```json -[ - { - "name": "opcua_server", - "protocol": "OPC-UA", - "config": { - "server": { - "name": "string", - "application_uri": "string", - "product_uri": "string", - "endpoint_url": "string", - "security_profiles": [ - { - "name": "string", - "enabled": true, - "security_policy": "string", - "security_mode": "string", - "auth_methods": ["string"] - } - ] - }, - "security": { - "server_certificate_strategy": "string", - "server_certificate_custom": "string|null", - "server_private_key_custom": "string|null", - "trusted_client_certificates": [ - { - "id": "string", - "pem": "string" - } - ] - }, - "users": [ - { - "type": "string", - "username": "string|null", - "password_hash": "string|null", - "certificate_id": "string|null", - "role": "string" - } - ], - "cycle_time_ms": 100, - "address_space": { - "namespace_uri": "string", - "variables": [...], - "structures": [...], - "arrays": [...] - } - } - } -] -``` - -### 3.2 Address Space Variable Format - -```json -{ - "node_id": "PLC.Main.MotorSpeed", - "browse_name": "MotorSpeed", - "display_name": "Motor Speed", - "datatype": "INT", - "initial_value": 0, - "description": "Current motor speed in RPM", - "index": 5, - "permissions": { - "viewer": "r", - "operator": "rw", - "engineer": "rw" - } -} -``` - -### 3.3 Address Space Structure Format - -```json -{ - "node_id": "PLC.Main.Sensor", - "browse_name": "Sensor", - "display_name": "Sensor Data", - "description": "Main sensor data structure", - "fields": [ - { - "name": "temperature", - "datatype": "REAL", - "initial_value": 0.0, - "index": 10, - "permissions": { - "viewer": "r", - "operator": "r", - "engineer": "rw" - } - }, - { - "name": "pressure", - "datatype": "REAL", - "initial_value": 0.0, - "index": 11, - "permissions": { - "viewer": "r", - "operator": "r", - "engineer": "rw" - } - }, - { - "name": "status.is_valid", - "datatype": "BOOL", - "initial_value": false, - "index": 12, - "permissions": { - "viewer": "r", - "operator": "r", - "engineer": "r" - } - } - ] -} -``` - -### 3.4 Address Space Array Format - -```json -{ - "node_id": "PLC.Main.Temperatures", - "browse_name": "Temperatures", - "display_name": "Temperature Readings", - "datatype": "REAL", - "length": 10, - "initial_value": 0.0, - "index": 20, - "permissions": { - "viewer": "r", - "operator": "rw", - "engineer": "rw" - } -} -``` - -## 4. Field Mapping Reference - -### 4.1 Server Settings - -| Editor Field | Runtime JSON Field | Transformation | -|--------------|-------------------|----------------| -| server.enabled | (not in JSON) | Only generate JSON if enabled | -| server.name | config.server.name | Direct copy | -| server.applicationUri | config.server.application_uri | camelCase → snake_case | -| server.productUri | config.server.product_uri | camelCase → snake_case | -| server.bindAddress | config.server.endpoint_url | Combined into URL | -| server.port | config.server.endpoint_url | Combined into URL | -| server.endpointPath | config.server.endpoint_url | Combined into URL | - -**Endpoint URL Construction:** -```typescript -const endpointUrl = `opc.tcp://${bindAddress}:${port}${endpointPath}` -// Example: opc.tcp://0.0.0.0:4840/openplc/opcua -``` - -### 4.2 Security Profiles - -| Editor Field | Runtime JSON Field | Transformation | -|--------------|-------------------|----------------| -| securityProfile.name | security_profiles[].name | Direct copy | -| securityProfile.enabled | security_profiles[].enabled | Direct copy | -| securityProfile.securityPolicy | security_profiles[].security_policy | camelCase → snake_case | -| securityProfile.securityMode | security_profiles[].security_mode | camelCase → snake_case | -| securityProfile.authMethods | security_profiles[].auth_methods | camelCase → snake_case | - -**Note:** Only enabled profiles are included in the output. - -### 4.3 Security Settings - -| Editor Field | Runtime JSON Field | Transformation | -|--------------|-------------------|----------------| -| security.serverCertificateStrategy | security.server_certificate_strategy | camelCase → snake_case | -| security.serverCertificateCustom | security.server_certificate_custom | camelCase → snake_case | -| security.serverPrivateKeyCustom | security.server_private_key_custom | camelCase → snake_case | -| security.trustedClientCertificates | security.trusted_client_certificates | Map to {id, pem} only | - -### 4.4 Users - -| Editor Field | Runtime JSON Field | Transformation | -|--------------|-------------------|----------------| -| user.type | users[].type | Direct copy | -| user.username | users[].username | Direct copy | -| user.passwordHash | users[].password_hash | camelCase → snake_case | -| user.certificateId | users[].certificate_id | camelCase → snake_case | -| user.role | users[].role | Direct copy | - -### 4.5 Address Space Variables - -| Editor Field | Runtime JSON Field | Transformation | -|--------------|-------------------|----------------| -| node.nodeId | variables[].node_id | camelCase → snake_case | -| node.browseName | variables[].browse_name | camelCase → snake_case | -| node.displayName | variables[].display_name | camelCase → snake_case | -| node.variableType | variables[].datatype | Renamed | -| node.initialValue | variables[].initial_value | camelCase → snake_case | -| node.description | variables[].description | Direct copy | -| (resolved) | variables[].index | **Resolved from debug.c** | -| node.permissions | variables[].permissions | Direct copy | - -## 5. Index Resolution Process - -### 5.1 Overview - -Variable indices are resolved during compilation by matching the editor's variable references to entries in the generated `debug.c` file. - -``` -┌─────────────────────────────────────────────────────────────────────────┐ -│ Index Resolution Flow │ -├─────────────────────────────────────────────────────────────────────────┤ -│ │ -│ Editor Config debug.c Runtime JSON │ -│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ -│ │ pouName: │ │ debug_vars[]│ │ index: 5 │ │ -│ │ "main" │ Match │ = { │ Copy │ datatype: │ │ -│ │ variablePath│ ──────────► │ [5] VAR │ ──────► │ "INT" │ │ -│ │ "SPEED" │ │ ... │ │ │ │ -│ └─────────────┘ └─────────────┘ └─────────────┘ │ -│ │ -└─────────────────────────────────────────────────────────────────────────┘ -``` - -### 5.2 Path Building Rules - -Different variable types require different debug path formats: - -#### Simple Variables (Program/FB Instance) - -```typescript -// Editor: pouName="main", variablePath="MOTOR_SPEED" -// Debug path: RES0__INSTANCE0.MOTOR_SPEED -const buildSimplePath = (pouName: string, varPath: string): string => { - return `RES0__INSTANCE0.${varPath.toUpperCase()}` -} -``` - -#### Global Variables - -```typescript -// Editor: pouName="GVL", variablePath="SYSTEM_STATE" -// Debug path: CONFIG0__SYSTEM_STATE -const buildGlobalPath = (varPath: string): string => { - return `CONFIG0__${varPath.toUpperCase()}` -} -``` - -#### Structure Fields - -```typescript -// Editor: pouName="main", variablePath="SENSOR.temperature" -// Debug path: RES0__INSTANCE0.SENSOR.value.TEMPERATURE -const buildStructFieldPath = (pouName: string, varPath: string): string => { - const parts = varPath.split('.') - const structVar = parts[0] - const fieldPath = parts.slice(1).join('.value.') - return `RES0__INSTANCE0.${structVar.toUpperCase()}.value.${fieldPath.toUpperCase()}` -} -``` - -#### Nested Structure Fields - -```typescript -// Editor: pouName="main", variablePath="SENSOR.status.is_valid" -// Debug path: RES0__INSTANCE0.SENSOR.value.STATUS.value.IS_VALID -const buildNestedStructPath = (pouName: string, varPath: string): string => { - const parts = varPath.split('.') - let path = `RES0__INSTANCE0.${parts[0].toUpperCase()}` - for (let i = 1; i < parts.length; i++) { - path += `.value.${parts[i].toUpperCase()}` - } - return path -} -``` - -#### Function Block Variables - -```typescript -// Editor: pouName="main", variablePath="TON0.ET" -// Debug path: RES0__INSTANCE0.TON0.ET -const buildFBVarPath = (pouName: string, varPath: string): string => { - return `RES0__INSTANCE0.${varPath.toUpperCase()}` -} -``` - -#### Nested Function Block Variables - -```typescript -// Editor: pouName="main", variablePath="MOTOR_CTRL.PID.OUTPUT" -// Debug path: RES0__INSTANCE0.MOTOR_CTRL.PID.OUTPUT -const buildNestedFBPath = (pouName: string, varPath: string): string => { - return `RES0__INSTANCE0.${varPath.toUpperCase()}` -} -``` - -#### Array Elements - -```typescript -// Editor: pouName="main", variablePath="TEMPS" -// Debug path for element [0]: RES0__INSTANCE0.TEMPS.value.table[0] -// Index is the starting index; elements are sequential -const buildArrayPath = (pouName: string, varPath: string, elementIndex: number): string => { - return `RES0__INSTANCE0.${varPath.toUpperCase()}.value.table[${elementIndex}]` -} -``` - -### 5.3 Resolution Algorithm - -```typescript -import { parseDebugFile, DebugVariable } from '@root/renderer/utils/parse-debug-file' - -interface ResolvedVariable { - nodeId: string - browseName: string - displayName: string - datatype: string - initialValue: any - description: string - index: number - permissions: OpcUaPermissions -} - -const resolveVariableIndex = ( - node: OpcUaNodeConfig, - debugVariables: DebugVariable[] -): number => { - // Build the expected debug path based on variable location - let debugPath: string - - if (node.pouName === 'GVL' || node.pouName === 'CONFIG') { - // Global variable - debugPath = `CONFIG0__${node.variablePath.toUpperCase()}` - } else { - // Instance variable (program or FB) - // Handle nested paths by checking for struct/array patterns - debugPath = buildInstancePath(node.pouName, node.variablePath) - } - - // Find matching entry in debug.c - const match = debugVariables.find( - dv => dv.name.toUpperCase() === debugPath.toUpperCase() - ) - - if (!match) { - throw new Error( - `Cannot resolve index for variable: ${node.pouName}:${node.variablePath}\n` + - `Expected debug path: ${debugPath}` - ) - } - - return match.index -} - -const buildInstancePath = (pouName: string, variablePath: string): string => { - // For struct fields, insert ".value." before each field access - // For FB variables, use direct dot notation - // This logic should match the debugger's path building - - const parts = variablePath.split('.') - - // Check if this involves struct fields (need .value. insertion) - // This requires type information to determine correctly - // For now, use the same logic as the debugger - - return `RES0__INSTANCE0.${variablePath.toUpperCase()}` -} -``` - -### 5.4 Structure Index Resolution - -For structures, each field gets its own index: - -```typescript -const resolveStructureIndices = ( - node: OpcUaNodeConfig, - debugVariables: DebugVariable[] -): RuntimeStructure => { - const fields = node.fields!.map(field => { - const fieldPath = `${node.variablePath}.${field.fieldPath}` - const debugPath = buildStructFieldDebugPath(node.pouName, fieldPath) - - const match = debugVariables.find( - dv => dv.name.toUpperCase() === debugPath.toUpperCase() - ) - - if (!match) { - throw new Error(`Cannot resolve index for field: ${fieldPath}`) - } - - return { - name: field.fieldPath, - datatype: getFieldType(node.variableType, field.fieldPath), - initial_value: field.initialValue, - index: match.index, - permissions: field.permissions, - } - }) - - return { - node_id: node.nodeId, - browse_name: node.browseName, - display_name: node.displayName, - description: node.description, - fields, - } -} -``` - -### 5.5 Array Index Resolution - -For arrays, only the starting index is stored; elements are sequential: - -```typescript -const resolveArrayIndex = ( - node: OpcUaNodeConfig, - debugVariables: DebugVariable[] -): RuntimeArray => { - // Find the first element's index - const firstElementPath = buildArrayElementPath( - node.pouName, - node.variablePath, - 0 // First element (C-style index) - ) - - const match = debugVariables.find( - dv => dv.name.toUpperCase() === firstElementPath.toUpperCase() - ) - - if (!match) { - throw new Error(`Cannot resolve index for array: ${node.variablePath}`) - } - - return { - node_id: node.nodeId, - browse_name: node.browseName, - display_name: node.displayName, - datatype: node.elementType!, - length: node.arrayLength!, - initial_value: node.initialValue, - index: match.index, // Starting index - permissions: node.permissions, - } -} -``` - -## 6. Complete Generator Implementation - -```typescript -// src/utils/opcua/generate-opcua-config.ts - -import { parseDebugFile, DebugVariable } from '@root/renderer/utils/parse-debug-file' -import type { PLCServer, OpcUaServerConfig, OpcUaNodeConfig } from '@root/types/PLC/open-plc' - -interface RuntimeConfig { - name: string - protocol: 'OPC-UA' - config: { - server: RuntimeServerConfig - security: RuntimeSecurityConfig - users: RuntimeUser[] - cycle_time_ms: number - address_space: RuntimeAddressSpace - } -} - -export const generateOpcUaConfig = ( - servers: PLCServer[] | undefined, - debugFileContent: string -): string | null => { - // 1. Find OPC-UA server configuration - if (!servers || servers.length === 0) return null - - const opcuaServer = servers.find( - s => s.protocol === 'opcua' && s.opcuaServerConfig?.server.enabled - ) - - if (!opcuaServer?.opcuaServerConfig) return null - - const config = opcuaServer.opcuaServerConfig - - // 2. Parse debug.c to get variable indices - const parsed = parseDebugFile(debugFileContent) - const debugVariables = parsed.variables - - // 3. Build runtime configuration - const runtimeConfig: RuntimeConfig = { - name: 'opcua_server', - protocol: 'OPC-UA', - config: { - server: buildServerConfig(config), - security: buildSecurityConfig(config), - users: buildUsersConfig(config), - cycle_time_ms: config.cycleTimeMs, - address_space: buildAddressSpace(config, debugVariables), - }, - } - - // 4. Return as JSON string - return JSON.stringify([runtimeConfig], null, 2) -} - -const buildServerConfig = (config: OpcUaServerConfig): RuntimeServerConfig => { - const { server, securityProfiles } = config - - return { - name: server.name, - application_uri: server.applicationUri, - product_uri: server.productUri, - endpoint_url: `opc.tcp://${server.bindAddress}:${server.port}${server.endpointPath}`, - security_profiles: securityProfiles - .filter(sp => sp.enabled) - .map(sp => ({ - name: sp.name, - enabled: sp.enabled, - security_policy: sp.securityPolicy, - security_mode: sp.securityMode, - auth_methods: sp.authMethods, - })), - } -} - -const buildSecurityConfig = (config: OpcUaServerConfig): RuntimeSecurityConfig => { - const { security } = config - - return { - server_certificate_strategy: security.serverCertificateStrategy, - server_certificate_custom: security.serverCertificateCustom, - server_private_key_custom: security.serverPrivateKeyCustom, - trusted_client_certificates: security.trustedClientCertificates.map(cert => ({ - id: cert.id, - pem: cert.pem, - })), - } -} - -const buildUsersConfig = (config: OpcUaServerConfig): RuntimeUser[] => { - return config.users.map(user => ({ - type: user.type, - username: user.username, - password_hash: user.passwordHash, - certificate_id: user.certificateId, - role: user.role, - })) -} - -const buildAddressSpace = ( - config: OpcUaServerConfig, - debugVariables: DebugVariable[] -): RuntimeAddressSpace => { - const variables: RuntimeVariable[] = [] - const structures: RuntimeStructure[] = [] - const arrays: RuntimeArray[] = [] - - for (const node of config.addressSpace.nodes) { - switch (node.nodeType) { - case 'variable': - variables.push(resolveVariable(node, debugVariables)) - break - case 'structure': - structures.push(resolveStructure(node, debugVariables)) - break - case 'array': - arrays.push(resolveArray(node, debugVariables)) - break - } - } - - return { - namespace_uri: config.addressSpace.namespaceUri, - variables, - structures, - arrays, - } -} - -// ... resolution functions as defined in section 5 -``` - -## 7. Example Transformation - -### 7.1 Editor Configuration (Stored in Project) - -```json -{ - "server": { - "enabled": true, - "name": "OpenPLC OPC UA Server", - "applicationUri": "urn:openplc:opcua:server", - "productUri": "urn:openplc:runtime", - "bindAddress": "0.0.0.0", - "port": 4840, - "endpointPath": "/openplc/opcua" - }, - "securityProfiles": [ - { - "id": "uuid-1", - "name": "insecure", - "enabled": true, - "securityPolicy": "None", - "securityMode": "None", - "authMethods": ["Anonymous"] - } - ], - "security": { - "serverCertificateStrategy": "auto_self_signed", - "serverCertificateCustom": null, - "serverPrivateKeyCustom": null, - "trustedClientCertificates": [] - }, - "users": [ - { - "id": "uuid-2", - "type": "password", - "username": "engineer", - "passwordHash": "$2b$12$...", - "certificateId": null, - "role": "engineer" - } - ], - "cycleTimeMs": 100, - "addressSpace": { - "namespaceUri": "urn:openplc:opcua:namespace", - "nodes": [ - { - "id": "uuid-3", - "pouName": "main", - "variablePath": "MOTOR_SPEED", - "variableType": "INT", - "nodeId": "PLC.Main.MotorSpeed", - "browseName": "MotorSpeed", - "displayName": "Motor Speed", - "description": "Current motor speed", - "initialValue": 0, - "permissions": { - "viewer": "r", - "operator": "rw", - "engineer": "rw" - }, - "nodeType": "variable" - } - ] - } -} -``` - -### 7.2 Generated Runtime JSON (opcua.json) - -```json -[ - { - "name": "opcua_server", - "protocol": "OPC-UA", - "config": { - "server": { - "name": "OpenPLC OPC UA Server", - "application_uri": "urn:openplc:opcua:server", - "product_uri": "urn:openplc:runtime", - "endpoint_url": "opc.tcp://0.0.0.0:4840/openplc/opcua", - "security_profiles": [ - { - "name": "insecure", - "enabled": true, - "security_policy": "None", - "security_mode": "None", - "auth_methods": ["Anonymous"] - } - ] - }, - "security": { - "server_certificate_strategy": "auto_self_signed", - "server_certificate_custom": null, - "server_private_key_custom": null, - "trusted_client_certificates": [] - }, - "users": [ - { - "type": "password", - "username": "engineer", - "password_hash": "$2b$12$...", - "certificate_id": null, - "role": "engineer" - } - ], - "cycle_time_ms": 100, - "address_space": { - "namespace_uri": "urn:openplc:opcua:namespace", - "variables": [ - { - "node_id": "PLC.Main.MotorSpeed", - "browse_name": "MotorSpeed", - "display_name": "Motor Speed", - "datatype": "INT", - "initial_value": 0, - "description": "Current motor speed", - "index": 5, - "permissions": { - "viewer": "r", - "operator": "rw", - "engineer": "rw" - } - } - ], - "structures": [], - "arrays": [] - } - } - } -] -``` - -## 8. Error Handling - -### 8.1 Index Resolution Errors - -When a variable cannot be resolved: - -```typescript -class OpcUaConfigError extends Error { - constructor( - public readonly variableRef: string, - public readonly expectedPath: string, - public readonly message: string - ) { - super(message) - this.name = 'OpcUaConfigError' - } -} - -// Usage: -if (!match) { - throw new OpcUaConfigError( - `${node.pouName}:${node.variablePath}`, - debugPath, - `Cannot resolve OPC-UA variable index.\n` + - `Variable: ${node.pouName}:${node.variablePath}\n` + - `Expected debug path: ${debugPath}\n` + - `This may happen if the PLC program was modified after configuring OPC-UA.\n` + - `Please verify the variable exists in the program.` - ) -} -``` - -### 8.2 Validation Before Generation - -```typescript -const validateAddressSpace = ( - nodes: OpcUaNodeConfig[], - debugVariables: DebugVariable[] -): { valid: boolean; errors: string[] } => { - const errors: string[] = [] - - for (const node of nodes) { - try { - resolveVariableIndex(node, debugVariables) - } catch (e) { - errors.push((e as Error).message) - } - } - - return { - valid: errors.length === 0, - errors, - } -} -``` - -## 9. Integration with Compiler - -The OPC-UA JSON generation is called as part of the compilation process: - -```typescript -// In compiler-module.ts - -async handleGenerateOpcUaConfig( - sourceTargetFolderPath: string, - projectData: PLCProjectData -): Promise { - // Check if OPC-UA is enabled - const opcuaServer = projectData.servers?.find( - s => s.protocol === 'opcua' && s.opcuaServerConfig?.server.enabled - ) - - if (!opcuaServer) { - // OPC-UA not configured, skip - return - } - - // Read the debug.c file generated by xml2st - const debugCPath = join(sourceTargetFolderPath, 'debug.c') - const debugContent = await readFile(debugCPath, 'utf-8') - - // Generate the OPC-UA configuration - const opcuaJson = generateOpcUaConfig(projectData.servers, debugContent) - - if (opcuaJson) { - // Write to conf/opcua.json - const confDir = join(sourceTargetFolderPath, 'conf') - await mkdir(confDir, { recursive: true }) - await writeFile(join(confDir, 'opcua.json'), opcuaJson, 'utf-8') - } -} -``` diff --git a/docs/outdated/opcua-server-configuration/04-implementation-phases.md b/docs/outdated/opcua-server-configuration/04-implementation-phases.md deleted file mode 100644 index 192e21c4d..000000000 --- a/docs/outdated/opcua-server-configuration/04-implementation-phases.md +++ /dev/null @@ -1,973 +0,0 @@ -# OPC-UA Server Configuration - Implementation Phases - -This document outlines a phased approach to implementing the OPC-UA server configuration feature in OpenPLC Editor. - -## Overview - -The implementation is divided into 6 phases, designed to deliver incremental functionality while managing complexity: - -| Phase | Focus | Deliverable | -|-------|-------|-------------| -| 1 | Foundation & Types | Type definitions, project integration, basic UI shell | -| 2 | General Settings & Security Profiles | First two tabs fully functional | -| 3 | Users & Certificates | Authentication and certificate management | -| 4 | Address Space - Basic Variables | Variable tree and simple variable selection | -| 5 | Address Space - Complex Types | Structures, arrays, nested FBs | -| 6 | Compiler Integration & Testing | JSON generation, end-to-end testing | - ---- - -## Phase 1: Foundation & Type Definitions - -### Objective -Establish the foundational type system and basic UI infrastructure for OPC-UA configuration. - -### Deliverables - -#### 1.1 Type Definitions - -**File:** `src/types/PLC/open-plc.ts` - -Add Zod schemas for all OPC-UA configuration types: - -```typescript -// Security Profile Schema -const OpcUaSecurityProfileSchema = z.object({ - id: z.string().uuid(), - name: z.string().min(1).max(64), - enabled: z.boolean(), - securityPolicy: z.enum(['None', 'Basic128Rsa15', 'Basic256', 'Basic256Sha256']), - securityMode: z.enum(['None', 'Sign', 'SignAndEncrypt']), - authMethods: z.array(z.enum(['Anonymous', 'Username', 'Certificate'])).min(1), -}) - -// Trusted Certificate Schema -const OpcUaTrustedCertificateSchema = z.object({ - id: z.string().min(1).max(64), - pem: z.string(), - subject: z.string().optional(), - validFrom: z.string().optional(), - validTo: z.string().optional(), - fingerprint: z.string().optional(), -}) - -// User Schema -const OpcUaUserSchema = z.object({ - id: z.string().uuid(), - type: z.enum(['password', 'certificate']), - username: z.string().nullable(), - passwordHash: z.string().nullable(), - certificateId: z.string().nullable(), - role: z.enum(['viewer', 'operator', 'engineer']), -}) - -// Permissions Schema -const OpcUaPermissionsSchema = z.object({ - viewer: z.enum(['r', 'w', 'rw']).default('r'), - operator: z.enum(['r', 'w', 'rw']).default('r'), - engineer: z.enum(['r', 'w', 'rw']).default('rw'), -}) - -// Field Configuration Schema (for structures) -const OpcUaFieldConfigSchema = z.object({ - fieldPath: z.string(), - displayName: z.string(), - initialValue: z.union([z.boolean(), z.number(), z.string()]), - permissions: OpcUaPermissionsSchema, -}) - -// Node Configuration Schema -const OpcUaNodeConfigSchema = z.object({ - id: z.string().uuid(), - pouName: z.string(), - variablePath: z.string(), - variableType: z.string(), - nodeId: z.string(), - browseName: z.string(), - displayName: z.string(), - description: z.string().default(''), - initialValue: z.union([z.boolean(), z.number(), z.string()]), - permissions: OpcUaPermissionsSchema, - nodeType: z.enum(['variable', 'structure', 'array']), - fields: z.array(OpcUaFieldConfigSchema).optional(), - arrayLength: z.number().optional(), - elementType: z.string().optional(), -}) - -// Complete Server Configuration Schema -const OpcUaServerConfigSchema = z.object({ - server: z.object({ - enabled: z.boolean().default(false), - name: z.string().max(128).default('OpenPLC OPC UA Server'), - applicationUri: z.string().default('urn:openplc:opcua:server'), - productUri: z.string().default('urn:openplc:runtime'), - bindAddress: z.string().default('0.0.0.0'), - port: z.number().int().min(1).max(65535).default(4840), - endpointPath: z.string().default('/openplc/opcua'), - }), - securityProfiles: z.array(OpcUaSecurityProfileSchema).default([]), - security: z.object({ - serverCertificateStrategy: z.enum(['auto_self_signed', 'custom']).default('auto_self_signed'), - serverCertificateCustom: z.string().nullable().default(null), - serverPrivateKeyCustom: z.string().nullable().default(null), - trustedClientCertificates: z.array(OpcUaTrustedCertificateSchema).default([]), - }), - users: z.array(OpcUaUserSchema).default([]), - cycleTimeMs: z.number().int().min(10).max(10000).default(100), - addressSpace: z.object({ - namespaceUri: z.string().default('urn:openplc:opcua:namespace'), - nodes: z.array(OpcUaNodeConfigSchema).default([]), - }), -}) -``` - -#### 1.2 Protocol Extension - -**File:** `src/types/PLC/open-plc.ts` - -Add 'opcua' to the PLCServer protocol union: - -```typescript -const PLCServerSchema = z.object({ - name: z.string(), - protocol: z.enum(['modbus-tcp', 's7comm', 'ethernet-ip', 'opcua']), - modbusSlaveConfig: ModbusSlaveConfigSchema.optional(), - s7commSlaveConfig: S7CommSlaveConfigSchema.optional(), - opcuaServerConfig: OpcUaServerConfigSchema.optional(), // Add this -}) -``` - -#### 1.3 Project Slice Actions - -**File:** `src/renderer/store/slices/project/slice.ts` - -Add basic CRUD actions for OPC-UA configuration: - -```typescript -// OPC-UA Server Actions -createOpcUaServer: (serverName: string) => void -updateOpcUaServerSettings: (serverName: string, settings: Partial) => void -deleteOpcUaServer: (serverName: string) => void -``` - -#### 1.4 UI Shell Component - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/index.tsx` - -Create the basic tabbed UI structure: - -```tsx -const OpcUaServerEditor = () => { - const [activeTab, setActiveTab] = useState('general') - - const tabs = [ - { id: 'general', label: 'General Settings' }, - { id: 'security', label: 'Security Profiles' }, - { id: 'users', label: 'Users' }, - { id: 'certificates', label: 'Certificates' }, - { id: 'address-space', label: 'Address Space' }, - ] - - return ( -
- -
- {activeTab === 'general' && } - {activeTab === 'security' && } - {activeTab === 'users' && } - {activeTab === 'certificates' && } - {activeTab === 'address-space' && } -
-
- ) -} -``` - -### Acceptance Criteria - -- [ ] All Zod schemas defined and exported -- [ ] PLCServer type updated with opcuaServerConfig -- [ ] Basic project slice actions implemented -- [ ] Tabbed UI component renders with placeholder content -- [ ] OPC-UA server can be created/deleted from Servers panel - ---- - -## Phase 2: General Settings & Security Profiles - -### Objective -Implement the first two configuration tabs with full functionality. - -### Deliverables - -#### 2.1 General Settings Tab - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/tabs/general-settings.tsx` - -Implement all fields: -- Enable toggle -- Server name -- Application URI -- Product URI -- Bind address (dropdown with common options) -- Port number -- Endpoint path -- Cycle time with validation - -```tsx -const GeneralSettingsTab = () => { - const { project, projectActions } = useOpenPLCStore() - const opcuaConfig = getOpcUaConfig(project) - - const handleUpdateServer = (updates: Partial) => { - projectActions.updateOpcUaServerSettings(serverName, updates) - } - - return ( -
- {/* Enable Toggle */} -
- - handleUpdateServer({ enabled })} - /> -
- - {/* Server Identity Section */} -
-

Server Identity

- {/* Form fields */} -
- - {/* Network Configuration Section */} -
-

Network Configuration

- {/* Form fields with endpoint URL preview */} -
- - {/* Performance Section */} -
-

Performance

- {/* Cycle time input */} -
-
- ) -} -``` - -#### 2.2 Security Profiles Tab - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/tabs/security-profiles.tsx` - -Implement: -- List of security profiles with enable/disable toggle -- Add/Edit/Delete profile functionality -- Profile modal with all settings -- Validation rules (Anonymous only with None policy, etc.) - -#### 2.3 Security Profile Modal - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/components/security-profile-modal.tsx` - -Implement modal with: -- Profile name input -- Enable toggle -- Security policy dropdown -- Security mode dropdown -- Authentication methods checkboxes -- Validation feedback - -#### 2.4 Project Slice Actions - -Add actions for security profile management: - -```typescript -addOpcUaSecurityProfile: (serverName: string, profile: OpcUaSecurityProfile) => void -updateOpcUaSecurityProfile: (serverName: string, profileId: string, updates: Partial) => void -removeOpcUaSecurityProfile: (serverName: string, profileId: string) => void -``` - -### Acceptance Criteria - -- [ ] General settings tab saves all fields correctly -- [ ] Endpoint URL preview updates dynamically -- [ ] Cycle time validates range (10-10000) -- [ ] Security profiles can be added/edited/removed -- [ ] At least one profile must be enabled (validation) -- [ ] Anonymous auth only available with None policy -- [ ] Profile list shows summary of each profile - ---- - -## Phase 3: Users & Certificates - -### Objective -Implement user authentication and certificate management. - -### Deliverables - -#### 3.1 Users Tab - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/tabs/users.tsx` - -Implement: -- User list with role badges -- Add/Edit/Delete user functionality -- User type toggle (password/certificate) -- Password validation and hashing - -#### 3.2 User Modal - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/components/user-modal.tsx` - -Implement: -- Authentication type selector -- Password fields with strength indicator -- Certificate selector (from trusted certificates) -- Role selector with descriptions - -#### 3.3 Password Hashing Utility - -**File:** `src/utils/opcua/bcrypt-utils.ts` - -```typescript -import bcrypt from 'bcryptjs' - -export const hashPassword = async (password: string): Promise => { - const salt = await bcrypt.genSalt(12) - return bcrypt.hash(password, salt) -} - -export const verifyPassword = async ( - password: string, - hash: string -): Promise => { - return bcrypt.compare(password, hash) -} -``` - -#### 3.4 Certificates Tab - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/tabs/certificates.tsx` - -Implement: -- Server certificate strategy selector -- Custom certificate upload (when strategy = custom) -- Private key upload -- Trusted client certificates list -- Certificate details display - -#### 3.5 Certificate Parsing Utility - -**File:** `src/utils/opcua/certificate-utils.ts` - -Use a library like `node-forge` to parse PEM certificates: - -```typescript -import forge from 'node-forge' - -export interface CertificateInfo { - subject: string - issuer: string - validFrom: Date - validTo: Date - fingerprint: string -} - -export const parsePemCertificate = (pem: string): CertificateInfo => { - const cert = forge.pki.certificateFromPem(pem) - return { - subject: cert.subject.getField('CN')?.value || 'Unknown', - issuer: cert.issuer.getField('CN')?.value || 'Unknown', - validFrom: cert.validity.notBefore, - validTo: cert.validity.notAfter, - fingerprint: forge.md.sha256 - .create() - .update(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes()) - .digest() - .toHex(), - } -} -``` - -#### 3.6 Add Trusted Certificate Modal - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/components/certificate-modal.tsx` - -Implement: -- Certificate ID input -- PEM content text area -- File browse button -- Certificate details preview (parsed from PEM) - -### Acceptance Criteria - -- [ ] Password users can be created with bcrypt hashing -- [ ] Certificate users can be created referencing trusted certs -- [ ] At least one user required when Username auth enabled -- [ ] Server certificate strategy works (auto/custom) -- [ ] Custom certificates can be uploaded via file or paste -- [ ] Trusted certificates show parsed details (subject, validity) -- [ ] Certificate IDs are unique - ---- - -## Phase 4: Address Space - Basic Variables - -### Objective -Implement the variable tree and basic variable selection functionality. - -### Deliverables - -#### 4.1 Address Space Tab - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/tabs/address-space.tsx` - -Implement: -- Namespace URI input -- Split-pane layout (available variables | selected variables) -- Add/Remove buttons -- Filter input for variable search - -#### 4.2 Variable Tree Component - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/components/variable-tree.tsx` - -Build the hierarchical variable tree from project POUs: - -```tsx -interface TreeNode { - id: string - name: string - type: 'program' | 'function_block' | 'global' | 'variable' | 'structure' | 'array' - variableType?: string // IEC type for variables - children?: TreeNode[] - pouName: string - variablePath: string - isSelectable: boolean - isExpanded?: boolean -} - -const VariableTree = ({ - nodes, - selectedIds, - onSelect, - onExpand, -}: VariableTreeProps) => { - const renderNode = (node: TreeNode, depth: number) => ( -
-
- {node.children && ( - onExpand(node.id)} - /> - )} - {node.isSelectable && ( - onSelect(node)} - /> - )} - - {node.name} - {node.variableType && ( - ({node.variableType}) - )} -
- {node.isExpanded && node.children?.map(child => - renderNode(child, depth + 1) - )} -
- ) - - return
{nodes.map(node => renderNode(node, 0))}
-} -``` - -#### 4.3 Variable Extraction from Project - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/hooks/use-project-variables.ts` - -Extract variables from project POUs: - -```typescript -const useProjectVariables = (): TreeNode[] => { - const { project } = useOpenPLCStore() - - return useMemo(() => { - const nodes: TreeNode[] = [] - - // Programs and Function Block instances - for (const pou of project.data.pous) { - if (pou.type === 'program') { - nodes.push(buildProgramNode(pou, project.data)) - } - } - - // Global Variables - if (project.data.globalVariables?.length) { - nodes.push(buildGlobalVariablesNode(project.data.globalVariables)) - } - - return nodes - }, [project.data]) -} - -const buildProgramNode = (pou: POU, projectData: PLCProjectData): TreeNode => { - return { - id: `pou-${pou.data.name}`, - name: pou.data.name, - type: 'program', - pouName: pou.data.name, - variablePath: '', - isSelectable: false, - children: pou.data.variables.map(v => - buildVariableNode(v, pou.data.name, projectData) - ), - } -} -``` - -#### 4.4 Simple Variable Configuration Modal - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/components/variable-config-modal.tsx` - -Implement modal for configuring selected variables: -- Node ID (auto-generated, editable) -- Browse name -- Display name -- Description -- Initial value (type-appropriate input) -- Permissions matrix - -#### 4.5 Selected Variables List - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/components/selected-variables-list.tsx` - -Display selected variables with: -- Node summary (node ID, type, permissions) -- Edit/Remove buttons -- Drag-and-drop reordering - -### Acceptance Criteria - -- [ ] Variable tree displays all program variables -- [ ] Tree supports expand/collapse for nested items -- [ ] Simple variables (INT, BOOL, REAL, etc.) can be selected -- [ ] Selected variables appear in right panel -- [ ] Variables can be configured with OPC-UA properties -- [ ] Node IDs are auto-generated but editable -- [ ] Permissions can be set per variable -- [ ] Variables can be removed from selection - ---- - -## Phase 5: Address Space - Complex Types - -### Objective -Add support for structures, arrays, and deeply nested function blocks. - -### Deliverables - -#### 5.1 Structure Support - -Extend the variable tree to show structure fields: - -```typescript -const buildStructureNode = ( - variable: Variable, - pouName: string, - structType: StructType -): TreeNode => { - return { - id: `${pouName}-${variable.name}`, - name: variable.name, - type: 'structure', - variableType: variable.type.value, - pouName, - variablePath: variable.name, - isSelectable: true, // Can select entire structure - children: structType.variable.map(field => ({ - id: `${pouName}-${variable.name}-${field.name}`, - name: field.name, - type: 'variable', - variableType: field.type.value, - pouName, - variablePath: `${variable.name}.${field.name}`, - isSelectable: true, // Or select individual fields - })), - } -} -``` - -#### 5.2 Structure Configuration Modal - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/components/structure-config-modal.tsx` - -Implement modal for structure configuration: -- Structure-level properties (node ID, browse name, etc.) -- Field table with individual permissions -- Nested structure handling - -#### 5.3 Array Support - -Extend the variable tree for arrays: - -```typescript -const buildArrayNode = ( - variable: Variable, - pouName: string, - arrayDimension: string // e.g., "1..10" -): TreeNode => { - const [min, max] = arrayDimension.split('..').map(Number) - const length = max - min + 1 - - return { - id: `${pouName}-${variable.name}`, - name: variable.name, - type: 'array', - variableType: `ARRAY[${arrayDimension}] OF ${elementType}`, - pouName, - variablePath: variable.name, - isSelectable: true, - children: Array.from({ length }, (_, i) => ({ - id: `${pouName}-${variable.name}-${min + i}`, - name: `[${min + i}]`, - type: 'variable', - variableType: elementType, - pouName, - variablePath: `${variable.name}[${min + i}]`, - isSelectable: true, - })), - } -} -``` - -#### 5.4 Array Configuration Modal - -**File:** `src/renderer/components/_features/[workspace]/editor/server/opcua-server/components/array-config-modal.tsx` - -Implement modal for array configuration: -- Array-level properties -- Element type display -- Length display -- Permissions (apply to all elements) - -#### 5.5 Nested Function Block Support - -Handle deeply nested FB instances: - -```typescript -const buildFunctionBlockNode = ( - variable: Variable, - pouName: string, - fbType: FunctionBlock, - projectData: PLCProjectData, - currentPath: string = '' -): TreeNode => { - const variablePath = currentPath - ? `${currentPath}.${variable.name}` - : variable.name - - return { - id: `${pouName}-${variablePath}`, - name: variable.name, - type: 'function_block', - variableType: variable.type.value, - pouName, - variablePath, - isSelectable: true, // Select entire FB - children: fbType.variable.map(fbVar => { - // Check if this FB variable is itself an FB - const nestedFbType = findFunctionBlock(fbVar.type.value, projectData) - - if (nestedFbType) { - // Recursive: nested FB - return buildFunctionBlockNode( - fbVar, - pouName, - nestedFbType, - projectData, - variablePath - ) - } - - // Leaf: simple variable - return { - id: `${pouName}-${variablePath}-${fbVar.name}`, - name: fbVar.name, - type: 'variable', - variableType: fbVar.type.value, - pouName, - variablePath: `${variablePath}.${fbVar.name}`, - isSelectable: true, - } - }), - } -} -``` - -#### 5.6 Selection Behavior for Complex Types - -Implement selection logic: - -```typescript -const handleSelectNode = (node: TreeNode) => { - if (node.type === 'variable') { - // Simple toggle - toggleSelection(node.id) - } else { - // Complex type: select/deselect all children - const childIds = getAllChildIds(node) - if (allSelected(childIds)) { - deselectAll(childIds) - } else { - selectAll(childIds) - } - } -} -``` - -#### 5.7 Deeply Nested Path Display - -Show full path in selected variables list: - -``` -main:PROCESS_CTRL.HEATER.PID.OUTPUT - └── FB ──┘ └─ FB ─┘ └─ var -``` - -### Acceptance Criteria - -- [ ] Structures display with expandable fields -- [ ] Entire structure or individual fields can be selected -- [ ] Arrays display with expandable elements -- [ ] Entire array can be selected (not individual elements for now) -- [ ] Nested FBs display correctly (FB inside FB inside FB) -- [ ] All levels of nesting are navigable -- [ ] Selection of parent auto-selects all children -- [ ] Path display shows full hierarchy - ---- - -## Phase 6: Compiler Integration & Testing - -### Objective -Integrate OPC-UA JSON generation into the compiler and perform end-to-end testing. - -### Deliverables - -#### 6.1 JSON Generator Implementation - -**File:** `src/utils/opcua/generate-opcua-config.ts` - -Implement the complete generator as documented in `03-json-configuration-mapping.md`: - -```typescript -export const generateOpcUaConfig = ( - servers: PLCServer[] | undefined, - debugFileContent: string -): string | null => { - // Implementation as documented -} -``` - -#### 6.2 Index Resolution Implementation - -**File:** `src/utils/opcua/resolve-indices.ts` - -Implement index resolution logic: - -```typescript -export const resolveVariableIndex = ( - pouName: string, - variablePath: string, - debugVariables: DebugVariable[] -): number => { - // Build debug path based on variable type - // Match against parsed debug.c - // Return index or throw error -} -``` - -#### 6.3 Compiler Module Integration - -**File:** `src/main/modules/compiler/compiler-module.ts` - -Add OPC-UA configuration generation to the compilation pipeline: - -```typescript -import { generateOpcUaConfig } from '@root/utils/opcua' - -// In the compile method, after debug.c is generated: -async handleGenerateOpcUaConfig( - sourceTargetFolderPath: string, - projectData: PLCProjectData -): Promise { - const opcuaServer = projectData.servers?.find( - s => s.protocol === 'opcua' && s.opcuaServerConfig?.server.enabled - ) - - if (!opcuaServer) return - - const debugCPath = join(sourceTargetFolderPath, 'debug.c') - const debugContent = await readFile(debugCPath, 'utf-8') - - const opcuaJson = generateOpcUaConfig(projectData.servers, debugContent) - - if (opcuaJson) { - const confDir = join(sourceTargetFolderPath, 'conf') - await mkdir(confDir, { recursive: true }) - await writeFile(join(confDir, 'opcua.json'), opcuaJson, 'utf-8') - } -} -``` - -#### 6.4 Error Handling & User Feedback - -Implement error handling for common issues: - -- Variable not found in debug.c -- Invalid configuration (no enabled profiles, no users, etc.) -- Circular references in FB structures - -```typescript -export class OpcUaConfigError extends Error { - constructor( - public code: 'VARIABLE_NOT_FOUND' | 'INVALID_CONFIG' | 'CIRCULAR_REF', - public details: Record, - message: string - ) { - super(message) - } -} -``` - -#### 6.5 Unit Tests - -**File:** `src/utils/opcua/__tests__/generate-opcua-config.test.ts` - -Write comprehensive tests: - -```typescript -describe('generateOpcUaConfig', () => { - it('returns null when OPC-UA not configured', () => { - // Test - }) - - it('generates valid JSON for simple variables', () => { - // Test - }) - - it('resolves structure field indices correctly', () => { - // Test - }) - - it('resolves array start indices correctly', () => { - // Test - }) - - it('handles nested FB paths correctly', () => { - // Test - }) - - it('throws error for unresolvable variables', () => { - // Test - }) -}) -``` - -#### 6.6 Integration Tests - -**File:** `e2e/opcua-configuration.spec.ts` - -Write end-to-end tests: - -```typescript -test('OPC-UA server can be configured and compiled', async ({ page }) => { - // 1. Create new project - // 2. Add program with variables - // 3. Configure OPC-UA server - // 4. Select variables for address space - // 5. Compile project - // 6. Verify opcua.json is generated correctly -}) -``` - -#### 6.7 Documentation Updates - -Update user documentation: -- How to configure OPC-UA server -- Security best practices -- Troubleshooting guide - -### Acceptance Criteria - -- [ ] JSON generator produces valid output -- [ ] Index resolution works for all variable types -- [ ] Compiler generates opcua.json when OPC-UA enabled -- [ ] Errors provide helpful messages -- [ ] Unit tests pass with >80% coverage -- [ ] E2E tests pass -- [ ] Generated JSON validated against runtime plugin - ---- - -## Summary Timeline - -| Phase | Description | Dependencies | -|-------|-------------|--------------| -| 1 | Foundation & Types | None | -| 2 | General Settings & Security | Phase 1 | -| 3 | Users & Certificates | Phase 2 | -| 4 | Address Space - Basic | Phase 1 | -| 5 | Address Space - Complex | Phase 4 | -| 6 | Compiler Integration | Phases 1-5 | - -**Parallel Work Opportunities:** -- Phases 2-3 (Security/Users) can be developed in parallel with Phases 4-5 (Address Space) -- Unit tests can be written alongside each phase - ---- - -## Risk Mitigation - -### Technical Risks - -| Risk | Mitigation | -|------|------------| -| Complex nested FB paths | Reuse debugger's proven path building logic | -| Password security | Use bcryptjs, never store plain text | -| Certificate parsing errors | Validate PEM format before storing | -| Large variable trees | Implement tree virtualization | - -### Integration Risks - -| Risk | Mitigation | -|------|------------| -| Index resolution failures | Comprehensive error messages with expected path | -| Runtime incompatibility | Test against actual OPC-UA plugin during development | -| Project file migration | Provide default values for new fields | - ---- - -## Dependencies - -### External Libraries - -| Library | Purpose | Phase | -|---------|---------|-------| -| bcryptjs | Password hashing | 3 | -| node-forge | Certificate parsing | 3 | -| react-window | Tree virtualization | 4 | -| uuid | Generate unique IDs | 1 | - -### Internal Dependencies - -| Component | Purpose | Phase | -|-----------|---------|-------| -| parseDebugFile | Index resolution | 6 | -| Project store | State management | 1 | -| Compiler module | JSON generation | 6 | diff --git a/docs/outdated/opcua-server-configuration/README.md b/docs/outdated/opcua-server-configuration/README.md deleted file mode 100644 index b6902db6c..000000000 --- a/docs/outdated/opcua-server-configuration/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# OPC-UA Server Configuration - -This folder contains design documentation for the OPC-UA server configuration feature in OpenPLC Editor. - -## Overview - -The OPC-UA server configuration allows users to configure an OPC-UA server that runs on the OpenPLC Runtime. This feature enables industrial clients to access PLC variables through the OPC-UA protocol, which is significantly more complex than the existing Modbus and S7Comm protocols. - -## Key Features - -- **Full Variable Access**: Unlike Modbus/S7Comm which only access I/O image tables, OPC-UA can expose any PLC program variable -- **Security Profiles**: Multiple security configurations (None, Sign, SignAndEncrypt) -- **Certificate Management**: Server and client certificate handling -- **User Authentication**: Password-based and certificate-based authentication -- **Role-Based Access Control**: Viewer, Operator, and Engineer roles with per-variable permissions -- **Complex Data Types**: Support for structures, arrays, and nested function blocks - -## Documentation Structure - -1. **[Design Overview](./01-design-overview.md)** - High-level architecture, configuration flow, and integration points -2. **[UI Screen Specifications](./02-ui-screen-specifications.md)** - Detailed UI designs for each configuration tab -3. **[JSON Configuration Mapping](./03-json-configuration-mapping.md)** - How editor configuration maps to runtime JSON format -4. **[Implementation Phases](./04-implementation-phases.md)** - Phased implementation plan with deliverables - -## Related Documentation - -- OpenPLC Runtime OPC-UA Plugin: See the `RTOP-100-OPC-UA` branch of openplc-runtime -- Existing protocol implementations: Modbus and S7Comm configurations in openplc-editor - -## Key Differences from Modbus/S7Comm - -| Aspect | Modbus/S7Comm | OPC-UA | -|--------|---------------|--------| -| Variable Access | I/O Image Tables only | Any PLC variable | -| Security | Basic or none | Multiple security profiles | -| Authentication | None | Anonymous, Username/Password, Certificate | -| Access Control | None | Role-based per-variable permissions | -| Data Types | Simple registers/coils | Variables, Structures, Arrays | -| Configuration Complexity | Low-Medium | High | diff --git a/docs/outdated/s7comm-server-implementation.md b/docs/outdated/s7comm-server-implementation.md deleted file mode 100644 index 118fa6fd0..000000000 --- a/docs/outdated/s7comm-server-implementation.md +++ /dev/null @@ -1,811 +0,0 @@ -# S7Comm Server Configuration Implementation Plan - -This document outlines the implementation plan for adding Siemens S7Comm server configuration support to the OpenPLC Editor. The implementation follows the existing Modbus server pattern and integrates with the S7Comm plugin on the OpenPLC Runtime. - -## Table of Contents - -- [Overview](#overview) -- [S7Comm Plugin Configuration Reference](#s7comm-plugin-configuration-reference) -- [Implementation Phases](#implementation-phases) - - [Phase 1: Type Definitions](#phase-1-type-definitions) - - [Phase 2: State Management](#phase-2-state-management) - - [Phase 3: Configuration Generation](#phase-3-configuration-generation) - - [Phase 4: Compiler Integration](#phase-4-compiler-integration) - - [Phase 5: UI Components](#phase-5-ui-components) - - [Phase 6: Enable S7Comm in UI](#phase-6-enable-s7comm-in-ui) -- [Files Summary](#files-summary) -- [Testing Checklist](#testing-checklist) - ---- - -## Overview - -The S7Comm plugin allows OpenPLC Runtime to act as a Siemens S7 protocol server, enabling communication with Siemens HMIs and SCADA systems. The editor needs to provide a configuration interface that generates the JSON configuration file expected by the runtime plugin. - -### Architecture Flow - -``` -User UI (Editor) - ↓ createServer('s7comm') -ProjectSlice (Zustand State) - ↓ stores s7commSlaveConfig -S7CommServerEditor (React Component) - ↓ updateS7CommServerConfig() -ProjectSlice - ↓ updates configuration -Compiler.compileProgram() - ↓ calls handleGenerateS7CommConfig() -generateS7CommConfig() - ↓ converts to runtime format -File: conf/s7comm.json - ↓ sent to runtime via upload -Runtime S7Comm Plugin - ↓ loads configuration -S7 Server starts on configured port -``` - ---- - -## S7Comm Plugin Configuration Reference - -The runtime S7Comm plugin expects a JSON configuration file with the following structure: - -### Server Configuration - -| Field | Type | Required | Default | Range | Description | -|-------|------|----------|---------|-------|-------------| -| `enabled` | boolean | No | `true` | - | Enable/disable the S7 server | -| `bind_address` | string | No | `"0.0.0.0"` | Valid IP | Network interface to bind | -| `port` | integer | No | `102` | 1-65535 | TCP port (102 is standard S7) | -| `max_clients` | integer | No | `32` | 1-1024 | Maximum simultaneous connections | -| `work_interval_ms` | integer | No | `100` | 1-10000 | Worker thread polling interval | -| `send_timeout_ms` | integer | No | `3000` | 100-60000 | Socket send timeout | -| `recv_timeout_ms` | integer | No | `3000` | 100-60000 | Socket receive timeout | -| `ping_timeout_ms` | integer | No | `10000` | 1000-300000 | Keep-alive timeout | -| `pdu_size` | integer | No | `480` | 240-960 | Maximum PDU size per S7 spec | - -> **Note:** Port 102 requires root/administrator privileges on most systems. - -### PLC Identity Configuration - -These values are returned in S7 SZL (System Status List) queries for HMI compatibility: - -| Field | Type | Required | Default | Max Length | Description | -|-------|------|----------|---------|------------|-------------| -| `name` | string | No | `"OpenPLC Runtime"` | 64 chars | PLC display name | -| `module_type` | string | No | `"CPU 315-2 PN/DP"` | 64 chars | CPU type string | -| `serial_number` | string | No | `"S C-XXXXXXXXX"` | 64 chars | Serial number | -| `copyright` | string | No | `"OpenPLC Project"` | 64 chars | Copyright string | -| `module_name` | string | No | `"OpenPLC"` | 64 chars | Module name | - -### Data Blocks Configuration - -Data blocks map S7 protocol DB areas to OpenPLC buffers. Maximum 64 data blocks supported. - -```json -{ - "db_number": 1, - "description": "Digital Inputs (%IX)", - "size_bytes": 128, - "mapping": { - "type": "bool_input", - "start_buffer": 0, - "bit_addressing": true - } -} -``` - -#### Data Block Fields - -| Field | Type | Required | Constraints | Description | -|-------|------|----------|-------------|-------------| -| `db_number` | integer | **Yes** | 1-65535, unique | S7 DB number | -| `description` | string | No | Max 128 chars | Human-readable description | -| `size_bytes` | integer | **Yes** | 1-65536 | DB size in bytes | -| `mapping.type` | string | **Yes** | See type list | OpenPLC buffer type | -| `mapping.start_buffer` | integer | No | 0-1023 | Starting buffer index | -| `mapping.bit_addressing` | boolean | No | - | Enable bit-level access | - -#### Supported Buffer Types - -| Type | IEC Address | Element Size | Direction | Description | -|------|-------------|--------------|-----------|-------------| -| `bool_input` | %IX | 1 bit | Read | Digital inputs | -| `bool_output` | %QX | 1 bit | Read/Write | Digital outputs | -| `bool_memory` | %MX | 1 bit | Read/Write | Internal markers | -| `byte_input` | %IB | 1 byte | Read | Byte inputs | -| `byte_output` | %QB | 1 byte | Read/Write | Byte outputs | -| `int_input` | %IW | 2 bytes | Read | Word inputs (UINT) | -| `int_output` | %QW | 2 bytes | Read/Write | Word outputs (UINT) | -| `int_memory` | %MW | 2 bytes | Read/Write | Word memory (UINT) | -| `dint_input` | %ID | 4 bytes | Read | Double word inputs (UDINT) | -| `dint_output` | %QD | 4 bytes | Read/Write | Double word outputs (UDINT) | -| `dint_memory` | %MD | 4 bytes | Read/Write | Double word memory (UDINT) | -| `lint_input` | %IL | 8 bytes | Read | Long word inputs (ULINT) | -| `lint_output` | %QL | 8 bytes | Read/Write | Long word outputs (ULINT) | -| `lint_memory` | %ML | 8 bytes | Read/Write | Long word memory (ULINT) | - -### System Areas Configuration - -System areas provide standard S7 address space mapping (PE, PA, MK): - -| Area | S7 Code | S7 Address | OpenPLC Default | Description | -|------|---------|------------|-----------------|-------------| -| `pe_area` | 0x81 | I | %IX | Process inputs | -| `pa_area` | 0x82 | Q | %QX | Process outputs | -| `mk_area` | 0x83 | M | %MX | Markers/flags | - -### Logging Configuration - -| Field | Type | Default | Description | -|-------|------|---------|-------------| -| `log_connections` | boolean | `true` | Log client connect/disconnect | -| `log_data_access` | boolean | `false` | Log read/write operations (verbose) | -| `log_errors` | boolean | `true` | Log errors and warnings | - ---- - -## Implementation Phases - -### Phase 1: Type Definitions - -**File:** `src/types/PLC/open-plc.ts` - -Add Zod schemas for S7Comm configuration validation and type inference. - -#### New Schemas to Add - -```typescript -// S7Comm Buffer Type Enumeration -const S7CommBufferTypeSchema = z.enum([ - 'bool_input', - 'bool_output', - 'bool_memory', - 'byte_input', - 'byte_output', - 'int_input', - 'int_output', - 'int_memory', - 'dint_input', - 'dint_output', - 'dint_memory', - 'lint_input', - 'lint_output', - 'lint_memory', -]) - -// S7Comm Server Configuration Schema -const S7CommServerSettingsSchema = z.object({ - enabled: z.boolean().default(true), - bindAddress: z.string().default('0.0.0.0'), - port: z.number().min(1).max(65535).default(102), - maxClients: z.number().min(1).max(1024).default(32), - workIntervalMs: z.number().min(1).max(10000).default(100), - sendTimeoutMs: z.number().min(100).max(60000).default(3000), - recvTimeoutMs: z.number().min(100).max(60000).default(3000), - pingTimeoutMs: z.number().min(1000).max(300000).default(10000), - pduSize: z.number().min(240).max(960).default(480), -}) - -// PLC Identity Schema -const S7CommPlcIdentitySchema = z.object({ - name: z.string().max(64).default('OpenPLC Runtime'), - moduleType: z.string().max(64).default('CPU 315-2 PN/DP'), - serialNumber: z.string().max(64).default('S C-OPENPLC01'), - copyright: z.string().max(64).default('OpenPLC Project'), - moduleName: z.string().max(64).default('OpenPLC'), -}) - -// Buffer Mapping Schema -const S7CommBufferMappingSchema = z.object({ - type: S7CommBufferTypeSchema, - startBuffer: z.number().min(0).max(1023).default(0), - bitAddressing: z.boolean().default(false), -}) - -// Data Block Schema -const S7CommDataBlockSchema = z.object({ - dbNumber: z.number().min(1).max(65535), - description: z.string().max(128).default(''), - sizeBytes: z.number().min(1).max(65536), - mapping: S7CommBufferMappingSchema, -}) - -// System Area Schema -const S7CommSystemAreaSchema = z.object({ - enabled: z.boolean().default(false), - sizeBytes: z.number().min(1).max(65536).default(128), - mapping: S7CommBufferMappingSchema.optional(), -}) - -// System Areas Container Schema -const S7CommSystemAreasSchema = z.object({ - peArea: S7CommSystemAreaSchema.optional(), - paArea: S7CommSystemAreaSchema.optional(), - mkArea: S7CommSystemAreaSchema.optional(), -}) - -// Logging Schema -const S7CommLoggingSchema = z.object({ - logConnections: z.boolean().default(true), - logDataAccess: z.boolean().default(false), - logErrors: z.boolean().default(true), -}) - -// Complete S7Comm Slave Config Schema -const S7CommSlaveConfigSchema = z.object({ - server: S7CommServerSettingsSchema, - plcIdentity: S7CommPlcIdentitySchema.optional(), - dataBlocks: z.array(S7CommDataBlockSchema).max(64).default([]), - systemAreas: S7CommSystemAreasSchema.optional(), - logging: S7CommLoggingSchema.optional(), -}) -``` - -#### Update PLCServerSchema - -```typescript -const PLCServerSchema = z.object({ - name: z.string(), - protocol: PLCServerProtocolSchema, - modbusSlaveConfig: ModbusSlaveConfigSchema.optional(), - s7commSlaveConfig: S7CommSlaveConfigSchema.optional(), // Add this line -}) -``` - -#### Export Types - -```typescript -export type S7CommBufferType = z.infer -export type S7CommServerSettings = z.infer -export type S7CommPlcIdentity = z.infer -export type S7CommBufferMapping = z.infer -export type S7CommDataBlock = z.infer -export type S7CommSystemArea = z.infer -export type S7CommSystemAreas = z.infer -export type S7CommLogging = z.infer -export type S7CommSlaveConfig = z.infer -``` - ---- - -### Phase 2: State Management - -**File:** `src/renderer/store/slices/project/slice.ts` - -Add Zustand actions for S7Comm configuration management. - -#### New Actions - -```typescript -// Update S7Comm server settings (enabled, port, timeouts, etc.) -updateS7CommServerSettings: ( - serverName: string, - settings: Partial -) => void - -// Update PLC identity configuration -updateS7CommPlcIdentity: ( - serverName: string, - identity: Partial -) => void - -// Add a new data block -addS7CommDataBlock: ( - serverName: string, - dataBlock: S7CommDataBlock -) => void - -// Update an existing data block -updateS7CommDataBlock: ( - serverName: string, - dbNumber: number, - updates: Partial -) => void - -// Remove a data block -removeS7CommDataBlock: ( - serverName: string, - dbNumber: number -) => void - -// Update system area configuration -updateS7CommSystemArea: ( - serverName: string, - area: 'peArea' | 'paArea' | 'mkArea', - config: Partial -) => void - -// Update logging configuration -updateS7CommLogging: ( - serverName: string, - logging: Partial -) => void -``` - -#### Modify createServer Action - -Update the `createServer` action to initialize default S7Comm configuration: - -```typescript -createServer: (request) => { - // ... existing validation ... - - const newServer: PLCServer = { - name: request.name, - protocol: request.protocol, - // Initialize config based on protocol - ...(request.protocol === 'modbus-tcp' && { - modbusSlaveConfig: { - enabled: false, - networkInterface: '0.0.0.0', - port: 502, - }, - }), - ...(request.protocol === 's7comm' && { - s7commSlaveConfig: { - server: { - enabled: false, - bindAddress: '0.0.0.0', - port: 102, - maxClients: 32, - workIntervalMs: 100, - sendTimeoutMs: 3000, - recvTimeoutMs: 3000, - pingTimeoutMs: 10000, - pduSize: 480, - }, - dataBlocks: [], - }, - }), - } - - // ... rest of implementation ... -} -``` - -#### Default Data Blocks - -Consider providing a helper function to add common default data blocks: - -```typescript -const DEFAULT_S7COMM_DATA_BLOCKS: S7CommDataBlock[] = [ - { - dbNumber: 1, - description: 'Digital Inputs (%IX)', - sizeBytes: 128, - mapping: { type: 'bool_input', startBuffer: 0, bitAddressing: true }, - }, - { - dbNumber: 2, - description: 'Digital Outputs (%QX)', - sizeBytes: 128, - mapping: { type: 'bool_output', startBuffer: 0, bitAddressing: true }, - }, - { - dbNumber: 10, - description: 'Analog Inputs (%IW)', - sizeBytes: 2048, - mapping: { type: 'int_input', startBuffer: 0, bitAddressing: false }, - }, - { - dbNumber: 20, - description: 'Analog Outputs (%QW)', - sizeBytes: 2048, - mapping: { type: 'int_output', startBuffer: 0, bitAddressing: false }, - }, -] -``` - ---- - -### Phase 3: Configuration Generation - -**New File:** `src/utils/s7comm/generate-s7comm-config.ts` - -Create the JSON configuration generator that converts editor state to runtime format. - -```typescript -import type { PLCServer, S7CommSlaveConfig } from '@root/types/PLC/open-plc' - -/** - * Generates the S7Comm configuration JSON for the runtime plugin. - * Converts camelCase properties to snake_case expected by the C plugin. - * - * @param servers - Array of configured PLC servers - * @returns JSON string for s7comm.json or null if no enabled S7Comm server - */ -export function generateS7CommConfig(servers: PLCServer[]): string | null { - const s7commServer = servers.find( - (server) => - server.protocol === 's7comm' && - server.s7commSlaveConfig?.server.enabled - ) - - if (!s7commServer?.s7commSlaveConfig) { - return null - } - - const config = s7commServer.s7commSlaveConfig - - const runtimeConfig = { - server: { - enabled: config.server.enabled, - bind_address: config.server.bindAddress, - port: config.server.port, - max_clients: config.server.maxClients, - work_interval_ms: config.server.workIntervalMs, - send_timeout_ms: config.server.sendTimeoutMs, - recv_timeout_ms: config.server.recvTimeoutMs, - ping_timeout_ms: config.server.pingTimeoutMs, - pdu_size: config.server.pduSize, - }, - - ...(config.plcIdentity && { - plc_identity: { - name: config.plcIdentity.name, - module_type: config.plcIdentity.moduleType, - serial_number: config.plcIdentity.serialNumber, - copyright: config.plcIdentity.copyright, - module_name: config.plcIdentity.moduleName, - }, - }), - - data_blocks: config.dataBlocks.map((db) => ({ - db_number: db.dbNumber, - description: db.description, - size_bytes: db.sizeBytes, - mapping: { - type: db.mapping.type, - start_buffer: db.mapping.startBuffer, - bit_addressing: db.mapping.bitAddressing, - }, - })), - - ...(config.systemAreas && { - system_areas: { - ...(config.systemAreas.peArea && { - pe_area: { - enabled: config.systemAreas.peArea.enabled, - size_bytes: config.systemAreas.peArea.sizeBytes, - ...(config.systemAreas.peArea.mapping && { - mapping: { - type: config.systemAreas.peArea.mapping.type, - start_buffer: config.systemAreas.peArea.mapping.startBuffer, - }, - }), - }, - }), - ...(config.systemAreas.paArea && { - pa_area: { - enabled: config.systemAreas.paArea.enabled, - size_bytes: config.systemAreas.paArea.sizeBytes, - ...(config.systemAreas.paArea.mapping && { - mapping: { - type: config.systemAreas.paArea.mapping.type, - start_buffer: config.systemAreas.paArea.mapping.startBuffer, - }, - }), - }, - }), - ...(config.systemAreas.mkArea && { - mk_area: { - enabled: config.systemAreas.mkArea.enabled, - size_bytes: config.systemAreas.mkArea.sizeBytes, - ...(config.systemAreas.mkArea.mapping && { - mapping: { - type: config.systemAreas.mkArea.mapping.type, - start_buffer: config.systemAreas.mkArea.mapping.startBuffer, - }, - }), - }, - }), - }, - }), - - ...(config.logging && { - logging: { - log_connections: config.logging.logConnections, - log_data_access: config.logging.logDataAccess, - log_errors: config.logging.logErrors, - }, - }), - } - - return JSON.stringify(runtimeConfig, null, 2) -} -``` - -**New File:** `src/utils/s7comm/index.ts` - -```typescript -export { generateS7CommConfig } from './generate-s7comm-config' -``` - ---- - -### Phase 4: Compiler Integration - -**File:** `src/main/modules/compiler/compiler-module.ts` - -Add the S7Comm configuration file generation to the compilation pipeline. - -#### Add Import - -```typescript -import { generateS7CommConfig } from '@utils/s7comm' -``` - -#### Add New Method - -```typescript -/** - * Generates the S7Comm server configuration file for the runtime plugin. - * Creates conf/s7comm.json if an S7Comm server is enabled. - */ -async handleGenerateS7CommConfig( - sourceTargetFolderPath: string, - projectData: ProjectState['data'], - handleOutputData: HandleOutputDataCallback, -): Promise { - const s7commConfig = generateS7CommConfig(projectData.servers) - - if (s7commConfig) { - const confFolderPath = join(sourceTargetFolderPath, 'conf') - await mkdir(confFolderPath, { recursive: true }) - - const configFilePath = join(confFolderPath, 's7comm.json') - await writeFile(configFilePath, s7commConfig, 'utf-8') - - handleOutputData('Generated conf/s7comm.json', 'info') - } -} -``` - -#### Update Compilation Pipeline - -Find the location where `handleGenerateModbusSlaveConfig` is called and add the S7Comm generation alongside it: - -```typescript -// Generate protocol configurations -await this.handleGenerateModbusSlaveConfig(sourceTargetFolderPath, projectData, handleOutputData) -await this.handleGenerateModbusMasterConfig(sourceTargetFolderPath, projectData, handleOutputData) -await this.handleGenerateS7CommConfig(sourceTargetFolderPath, projectData, handleOutputData) // Add this -``` - ---- - -### Phase 5: UI Components - -Create the S7Comm server editor UI components. - -**New Directory:** `src/renderer/components/_features/[workspace]/editor/server/s7comm-server/` - -#### Component Structure - -``` -s7comm-server/ -├── index.tsx # Main S7Comm server editor -├── server-config-section.tsx # Server network settings -├── plc-identity-section.tsx # PLC identity configuration -├── data-blocks-section.tsx # Data blocks list -├── data-block-modal.tsx # Modal for add/edit data block -├── system-areas-section.tsx # PE/PA/MK area configuration -└── logging-section.tsx # Logging toggles -``` - -#### Main Editor Component (`index.tsx`) - -```tsx -import { useOpenPLCStore } from '@root/renderer/store' -import { ServerConfigSection } from './server-config-section' -import { PlcIdentitySection } from './plc-identity-section' -import { DataBlocksSection } from './data-blocks-section' -import { SystemAreasSection } from './system-areas-section' -import { LoggingSection } from './logging-section' - -type S7CommServerEditorProps = { - serverName: string -} - -export const S7CommServerEditor = ({ serverName }: S7CommServerEditorProps) => { - const { servers } = useOpenPLCStore.useSelector('project').data - const server = servers.find((s) => s.name === serverName) - - if (!server || server.protocol !== 's7comm' || !server.s7commSlaveConfig) { - return
S7Comm server configuration not found
- } - - const config = server.s7commSlaveConfig - - return ( -
- - - - - -
- ) -} -``` - -#### Server Configuration Section (`server-config-section.tsx`) - -UI elements: -- Enable/disable toggle switch -- Bind address dropdown (`0.0.0.0`, `127.0.0.1`) -- Port input with validation (1-65535, note about port 102) -- Max clients input (1-1024) -- Collapsible "Advanced Settings" accordion: - - Work interval (ms) - - Send timeout (ms) - - Receive timeout (ms) - - Ping timeout (ms) - - PDU size slider (240-960) - -#### PLC Identity Section (`plc-identity-section.tsx`) - -Collapsible accordion with: -- Name input (max 64 chars) -- Module type input (max 64 chars) -- Serial number input (max 64 chars) -- Copyright input (max 64 chars) -- Module name input (max 64 chars) - -Pre-populated with defaults for convenience. - -#### Data Blocks Section (`data-blocks-section.tsx`) - -Main configuration area: -- Table with columns: DB Number, Description, Size (bytes), Mapping Type, Actions -- "Add Data Block" button -- Edit/Delete icons per row -- Empty state message when no blocks configured -- Option to "Add Default Blocks" for quick setup - -#### Data Block Modal (`data-block-modal.tsx`) - -Modal dialog for creating/editing data blocks: -- DB Number input (1-65535, unique validation) -- Description input (optional, max 128 chars) -- Size in bytes input (1-65536) -- Mapping type dropdown (all 15 buffer types) -- Start buffer input (0-1023) -- Bit addressing toggle (only shown for bool types) - -Validation: -- DB number must be unique -- Size must be positive -- Buffer range must not exceed limits - -#### System Areas Section (`system-areas-section.tsx`) - -Collapsible accordion with three sub-sections: -- **PE Area (Process Inputs):** Enable toggle, size, mapping config -- **PA Area (Process Outputs):** Enable toggle, size, mapping config -- **MK Area (Markers):** Enable toggle, size, mapping config - -Each area shows relevant defaults when enabled. - -#### Logging Section (`logging-section.tsx`) - -Simple toggle switches: -- Log connections (default: on) -- Log data access (default: off, with "verbose" warning) -- Log errors (default: on) - ---- - -### Phase 6: Enable S7Comm in UI - -#### Update Server Creation Modal - -**File:** `src/renderer/components/_features/[workspace]/create-element/element-card/index.tsx` - -Change the disabled flag for S7Comm: - -```typescript -const ServerProtocolSources = [ - { value: 'modbus-tcp', label: 'Modbus/TCP', disabled: false }, - { value: 's7comm', label: 'Siemens S7comm', disabled: false }, // Changed from true - { value: 'ethernet-ip', label: 'EtherNet/IP', disabled: true }, -] as const -``` - -#### Update Server Editor Export - -**File:** `src/renderer/components/_features/[workspace]/editor/server/index.ts` - -```typescript -export { ModbusServerEditor } from './modbus-server' -export { S7CommServerEditor } from './s7comm-server' -``` - -#### Update Editor Tab Routing - -Find where server tabs are rendered (likely in the workspace editor area) and add routing for S7Comm: - -```tsx -{server.protocol === 'modbus-tcp' && ( - -)} -{server.protocol === 's7comm' && ( - -)} -``` - ---- - -## Files Summary - -### Files to Create - -| File | Purpose | -|------|---------| -| `src/utils/s7comm/generate-s7comm-config.ts` | JSON configuration generator | -| `src/utils/s7comm/index.ts` | Utils barrel export | -| `src/renderer/components/_features/[workspace]/editor/server/s7comm-server/index.tsx` | Main editor component | -| `src/renderer/components/_features/[workspace]/editor/server/s7comm-server/server-config-section.tsx` | Server settings UI | -| `src/renderer/components/_features/[workspace]/editor/server/s7comm-server/plc-identity-section.tsx` | PLC identity UI | -| `src/renderer/components/_features/[workspace]/editor/server/s7comm-server/data-blocks-section.tsx` | Data blocks list UI | -| `src/renderer/components/_features/[workspace]/editor/server/s7comm-server/data-block-modal.tsx` | Data block add/edit modal | -| `src/renderer/components/_features/[workspace]/editor/server/s7comm-server/system-areas-section.tsx` | System areas UI | -| `src/renderer/components/_features/[workspace]/editor/server/s7comm-server/logging-section.tsx` | Logging toggles UI | - -### Files to Modify - -| File | Changes | -|------|---------| -| `src/types/PLC/open-plc.ts` | Add S7Comm Zod schemas and types | -| `src/renderer/store/slices/project/slice.ts` | Add S7Comm state actions | -| `src/main/modules/compiler/compiler-module.ts` | Add S7Comm config generation | -| `src/renderer/components/_features/[workspace]/create-element/element-card/index.tsx` | Enable S7Comm option | -| `src/renderer/components/_features/[workspace]/editor/server/index.ts` | Export S7CommServerEditor | - ---- - -## Testing Checklist - -### Unit Tests - -- [ ] S7Comm Zod schema validation -- [ ] `generateS7CommConfig` function with various configurations -- [ ] State actions for all S7Comm operations -- [ ] Data block uniqueness validation - -### Integration Tests - -- [ ] Create S7Comm server via UI -- [ ] Edit server configuration -- [ ] Add/edit/delete data blocks -- [ ] Configuration persists across sessions -- [ ] JSON file generated during compilation - -### Manual Testing - -- [ ] S7Comm option visible and enabled in server creation modal -- [ ] Server configuration section renders correctly -- [ ] All input fields validate properly -- [ ] Data block modal opens/closes correctly -- [ ] Table updates when data blocks change -- [ ] Advanced settings accordion collapses/expands -- [ ] Generated JSON matches runtime expectations -- [ ] Runtime successfully loads generated configuration - -### Edge Cases - -- [ ] Empty data blocks array -- [ ] Maximum 64 data blocks -- [ ] Duplicate DB number validation -- [ ] Port 102 privilege warning -- [ ] Buffer range overflow validation -- [ ] Very long description strings (128 char limit) - ---- - -## References - -- [OpenPLC Runtime S7Comm Plugin Documentation](../../../openplc-runtime/docs/) -- [S7Comm Protocol Specification](https://snap7.sourceforge.net/) -- [Existing Modbus Server Implementation](../src/renderer/components/_features/[workspace]/editor/server/modbus-server/) diff --git a/docs/outdated/unified-frontend-serialization.md b/docs/outdated/unified-frontend-serialization.md deleted file mode 100644 index 9809225b6..000000000 --- a/docs/outdated/unified-frontend-serialization.md +++ /dev/null @@ -1,115 +0,0 @@ -# Unified Frontend Parsing & Serialization: Load + Save - -## Context - -The frontend/backend separation should follow a clean principle: **the backend is a file I/O layer** (read/write raw files), and the **frontend owns all parsing and serialization**. This makes the frontend code identical for Electron and web, and eliminates double-parsing. - -Currently: -- **Load**: Backend reads files, parses POUs, converts to IPC format → adapter converts to flat format → frontend re-parses variables for type reclassification (double-parsing) -- **Save**: Frontend sends structured objects → adapter converts to IPC format → backend re-serializes POUs to text (double-serialization) -- **3 duplicate `executeSave` functions** exist across components - -### Shared Utilities (already exist, will be reused) -- `src/frontend/utils/PLC/pou-text-parser.ts` — parsers for all POU languages (already used by backend via `@root/`) -- `src/frontend/utils/PLC/pou-text-serializer.ts` — serializers for all POU languages (already used by backend via `@root/`) -- `src/frontend/utils/PLC/pou-file-extensions.ts` — extension/folder/keyword mappings -- `src/frontend/utils/save-project.ts` — `sanitizePou`, `prepareSavePayload`, `collectDebugVariables` -- `src/frontend/utils/generate-iec-variables-to-string.ts` — variable serialization -- `src/frontend/utils/generate-iec-string-to-variables.ts` — variable parsing with project context - ---- - -## Phase 0: Eliminate executeSave Duplication - -### Step 0.1: Create `src/frontend/utils/save-actions.ts` -Shared `executeSaveProject(projectPort)` function that reads state, prepares payload, calls port, updates state. Replaces 3 inline copies. - -### Step 0.2: Export `sanitizePou` from `save-project.ts` -Change from private to exported. Needed by single-file save. - -### Step 0.3: Replace 3 executeSave duplicates -- `accelerator-handler.tsx`, `file.tsx` (menu), `default.tsx` (activity bar) - -### Step 0.4: Replace save logic in 2 modals -- `save-changes-modal.tsx`, `save-changes-file-modal.tsx` - ---- - -## Phase 1: Single-File Save with Frontend Serialization - -### Step 1.1: Fix backend saveFile for raw strings -**File:** `src/backend/editor/services/project-service/index.ts` - -Add `typeof content === 'string'` branch — write string directly without `JSON.stringify`. - -### Step 1.2: Add `executeSaveActiveFile` to `save-actions.ts` -For POUs: `sanitizePou()` → `serializePouToText()` → compute path → `projectPort.saveFile(path, text)`. -For device/data-type/resource/server: appropriate JSON serialization → `projectPort.saveFile()`. -Also updates `project.json` for debug variables. - -### Step 1.3: Wire Ctrl+S to `executeSaveActiveFile` -- Ctrl+S → `executeSaveActiveFile(projectPort)` (single file) -- Ctrl+Shift+S → `executeSaveProject(projectPort)` (full project) -- File menu: separate "Save" and "Save Project" items - ---- - -## Phase 2: Load with Frontend Parsing - -### Current load flow (to be simplified): -``` -Backend: read disk → parse POUs → IPC format → Adapter: convert to flat → Frontend: re-parse variables -``` - -### Target load flow: -``` -Backend: read disk → raw file contents → Adapter: pass through → Frontend: parse everything once -``` - -### Step 2.1: Add `readProjectFiles` to ProjectPort interface -**File:** `src/middleware/shared/ports/project-port.ts` - -New port method that returns raw file contents as strings. - -### Step 2.2: Implement Electron adapter for `readProjectFiles` -New IPC call that reads all project files and returns raw strings without parsing. - -### Step 2.3: Create frontend project parser utility -**File:** `src/frontend/utils/parse-project-files.ts` - -Pure function that takes raw file contents and produces `OpenProjectResponseData`: -1. Parse `projectJson` string -2. For each POU file: detect language/type from path, call appropriate parser -3. Parse variables with full project context (single pass) -4. Parse device config and pin mapping -5. Return data ready for `handleOpenProjectResponse` - -### Step 2.4: Update `openProject`/`openProjectByPath` flow -Adapter calls `readProjectFiles` + `parseProjectFiles` internally. Port contract stays the same. - -### Step 2.5: Backend simplification -Add raw file read IPC handler. Existing `openProject` remains for backward compatibility. - ---- - -## Phase 3: Full Project Save with Frontend Serialization (future PR) - -Migrate `executeSaveProject` to serialize all files on the frontend before sending to the backend. - ---- - -## Architecture After All Phases - -``` -LOAD: - Backend: fs.readFile → raw strings → IPC → Adapter: parseProjectFiles() → ProjectResponse → Store - -SAVE (single file): - Frontend: serializePouToText() → text string → projectPort.saveFile(path, text) → Backend: fs.writeFile - -SAVE (full project): - Frontend: serialize all files → projectPort.saveFiles([{path, content}]) → Backend: fs.writeFile each -``` - -Frontend owns: parsing, serialization, type classification, variable reclassification -Backend owns: file I/O, directory management, file watching, IPC transport diff --git a/docs/outdated/variable-id-audit.md b/docs/outdated/variable-id-audit.md deleted file mode 100644 index 6fd022558..000000000 --- a/docs/outdated/variable-id-audit.md +++ /dev/null @@ -1,918 +0,0 @@ -# Variable ID Audit - OpenPLC Editor - -## Executive Summary - -This document provides a comprehensive audit of all locations where variable IDs are currently used for linking variables to graphical elements in the OpenPLC Editor codebase. The audit was conducted as part of Phase 1 of the migration from UUID-based variable linking to name+type-based linking following IEC 61131-3 standards. - -**Key Findings:** -- Variable IDs are optional in the schema (`z.string().optional()`) -- IDs are generated using `crypto.randomUUID()` during variable creation -- Primary usage is in graphical editors (Ladder, FBD) for block-variable linking -- XML generation already uses variable names, not IDs -- A dual lookup system exists (by row index OR by variable ID) -- Broken links are created with synthetic IDs (`broken-${nodeId}`) - ---- - -## 1. Variable Schema Definitions - -### 1.1 PLCVariableSchema -**File:** `src/types/PLC/open-plc.ts` -**Lines:** 120-160 - -**Description:** Core variable type definition that includes an optional `id` field. - -```typescript -const PLCVariableSchema = z.object({ - id: z.string().optional(), // Line 121 - name: z.string(), - class: z.enum(['input', 'output', 'inOut', 'external', 'local', 'temp', 'global']).optional(), - type: z.discriminatedUnion('definition', [...]), - location: z.string(), - initialValue: z.string().or(z.null()).optional(), - documentation: z.string(), - debug: z.boolean(), -}) -``` - -**Usage Type:** Schema Definition -**Classification:** Data Structure -**Notes:** -- ID field is **optional**, indicating the system was designed to potentially work without IDs -- No validation or uniqueness constraints on the ID field -- ID is not required for variable creation - -### 1.2 PLCStructureVariableSchema -**File:** `src/types/PLC/open-plc.ts` -**Lines:** 55-100 - -**Description:** Variable definition within structure data types, also includes optional `id` field. - -```typescript -const PLCStructureVariableSchema = z.object({ - id: z.string().optional(), // Line 56 - name: z.string(), - type: z.discriminatedUnion('definition', [...]), - initialValue: z.object({...}).optional(), -}) -``` - -**Usage Type:** Schema Definition -**Classification:** Data Structure -**Notes:** -- Used for variables within user-defined structure types -- Same optional ID pattern as PLCVariableSchema - ---- - -## 2. Variable Creation with UUID Generation - -### 2.1 Local Variable Creation -**File:** `src/renderer/store/slices/project/slice.ts` -**Lines:** 276-280 - -**Description:** UUID generation during local variable creation in POUs. - -```typescript -variableToBeCreated.data = { - ...variableToBeCreated.data, - ...createVariableValidation(pou.data.variables, variableToBeCreated.data), - id: variableToBeCreated.data.id ? variableToBeCreated.data.id : crypto.randomUUID(), // Line 279 -} -``` - -**Usage Type:** Creation -**Classification:** ID Generation -**Fallback Mechanism:** Only generates UUID if `id` is not already present -**Notes:** -- Conditional generation: uses existing ID if present, otherwise generates new UUID -- Occurs in `createVariable` action for local scope -- Global variables do NOT receive IDs during creation (lines 250-265) - -### 2.2 Autocomplete Variable Creation (Ladder) -**File:** `src/renderer/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx` -**Lines:** 169-185 - -**Description:** UUID generation when creating variables from autocomplete in ladder editor. - -```typescript -const res = createVariable({ - data: { - id: uuidv4(), // Line 171 (using uuid library, equivalent to crypto.randomUUID()) - name: variableName, - type: {...}, - class: 'local', - location: '', - documentation: '', - debug: false, - }, - scope: 'local', - associatedPou: editor.meta.name, -}) -``` - -**Usage Type:** Creation -**Classification:** ID Generation -**Notes:** -- Uses `uuid` library's `v4()` function -- Always generates new ID for autocomplete-created variables -- Similar pattern exists in FBD autocomplete - -### 2.3 Additional UUID Generation Locations -**Files with `crypto.randomUUID()` usage:** -- `src/renderer/components/_atoms/graphical-editor/fbd/autocomplete/index.tsx` -- `src/renderer/store/slices/fbd/utils/index.ts` -- `src/renderer/store/slices/ladder/utils/index.ts` -- `src/utils/python/addPythonLocalVariables.ts` -- `src/utils/cpp/addCppLocalVariables.ts` - -**Usage Type:** Creation -**Classification:** ID Generation - ---- - -## 3. Variable Lookup Utilities - -### 3.1 Dual Lookup Function -**File:** `src/renderer/store/slices/project/utils/index.ts` -**Lines:** 3-26 - -**Description:** Core utility function that supports looking up variables by EITHER row index OR variable ID. - -```typescript -export const getVariableBasedOnRowIdOrVariableId = ( - variables: PLCVariable[] | Omit[], - rowId?: number, - variableId?: string, -): PLCVariable | Omit | null => { - // Priority 1: Lookup by row index - if (rowId !== undefined) { - const variable = variables[rowId] // Line 9 - if (!variable) return null - return variable - } - - // Priority 2: Lookup by variable ID - if (variableId) { - const variable = variables.find((variable) => variable.id === variableId) // Line 17 - if (!variable) return null - return variable - } - - console.error('variableId or rowId not given') - return null -} -``` - -**Usage Type:** Lookup -**Classification:** Utility Function -**Fallback Mechanism:** Row index takes priority over variable ID -**Notes:** -- **Critical finding:** Row index lookup is prioritized over ID lookup -- This suggests the system was designed to work primarily with positional references -- ID lookup is a fallback mechanism -- Used extensively throughout the codebase (20+ call sites) - -### 3.2 Lookup Usage in Project Actions -**File:** `src/renderer/store/slices/project/slice.ts` -**Multiple locations:** - -#### Update Variable (Global) -**Lines:** 320-324 -```typescript -const variableToUpdate = getVariableBasedOnRowIdOrVariableId( - project.data.configuration.resource.globalVariables, - dataToBeUpdated.rowId, - dataToBeUpdated.data.id, -) -``` - -#### Update Variable (Local) -**Lines:** 347-351 -```typescript -const variableToUpdate = getVariableBasedOnRowIdOrVariableId( - pou.data.variables, - dataToBeUpdated.rowId, - dataToBeUpdated.variableId, -) -``` - -#### Delete Variable -**Lines:** 462-466, 482-486 -```typescript -const variable = getVariableBasedOnRowIdOrVariableId( - project.data.configuration.resource.globalVariables, - variableToBeDeleted.rowId, - variableToBeDeleted.variableId, -) -``` - -#### Rearrange Variables -**Lines:** 510-514, 530 -```typescript -const variableToBeRemoved = getVariableBasedOnRowIdOrVariableId( - project.data.configuration.resource.globalVariables, - rowId, - variableId, -) -``` - -**Usage Type:** Lookup -**Classification:** State Management -**Notes:** -- All CRUD operations use this dual lookup pattern -- Row index is always provided when available -- Variable ID is used as fallback - ---- - -## 4. Graphical Editor Block-Variable Linking - -### 4.1 Block Node Data Structure -**File:** `src/renderer/components/_atoms/graphical-editor/ladder/block.tsx` -**Lines:** 34-48 - -**Description:** Data structure for blocks that includes `handleTableId` for tracking variable connections. - -```typescript -export type LadderBlockConnectedVariables = { - handleId: string - handleTableId?: string // Line 36 - References variable ID - type: 'input' | 'output' - variable: PLCVariable | undefined -}[] - -export type BlockNodeData = BasicNodeData & { - variant: T - executionControl: boolean - lockExecutionControl: boolean - connectedVariables: LadderBlockConnectedVariables // Line 45 - variable: { id?: string; name: string } | PLCVariable // Line 46 - hasDivergence?: boolean -} -``` - -**Usage Type:** Linking -**Classification:** Data Structure -**Notes:** -- `handleTableId` stores the variable ID from the block variant's variable definition -- Used to track which variables are connected to which block handles -- The `variable` field can be either a full PLCVariable or a minimal object with optional ID - -### 4.2 Variable Connection in Autocomplete -**File:** `src/renderer/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx` -**Lines:** 124-137 - -**Description:** Populates `handleTableId` when connecting variables to blocks via autocomplete. - -```typescript -const connectedVariables: LadderBlockConnectedVariables = [ - ...(relatedBlock.data as BlockNodeData).connectedVariables.filter( - (v) => v.type !== variableNode.data.variant || v.handleId !== (variableNode as VariableNode).data.block.handleId, - ), - { - handleId: (variableNode as VariableNode).data.block.handleId, - handleTableId: (relatedBlock.data as BlockNodeData).variant.variables.find( - (v) => v.name === (variableNode as VariableNode).data.block.handleId, - )?.id, // Line 131-133 - Looks up ID from block variant definition - type: (variableNode as VariableNode).data.variant, - variable: variable, - }, -] -``` - -**Usage Type:** Linking -**Classification:** Connection Management -**Notes:** -- `handleTableId` is populated by finding the matching variable in the block variant's definition -- This creates a link between the block handle and the variable table -- Used for multi-input/output blocks - -### 4.3 Block Divergence Detection -**File:** `src/renderer/components/_atoms/graphical-editor/ladder/block.tsx` -**Lines:** 627-773 (handleUpdateDivergence function) - -**Description:** Uses `handleTableId` to maintain connections during library updates. - -```typescript -const handleUpdateDivergence = () => { - const { variables, rung, node, edges } = getLadderPouVariablesRungNodeAndEdges(editor, pous, ladderFlows, { - nodeId: id, - }) - if (!rung || !node) return - - // ... divergence detection logic ... - - const connectedVariables: LadderBlockConnectedVariables = [] - data.connectedVariables.forEach((connectedVariable) => { - const newVariable = newNodeVariables.find( - (v) => v.id === connectedVariable.handleTableId, // Line 714 - Uses handleTableId to find matching variable - ) - if (newVariable) { - connectedVariables.push({ - handleId: newVariable.name, - handleTableId: newVariable.id, - type: connectedVariable.type, - variable: connectedVariable.variable, - }) - } - }) - // ... update node with new connections ... -} -``` - -**Usage Type:** Linking -**Classification:** Divergence Management -**Notes:** -- **Critical for library updates:** When a function block definition changes, this maintains variable connections -- Matches old connections to new block definition using `handleTableId` -- This is a key area where ID-based linking is actively used - -### 4.4 Variable Node ID Comparison -**File:** `src/renderer/components/_atoms/graphical-editor/ladder/block.tsx` -**Lines:** 479-496 - -**Description:** Compares variable IDs to detect when variable table changes affect blocks. - -```typescript -const variable = variables.selected -if (!variable) { - setWrongVariable(true) - return -} - -if ((node.data as BasicNodeData).variable.id === variable.id) { // Line 479 - ID comparison - if ((node.data as BasicNodeData).variable.name !== variable.name) { - updateNode({ - editorName: editor.meta.name, - rungId: rung.id, - nodeId: node.id, - node: { - ...node, - data: { - ...node.data, - variable, - }, - }, - }) - setWrongVariable(false) - return - } -} -``` - -**Usage Type:** Linking -**Classification:** Synchronization -**Notes:** -- Detects when a variable's name changes but ID remains the same -- Updates block to reflect new variable name -- Relies on ID stability across renames - ---- - -## 5. Variable Component Connection - -### 5.1 Variable Node Update on Table Changes -**File:** `src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx` -**Lines:** 143-213 - -**Description:** Monitors variable table changes and updates variable nodes using ID comparison. - -```typescript -useEffect(() => { - const { - node: variableNode, - rung, - variables, - } = getLadderPouVariablesRungNodeAndEdges(editor, pous, ladderFlows, { - nodeId: id, - variableName: variableValue, - }) - if (!rung || !variableNode) return - - const variable = variables.selected - if (!variable || !inputVariableRef) { - setIsAVariable(false) - } else { - // if the variable is not the same as the one in the node, update the node - if (variable.id !== (variableNode as VariableNode).data.variable.id) { // Line 159 - ID comparison - updateNode({...}) - updateRelatedNode(rung, variableNode as VariableNode, variable) - } - - // if the variable is the same as the one in the node, update the node - if ( - variable.id === (variableNode as VariableNode).data.variable.id && // Line 177 - ID comparison - variable.name !== (variableNode as VariableNode).data.variable.name - ) { - updateNode({...}) - updateRelatedNode(rung, variableNode as VariableNode, variable) - } - // ... validation logic ... - } -}, [pous]) -``` - -**Usage Type:** Linking -**Classification:** Synchronization -**Notes:** -- Uses ID to detect when variable has changed -- Handles both variable replacement and variable rename scenarios -- Updates both the variable node and related block nodes - -### 5.2 Update Related Block Node -**File:** `src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx` -**Lines:** 95-128 - -**Description:** Updates block's `connectedVariables` array when variable changes. - -```typescript -const updateRelatedNode = (rung: RungLadderState, variableNode: VariableNode, variable: PLCVariable) => { - const relatedBlock = rung.nodes.find((node) => node.id === variableNode.data.block.id) - if (!relatedBlock) { - setInputError(true) - return - } - - const connectedVariables: LadderBlockConnectedVariables = [ - ...(relatedBlock.data as BlockNodeData).connectedVariables.filter( - (v) => v.type !== variableNode.data.variant || v.handleId !== variableNode.data.block.handleId, - ), - { - handleId: variableNode.data.block.handleId, - handleTableId: (relatedBlock.data as BlockNodeData).variant.variables.find( - (v) => v.name === variableNode.data.block.handleId, - )?.id, // Line 108-110 - Populates handleTableId - type: variableNode.data.variant, - variable, - }, - ] - - updateNode({...}) -} -``` - -**Usage Type:** Linking -**Classification:** Connection Management -**Notes:** -- Maintains `handleTableId` when updating connections -- Ensures block knows which variable is connected to which handle - ---- - -## 6. Variable Renaming Logic - -### 6.1 Rename with Broken Link Creation -**File:** `src/renderer/components/_molecules/variables-table/editable-cell.tsx` -**Lines:** 124-265 (EditableNameCell onBlur) - -**Description:** When user declines to propagate rename, creates broken links with synthetic IDs. - -```typescript -const onBlur = async () => { - // ... validation ... - - /* 1 ▸ which blocks use the variable? */ - const nodesUsingVarLadder = findNodesUsingVariable(ladderFlows, oldName) - const nodesUsingVarFbd = findNodesUsingVariableFbd(fbdFlows, oldName) - - let shouldPropagate = true - if (nodesUsingVarLadder.length || nodesUsingVarFbd.length) { - shouldPropagate = await askRenameBlocks() - } - - /* 2 ▸ IF NOT propagating, break the link before renaming */ - if (nodesUsingVarLadder.length && !shouldPropagate && language === 'ld') { - addSnapshot(editor.meta.name) - - nodesUsingVarLadder.forEach(({ rungId, nodeId }) => { - const { rung, node } = getLadderPouVariablesRungNodeAndEdges(editor, pous, ladderFlows, { nodeId }) - if (!rung || !node) return - - const variableClone = { - ...(node.data as { variable: PLCVariable }).variable, - id: `broken-${nodeId}`, // Line 159 - Creates synthetic broken ID - name: oldName, - } - - updateNode({ - editorName: editor.meta.name, - rungId, - nodeId, - node: { - ...node, - data: { - ...node.data, - variable: variableClone, - wrongVariable: true, - }, - }, - }) - }) - } - // ... similar logic for FBD ... -} -``` - -**Usage Type:** Linking -**Classification:** Broken Link Management -**Notes:** -- **Synthetic ID pattern:** `broken-${nodeId}` indicates a deliberately broken link -- Preserves old variable name in the node -- Sets `wrongVariable: true` flag for visual indication -- Same pattern used for FBD (lines 179-208) - -### 6.2 Name-Based Variable Finding -**File:** `src/renderer/components/_molecules/variables-table/editable-cell.tsx` -**Lines:** 90-106, 108-122 - -**Description:** Finds nodes using variables by **name** (case-insensitive), not ID. - -```typescript -const findNodesUsingVariable = (ladderFlows: LadderFlowState['ladderFlows'], varName: string) => { - const lower = varName.toLowerCase() - const matches: { rungId: string; nodeId: string }[] = [] - - ladderFlows.forEach((flow) => - flow.rungs.forEach((rung) => - rung.nodes.forEach((node) => { - const data = (node.data as { variable?: PLCVariable | { name?: string } }).variable - if (data?.name?.toLowerCase() === lower) { // Line 98 - Name-based comparison - matches.push({ rungId: rung.id, nodeId: node.id }) - } - }), - ), - ) - - return matches -} -``` - -**Usage Type:** Lookup -**Classification:** Name-Based Search -**Notes:** -- **Critical finding:** Variable rename detection uses NAME matching, not ID matching -- Case-insensitive comparison -- This suggests the system already has name-based lookup infrastructure - -### 6.3 Propagate Rename to Blocks -**File:** `src/renderer/components/_molecules/variables-table/editable-cell.tsx` -**Lines:** 220-239 - -**Description:** When user accepts propagation, updates variable name in all nodes. - -```typescript -/* 4 ▸ If the user said YES, propagate the change to the blocks */ -if (nodesUsingVarLadder.length && shouldPropagate && language === 'ld') { - nodesUsingVarLadder.forEach(({ rungId, nodeId }) => { - const { rung, node } = getLadderPouVariablesRungNodeAndEdges(editor, pous, ladderFlows, { nodeId }) - if (!rung || !node) return - - updateNode({ - editorName: editor.meta.name, - rungId, - nodeId, - node: { - ...node, - data: { - ...node.data, - variable: { ...(node.data as { variable: PLCVariable }).variable, name: newName }, // Line 233 - Updates name - wrongVariable: false, - }, - }, - }) - }) -} -``` - -**Usage Type:** Linking -**Classification:** Synchronization -**Notes:** -- Updates only the name field, preserves other variable properties including ID -- Clears `wrongVariable` flag - ---- - -## 7. XML Serialization - -### 7.1 Ladder XML Generation - Contacts -**File:** `src/utils/PLC/xml-generator/codesys/language/ladder-xml.ts` -**Lines:** 337-383 - -**Description:** XML generation uses variable **names**, not IDs. - -```typescript -const contactToXML = ( - contact: ContactNode, - rung: RungLadderState, - offsetY: number = 0, - leftRailId: string, -): ContactLadderXML => { - // ... connection logic ... - return { - '@localId': contact.data.numericId, - '@negated': contact.data.variant === 'negated', - // ... other properties ... - variable: contact.data.variable && contact.data.variable.name !== '' ? contact.data.variable.name : 'A', // Line 381 - Uses NAME - } -} -``` - -**Usage Type:** Serialization -**Classification:** XML Export -**Notes:** -- **Critical finding:** XML output uses variable names, not IDs -- IDs are never serialized to XML -- This validates that name-based linking is architecturally sound for compilation - -### 7.2 Ladder XML Generation - Coils -**File:** `src/utils/PLC/xml-generator/codesys/language/ladder-xml.ts` -**Lines:** 385-427 - -**Description:** Coil XML generation also uses variable names. - -```typescript -const coilToXml = (coil: CoilNode, rung: RungLadderState, offsetY: number = 0, leftRailId: string): CoilLadderXML => { - // ... connection logic ... - return { - '@localId': coil.data.numericId, - // ... other properties ... - variable: coil.data.variable && coil.data.variable.name !== '' ? coil.data.variable.name : 'A', // Line 425 - Uses NAME - } -} -``` - -**Usage Type:** Serialization -**Classification:** XML Export - -### 7.3 Ladder XML Generation - Blocks -**File:** `src/utils/PLC/xml-generator/codesys/language/ladder-xml.ts` -**Lines:** 429-524 - -**Description:** Block XML generation uses variable names for input/output variables. - -```typescript -const blockToXml = ( - block: BlockNode, - rung: RungLadderState, - offsetY: number = 0, - leftRailId: string, -): BlockLadderXML => { - // ... connection logic ... - - const inputVariables = block.data.variant.variables - .filter((v) => v.class === 'input' || v.class === 'inOut') - .map((variable, index) => { - const variableNode = rung.nodes.find( - (node) => - node.type === 'variable' && - (node as VariableNode).data.variant === 'input' && - (node as VariableNode).data.block.handleId === variable.name, - ) as VariableNode | undefined - - return { - '@formalParameter': variable.name, // Line 476 - Uses variable NAME from block definition - '#text': variableNode?.data.variable.name ?? '', // Line 477 - Uses connected variable NAME - } - }) - - const outputVariable = block.data.variant.variables - .filter((v) => v.class === 'output') - .map((variable) => { - // ... similar pattern ... - return { - '@formalParameter': variable.name, // Line 504 - Uses NAME - '#text': variableNode?.data.variable.name ?? '', // Line 505 - Uses NAME - } - }) - - return { - '@localId': block.data.numericId, - '@typeName': block.data.variant.name, - '@instanceName': block.data.variable && 'name' in block.data.variable ? block.data.variable.name : '', // Line 508 - Uses NAME - // ... other properties ... - inputVariables, - outputVariables: outputVariable, - } -} -``` - -**Usage Type:** Serialization -**Classification:** XML Export -**Notes:** -- Block instance name uses variable name -- Input/output variable connections use names -- No IDs in XML output - ---- - -## 8. Project Persistence - -### 8.1 Project Save -**File:** `src/main/services/project-service/index.ts` -**Lines:** 307-310 (approximate, based on onboarding guide) - -**Description:** Projects are saved as JSON with variable IDs serialized. - -**Usage Type:** Persistence -**Classification:** File I/O -**Notes:** -- Variables with IDs will have those IDs persisted to JSON -- Variables without IDs will be saved without ID field (since it's optional) -- No special handling or transformation of IDs during save - -### 8.2 Project Load -**File:** `src/main/services/project-service/index.ts` - -**Description:** Projects are loaded from JSON, IDs are preserved if present. - -**Usage Type:** Persistence -**Classification:** File I/O -**Notes:** -- Existing projects may have variables with or without IDs -- Schema validation allows both cases (ID is optional) -- No migration or ID generation on load - ---- - -## 9. Additional ID Usage Patterns - -### 9.1 Variable Search in Graphical Editors -**File:** `src/renderer/components/_atoms/graphical-editor/ladder/utils/utils.ts` -**Estimated lines:** Multiple locations (4 occurrences of `variable.id`) - -**Description:** Utility functions for finding variables in ladder diagrams. - -**Usage Type:** Lookup -**Classification:** Utility Function -**Notes:** -- Used for variable selection and highlighting -- Likely uses ID for quick lookups - -### 9.2 Autocomplete Variable Selection -**File:** `src/renderer/components/_atoms/graphical-editor/autocomplete/index.tsx` -**Estimated lines:** Multiple locations (2 occurrences of `variable.id`) - -**Description:** Autocomplete component uses IDs for variable selection. - -**Usage Type:** Lookup -**Classification:** UI Component -**Notes:** -- Dropdown selection likely uses ID as key -- Falls back to name if ID not present - -### 9.3 FBD Editor Usage -**Files:** -- `src/renderer/components/_atoms/graphical-editor/fbd/block.tsx` (1 occurrence) -- `src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx` (2 occurrences) -- `src/renderer/components/_atoms/graphical-editor/fbd/autocomplete/index.tsx` (3 occurrences) -- `src/renderer/components/_atoms/graphical-editor/fbd/utils/utils.ts` (4 occurrences) - -**Description:** FBD editor has similar ID usage patterns as Ladder editor. - -**Usage Type:** Linking, Lookup -**Classification:** Graphical Editor -**Notes:** -- Same patterns as Ladder: block-variable linking, autocomplete, synchronization -- Should be migrated in parallel with Ladder editor - -### 9.4 Variables Editor -**File:** `src/renderer/components/_organisms/variables-editor/index.tsx` -**Estimated lines:** 1 occurrence of `variable.id` - -**Description:** Variables table editor uses IDs for row identification. - -**Usage Type:** Lookup -**Classification:** UI Component -**Notes:** -- Likely uses ID as React key for table rows -- May need to switch to using row index or composite key - ---- - -## 10. Summary of ID Usage by Category - -### 10.1 By Usage Type - -| Usage Type | Count | Critical? | Notes | -|------------|-------|-----------|-------| -| Schema Definition | 2 | No | Optional field, no constraints | -| Creation/Generation | 8+ | No | Conditional, only if not present | -| Lookup | 15+ | **Yes** | Dual system with row index fallback | -| Linking | 10+ | **Yes** | Block-variable connections, divergence | -| Synchronization | 5+ | **Yes** | Variable table changes → diagram updates | -| Serialization | 0 | No | XML uses names, not IDs | -| Persistence | 2 | No | Pass-through, no special handling | - -### 10.2 By File Category - -| Category | File Count | ID Usage Intensity | -|----------|------------|-------------------| -| Type Definitions | 1 | Low (schema only) | -| State Management | 2 | High (CRUD operations) | -| Graphical Editors (Ladder) | 8+ | **Very High** (linking, sync) | -| Graphical Editors (FBD) | 5+ | **Very High** (linking, sync) | -| UI Components | 5+ | Medium (display, selection) | -| Utilities | 3+ | Medium (lookup helpers) | -| XML Generation | 1 | **None** (uses names) | -| Services | 1 | Low (pass-through) | - -### 10.3 Critical Migration Areas - -**High Priority (Core Functionality):** -1. **Block-variable linking** (`handleTableId` mechanism) -2. **Variable lookup utilities** (dual row/ID system) -3. **Divergence detection** (library update handling) -4. **Variable synchronization** (table ↔ diagram) - -**Medium Priority (User Experience):** -5. **Autocomplete** (variable selection) -6. **Variable renaming** (broken link creation) -7. **Variables table** (row identification) - -**Low Priority (Already Name-Based):** -8. **XML generation** (already uses names) -9. **Project persistence** (pass-through) - ---- - -## 11. Fallback Mechanisms Already in Place - -### 11.1 Row Index Priority -The `getVariableBasedOnRowIdOrVariableId` function prioritizes row index over ID, suggesting the system was designed to work primarily with positional references. - -### 11.2 Optional ID Field -The schema defines ID as optional, indicating the system was architected to potentially work without IDs. - -### 11.3 Name-Based Rename Detection -Variable rename detection uses name matching (case-insensitive), not ID matching, showing existing name-based infrastructure. - -### 11.4 XML Uses Names -The fact that XML generation uses names exclusively validates that name-based linking is architecturally sound for the compilation pipeline. - ---- - -## 12. Recommendations for Phase 2 - -Based on this audit, the following areas should be prioritized in Phase 2 (Implementation): - -1. **Replace `handleTableId` with name+type composite key** in block-variable linking -2. **Refactor `getVariableBasedOnRowIdOrVariableId`** to use name+type lookup -3. **Update divergence detection** to match variables by name+type instead of ID -4. **Modify synchronization logic** to use name+type comparison -5. **Update autocomplete** to use name+type for variable selection -6. **Revise broken link creation** to use name+type instead of synthetic IDs -7. **Add migration logic** for existing projects with IDs - ---- - -## Appendix A: Files with Variable ID Usage - -Complete list of files containing `variable.id` references (20 files total): - -1. `src/types/PLC/open-plc.ts` (schema definitions) -2. `src/renderer/store/slices/project/slice.ts` (CRUD operations) -3. `src/renderer/store/slices/project/utils/index.ts` (lookup utility) -4. `src/renderer/components/_atoms/graphical-editor/ladder/block.tsx` (linking) -5. `src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx` (synchronization) -6. `src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx` (linking) -7. `src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx` (linking) -8. `src/renderer/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx` (creation) -9. `src/renderer/components/_atoms/graphical-editor/ladder/utils/utils.ts` (utilities) -10. `src/renderer/components/_atoms/graphical-editor/fbd/block.tsx` (linking) -11. `src/renderer/components/_atoms/graphical-editor/fbd/variable.tsx` (synchronization) -12. `src/renderer/components/_atoms/graphical-editor/fbd/autocomplete/index.tsx` (creation) -13. `src/renderer/components/_atoms/graphical-editor/fbd/utils/utils.ts` (utilities) -14. `src/renderer/components/_atoms/graphical-editor/autocomplete/index.tsx` (UI) -15. `src/renderer/components/_molecules/variables-table/editable-cell.tsx` (renaming) -16. `src/renderer/components/_molecules/graphical-editor/ladder/rung/body.tsx` (diagram) -17. `src/renderer/components/_molecules/graphical-editor/fbd/index.tsx` (diagram) -18. `src/renderer/components/_organisms/variables-editor/index.tsx` (table) -19. `src/renderer/components/_organisms/workspace-activity-bar/ladder-toolbox.tsx` (UI) -20. `src/renderer/components/_organisms/modals/delete-confirmation-modal.tsx` (UI) - ---- - -## Appendix B: Files with UUID Generation - -Complete list of files containing `crypto.randomUUID()` or `uuidv4()` (12 files total): - -1. `src/renderer/store/slices/project/slice.ts` -2. `src/renderer/store/slices/fbd/utils/index.ts` -3. `src/renderer/store/slices/ladder/utils/index.ts` -4. `src/renderer/components/_atoms/graphical-editor/ladder/block.tsx` -5. `src/renderer/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx` -6. `src/renderer/components/_atoms/graphical-editor/fbd/block.tsx` -7. `src/renderer/components/_atoms/graphical-editor/fbd/autocomplete/index.tsx` -8. `src/renderer/components/_organisms/workspace-activity-bar/default.tsx` -9. `src/renderer/components/_features/[workspace]/editor/monaco/index.tsx` -10. `src/renderer/screens/workspace-screen.tsx` -11. `src/utils/python/addPythonLocalVariables.ts` -12. `src/utils/cpp/addCppLocalVariables.ts` - ---- - -**Document Version:** 1.0 -**Date:** 2025-10-22 -**Author:** Devin (AI Assistant) -**Status:** Complete diff --git a/docs/ports/accelerator-port.ts b/docs/ports/accelerator-port.ts deleted file mode 100644 index 650e25cfe..000000000 --- a/docs/ports/accelerator-port.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * AcceleratorPort — Abstracts keyboard shortcuts and application-level actions. - * - * Editor adapter: Registers IPC event listeners for Electron menu accelerators. - * Each accelerator fires an IPC event from the main process menu. - * Web adapter: Registers browser keyboard event listeners (e.g., Ctrl+S, Ctrl+Z). - * Shortcuts handled via useSaveShortcut, useUndoRedoShortcut hooks. - * - * This port provides a uniform way for the shared UI to subscribe to - * application-level actions without knowing the platform mechanism. - * - * ## Editor IPC methods replaced: - * - window.bridge.createProjectAccelerator() - * - window.bridge.handleOpenProjectRequest() - * - window.bridge.openRecentAccelerator() - * - window.bridge.saveProjectAccelerator() - * - window.bridge.saveFileAccelerator() - * - window.bridge.closeProjectAccelerator() - * - window.bridge.closeTabAccelerator() - * - window.bridge.deleteFileAccelerator() - * - window.bridge.exportProjectRequest() - * - window.bridge.findInProjectAccelerator() - * - window.bridge.handleUndoRequest() - * - window.bridge.handleRedoRequest() - * - window.bridge.switchPerspective() - * - window.bridge.aboutAccelerator() - * - window.bridge.aboutModalAccelerator() - * - All corresponding remove*Listener() methods - * - * ## Web equivalents: - * - useSaveShortcut hook (Ctrl+S) - * - useUndoRedoShortcut hook (Ctrl+Z, Ctrl+Shift+Z) - * - Browser keyboard event listeners - */ - -import type { Unsubscribe } from './types' - -export interface AcceleratorPort { - // --- Project actions --- - onCreateProject(callback: () => void): Unsubscribe - onOpenProject(callback: () => void): Unsubscribe - onOpenRecent(callback: () => void): Unsubscribe - onSaveProject(callback: () => void): Unsubscribe - onSaveFile(callback: () => void): Unsubscribe - onCloseProject(callback: () => void): Unsubscribe - onExportProject(callback: () => void): Unsubscribe - - // --- Editor actions --- - onCloseTab(callback: () => void): Unsubscribe - onDeleteFile(callback: () => void): Unsubscribe - onFindInProject(callback: () => void): Unsubscribe - onUndo(callback: () => void): Unsubscribe - onRedo(callback: () => void): Unsubscribe - - // --- View actions --- - onSwitchPerspective(callback: () => void): Unsubscribe - onAbout(callback: () => void): Unsubscribe -} diff --git a/docs/ports/compiler-port.ts b/docs/ports/compiler-port.ts deleted file mode 100644 index 09f1c816c..000000000 --- a/docs/ports/compiler-port.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * CompilerPort — Abstracts the PLC compilation pipeline. - * - * Editor adapter: Delegates to main process via IPC (local binaries: xml2st, iec2c, arduino-cli). - * Web adapter: Delegates to remote API at compile.getedge.me (callGenerateSt, callCompileSt, etc.). - * - * The UI only knows "compile this project" and receives progress events. - * The adapter decides whether to call local binaries or remote HTTP endpoints. - * - * ## Editor IPC methods replaced: - * - window.bridge.runCompileProgram() - * - window.bridge.runDebugCompilation() - * - window.bridge.exportProjectXml() - * - window.bridge.createBuildDirectory() - * - window.bridge.createXmlFileToBuild() - * - window.bridge.compileRequest() - * - window.bridge.generateCFilesRequest() - * - window.bridge.setupCompilerEnvironment() - * - * ## Web service methods replaced: - * - callGenerateSt() - * - callCompileSt() - * - callGenerateDebug() - * - callGenerateGlueVars() - * - callCompileArduino() - */ - -import type { - CompileProgressEvent, - CompileResult, - DebugCompileResult, - PLCProjectData, - Result, - Unsubscribe, -} from './types' - -export interface CompileProgramArgs { - projectData: PLCProjectData - boardTarget: string - projectPath: string - communicationPort?: string - compileOnly?: boolean -} - -export interface DebugCompileArgs { - projectData: PLCProjectData - boardTarget: string - projectPath: string -} - -export interface ExportXmlArgs { - projectData: PLCProjectData - projectPath: string - format: 'old-editor' | 'codesys' -} - -export interface CompilerPort { - /** - * Run the full compilation pipeline (XML -> ST -> C -> binary). - * Emits progress events for UI feedback. - */ - compileProgram( - args: CompileProgramArgs, - onProgress: (event: CompileProgressEvent) => void, - ): Promise - - /** - * Run a debug-mode compilation that produces debug symbols. - * Used before connecting the debugger to verify program integrity. - */ - compileForDebug( - args: DebugCompileArgs, - onProgress: (event: CompileProgressEvent) => void, - ): Promise - - /** - * Export the project as IEC 61131-3 XML. - * Editor: writes XML file to disk. - * Web: returns XML string (or triggers download). - */ - exportProjectXml(args: ExportXmlArgs): Promise> - - /** - * Subscribe to compilation output logs (stdout/stderr streaming). - * Returns unsubscribe function. - */ - onCompileOutput?(callback: (line: string) => void): Unsubscribe -} diff --git a/docs/ports/debugger-port.ts b/docs/ports/debugger-port.ts deleted file mode 100644 index cccf85eae..000000000 --- a/docs/ports/debugger-port.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * DebuggerPort — Abstracts the debug protocol for reading/writing PLC variables. - * - * Editor adapter: Routes through IPC to main process Modbus clients - * (TCP, RTU, WebSocket, or simulator virtual serial port). - * Web adapter: Routes through DebugBridge singleton which delegates to - * WebRTCTransport, ModbusRtuTransport (simulator), or HTTP fallback. - * - * The underlying protocol is always Modbus PDU with custom function codes - * (0x41-0x45) for debug operations. The port hides transport specifics. - * - * ## Editor IPC methods replaced: - * - window.bridge.debuggerConnect() - * - window.bridge.debuggerDisconnect() - * - window.bridge.debuggerGetVariablesList() - * - window.bridge.debuggerSetVariable() - * - window.bridge.debuggerVerifyMd5() - * - window.bridge.debuggerReadProgramStMd5() - * - window.bridge.readDebugFile() - * - * ## Web service methods replaced: - * - debugBridge.connect() - * - debugBridge.disconnect() - * - debugBridge.getVariablesList() - * - debugBridge.setVariable() - * - debugBridge.verifyMd5() - * - debugBridge.onStopped() - * - DebugTransport interface implementations - */ - -import type { - DebugSetResult, - DebugVariableResult, - Md5VerifyResult, - Unsubscribe, -} from './types' - -export interface DebuggerPort { - /** - * Connect to a debug target. - * The adapter resolves the actual transport (TCP, RTU, WebSocket, WebRTC, simulator) - * based on the current device configuration and platform capabilities. - */ - connect(): Promise<{ success: boolean; error?: string }> - - /** Disconnect from the current debug target. */ - disconnect(): Promise<{ success: boolean }> - - /** - * Read variable values by their debug indexes (batched). - * Returns tick count, last index processed, and raw data array. - * `needsReconnect` signals the UI to re-establish the connection. - */ - getVariablesList(indexes: number[]): Promise - - /** - * Write or force a variable value. - * @param index — Variable debug index - * @param force — If true, the value is forced (overrides PLC logic) - * @param valueBuffer — Raw value bytes (Uint8Array) - */ - setVariable(index: number, force: boolean, valueBuffer?: Uint8Array): Promise - - /** - * Verify that the running program matches the expected MD5 hash. - * Used to detect program mismatch before starting a debug session. - */ - verifyMd5(expectedMd5: string): Promise - - /** - * Read the MD5 hash of the compiled ST program from the debug artifacts. - * Editor: reads from disk via IPC. - * Web: reads from compiler API response or cached artifacts. - */ - readProgramMd5(projectPath: string, boardTarget: string): Promise<{ success: boolean; md5?: string; error?: string }> - - /** - * Read the debug file content (variable mapping produced by compiler). - * Editor: reads .dbg file from disk. - * Web: reads from compiler API response. - */ - readDebugFile(projectPath: string, boardTarget: string): Promise<{ success: boolean; content?: string; error?: string }> - - /** - * Subscribe to debugger disconnection events (e.g., simulator stopped, connection lost). - * Returns unsubscribe function. - */ - onDisconnected(callback: () => void): Unsubscribe - - /** Check if the debugger is currently connected. */ - isConnected(): boolean -} diff --git a/docs/ports/device-port.ts b/docs/ports/device-port.ts deleted file mode 100644 index 4ceb9e0b6..000000000 --- a/docs/ports/device-port.ts +++ /dev/null @@ -1,61 +0,0 @@ -/** - * DevicePort — Abstracts hardware/board discovery and configuration. - * - * Editor adapter: Delegates to main process which reads HAL JSON files from - * resources/bin/ and queries arduino-cli for installed cores. - * Board preview images loaded from local filesystem. - * Web adapter: Reads board definitions from bundled hals.json asset. - * Orchestrator/agent list fetched from backend API. - * Board preview images loaded from bundled assets. - * - * ## Editor IPC methods replaced: - * - window.bridge.getAvailableBoards() - * - window.bridge.getAvailableCommunicationPorts() - * - window.bridge.refreshAvailableBoards() - * - window.bridge.refreshCommunicationPorts() - * - window.bridge.getPreviewImage() - * - * ## Web service methods replaced: - * - Board data from bundled hals.json - * - Orchestrator list from orchestrator API - * - getDeviceStatus() - */ - -import type { BoardInfo, CommunicationPort } from './types' - -export interface DevicePort { - /** - * Get all available boards with their hardware specs and pin configurations. - * Editor: reads from local HAL files + arduino-cli board list. - * Web: reads from bundled hals.json + orchestrator device list. - */ - getAvailableBoards(): Promise> - - /** - * Get available serial/communication ports. - * Editor: queries local system serial ports. - * Web: may not be applicable locally (ports come from remote device). - */ - getCommunicationPorts(): Promise - - /** - * Refresh the board list (e.g., after installing a new arduino core). - * Editor: re-scans arduino-cli and HAL files. - * Web: re-fetches from backend/orchestrator. - */ - refreshBoards(): Promise> - - /** - * Refresh communication ports list. - * Editor: re-scans local system serial ports. - * Web: re-queries orchestrator for available ports. - */ - refreshCommunicationPorts(): Promise - - /** - * Get a board preview image for display in the UI. - * Editor: loads image from local resources/ directory, returns base64 or file path. - * Web: returns URL to bundled image asset. - */ - getPreviewImage(imageName: string): Promise -} diff --git a/docs/ports/index.ts b/docs/ports/index.ts deleted file mode 100644 index d8089765a..000000000 --- a/docs/ports/index.ts +++ /dev/null @@ -1,129 +0,0 @@ -/** - * Port Interfaces — Contracts that decouple the shared UI from platform-specific implementations. - * - * Architecture: - * - * Shared UI (React components, hooks, store) - * │ - * │ depends on - * ▼ - * Port Interfaces (this package) - * │ - * │ implemented by - * ▼ - * ┌─────────────────┬──────────────────┐ - * │ Editor Adapters │ Web Adapters │ - * │ (Electron IPC) │ (HTTP/WebRTC) │ - * └─────────────────┴──────────────────┘ - * - * Usage: - * The PlatformProvider React context (see platform-provider.ts) supplies - * concrete port implementations to the component tree. Components access - * ports via usePlatform() hook: - * - * const { compiler, runtime, debugger } = usePlatform() - * await compiler.compileProgram(args, onProgress) - * - * 10 Port Interfaces: - * 1. CompilerPort — PLC compilation pipeline - * 2. RuntimePort — Remote PLC runtime communication - * 3. DebuggerPort — Debug protocol (variable read/write) - * 4. SimulatorPort — Built-in AVR simulator - * 5. ProjectPort — Project lifecycle (create, open, save, POU management) - * 6. DevicePort — Board/hardware discovery - * 7. SystemPort — Platform services (store, links, logging) - * 8. WindowPort — Native window management - * 9. AcceleratorPort — Keyboard shortcuts - * 10. ThemePort — Theme detection and switching - * - * Plus: - * - PlatformCapabilities — Feature toggle flags - * - Shared domain types — PLCVariable, BoardInfo, TimingStats, etc. - */ - -// --- Port interfaces --- -export type { CompilerPort } from './compiler-port' -export type { RuntimePort } from './runtime-port' -export type { DebuggerPort } from './debugger-port' -export type { SimulatorPort } from './simulator-port' -export type { ProjectPort } from './project-port' -export type { DevicePort } from './device-port' -export type { SystemPort } from './system-port' -export type { WindowPort } from './window-port' -export type { AcceleratorPort } from './accelerator-port' -export type { ThemePort } from './theme-port' - -// --- Feature toggles --- -export type { PlatformCapabilities } from './platform-capabilities' -export { EDITOR_CAPABILITIES, WEB_CAPABILITIES } from './platform-capabilities' - -// --- Shared domain types --- -export type { - // Result wrappers - Result, - Unsubscribe, - // PLC - PLCLanguage, - PLCExtendedLanguage, - PouType, - VariableClass, - PLCVariable, - PLCVariableType, - PLCTask, - PLCInstance, - PLCDataType, - PLCBody, - PLCPou, - PLCProjectData, - ProjectMeta, - // Device - CompilerType, - BoardInfo, - CommunicationPort, - SerialPort, - PinType, - DevicePin, - DeviceConfiguration, - ModbusRTUConfig, - ModbusTCPConfig, - // Runtime - PlcStatus, - TimingStats, - RuntimeLogLevel, - RuntimeLogEntry, - // Debugger - DebugVariableResult, - DebugSetResult, - Md5VerifyResult, - // Compiler - CompileProgressEvent, - CompileResult, - DebugCompileResult, - // Console - LogObject, - // System - Platform, - Architecture, - SystemInfo, - RecentProject, -} from './types' - -// --- Port parameter/result types --- -export type { CompileProgramArgs, DebugCompileArgs, ExportXmlArgs } from './compiler-port' -export type { - LoginParams, - LoginResult, - CreateUserParams, - UsersInfoResult, - RuntimeStatusResult, - CompilationStatusResult, - RuntimeLogsResult, -} from './runtime-port' -export type { - CreateProjectParams, - ProjectResponse, - SaveProjectParams, - CreatePouParams, - RenamePouParams, -} from './project-port' -export type { ThemeVariant } from './theme-port' diff --git a/docs/ports/platform-capabilities.ts b/docs/ports/platform-capabilities.ts deleted file mode 100644 index 46eba23a0..000000000 --- a/docs/ports/platform-capabilities.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * PlatformCapabilities — Feature toggles for platform-specific UI behavior. - * - * The shared UI uses these flags to conditionally render or enable features - * that differ between the Electron editor and the web application. - * This replaces branching on platform detection and keeps the UI code clean. - * - * Each adapter provides its own PlatformCapabilities instance: - * - Editor: native window controls, local filesystem, no auth, etc. - * - Web: browser-based, cloud auth, WebRTC, orchestrator management, etc. - */ - -export interface PlatformCapabilities { - // --- Window & Chrome --- - - /** True if the app has native window controls (minimize, maximize, close buttons in title bar). */ - hasNativeWindowControls: boolean - - /** True if the app has a native application menu (Electron menu bar). */ - hasNativeMenu: boolean - - /** True if the app supports native file dialogs (open, save, pick directory). */ - hasNativeFileDialogs: boolean - - // --- Authentication --- - - /** True if the app requires user authentication to access the workspace. */ - hasAuthentication: boolean - - // --- Device & Hardware --- - - /** True if the app can detect local serial/communication ports. */ - hasLocalSerialPorts: boolean - - /** True if the app supports orchestrator-managed devices (cloud device fleet). */ - hasOrchestratorDevices: boolean - - /** True if the app supports WebRTC connections to runtime devices. */ - hasWebRTC: boolean - - // --- Simulator --- - - /** True if the simulator runs in the same process (web: in-browser, editor: main process). */ - hasInProcessSimulator: boolean - - // --- Project Management --- - - /** True if the app supports a local filesystem project structure (directories, files). */ - hasLocalFilesystem: boolean - - /** True if the app supports exporting projects as XML files (Codesys, old-editor formats). */ - hasProjectExport: boolean - - /** True if the app supports the "About" dialog. */ - hasAboutDialog: boolean - - // --- Editor Features --- - - /** True if the app has a Python LSP (language server protocol) for code completion. */ - hasPythonLSP: boolean - - /** True if the app supports undo/redo history tracking. */ - hasUndoRedoHistory: boolean - - /** True if the app can watch files for external changes. */ - hasFileWatcher: boolean - - // --- AI Features --- - - /** True if the app has AI-assisted coding (inline completions, chat panel, telemetry). */ - hasAIAssistant: boolean - - // --- Runtime Connection --- - - /** True if the runtime connection goes through an orchestrator/agent proxy. */ - hasProxiedRuntimeConnection: boolean - - /** - * True if the app can upload compiled programs directly to the runtime. - * Web: uploads zip via API. Editor: runtime compiles from uploaded source. - */ - hasDirectProgramUpload: boolean -} - -// --------------------------------------------------------------------------- -// Default capability profiles -// --------------------------------------------------------------------------- - -export const EDITOR_CAPABILITIES: PlatformCapabilities = { - hasNativeWindowControls: true, - hasNativeMenu: true, - hasNativeFileDialogs: true, - hasAuthentication: false, - hasLocalSerialPorts: true, - hasOrchestratorDevices: false, - hasWebRTC: false, - hasInProcessSimulator: true, - hasLocalFilesystem: true, - hasProjectExport: true, - hasAboutDialog: true, - hasPythonLSP: true, - hasUndoRedoHistory: true, - hasFileWatcher: true, - hasAIAssistant: false, - hasProxiedRuntimeConnection: false, - hasDirectProgramUpload: false, -} - -export const WEB_CAPABILITIES: PlatformCapabilities = { - hasNativeWindowControls: false, - hasNativeMenu: false, - hasNativeFileDialogs: false, - hasAuthentication: true, - hasLocalSerialPorts: false, - hasOrchestratorDevices: true, - hasWebRTC: true, - hasInProcessSimulator: true, - hasLocalFilesystem: false, - hasProjectExport: false, - hasAboutDialog: false, - hasPythonLSP: false, - hasUndoRedoHistory: false, - hasFileWatcher: false, - hasAIAssistant: true, - hasProxiedRuntimeConnection: true, - hasDirectProgramUpload: true, -} diff --git a/docs/ports/project-port.ts b/docs/ports/project-port.ts deleted file mode 100644 index 64585857c..000000000 --- a/docs/ports/project-port.ts +++ /dev/null @@ -1,152 +0,0 @@ -/** - * ProjectPort — Abstracts project lifecycle operations (create, open, save, POU management). - * - * Editor adapter: Delegates to main process project-service and pou-service via IPC. - * Files stored on local filesystem. Recent projects tracked in electron-store. - * Web adapter: Delegates to project-api.ts and in-memory state. - * Projects stored on backend API. Recent projects tracked in cookies/localStorage. - * - * ## Editor IPC methods replaced: - * - window.bridge.createProject() - * - window.bridge.openProject() - * - window.bridge.openProjectByPath() - * - window.bridge.saveProject() - * - window.bridge.saveFile() - * - window.bridge.createPouFile() - * - window.bridge.deletePouFile() - * - window.bridge.renamePouFile() - * - window.bridge.pathPicker() - * - window.bridge.retrieveRecent() - * - window.bridge.getRecent() - * - window.bridge.fileWatchStart() - * - window.bridge.fileWatchStop() - * - window.bridge.fileWatchStopAll() - * - window.bridge.fileReadContent() - * - window.bridge.onFileExternalChange() - * - * ## Web service methods replaced: - * - fetchProjectFromApi() - * - Project state in Zustand store - */ - -import type { - DeviceConfiguration, - DevicePin, - PLCProjectData, - ProjectMeta, - RecentProject, - Unsubscribe, -} from './types' - -export interface CreateProjectParams { - name: string - type: 'plc-project' | 'plc-library' - path?: string -} - -export interface ProjectResponse { - success: boolean - data?: { - meta: ProjectMeta - projectData: PLCProjectData - deviceConfiguration?: DeviceConfiguration - devicePinMapping?: DevicePin[] - } - error?: { - title: string - description: string - } -} - -export interface SaveProjectParams { - projectPath: string - projectData: PLCProjectData - deviceConfiguration: DeviceConfiguration - devicePinMapping: DevicePin[] -} - -export interface CreatePouParams { - name: string - pouType: PouType - language: string - filePath?: string -} - -export interface RenamePouParams { - filePath: string - newFileName: string - fileContent?: unknown -} - -import type { PouType } from './types' - -export interface ProjectPort { - /** Create a new project. */ - createProject(params: CreateProjectParams): Promise - - /** - * Open a project via platform file picker. - * Editor: shows native file dialog. - * Web: may show file input or project list. - */ - openProject(): Promise - - /** Open a project by its path or identifier. */ - openProjectByPath(projectPath: string): Promise - - /** Save the entire project (all files, configuration, pin mapping). */ - saveProject(params: SaveProjectParams): Promise<{ success: boolean; error?: string }> - - /** - * Save a single file within the project. - * Editor: writes to disk. - * Web: updates in-memory state and/or syncs to backend. - */ - saveFile(filePath: string, content: unknown): Promise<{ success: boolean; error?: string }> - - /** Create a new POU file. */ - createPou(params: CreatePouParams): Promise<{ success: boolean; data?: unknown; error?: string }> - - /** Delete a POU file. */ - deletePou(filePath: string): Promise<{ success: boolean; error?: string }> - - /** Rename a POU file. */ - renamePou(params: RenamePouParams): Promise<{ success: boolean; data?: unknown; error?: string }> - - /** - * Pick a filesystem path (for project location). - * Editor: shows native directory picker dialog. - * Web: may not be applicable (returns pre-configured path). - */ - pickPath(): Promise<{ success: boolean; path?: string; error?: { title: string; description: string } }> - - /** Get list of recently opened projects. */ - getRecentProjects(): Promise - - /** - * Read a file's content by path. - * Editor: reads from local filesystem via IPC. - * Web: reads from in-memory project state or API. - */ - readFileContent(filePath: string): Promise<{ success: boolean; content?: string; error?: string }> - - /** - * Start watching a file for external changes. - * Editor: uses fs.watch via IPC. - * Web: no-op (files don't change externally in browser). - */ - watchFile?(filePath: string): Promise<{ success: boolean; error?: string }> - - /** Stop watching a file. */ - unwatchFile?(filePath: string): Promise<{ success: boolean }> - - /** Stop watching all files. */ - unwatchAll?(): Promise<{ success: boolean }> - - /** - * Subscribe to external file change events. - * Editor: fires when file changes on disk outside the editor. - * Web: not applicable (never fires). - */ - onFileExternalChange?(callback: (filePath: string) => void): Unsubscribe -} diff --git a/docs/ports/runtime-port.ts b/docs/ports/runtime-port.ts deleted file mode 100644 index e6ab3f6f0..000000000 --- a/docs/ports/runtime-port.ts +++ /dev/null @@ -1,136 +0,0 @@ -/** - * RuntimePort — Abstracts communication with a remote OpenPLC runtime device. - * - * Editor adapter: Proxies HTTP calls through main process IPC - * (window.bridge.runtimeLogin, runtimeGetStatus, etc.). - * Web adapter: Calls runtime-api.ts which routes through orchestrator/agent proxy, - * with WebRTC-first strategy and HTTP fallback. - * - * Connection details (IP address, JWT token, agentId, deviceId) are managed - * internally by each adapter. The UI only passes logical identifiers - * when needed, and the adapter resolves them to transport-specific params. - * - * ## Editor IPC methods replaced: - * - window.bridge.runtimeLogin() - * - window.bridge.runtimeCreateUser() - * - window.bridge.runtimeGetUsersInfo() - * - window.bridge.runtimeGetStatus() - * - window.bridge.runtimeStartPlc() - * - window.bridge.runtimeStopPlc() - * - window.bridge.runtimeGetLogs() - * - window.bridge.runtimeGetSerialPorts() - * - window.bridge.runtimeGetCompilationStatus() - * - window.bridge.runtimeClearCredentials() - * - window.bridge.onRuntimeTokenRefreshed() - * - * ## Web service methods replaced: - * - runtimeLogin() - * - runtimeCreateUser() - * - runtimeGetUsersInfo() - * - runtimeGetStatus() - * - runtimeStartPlc() - * - runtimeStopPlc() - * - runtimeGetLogs() - * - runtimeGetSerialPorts() - * - runtimeGetCompilationStatus() - * - runtimeUploadProgram() - * - runtimeLogout() - */ - -import type { - PlcStatus, - RuntimeLogEntry, - SerialPort, - TimingStats, - Unsubscribe, -} from './types' - -export interface LoginParams { - username: string - password: string -} - -export interface LoginResult { - success: boolean - accessToken?: string - error?: string -} - -export interface CreateUserParams { - username: string - password: string -} - -export interface UsersInfoResult { - hasUsers: boolean - runtimeVersion?: string - error?: string -} - -export interface RuntimeStatusResult { - success: boolean - status?: PlcStatus | string - timingStats?: TimingStats - error?: string -} - -export interface CompilationStatusResult { - success: boolean - data?: { - status: string - logs: string[] - exit_code: number | null - } - error?: string -} - -export interface RuntimeLogsResult { - success: boolean - logs?: string | RuntimeLogEntry[] - error?: string -} - -export interface RuntimePort { - /** Authenticate with the runtime. Returns JWT on success. */ - login(params: LoginParams): Promise - - /** Create a new user on the runtime (first-time setup). */ - createUser(params: CreateUserParams): Promise<{ success: boolean; error?: string }> - - /** Check if the runtime has users and get its version. */ - getUsersInfo(): Promise - - /** Get current PLC runtime status with optional timing statistics. */ - getStatus(includeStats?: boolean): Promise - - /** Start the PLC program on the runtime. */ - startPlc(): Promise<{ success: boolean; error?: string }> - - /** Stop the PLC program on the runtime. */ - stopPlc(): Promise<{ success: boolean; error?: string }> - - /** Get runtime logs, optionally filtered by minimum log ID. */ - getLogs(minId?: number): Promise - - /** Get serial ports available on the runtime device. */ - getSerialPorts(): Promise<{ success: boolean; ports?: SerialPort[]; error?: string }> - - /** Get the status of an ongoing compilation on the runtime. */ - getCompilationStatus(): Promise - - /** Clear stored credentials (logout). */ - clearCredentials(): Promise<{ success: boolean }> - - /** - * Upload a compiled program to the runtime. - * Web adapter: sends zip as base64. - * Editor adapter: sends via file path or streamed content. - */ - uploadProgram?(programData: string | ArrayBuffer): Promise<{ success: boolean; error?: string }> - - /** - * Subscribe to token refresh events (e.g., JWT auto-renewal). - * Returns unsubscribe function. - */ - onTokenRefreshed?(callback: (newToken: string) => void): Unsubscribe -} diff --git a/docs/ports/simulator-port.ts b/docs/ports/simulator-port.ts deleted file mode 100644 index 9ab166aed..000000000 --- a/docs/ports/simulator-port.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * SimulatorPort — Abstracts the built-in AVR simulator (ATmega2560 via avr8js). - * - * Editor adapter: Delegates to main process SimulatorModule via IPC. - * Firmware loaded from disk path. USART0 bridged to ModbusRtuClient. - * Web adapter: Delegates to in-browser SimulatorService singleton. - * Firmware loaded from hex string content (from compiler API). - * Uses browser-compatible Uint8Array-based Modbus RTU client. - * - * Both adapters share the same underlying avr8js@0.20.0 emulator, but differ - * in how firmware is loaded and how the virtual serial port is bridged. - * - * ## Editor IPC methods replaced: - * - window.bridge.simulatorLoadFirmware(hexPath) - * - window.bridge.simulatorStop() - * - window.bridge.simulatorIsRunning() - * - window.bridge.onSimulatorStopped() - * - * ## Web service methods replaced: - * - simulatorService.loadAndRun(hexContent) - * - simulatorService.stop() - * - simulatorService.isRunning() - * - simulatorService.onStopped() - * - simulatorService.connectDebugger() - * - simulatorService.disconnectDebugger() - */ - -import type { Unsubscribe } from './types' - -export interface SimulatorPort { - /** - * Load firmware and start the simulator. - * Editor: receives a file path to the .hex file on disk. - * Web: receives the hex content as a string. - * The adapter handles the difference. - */ - loadFirmware(hexPathOrContent: string): Promise<{ success: boolean; error?: string }> - - /** Stop the simulator. */ - stop(): Promise<{ success: boolean }> - - /** Check if the simulator is currently running. */ - isRunning(): boolean - - /** - * Subscribe to simulator stop events (crash, user stop, or program exit). - * Returns unsubscribe function. - */ - onStopped(callback: () => void): Unsubscribe - - /** - * Connect the debugger to the running simulator. - * Web adapter: initializes virtual serial port and Modbus RTU client. - * Editor adapter: already connected via main process; this is a no-op or - * triggers debuggerConnect with 'simulator' type. - */ - connectDebugger(): Promise - - /** - * Disconnect the debugger from the simulator. - * Cleans up virtual serial port and Modbus client state. - */ - disconnectDebugger(): void -} diff --git a/docs/ports/system-port.ts b/docs/ports/system-port.ts deleted file mode 100644 index d1eeae688..000000000 --- a/docs/ports/system-port.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * SystemPort — Abstracts application-level platform services. - * - * Editor adapter: Delegates to Electron APIs (electron-store, shell, app). - * Web adapter: Uses browser APIs (localStorage, window.open, navigator). - * - * ## Editor IPC methods replaced: - * - window.bridge.getSystemInfo() - * - window.bridge.getStoreValue() - * - window.bridge.setStoreValue() - * - window.bridge.openExternalLinkAccelerator() - * - window.bridge.log() - * - * ## Web equivalents: - * - navigator.userAgent / window.matchMedia for system info - * - localStorage for persistent key-value storage - * - window.open() for external links - * - console.log/error for logging - */ - -import type { SystemInfo } from './types' - -export interface SystemPort { - /** Get platform info (OS, architecture, dark mode preference, window state). */ - getSystemInfo(): Promise - - /** - * Get a persistent key-value store value. - * Editor: reads from electron-store. - * Web: reads from localStorage. - */ - getStoreValue(key: string): Promise - - /** - * Set a persistent key-value store value. - * Editor: writes to electron-store. - * Web: writes to localStorage. - */ - setStoreValue(key: string, value: string): void - - /** - * Open an external URL in the default browser. - * Editor: uses shell.openExternal(). - * Web: uses window.open(). - */ - openExternalLink(url: string): Promise<{ success: boolean }> - - /** - * Log a message (for diagnostics, not UI console). - * Editor: logs via main process logger. - * Web: logs via console or remote logging service. - */ - log(level: 'info' | 'error', message: string): void -} diff --git a/docs/ports/theme-port.ts b/docs/ports/theme-port.ts deleted file mode 100644 index f41361248..000000000 --- a/docs/ports/theme-port.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * ThemePort — Abstracts theme detection and switching. - * - * Editor adapter: Listens for IPC theme-update events from main process. - * Main process detects OS theme changes via nativeTheme API. - * Web adapter: Uses window.matchMedia('(prefers-color-scheme: dark)') listener. - * May also read theme preference from localStorage. - * - * ## Editor IPC methods replaced: - * - window.bridge.handleUpdateTheme() - * - window.bridge.winHandleUpdateTheme() - * - * ## Web equivalents: - * - matchMedia listener - * - localStorage theme preference - * - theme.ts utility - */ - -import type { Unsubscribe } from './types' - -export type ThemeVariant = 'light' | 'dark' - -export interface ThemePort { - /** Get the current active theme. */ - getCurrentTheme(): ThemeVariant - - /** Set the theme explicitly (persists the preference). */ - setTheme(theme: ThemeVariant): void - - /** Toggle between light and dark themes. */ - toggleTheme(): void - - /** - * Subscribe to theme change events (OS-level or user-initiated). - * Returns unsubscribe function. - */ - onThemeChanged(callback: (theme: ThemeVariant) => void): Unsubscribe -} diff --git a/docs/ports/types.ts b/docs/ports/types.ts deleted file mode 100644 index 97f817d43..000000000 --- a/docs/ports/types.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * Shared domain types used by port interfaces. - * - * These types are platform-agnostic and represent the domain concepts - * shared between openplc-editor (Electron) and openplc-web (Browser). - * Inner layers (domain/application) depend on these; outer layers (adapters) - * implement the ports that use them. - */ - -// --------------------------------------------------------------------------- -// Result wrappers -// --------------------------------------------------------------------------- - -/** Standard result for operations that can fail */ -export type Result = { success: true } & T | { success: false; error: string } - -/** Unsubscribe function returned by event subscriptions */ -export type Unsubscribe = () => void - -// --------------------------------------------------------------------------- -// PLC Languages & POU -// --------------------------------------------------------------------------- - -export type PLCLanguage = 'IL' | 'ST' | 'LD' | 'FBD' | 'SFC' - -/** Extended languages supported by function blocks */ -export type PLCExtendedLanguage = PLCLanguage | 'python' | 'cpp' - -export type PouType = 'program' | 'function' | 'function-block' - -export type VariableClass = 'input' | 'output' | 'inOut' | 'external' | 'local' | 'temp' | 'global' - -export type VariableTypeDefinition = 'base-type' | 'user-data-type' | 'array' | 'derived' - -export interface PLCVariableType { - definition: VariableTypeDefinition - value: string - data?: { - baseType: string - dimensions: Array<{ dimension: string }> - } -} - -export interface PLCVariable { - name: string - class?: VariableClass - type: PLCVariableType - location: string - initialValue?: string | null - documentation: string - debug?: boolean -} - -export interface PLCTask { - name: string - triggering: 'Cyclic' | 'Interrupt' - interval: string - priority: number -} - -export interface PLCInstance { - name: string - taskName: string - pouName: string -} - -export type PLCDataType = - | { name: string; derivation: 'structure'; variable: PLCVariable[] } - | { - name: string - derivation: 'enumerated' - initialValue?: string - values: Array<{ description: string }> - } - | { - name: string - derivation: 'array' - baseType: string - initialValue?: string - dimensions: Array<{ dimension: string }> - } - -export interface PLCBody { - language: PLCExtendedLanguage | 'il' | 'st' | 'ld' | 'fbd' | 'sfc' | 'python' | 'cpp' - value: unknown -} - -export interface PLCPou { - name: string - pouType: PouType - interface?: { - returnType?: string - variables: PLCVariable[] - } - body: PLCBody - documentation?: string -} - -// --------------------------------------------------------------------------- -// Project -// --------------------------------------------------------------------------- - -export interface PLCProjectData { - dataTypes: PLCDataType[] - pous: PLCPou[] - configurations: { - resource: { - tasks: PLCTask[] - instances: PLCInstance[] - globalVariables: PLCVariable[] - } - } -} - -export interface ProjectMeta { - name: string - type: 'plc-project' | 'plc-library' - path: string -} - -// --------------------------------------------------------------------------- -// Device & Board -// --------------------------------------------------------------------------- - -export type CompilerType = 'arduino-cli' | 'openplc-compiler' | 'simulator' - -export interface BoardInfo { - compiler: CompilerType | string - core: string - preview: string - specs: Record - coreVersion?: string - pins?: { - defaultAin?: string[] - defaultAout?: string[] - defaultDin?: string[] - defaultDout?: string[] - } -} - -export interface CommunicationPort { - name: string - address: string -} - -export interface SerialPort { - device: string - description?: string -} - -export type PinType = 'digitalInput' | 'digitalOutput' | 'analogInput' | 'analogOutput' - -export interface DevicePin { - pin: string - pinType: PinType - address: string - name?: string -} - -export interface ModbusRTUConfig { - rtuInterface: string - rtuBaudRate: string - rtuSlaveId: number | null - rtuRS485ENPin: string | null -} - -export interface ModbusTCPConfig { - tcpInterface: string - tcpMacAddress: string | null - tcpWifiSSID?: string | null - tcpWifiPassword?: string | null - tcpStaticHostConfiguration: { - ipAddress: string - dns: string - gateway: string - subnet: string - } -} - -export interface DeviceConfiguration { - deviceBoard: string - communicationPort: string - runtimeIpAddress?: string - compileOnly: boolean - communicationConfiguration: { - modbusRTU: ModbusRTUConfig - modbusTCP: ModbusTCPConfig - communicationPreferences: { - enabledRTU: boolean - enabledTCP: boolean - enabledDHCP: boolean - } - } -} - -// --------------------------------------------------------------------------- -// Runtime -// --------------------------------------------------------------------------- - -export type PlcStatus = 'INIT' | 'RUNNING' | 'STOPPED' | 'ERROR' | 'EMPTY' | 'UNKNOWN' - -export interface TimingStats { - scan_count: number - scan_time_min: number | null - scan_time_max: number | null - scan_time_avg: number | null - cycle_time_min: number | null - cycle_time_max: number | null - cycle_time_avg: number | null - cycle_latency_min: number | null - cycle_latency_max: number | null - cycle_latency_avg: number | null - overruns: number -} - -export type RuntimeLogLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR' - -export interface RuntimeLogEntry { - id: number | null - timestamp: string - level: RuntimeLogLevel - message: string -} - -// --------------------------------------------------------------------------- -// Debugger -// --------------------------------------------------------------------------- - -export interface DebugVariableResult { - success: boolean - tick?: number - lastIndex?: number - data?: number[] - error?: string - needsReconnect?: boolean -} - -export interface DebugSetResult { - success: boolean - error?: string -} - -export interface Md5VerifyResult { - success: boolean - match?: boolean - targetMd5?: string - error?: string -} - -// --------------------------------------------------------------------------- -// Compiler -// --------------------------------------------------------------------------- - -export interface CompileProgressEvent { - stage: 'xml' | 'st' | 'c' | 'glue' | 'arduino' | 'done' | 'error' - message: string - progress?: number -} - -export interface CompileResult { - success: boolean - message?: string - hexPath?: string - error?: string -} - -export interface DebugCompileResult { - success: boolean - debugContent?: string - md5?: string - error?: string -} - -// --------------------------------------------------------------------------- -// Console -// --------------------------------------------------------------------------- - -export interface LogObject { - id: string - level?: 'debug' | 'info' | 'warning' | 'error' - message: string - tstamp?: Date -} - -// --------------------------------------------------------------------------- -// System -// --------------------------------------------------------------------------- - -export type Platform = 'linux' | 'darwin' | 'win32' | '' - -export type Architecture = 'x64' | 'arm' | '' - -export interface SystemInfo { - OS: Platform - architecture: Architecture - prefersDarkMode: boolean - isWindowMaximized: boolean -} - -export interface RecentProject { - name: string - path: string - lastOpenedAt: string - createdAt: string -} diff --git a/docs/ports/window-port.ts b/docs/ports/window-port.ts deleted file mode 100644 index c5c47ecc0..000000000 --- a/docs/ports/window-port.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * WindowPort — Abstracts native window management. - * - * Editor adapter: Delegates to Electron BrowserWindow APIs via IPC. - * Web adapter: No-op implementation (browser tabs managed by the browser itself). - * Some methods may map to browser equivalents where applicable. - * - * ## Editor IPC methods replaced: - * - window.bridge.minimizeWindow() - * - window.bridge.maximizeWindow() - * - window.bridge.closeWindow() - * - window.bridge.hideWindow() - * - window.bridge.reloadWindow() - * - window.bridge.handleCloseOrHideWindow() - * - window.bridge.handleQuitApp() - * - window.bridge.rebuildMenu() - * - window.bridge.isMaximizedWindow() - * - window.bridge.windowIsClosing() - * - window.bridge.darwinAppIsClosing() - * - * ## Web equivalents: - * - Most methods are no-ops in the browser - * - reload -> window.location.reload() - * - close -> handled by browser tab close (beforeunload event) - */ - -import type { Unsubscribe } from './types' - -export interface WindowPort { - /** Minimize the application window. No-op on web. */ - minimize(): void - - /** Maximize/restore the application window. No-op on web. */ - maximize(): void - - /** Close the application window. Web: triggers beforeunload. */ - close(): void - - /** Hide the application window (minimize to tray). No-op on web. */ - hide(): void - - /** Reload the application. Web: window.location.reload(). */ - reload(): void - - /** Quit the application entirely. No-op on web. */ - quit(): void - - /** Rebuild the native application menu. No-op on web. */ - rebuildMenu(): void - - /** - * Subscribe to window close/quit requests. - * Editor: fires when user clicks close button or uses Cmd+Q. - * Web: fires on beforeunload event. - */ - onCloseRequested(callback: () => void): Unsubscribe - - /** - * Subscribe to window maximize/restore toggle events. - * Editor: fires when window state changes. - * Web: no-op (never fires). - */ - onMaximizedChanged?(callback: (isMaximized: boolean) => void): Unsubscribe -} diff --git a/docs/strucpp-migration/00-overview.md b/docs/strucpp-migration/00-overview.md index ba6c6b1bb..5004d73ed 100644 --- a/docs/strucpp-migration/00-overview.md +++ b/docs/strucpp-migration/00-overview.md @@ -1,8 +1,18 @@ # STruC++ Migration: Overview +> **Status.** STruC++ is the editor's ST compiler today: it is pinned in +> `binary-versions.json` (v0.5.x) and installed as an npm tarball by +> `scripts/download-binaries.ts`; `compiler-module.ts` compiles ST to C++ with it. +> The editor no longer ships or invokes a local `iec2c` binary; remaining +> `iec2c`/`matiec` mentions in `src/` are comments. MatIEC itself is not gone: +> on the Runtime v3 path the editor uploads plain `program.st` and the runtime +> recompiles it on-device with MatIEC +> (`src/backend/shared/compile/pipeline.ts`). The OPC UA plugin migration +> (Phase 9) remains paused. + ## Problem Statement -The OpenPLC Editor uses MatIEC (`iec2c`) to compile IEC 61131-3 Structured Text into C code, +The OpenPLC Editor used MatIEC (`iec2c`) to compile IEC 61131-3 Structured Text into C code, and `xml2st` to generate debug infrastructure. This pipeline has two critical issues: 1. **MatIEC is unmaintained**: The compiler is old, barely maintained, and has poor scalability. @@ -14,7 +24,8 @@ and `xml2st` to generate debug infrastructure. This pipeline has two critical is ## Solution Replace `iec2c` (MatIEC) with **STruC++**, a modern IEC 61131-3 Structured Text to C++17 compiler -written in TypeScript. STruC++ is located at `~/Documents/Code/strucpp`. +written in TypeScript. STruC++ lives in the `Autonomy-Logic/STruCpp` repository and is +distributed as an npm tarball pinned in `binary-versions.json`. STruC++ solves both problems: - It is actively developed with full CODESYS compatibility and OOP support diff --git a/docs/transpiler/block-output-ordering-830.md b/docs/transpiler/block-output-ordering-830.md new file mode 100644 index 000000000..442fb5b01 --- /dev/null +++ b/docs/transpiler/block-output-ordering-830.md @@ -0,0 +1,58 @@ +# Chained block output ordering in LD → ST (issue #830) + +Status: **resolved** on `fix/parity/parallel-coil-single-if` (folded into PR #891 / web #543). +Applied identically in both repos across the byte-identical +`src/backend/shared/transpilers/st-transpiler/` surface. + +## The bug + +When a rung chains function/FB calls, the generated ST emitted **all the block calls +first, then all the output assignments afterward**, so a later block read a variable whose +write had not been emitted yet. Reporter's case: + +```st +_TMP_MOD6363443_OUT := MOD(EN := TRUE, IN1 := Input, IN2 := 86400, ENO => _TMP_MOD6363443_ENO); +_TMP_DIV4651235_OUT := DIV(EN := _TMP_MOD6363443_ENO, IN1 := Output, IN2 := 60, ENO => _TMP_DIV4651235_ENO); -- reads Output before it is set +IF _TMP_MOD6363443_ENO THEN Output := _TMP_MOD6363443_OUT; END_IF; +IF _TMP_DIV4651235_ENO THEN Output := _TMP_DIV4651235_OUT; END_IF; +``` +`DIV` reads `Output` while it is still stale → `Output` is always 0. + +## Root cause + +Both the TS walker and the python oracle (`PLCGenerator.py:1339-1359`, `ComputeProgram`) emit +instances as *all execution-ordered (eo>0) instances first, then eo=0 instances by position*. +When the editor numbers the blocks but leaves their output variables at executionOrderId 0, +every block call emits in the ordered pass and every assignment in the unordered pass. A +shared bug with xml2st; regressed from v1.x; FBD escapes it via interleaved execution orders. +The same hazard applies to **coils** fed by a block (confirmed). + +## The fix + +An output-write sink (coil / output / inOut variable) fed solely by a block is that block's +output binding, so it must emit immediately after the block's **actual call** — eager or lazy. +Implemented at the emission level in `walker/ld.ts`: + +- `blockFedSink` identifies such sinks (single incoming edge from a block). +- `emitLdBody` indexes them per block into `state.consumersByBlock`. +- `emitFunctionCall` / `emitFunctionBlockCall` call `emitBlockConsumers` right after pushing + the call, which emits the block's consumers through the existing coil-grouping sweep (so the + #836 grouping still applies to multiple SET/RESET coils off one block). +- `state.emittedSinks` makes emission idempotent; the main sink sweep skips what coupling + already emitted. + +Emitting at the *call* (not the sink slot) is what makes it robust to blocks pulled lazily +through a later block's `EN` expression (e.g. `GT.EN := MUL.ENO OR SUB.ENO`), where the call +lands mid-walk. The earlier sink-list-reorder approach failed that case. + +This is a deliberate divergence from xml2st (same class as #836 / the D1 task-sort fix). + +## Verification + +- Regression test `st-transpiler/__tests__/block-output-ordering-830.test.ts` — the reported + MOD→DIV chain, the eager-pull (`GT.EN`) shape, and a block-fed coil; exact-ST assertions. +- Corpus: 10 of 193 LD fixtures changed, **all corrections** — each block's output write moved + to immediately follow its call. Notably `edge__matiasacuna_..._sc_freidora` (a 4-block + `INT_TO_REAL→SUB→MUL→DIV` chain) and `edge__frontadomarcositsu_...` (a 5-block chain) were + fully broken and are now correct. The other 183 fixtures are unchanged. +- #836 coil-grouping regression test still passes; 301 compile/library integration tests pass. diff --git a/docs/transpiler/coil-reevaluation-836.md b/docs/transpiler/coil-reevaluation-836.md new file mode 100644 index 000000000..5286b4552 --- /dev/null +++ b/docs/transpiler/coil-reevaluation-836.md @@ -0,0 +1,61 @@ +# Coil condition re-evaluation in LD → ST (issue #836) + +Status: **resolved** on `fix/parity/parallel-coil-single-if` (folded into PR #891 / web #543, +which become the complete "coil grouping" fix). Applied identically in both repos +(openplc-editor + openplc-web) across the byte-identical +`src/backend/shared/transpilers/st-transpiler/` surface; verified with +`scripts/compare-surfaces.py`. + +## The bug + +The LD walker emitted one `IF THEN := …; END_IF;` per coil, re-reading +the condition's variables each time. When a coil writes a variable that a later-emitted +coil's condition reads, the later coil sees the mutated value. The reporter's case: a +`FirstScan` contact feeding `Output1(S)`, a `FirstScan(R)` reset, and `Output2(S)` — with +`FirstScan` initially TRUE, the reset clears it before `Output2`'s `IF` re-reads it, so +`Output2` never sets. + +The old `xml2st` engine has the same bug, so this fix is a deliberate divergence from the +oracle (consistent with the D1-fix precedent on the parity branch). + +## Expected behaviour (from the maintainer, three topologies) + +| Topology | Expected ST | +|---|---| +| **S1** `FirstScan → [Output1(S) ∥ Output2(S)] → FirstScan(R)` | `Output1`+`Output2` in one `IF`; reset in a separate `IF` | +| **S2** `FirstScan → [Output1(S) ∥ FirstScan(R) ∥ Output2(S)]` | all three assignments in one `IF` | +| **S3** sequential `FirstScan → Output1(S) → FirstScan(R) → Output2(S)` | three separate `IF`s (Output2 stays unset) | + +The rule: SET/RESET coils that branch off the **same source** (parallel rails) share one +energization and collapse into a single `IF`; coils with **distinct sources** (sequential) +stay as separate `IF`s. Sequential coils intentionally keep the re-evaluation semantics. + +## The fix (two edits) + +1. **`walker/ld.ts` — `emitSinksWithCoilGrouping`**: collapse same-source SET/RESET coils + into one `IF`, gathering them **even when not adjacent** in emission order. A coil fed by + their merge (sharing neither source) can sort between two parallel branches by position; + the branches still group, emitted at the first branch's slot, with the merge-fed coil + following after (its dataflow order). The group key is the coil's sorted immediate + incoming source-node ids (`setResetCoilGroupKey`); the group emits via `emitCoilGroup`. +2. **`core/path-tree.ts` — `factorizePaths`**: drop byte-identical duplicate paths + (`X OR X` → `X`) before factoring. Two parallel branches tracing to the same contact + would otherwise render the reset condition as `FirstScan AND (TRUE OR TRUE)` (the TS + port's factorization of two identical single-leaf paths; xml2st renders the same + topology as `FirstScan OR FirstScan`). Dedup keys on the full repr — text **and** source + locations — so two *distinct* contacts on the same variable still survive as separate OR + terms. No behavioural change; cosmetic only. + +This supersedes the earlier "Approach A" (condition snapshotting into temps), which was +dropped once the maintainer's expected outputs showed the intended solution is grouping — +and that sequential coils (S3) should *not* be "fixed" by a snapshot. + +## Verification + +- Unit regression test (byte-identical both repos): + `st-transpiler/__tests__/coil-grouping-836.test.ts` — reconstructs all three topologies + and asserts exact ST. All three now match the maintainer's expected output. +- Corpus: ran the 193-project LD corpus (`~/src/xml2st/fixtures/golden/real_projects_json`) + through `emitLdBody` before/after. **0 fixtures changed** for the grouping extension and + **0** for the dedup — S1's topology (parallel branches merging into a downstream coil) + does not occur in the corpus, which is why it was a blind spot. diff --git a/jest.config.json b/jest.config.json index 482c4ef06..7572446f6 100644 --- a/jest.config.json +++ b/jest.config.json @@ -15,7 +15,13 @@ "url": "http://localhost/" }, "testMatch": ["/src/**/?(*.)+(spec|test).(ts|tsx)", "/src/**/__tests__/**/*.(ts|tsx)"], - "testPathIgnorePatterns": ["/node_modules/", "src/frontend/utils/__tests__/notify-no-write-permission.test.ts"], + "testPathIgnorePatterns": [ + "/node_modules/", + "src/frontend/utils/__tests__/notify-no-write-permission.test.ts", + "src/frontend/services/__tests__/export-actions.test.ts", + "src/frontend/services/__tests__/import-actions.test.ts", + "src/frontend/components/_organisms/modals/__tests__/confirm-plcopen-import-modal.test.tsx" + ], "transformIgnorePatterns": ["node_modules/(?!strucpp)"], "transform": { "\\.(ts|tsx|js|jsx)$": [ diff --git a/package-lock.json b/package-lock.json index bd4b6038f..c072c4bb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-plc-editor", - "version": "4.2.6", + "version": "4.2.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-plc-editor", - "version": "4.2.6", + "version": "4.2.8", "hasInstallScript": true, "license": "GPL-3.0", "dependencies": { @@ -33,7 +33,7 @@ "@tailwindcss/forms": "^0.5.10", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.2", - "@xyflow/react": "^12.0.1", + "@xyflow/react": "^12.11.2", "auto-zustand-selectors-hook": "^2.0.0", "avr8js": "0.20.0", "axios": "^1.13.6", @@ -9816,15 +9816,15 @@ } }, "node_modules/@types/d3-selection": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.10.tgz", - "integrity": "sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg==", + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", "license": "MIT" }, "node_modules/@types/d3-transition": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.8.tgz", - "integrity": "sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", "license": "MIT", "dependencies": { "@types/d3-selection": "*" @@ -11049,17 +11049,28 @@ "license": "Apache-2.0" }, "node_modules/@xyflow/react": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.0.1.tgz", - "integrity": "sha512-iGh/nO7key0sVH0c8TW2qvLNU0akJ20Mi3LPUF2pymhRqerrBk0EJhPLXRThbYWy4pNWUnkhpBLB0/gr884qnw==", + "version": "12.11.2", + "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.11.2.tgz", + "integrity": "sha512-eLAlDWJfWnQEhJwGMjlWdAXO9eYllKpliUmPQlAmOLxz6mExXuzMVDUKLMquixgkrtmMFFtug3jGKmYYld12cA==", + "license": "MIT", "dependencies": { - "@xyflow/system": "0.0.35", + "@xyflow/system": "0.0.79", "classcat": "^5.0.3", "zustand": "^4.4.0" }, "peerDependencies": { + "@types/react": ">=17", + "@types/react-dom": ">=17", "react": ">=17", "react-dom": ">=17" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "node_modules/@xyflow/react/node_modules/zustand": { @@ -11091,15 +11102,18 @@ } }, "node_modules/@xyflow/system": { - "version": "0.0.35", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.35.tgz", - "integrity": "sha512-QaUkahvmMs2gY2ykxUfjs5CbkXzU5fQNtmoQQ6HmHoAr8n2D7UyLO/UEXlke2jxuCDuiwpXhrzn4DmffVJd2qA==", + "version": "0.0.79", + "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.79.tgz", + "integrity": "sha512-czLyOh91NF0hIzbNzwi8I6GlqG23BHh2435OddfI6uiaLH3xdrdygO93gqgH1Bv9mhy8XPFQJOBn1FTq4LvEWA==", + "license": "MIT", "dependencies": { "@types/d3-drag": "^3.0.7", + "@types/d3-interpolate": "^3.0.4", "@types/d3-selection": "^3.0.10", "@types/d3-transition": "^3.0.8", "@types/d3-zoom": "^3.0.8", "d3-drag": "^3.0.0", + "d3-interpolate": "^3.0.1", "d3-selection": "^3.0.0", "d3-zoom": "^3.0.0" } diff --git a/package.json b/package.json index 8582d6414..b50ae32f1 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.8", + "version": "4.2.9", "license": "GPL-3.0", "author": { "name": "Autonomy Logic" @@ -56,7 +56,7 @@ "@tailwindcss/forms": "^0.5.10", "@tanstack/react-query": "^5.90.21", "@tanstack/react-table": "^8.21.2", - "@xyflow/react": "^12.0.1", + "@xyflow/react": "^12.11.2", "auto-zustand-selectors-hook": "^2.0.0", "avr8js": "0.20.0", "axios": "^1.13.6", diff --git a/scripts/compare-surfaces.py b/scripts/compare-surfaces.py index 508d6b9a6..c3b1465f3 100644 --- a/scripts/compare-surfaces.py +++ b/scripts/compare-surfaces.py @@ -13,7 +13,10 @@ - bare-metal-runtime (editor: resources/sources/{arduino,Baremetal}; web: src/assets/firmware/{arduino,Baremetal}) -Every file in these surfaces must be byte-identical across both repos. +Every program file in these surfaces must be byte-identical across both +repos. Test files (`__tests__/` directories, `*.test.ts(x)`, `*.spec.ts(x)`) +are excluded — they're allowed to diverge (platform-specific mocks, adapter- +only e2e suites) without indicating a real program-file drift. Exit code 0 = all identical, 1 = differences found. """ @@ -63,13 +66,24 @@ def hash_file(path: Path) -> str: return h.hexdigest() +def is_test_file(path: Path) -> bool: + """True for test files/directories — same exclusion the architecture + validator (__architecture__/validate.ts) already applies to its own scan. + Test files are allowed to diverge between repos (platform-specific mocks, + adapter-only e2e suites) without indicating a real program-file drift.""" + if "__tests__" in path.parts: + return True + name = path.name + return any(name.endswith(suffix) for suffix in (".test.ts", ".test.tsx", ".spec.ts", ".spec.tsx")) + + def collect_hashes(root: Path, surface: str) -> dict[str, str]: base = root / surface if not base.exists(): return {} result = {} for path in sorted(base.rglob("*")): - if path.is_file(): + if path.is_file() and not is_test_file(path): rel = str(path.relative_to(root)) result[rel] = hash_file(path) return result @@ -108,7 +122,7 @@ def collect_all_hashes(base: Path, exts: list[str] | None = None) -> dict[str, s return result ext_set = set(exts) if exts else None for path in sorted(base.rglob("*")): - if path.is_file() and (ext_set is None or path.suffix in ext_set): + if path.is_file() and not is_test_file(path) and (ext_set is None or path.suffix in ext_set): result[str(path.relative_to(base))] = hash_file(path) return result diff --git a/scripts/download-binaries.ts b/scripts/download-binaries.ts index e7a18860c..321641d5b 100644 --- a/scripts/download-binaries.ts +++ b/scripts/download-binaries.ts @@ -1,10 +1,10 @@ /** - * 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 the pinned version is already present. */ import { execSync } from 'child_process' @@ -21,19 +21,9 @@ interface ToolEntry { } interface BinaryVersions { - xml2st: ToolEntry strucpp: ToolEntry } -interface CacheMetadata { - xml2st: string - platform: string - arch: string -} - -type Platform = 'darwin' | 'linux' | 'win32' -type Arch = 'x64' | 'arm64' - // --------------------------------------------------------------------------- // Paths // --------------------------------------------------------------------------- @@ -42,84 +32,18 @@ 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; strucppOnly: boolean } { - const args = process.argv.slice(2) - let platform = process.platform as string - let arch = process.arch as string - let force = false - // strucpp-only: install just the platform-independent strucpp package (its - // libs/ + runtime headers are imported by the TS sources). Skips the xml2st - // platform binary — used by the unit-test CI job, which runs `npm ci - // --ignore-scripts` (so the postinstall never fetched strucpp) but doesn't - // need the heavyweight platform binaries or a native rebuild. - let strucppOnly = 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 - } else if (args[i] === '--strucpp-only') { - strucppOnly = 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, strucppOnly } +function parseArgs(): { force: boolean } { + return { force: process.argv.slice(2).includes('--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 { // strucpp is `npm install`-ed into `node_modules/`, so npm's own // `package.json` is the canonical source of truth for what's @@ -138,16 +62,6 @@ function needsStrucpp(versions: BinaryVersions): boolean { 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 // --------------------------------------------------------------------------- @@ -161,97 +75,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 // --------------------------------------------------------------------------- @@ -262,6 +91,7 @@ async function downloadStrucpp(tool: ToolEntry): Promise { const url = `https://github.com/${tool.repository}/releases/download/${tool.version}/strucpp-${version}.tgz` console.log(` Downloading strucpp ${tool.version}...`) + fs.mkdirSync(RESOURCES_DIR, { recursive: true }) const tmpDir = fs.mkdtempSync(path.join(RESOURCES_DIR, '.tmp-strucpp-')) try { @@ -297,7 +127,7 @@ async function downloadStrucpp(tool: ToolEntry): Promise { // --------------------------------------------------------------------------- async function main(): Promise { - const { platform, arch, force, strucppOnly } = parseArgs() + const { force } = parseArgs() if (!fs.existsSync(VERSIONS_FILE)) { console.error(`binary-versions.json not found at ${VERSIONS_FILE}`) @@ -306,34 +136,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} strucppOnly=${strucppOnly}`) + console.log(`[download-binaries] force=${force}`) - // strucpp is platform-independent (its libs/ + runtime headers are imported - // by the TS sources) — download it whenever it's missing or outdated. + // strucpp is platform-independent — installed into node_modules. if (force || needsStrucpp(versions)) { await downloadStrucpp(versions.strucpp) } else { console.log(` strucpp ${versions.strucpp.version} already installed, skipping.`) } - // --strucpp-only stops here: the caller (e.g. the unit-test CI job) needs the - // strucpp package but not the platform binary or the platform cache marker. - if (strucppOnly) { - console.log(`[download-binaries] strucpp-only: skipped xml2st platform binary.`) - return - } - - const targetBinDir = binDir(platform, arch) - fs.mkdirSync(targetBinDir, { recursive: true }) - - const cached = force ? null : getCachedMetadata(platform, arch) - if (force || needsXml2st(versions, cached, platform, arch)) { - await downloadXml2st(versions.xml2st, platform, arch, targetBinDir) - } else { - console.log(` xml2st ${versions.xml2st.version} already installed, skipping.`) - } - - writeCache(versions, platform, arch) console.log(`[download-binaries] Done.`) } diff --git a/src/__architecture__/validate.ts b/src/__architecture__/validate.ts index e498384df..8070dc4b8 100644 --- a/src/__architecture__/validate.ts +++ b/src/__architecture__/validate.ts @@ -279,6 +279,13 @@ const KNOWN_EXCEPTIONS: Record = { 'frontend/store/slices/ladder/utils/index.ts': ['components'], // Ladder slice — needs nodesBuilder + defaultCustomNodesStyles for rung creation 'frontend/store/slices/ladder/slice.ts': ['components'], + // PLCopen export — needs the shared XmlGenerator composing function + // (backend/shared/utils/PLC/xml-generator.ts) to turn the converted + // project data into XML before handing it to the platform port. No + // frontend-reachable layer re-exports this function today; the + // conversion logic itself stays local (mirrors compiler-adapter.ts's + // portToSchemaProjectData) and has no other backend-shared dependency. + 'frontend/services/export-actions.ts': ['backend-shared'], } // --------------------------------------------------------------------------- diff --git a/src/backend/editor/compiler/compiler-module.ts b/src/backend/editor/compiler/compiler-module.ts index c8b914fc9..6a3ae3cdb 100644 --- a/src/backend/editor/compiler/compiler-module.ts +++ b/src/backend/editor/compiler/compiler-module.ts @@ -93,7 +93,6 @@ 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' @@ -177,8 +176,6 @@ class CompilerModule { arduinoCliConfigurationFilePath: string arduinoCliBaseParameters: string[] - xml2stBinaryPath: string - strucppRuntimeDir: string // Memoised arduino-cli `--show-properties=expanded` output keyed by FQBN. @@ -233,8 +230,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() } @@ -361,10 +356,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: @@ -443,14 +434,6 @@ 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 ================================= // ############################################################################ @@ -852,64 +835,6 @@ class CompilerModule { // +++++++++++++++++++++++++++ Compilation Methods +++++++++++++++++++++++++++++ - async handleGenerateXMLfromJSON(sourceTargetFolderPath: string, jsonData: PLCProjectData) { - return new Promise>((resolve, reject) => { - const { data: xmlData } = XmlGenerator(jsonData as Parameters[0], 'old-editor') - if (typeof xmlData !== 'string') { - reject(new Error('XML data is not a string')) - return - } - - const xmlCreationResult = CreateXMLFile(sourceTargetFolderPath, xmlData, 'plc') - - if (xmlCreationResult.success) { - resolve({ success: true, data: { xmlPath: sourceTargetFolderPath, xmlContent: xmlData } }) - } else { - reject(new Error('Failed to create XML file')) - } - }) - } - - 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, @@ -2391,7 +2316,7 @@ class CompilerModule { * Main compile entry point. Drives the full Step 0-13 flow * through the shared `runCompilePipeline` orchestrator * (`backend/shared/compile/pipeline.ts`); platform-specific bits - * (xml2st spawn, arduino-cli spawn, runtime upload) are abstracted + * (arduino-cli spawn, runtime upload) are abstracted * behind `EditorCompilerPlatformPort`. Single source of truth * for compile behaviour shared with openplc-web. */ @@ -2644,7 +2569,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), @@ -2855,71 +2779,45 @@ class CompilerModule { return } - if (isNewTranspilerEnabled()) { - // JSON → ST in-process via `st-transpiler`. Mirrors what - // `editor-compiler-platform-port.transpileToSt` does for the - // shared pipeline path, scoped down to the debug compile here. + // Project -> ST via the in-process transpiler. The resulting program.st + // is what STruC++ reads downstream. + type DebugStResult = { ok: true; programSt: string } | { ok: false; programSt?: undefined; error: string } + + const runDebugInProcess = (): DebugStResult => { 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 }) + return { ok: false, error: result.errors.join('\n') || 'Failed to generate Structured Text' } } - 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}` }) + return { ok: true, programSt: result.programSt } } catch (error) { - _mainProcessPort.postMessage({ - logLevel: 'error', - message: `Error transpiling JSON to ST: ${getErrorMessage(error)}\nStopping debug compilation process.`, - }) - _mainProcessPort.close() - return - } - } else { - try { - const generateXMLResult = await this.handleGenerateXMLfromJSON(sourceTargetFolderPath, projectData) - _mainProcessPort.postMessage({ - logLevel: 'info', - message: `Generated XML from JSON at: ${generateXMLResult.data?.xmlPath as string}`, - }) - } catch (error) { - _mainProcessPort.postMessage({ - logLevel: 'error', - message: `Error generating XML from JSON: ${error as string}\nStopping debug compilation process.`, - }) - _mainProcessPort.close() - return + return { ok: false, error: `st-transpiler failed: ${getErrorMessage(error)}` } } + } - const generatedXMLFilePath = join(sourceTargetFolderPath, 'plc.xml') - try { - await this.handleTranspileXMLtoST( - generatedXMLFilePath, - (data, logLevel) => { - _mainProcessPort.postMessage({ logLevel, message: data }) - }, - ['--keep-structs'], - ) - } catch (error) { + const programStPath = join(sourceTargetFolderPath, 'program.st') + try { + await mkdir(sourceTargetFolderPath, { recursive: true }) + const stResult = runDebugInProcess() + if (!stResult.ok) { _mainProcessPort.postMessage({ logLevel: 'error', - message: `Error transpiling XML to ST: ${error as string}\nStopping debug compilation process.`, + message: `${stResult.error}\nStopping debug compilation process.`, }) _mainProcessPort.close() return } + await writeFile(programStPath, stResult.programSt, 'utf-8') + } catch (error) { + _mainProcessPort.postMessage({ + logLevel: 'error', + message: `Error writing ST file: ${getErrorMessage(error)}\nStopping debug compilation process.`, + }) + _mainProcessPort.close() + return } + _mainProcessPort.postMessage({ logLevel: 'info', message: `ST file generated at: ${programStPath}` }) try { await this.copyStaticFiles(compilationPath, boardRuntime) @@ -3026,15 +2924,10 @@ class CompilerModule { * 1. Read `/library.json` from disk (the manifest * tab's surgical save has already written the live buffer * ahead of this call — Phase 5). - * 2. `prepareXmlForLibraryBuild` validates the manifest and - * emits the XML xml2st consumes. Manifest errors fail fast - * here without spawning xml2st. - * 3. Persist plc.xml under `/build/library/src/` - * and run the existing `handleTranspileXMLtoST` helper so - * this path shares the xml2st spawn / error-handling code - * the program build already uses. - * 4. Read xml2st's program.st back and hand it + - * knownPous + manifest to `libraryBuildFromTranspiledSt`, + * 2. Validate the manifest. Manifest errors fail fast here. + * 3-4. Transpile the library's POUs to ST via the in-process + * transpiler (through the desktop library build port) and hand + * the ST + knownPous + manifest to `libraryBuildFromTranspiledSt`, * which drops the synthetic stub and calls strucpp's * `compileStlib`. * 5. Write the archive (same `JSON.stringify(archive, null, 2)` @@ -3071,7 +2964,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 6e074e9d7..d1afb3504 100644 --- a/src/backend/editor/compiler/desktop-library-build-port.ts +++ b/src/backend/editor/compiler/desktop-library-build-port.ts @@ -7,10 +7,7 @@ * `runLibraryBuildPipeline` cannot perform itself: * * - MD5 hashing (Node `crypto`) - * - ST transpilation through either backend, selected via - * `isNewTranspilerEnabled()` — the in-process JSON-fed - * transpiler when on, the bundled `xml2st` subprocess (default) - * when off + * - ST transpilation via the in-process JSON-fed 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 @@ -23,19 +20,16 @@ * 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 { 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' @@ -45,22 +39,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. - * - * 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 @@ -93,77 +71,31 @@ export function createDesktopLibraryBuildPort(deps: DesktopLibraryBuildPortDeps) return Promise.resolve(createHash('md5').update(input).digest('hex')) }, - async transpileToSt( + transpileToSt( args: TranspileToStArgs, log: (message: string, level: 'info' | 'warning' | 'error') => void, ): Promise { - if (isNewTranspilerEnabled()) { - try { - // Editor library builds receive the same schema-shape - // project data as `compileProgram` (see `transpileToSt` on - // `editor-compiler-platform-port` for the IPC-shape note). - // The double cast bridges the port's declared port-shape type - // and the actual schema-shape payload at the boundary. - const ir = fromSchemaShape(args.projectData as unknown as SchemaProjectData) - const result = runJsonTranspiler(ir) - if (result.programSt === null || result.errors.length > 0) { - const message = result.errors.join('\n') || 'transpile-from-json failed' - log(message, 'error') - return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } - } - for (const warning of result.warnings) { - log(warning, 'info') - } - return { ok: true, programSt: result.programSt } - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - log(`transpile-from-json failed: ${message}`, 'error') - return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } - } - } - - // Legacy path: serialise the project to PLCOpen XML and spawn - // the bundled `xml2st` binary on a temp file. Lifts the - // pre-Phase-2 implementation byte-for-byte; the only delta is - // the leading `XmlGenerator` call because the shared pipeline - // no longer hands the port a pre-built XML payload. - const xmlResult = XmlGenerator(args.projectData as never, 'old-editor') - if (!xmlResult.ok || !xmlResult.data) { - log(`XML generation failed: ${xmlResult.message}`, 'error') - return { ok: false, errors: [{ message: xmlResult.message, line: 0, column: 0, severity: 'error' }] } - } - // xml2st takes a file path on stdin, so materialise the - // in-memory XML to a unique temp file before spawning. - // Lives in `os.tmpdir()` because the user-visible `plc.xml` - // 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 } + // 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 Promise.resolve({ ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] }) + } + for (const warning of result.warnings) { + log(warning, 'info') + } + return Promise.resolve({ ok: true, programSt: result.programSt }) } catch (error) { const message = error instanceof Error ? error.message : String(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 */ - }) + log(`transpile-from-json failed: ${message}`, 'error') + return Promise.resolve({ ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] }) } }, diff --git a/src/backend/editor/compiler/editor-compiler-platform-port.ts b/src/backend/editor/compiler/editor-compiler-platform-port.ts index 5dc1a2116..8b689d18d 100644 --- a/src/backend/editor/compiler/editor-compiler-platform-port.ts +++ b/src/backend/editor/compiler/editor-compiler-platform-port.ts @@ -16,21 +16,14 @@ * - Translates the handler's return value back into the port's * canonical result shape * - * `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. + * `transpileToSt` runs the in-process JSON-fed transpiler + * (`backend/shared/transpilers/st-transpiler/`). * * 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 { @@ -38,7 +31,6 @@ 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, @@ -71,7 +63,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'] @@ -149,6 +140,30 @@ export function createEditorCompilerPlatformPort( handlers: EditorCompilerHandlers, context: EditorCompilerPlatformPortContext, ): CompilerPlatformPort { + /** + * Transpile: project the schema-shape payload via `fromSchemaShape` + * and run the in-process JSON transpiler. Folds failures into the result. + * The editor's IPC payload is schema-shape; the double cast bridges the + * declared-vs-runtime mismatch. + */ + const runInProcessTranspile = (args: TranspileToStArgs): TranspileToStResult => { + try { + 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' + return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } + } + return { ok: true, programSt: result.programSt } + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + return { + ok: false, + errors: [{ message: `st-transpiler failed: ${message}`, line: 0, column: 0, severity: 'error' }], + } + } + } + return { /** * Node's `crypto.createHash('md5')` produces the canonical MD5 @@ -156,77 +171,21 @@ export function createEditorCompilerPlatformPort( * adapter computes the same hash via `spark-md5`; both outputs * are byte-identical. */ - async computeMd5(input: string): Promise { - return createHash('md5').update(input).digest('hex') + computeMd5(input: string): Promise { + return Promise.resolve(createHash('md5').update(input).digest('hex')) }, /** - * 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. + * Transpile the project IR to Structured Text via the in-process JSON + * transpiler (`backend/shared/transpilers/st-transpiler/`). */ - async transpileToSt(args: TranspileToStArgs, log: PlatformLog): Promise { - if (isNewTranspilerEnabled()) { - try { - // Editor IPC delivers the schema-shape project data - // (discriminated-union POUs + singular `configuration`). - // The port's declared `projectData` type is port-shape, but - // the pipeline reaches us with the editor's schema-shape IPC - // payload (matching `compileProgram`'s actual contract). The - // double cast bridges the static mismatch without serialising - // through `unknown` at runtime. - const ir = fromSchemaShape(args.projectData as unknown as SchemaProjectData) - const result = runJsonTranspiler(ir) - if (result.programSt === null || result.errors.length > 0) { - const message = result.errors.join('\n') || 'Failed to generate Structured Text' - log(message, 'error') - return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } - } - for (const warning of result.warnings) { - log(warning, 'info') - } - return { ok: true, programSt: result.programSt } - } catch (error) { - const message = error instanceof Error ? error.message : String(error) - log(`st-transpiler failed: ${message}`, 'error') - return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } - } - } - - const xmlResult = XmlGenerator(args.projectData as never, 'old-editor') - if (!xmlResult.ok || !xmlResult.data) { - log(`XML generation failed: ${xmlResult.message}`, 'error') - return { ok: false, errors: [{ message: xmlResult.message, line: 0, column: 0, severity: 'error' }] } - } - const xmlPath = join(context.sourceTargetFolderPath, 'plc.xml') - try { - await fs.mkdir(dirname(xmlPath), { recursive: true }) - await fs.writeFile(xmlPath, 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(`xml2st failed: ${message}`, 'error') - return { ok: false, errors: [{ message, line: 0, column: 0, severity: 'error' }] } + transpileToSt(args: TranspileToStArgs, log: PlatformLog): Promise { + const result = runInProcessTranspile(args) + if (!result.ok) { + const message = result.errors?.map((e) => e.message).join('\n') || 'Failed to generate Structured Text' + log(message, 'error') } + return Promise.resolve(result) }, /** diff --git a/src/backend/editor/hardware/__tests__/serial-port-list.test.ts b/src/backend/editor/hardware/__tests__/serial-port-list.test.ts new file mode 100644 index 000000000..be8c99782 --- /dev/null +++ b/src/backend/editor/hardware/__tests__/serial-port-list.test.ts @@ -0,0 +1,140 @@ +import { mergeSerialPortList, toCalloutPath } from '../serial-port-list' + +const boardMap = (entries: Array<[string, string | undefined]>) => new Map(entries) + +describe('toCalloutPath', () => { + it('rewrites a macOS dial-in (tty.) node to its call-out (cu.) node', () => { + expect(toCalloutPath('/dev/tty.usbmodem11301')).toBe('/dev/cu.usbmodem11301') + }) + + it('leaves an already-call-out (cu.) path unchanged', () => { + expect(toCalloutPath('/dev/cu.usbmodem11301')).toBe('/dev/cu.usbmodem11301') + }) + + it('leaves Linux paths unchanged (no dotted tty. prefix)', () => { + expect(toCalloutPath('/dev/ttyUSB0')).toBe('/dev/ttyUSB0') + expect(toCalloutPath('/dev/ttyACM0')).toBe('/dev/ttyACM0') + }) + + it('leaves Windows COM paths unchanged', () => { + expect(toCalloutPath('COM3')).toBe('COM3') + }) +}) + +describe('mergeSerialPortList', () => { + it('labels a port with the arduino-cli board name when identified', () => { + const boards = boardMap([['/dev/cu.usbmodem1', 'Arduino Uno']]) + const manufacturers = boardMap([['/dev/cu.usbmodem1', 'Arduino LLC']]) + + expect(mergeSerialPortList(boards, manufacturers)).toEqual([ + { name: '/dev/cu.usbmodem1 (Arduino Uno)', address: '/dev/cu.usbmodem1' }, + ]) + }) + + it('prefers the board name over the manufacturer when both are present', () => { + const boards = boardMap([['COM1', 'Opta']]) + const manufacturers = boardMap([['COM1', 'Arduino']]) + + expect(mergeSerialPortList(boards, manufacturers)[0].name).toBe('COM1 (Opta)') + }) + + it('falls back to the manufacturer when the board is detected but not identified', () => { + const boards = boardMap([['COM6', undefined]]) + const manufacturers = boardMap([['COM6', 'com0com - serial port emulator']]) + + expect(mergeSerialPortList(boards, manufacturers)).toEqual([ + { name: 'COM6 (com0com - serial port emulator)', address: 'COM6' }, + ]) + }) + + it('uses the bare path when neither board nor manufacturer is known', () => { + const boards = boardMap([]) + const manufacturers = boardMap([['/dev/ttyUSB0', undefined]]) + + expect(mergeSerialPortList(boards, manufacturers)).toEqual([{ name: '/dev/ttyUSB0', address: '/dev/ttyUSB0' }]) + }) + + it('treats an empty-string descriptor as absent', () => { + const boards = boardMap([['COM1', '']]) + const manufacturers = boardMap([['COM1', '']]) + + expect(mergeSerialPortList(boards, manufacturers)).toEqual([{ name: 'COM1', address: 'COM1' }]) + }) + + it('unions both scans, keeps serialport ordering, and dedupes by path', () => { + // COM3/COM4 come from serialport; arduino-cli enriches COM4 and adds COM9. + const manufacturers = boardMap([ + ['COM3', 'FTDI'], + ['COM4', undefined], + ]) + const boards = boardMap([ + ['COM4', 'Arduino Mega'], + ['COM9', 'Arduino Nano'], + ]) + + expect(mergeSerialPortList(boards, manufacturers)).toEqual([ + { name: 'COM3 (FTDI)', address: 'COM3' }, + { name: 'COM4 (Arduino Mega)', address: 'COM4' }, + { name: 'COM9 (Arduino Nano)', address: 'COM9' }, + ]) + }) + + it('returns an empty list when both scans are empty', () => { + expect(mergeSerialPortList(boardMap([]), boardMap([]))).toEqual([]) + }) + + describe('macOS tty./cu. canonicalization', () => { + it('collapses the tty. (serialport) and cu. (arduino-cli) nodes of one device into a single cu. entry', () => { + const manufacturers = boardMap([['/dev/tty.usbmodem11301', 'Arduino']]) + const boards = boardMap([['/dev/cu.usbmodem11301', 'Opta']]) + + expect(mergeSerialPortList(boards, manufacturers)).toEqual([ + { name: '/dev/cu.usbmodem11301 (Opta)', address: '/dev/cu.usbmodem11301' }, + ]) + }) + + it('canonicalizes a tty.-only device (from serialport) to its cu. node', () => { + const manufacturers = boardMap([['/dev/tty.usbserial-99', 'FTDI']]) + + expect(mergeSerialPortList(boardMap([]), manufacturers)).toEqual([ + { name: '/dev/cu.usbserial-99 (FTDI)', address: '/dev/cu.usbserial-99' }, + ]) + }) + + it('reproduces the reported duplicate-ports scenario as a single deduped, cu.-based list', () => { + // serialport reports tty.* with manufacturers; arduino-cli reports cu.* with board id. + const manufacturers = boardMap([ + ['/dev/tty.debug-console', undefined], + ['/dev/tty.Bluetooth-Incoming-Port', undefined], + ['/dev/tty.usbserial-1140', 'Prolific Technology Inc.'], + ['/dev/tty.usbmodem11301', 'Arduino'], + ]) + const boards = boardMap([ + ['/dev/cu.debug-console', undefined], + ['/dev/cu.Bluetooth-Incoming-Port', undefined], + ['/dev/cu.usbserial-1140', undefined], + ['/dev/cu.usbmodem11301', 'Opta'], + ]) + + expect(mergeSerialPortList(boards, manufacturers)).toEqual([ + { name: '/dev/cu.debug-console', address: '/dev/cu.debug-console' }, + { name: '/dev/cu.Bluetooth-Incoming-Port', address: '/dev/cu.Bluetooth-Incoming-Port' }, + { name: '/dev/cu.usbserial-1140 (Prolific Technology Inc.)', address: '/dev/cu.usbserial-1140' }, + { name: '/dev/cu.usbmodem11301 (Opta)', address: '/dev/cu.usbmodem11301' }, + ]) + }) + + it('does not merge Linux tty paths (no dotted tty./cu. prefix)', () => { + const manufacturers = boardMap([ + ['/dev/ttyUSB0', 'FTDI'], + ['/dev/ttyACM0', undefined], + ]) + const boards = boardMap([['/dev/ttyACM0', 'Arduino Uno']]) + + expect(mergeSerialPortList(boards, manufacturers)).toEqual([ + { name: '/dev/ttyUSB0 (FTDI)', address: '/dev/ttyUSB0' }, + { name: '/dev/ttyACM0 (Arduino Uno)', address: '/dev/ttyACM0' }, + ]) + }) + }) +}) diff --git a/src/backend/editor/hardware/hardware-module.ts b/src/backend/editor/hardware/hardware-module.ts index c51b361d2..ea312170a 100644 --- a/src/backend/editor/hardware/hardware-module.ts +++ b/src/backend/editor/hardware/hardware-module.ts @@ -1,6 +1,8 @@ +import { execFile } 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' @@ -12,8 +14,11 @@ import { PackageManagerModule } from '../package-manager' import { logger } from '../services/logger-service' import { assertPathContained } from '../utils/path-containment' import { orderBoardsByVppGroup } from './order-boards-by-vpp-group' +import { mergeSerialPortList } from './serial-port-list' import type { AvailableBoards, HalsFile, SerialPort } from './types' +const execFileAsync = promisify(execFile) + // interface MethodsResult { // success: boolean // data?: T @@ -94,22 +99,74 @@ class HardwareModule { // ++ ============================= Getters ================================ ++ async getAvailableSerialPorts(): Promise { - // 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. + // Two independent, best-effort scans merged by device path. The path is + // always the primary, unique label (mirrors the Arduino IDE); the + // parenthetical descriptor is the arduino-cli-identified board name when + // known, falling back to `serialport`'s manufacturer/vendor string. See + // `mergeSerialPortList` for the labelling rules. Running both scans is + // cheap here: the list is static after build and only re-scanned on an + // explicit user refresh. + const [boardNamesByPath, manufacturersByPath] = await Promise.all([ + this.#identifyBoardsByPath(), + this.#listSerialPortManufacturers(), + ]) + return mergeSerialPortList(boardNamesByPath, manufacturersByPath) + } + + /** + * `serialport` enumeration → `path → manufacturer`. This is the reliable, + * instant, cross-platform source for the *set* of ports; arduino-cli only + * enriches it. Best-effort: any failure yields an empty map (never throws) + * so arduino-cli-discovered ports still come through. + */ + async #listSerialPortManufacturers(): Promise> { try { const ports = await NodeSerialPort.list() - return ports.map((port) => ({ - name: port.manufacturer ?? port.path, - address: port.path, - })) + return new Map(ports.map((port) => [port.path, port.manufacturer])) } catch (error: unknown) { logger.error(`Failed to enumerate serial ports: ${String(error)}`) - return [] + return new Map() + } + } + + /** + * `arduino-cli board list --format json` → `path → board name`. arduino-cli + * matches each port's USB VID/PID against the installed cores' `boards.txt` + * — the exact identification the Arduino IDE surfaces (e.g. `Arduino Uno`, + * `Opta`). A detected-but-unmatched port maps to `undefined` (it will fall + * back to the manufacturer descriptor). Best-effort: a missing binary, no + * installed cores, a spawn error, or malformed JSON all yield an empty map + * so plain `serialport` enumeration still works. Reuses the same binary and + * `--config-file` as compile/upload, so it sees the same installed cores. + */ + async #identifyBoardsByPath(): Promise> { + const boardNamesByPath = new Map() + try { + let binaryPath = this.arduinoCliBinaryPath + if (HardwareModule.HOST_PLATFORM === 'win32') binaryPath += '.exe' + + const { stdout } = await execFileAsync( + binaryPath, + ['board', 'list', '--format', 'json', ...this.arduinoCliBaseParameters], + { timeout: 15_000, maxBuffer: 16 * 1024 * 1024 }, + ) + + const parsed = JSON.parse(stdout) as { + detected_ports?: Array<{ + matching_boards?: Array<{ name?: string }> + port?: { address?: string } + }> + } + + for (const detected of parsed.detected_ports ?? []) { + const address = detected.port?.address + if (!address) continue + boardNamesByPath.set(address, detected.matching_boards?.[0]?.name) + } + } catch (error: unknown) { + logger.warn(`arduino-cli board list failed; serial ports will show without board names: ${String(error)}`) } + return boardNamesByPath } /** diff --git a/src/backend/editor/hardware/serial-port-list.ts b/src/backend/editor/hardware/serial-port-list.ts new file mode 100644 index 000000000..3a925a310 --- /dev/null +++ b/src/backend/editor/hardware/serial-port-list.ts @@ -0,0 +1,82 @@ +import type { SerialPort } from './types' + +/** + * Canonicalize a serial-port path to the macOS call-out (`/dev/cu.*`) node. + * + * macOS exposes each serial device as a paired dial-in node (`/dev/tty.*`) + * and call-out node (`/dev/cu.*`) that differ only by that prefix. + * `serialport`'s native binding hardcodes the dial-in (`tty.*`) name + * (`@serialport/bindings-cpp` `darwin_list.cpp` reads `kIODialinDeviceKey`), + * but callers must use the call-out (`cu.*`) node to actually talk to a + * device — and that is the name arduino-cli and the Arduino IDE report. So + * we rewrite `tty.` → `cu.` at the source: both scans then agree on one path + * and no cross-node reconciliation is needed. IOKit always publishes both + * nodes for a serial service, so the rewritten path is guaranteed to exist. + * + * The pattern is macOS-specific (dotted prefix): Linux (`/dev/ttyUSB0`, + * `/dev/ttyACM0`) and Windows (`COM3`) paths don't match and pass through + * unchanged. Already-`cu.*` paths are left as-is. + */ +export function toCalloutPath(address: string): string { + return address.replace(/^\/dev\/tty\./, '/dev/cu.') +} + +/** Re-key a scan's map onto canonical call-out paths, keeping the first defined descriptor per device. */ +function toCalloutMap(byPath: Map): Map { + const result = new Map() + for (const [address, descriptor] of byPath) { + const key = toCalloutPath(address) + // `existing || descriptor` keeps the first non-empty descriptor seen for + // a device (e.g. if it somehow surfaced under both nodes). + result.set(key, result.get(key) || descriptor) + } + return result +} + +/** + * Merge the two independent serial-port scans into the `{ name, address }` + * shape the renderer's communication-port dropdown expects. + * + * The device path (`address`) is ALWAYS the primary, guaranteed-unique + * label — this mirrors the Arduino IDE, which keys every entry on the + * port path (`COM3`, `/dev/ttyACM0`, `/dev/cu.usbmodem…`) and never + * collapses ports to a shared vendor string. A descriptor is appended + * in parentheses when available, in order of usefulness: + * + * 1. the arduino-cli-identified board name (from the connected core's + * `boards.txt` VID/PID — e.g. `COM3 (Arduino Uno)`), else + * 2. the OS manufacturer/vendor string reported by `serialport` + * (e.g. `COM6 (com0com - serial port emulator)`), else + * 3. nothing — just the bare path. + * + * Both scans are first canonicalized to the macOS call-out node + * (`toCalloutPath`), so a device is keyed identically regardless of which + * scan reported it; the merge is then a plain union deduped by path. + * `serialport` provides the reliable, instant set of ports and is folded + * in first so its ordering is preserved; arduino-cli only enriches those + * entries and may contribute additional ports it discovered on its own. + * + * @param boardNamesByPath path → arduino-cli board name (`undefined` when + * the port was detected but no board matched) + * @param manufacturersByPath path → `serialport` manufacturer/vendor string + */ +export function mergeSerialPortList( + boardNamesByPath: Map, + manufacturersByPath: Map, +): SerialPort[] { + const boardNames = toCalloutMap(boardNamesByPath) + const manufacturers = toCalloutMap(manufacturersByPath) + + // serialport ordering first, then any arduino-cli-only ports. A Set keeps + // insertion order and dedupes ports seen by both scans. + const addresses = new Set([...manufacturers.keys(), ...boardNames.keys()]) + + return [...addresses].map((address) => { + // Board name (more specific) wins; `||` so an empty descriptor falls through. + const descriptor = boardNames.get(address) || manufacturers.get(address) + return { + name: descriptor ? `${address} (${descriptor})` : address, + address, + } + }) +} diff --git a/src/backend/editor/utils/__tests__/path-picker.test.ts b/src/backend/editor/utils/__tests__/path-picker.test.ts new file mode 100644 index 000000000..782975165 --- /dev/null +++ b/src/backend/editor/utils/__tests__/path-picker.test.ts @@ -0,0 +1,132 @@ +/** + * `getPlcopenImportFilePath` / `getPlcopenExportSavePath` — focused tests. + * + * Electron's `dialog` is mocked (no real native dialog in Jest); the + * filesystem is real (per-test temp dir) so read/write failure paths + * are exercised against actual I/O rather than stubbed error shapes. + */ + +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' + +const showOpenDialogMock = jest.fn() +const showSaveDialogMock = jest.fn() + +jest.mock('electron', () => ({ + BrowserWindow: class {}, + dialog: { + showOpenDialog: (...args: unknown[]) => showOpenDialogMock(...args), + showSaveDialog: (...args: unknown[]) => showSaveDialogMock(...args), + }, +})) + +import { getPlcopenExportSavePath, getPlcopenImportFilePath } from '../path-picker' + +describe('getPlcopenImportFilePath', () => { + let dir: string + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'plcopen-import-')) + jest.clearAllMocks() + }) + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }) + }) + + it('reads and returns the selected file content', async () => { + const filePath = join(dir, 'program.xml') + writeFileSync(filePath, '') + showOpenDialogMock.mockResolvedValue({ canceled: false, filePaths: [filePath] }) + + const result = await getPlcopenImportFilePath({} as never) + + expect(showOpenDialogMock).toHaveBeenCalledWith( + {}, + expect.objectContaining({ + title: 'Select a PLCopen XML file to import', + properties: ['openFile'], + filters: [{ name: 'PLCopen XML', extensions: ['xml'] }], + }), + ) + expect(result).toEqual({ success: true, content: '' }) + }) + + it('returns a canceled error when the user dismisses the dialog', async () => { + showOpenDialogMock.mockResolvedValue({ canceled: true, filePaths: [] }) + + const result = await getPlcopenImportFilePath({} as never) + + expect(result).toEqual({ + success: false, + error: { title: 'Operation canceled', description: 'Operation canceled by the user.' }, + }) + }) + + it('returns a read error when the selected file cannot be read', async () => { + const missingPath = join(dir, 'missing.xml') + showOpenDialogMock.mockResolvedValue({ canceled: false, filePaths: [missingPath] }) + + const result = await getPlcopenImportFilePath({} as never) + + expect(result).toEqual({ + success: false, + error: { title: 'Error reading file', description: 'Failed to read the selected PLCopen XML file.' }, + }) + }) +}) + +describe('getPlcopenExportSavePath', () => { + let dir: string + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'plcopen-export-')) + jest.clearAllMocks() + }) + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }) + }) + + it('writes the XML content to the chosen path', async () => { + const filePath = join(dir, 'exported.xml') + showSaveDialogMock.mockResolvedValue({ canceled: false, filePath }) + + const result = await getPlcopenExportSavePath({} as never, 'exported.xml', '') + + expect(showSaveDialogMock).toHaveBeenCalledWith( + {}, + expect.objectContaining({ + title: 'Export PLCopen XML', + defaultPath: 'exported.xml', + filters: [{ name: 'PLCopen XML', extensions: ['xml'] }], + }), + ) + expect(result).toEqual({ success: true }) + expect(readFileSync(filePath, 'utf-8')).toBe('') + }) + + it('returns a canceled error when the user dismisses the dialog', async () => { + showSaveDialogMock.mockResolvedValue({ canceled: true, filePath: undefined }) + + const result = await getPlcopenExportSavePath({} as never, 'exported.xml', '') + + expect(result).toEqual({ + success: false, + error: { title: 'Operation canceled', description: 'Operation canceled by the user.' }, + }) + }) + + it('returns a write error when the target path cannot be written', async () => { + const badPath = join(dir, 'nonexistent-subdir', 'exported.xml') + showSaveDialogMock.mockResolvedValue({ canceled: false, filePath: badPath }) + + const result = await getPlcopenExportSavePath({} as never, 'exported.xml', '') + + expect(result).toEqual({ + success: false, + error: { title: 'Error writing file', description: 'Failed to write the PLCopen XML file.' }, + }) + }) +}) diff --git a/src/backend/editor/utils/path-picker.ts b/src/backend/editor/utils/path-picker.ts index 85dc985f0..81b52500a 100644 --- a/src/backend/editor/utils/path-picker.ts +++ b/src/backend/editor/utils/path-picker.ts @@ -74,4 +74,66 @@ const getOpenProjectPath = async (serviceManager: GetProjectPathProps) => { } } -export { getOpenProjectPath, getProjectPath } +const getPlcopenImportFilePath = async (serviceManager: GetProjectPathProps) => { + const { canceled, filePaths } = await dialog.showOpenDialog(serviceManager, { + title: 'Select a PLCopen XML file to import', + properties: ['openFile'], + filters: [{ name: 'PLCopen XML', extensions: ['xml'] }], + }) + if (canceled) { + return { + success: false, + error: { + title: 'Operation canceled', + description: 'Operation canceled by the user.', + }, + } + } + + const [filePath] = filePaths + + try { + const content = await promises.readFile(filePath, 'utf-8') + return { success: true, content } + } catch { + return { + success: false, + error: { + title: 'Error reading file', + description: 'Failed to read the selected PLCopen XML file.', + }, + } + } +} + +const getPlcopenExportSavePath = async (serviceManager: GetProjectPathProps, defaultFileName: string, xml: string) => { + const { canceled, filePath } = await dialog.showSaveDialog(serviceManager, { + title: 'Export PLCopen XML', + defaultPath: defaultFileName, + filters: [{ name: 'PLCopen XML', extensions: ['xml'] }], + }) + if (canceled || !filePath) { + return { + success: false, + error: { + title: 'Operation canceled', + description: 'Operation canceled by the user.', + }, + } + } + + try { + await promises.writeFile(filePath, xml, 'utf-8') + return { success: true } + } catch { + return { + success: false, + error: { + title: 'Error writing file', + description: 'Failed to write the PLCopen XML file.', + }, + } + } +} + +export { getOpenProjectPath, getPlcopenExportSavePath, getPlcopenImportFilePath, getProjectPath } diff --git a/src/backend/editor/utils/transpiler-mode.ts b/src/backend/editor/utils/transpiler-mode.ts deleted file mode 100644 index 172425990..000000000 --- a/src/backend/editor/utils/transpiler-mode.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Build-time toggle that selects between the new in-process JSON → - * Structured Text transpiler and the legacy bundled `xml2st` - * subprocess. Default is `false` (legacy xml2st) — flip with - * `OPENPLC_USE_NEW_TRANSPILER=1` to opt into the JSON-fed transpiler. - * - * Lives in `backend/editor/utils/` so every editor-side transpilation - * call site (`editor-compiler-platform-port.transpileToSt`, - * `desktop-library-build-port.transpileToSt`, - * `CompilerModule.compileForDebugger`) reads the same flag. - * - * Named without the `use` prefix on purpose: `react-hooks/rules-of-hooks` - * treats any `use*` call inside a non-component / non-hook function as - * a violation. - */ -export function isNewTranspilerEnabled(): boolean { - const v = process.env.OPENPLC_USE_NEW_TRANSPILER - return v === '1' || v === 'true' -} diff --git a/src/backend/editor/utils/xml-manager.ts b/src/backend/editor/utils/xml-manager.ts index fa2facb6b..1b81ac30a 100644 --- a/src/backend/editor/utils/xml-manager.ts +++ b/src/backend/editor/utils/xml-manager.ts @@ -4,11 +4,11 @@ import { join } from 'path' /** * Create an xml file with the given params. Synchronous on * purpose, same reason `CreateJSONFile` is: every caller chains - * the next step (running xml2st on the file) immediately after, + * the next step (reading the file) immediately after, * and the previous async-fire-and-forget form returned success * before libuv had actually flushed the bytes — a fast subsequent * spawn could read 0 bytes. The compile pipeline never observed - * this in practice because xml2st spawns slowly enough that the + * this in practice because the read happened slowly enough that the * write usually won the race, but the API contract has always * been wrong. * diff --git a/src/backend/shared/compile/__tests__/pipeline-runtime-v3.test.ts b/src/backend/shared/compile/__tests__/pipeline-runtime-v3.test.ts index e06bb12a8..2d6308f7c 100644 --- a/src/backend/shared/compile/__tests__/pipeline-runtime-v3.test.ts +++ b/src/backend/shared/compile/__tests__/pipeline-runtime-v3.test.ts @@ -10,7 +10,7 @@ * core). These tests lock that ordering in. * * Kept in a separate file from `pipeline.test.ts` (which is stale from - * the xml2st→JSON-transpiler migration and references the removed + * the XML→JSON-transpiler migration and references the removed * `transpileXmlToSt` port method) so the v3 coverage compiles + runs * against the current `transpileToSt` contract. */ diff --git a/src/backend/shared/compile/__tests__/pipeline.test.ts b/src/backend/shared/compile/__tests__/pipeline.test.ts index 170260375..06f2f3353 100644 --- a/src/backend/shared/compile/__tests__/pipeline.test.ts +++ b/src/backend/shared/compile/__tests__/pipeline.test.ts @@ -160,10 +160,8 @@ describe('runCompilePipeline — simulator path', () => { expect(result.binary).toBeInstanceOf(Uint8Array) expect(result.uploaded).toBe(false) expect(port.transpileToSt).toHaveBeenCalledTimes(1) - // The pipeline hands the transpiler the (preprocessed) project IR and a - // log callback; the port impl owns xml2st-vs-JSON backend selection and any - // format-specific flags internally (see transpiler-mode.ts). The pipeline - // stays format-agnostic — it only passes { projectData }. + // The pipeline hands the in-process transpiler the project IR plus + // a log callback — no XML / transpiler flags flow through anymore. expect(port.transpileToSt).toHaveBeenCalledWith( expect.objectContaining({ projectData: expect.anything() }), expect.any(Function), @@ -264,7 +262,7 @@ describe('runCompilePipeline — blank FBD variable guard', () => { const result = await runCompilePipeline(makeArgs({ projectData }), port, emit) expect(result.success).toBe(false) - // Validation runs before the transpile step. + // Validation runs before transpilation. expect(port.transpileToSt).not.toHaveBeenCalled() // The user-facing error names the POU and the kind of block. const validateError = events.find((e) => e.stage === 'validate' && e.level === 'error') @@ -1041,15 +1039,15 @@ describe('runCompilePipeline — side effects', () => { // lambda body uncovered — this test pins the wiring explicitly. const port = makePort({ transpileToSt: jest.fn().mockImplementation(async (_args, log) => { - log('xml2st spawned subprocess', 'info') - log('xml2st: parsed 5 POUs', 'info') + log('transpiler started', 'info') + log('transpiler: parsed 5 POUs', 'info') return { ok: true, programSt: 'PROGRAM main\nEND_PROGRAM' } }), }) const { events, emit } = captureEvents() await runCompilePipeline(makeArgs(), port, emit) const stEvents = events.filter((e) => e.stage === 'st') - expect(stEvents.some((e) => e.message === 'xml2st spawned subprocess' && e.level === 'info')).toBe(true) - expect(stEvents.some((e) => e.message === 'xml2st: parsed 5 POUs' && e.level === 'info')).toBe(true) + expect(stEvents.some((e) => e.message === 'transpiler started' && e.level === 'info')).toBe(true) + expect(stEvents.some((e) => e.message === 'transpiler: parsed 5 POUs' && e.level === 'info')).toBe(true) }) }) diff --git a/src/backend/shared/compile/__tests__/validate-empty-variables.test.ts b/src/backend/shared/compile/__tests__/validate-empty-variables.test.ts index 79be89510..b58d4b644 100644 --- a/src/backend/shared/compile/__tests__/validate-empty-variables.test.ts +++ b/src/backend/shared/compile/__tests__/validate-empty-variables.test.ts @@ -2,7 +2,7 @@ * Tests for the pre-compile blank-FBD-variable guard. * * An unnamed FBD input/output variable block becomes an empty - * `` in the PLCopen XML, which crashes xml2st with + * `` in the PLCopen XML, which the compiler rejects with * `'NoneType' object has no attribute 'split'`. These tests pin the * detector that lets the pipeline bail with a clear message — naming * what the block is wired to, or its position when it is wired to diff --git a/src/backend/shared/compile/pipeline.ts b/src/backend/shared/compile/pipeline.ts index 4e5d3fbec..c3922edf1 100644 --- a/src/backend/shared/compile/pipeline.ts +++ b/src/backend/shared/compile/pipeline.ts @@ -4,7 +4,7 @@ * Single source of truth for the full compile flow (Steps 0–13 in * the editor's canonical pipeline). Editor and web both drive this * function through a `CompilerPlatformPort`; the platform port - * abstracts the three places where platform truly differs (xml2st + * abstracts the three places where platform truly differs (ST transpiler * transport, arduino-cli transport, runtime upload transport). * Everything else — preprocessing, XML generation, strucpp compile, * conf authoring, defines authoring, bundle composition, ordering, @@ -238,7 +238,7 @@ export interface RunCompilePipelineArgs { export interface RunCompilePipelineResult { success: boolean - /** Structured strucpp + xml2st diagnostics from this run. Carries + /** Structured strucpp diagnostics from this run. Carries * the per-error events the renderer's navigation keys off. */ errors?: StructuredCompileError[] /** Compiled firmware bytes when the pipeline reached the @@ -385,10 +385,9 @@ async function runCompilePipelineInner( // --------------------------------------------------------------------- // Step 0b: Reject blank FBD variable blocks before XML generation. // - // An unnamed FBD in/out variable serialises to an empty - // ``, which makes xml2st crash with the opaque - // `'NoneType' object has no attribute 'split'`. Catch it here and - // tell the user exactly which POU to fix. + // An unnamed FBD in/out variable has no expression for the ST + // transpiler to emit, producing invalid code downstream. Catch it + // here and tell the user exactly which POU to fix. // --------------------------------------------------------------------- const emptyVariables = findEmptyFbdVariables(processedData) if (emptyVariables.length > 0) { @@ -411,7 +410,7 @@ async function runCompilePipelineInner( // 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. + // equivalents of the old struct-rewrite flags. // --------------------------------------------------------------------- emit({ stage: 'st', message: 'Generating Structured Text...', level: 'info' }) // The pipeline carries the editor's schema-shape `PLCProjectData`, diff --git a/src/backend/shared/compile/steps/generate-defines.ts b/src/backend/shared/compile/steps/generate-defines.ts index 5820ac593..ecddca33e 100644 --- a/src/backend/shared/compile/steps/generate-defines.ts +++ b/src/backend/shared/compile/steps/generate-defines.ts @@ -48,7 +48,7 @@ export interface GenerateDefinesInput { * category (`DIN` / `AIN` / `DOUT` / `AOUT`) plus matching * count defines (`NUM_DISCRETE_INPUT` etc.). */ devicePinMapping: DevicePin[] - /** Concatenated ST program content (the output of xml2st). + /** Concatenated ST program content (the output of the ST transpiler). * Scanned with `String.prototype.includes` for the marker * function-block names that toggle the Arduino-library * `USE_*_BLOCK` defines. The set of marker strings here is diff --git a/src/backend/shared/compile/steps/validate-empty-variables.ts b/src/backend/shared/compile/steps/validate-empty-variables.ts index 97b6876e0..0d6f7f95a 100644 --- a/src/backend/shared/compile/steps/validate-empty-variables.ts +++ b/src/backend/shared/compile/steps/validate-empty-variables.ts @@ -4,7 +4,7 @@ import { PLCProjectData } from '../../types/PLC/open-plc' * An FBD variable block (input or output) whose name is blank. * * Such a block serialises to an empty `` in the PLCopen - * XML, which makes xml2st abort the whole compile with the opaque + * XML, which would make the compiler abort the whole compile with the opaque * `'NoneType' object has no attribute 'split'` error. We catch it * before XML generation and report it in terms the user can act on: * what the block is wired to, falling back to its canvas position when diff --git a/src/backend/shared/ethercat/__tests__/fixtures/cia402-servo-esi.xml b/src/backend/shared/ethercat/__tests__/fixtures/cia402-servo-esi.xml new file mode 100644 index 000000000..d30146a95 --- /dev/null +++ b/src/backend/shared/ethercat/__tests__/fixtures/cia402-servo-esi.xml @@ -0,0 +1,1398 @@ + + + + #x00000B95 + oss + + + + + groupType + groupName + + + + + cia402_id + cia402_drive_name + groupType + + 402 + 2 + + + + DT1018 + 144 + + 0 + Max SubIndex + USINT + 8 + 0 + + ro + + + + 1 + Vendor ID + UDINT + 32 + 16 + + ro + + + + 2 + Product Code + UDINT + 32 + 48 + + ro + + + + 3 + Revision Number + UDINT + 32 + 80 + + ro + + + + 4 + Serial Number + UDINT + 32 + 112 + + ro + + + + + DT10F1 + 64 + + 0 + Max SubIndex + USINT + 8 + 0 + + ro + + + + 1 + Local Error Reaction + UDINT + 32 + 16 + + rw + + + + 2 + SyncErrorCounterLimit + UINT + 16 + 48 + + rw + + + + + DT1600 + 80 + + 0 + Max SubIndex + USINT + 8 + 0 + + ro + + + + 1 + Control Word + UDINT + 32 + 16 + + ro + + + + 2 + Target position + UDINT + 32 + 48 + + ro + + + + + DT1A00 + 80 + + 0 + Max SubIndex + USINT + 8 + 0 + + ro + + + + 1 + Status Word + UDINT + 32 + 16 + + ro + + + + 2 + Position actual + UDINT + 32 + 48 + + ro + + + + + DT1C00ARR + USINT + 32 + + 1 + 4 + + + + DT1C00 + 48 + + 0 + Max SubIndex + USINT + 8 + 0 + + ro + + + + Elements + DT1C00ARR + 32 + 16 + + ro + + + + + DT1C12ARR + UINT + 16 + + 1 + 1 + + + + DT1C12 + 32 + + 0 + Max SubIndex + USINT + 8 + 0 + + ro + + + + Elements + DT1C12ARR + 16 + 16 + + ro + + + + + DT1C13ARR + UINT + 16 + + 1 + 1 + + + + DT1C13 + 32 + + 0 + Max SubIndex + USINT + 8 + 0 + + ro + + + + Elements + DT1C13ARR + 16 + 16 + + ro + + + + + DT1C32 + 488 + + 0 + Max SubIndex + USINT + 8 + 0 + + ro + + + + 1 + Sync mode + UINT + 16 + 16 + + rw + + + + 2 + CycleTime + UDINT + 32 + 32 + + ro + + + + 3 + ShiftTime + UDINT + 32 + 64 + + ro + + + + 4 + Sync modes supported + UINT + 16 + 96 + + ro + + + + 5 + Minimum Cycle Time + UDINT + 32 + 112 + + ro + + + + 6 + Calc and Copy Time + UDINT + 32 + 144 + + ro + + + + 7 + Minimum Delay Time + UDINT + 32 + 176 + + ro + + + + 8 + GetCycleTime + UINT + 16 + 208 + + rw + + + + 9 + DelayTime + UDINT + 32 + 224 + + ro + + + + 10 + Sync0CycleTime + UDINT + 32 + 256 + + ro + + + + 11 + SM event missed counter + UINT + 16 + 288 + + ro + + + + 12 + CycleTimeTooSmallCnt + UINT + 16 + 304 + + ro + + + + 13 + Shift too short counter + UINT + 16 + 320 + + ro + + + + 14 + RxPDOToggleFailed + UINT + 16 + 336 + + ro + + + + 15 + Minimum Cycle Distance + UDINT + 32 + 352 + + ro + + + + 16 + Maximum Cycle Distance + UDINT + 32 + 384 + + ro + + + + 17 + Minimum SM Sync Distance + UDINT + 32 + 416 + + ro + + + + 18 + Maximum SM Sync Distance + UDINT + 32 + 448 + + ro + + + + 32 + SyncError + BOOL + 1 + 480 + + ro + + + + + DT1C33 + 488 + + 0 + Max SubIndex + USINT + 8 + 0 + + ro + + + + 1 + Sync mode + UINT + 16 + 16 + + rw + + + + 2 + CycleTime + UDINT + 32 + 32 + + ro + + + + 3 + ShiftTime + UDINT + 32 + 64 + + ro + + + + 4 + Sync modes supported + UINT + 16 + 96 + + ro + + + + 5 + Minimum Cycle Time + UDINT + 32 + 112 + + ro + + + + 6 + Calc and Copy Time + UDINT + 32 + 144 + + ro + + + + 7 + Minimum Delay Time + UDINT + 32 + 176 + + ro + + + + 8 + GetCycleTime + UINT + 16 + 208 + + rw + + + + 9 + DelayTime + UDINT + 32 + 224 + + ro + + + + 10 + Sync0CycleTime + UDINT + 32 + 256 + + ro + + + + 11 + SM event missed counter + UINT + 16 + 288 + + ro + + + + 12 + CycleTimeTooSmallCnt + UINT + 16 + 304 + + ro + + + + 13 + Shift too short counter + UINT + 16 + 320 + + ro + + + + 14 + RxPDOToggleFailed + UINT + 16 + 336 + + ro + + + + 15 + Minimum Cycle Distance + UDINT + 32 + 352 + + ro + + + + 16 + Maximum Cycle Distance + UDINT + 32 + 384 + + ro + + + + 17 + Minimum SM Sync Distance + UDINT + 32 + 416 + + ro + + + + 18 + Maximum SM Sync Distance + UDINT + 32 + 448 + + ro + + + + 32 + SyncError + BOOL + 1 + 480 + + ro + + + + + SINT + 8 + + + BOOL + 1 + + + STRING(3) + 24 + + + UDINT + 32 + + + UINT + 16 + + + USINT + 8 + + + STRING(9) + 72 + + + + + #x1000 + Device Type + UDINT + 32 + + #x00020192 + + + ro + m + + + + #x1001 + Error register + USINT + 8 + + 0 + + + ro + + + + #x1008 + Device Name + STRING(9) + 72 + + cia402_id + + + ro + + + + #x1009 + Hardware Version + STRING(3) + 24 + + 1.0 + + + ro + o + + + + #x100A + Software Version + STRING(3) + 24 + + 1.0 + + + ro + + + + #x1018 + Identity Object + DT1018 + 144 + + + Max SubIndex + + 4 + + + + Vendor ID + + #x00000B95 + + + + Product Code + + #x00020192 + + + + Revision Number + + 42 + + + + Serial Number + + #x00000000 + + + + + ro + + + + #x10F1 + ErrorSettings + DT10F1 + 64 + + + Max SubIndex + + 2 + + + + Local Error Reaction + + 0 + + + + SyncErrorCounterLimit + + 200 + + + + + ro + + + + #x1600 + Control Position + DT1600 + 80 + + + Max SubIndex + + 2 + + + + Control Word + + #x60400010 + + + + Target position + + #x607A0020 + + + + + ro + + + + #x1A00 + Status Position + DT1A00 + 80 + + + Max SubIndex + + 2 + + + + Status Word + + #x60410010 + + + + Position actual + + #x60640020 + + + + + ro + + + + #x1C00 + Sync Manager Communication Type + DT1C00 + 48 + + + Max SubIndex + + 4 + + + + Communications Type SM0 + + 1 + + + + Communications Type SM1 + + 2 + + + + Communications Type SM2 + + 3 + + + + Communications Type SM3 + + 4 + + + + + ro + + + + #x1C12 + Sync Manager 2 PDO Assignment + DT1C12 + 32 + + + Max SubIndex + + 1 + + + + PDO Mapping + + #x1600 + + + + + ro + + + + #x1C13 + Sync Manager 3 PDO Assignment + DT1C13 + 32 + + + Max SubIndex + + 1 + + + + PDO Mapping + + #x1A00 + + + + + ro + + + + #x1C32 + Sync Manager 2 Parameters + DT1C32 + 488 + + + Max SubIndex + + 32 + + + + Sync mode + + 1 + + + + CycleTime + + 0 + + + + ShiftTime + + 0 + + + + Sync modes supported + + 6 + + + + Minimum Cycle Time + + 125000 + + + + Calc and Copy Time + + 0 + + + + Minimum Delay Time + + 0 + + + + GetCycleTime + + 0 + + + + DelayTime + + 0 + + + + Sync0CycleTime + + 0 + + + + SM event missed counter + + 0 + + + + CycleTimeTooSmallCnt + + 0 + + + + Shift too short counter + + 0 + + + + RxPDOToggleFailed + + 0 + + + + Minimum Cycle Distance + + 0 + + + + Maximum Cycle Distance + + 0 + + + + Minimum SM Sync Distance + + 0 + + + + Maximum SM Sync Distance + + 0 + + + + SyncError + + 0 + + + + + ro + + + + #x1C33 + Sync Manager 3 Parameters + DT1C33 + 488 + + + Max SubIndex + + 32 + + + + Sync mode + + 1 + + + + CycleTime + + 0 + + + + ShiftTime + + 0 + + + + Sync modes supported + + 6 + + + + Minimum Cycle Time + + 125000 + + + + Calc and Copy Time + + 0 + + + + Minimum Delay Time + + 0 + + + + GetCycleTime + + 0 + + + + DelayTime + + 0 + + + + Sync0CycleTime + + 0 + + + + SM event missed counter + + 0 + + + + CycleTimeTooSmallCnt + + 0 + + + + Shift too short counter + + 0 + + + + RxPDOToggleFailed + + 0 + + + + Minimum Cycle Distance + + 0 + + + + Maximum Cycle Distance + + 0 + + + + Minimum SM Sync Distance + + 0 + + + + Maximum SM Sync Distance + + 0 + + + + SyncError + + 0 + + + + + ro + + + + #x6040 + Control Word + UINT + 16 + + 0 + + + ro + R + + + + #x6041 + Status Word + UINT + 16 + + 0 + + + ro + T + + + + #x6060 + Modes of operation + SINT + 8 + + 8 + + + rw + + + + #x6061 + Mode of operation display + SINT + 8 + + 8 + + + ro + + + + #x6064 + Position actual + UDINT + 32 + + 0 + + + ro + T + + + + #x607A + Target position + UDINT + 32 + + 0 + + + ro + R + + + + #x6502 + Supported drive modes + UDINT + 32 + + 128 + + + ro + + + + + + Outputs + Inputs + MBoxState + MBoxOut + MBoxIn + Outputs + Inputs + + #x1600 + Control Position + + #x6040 + #x0 + 16 + Control Word + UINT + + + #x607a + #x0 + 32 + Target position + UDINT + + + + #x1A00 + Status Position + + #x6041 + #x0 + 16 + Status Word + UINT + + + #x6064 + #x0 + 32 + Position actual + UDINT + + + + + + + 2048 + 800603440A000000 + 0010000200120002 + + + + + \ No newline at end of file diff --git a/src/backend/shared/ethercat/__tests__/generate-softmotion.test.ts b/src/backend/shared/ethercat/__tests__/generate-softmotion.test.ts new file mode 100644 index 000000000..f3cdb4637 --- /dev/null +++ b/src/backend/shared/ethercat/__tests__/generate-softmotion.test.ts @@ -0,0 +1,281 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' + +import type { ConfiguredEtherCATDevice } from '@root/middleware/shared/ports/esi-types' +import type { PLCProjectData } from '@root/middleware/shared/ports/types' + +import { enrichDeviceData } from '../enrich-device-data' +import { parseESIDeviceFull } from '../esi-parser-main' +import { + generateSoftMotionArtifacts, + injectAxisExternals, + SM3_BRIDGE_INSTANCE_NAME, + SM3_BRIDGE_POU_NAME, +} from '../generate-softmotion' + +const ESI_XML = readFileSync(resolve(__dirname, 'fixtures/cia402-servo-esi.xml'), 'utf-8') + +function makeDevice(name: string): ConfiguredEtherCATDevice { + const parsed = parseESIDeviceFull(ESI_XML, 0) + const enriched = enrichDeviceData(parsed.device!) + return { + id: 'dev-1', + name, + esiDeviceRef: { repositoryItemId: 'repo-1', deviceIndex: 0 }, + vendorId: '0x0', + productCode: '0x0', + revisionNo: '0x0', + addedFrom: 'repository', + config: {} as ConfiguredEtherCATDevice['config'], + ...enriched, + } +} + +function makeProject(devices: ConfiguredEtherCATDevice[]): PLCProjectData { + return { + dataTypes: [], + pous: [ + { + name: 'main', + pouType: 'program', + interface: { variables: [] }, + body: { language: 'st', value: '' }, + }, + ], + configurations: { + resource: { + tasks: [{ name: 'task0', triggering: 'Cyclic', interval: 'T#20ms', priority: 1 }], + instances: [{ name: 'instance0', task: 'task0', program: 'main' }], + globalVariables: [], + }, + }, + remoteDevices: [{ name: 'ethercat-bus', protocol: 'ethercat', ethercatConfig: { devices } }], + } +} + +describe('generateSoftMotionArtifacts', () => { + it('is a no-op when there are no CiA 402 axes', () => { + const project = makeProject([]) + expect(generateSoftMotionArtifacts(project)).toBe(project) + }) + + describe('injectAxisExternals', () => { + const prog = (value: string) => + ({ name: 'p', pouType: 'program', interface: { variables: [] }, body: { language: 'st', value } }) as never + + it('adds the external to a program that references the axis', () => { + const out = injectAxisExternals(prog('pwr(Axis := Ax);'), ['Ax']) + expect(out.interface!.variables.some((v) => v.name === 'Ax' && v.class === 'external')).toBe(true) + }) + it('leaves a POU that does not reference any axis unchanged', () => { + const pou = prog('y := 1;') + expect(injectAxisExternals(pou, ['Ax'])).toBe(pou) + }) + it('skips a function POU', () => { + const fn = { + name: 'f', + pouType: 'function', + interface: { variables: [] }, + body: { language: 'st', value: 'x := Ax;' }, + } as never + expect(injectAxisExternals(fn, ['Ax'])).toBe(fn) + }) + }) + + it('is a no-op when the CiA 402 device is disabled', () => { + const dev = makeDevice('X_Axis') + dev.cia402 = { ...dev.cia402!, enabled: false } + const project = makeProject([dev]) + expect(generateSoftMotionArtifacts(project)).toBe(project) + }) + + it('generates the AXIS_REF_SM3 global named after the device', () => { + const project = makeProject([makeDevice('X_Axis')]) + const out = generateSoftMotionArtifacts(project) + const globals = out.configurations.resource.globalVariables + const axis = globals.find((g) => g.name === 'X_Axis') + expect(axis).toBeDefined() + expect(axis!.type).toEqual({ definition: 'derived', value: 'AXIS_REF_SM3' }) + expect(axis!.location).toBe('') + }) + + it('generates located scalar globals bound to the drive PDO addresses', () => { + const project = makeProject([makeDevice('X_Axis')]) + const globals = generateSoftMotionArtifacts(project).configurations.resource.globalVariables + const ctrl = globals.find((g) => g.name === 'X_Axis_controlWord') + const status = globals.find((g) => g.name === 'X_Axis_statusWord') + const target = globals.find((g) => g.name === 'X_Axis_targetPosition') + expect(ctrl?.type.value).toBe('uint') + expect(ctrl?.location).toMatch(/^%Q/) + expect(status?.type.value).toBe('uint') + expect(status?.location).toMatch(/^%I/) + expect(target?.type.value).toBe('dint') // forced to DINT for bridge compatibility + expect(target?.location).toMatch(/^%Q/) + }) + + it('generates a bridge program with an SM_Drive instance and pin bindings', () => { + const project = makeProject([makeDevice('X_Axis')]) + const out = generateSoftMotionArtifacts(project) + const bridge = out.pous.find((p) => p.name === SM3_BRIDGE_POU_NAME) + expect(bridge).toBeDefined() + expect(bridge!.pouType).toBe('program') + const fbVar = bridge!.interface!.variables.find((v) => v.name === 'X_Axis_drive') + expect(fbVar!.type).toEqual({ definition: 'derived', value: 'SM_Drive_GenericDS402' }) + const body = bridge!.body.value as string + // input pins bound with :=, output pins captured with => + expect(body).toContain('Axis := X_Axis') + expect(body).toContain('wStatusWord := X_Axis_statusWord') + expect(body).toContain('diActualPosition := X_Axis_positionActual') + expect(body).toContain('wControlWord => X_Axis_controlWord') + expect(body).toContain('diTargetPosition => X_Axis_targetPosition') + // scaling applied + expect(body).toContain('X_Axis.fScalefactor :=') + }) + + it('injects VAR_EXTERNAL for the axis into a user program that references it', () => { + const project = makeProject([makeDevice('X_Axis')]) + project.pous[0].body = { language: 'st', value: 'pwr(Axis := X_Axis, Enable := TRUE);' } + const out = generateSoftMotionArtifacts(project) + const main = out.pous.find((p) => p.name === 'main')! + const ext = main.interface!.variables.find((v) => v.name === 'X_Axis') + expect(ext).toBeDefined() + expect(ext!.class).toBe('external') + expect(ext!.type).toEqual({ definition: 'derived', value: 'AXIS_REF_SM3' }) + }) + + it('does not double-declare an axis the user already declared', () => { + const project = makeProject([makeDevice('X_Axis')]) + project.pous[0].interface = { + variables: [ + { + name: 'X_Axis', + class: 'external', + type: { definition: 'derived', value: 'AXIS_REF_SM3' }, + location: '', + documentation: '', + }, + ], + } + project.pous[0].body = { language: 'st', value: 'pwr(Axis := X_Axis);' } + const out = generateSoftMotionArtifacts(project) + const main = out.pous.find((p) => p.name === 'main')! + expect(main.interface!.variables.filter((v) => v.name === 'X_Axis')).toHaveLength(1) + }) + + it('detects axis references in graphical (non-string) POU bodies', () => { + const project = makeProject([makeDevice('X_Axis')]) + // A function POU is left untouched; a graphical program referencing the axis gets the external. + project.pous[0].body = { language: 'fbd', value: { rung: { nodes: [{ variable: 'X_Axis' }] } } } as never + const out = generateSoftMotionArtifacts(project) + const main = out.pous.find((p) => p.name === 'main')! + expect(main.interface!.variables.some((v) => v.name === 'X_Axis' && v.class === 'external')).toBe(true) + }) + + it('injects the axis external into a function block that references it', () => { + const project = makeProject([makeDevice('X_Axis')]) + project.pous.push({ + name: 'MotionFB', + pouType: 'function-block', + interface: { variables: [] }, + body: { language: 'st', value: 'x := X_Axis.fActPosition;' }, + }) + const out = generateSoftMotionArtifacts(project) + const fb = out.pous.find((p) => p.name === 'MotionFB')! + const ext = fb.interface!.variables.find((v) => v.name === 'X_Axis') + expect(ext?.class).toBe('external') + expect(ext?.type).toEqual({ definition: 'derived', value: 'AXIS_REF_SM3' }) + }) + + it('leaves functions untouched (they cannot hold VAR_EXTERNAL)', () => { + const project = makeProject([makeDevice('X_Axis')]) + project.pous.push({ + name: 'helperFn', + pouType: 'function', + interface: { variables: [] }, + body: { language: 'st', value: 'x := X_Axis.fActPosition;' }, + }) + const out = generateSoftMotionArtifacts(project) + const fn = out.pous.find((p) => p.name === 'helperFn')! + expect(fn.interface!.variables.some((v) => v.name === 'X_Axis')).toBe(false) + }) + + it('runs the bridge first each scan (instance unshifted to the front)', () => { + const project = makeProject([makeDevice('X_Axis')]) + const instances = generateSoftMotionArtifacts(project).configurations.resource.instances + expect(instances[0].name).toBe(SM3_BRIDGE_INSTANCE_NAME) + expect(instances[0].program).toBe(SM3_BRIDGE_POU_NAME) + expect(instances[0].task).toBe('task0') + expect(instances.map((i) => i.name)).toContain('instance0') + }) + + it('honors configured scaling in the generated bridge', () => { + const dev = makeDevice('X_Axis') + dev.cia402 = { enabled: true, scaleNum: 1, scaleDenom: 1, scaleFactor: 1000 } + const out = generateSoftMotionArtifacts(makeProject([dev])) + const body = out.pous.find((p) => p.name === SM3_BRIDGE_POU_NAME)!.body.value as string + expect(body).toContain('X_Axis.fScalefactor := 1000.0;') + }) + + describe('edge cases', () => { + it('ignores non-ethercat remote devices', () => { + const project = makeProject([]) + project.remoteDevices = [{ name: 'mb', protocol: 'modbus-tcp' }] + expect(generateSoftMotionArtifacts(project)).toBe(project) + }) + + it('handles a remote device with no ethercatConfig', () => { + const project = makeProject([]) + project.remoteDevices = [{ name: 'ec', protocol: 'ethercat' }] + expect(generateSoftMotionArtifacts(project)).toBe(project) + }) + + it('skips an enabled device whose mandatory objects are unmapped', () => { + const dev = makeDevice('X_Axis') + dev.channelMappings = [] // nothing resolves -> no controlWord/statusWord + const project = makeProject([dev]) + expect(generateSoftMotionArtifacts(project)).toBe(project) + }) + + it('skips a device whose channelInfo is absent', () => { + const dev = makeDevice('X_Axis') + dev.channelInfo = undefined + const project = makeProject([dev]) + expect(generateSoftMotionArtifacts(project)).toBe(project) + }) + + it('deduplicates axes that sanitize to the same identifier', () => { + const a = makeDevice('X Axis') + const b = makeDevice('X_Axis') // both -> X_Axis + a.id = 'a' + b.id = 'b' + const out = generateSoftMotionArtifacts(makeProject([a, b])) + const axisGlobals = out.configurations.resource.globalVariables.filter((g) => g.name === 'X_Axis') + expect(axisGlobals).toHaveLength(1) + }) + + it('handles a project with no remoteDevices field', () => { + const project = makeProject([]) + delete project.remoteDevices + expect(generateSoftMotionArtifacts(project)).toBe(project) + }) + + it('emits fractional scale factors verbatim', () => { + const dev = makeDevice('X_Axis') + dev.cia402 = { enabled: true, scaleNum: 1, scaleDenom: 1, scaleFactor: 0.5 } + const out = generateSoftMotionArtifacts(makeProject([dev])) + const body = out.pous.find((p) => p.name === SM3_BRIDGE_POU_NAME)!.body.value as string + expect(body).toContain('X_Axis.fScalefactor := 0.5;') + }) + + it('creates a fallback cyclic task when the resource has none', () => { + const project = makeProject([makeDevice('X_Axis')]) + project.configurations.resource.tasks = [] + project.configurations.resource.instances = [] + const out = generateSoftMotionArtifacts(project) + expect(out.configurations.resource.tasks).toHaveLength(1) + expect(out.configurations.resource.tasks[0].triggering).toBe('Cyclic') + expect(out.configurations.resource.instances[0].task).toBe(out.configurations.resource.tasks[0].name) + }) + }) +}) diff --git a/src/backend/shared/ethercat/enrich-device-data.ts b/src/backend/shared/ethercat/enrich-device-data.ts index 7d04d2eab..2751b0116 100644 --- a/src/backend/shared/ethercat/enrich-device-data.ts +++ b/src/backend/shared/ethercat/enrich-device-data.ts @@ -14,6 +14,11 @@ import type { PersistedPdoEntry, SDOConfigurationEntry, } from '@root/middleware/shared/ports/esi-types' +import { + type Cia402AxisConfig, + DEFAULT_CIA402_AXIS_CONFIG, + isCia402Drive, +} from '@root/middleware/shared/utils/ethercat' import { esiTypeToIecType, generateDefaultChannelMappings, pdoToChannels } from './esi-parser' import { extractDefaultSdoConfigurations } from './sdo-config-defaults' @@ -113,6 +118,7 @@ export function enrichDeviceData( slaveType: string sdoConfigurations?: SDOConfigurationEntry[] channelMappings: EtherCATChannelMapping[] + cia402?: Cia402AxisConfig } { return { channelInfo: buildChannelInfo(device), @@ -121,5 +127,8 @@ export function enrichDeviceData( slaveType: deriveSlaveType(device), sdoConfigurations: device.coeObjects?.length ? extractDefaultSdoConfigurations(device.coeObjects) : undefined, channelMappings: generateDefaultChannelMappings(pdoToChannels(device), usedAddresses), + // A CiA 402 servo is auto-recognized as a SoftMotion axis; the user can + // disable/tune it in the device's Axis configuration. + cia402: isCia402Drive(device) ? { ...DEFAULT_CIA402_AXIS_CONFIG } : undefined, } } diff --git a/src/backend/shared/ethercat/generate-softmotion.ts b/src/backend/shared/ethercat/generate-softmotion.ts new file mode 100644 index 000000000..5d2465fb5 --- /dev/null +++ b/src/backend/shared/ethercat/generate-softmotion.ts @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2026 Autonomy / OpenPLC Project +/** + * Compile-time SoftMotion code generation. + * + * Turns each CiA 402 EtherCAT drive (recognized by + * middleware/shared/utils/ethercat/cia402.ts, opted-in via its + * Cia402AxisConfig) into the ST glue that lets CODESYS-style application code + * run unmodified. Axis discovery/naming (`collectAxes`, `sanitizeAxisName`, …) + * lives in middleware/shared/utils/ethercat/softmotion-axis-naming.ts — this + * file only owns the codegen that the discovery feeds into: + * + * - an AXIS_REF_SM3 global named after the device (so `MC_Power(Axis := X_Axis)` + * resolves directly to the drive), + * - a located scalar global per mapped CiA 402 PDO object, bound to the + * editor-allocated %I/%Q address, + * - a per-scan `__sm3_bridge` PROGRAM (run first each cycle) that calls + * SM_Drive_GenericDS402 to marshal the PDO image <-> the axis and apply the + * configured scaling. + * + * The user never maps an address: the EtherCAT device's name IS the axis name. + * Called from preprocessPous so every compile path (build/download/deploy/debug) + * gets the generated artifacts. Pure: returns a new PLCProjectData, never + * mutates the input. + */ + +import type { PLCInstance, PLCPou, PLCProjectData, PLCTask, PLCVariable } from '@root/middleware/shared/ports/types' +import { collectAxes } from '@root/middleware/shared/utils/ethercat' + +export const SM3_BRIDGE_POU_NAME = '__sm3_bridge' +export const SM3_BRIDGE_INSTANCE_NAME = '__sm3_bridge_inst' +const SM3_FALLBACK_TASK: PLCTask = { + name: '__sm3_task', + triggering: 'Cyclic', + interval: 'T#10ms', + priority: 0, +} + +function lrealLiteral(n: number): string { + return Number.isInteger(n) ? `${n}.0` : `${n}` +} + +function global(name: string, typeValue: string, definition: 'base-type' | 'derived', location: string): PLCVariable { + return { name, type: { definition, value: typeValue }, location, documentation: '' } +} + +/** A VAR_EXTERNAL declaration referencing a configuration global. */ +function external(name: string, typeValue: string, definition: 'base-type' | 'derived'): PLCVariable { + return { name, class: 'external', type: { definition, value: typeValue }, location: '', documentation: '' } +} + +/** A POU-local variable (e.g. the bridge FB instance). */ +function local(name: string, typeValue: string, definition: 'base-type' | 'derived'): PLCVariable { + return { name, class: 'local', type: { definition, value: typeValue }, location: '', documentation: '' } +} + +/** True when a POU body (ST text or a serialized graphical body) references `identifier`. */ +function bodyReferences(bodyValue: unknown, identifier: string): boolean { + const text = typeof bodyValue === 'string' ? bodyValue : JSON.stringify(bodyValue ?? '') + return new RegExp(`\\b${identifier}\\b`).test(text) +} + +/** POU types that may access a SoftMotion axis global via VAR_EXTERNAL. Functions + * are stateless and can't hold VAR_EXTERNAL, so they're excluded. */ +const AXIS_EXTERNAL_POU_TYPES = new Set(['program', 'function-block']) + +/** + * Inject a `VAR_EXTERNAL : AXIS_REF_SM3` into `pou` for every axis in + * `axisNames` its body references but hasn't already declared — so + * `MC_*(Axis := )` resolves without the user declaring the global. + * Returns the POU unchanged when nothing applies. Programs and function blocks + * only: strucpp requires a VAR_EXTERNAL to touch a global, and both POU kinds + * support it (a function can't). Shared by the compiler and the language server + * so the editor sees exactly what the compiler generates. + */ +export function injectAxisExternals(pou: PLCPou, axisNames: string[]): PLCPou { + if (!AXIS_EXTERNAL_POU_TYPES.has(pou.pouType)) return pou + const declared = new Set((pou.interface?.variables ?? []).map((v) => v.name.toUpperCase())) + const toAdd = axisNames + .filter((name) => !declared.has(name.toUpperCase()) && bodyReferences(pou.body.value, name)) + .map((name) => external(name, 'AXIS_REF_SM3', 'derived')) + if (toAdd.length === 0) return pou + return { + ...pou, + interface: { ...pou.interface, variables: [...(pou.interface?.variables ?? []), ...toAdd] }, + } +} + +/** + * Inject generated SoftMotion globals + the per-scan bridge program for every + * CiA 402 axis in the project. No-op (returns the input) when there are none. + */ +export function generateSoftMotionArtifacts(project: PLCProjectData): PLCProjectData { + const axes = collectAxes(project) + if (axes.length === 0) return project + + const newGlobals: PLCVariable[] = [] + const bridgeVars: PLCVariable[] = [] + const bodyLines: string[] = [] + + for (const axis of axes) { + // AXIS_REF_SM3 instance (the name used in MC_*(Axis := ...)) — a config + // global; the bridge reaches it via VAR_EXTERNAL. + newGlobals.push(global(axis.axisName, 'AXIS_REF_SM3', 'derived', '')) + bridgeVars.push(external(axis.axisName, 'AXIS_REF_SM3', 'derived')) + + bodyLines.push(`(* ---- SoftMotion axis ${axis.axisName} ---- *)`) + // Apply configured scaling each scan (device config is authoritative). + bodyLines.push(`${axis.axisName}.iRatioTechUnitsNum := DINT#${Math.trunc(axis.scaleNum)};`) + bodyLines.push(`${axis.axisName}.dwRatioTechUnitsDenom := DWORD#${Math.trunc(axis.scaleDenom)};`) + bodyLines.push(`${axis.axisName}.fScalefactor := ${lrealLiteral(axis.scaleFactor)};`) + + const inBinds: string[] = [] + const outBinds: string[] = [] + for (const obj of axis.objects) { + const iecType = obj.binding.iecType.toLowerCase() + // located scalar global bound to the drive PDO address... + newGlobals.push(global(obj.scalarName, iecType, 'base-type', obj.iecLocation)) + // ...and the bridge's VAR_EXTERNAL view of it. + bridgeVars.push(external(obj.scalarName, iecType, 'base-type')) + if (obj.binding.pinKind === 'input') inBinds.push(`${obj.binding.pin} := ${obj.scalarName}`) + else outBinds.push(`${obj.binding.pin} => ${obj.scalarName}`) + } + + const fbInstance = `${axis.axisName}_drive` + bridgeVars.push(local(fbInstance, 'SM_Drive_GenericDS402', 'derived')) + const binds = [`Axis := ${axis.axisName}`, ...inBinds, 'bOnline := TRUE', ...outBinds] + bodyLines.push(`${fbInstance}(`) + bodyLines.push(`\t${binds.join(',\n\t')});`) + } + + const bridgePou: PLCPou = { + name: SM3_BRIDGE_POU_NAME, + pouType: 'program', + interface: { variables: bridgeVars }, + body: { language: 'st', value: bodyLines.join('\n') }, + documentation: 'Auto-generated SoftMotion drive bridge — do not edit; regenerated each compile.', + } + + // Inject a VAR_EXTERNAL for each axis into user programs and function blocks + // that reference it, so `MC_*(Axis := X_Axis)` resolves without the user + // declaring the global. + const axisNames = axes.map((a) => a.axisName) + const patchedPous = project.pous.map((pou) => injectAxisExternals(pou, axisNames)) + + const resource = project.configurations.resource + // Ensure a task exists to run the bridge, then attach the bridge instance at + // the FRONT of the instance list so it runs before user POUs each scan + // (fresh PDO feedback in, commands out). + const tasks = resource.tasks.length > 0 ? resource.tasks : [SM3_FALLBACK_TASK] + const bridgeInstance: PLCInstance = { + name: SM3_BRIDGE_INSTANCE_NAME, + task: tasks[0].name, + program: SM3_BRIDGE_POU_NAME, + } + + return { + ...project, + pous: [...patchedPous, bridgePou], + configurations: { + ...project.configurations, + resource: { + ...resource, + tasks, + globalVariables: [...resource.globalVariables, ...newGlobals], + instances: [bridgeInstance, ...resource.instances], + }, + }, + } +} diff --git a/src/backend/shared/firmware/__tests__/runtime-version-gate.test.ts b/src/backend/shared/firmware/__tests__/runtime-version-gate.test.ts index 1686f416a..3f8733187 100644 --- a/src/backend/shared/firmware/__tests__/runtime-version-gate.test.ts +++ b/src/backend/shared/firmware/__tests__/runtime-version-gate.test.ts @@ -1,10 +1,42 @@ import { describeIncompatibleRuntime, isStrucppCompatibleRuntime, + isUserManagementCapableRuntime, MIN_STRUCPP_RUNTIME_VERSION, + MIN_USER_MANAGEMENT_RUNTIME_VERSION, parseRuntimeVersion, } from '../runtime-version-gate' +describe('isUserManagementCapableRuntime', () => { + it('is exposed with the documented minimum version', () => { + expect(MIN_USER_MANAGEMENT_RUNTIME_VERSION).toBe('4.1.9') + }) + + it('accepts v4.1.9 and newer', () => { + expect(isUserManagementCapableRuntime('v4.1.9')).toBe(true) + expect(isUserManagementCapableRuntime('4.1.10')).toBe(true) + expect(isUserManagementCapableRuntime('v4.2.0')).toBe(true) + expect(isUserManagementCapableRuntime('v5.0.0')).toBe(true) + }) + + it('accepts a pre-release on the target patch', () => { + expect(isUserManagementCapableRuntime('v4.1.9-rc.1')).toBe(true) + }) + + it('rejects versions older than 4.1.9', () => { + expect(isUserManagementCapableRuntime('v4.1.8')).toBe(false) + expect(isUserManagementCapableRuntime('v4.0.9')).toBe(false) + expect(isUserManagementCapableRuntime('v3.9.9')).toBe(false) + }) + + it('rejects unparseable / legacy version strings', () => { + expect(isUserManagementCapableRuntime('v4')).toBe(false) + expect(isUserManagementCapableRuntime('dev')).toBe(false) + expect(isUserManagementCapableRuntime(null)).toBe(false) + expect(isUserManagementCapableRuntime(undefined)).toBe(false) + }) +}) + describe('parseRuntimeVersion', () => { it('parses tagged release versions (with and without leading v)', () => { expect(parseRuntimeVersion('v4.1.0')).toEqual({ major: 4, minor: 1, patch: 0 }) diff --git a/src/backend/shared/firmware/runtime-version-gate.ts b/src/backend/shared/firmware/runtime-version-gate.ts index ca96ca6ca..179010a1a 100644 --- a/src/backend/shared/firmware/runtime-version-gate.ts +++ b/src/backend/shared/firmware/runtime-version-gate.ts @@ -75,6 +75,26 @@ export function isStrucppCompatibleRuntime(raw: string | null | undefined): bool return v.minor >= 1 } +/** Minimum runtime version that ships the user-management API + * (roles, whoami, unified update-user, delete/last-admin guards). */ +export const MIN_USER_MANAGEMENT_RUNTIME_VERSION = '4.1.9' + +/** + * Returns true iff the runtime version string represents a runtime + * that ships the user-management API (≥ 4.1.9). Older runtimes lack + * `whoami` / `update-user` and the RBAC guards, so the editor hides + * the User Management screen for them. Pre-release tags on the target + * patch (e.g. `v4.1.9-rc.1`) count as capable, matching the strucpp + * gate's treatment of the rc lineage. + */ +export function isUserManagementCapableRuntime(raw: string | null | undefined): boolean { + const v = parseRuntimeVersion(raw) + if (!v) return false + if (v.major !== 4) return v.major > 4 + if (v.minor !== 1) return v.minor > 1 + return v.patch >= 9 +} + /** * Human-readable explanation suitable for surfacing as an error * when the gate rejects a runtime. The reported version (or diff --git a/src/backend/shared/library/__tests__/build-pipeline.test.ts b/src/backend/shared/library/__tests__/build-pipeline.test.ts index fa0f37d12..178c37655 100644 --- a/src/backend/shared/library/__tests__/build-pipeline.test.ts +++ b/src/backend/shared/library/__tests__/build-pipeline.test.ts @@ -2,7 +2,7 @@ * Tests for the library build pipeline. * * `prepareXmlForLibraryBuild` no longer generates PLCopen XML — the - * old xml2st flow was replaced by an in-process JSON → ST transpiler. + * the legacy XML→ST flow was replaced by an in-process JSON → ST transpiler. * The function now only validates the manifest and returns the stubbed * project data (plus the POU inventory the splitter needs); the actual * transpile happens later via `LibraryBuildPort.transpileToSt`. @@ -422,7 +422,7 @@ describe('libraryBuildFromTranspiledSt', () => { it('drops `_config.st` so strucpp does not error on the stub configuration', () => { // The stub program (which the splitter recognises and the - // pipeline drops) is referenced by xml2st's emitted + // pipeline drops) is referenced by the transpiler's emitted // CONFIGURATION block. Leaving `_config.st` in the strucpp // inputs makes strucpp emit "Unknown program type 'MAIN'" // diagnostics because the stub source isn't there anymore. @@ -637,7 +637,7 @@ describe('libraryBuildFromTranspiledSt', () => { expect(res.success).toBe(true) }) - it('matches POU docs case-insensitively (xml2st upper-cases identifiers)', () => { + it('matches POU docs case-insensitively (the transpiler upper-cases identifiers)', () => { const archive = { manifest: { name: 'demo_lib', diff --git a/src/backend/shared/library/__tests__/program-build-helpers.test.ts b/src/backend/shared/library/__tests__/program-build-helpers.test.ts index 44bdf3684..4341fc156 100644 --- a/src/backend/shared/library/__tests__/program-build-helpers.test.ts +++ b/src/backend/shared/library/__tests__/program-build-helpers.test.ts @@ -235,7 +235,7 @@ describe('enrichErrorWithPouContext', () => { }) it('skips blank separator lines between END_VAR and the body', () => { - // The ST generators (`pou-text-serializer.ts` and xml2st on the + // The ST generators (`pou-text-serializer.ts` and the ST transpiler on the // compile path) insert blank lines after END_VAR for readability. // Those blanks live in the per-POU file the splitter feeds // strucpp but NOT in `pou.body.value`, which is what the body diff --git a/src/backend/shared/library/build-pipeline.ts b/src/backend/shared/library/build-pipeline.ts index 622eff878..8d026e6f0 100644 --- a/src/backend/shared/library/build-pipeline.ts +++ b/src/backend/shared/library/build-pipeline.ts @@ -7,15 +7,14 @@ * 1. `prepareXmlForLibraryBuild(project, manifest)` — synthesizes * a stub main program / task / instance into a transient * PLCProject (the on-disk project remains untouched) and runs - * the canonical XmlGenerator on it. xml2st rejects programless + * the canonical XmlGenerator on it. the ST transpiler rejects programless * projects, so the stub is mandatory; the stub's POU body is * intentionally non-empty (`LocalVar := 3;` against a single - * INT local) because some xml2st codepaths also reject empty + * INT local) because some ST-transpiler codepaths also reject empty * program bodies. * - * 2. *(caller runs xml2st on the resulting plc.xml — Electron - * spawns a local binary, web backend posts to its xml2st - * service — produces `program.st`.)* + * 2. *(the in-process ST transpiler runs on the project and + * produces `program.st`.)* * * 3. `libraryBuildFromTranspiledSt(programSt, knownPous, manifest)` * — splits `program.st` per-POU via the shared splitter, drops @@ -74,8 +73,8 @@ export interface LibraryBuildManifest { * console can render through the existing diagnostic pipeline. * * Strucpp itself validates manifests during compile, but doing it - * here lets the build fail BEFORE running xml2st when the manifest - * is obviously broken — saves a slow xml2st spawn on every + * here lets the build fail early when the manifest + * is obviously broken — saves wasted transpile work on every * mis-edited save. */ function parseLibraryManifest(json: string): ManifestParseResult { @@ -128,7 +127,7 @@ function parseLibraryManifest(json: string): ManifestParseResult { } // --------------------------------------------------------------------------- -// Stub program — makes xml2st accept a programless library project +// Stub program — makes the ST transpiler accept a programless library project // --------------------------------------------------------------------------- /** @@ -147,14 +146,14 @@ const STUB_INSTANCE_NAME = '__openplc_library_stub_instance__' /** * Build a transient PLCProject with a stub main program added on * top of the library's POUs / data types. The stub is what - * satisfies xml2st (and strucpp's main-program assumption later in + * satisfies the ST transpiler (and strucpp's main-program assumption later in * the verification path). Caller drops the stub's per-POU output * before handing the remaining sources to compileStlib. * - * The stub's body is non-empty (`LocalVar := 3;`) because xml2st + * The stub's body is non-empty (`LocalVar := 3;`) because the ST transpiler * has been observed to reject programs with completely empty bodies * — a single trivial assignment + a single INT local is the smallest - * shape that gets accepted across every xml2st version. + * shape that gets accepted across transpiler versions. */ function stubProgramFor(project: PLCProject): PLCProject { return { @@ -204,14 +203,14 @@ function stubProgramFor(project: PLCProject): PLCProject { * Synthetic filename the splitter emits for the stub program. The * splitter writes its file keys using the caller-side POU name * verbatim (case preserved), so this matches what `splitProgramSt` - * returns regardless of how xml2st upper-cases identifiers in the + * returns regardless of how the transpiler upper-cases identifiers in the * monolithic ST output. Caller drops this entry before feeding the * rest to compileStlib. */ const STUB_SPLIT_FILENAME = `${STUB_PROGRAM_NAME}.st` // --------------------------------------------------------------------------- -// Stage 1: pre-xml2st (pure) +// Stage 1: pre-transpile (pure) // --------------------------------------------------------------------------- export interface PrepareXmlResult { @@ -257,7 +256,7 @@ export function prepareXmlForLibraryBuild(project: PLCProject, manifestJson: str } // --------------------------------------------------------------------------- -// Stage 2: post-xml2st (pure) +// Stage 2: post-transpile (pure) // --------------------------------------------------------------------------- export interface LibraryBuildResult { @@ -337,7 +336,7 @@ export interface LibraryBuildAux { } /** - * Stage 2. Given xml2st's monolithic `program.st`, the POU + * Stage 2. Given the transpiler's monolithic `program.st`, the POU * inventory from Stage 1, and the parsed manifest: split program.st * per-POU, drop the stub, hand the remaining sources to strucpp's * compileStlib. @@ -369,7 +368,7 @@ export function libraryBuildFromTranspiledSt( // // - The stub program's `.st` file (the library doesn't ship // the stub). - // - `_config.st` (xml2st's CONFIGURATION block references the + // - `_config.st` (the transpiler's CONFIGURATION block references the // stub program, which we've just removed — leaving it in // causes strucpp to emit "Unknown program type 'MAIN'" // diagnostics). Libraries don't carry configurations diff --git a/src/backend/shared/library/program-build-helpers.ts b/src/backend/shared/library/program-build-helpers.ts index 84ad74fd2..0a03a0e56 100644 --- a/src/backend/shared/library/program-build-helpers.ts +++ b/src/backend/shared/library/program-build-helpers.ts @@ -119,7 +119,7 @@ export function enrichErrorWithPouContext( if (/^\s*END_VAR\b/i.test(lines[i])) lastEndVar = i + 1 // 1-indexed } // Body starts after the last END_VAR, but the ST generator - // (`pou-text-serializer.ts` and xml2st on the compile path) + // (`pou-text-serializer.ts` and the ST transpiler on the compile path) // inserts blank separator lines between END_VAR and the body // content. Those blank lines exist in the per-POU file the // splitter handed strucpp but NOT in `pou.body.value`, which is diff --git a/src/backend/shared/library/program-build-pipeline.ts b/src/backend/shared/library/program-build-pipeline.ts index d72429616..4b2e554b8 100644 --- a/src/backend/shared/library/program-build-pipeline.ts +++ b/src/backend/shared/library/program-build-pipeline.ts @@ -28,7 +28,7 @@ * pre-computes the hash and passes it in; strucpp embeds it * into the debug map for stale-layout detection. * - * - No external-process orchestration — `xml2st` (XML→ST) and + * - No external-process orchestration — the ST transpiler and * `arduino-cli` (firmware compile) stay in the platform-specific * orchestrator that wraps this pipeline. This module is purely * about the strucpp invocation slice. diff --git a/src/backend/shared/transpilers/st-transpiler/__tests__/block-output-ordering-830.test.ts b/src/backend/shared/transpilers/st-transpiler/__tests__/block-output-ordering-830.test.ts new file mode 100644 index 000000000..8f0d6b29a --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/__tests__/block-output-ordering-830.test.ts @@ -0,0 +1,203 @@ +import { emitLdBody } from '@root/backend/shared/transpilers/st-transpiler/walker/ld' +import type { RFBody, RFEdge, RFNode } from '@root/backend/shared/transpilers/st-transpiler/walker/types' + +// Regression coverage for issue #830 — when function calls are chained in +// a rung, each block's output assignment must emit immediately after the +// call, so a downstream block reads the written value and not a stale one. + +let edgeId = 0 +const e = (s: string, t: string, sh: string | null, th: string | null): RFEdge => ({ + id: `e${edgeId++}`, + source: s, + target: t, + sourceHandle: sh, + targetHandle: th, +}) +const rail = (id: string, variant: 'left' | 'right', x: number): RFNode => ({ + id, + type: 'powerRail', + position: { x, y: 30 }, + data: { variant }, +}) +const inVar = (id: string, name: string, x: number, y: number): RFNode => ({ + id, + type: 'variable', + position: { x, y }, + data: { variant: 'input', variable: { name }, executionOrder: 0, numericId: id }, +}) +const outVar = (id: string, name: string, x: number, y: number): RFNode => ({ + id, + type: 'variable', + position: { x, y }, + data: { variant: 'output', variable: { name }, executionOrder: 0, numericId: id }, +}) +const coil = (id: string, name: string, x: number, y: number): RFNode => ({ + id, + type: 'coil', + position: { x, y }, + data: { variant: 'default', variable: { name }, executionOrder: 0, numericId: id }, +}) +const fblock = (id: string, name: string, nid: string, eo: number, x: number, y: number, outType = 'DINT'): RFNode => ({ + id, + type: 'block', + position: { x, y }, + data: { + numericId: nid, + executionOrder: eo, + executionControl: true, + variant: { + name, + type: 'function', + extensible: false, + variables: [ + { name: 'EN', class: 'input', type: { definition: 'generic-type', value: 'BOOL' } }, + { name: 'ENO', class: 'output', type: { definition: 'generic-type', value: 'BOOL' } }, + { name: 'OUT', class: 'output', type: { definition: 'base-type', value: outType } }, + { name: 'IN1', class: 'input', type: { definition: 'base-type', value: 'DINT' } }, + { name: 'IN2', class: 'input', type: { definition: 'base-type', value: 'DINT' } }, + ], + }, + }, +}) + +const call = (lhs: string, rhs: string): string => ` ${lhs} := ${rhs};\n` +const enoIf = (eno: string, v: string, val: string): string => ` IF ${eno} THEN\n ${v} := ${val};\n END_IF;\n` +const asg = (v: string, val: string): string => ` ${v} := ${val};\n` + +describe('issue 830 — chained block output assignments emit before downstream calls', () => { + it('writes a block output before the next block reads it (reported MOD->DIV chain)', () => { + // MOD(EN:=TRUE) -> Output ; DIV(EN:=MOD.ENO, IN1:=Output) -> Output. Blocks numbered, writes at eo 0. + const body: RFBody = { + rungs: [ + { + reactFlowViewport: [800, 300], + nodes: [ + rail('L', 'left', 0), + inVar('901', 'Input', 20, 80), + inVar('902', '86400', 20, 120), + fblock('MOD', 'MOD', '6363443', 1, 150, 30), + outVar('903', 'Output', 320, 30), + inVar('904', 'Output', 20, 200), + inVar('905', '60', 20, 240), + fblock('DIV', 'DIV', '4651235', 2, 450, 30), + outVar('906', 'Output', 620, 30), + rail('R', 'right', 760), + ], + edges: [ + e('L', 'MOD', 'left-rail', 'EN'), + e('901', 'MOD', null, 'IN1'), + e('902', 'MOD', null, 'IN2'), + e('MOD', '903', 'OUT', null), + e('MOD', 'DIV', 'ENO', 'EN'), + e('904', 'DIV', null, 'IN1'), + e('905', 'DIV', null, 'IN2'), + e('DIV', '906', 'OUT', null), + ], + }, + ], + } + const { bodySt, warnings } = emitLdBody(body) + expect(warnings).toEqual([]) + expect(bodySt).toBe( + '\n' + + call('_TMP_MOD6363443_OUT', 'MOD(EN := TRUE, IN1 := Input, IN2 := 86400, ENO => _TMP_MOD6363443_ENO)') + + enoIf('_TMP_MOD6363443_ENO', 'Output', '_TMP_MOD6363443_OUT') + + call( + '_TMP_DIV4651235_OUT', + 'DIV(EN := _TMP_MOD6363443_ENO, IN1 := Output, IN2 := 60, ENO => _TMP_DIV4651235_ENO)', + ) + + enoIf('_TMP_DIV4651235_ENO', 'Output', '_TMP_DIV4651235_OUT'), + ) + }) + + it('interleaves output writes for blocks pulled eagerly through a later block EN', () => { + // MUL -> producto ; SUB(IN1:=producto) -> dif ; GT(EN := MUL.ENO OR SUB.ENO) -> flag. + // No execution order: GT.EN pulls MUL and SUB; their writes must still land between the calls. + const body: RFBody = { + rungs: [ + { + reactFlowViewport: [800, 300], + nodes: [ + rail('L', 'left', 0), + inVar('901', 'valor', 20, 80), + inVar('902', 'k', 20, 120), + fblock('MUL', 'MUL', '111', 0, 150, 30), + outVar('903', 'producto', 320, 200), + inVar('904', 'producto', 20, 160), + inVar('905', 'k2', 20, 200), + fblock('SUB', 'SUB', '222', 0, 150, 130), + outVar('906', 'dif', 320, 260), + fblock('GT', 'GT', '333', 0, 500, 30, 'BOOL'), + outVar('907', 'flag', 660, 30), + rail('R', 'right', 760), + ], + edges: [ + e('L', 'MUL', 'left-rail', 'EN'), + e('901', 'MUL', null, 'IN1'), + e('902', 'MUL', null, 'IN2'), + e('MUL', '903', 'OUT', null), + e('904', 'SUB', null, 'IN1'), + e('905', 'SUB', null, 'IN2'), + e('SUB', '906', 'OUT', null), + e('MUL', 'GT', 'ENO', 'EN'), + e('SUB', 'GT', 'ENO', 'EN'), + e('GT', '907', 'OUT', null), + ], + }, + ], + } + const { bodySt, warnings } = emitLdBody(body) + expect(warnings).toEqual([]) + expect(bodySt).toBe( + '\n' + + call('_TMP_MUL111_OUT', 'MUL(EN := TRUE, IN1 := valor, IN2 := k, ENO => _TMP_MUL111_ENO)') + + enoIf('_TMP_MUL111_ENO', 'producto', '_TMP_MUL111_OUT') + + call('_TMP_SUB222_OUT', 'SUB(IN1 := producto, IN2 := k2, ENO => _TMP_SUB222_ENO)') + + asg('dif', '_TMP_SUB222_OUT') + + call('_TMP_GT333_OUT', 'GT(EN := _TMP_MUL111_ENO OR _TMP_SUB222_ENO, ENO => _TMP_GT333_ENO)') + + enoIf('_TMP_GT333_ENO', 'flag', '_TMP_GT333_OUT'), + ) + }) + + it('writes a block-fed coil before the next block reads it', () => { + // GEA -> coil Flag ; GEB(EN:=GEA.ENO, IN1:=Flag) -> coil Result. + const body: RFBody = { + rungs: [ + { + reactFlowViewport: [800, 300], + nodes: [ + rail('L', 'left', 0), + inVar('901', 'Input', 20, 80), + inVar('902', '86400', 20, 120), + fblock('GEA', 'GE', '111', 1, 150, 30, 'BOOL'), + coil('cFlag', 'Flag', 320, 30), + inVar('904', 'Flag', 20, 200), + inVar('905', '60', 20, 240), + fblock('GEB', 'GE', '222', 2, 450, 30, 'BOOL'), + coil('cRes', 'Result', 620, 30), + rail('R', 'right', 760), + ], + edges: [ + e('L', 'GEA', 'left-rail', 'EN'), + e('901', 'GEA', null, 'IN1'), + e('902', 'GEA', null, 'IN2'), + e('GEA', 'cFlag', 'OUT', null), + e('GEA', 'GEB', 'ENO', 'EN'), + e('904', 'GEB', null, 'IN1'), + e('905', 'GEB', null, 'IN2'), + e('GEB', 'cRes', 'OUT', null), + ], + }, + ], + } + const { bodySt, warnings } = emitLdBody(body) + expect(warnings).toEqual([]) + expect(bodySt).toBe( + '\n' + + call('_TMP_GE111_OUT', 'GE(EN := TRUE, IN1 := Input, IN2 := 86400, ENO => _TMP_GE111_ENO)') + + asg('Flag', '_TMP_GE111_OUT') + + call('_TMP_GE222_OUT', 'GE(EN := _TMP_GE111_ENO, IN1 := Flag, IN2 := 60, ENO => _TMP_GE222_ENO)') + + asg('Result', '_TMP_GE222_OUT'), + ) + }) +}) diff --git a/src/backend/shared/transpilers/st-transpiler/__tests__/coil-grouping-836.test.ts b/src/backend/shared/transpilers/st-transpiler/__tests__/coil-grouping-836.test.ts new file mode 100644 index 000000000..c99aa94b8 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/__tests__/coil-grouping-836.test.ts @@ -0,0 +1,145 @@ +import { emitLdBody } from '@root/backend/shared/transpilers/st-transpiler/walker/ld' +import type { RFBody, RFEdge, RFNode } from '@root/backend/shared/transpilers/st-transpiler/walker/types' + +// Regression coverage for issue #836 — a contact feeding multiple coils +// must be evaluated once per energization path, not re-read between coil +// assignments. SET/RESET coils that branch off the same source collapse +// into one IF even when a merge-fed coil sorts between them; sequential +// coils (distinct sources) stay as separate IFs. + +let edgeId = 0 +const e = (source: string, target: string): RFEdge => ({ id: `e${edgeId++}`, source, target }) +const rail = (id: string, variant: 'left' | 'right', x: number): RFNode => ({ + id, + type: 'powerRail', + position: { x, y: 30 }, + data: { variant }, +}) +const contact = (id: string, name: string, x: number, nid: string): RFNode => ({ + id, + type: 'contact', + position: { x, y: 38 }, + data: { variant: 'default', variable: { name }, numericId: nid }, +}) +const coil = (id: string, name: string, variant: 'set' | 'reset', x: number, y: number, nid: string): RFNode => ({ + id, + type: 'coil', + position: { x, y }, + data: { variant, variable: { name }, executionOrder: 0, numericId: nid }, +}) +const par = (id: string, side: 'open' | 'close', x: number): RFNode => ({ + id, + type: 'parallel', + position: { x, y: 49 }, + data: { type: side }, +}) + +// One emitted `IF THEN END_IF;` block, base indent 2. +const block = (cond: string, ...assigns: string[]): string => + ` IF ${cond} THEN\n${assigns.map((a) => ` ${a}\n`).join('')} END_IF;\n` + +describe('issue 836 — contact evaluated once across multiple coils', () => { + it('groups parallel outputs even when the reset coil sorts between them', () => { + // FirstScan -> [Output1(S) || Output2(S)] -> FirstScan(R) + const body: RFBody = { + rungs: [ + { + reactFlowViewport: [700, 300], + nodes: [ + rail('L', 'left', 0), + contact('C', 'FirstScan', 68, '10'), + par('PO', 'open', 251), + coil('K1', 'Output1', 'set', 300, 38, '20'), + coil('K2', 'Output2', 'set', 300, 130, '21'), + par('PC', 'close', 373), + coil('KR', 'FirstScan', 'reset', 500, 38, '22'), + rail('R', 'right', 600), + ], + edges: [ + e('L', 'C'), + e('C', 'PO'), + e('PO', 'K1'), + e('PO', 'K2'), + e('K1', 'PC'), + e('K2', 'PC'), + e('PC', 'KR'), + e('KR', 'R'), + ], + }, + ], + } + const { bodySt, warnings } = emitLdBody(body) + expect(warnings).toEqual([]) + expect(bodySt).toBe( + '\n' + + block('FirstScan', 'Output1 := TRUE; (*set*)', 'Output2 := TRUE; (*set*)') + + block('FirstScan', 'FirstScan := FALSE; (*reset*)'), + ) + }) + + it('groups three coils sharing one source into a single IF', () => { + // FirstScan -> [Output1(S) || FirstScan(R) || Output2(S)] + const body: RFBody = { + rungs: [ + { + reactFlowViewport: [600, 300], + nodes: [ + rail('L', 'left', 0), + contact('C', 'FirstScan', 68, '10'), + par('PO', 'open', 251), + coil('K1', 'Output1', 'set', 300, 38, '20'), + coil('KR', 'FirstScan', 'reset', 300, 130, '22'), + coil('K2', 'Output2', 'set', 300, 220, '21'), + par('PC', 'close', 400), + rail('R', 'right', 500), + ], + edges: [ + e('L', 'C'), + e('C', 'PO'), + e('PO', 'K1'), + e('PO', 'KR'), + e('PO', 'K2'), + e('K1', 'PC'), + e('KR', 'PC'), + e('K2', 'PC'), + e('PC', 'R'), + ], + }, + ], + } + const { bodySt, warnings } = emitLdBody(body) + expect(warnings).toEqual([]) + expect(bodySt).toBe( + '\n' + + block('FirstScan', 'Output1 := TRUE; (*set*)', 'FirstScan := FALSE; (*reset*)', 'Output2 := TRUE; (*set*)'), + ) + }) + + it('keeps sequential coils (distinct sources) as separate IFs', () => { + // FirstScan -> Output1(S) -> FirstScan(R) -> Output2(S) + const body: RFBody = { + rungs: [ + { + reactFlowViewport: [700, 100], + nodes: [ + rail('L', 'left', 0), + contact('C', 'FirstScan', 68, '10'), + coil('K1', 'Output1', 'set', 200, 38, '20'), + coil('KR', 'FirstScan', 'reset', 350, 38, '22'), + coil('K2', 'Output2', 'set', 500, 38, '21'), + rail('R', 'right', 650), + ], + edges: [e('L', 'C'), e('C', 'K1'), e('K1', 'KR'), e('KR', 'K2'), e('K2', 'R')], + }, + ], + } + const { bodySt, warnings } = emitLdBody(body) + expect(warnings).toEqual([]) + expect(bodySt).toBe( + '\n' + + block('FirstScan', 'Output1 := TRUE; (*set*)') + + block('FirstScan', 'FirstScan := FALSE; (*reset*)') + + block('FirstScan', 'Output2 := TRUE; (*set*)'), + ) + }) +}) diff --git a/src/backend/shared/transpilers/st-transpiler/core/path-tree.ts b/src/backend/shared/transpilers/st-transpiler/core/path-tree.ts index f76337e7f..a56be025a 100644 --- a/src/backend/shared/transpilers/st-transpiler/core/path-tree.ts +++ b/src/backend/shared/transpilers/st-transpiler/core/path-tree.ts @@ -41,7 +41,7 @@ export function leafNode(chunks: ProgramChunk[]): PathNode { /** * 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. + * original PLCopen pipeline keys on. */ export function pythonReprNode(node: PathNode): string { switch (node.kind) { @@ -110,7 +110,9 @@ export function pythonReprString(s: string): string { /** * Boolean simplification — `(A AND B) OR (A AND C)` → `A AND (B OR C)`. * - * Strategy mirrors `FactorizePaths` (PLCGenerator.py:1429): + * Strategy mirrors `FactorizePaths` (PLCGenerator.py:1429), with one + * deliberate divergence — duplicate-path dedup (see below): + * 0. Drop byte-identical duplicate paths (`X OR X` → `X`). * 1. Sort the paths by their Python `repr`. * 2. For each pair, find common head/tail prefixes/suffixes between * adjacent AND nodes. @@ -119,8 +121,25 @@ export function pythonReprString(s: string): string { export function factorizePaths(paths: readonly PathNode[]): PathNode[] { if (paths.length <= 1) return [...paths] + // Collapse byte-identical parallel paths (`X OR X` = `X`). The + // python oracle keeps duplicates (parallel coils feeding a merged + // sink emit `FirstScan OR FirstScan`); we drop them, a deliberate + // divergence that yields the cleaner condition with no behavioural + // change. Paths are equal only when their full repr — text AND + // source locations — matches, so two distinct contacts on the same + // variable still survive as separate OR terms. + const seenRepr = new Set() + const unique: PathNode[] = [] + for (const p of paths) { + const k = pythonReprNode(p) + if (seenRepr.has(k)) continue + seenRepr.add(k) + unique.push(p) + } + if (unique.length <= 1) return unique + // Sort by stable repr key — same order Python produces. - const sorted = pythonStableSort(paths) + const sorted = pythonStableSort(unique) // Walk sorted list, group adjacent paths sharing head AND. const out: PathNode[] = [] 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 deleted file mode 100644 index daf0e5474..000000000 --- a/src/backend/shared/transpilers/st-transpiler/data/std_block_catalog.json +++ /dev/null @@ -1,15855 +0,0 @@ -{ - "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 index f2e89f665..3d5cb5c4c 100644 --- a/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts +++ b/src/backend/shared/transpilers/st-transpiler/emit/configuration.ts @@ -32,6 +32,11 @@ export function generateConfigurations(project: TranspileProject): ProgramChunk[ const out: ProgramChunk[] = [] + // Tasks emit in ascending-priority order (stable), matching the old + // editor XML generator's `[...tasks].sort((a,b)=>a.priority-b.priority)`. + // Instance (PROGRAM) lines are grouped by this same task order below. + const sortedTasks = [...project.configuration.tasks].sort((a, b) => a.priority - b.priority) + out.push(['\nCONFIGURATION ', []]) out.push([CONFIG_NAME, [configTagname, 'name']]) out.push(['\n', []]) @@ -57,7 +62,7 @@ export function generateConfigurations(project: TranspileProject): ProgramChunk[ // current shape. // Tasks. - project.configuration.tasks.forEach((task, taskNumber) => { + sortedTasks.forEach((task, taskNumber) => { out.push([' TASK ', []]) out.push([task.name, [resourceTagname, 'task', taskNumber, 'name']]) out.push(['(', []]) @@ -90,7 +95,7 @@ export function generateConfigurations(project: TranspileProject): ProgramChunk[ // 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 task of sortedTasks) { for (const instance of project.configuration.instances) { if (instance.task !== task.name) continue out.push([' PROGRAM ', []]) 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 0540334e7..1f2ab278a 100644 --- a/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts +++ b/src/backend/shared/transpilers/st-transpiler/emit/pou-graphical.ts @@ -10,11 +10,12 @@ */ import { PLC_BASE_TYPES } from '../helpers/base-types' -import { resolveBlockType } from '../helpers/block-library' +import { type BlockInfos, blockInfosFromVariant, isRecord } from '../helpers/block-library' import type { ProgramChunk } from '../helpers/program' import { computePouName } from '../helpers/text-helpers' import { varTypeNames } from '../helpers/type-text' import type { TranspilePou, TranspileProject, TranspileVariable, TranspileVariableClass } from '../types' +import type { TypeContext } from '../walker/connection-types' import { emitFbdBody } from '../walker/fbd' import type { SyntheticVar } from '../walker/ld' import { emitLdBody } from '../walker/ld' @@ -24,40 +25,9 @@ import { computeValue } from './value' interface InterfaceEntry { keyword: string vars: TranspileVariable[] + located?: boolean } -/** - * 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 ───────────────────────────────── */ /** @@ -78,7 +48,9 @@ export function generateGraphicalPou(pou: TranspilePou, project: TranspileProjec 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) + const typeContext = buildTypeContext(pou, project) + const emitted = + pou.body.language === 'ld' ? emitLdBody(pou.body.value, typeContext) : emitFbdBody(pou.body.value, typeContext) // Compose the final POU chunk stream now that the walker has // emitted the body bytes + any synthetic vars. @@ -92,45 +64,7 @@ export function generateGraphicalPou(pou: TranspilePou, project: TranspileProjec } program.push(['\n', []]) - // Resolve `ANY` placeholders in the synthesised function-output - // temps: - // 1. User-defined project functions → declared `interface.returnType`. - // 2. Standard catalog functions (ADD, MUL, NOT, AND, …) → catalog's - // formal output `type`. Generic groups (`ANY_BIT`, `ANY_NUM`, - // …) collapse to `BOOL`, which matches the corpus where these - // operators are always Boolean rung logic. A future - // computeConnectionTypes port will narrow these properly. - // 3. Polymorphic IEC 61131-3 type-conversion functions of the - // form `TO_` (TO_INT, TO_UINT, TO_REAL, …) — the - // catalog enumerates the source-specific variants - // (`BOOL_TO_UINT`, `INT_TO_UINT`, …) but NOT the generic - // `TO_` family, so resolveBlockType returns null for - // them. Without this case the synthetic var stayed at - // `ANY` and strucpp rejected the program with - // "Undefined type 'ANY' in PROGRAM" — fixed here by reading - // the destination type directly from the function name. - const resolvedSyntheticVars = emitted.syntheticVars.map((sv) => { - if (sv.type !== 'ANY' || sv.originBlockTypeName === undefined) return sv - const referenced = project.pous.find((p) => p.name === sv.originBlockTypeName) - if (referenced && referenced.pouType === 'function' && referenced.interface.returnType) { - return { ...sv, type: referenced.interface.returnType } - } - const stdResolved = resolveBlockType(sv.originBlockTypeName) - if (stdResolved) { - const outPort = stdResolved.infos.outputs.find((o) => o.name === sv.originFormalParameter) - if (outPort) { - const collapsed = outPort.type.startsWith('ANY') ? 'BOOL' : outPort.type - return { ...sv, type: collapsed } - } - } - const polymorphicMatch = sv.originBlockTypeName.match(/^TO_([A-Z]+)$/) - if (polymorphicMatch && TO_CONVERSION_TARGETS.has(polymorphicMatch[1])) { - return { ...sv, type: polymorphicMatch[1] } - } - return sv - }) - - const iface = computeInterface(pou.interface?.variables ?? [], resolvedSyntheticVars) + const iface = computeInterface(pou.interface?.variables ?? [], emitted.syntheticVars) for (const entry of iface) { const variableType = locationCategory(entry.keyword) program.push([` ${entry.keyword}`, []]) @@ -166,6 +100,122 @@ export function generateGraphicalPou(pou: TranspilePou, project: TranspileProjec /* ────────────────────────── helpers ─────────────────────────────────────── */ +// Block signatures from every graphical block instance's variant — the +// co-located equivalent of the embedded payload. Deduped +// by name, first instance wins — deliberately mirroring the oracle's +// dedup; user POUs are excluded (they resolve from their +// own interface). +function collectBlockSignatures(project: TranspileProject): Map { + const userPouNames = new Set(project.pous.map((p) => p.name)) + const registry = new Map() + const visit = (nodes: unknown): void => { + if (!Array.isArray(nodes)) return + for (const node of nodes) { + if (!isRecord(node) || node.type !== 'block' || !isRecord(node.data)) continue + const infos = blockInfosFromVariant(node.data.variant) + if (infos === null || userPouNames.has(infos.name) || registry.has(infos.name)) continue + registry.set(infos.name, infos) + } + } + for (const pou of project.pous) { + if (pou.body.language === 'ld') { + for (const rung of pou.body.value.rungs) visit(rung.nodes) + } else if (pou.body.language === 'fbd') { + visit(pou.body.value.rung.nodes) + } + } + return registry +} + +// GetVariableType + GetBlockType ports (PLCGenerator.py:786-817, PLCControler.py:1288-1335) +function buildTypeContext(pou: TranspilePou, project: TranspileProject): TypeContext { + const interfaceTypes = new Map() + for (const v of pou.interface?.variables ?? []) { + interfaceTypes.set(v.name, getTypeAsText(v)) + } + + const normalizeReturnType = (rt: string): string => (PLC_BASE_TYPES.has(rt.toUpperCase()) ? rt.toUpperCase() : rt) + + const projectBlockInfos = (typeName: string): BlockInfos | null => { + const p = project.pous.find((x) => x.name === typeName) + if (p === undefined) return null + const inputs: BlockInfos['inputs'] = [] + const outputs: BlockInfos['outputs'] = [] + for (const v of p.interface?.variables ?? []) { + const io = { name: v.name, type: getTypeAsText(v), qualifier: 'none' as const } + if (v.class === 'input' || v.class === 'inOut') inputs.push(io) + if (v.class === 'output' || v.class === 'inOut') outputs.push(io) + } + if (p.pouType === 'function') { + outputs.push({ + name: 'OUT', + type: normalizeReturnType(p.interface.returnType ?? 'BOOL'), + qualifier: 'none', + }) + } + return { + name: typeName, + type: p.pouType === 'function' ? 'function' : 'functionBlock', + extensible: false, + inputs, + outputs, + comment: '', + usage: '', + } + } + + const libraryBlocks = collectBlockSignatures(project) + + // Library blocks (single generic signature each) resolve from the project's + // own variants; user-defined POUs resolve from their interface. Generic + // types stay verbatim — connection-type unification concretizes them. + const resolveBlock = (typeName: string): BlockInfos | null => + libraryBlocks.get(typeName) ?? projectBlockInfos(typeName) + + const memberBlockInfos = resolveBlock + + const variableType: TypeContext['variableType'] = (expression) => { + const parts = expression.split('.') + let name = parts.shift() ?? '' + let current: string | null = null + if (pou.pouType === 'function' && name === pou.name && pou.interface.returnType !== undefined) { + current = normalizeReturnType(pou.interface.returnType) + } else { + current = interfaceTypes.get(name) ?? null + } + while (current !== null && parts.length > 0) { + const block = memberBlockInfos(current) + if (block !== null) { + name = parts.shift() ?? '' + current = null + for (const io of [...block.inputs, ...block.outputs]) { + if (io.name === name) { + current = io.type + break + } + } + } else { + const dt = project.dataTypes.find((d) => d.name === current) + if (dt !== undefined && dt.derivation === 'structure') { + name = parts.shift() ?? '' + current = null + for (const el of dt.variable) { + if (el.name === name) { + current = getTypeAsText(el) + break + } + } + } else { + break + } + } + } + return current + } + + return { variableType, resolveBlock } +} + function computeInterface(variables: TranspileVariable[], syntheticVars: SyntheticVar[]): InterfaceEntry[] { const classToKeyword: Record = { input: varTypeNames.inputVars, @@ -183,27 +233,32 @@ function computeInterface(variables: TranspileVariable[], syntheticVars: Synthet 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. + // python splits each varlist into an unlocated block then a located one (DIV-03) + const out: InterfaceEntry[] = [] + for (const [keyword, vars] of grouped) { + const unlocated = vars.filter((v) => !v.location) + const located = vars.filter((v) => v.location) + if (unlocated.length > 0) out.push({ keyword, vars: unlocated }) + if (located.length > 0) out.push({ keyword, vars: located, located: true }) + } if (syntheticVars.length > 0) { - const localKeyword = varTypeNames.localVars - const localBucket = grouped.get(localKeyword) ?? [] - for (const sv of syntheticVars) { + const synth: TranspileVariable[] = syntheticVars.map((sv) => { const isElementary = PLC_BASE_TYPES.has(sv.type.toUpperCase()) - localBucket.push({ + return { name: sv.name, type: isElementary ? { definition: 'base-type', value: sv.type.toUpperCase() } : { definition: 'derived', value: sv.type }, class: 'local', - }) + } + }) + const last = out[out.length - 1] + // python reuses the trailing block only when it is a plain unlocated VAR (DIV-16) + if (last !== undefined && last.keyword === varTypeNames.localVars && !last.located) { + last.vars.push(...synth) + } else { + out.push({ keyword: varTypeNames.localVars, vars: synth }) } - grouped.set(localKeyword, localBucket) - } - const out: InterfaceEntry[] = [] - for (const [keyword, vars] of grouped) { - out.push({ keyword, vars }) } return out } diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/base-types.ts b/src/backend/shared/transpilers/st-transpiler/helpers/base-types.ts index 8f5aa670b..713712d34 100644 --- a/src/backend/shared/transpilers/st-transpiler/helpers/base-types.ts +++ b/src/backend/shared/transpilers/st-transpiler/helpers/base-types.ts @@ -7,6 +7,9 @@ * * `WSTRING` is intentionally absent — matches python's `# TODO` * comment at `definitions.py:118`. + * + * `__XWORD` is the platform-width address type carried by strucpp library + * block signatures; declarable and emitted verbatim. */ export const PLC_BASE_TYPES: ReadonlySet = new Set([ diff --git a/src/backend/shared/transpilers/st-transpiler/helpers/block-library.ts b/src/backend/shared/transpilers/st-transpiler/helpers/block-library.ts index e98d51ae6..537f5b920 100644 --- a/src/backend/shared/transpilers/st-transpiler/helpers/block-library.ts +++ b/src/backend/shared/transpilers/st-transpiler/helpers/block-library.ts @@ -1,22 +1,16 @@ /** - * Standard block-library resolution against the pre-built catalog. + * Block-library signatures, sourced from the project's own block variants. * - * 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. + * openplc-web is co-located with strucpp and the user's installed libraries, + * so every placed block already carries its full typed signature in + * `node.data.variant` (the editor stamps it from the library on placement and + * `restamp-library-variants` keeps it fresh). The transpiler resolves block + * types from those variants — the same source `collect-library-blocks.ts` + * feeds the Python oracle as the embedded `` payload — instead + * of bundling a separate catalog. Unknown blocks degrade to permissive + * synthesis in `connection-types.ts`. */ -import stdCatalog from '../data/std_block_catalog.json' - export interface BlockIO { name: string type: string @@ -33,56 +27,49 @@ export interface BlockInfos { usage: string } -export interface BlockResolution { - source: 'standard' - infos: BlockInfos -} - -interface CatalogEntry { - section: string - infos: BlockInfos +export function isRecord(v: unknown): v is Record { + return typeof v === 'object' && v !== null && !Array.isArray(v) } -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`. + * Build a block signature from a placed block's `node.data.variant`. + * + * Mirrors `collect-library-blocks.ts`'s block-info derivation: + * EN/ENO are implicit control pins (dropped); inOut params appear on both + * sides; a function's return is already a class-`output` variable named `OUT`. + * Generic IEC meta-types (`ANY`, `ANY_NUM`, …) are kept verbatim and resolved + * from the wired connections during type inference. */ -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 } -} +export function blockInfosFromVariant(variant: unknown): BlockInfos | null { + if (!isRecord(variant)) return null + const name = typeof variant.name === 'string' ? variant.name : null + if (name === null) return null -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, + const inputs: BlockIO[] = [] + const outputs: BlockIO[] = [] + const variables = Array.isArray(variant.variables) ? variant.variables : [] + for (const v of variables) { + if (!isRecord(v)) continue + const vName = typeof v.name === 'string' ? v.name : null + if (vName === null || vName === 'EN' || vName === 'ENO') continue + const type = isRecord(v.type) && typeof v.type.value === 'string' ? v.type.value : 'ANY' + const io: BlockIO = { name: vName, type, qualifier: 'none' } + if (v.class === 'input') inputs.push(io) + else if (v.class === 'output') outputs.push(io) + else if (v.class === 'inOut' || v.class === 'inout') { + inputs.push(io) + outputs.push(io) + } } -} -function collapseToAny(infos: BlockInfos): BlockInfos { return { - ...infos, - inputs: infos.inputs.map((i) => ({ ...i, type: 'ANY' })), - outputs: infos.outputs.map((o) => ({ ...o, type: 'ANY' })), + name, + type: + variant.type === 'function-block' || variant.type === 'function-block-instance' ? 'functionBlock' : 'function', + extensible: variant.extensible === true, + inputs, + outputs, + comment: '', + usage: '', } } 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 256758f01..87c6fdbf9 100644 --- a/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts +++ b/src/backend/shared/transpilers/st-transpiler/helpers/text-helpers.ts @@ -46,16 +46,11 @@ export function reIndentText(text: string, nbSpaces: number): string { 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. - */ +// Mirrors str.splitlines(): consumes CR forms (RF body text is raw editor +// bytes, no lxml EOL normalization) and drops the trailing empty element. function pySplitLines(text: string): string[] { if (text.length === 0) return [] - const lines = text.split('\n') + const lines = text.split(/\r\n|\r|\n/) if (lines[lines.length - 1] === '') lines.pop() return lines } 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 7f93cbc32..ad239c084 100644 --- a/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts +++ b/src/backend/shared/transpilers/st-transpiler/helpers/type-hierarchy.ts @@ -1,22 +1,15 @@ /** - * IEC 61131-3 type hierarchy + compatibility predicate. + * IEC 61131-3 type hierarchy. * - * Mirrors ``plcopen/definitions.py:84-119`` (`TypeHierarchy_list`), - * ``plcopen/structures.py:36-48`` (`IsOfType`), and + * Mirrors ``plcopen/definitions.py:84-119`` (`TypeHierarchy_list`) 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")`. + * meta-parent. * * 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. + * with a TODO. We preserve that — WSTRING is absent from the table. */ /** @@ -57,5 +50,8 @@ export const TypeHierarchy: Readonly> = { WORD: 'ANY_NBIT', DWORD: 'ANY_NBIT', LWORD: 'ANY_NBIT', + // Platform-width address type (strucpp/CODESYS parity); strucpp resolves the + // concrete width per target. + __XWORD: 'ANY_NBIT', // WSTRING intentionally absent — matches Python's `# TODO` comment. } diff --git a/src/backend/shared/transpilers/st-transpiler/walker/connection-types.ts b/src/backend/shared/transpilers/st-transpiler/walker/connection-types.ts new file mode 100644 index 000000000..ce67686b7 --- /dev/null +++ b/src/backend/shared/transpilers/st-transpiler/walker/connection-types.ts @@ -0,0 +1,301 @@ +/** + * Pin-type inference — port of python's ComputeConnectionTypes + + * ComputeBlockInputTypes (PLCGenerator.py:971-1241). + * + * Two passes over the flattened body graph. Pass 1 seeds concrete + * types from declared variables, literals, contacts/coils/rails and + * resolvable blocks; blocks with no known signature are deferred. + * Pass 2 gives each deferred block a permissive all-ANY synthesized + * signature and unifies each ANY-class pin group with any concrete + * connected type. Pins known to share a type but still untyped live + * in `related` groups that collapse when one member gets typed. + */ + +import type { BlockInfos } from '../helpers/block-library' +import { + asBlockData, + asConnectorData, + asContinuationData, + asVariableData, + asVariableExpressionData, + type BlockData, +} from './narrow' +import type { RFBody, RFEdge, RFNode } from './types' + +export interface TypeContext { + /** Declared/dot-path/literal type of an expression, or null. */ + variableType(expression: string): string | null + /** Block signature by type name (single generic signature), or null. */ + resolveBlock(typeName: string): BlockInfos | null +} + +export function pinIn(nodeId: string, handle = ''): string { + return `in:${nodeId}:${handle}` +} + +export function pinOut(nodeId: string, handle = ''): string { + return `out:${nodeId}:${handle}` +} + +const LITERAL_TYPES: Record = { + T: 'TIME', + D: 'DATE', + TOD: 'TIME_OF_DAY', + DT: 'DATE_AND_TIME', + '2': null, + '8': null, + '16': null, +} + +/** Literal-prefix typing from ComputeConnectionTypes (PLCGenerator.py:1001-1010). */ +export function literalType(expression: string): string | null { + const parts = expression.split('#') + if (parts.length > 1) { + const prefix = parts[0].toUpperCase() + return prefix in LITERAL_TYPES ? LITERAL_TYPES[prefix] : prefix + } + if (expression.startsWith("'")) return 'STRING' + if (expression.startsWith('"')) return 'WSTRING' + return null +} + +interface Graph { + byId: Map + incoming: Map +} + +class PinTypes { + types = new Map() + related: string[][] = [] + + extractRelated(pin: string): string[] { + for (let i = 0; i < this.related.length; i++) { + if (this.related[i].includes(pin)) return this.related.splice(i, 1)[0] + } + return [pin] + } + + assign(pins: readonly string[], type: string): void { + for (const pin of pins) this.types.set(pin, type) + } +} + +export function computeConnectionTypes(body: RFBody, ctx: TypeContext): Map { + const graph: Graph = { byId: new Map(), incoming: new Map() } + const nodes: RFNode[] = [] + for (const rung of body.rungs) { + for (const node of rung.nodes) { + graph.byId.set(node.id, node) + graph.incoming.set(node.id, []) + nodes.push(node) + } + for (const edge of rung.edges) { + graph.incoming.get(edge.target)?.push(edge) + } + } + + const pt = new PinTypes() + const deferred: { node: RFNode; data: BlockData }[] = [] + + for (const node of nodes) { + switch (node.type) { + case 'variable': + case 'input-variable': + case 'output-variable': + case 'inout-variable': { + const data = asVariableData(node.data) + if (data === null) break + const expr = asVariableExpressionData(node.data)?.expression ?? data.variable + const varType = ctx.variableType(expr) ?? literalType(expr) + if (varType === null) break + if (data.variant === 'input' || data.variant === 'inout') { + pt.assign(pt.extractRelated(pinOut(node.id)), varType) + } + if (data.variant === 'output' || data.variant === 'inout') { + pt.types.set(pinIn(node.id), varType) + const source = sourcePin(graph, node.id) + if (source !== null && !pt.types.has(source)) { + pt.assign(pt.extractRelated(source), varType) + } + } + break + } + case 'contact': + case 'coil': + case 'parallel': + case 'powerRail': { + pt.assign(pt.extractRelated(pinOut(node.id)), 'BOOL') + pt.types.set(pinIn(node.id), 'BOOL') + for (const edge of graph.incoming.get(node.id) ?? []) { + const source = edgeSourcePin(graph, edge) + if (!pt.types.has(source)) pt.assign(pt.extractRelated(source), 'BOOL') + } + break + } + case 'continuation': { + const name = asContinuationData(node.data)?.name + if (name === undefined) break + let connector: RFNode | null = null + for (const candidate of nodes) { + if (candidate.type === 'connector' && asConnectorData(candidate.data)?.name === name) { + connector = candidate + break + } + } + if (connector === null) break + const undefinedPins = [pinOut(node.id), pinIn(connector.id)] + const source = sourcePin(graph, connector.id) + if (source !== null) undefinedPins.push(source) + let varType = 'ANY' + const related: string[] = [] + for (const pin of undefinedPins) { + const known = pt.types.get(pin) + if (known !== undefined) varType = known + else related.push(...pt.extractRelated(pin)) + } + if (varType.startsWith('ANY') && related.length > 0) pt.related.push(related) + else pt.assign(related, varType) + break + } + case 'block': { + const data = asBlockData(node.data) + if (data === null) break + const infos = ctx.resolveBlock(data.typeName) + if (infos !== null) { + computeBlockInputTypes(graph, pt, node, data, infos) + } else { + for (const handle of wiredInputHandles(graph, node, data)) { + const pin = pinIn(node.id, handle) + const source = sourcePin(graph, node.id, handle) + if (source === null) continue + const sourceType = pt.types.get(source) + if (sourceType !== undefined) { + pt.types.set(pin, sourceType) + } else { + const related = pt.extractRelated(source) + related.push(pin) + pt.related.push(related) + } + } + deferred.push({ node, data }) + } + break + } + } + } + + for (const { node, data } of deferred) { + const infos = synthesizePermissiveBlockInfos(graph, node, data) + computeBlockInputTypes(graph, pt, node, data, infos) + } + + return pt.types +} + +/** ComputeBlockInputTypes (PLCGenerator.py:1183-1241). */ +function computeBlockInputTypes(graph: Graph, pt: PinTypes, node: RFNode, data: BlockData, infos: BlockInfos): void { + const undefinedGroups = new Map() + const bucket = (anyClass: string, pin: string) => { + const group = undefinedGroups.get(anyClass) ?? [] + group.push(pin) + undefinedGroups.set(anyClass, group) + } + + for (const out of outputHandles(data)) { + const pin = pinOut(node.id, out) + if (out === 'ENO') { + pt.assign(pt.extractRelated(pin), 'BOOL') + continue + } + for (const formal of infos.outputs) { + if (formal.name !== out) continue + if (formal.type.startsWith('ANY')) bucket(formal.type, pin) + else if (!pt.types.has(pin)) pt.assign(pt.extractRelated(pin), formal.type) + } + } + + for (const handle of wiredInputHandles(graph, node, data)) { + const pin = pinIn(node.id, handle) + if (handle === 'EN') { + pt.assign(pt.extractRelated(pin), 'BOOL') + continue + } + for (const formal of infos.inputs) { + if (formal.name !== handle) continue + const source = sourcePin(graph, node.id, handle) + if (formal.type.startsWith('ANY')) { + bucket(formal.type, pin) + if (source !== null) bucket(formal.type, source) + } else { + pt.types.set(pin, formal.type) + if (source !== null && !pt.types.has(source)) { + pt.assign(pt.extractRelated(source), formal.type) + } + } + } + } + + for (const [anyClass, pins] of undefinedGroups) { + let varType = anyClass + const related: string[] = [] + for (const pin of pins) { + const known = pt.types.get(pin) + if (known !== undefined && !known.startsWith('ANY')) varType = known + else related.push(...pt.extractRelated(pin)) + } + if (varType.startsWith('ANY') && related.length > 0) pt.related.push(related) + else pt.assign(related, varType) + } +} + +/** SynthesizePermissiveBlockInfos (PLCGenerator.py:732-763). */ +function synthesizePermissiveBlockInfos(graph: Graph, node: RFNode, data: BlockData): BlockInfos { + return { + name: data.typeName, + type: data.blockKind === 'function-block-instance' ? 'functionBlock' : 'function', + extensible: false, + inputs: wiredInputHandles(graph, node, data) + .filter((h) => h !== 'EN') + .map((h) => ({ name: h, type: 'ANY', qualifier: 'none' as const })), + outputs: outputHandles(data) + .filter((h) => h !== 'ENO') + .map((h) => ({ name: h, type: 'ANY', qualifier: 'none' as const })), + comment: '', + usage: '', + } +} + +// python only ever sees serialized (wired) input pins; EN first when wired +function wiredInputHandles(graph: Graph, node: RFNode, data: BlockData): string[] { + const wired = new Set((graph.incoming.get(node.id) ?? []).map((e) => e.targetHandle ?? '')) + const handles: string[] = [] + if (wired.has('EN')) handles.push('EN') + for (const name of data.inputs) { + if (name === 'EN') continue + if (wired.has(name)) handles.push(name) + } + return handles +} + +function outputHandles(data: BlockData): string[] { + const handles = [...data.outputs] + if (data.executionControl && !handles.includes('ENO')) handles.push('ENO') + return handles +} + +function sourcePin(graph: Graph, nodeId: string, handle?: string): string | null { + for (const edge of graph.incoming.get(nodeId) ?? []) { + if (handle === undefined || (edge.targetHandle ?? '') === handle) { + return edgeSourcePin(graph, edge) + } + } + return null +} + +function edgeSourcePin(graph: Graph, edge: RFEdge): string { + const source = graph.byId.get(edge.source) + if (source !== undefined && source.type === 'block') { + return pinOut(edge.source, edge.sourceHandle ?? '') + } + return pinOut(edge.source) +} diff --git a/src/backend/shared/transpilers/st-transpiler/walker/fbd.ts b/src/backend/shared/transpilers/st-transpiler/walker/fbd.ts index 82c9b481b..62c48792f 100644 --- a/src/backend/shared/transpilers/st-transpiler/walker/fbd.ts +++ b/src/backend/shared/transpilers/st-transpiler/walker/fbd.ts @@ -14,6 +14,7 @@ * sink sort) all live in the LD walker — they're no-ops for LD * bodies that don't exercise them. */ +import type { TypeContext } from './connection-types' import { emitLdBody, type EmitResult } from './ld' import type { RFRung } from './types' @@ -24,6 +25,6 @@ export interface RFFbdBody { rung: RFRung } -export function emitFbdBody(body: RFFbdBody): EmitResult { - return emitLdBody({ rungs: [body.rung] }) +export function emitFbdBody(body: RFFbdBody, typeContext?: TypeContext): EmitResult { + return emitLdBody({ rungs: [body.rung] }, typeContext) } diff --git a/src/backend/shared/transpilers/st-transpiler/walker/ld.ts b/src/backend/shared/transpilers/st-transpiler/walker/ld.ts index fe2c3a6b4..57669b916 100644 --- a/src/backend/shared/transpilers/st-transpiler/walker/ld.ts +++ b/src/backend/shared/transpilers/st-transpiler/walker/ld.ts @@ -3,7 +3,7 @@ * * 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; + * byte-for-byte against the python oracle; * `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. @@ -25,6 +25,7 @@ import { type ProgramChunk, TRUE_NODE, } from '../core/path-tree' +import { computeConnectionTypes, pinOut, type TypeContext } from './connection-types' import { asBlockData, asCoilData, @@ -47,19 +48,15 @@ import type { RFBody, RFEdge, RFNode, RFRung } from './types' * 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. + * already the resolved `'R_TRIG'`/`'F_TRIG'` name. * - 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`. + * — `type` is `'BOOL'` for `ENO`, otherwise the graph-inferred + * pin type from `connection-types.ts` (literal `'ANY'` when no + * concrete type reaches the pin, matching Python). */ export interface SyntheticVar { name: string type: string - originBlockTypeName?: string - originFormalParameter?: string } export interface EmitResult { @@ -81,17 +78,20 @@ interface WalkerState { 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. */ + * function-call emission. Types come from the pre-computed + * connection-type inference (`'ANY'` terminal fallback). */ functionTempVars: { name: string type: string - originBlockTypeName: string - originFormalParameter: string }[] emittedBlocks: Set + /** Output-write sinks (coil / output / inOut variable) keyed by the + * block that feeds them — emitted right after the block's call so a + * downstream block reads the written value, not a stale one (#830). */ + consumersByBlock: Map + /** Sinks already emitted — by block coupling or the main sweep — so + * the other path skips them. */ + emittedSinks: Set /** Connector expressions cached by name, consumed by continuation * visits later in the same rung (FBD-only). */ connectorExprs: Map @@ -102,6 +102,8 @@ interface WalkerState { * in the same coordinate space the python oracle sees. */ yOffset: Map warnings: string[] + /** Pin types from computeConnectionTypes; empty without a TypeContext. */ + connTypes: Map } function rungHeight(rung: RFRung): number { @@ -152,7 +154,7 @@ function isVariableNode(node: RFNode): boolean { /* ─────────────────────────── public entry ───────────────────────────────── */ -export function emitLdBody(body: RFBody): EmitResult { +export function emitLdBody(body: RFBody, typeContext?: TypeContext): 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 @@ -169,9 +171,12 @@ export function emitLdBody(body: RFBody): EmitResult { triggerVars: [], functionTempVars: [], emittedBlocks: new Set(), + consumersByBlock: new Map(), + emittedSinks: new Set(), connectorExprs: new Map(), yOffset: new Map(), warnings: [], + connTypes: typeContext ? computeConnectionTypes(body, typeContext) : new Map(), } // Index every node + edge from every rung up front. Sinks are then @@ -246,8 +251,24 @@ export function emitLdBody(body: RFBody): EmitResult { 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) + // Index each block's output-write sinks (coils / output / inOut + // variables fed solely by that block). These are emitted right after + // the block's call by `emitBlockConsumers` — wherever the call lands, + // eager or lazy — and skipped in the sweep below. Otherwise a block + // numbered ahead of its output variable (write left at + // executionOrderId 0) emits its call in the ordered pass while the + // assignment waits for the unordered pass, so a later block in the + // rung reads the variable before it is written (#830). + for (const node of [...ordered, ...others]) { + const blockId = blockFedSink(state, node) + if (blockId === null) continue + const list = state.consumersByBlock.get(blockId) ?? [] + list.push(node) + state.consumersByBlock.set(blockId, list) + } + + emitSinksWithCoilGrouping(state, ordered) + emitSinksWithCoilGrouping(state, others) const bodySt = '\n' + state.program.map((c) => c[0]).join('') const syntheticVars: SyntheticVar[] = [ @@ -275,6 +296,25 @@ function compareNodePosition(state: WalkerState, a: RFNode, b: RFNode): number { return ax - bx } +/** + * If `node` is an output-write sink (coil / output- or inOut-variable) + * whose value comes solely from a single block, return that block's id. + * Such a sink is the block's output binding — it must emit right after + * the block's call so downstream blocks read the written value, not a + * stale one (#830). Returns null for anything else (multi-source + * sinks, non-block sources, blocks, connectors). + */ +function blockFedSink(state: WalkerState, node: RFNode): string | null { + if (node.type !== 'coil' && !isVariableNode(node)) return null + const edges = state.incoming.get(node.id) ?? [] + // Only direct block->sink edges couple; sinks fed through a + // passthrough node or with extra wires stay on the positional sweep. + if (edges.length !== 1) return null + const src = state.byId.get(edges[0].source) + if (src === undefined || src.type !== 'block') return null + return src.id +} + 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 @@ -293,8 +333,111 @@ function emitSink(state: WalkerState, node: RFNode): void { if (node.type === 'connector') return emitConnectorNode(state, node) } +/** + * Emit sinks in order, collapsing SET/RESET coils that branch off the + * SAME upstream source into a single `IF`. Parallel coils share one + * energization path, so the rung condition must be evaluated ONCE and + * applied to every branch — emitting a separate `IF` per coil + * re-evaluates the condition between assignments, which is wrong when + * the condition reads a coil an earlier branch just set + * (e.g. `IF NOT(coils1)... coils1 := TRUE`). + * + * Same-source coils are gathered even when they are NOT adjacent in + * emission order: a coil fed by their merge (so it shares neither + * source) can sort between two parallel branches by position, yet the + * branches must still collapse into one `IF`. The group is emitted at + * the first branch's slot; the intervening sink (and any other + * downstream work) follows after — which is also its dataflow order, + * since a merge-fed sink is downstream of the branches it consumes. + */ +function emitSinksWithCoilGrouping(state: WalkerState, sinks: RFNode[]): void { + for (let i = 0; i < sinks.length; i++) { + if (state.emittedSinks.has(sinks[i].id)) continue + const key = setResetCoilGroupKey(state, sinks[i]) + if (key === null) { + state.emittedSinks.add(sinks[i].id) + emitSink(state, sinks[i]) + continue + } + const group: RFNode[] = [sinks[i]] + for (let j = i + 1; j < sinks.length; j++) { + if (state.emittedSinks.has(sinks[j].id)) continue + if (setResetCoilGroupKey(state, sinks[j]) === key) group.push(sinks[j]) + } + for (const g of group) state.emittedSinks.add(g.id) + if (group.length === 1) emitCoilNode(state, sinks[i]) + else emitCoilGroup(state, group) + } +} + +/** + * Emit a block's output-write sinks (coils / output / inOut variables + * fed solely by it) right after its call — wherever that call lands. + * Reuses the coil-grouping sweep so parallel SET/RESET coils off the + * same block still collapse into one `IF`. Idempotent via + * `state.emittedSinks`, so the main sweep skips what is emitted here. + */ +function emitBlockConsumers(state: WalkerState, blockId: string): void { + const consumers = state.consumersByBlock.get(blockId) + if (consumers === undefined) return + emitSinksWithCoilGrouping(state, consumers) +} + +/** + * Group identity for a SET/RESET coil: the sorted set of its upstream + * source node ids. Two coils with the same key branch off the same + * point (parallel rails) and therefore share a condition. Returns + * null for anything not groupable (non-coils, plain/negated/edge + * coils, or an unconnected coil). + */ +function setResetCoilGroupKey(state: WalkerState, node: RFNode): string | null { + if (node.type !== 'coil') return null + const data = asCoilData(node.data) + if (data === null || (data.variant !== 'set' && data.variant !== 'reset')) return null + const sources = (state.incoming.get(node.id) ?? []).map((e) => e.source).sort() + if (sources.length === 0) return null + return sources.join('|') +} + /* ─────────────────────────── coil emission ──────────────────────────────── */ +/** + * Emit a group of parallel SET/RESET coils under one shared `IF`. + * The condition is computed once from the first coil's upstream (all + * coils in the group share it by construction), then each branch's + * assignment is written inside the single `THEN` body. + */ +function emitCoilGroup(state: WalkerState, coils: RFNode[]): void { + const first = coils[0] + const paths = pathsFromIncoming(state, first.id, /*order=*/ false) + if (paths.length === 0) { + // Shared upstream resolved to nothing — fall back to per-coil + // emission so each surfaces its own "must be connected" warning. + for (const coil of coils) emitCoilNode(state, coil) + return + } + const expr = pathsToChunks(paths) + const firstData = asCoilData(first.data) + const firstInfo: Location = [state.tagName, 'coil', locId(first)] + + state.program.push([`${state.currentIndent}IF `, [...firstInfo, firstData?.variant ?? 'set']]) + for (const chunk of expr) state.program.push(chunk) + state.program.push([' THEN\n', []]) + + const inner = state.currentIndent + ' ' + for (const coil of coils) { + const data = asCoilData(coil.data) + if (data === null) continue + const storage = data.variant === 'reset' ? 'reset' : 'set' + const value = storage === 'set' ? 'TRUE' : 'FALSE' + const info: Location = [state.tagName, 'coil', locId(coil)] + state.program.push([inner, []]) + state.program.push([data.variable, [...info, 'reference']]) + state.program.push([` := ${value}; (*${storage}*)\n`, []]) + } + state.program.push([`${state.currentIndent}END_IF;\n`, []]) +} + function emitCoilNode(state: WalkerState, node: RFNode): void { const data = asCoilData(node.data) if (data === null) { @@ -670,6 +813,7 @@ function emitFunctionBlockCall(state: WalkerState, node: RFNode, data: BlockData for (const chunk of parts[i]) state.program.push(chunk) } state.program.push([');\n', []]) + emitBlockConsumers(state, node.id) } function emitFunctionCall(state: WalkerState, node: RFNode, data: BlockData): void { @@ -678,7 +822,8 @@ function emitFunctionCall(state: WalkerState, node: RFNode, data: BlockData): vo 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 + // python only ever sees wired pins for extensible blocks (DIV-18) + const allInputConnected = data.extensible || wiredInputs.length === data.inputs.length const useNamedArgs = data.outputs.length > 1 || !allInputConnected const recurseOrdered = data.executionOrder > 0 @@ -692,12 +837,10 @@ function emitFunctionCall(state: WalkerState, node: RFNode, data: BlockData): vo 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' + const tempType = state.connTypes.get(pinOut(node.id, out)) ?? (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) { @@ -723,6 +866,7 @@ function emitFunctionCall(state: WalkerState, node: RFNode, data: BlockData): vo } state.program.push([');\n', []]) void primaryFormal + emitBlockConsumers(state, node.id) } function buildInputArgs( diff --git a/src/backend/shared/types/PLC/open-plc.ts b/src/backend/shared/types/PLC/open-plc.ts index 4a5fa1b57..31c808344 100644 --- a/src/backend/shared/types/PLC/open-plc.ts +++ b/src/backend/shared/types/PLC/open-plc.ts @@ -705,6 +705,21 @@ const SDOConfigurationEntrySchema = z.object({ objectName: z.string(), }) +/** + * CiA 402 SoftMotion axis configuration on a recognized EtherCAT drive. When + * present and enabled, the device is treated as a SoftMotion axis: at compile + * time the editor generates an AXIS_REF_SM3 global (named after the device), + * located scalar globals bound to the drive's CiA 402 PDO addresses, and a + * per-scan SM_Drive_GenericDS402 bridge. Scaling mirrors the AXIS_REF_SM3 + * fields (increments per unit = scaleFactor * scaleNum / scaleDenom). + */ +const Cia402AxisConfigSchema = z.object({ + enabled: z.boolean(), + scaleNum: z.number(), + scaleDenom: z.number(), + scaleFactor: z.number(), +}) + const ConfiguredEtherCATDeviceSchema = z.object({ id: z.string(), position: z.number().optional(), @@ -721,6 +736,8 @@ const ConfiguredEtherCATDeviceSchema = z.object({ txPdos: z.array(PersistedPdoSchema).optional(), slaveType: z.string().optional(), sdoConfigurations: z.array(SDOConfigurationEntrySchema).optional(), + /** Present when this drive is a CiA 402 SoftMotion axis (see schema above). */ + cia402: Cia402AxisConfigSchema.optional(), }) const EtherCATMasterConfigSchema = z.object({ diff --git a/src/backend/shared/utils/PLC/__tests__/collect-library-blocks.test.ts b/src/backend/shared/utils/PLC/__tests__/collect-library-blocks.test.ts index a6949a688..879f64f49 100644 --- a/src/backend/shared/utils/PLC/__tests__/collect-library-blocks.test.ts +++ b/src/backend/shared/utils/PLC/__tests__/collect-library-blocks.test.ts @@ -1,6 +1,6 @@ /** * Tests for collectLibraryBlocks — the pure project→ collector that - * embeds used library-block signatures for the xml2st transpiler. + * embeds used library-block signatures. */ import type { PLCProjectData } from '@root/middleware/shared/ports/open-plc-types' @@ -75,7 +75,7 @@ describe('collectLibraryBlocks', () => { ]) const result = collectLibraryBlocks(project) as any - expect(result.data['@name']).toBe('openplc.org/xml2st/library-blocks') + expect(result.data['@name']).toBe('openplc.org/library-blocks') const pous = result.data.libraryBlocks.pou expect(pous).toHaveLength(1) expect(pous[0]).toMatchObject({ diff --git a/src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts b/src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts index 109380992..8fbe49400 100644 --- a/src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts +++ b/src/backend/shared/utils/PLC/__tests__/preprocess-pous.test.ts @@ -310,4 +310,71 @@ describe('preprocessPous — mixed', () => { const { projectData } = preprocessPous(project, false, logger.log) expect(projectData.originalCppPous).toBeUndefined() }) + + it('generates SoftMotion axis artifacts for a CiA 402 EtherCAT drive', () => { + const project: PLCProjectData = { + ...makeProjectData([makeStPou('main', 'pwr(Axis := X_Axis, Enable := TRUE);')]), + remoteDevices: [ + { + name: 'ethercat-bus', + protocol: 'ethercat', + ethercatConfig: { + devices: [ + { + id: 'd1', + name: 'X_Axis', + esiDeviceRef: { repositoryItemId: 'r', deviceIndex: 0 }, + vendorId: '0x0', + productCode: '0x0', + revisionNo: '0x0', + addedFrom: 'repository', + config: {}, + cia402: { enabled: true, scaleNum: 1, scaleDenom: 1, scaleFactor: 1 }, + channelInfo: [ + { + channelId: 'c1', + name: 'Controlword', + direction: 'output', + pdoIndex: '0x1600', + entryIndex: '0x6040', + entrySubIndex: '0x0', + dataType: 'UINT', + bitLen: 16, + iecType: 'UINT', + }, + { + channelId: 'c2', + name: 'Statusword', + direction: 'input', + pdoIndex: '0x1A00', + entryIndex: '0x6041', + entrySubIndex: '0x0', + dataType: 'UINT', + bitLen: 16, + iecType: 'UINT', + }, + ], + channelMappings: [ + { channelId: 'c1', iecLocation: '%QW0' }, + { channelId: 'c2', iecLocation: '%IW0' }, + ], + }, + ], + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any, + ], + } + const logger = collectLog() + const { projectData } = preprocessPous(project, false, logger.log) + // Bridge program + axis global generated; VAR_EXTERNAL injected into main. + expect(projectData.pous.some((p) => p.name === '__sm3_bridge')).toBe(true) + expect( + projectData.configurations.resource.globalVariables.some( + (g) => g.name === 'X_Axis' && g.type.value === 'AXIS_REF_SM3', + ), + ).toBe(true) + const main = projectData.pous.find((p) => p.name === 'main')! + expect(main.interface!.variables.some((v) => v.name === 'X_Axis' && v.class === 'external')).toBe(true) + }) }) diff --git a/src/backend/shared/utils/PLC/__tests__/split-program-st.test.ts b/src/backend/shared/utils/PLC/__tests__/split-program-st.test.ts index 14e2af57f..bc64d7aaf 100644 --- a/src/backend/shared/utils/PLC/__tests__/split-program-st.test.ts +++ b/src/backend/shared/utils/PLC/__tests__/split-program-st.test.ts @@ -49,8 +49,8 @@ describe('splitProgramSt', () => { expect(result!.files.get('Main.st')).toBe(source) }) - it('matches POU names case-insensitively (xml2st may upper-case)', () => { - // The editor's project model has the user-typed casing; xml2st + it('matches POU names case-insensitively (the transpiler may upper-case)', () => { + // The editor's project model has the user-typed casing; the transpiler // sometimes upper-cases identifiers. The splitter must handle // either direction. const source = 'PROGRAM MAIN\n VAR x : INT; END_VAR\n x := 1;\nEND_PROGRAM\n' @@ -186,7 +186,7 @@ describe('splitProgramSt', () => { expect(result!.files.has('State_Display.st')).toBe(false) }) - it('emits `.st` for ST and graphical POUs (xml2st renders them as ST)', () => { + it('emits `.st` for ST and graphical POUs (the transpiler renders them as ST)', () => { const source = 'PROGRAM Main_LD\n VAR x : INT; END_VAR\n x := 1;\nEND_PROGRAM\n' + 'PROGRAM Main_ST\n VAR y : INT; END_VAR\n y := 2;\nEND_PROGRAM\n' @@ -243,9 +243,9 @@ describe('splitProgramSt', () => { }) }) - describe('realistic xml2st-shaped output', () => { + describe('realistic transpiler-shaped output', () => { it('handles a multi-POU + TYPE + CONFIGURATION program', () => { - // Mimics the shape xml2st emits for a typical project. + // Mimics the shape the transpiler emits for a typical project. const source = `TYPE TrafficState : (RED, YELLOW, GREEN); END_TYPE diff --git a/src/backend/shared/utils/PLC/collect-library-blocks.ts b/src/backend/shared/utils/PLC/collect-library-blocks.ts index 13dbe2ff6..e204ca496 100644 --- a/src/backend/shared/utils/PLC/collect-library-blocks.ts +++ b/src/backend/shared/utils/PLC/collect-library-blocks.ts @@ -2,15 +2,15 @@ import type { PLCProjectData } from '@root/middleware/shared/ports/open-plc-type /** * Collect the signatures of every *library* block a project uses and emit them - * as a PLCopen `` payload that the xml2st transpiler reads. + * as a PLCopen `` payload embedded in the exported project XML. * - * Why: xml2st emits a local temporary per FUNCTION output and must declare it - * with a concrete type. It infers that type from the wired connections, but - * cannot when a function has no typed pin to borrow from (e.g. a nullary - * `CURRENT_DT` whose output is unconnected) — it then falls back to the illegal - * type `ANY`, which STruC++ rejects. Rather than make xml2st carry a block - * library (which would diverge from the real STruC++ library), we hand it the - * exact signatures the project uses, embedded in the project file itself. + * Why: an ST generator emits a local temporary per FUNCTION output and must + * declare it with a concrete type. It infers that type from the wired + * connections, but cannot when a function has no typed pin to borrow from + * (e.g. a nullary `CURRENT_DT` whose output is unconnected) — it would then + * fall back to the illegal type `ANY`, which STruC++ rejects. Embedding the + * exact signatures the project uses avoids carrying a separate block library + * that could diverge from the real STruC++ library. * * Every graphical block instance already carries its full typed signature in * `node.data.variant` (the editor stamps it from the library on placement), so @@ -19,10 +19,10 @@ import type { PLCProjectData } from '@root/middleware/shared/ports/open-plc-type * identical across the desktop and web builds. * * Output shape feeds xmlbuilder2 (see XmlGenerator); it is inserted as - * `/` after ``. Contract: xml2st/docs/library-blocks.md. + * `/` after ``. */ -const DATA_NAME = 'openplc.org/xml2st/library-blocks' +const DATA_NAME = 'openplc.org/library-blocks' type XmlElement = Record @@ -41,7 +41,7 @@ type BlockVariant = { variables: VariantVariable[] } -/** ``, ``, ... — xml2st reads the (upper-cased) tag name. */ +/** ``, ``, ... — the consumer reads the (upper-cased) tag name. */ const typeElement = (variable: VariantVariable): XmlElement => ({ [variable.type.value]: '' }) const variableElement = (variable: VariantVariable): XmlElement => ({ @@ -78,7 +78,7 @@ const collectBlockVariants = (project: PLCProjectData): BlockVariant[] => { const variantToPou = (variant: BlockVariant): XmlElement => { const isFunctionBlock = variant.type === 'function-block' - // EN/ENO are implicit control pins; xml2st adds them itself. + // EN/ENO are implicit control pins; the ST generator adds them itself. const vars = variant.variables.filter((v) => v.name !== 'EN' && v.name !== 'ENO') const inputs = vars.filter((v) => v.class === 'input') const inouts = vars.filter((v) => v.class === 'inOut') @@ -113,7 +113,7 @@ const variantToPou = (variant: BlockVariant): XmlElement => { * project references no library blocks (e.g. text-only POUs). * * User-defined POUs are excluded: their definitions already travel in - * `` and xml2st resolves them directly. + * `` and the generator resolves them directly. */ export const collectLibraryBlocks = (project: PLCProjectData): XmlElement | null => { const userPouNames = new Set(project.pous.map((pou) => pou.data?.name)) diff --git a/src/backend/shared/utils/PLC/preprocess-pous.ts b/src/backend/shared/utils/PLC/preprocess-pous.ts index bced661cf..3e2760274 100644 --- a/src/backend/shared/utils/PLC/preprocess-pous.ts +++ b/src/backend/shared/utils/PLC/preprocess-pous.ts @@ -5,6 +5,7 @@ import { addPythonLocalVariables } from '../../../../frontend/utils/python/addPy import { generateSTCode } from '../../../../frontend/utils/python/generateSTCode' import { injectPythonCode } from '../../../../frontend/utils/python/injectPythonCode' import type { PLCPou, PLCProjectData, PLCVariable } from '../../../../middleware/shared/ports/types' +import { generateSoftMotionArtifacts } from '../../ethercat/generate-softmotion' type CppPouData = { name: string @@ -174,6 +175,14 @@ function preprocessPous(projectData: PLCProjectData, isSimulator: boolean, log: log('info', `Successfully processed ${cppPous.length} C/C++ POU(s)`) } + // --- SoftMotion: generate AXIS_REF_SM3 globals + PDO scalars + drive bridge + // for CiA 402 EtherCAT axes (no-op when the project has none). --- + const withMotion = generateSoftMotionArtifacts(processedProjectData) as ProjectDataWithCpp + if (withMotion !== processedProjectData) { + processedProjectData = withMotion + log('info', 'Generated SoftMotion axis bindings for CiA 402 drive(s)') + } + return { projectData: processedProjectData as ProjectDataWithCpp, validationFailed: false } } diff --git a/src/backend/shared/utils/PLC/split-program-st.ts b/src/backend/shared/utils/PLC/split-program-st.ts index eb5f9bf54..b282bf054 100644 --- a/src/backend/shared/utils/PLC/split-program-st.ts +++ b/src/backend/shared/utils/PLC/split-program-st.ts @@ -1,14 +1,14 @@ /** - * Split the monolithic `program.st` produced by xml2st into one + * Split the monolithic `program.st` produced by the ST transpiler into one * synthetic source file per POU, plus auxiliary files for the project- * level sections (`_types.st`, `_globals.st`, `_config.st`). The * editor feeds the result to strucpp via `additionalSources`, so error * reports come back with `error.file === '.st'` instead of a * generic `program.st` line. * - * Why post-process here instead of changing xml2st: xml2st is being - * abandoned, and Runtime v3 (MatIEC era) ingests the monolithic - * `program.st` verbatim — splitting upstream would break that target. + * Why post-process here rather than upstream: Runtime v3 (MatIEC era) + * ingests the monolithic `program.st` verbatim — splitting upstream + * would break that target. * The editor already knows the project's POU list (it produced the XML * the splitter consumes), so the operation is name-anchored and * deterministic, not a generic ST parse. @@ -22,7 +22,7 @@ export interface KnownPou { /** POU name as the user knows it. Strucpp uppercases internally; we - * match case-insensitively against xml2st output. */ + * match case-insensitively against the transpiler output. */ name: string kind: 'PROGRAM' | 'FUNCTION' | 'FUNCTION_BLOCK' /** @@ -62,7 +62,7 @@ interface RangeMatch { * Find the line index (1-indexed, inclusive) where a POU header for * `(name, kind)` starts in `lines`. Returns -1 when no match is found. * - * The header pattern is intentionally tight: xml2st always emits the + * The header pattern is intentionally tight: the transpiler always emits the * keyword at column 0 followed by the POU name and either a colon * (functions return-type), whitespace, or end-of-line. This rules out * matches on identifiers that contain the POU name as a substring diff --git a/src/backend/shared/utils/PLC/xml-generator.ts b/src/backend/shared/utils/PLC/xml-generator.ts index 211b0c794..a9c3e573a 100644 --- a/src/backend/shared/utils/PLC/xml-generator.ts +++ b/src/backend/shared/utils/PLC/xml-generator.ts @@ -71,7 +71,7 @@ const XmlGenerator = ( } /** - * Embed the signatures of every library block the project uses, so xml2st + * Embed the signatures of every library block the project uses, so an ST generator * can type the temporaries it generates for FUNCTION outputs without * carrying a block library of its own. Added last so it serialises after * , as the PLCopen schema requires for . diff --git a/src/backend/shared/utils/__tests__/parse-project-files.test.ts b/src/backend/shared/utils/__tests__/parse-project-files.test.ts index c93a423f8..929a0f8b0 100644 --- a/src/backend/shared/utils/__tests__/parse-project-files.test.ts +++ b/src/backend/shared/utils/__tests__/parse-project-files.test.ts @@ -212,6 +212,60 @@ describe('parseProjectFiles — fallback POU creation', () => { consoleSpy.mockRestore() }) + it('derives the fallback name from the basename even for Windows backslash paths', () => { + // The desktop reader builds relativePaths with path.join → backslashes on + // Windows. A parse failure must still yield the bare basename, not the whole + // `pous\functions\...` path (the origin of the deleting-function corruption). + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const pouFiles: RawProjectFile[] = [ + { + relativePath: 'pous\\functions\\Siren_FC.st', + // Missing END_FUNCTION / return type → parser throws → fallback. + content: 'FUNCTION Siren_FC\nVAR_INPUT\n x : INT;\nEND_VAR\nbody', + }, + ] + const result = parseProjectFiles('/p', makeProjectJson(), makeDeviceConfig(), makePinMapping(), pouFiles, [], []) + expect(result.projectData.pous).toHaveLength(1) + expect(result.projectData.pous[0].name).toBe('Siren_FC') + consoleSpy.mockRestore() + }) + + it('warns and preserves declarations when a PROGRAM has a located interface-class variable (issue #904)', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const pouFiles: RawProjectFile[] = [ + { + relativePath: 'pous/programs/main.st', + content: + 'PROGRAM main\nVAR_OUTPUT\n Q1 : BOOL AT %QX0.0;\nEND_VAR\nVAR\n latch : RS;\nEND_VAR\n\n\nEND_PROGRAM', + }, + ] + const result = parseProjectFiles('/p', makeProjectJson(), makeDeviceConfig(), makePinMapping(), pouFiles, [], []) + // Falls back: the structured variable list is empty, but the raw + // declarations survive in variablesText for in-app repair. + expect(result.projectData.pous).toHaveLength(1) + expect(result.projectData.pous[0].interface?.variables).toEqual([]) + expect(result.projectData.pous[0].variablesText).toContain('AT %QX0.0') + // The failure is surfaced: names the POU and file, states the offending + // rule, and points at the repair path. + expect(result.warnings).toBeDefined() + const warning = result.warnings!.find((w) => w.includes('pous/programs/main.st')) + expect(warning).toContain('POU "main"') + expect(warning).toMatch(/Location \("AT"\) is not allowed for variables of class "OUTPUT"/) + expect(warning).toContain('code view') + consoleSpy.mockRestore() + }) + + it('warns with a partial-data message when a graphical POU fails to parse', () => { + const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + const pouFiles: RawProjectFile[] = [ + { relativePath: 'pous/programs/FbdPou.fbd', content: 'PROGRAM FbdPou\nnot valid json\nEND_PROGRAM' }, + ] + const result = parseProjectFiles('/p', makeProjectJson(), makeDeviceConfig(), makePinMapping(), pouFiles, [], []) + expect(result.warnings).toBeDefined() + expect(result.warnings!.some((w) => w.includes('FbdPou') && w.includes('partial data'))).toBe(true) + consoleSpy.mockRestore() + }) + it('fallback extracts documentation from (* ... *) comments', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const pouFiles: RawProjectFile[] = [ diff --git a/src/backend/shared/utils/parse-project-files.ts b/src/backend/shared/utils/parse-project-files.ts index 646885ec5..0564d583d 100644 --- a/src/backend/shared/utils/parse-project-files.ts +++ b/src/backend/shared/utils/parse-project-files.ts @@ -111,11 +111,16 @@ function getLanguageFromExt(relativePath: string): string | null { /** * Extract the base filename without extension from a relative path. + * + * Splits on BOTH separators: the desktop reader builds relative paths with + * `path.join`, which emits backslashes on Windows, so a `/`-only split would + * return the whole `pous\functions\Name` path as the "basename" — the origin of + * the POU name→path corruption in the "deleting function" bug. */ function getBaseNameFromPath(relativePath: string): string { return ( relativePath - .split('/') + .split(/[\\/]/) .pop() ?.replace(/\.\w+$/, '') ?? 'unknown' ) @@ -257,7 +262,7 @@ function foldLegacyVariableAliases(value: unknown): unknown { return value } -function parsePouFile(file: RawProjectFile): (PLCPou & { variablesText?: string }) | null { +function parsePouFile(file: RawProjectFile, warnings: string[]): (PLCPou & { variablesText?: string }) | null { const ext = file.relativePath.split('.').pop()?.toLowerCase() /* istanbul ignore if -- defensive: parseProjectFiles upstream only forwards files whose extension matched the POU file glob; an extension-less file path can never reach here */ @@ -303,9 +308,20 @@ function parsePouFile(file: RawProjectFile): (PLCPou & { variablesText?: string } } catch (err) { console.error(`[parseProjectFiles] Failed to parse POU: ${file.relativePath}`, err) + const pouName = getBaseNameFromPath(file.relativePath) + const reason = + err instanceof Error ? err.message : /* istanbul ignore next -- every parser throw site uses Error */ String(err) + // Surface the failure on project open (the console panel shows these + // warnings) instead of silently loading the POU with no variables — + // GitHub issue #904. For textual POUs the raw declarations survive in + // `variablesText`, so point the user at the in-app repair path. + warnings.push( + language === 'st' || language === 'il' + ? `POU "${pouName}" (${file.relativePath}) could not be fully parsed: ${reason} Its variable declarations were preserved as raw text — open the POU's variables editor in code view, fix the declaration, and save.` + : `POU "${pouName}" (${file.relativePath}) could not be fully parsed and was loaded with partial data: ${reason}`, + ) // Fallback: preserve as much data as possible try { - const pouName = getBaseNameFromPath(file.relativePath) return createFallbackPou(file.content, language, pouType, pouName) } catch (fallbackErr) { /* istanbul ignore next -- defensive: createFallbackPou itself is non-throwing for any @@ -465,7 +481,7 @@ export function parseProjectFiles( // Parse POU files const pous: (PLCPou & { variablesText?: string })[] = [] for (const file of filteredPouFiles) { - const pou = parsePouFile(file) + const pou = parsePouFile(file, warnings) if (pou) { // Ensure all POUs have a name (derive from filename if missing) if (!pou.name) { diff --git a/src/frontend/assets/icons/interface/SoftMotion.tsx b/src/frontend/assets/icons/interface/SoftMotion.tsx new file mode 100644 index 000000000..074af025c --- /dev/null +++ b/src/frontend/assets/icons/interface/SoftMotion.tsx @@ -0,0 +1,46 @@ +import { ComponentProps } from 'react' + +import { cn } from '../../../utils/cn' + +type ISoftMotionIconProps = ComponentProps<'svg'> & { + size?: 'sm' | 'md' | 'lg' +} + +const sizeClasses = { + sm: 'w-5 h-5', + md: 'w-8 h-8', + lg: 'w-12 h-12', +} + +/** + * SoftMotion (CiA 402 servo axis) icon — a rounded device tile with a rotary + * motion glyph (a circular arrow around a hub), in teal to distinguish a + * recognized SoftMotion drive from a plain EtherCAT slave in the project tree. + */ +export const SoftMotionIcon = (props: ISoftMotionIconProps) => { + const { className, size = 'sm', ...res } = props + return ( + + + {/* rotary arc suggesting axis rotation */} + + {/* arrowhead closing the arc */} + + {/* motor hub */} + + + ) +} diff --git a/src/frontend/assets/icons/project/Users.tsx b/src/frontend/assets/icons/project/Users.tsx new file mode 100644 index 000000000..18ea4c13f --- /dev/null +++ b/src/frontend/assets/icons/project/Users.tsx @@ -0,0 +1,38 @@ +import { ComponentProps } from 'react' + +import { cn } from '../../../utils/cn' + +type IUsersIconProps = ComponentProps<'svg'> & { + size?: 'sm' | 'md' | 'lg' +} + +const sizeClasses = { + sm: 'w-5 h-5', + md: 'w-6 h-6', + lg: 'w-12 h-12', +} + +export const UsersIcon = (props: IUsersIconProps) => { + const { className, size = 'sm', ...res } = props + return ( + + + + + + + ) +} diff --git a/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx b/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx index 5a2400ccc..0ff9b9174 100644 --- a/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx +++ b/src/frontend/components/_atoms/graphical-editor/autocomplete/index.tsx @@ -230,7 +230,13 @@ export const GraphicalEditorAutocomplete = forwardRef {selectableValues.length > 0 && ( {variables && variables.length > 0 && ( <> -
-
+
+ {/* `scrollbar-gutter: stable` reserves the scrollbar's + width so the content-based auto-size accounts for it — + otherwise, when the list overflows and the scrollbar + appears, it steals horizontal space and the widest name + wraps its last character. */} +
{variables.map((variable) => (
- {variable.name} + {variable.name}
))}
@@ -288,7 +311,7 @@ export const GraphicalEditorAutocomplete = forwardRef
& { const FBDBlockAutoComplete = forwardRef( ({ block: unknownBlock, isOpen, setIsOpen, keyPressed, valueToSearch }: FBDBlockAutoCompleteProps, ref) => { const pouName = useBoundPou() - const { - project: { - data: { pous }, - }, - projectActions: { createVariable }, - fbdFlows, - fbdFlowActions: { updateNode, addNode }, - } = useOpenPLCStore() + const pous = useOpenPLCStore((state) => state.project.data.pous) + const createVariable = useOpenPLCStore((state) => state.projectActions.createVariable) + const fbdFlows = useOpenPLCStore((state) => state.fbdFlows) + const { updateNode, addNode } = useOpenPLCStore((state) => state.fbdFlowActions) const block = unknownBlock as Node & { positionAbsoluteX?: number; positionAbsoluteY?: number } const { edges, rung } = useMemo(() => { @@ -134,6 +130,16 @@ const FBDBlockAutoComplete = forwardRef({ }) => { const pouName = useBoundPou() const editor = useBoundEditorModel() - const { - editorActions: { updateModelVariables, updateModelFBD }, - libraries, - fbdFlows, - fbdFlowActions: { setNodes, setEdges }, - project, - project: { - data: { pous }, - }, - projectActions: { updateVariable, deleteVariable }, - snapshotActions: { pushToHistory }, - } = useOpenPLCStore() + const { updateModelVariables, updateModelFBD } = useOpenPLCStore((state) => state.editorActions) + const { setNodes, setEdges } = useOpenPLCStore((state) => state.fbdFlowActions) + const { updateVariable, deleteVariable } = useOpenPLCStore((state) => state.projectActions) + const pushToHistory = useOpenPLCStore((state) => state.snapshotActions.pushToHistory) const { name: blockName, @@ -82,9 +75,6 @@ export const BlockNodeElement = ({ const inputNameRef = useRef(null) const [inputNameFocus, setInputNameFocus] = useState(true) - const { pou, rung, node, variables, edges } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { - nodeId: nodeId ?? '', - }) /** * useEffect to focus the name input when the correct block type is selected */ @@ -119,6 +109,16 @@ export const BlockNodeElement = ({ return } + const { project, libraries, fbdFlows } = useOpenPLCStore.getState() + const { pou, rung, node, variables, edges } = getFBDPouVariablesRungNodeAndEdges( + pouName, + project.data.pous, + fbdFlows, + { + nodeId: nodeId ?? '', + }, + ) + const libraryBlock = libraries.system.flatMap((block) => block.pous).find((pou) => pou.name === blockNameValue) if (!libraryBlock) { @@ -328,20 +328,16 @@ export const BlockNodeElement = ({ ) } -export const Block = (block: BlockProps) => { +const Block = (block: BlockProps) => { const { data, dragging, height, width, selected, id } = block const pouName = useBoundPou() - const { - project, - project: { - data: { pous }, - }, - projectActions: { createVariable }, - snapshotActions: { pushToHistory }, - libraries: { user: userLibraries }, - fbdFlows, - fbdFlowActions: { updateNode, setNodes, setEdges }, - } = useOpenPLCStore() + const pous = useOpenPLCStore((state) => state.project.data.pous) + const createVariable = useOpenPLCStore((state) => state.projectActions.createVariable) + const pushToHistory = useOpenPLCStore((state) => state.snapshotActions.pushToHistory) + const { updateNode, setNodes, setEdges } = useOpenPLCStore((state) => state.fbdFlowActions) + // Pou-scoped subscription: immer's structural sharing keeps this flow's + // identity stable when other POUs' flows (or unrelated slices) change. + const flow = useOpenPLCStore((state) => state.fbdFlows.find((f) => f.name === pouName)) const { type: blockType } = (data.variant as BlockVariant) ?? DEFAULT_BLOCK_TYPE const documentation = getBlockDocumentation(data.variant as BlockVariant) @@ -349,7 +345,7 @@ export const Block = (block: BlockProps) => { const [wrongVariable, setWrongVariable] = useState(false) const [hoveringBlock, setHoveringBlock] = useState(false) - const { rung, node, variables } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { + const { rung, node, variables } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, flow ? [flow] : [], { nodeId: id ?? '', }) @@ -456,6 +452,7 @@ export const Block = (block: BlockProps) => { return } + const { fbdFlows } = useOpenPLCStore.getState() const { rung, node, variables } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { nodeId: id, }) @@ -508,7 +505,14 @@ export const Block = (block: BlockProps) => { if (matchingVariable) { variableToLink = matchingVariable } else if (createIfNotFound) { - const pouData = project.data.pous.find((p) => p.name === pouName) + // An entry that can't be a new variable NAME — a member/array reference, + // a typed literal (`T#500ms`), a reserved word — is bound to the block + // verbatim as a constant/reference instead of erroring. + if (!isLegalIdentifier(variableNameToSubmit)[0]) { + updateNodeVariable({ name: variableNameToSubmit }) + return + } + const pouData = pous.find((p) => p.name === pouName) pushToHistory(pouName, { variables: pouData?.interface?.variables ?? [], body: pouData?.body.value, @@ -550,6 +554,7 @@ export const Block = (block: BlockProps) => { } const handleUpdateDivergence = () => { + const { fbdFlows, libraries } = useOpenPLCStore.getState() const { rung, node, pou } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { nodeId: id, }) @@ -558,7 +563,7 @@ export const Block = (block: BlockProps) => { const variant = (node.data as BlockNodeData)?.variant if (!variant) return - const libMatch = userLibraries.find((lib) => lib.name === variant.name && lib.type === variant.type) + const libMatch = libraries.user.find((lib) => lib.name === variant.name && lib.type === variant.type) if (!libMatch) return const libPou = pous.find((pou) => pou.name === libMatch.name) @@ -819,3 +824,8 @@ export const Block = (block: BlockProps) => {
) } + +// Cast keeps the generic call signature `memo` would otherwise widen away. +const exportBlock = memo(Block) as typeof Block + +export { exportBlock as Block } diff --git a/src/frontend/components/_atoms/graphical-editor/fbd/comment.tsx b/src/frontend/components/_atoms/graphical-editor/fbd/comment.tsx index cd6c23176..8760887e9 100644 --- a/src/frontend/components/_atoms/graphical-editor/fbd/comment.tsx +++ b/src/frontend/components/_atoms/graphical-editor/fbd/comment.tsx @@ -12,14 +12,8 @@ import { CommentNode, CommentProps } from './utils/types' const CommentElement = (block: CommentProps) => { const { id, selected, data, width, height } = block const pouName = useBoundPou() - const { - editorActions: { updateModelFBD }, - fbdFlows, - fbdFlowActions: { updateNode }, - project: { - data: { pous }, - }, - } = useOpenPLCStore() + const updateModelFBD = useOpenPLCStore((state) => state.editorActions.updateModelFBD) + const updateNode = useOpenPLCStore((state) => state.fbdFlowActions.updateNode) const blockRef = useRef(null) const inputVariableRef = useRef< @@ -48,7 +42,8 @@ const CommentElement = (block: CommentProps) => { return } - const { node: commentaryBlock } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { + const { project, fbdFlows } = useOpenPLCStore.getState() + const { node: commentaryBlock } = getFBDPouVariablesRungNodeAndEdges(pouName, project.data.pous, fbdFlows, { nodeId: id, }) if (!commentaryBlock) return @@ -98,7 +93,8 @@ const CommentElement = (block: CommentProps) => { }, [commentFocused]) const handleSubmitCommentaryValueOnTextareaBlur = () => { - const { node: commentaryBlock } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { + const { project, fbdFlows } = useOpenPLCStore.getState() + const { node: commentaryBlock } = getFBDPouVariablesRungNodeAndEdges(pouName, project.data.pous, fbdFlows, { nodeId: id, }) if (!commentaryBlock) return diff --git a/src/frontend/components/_atoms/graphical-editor/fbd/connection.tsx b/src/frontend/components/_atoms/graphical-editor/fbd/connection.tsx index ab52cce01..cb6943e85 100644 --- a/src/frontend/components/_atoms/graphical-editor/fbd/connection.tsx +++ b/src/frontend/components/_atoms/graphical-editor/fbd/connection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { memo, useEffect, useRef, useState } from 'react' import { useOpenPLCStore } from '../../../../store' import { cn } from '../../../../utils/cn' @@ -19,14 +19,9 @@ import { getFBDPouVariablesRungNodeAndEdges } from './utils/utils' const ConnectionElement = (block: ConnectionProps) => { const { id, data, selected, type } = block const pouName = useBoundPou() - const { - editorActions: { updateModelFBD }, - fbdFlows, - fbdFlowActions: { updateNode }, - project: { - data: { pous }, - }, - } = useOpenPLCStore() + const updateModelFBD = useOpenPLCStore((state) => state.editorActions.updateModelFBD) + const updateNode = useOpenPLCStore((state) => state.fbdFlowActions.updateNode) + const pous = useOpenPLCStore((state) => state.project.data.pous) const inputConnectionRef = useRef< HTMLTextAreaElement & { @@ -66,6 +61,7 @@ const ConnectionElement = (block: ConnectionProps) => { * Update inputError state when the variable is updated */ useEffect(() => { + const { fbdFlows } = useOpenPLCStore.getState() const { rung, node: connectionNode } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { nodeId: id, }) @@ -95,6 +91,7 @@ const ConnectionElement = (block: ConnectionProps) => { const handleSubmitConnectionValueOnTextareaBlur = (connectionName?: string) => { const connectionNameToSubmit = connectionName || connectionValue + const { fbdFlows } = useOpenPLCStore.getState() const { pou, rung, @@ -254,4 +251,6 @@ const ConnectionElement = (block: ConnectionProps) => { ) } -export { ConnectionElement } +const exportConnectionElement = memo(ConnectionElement) + +export { exportConnectionElement as ConnectionElement } diff --git a/src/frontend/components/_atoms/graphical-editor/fbd/handle.tsx b/src/frontend/components/_atoms/graphical-editor/fbd/handle.tsx index 090edb5a9..d5830878f 100644 --- a/src/frontend/components/_atoms/graphical-editor/fbd/handle.tsx +++ b/src/frontend/components/_atoms/graphical-editor/fbd/handle.tsx @@ -3,6 +3,8 @@ import { Handle, HandleProps } from '@xyflow/react' import { cn } from '../../../../utils/cn' export type CustomHandleProps = HandleProps & { + /** FBD handles always carry an explicit id — strip the `null` xyflow ≥12.11 allows */ + id?: string glbPosition: { x: number y: number @@ -39,7 +41,7 @@ export const CustomHandle = ({ ) } -type BuildHandleProps = HandleProps & { +type BuildHandleProps = Omit & { glbX: number glbY: number relX: number @@ -53,7 +55,7 @@ type BuildHandleProps = HandleProps & { * @param relY: number - The y coordinate of the handle based on the relative position (inside the node) * @returns CustomHandleProps */ -export const buildHandle = ({ glbX, glbY, relX, relY, ...rest }: BuildHandleProps) => { +export const buildHandle = ({ glbX, glbY, relX, relY, ...rest }: BuildHandleProps): CustomHandleProps => { return { glbPosition: { x: glbX, diff --git a/src/frontend/components/_atoms/graphical-editor/fbd/utils/utils.ts b/src/frontend/components/_atoms/graphical-editor/fbd/utils/utils.ts index 535694e28..2ca409646 100644 --- a/src/frontend/components/_atoms/graphical-editor/fbd/utils/utils.ts +++ b/src/frontend/components/_atoms/graphical-editor/fbd/utils/utils.ts @@ -11,6 +11,102 @@ import { buildHandle } from '../handle' import { DEFAULT_BLOCK_CONNECTOR_Y, DEFAULT_BLOCK_CONNECTOR_Y_OFFSET, DEFAULT_BLOCK_WIDTH } from './constants' import type { BasicNodeData } from './types' +type FBDRung = FBDFlowType['rung'] +type FBDRungNode = FBDRung['nodes'][0] +type FBDRungEdge = FBDRung['edges'][0] + +type RungLookups = { + nodeById: Map + edgesBySource: Map + edgesByTarget: Map +} + +// Per-rung lookup tables, cached on the rung's (immutable) identity. This +// util runs during every FBD node render; the previous linear scans made a +// render pass O(nodes × (nodes + edges)). Immer replaces the rung object on +// any change, so a stale entry can never be served. +const rungLookupsCache = new WeakMap() + +const getRungLookups = (rung: FBDRung): RungLookups => { + let lookups = rungLookupsCache.get(rung) + if (!lookups) { + lookups = { + nodeById: new Map(), + edgesBySource: new Map(), + edgesByTarget: new Map(), + } + for (const node of rung.nodes) { + if (!lookups.nodeById.has(node.id)) lookups.nodeById.set(node.id, node) + } + for (const edge of rung.edges) { + const bySource = lookups.edgesBySource.get(edge.source) + if (bySource) bySource.push(edge) + else lookups.edgesBySource.set(edge.source, [edge]) + const byTarget = lookups.edgesByTarget.get(edge.target) + if (byTarget) byTarget.push(edge) + else lookups.edgesByTarget.set(edge.target, [edge]) + } + rungLookupsCache.set(rung, lookups) + } + return lookups +} + +// Variable names are unique per POU (case-insensitive, enforced by the +// variables table), so a first-wins lowercase index matches `find` exactly. +const variablesByNameCache = new WeakMap>() + +const getVariablesByName = (variables: PLCVariable[]): Map => { + let byName = variablesByNameCache.get(variables) + if (!byName) { + byName = new Map() + for (const variable of variables) { + const key = variable.name.toLowerCase() + if (!byName.has(key)) byName.set(key, variable) + } + variablesByNameCache.set(variables, byName) + } + return byName +} + +const EMPTY_EDGES: FBDRungEdge[] = [] + +const selectNodeVariable = ( + node: FBDRungNode, + variables: PLCVariable[], + variableName: string | undefined, +): PLCVariable | undefined => { + const byName = getVariablesByName(variables) + + const findByNodeVarOrFallback = (): PLCVariable | undefined => { + const nodeVarName = (node.data as BasicNodeData).variable.name + if (nodeVarName !== undefined) return byName.get(nodeVarName.toLowerCase()) + if (variableName === undefined) return undefined + const candidate = byName.get(variableName.toLowerCase()) + return candidate?.name === variableName ? candidate : undefined + } + + switch (node.type as keyof typeof customNodeTypes) { + case 'block': { + const nodeVarName = (node.data as BasicNodeData).variable.name + return nodeVarName !== undefined ? byName.get(nodeVarName.toLowerCase()) : undefined + } + case 'connector': + case 'continuation': + case 'comment': + return undefined + case 'input-variable': + case 'output-variable': + case 'inout-variable': + // Variable nodes - allow all types including derived (user-defined types) + return findByNodeVarOrFallback() + default: { + // Other node types - only allow base types (not derived/user-defined) + const candidate = findByNodeVarOrFallback() + return candidate && candidate.type.definition !== 'derived' ? candidate : undefined + } + } +} + // `pouName` is the bound POU for the caller's editor instance (from // `useBoundPou()` under multi-mount, or the active editor's name for // legacy single-mount call sites). Taking it as a string instead of @@ -34,38 +130,11 @@ export const getFBDPouVariablesRungNodeAndEdges = ( } => { const pou = pous.find((pou) => pou.name === pouName) const rung = fbdFlows.find((flow) => flow.name === pouName)?.rung - const node = rung?.nodes.find((node) => node.id === data.nodeId) + const lookups = rung ? getRungLookups(rung) : undefined + const node = lookups?.nodeById.get(data.nodeId) const variables: PLCVariable[] = pou?.interface?.variables ?? [] - let variable = variables.find((variable) => { - if (!node) return undefined - switch (node.type as keyof typeof customNodeTypes) { - case 'block': - return ( - (node.data as BasicNodeData).variable.name !== undefined && - (node.data as BasicNodeData).variable.name.toLowerCase() === variable.name.toLowerCase() - ) - case 'connector': - case 'continuation': - return undefined - case 'comment': - return undefined - case 'input-variable': - case 'output-variable': - case 'inout-variable': - // Variable nodes - allow all types including derived (user-defined types) - return (node.data as BasicNodeData).variable.name !== undefined - ? variable.name.toLowerCase() === (node.data as BasicNodeData).variable.name.toLowerCase() - : variable.name === data.variableName - default: - // Other node types - only allow base types (not derived/user-defined) - return ( - ((node.data as BasicNodeData).variable.name !== undefined - ? variable.name.toLowerCase() === (node.data as BasicNodeData).variable.name.toLowerCase() - : variable.name === data.variableName) && variable.type.definition !== 'derived' - ) - } - }) + let variable = node && variables.length > 0 ? selectNodeVariable(node, variables, data.variableName) : undefined // Fallback: try to resolve as array element access (e.g. "Sensor[0]") if (!variable && node) { @@ -88,8 +157,8 @@ export const getFBDPouVariablesRungNodeAndEdges = ( } } - const edgesThatNodeIsSource = rung?.edges.filter((edge) => edge.source === data.nodeId) - const edgesThatNodeIsTarget = rung?.edges.filter((edge) => edge.target === data.nodeId) + const edgesThatNodeIsSource = lookups ? (lookups.edgesBySource.get(data.nodeId) ?? EMPTY_EDGES) : undefined + const edgesThatNodeIsTarget = lookups ? (lookups.edgesByTarget.get(data.nodeId) ?? EMPTY_EDGES) : undefined return { pou, diff --git a/src/frontend/components/_atoms/graphical-editor/fbd/variable.tsx b/src/frontend/components/_atoms/graphical-editor/fbd/variable.tsx index c556fcb38..3f1a02777 100644 --- a/src/frontend/components/_atoms/graphical-editor/fbd/variable.tsx +++ b/src/frontend/components/_atoms/graphical-editor/fbd/variable.tsx @@ -1,5 +1,5 @@ import * as Popover from '@radix-ui/react-popover' -import { useEffect, useMemo, useRef, useState } from 'react' +import { memo, useEffect, useMemo, useRef, useState } from 'react' import { PLCVariable } from '../../../../../middleware/shared/ports/types' import { useDebugger } from '../../../../../middleware/shared/providers' @@ -41,14 +41,9 @@ import { getFBDPouVariablesRungNodeAndEdges } from './utils/utils' const VariableElement = (block: VariableProps) => { const { id, data, selected } = block const pouName = useBoundPou() - const { - editorActions: { updateModelFBD }, - fbdFlows, - fbdFlowActions: { updateNode }, - project: { - data: { pous }, - }, - } = useOpenPLCStore() + const updateModelFBD = useOpenPLCStore((state) => state.editorActions.updateModelFBD) + const updateNode = useOpenPLCStore((state) => state.fbdFlowActions.updateNode) + const pous = useOpenPLCStore((state) => state.project.data.pous) const debugger_ = useDebugger() const isDebuggerVisible = useIsDebuggerVisible() @@ -87,7 +82,7 @@ const VariableElement = (block: VariableProps) => { /** * Get the connection type */ - const flow = useMemo(() => fbdFlows.find((flow) => flow.name === pouName), [fbdFlows, pouName]) + const flow = useOpenPLCStore((state) => state.fbdFlows.find((flow) => flow.name === pouName)) const connections = useMemo(() => { const rung = flow?.rung @@ -277,7 +272,7 @@ const VariableElement = (block: VariableProps) => { const getVariableType = (): string | undefined => { if (!data.variable || !data.variable.name) return undefined - const { pou } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { nodeId: id }) + const pou = pous.find((pou) => pou.name === pouName) if (!pou) return undefined const variable = (pou.interface?.variables ?? []).find( (v: PLCVariable) => v.name.toLowerCase() === data.variable.name.toLowerCase(), @@ -421,7 +416,8 @@ const VariableElement = (block: VariableProps) => { const handleSubmitVariableValueOnTextareaBlur = (variableName?: string) => { const variableNameToSubmit = variableName || variableValue - const { pou, rung, node } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { + const { project, fbdFlows } = useOpenPLCStore.getState() + const { pou, rung, node } = getFBDPouVariablesRungNodeAndEdges(pouName, project.data.pous, fbdFlows, { nodeId: id, }) if (!pou || !rung || !node) return @@ -700,4 +696,6 @@ const VariableElement = (block: VariableProps) => { ) } -export { VariableElement } +const exportVariableElement = memo(VariableElement) + +export { exportVariableElement as VariableElement } diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx index 64dedd360..4a87655e0 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/autocomplete/index.tsx @@ -11,7 +11,7 @@ import { } from '../../../../../services/graphical-scope' import { useOpenPLCStore } from '../../../../../store' import { cn } from '../../../../../utils/cn' -import { getLiteralType } from '../../../../../utils/keywords' +import { getLiteralType, isLegalIdentifier } from '../../../../../utils/keywords' import { toast } from '../../../../_features/[app]/toast/use-toast' import { useBoundPou } from '../../../../_features/[workspace]/editor/graphical/active-context' import { GraphicalEditorAutocomplete } from '../../autocomplete' @@ -54,14 +54,9 @@ const VariablesBlockAutoComplete = forwardRef { const pouName = useBoundPou() - const { - project: { - data: { pous }, - }, - projectActions: { createVariable }, - ladderFlows, - ladderFlowActions: { updateNode }, - } = useOpenPLCStore() + const pous = useOpenPLCStore((state) => state.project.data.pous) + const createVariable = useOpenPLCStore((state) => state.projectActions.createVariable) + const updateNode = useOpenPLCStore((state) => state.ladderFlowActions.updateNode) const expectedType = expectedTypeForBlock(block, blockType) @@ -84,9 +79,15 @@ const VariablesBlockAutoComplete = forwardRef { - const { rung, node: variableNode } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { - nodeId: (block as Node).id, - }) + const { project, ladderFlows } = useOpenPLCStore.getState() + const { rung, node: variableNode } = getLadderPouVariablesRungNodeAndEdges( + pouName, + project.data.pous, + ladderFlows, + { + nodeId: (block as Node).id, + }, + ) if (!rung || !variableNode) return updateNode({ @@ -143,13 +144,19 @@ const VariablesBlockAutoComplete = forwardRef { + const { project, ladderFlows } = useOpenPLCStore.getState() if (!variableName.trim()) { // For variable nodes on block handles, clearing the name resets the variable // so that a branch (contacts/coils) can be placed on the handle instead. if (blockType === 'variable') { - const { rung, node: variableNode } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { - nodeId: (block as Node).id, - }) + const { rung, node: variableNode } = getLadderPouVariablesRungNodeAndEdges( + pouName, + project.data.pous, + ladderFlows, + { + nodeId: (block as Node).id, + }, + ) if (rung && variableNode) { updateNode({ editorName: pouName, @@ -175,11 +182,26 @@ const VariablesBlockAutoComplete = forwardRef).id, }) if (!rung || !node) return + // If the entry can't be a new variable NAME — a member/array reference + // (`some_struct.field`, `arr[3]`), a typed literal (`T#500ms`), a reserved + // word, etc. — don't try to create a variable. Bind it to the node + // verbatim as a constant/reference; strucpp validates the expression. New + // local-variable creation is only for plain, legal identifiers. + if (!isLegalIdentifier(variableName)[0]) { + updateNode({ + editorName: pouName, + rungId: rung.id, + nodeId: node.id, + node: { ...node, data: { ...node.data, variable: { name: variableName } } }, + }) + return + } + const variableType = newVariableTypeForExpected(expectedType) const res = createVariable({ diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/block.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/block.tsx index 17df159ce..0e252b784 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/block.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/block.tsx @@ -1,4 +1,4 @@ -import { FocusEvent, useEffect, useMemo, useRef, useState } from 'react' +import { FocusEvent, memo, useEffect, useMemo, useRef, useState } from 'react' import { v4 as uuidv4 } from 'uuid' import type { PLCVariable } from '../../../../../middleware/shared/ports' @@ -8,6 +8,7 @@ import { useOpenPLCStore } from '../../../../store' import { LibraryState } from '../../../../store/slices/library' import { checkVariableName } from '../../../../store/slices/project/validation/variables' import { cn } from '../../../../utils/cn' +import { isLegalIdentifier } from '../../../../utils/keywords' import { toast } from '../../../_features/[app]/toast/use-toast' import { useBoundEditorModel, useBoundPou } from '../../../_features/[workspace]/editor/graphical/active-context' import { updateDiagramElementsPosition } from '../../../_molecules/graphical-editor/ladder/rung/ladder-utils/elements/diagram' @@ -59,17 +60,10 @@ export const BlockNodeElement = ({ }) => { const pouName = useBoundPou() const editor = useBoundEditorModel() - const { - editorActions: { updateModelVariables }, - libraries, - ladderFlows, - ladderFlowActions: { setNodes, setEdges, setHandleBranches }, - project: { - data: { pous }, - }, - projectActions: { updateVariable, deleteVariable }, - snapshotActions: { pushToHistory }, - } = useOpenPLCStore() + const updateModelVariables = useOpenPLCStore((state) => state.editorActions.updateModelVariables) + const { setNodes, setEdges, setHandleBranches } = useOpenPLCStore((state) => state.ladderFlowActions) + const { updateVariable, deleteVariable } = useOpenPLCStore((state) => state.projectActions) + const pushToHistory = useOpenPLCStore((state) => state.snapshotActions.pushToHistory) const { name: blockName, @@ -166,6 +160,8 @@ export const BlockNodeElement = ({ return } + const { project, libraries, ladderFlows } = useOpenPLCStore.getState() + const pous = project.data.pous const libraryBlock = resolveLibraryBlock(blockNameValue, libraries, pous) if (!libraryBlock) { @@ -401,20 +397,19 @@ export const BlockNodeElement = ({ ) } -export const Block = (block: BlockProps) => { +const Block = (block: BlockProps) => { const { data, dragging, height, width, selected, id } = block const pouName = useBoundPou() + const pous = useOpenPLCStore((state) => state.project.data.pous) + const createVariable = useOpenPLCStore((state) => state.projectActions.createVariable) + const pushToHistory = useOpenPLCStore((state) => state.snapshotActions.pushToHistory) const { - project: { - data: { pous }, - }, - projectActions: { createVariable }, - snapshotActions: { pushToHistory }, - libraries: { user: userLibraries }, - ladderFlows, - ladderFlowActions: { updateNode, setNodes, setEdges, setHandleBranches: setHandleBranchesBlock }, - } = useOpenPLCStore() + updateNode, + setNodes, + setEdges, + setHandleBranches: setHandleBranchesBlock, + } = useOpenPLCStore((state) => state.ladderFlowActions) const { type: blockType } = (data.variant as BlockVariant) ?? DEFAULT_BLOCK_TYPE const documentation = getBlockDocumentation(data.variant as newBlockVariant) @@ -422,10 +417,6 @@ export const Block = (block: BlockProps) => { const [wrongVariable, setWrongVariable] = useState(false) const [hoveringBlock, setHoveringBlock] = useState(false) - const { variables, rung, node } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { - nodeId: id, - }) - const connectedOutputNames = useMemo(() => { const names = new Set() if (Array.isArray(data.connectedVariables)) { @@ -461,6 +452,10 @@ export const Block = (block: BlockProps) => { switch (blockType) { case 'function-block': { if (!data.variable || data.variable.name === '') { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { variables } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { + nodeId: id, + }) const { name, number } = checkVariableName(variables.all, (data.variant as BlockVariant).name.toUpperCase()) handleSubmitBlockVariableOnTextareaBlur(`${name}${number}`, true) @@ -485,6 +480,7 @@ export const Block = (block: BlockProps) => { return } + const { ladderFlows } = useOpenPLCStore.getState() const { variables: freshVariables, rung: freshRung, @@ -551,11 +547,23 @@ export const Block = (block: BlockProps) => { return } + const { ladderFlows } = useOpenPLCStore.getState() + const { variables, rung, node } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + nodeId: id, + }) + if (!rung || !node) { toast({ title: 'Error', description: 'Could not find the related rung or node', variant: 'fail' }) return } + // Blur with an unchanged name is not an edit — skip the write so merely + // clicking in and out of a block never marks the POU as modified. The + // autocomplete's explicit create action (createIfNotFound) still proceeds. + if (!createIfNotFound && variableNameToSubmit === (node.data as { variable?: { name?: string } }).variable?.name) { + return + } + const blockType = (node.data as BlockNodeData).variant.name const findMatchingVariable = () => @@ -595,6 +603,13 @@ export const Block = (block: BlockProps) => { if (matchingVariable) { variableToLink = matchingVariable } else if (createIfNotFound) { + // An entry that can't be a new variable NAME — a member/array reference, + // a typed literal (`T#500ms`), a reserved word — is bound to the block + // verbatim as a constant/reference instead of erroring. + if (!isLegalIdentifier(variableNameToSubmit)[0]) { + updateNodeVariable({ name: variableNameToSubmit }) + return + } const project = useOpenPLCStore.getState().project const currentPou = project.data.pous.find((p) => p.name === pouName) pushToHistory(pouName, { @@ -634,6 +649,7 @@ export const Block = (block: BlockProps) => { } const handleUpdateDivergence = () => { + const { ladderFlows, libraries } = useOpenPLCStore.getState() const { variables, rung, node, edges } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { nodeId: id, }) @@ -643,7 +659,7 @@ export const Block = (block: BlockProps) => { const variant = (node.data as BlockNodeData)?.variant if (!variant) return - const libMatch = userLibraries.find((lib) => lib.name === variant.name && lib.type === variant.type) + const libMatch = libraries.user.find((lib) => lib.name === variant.name && lib.type === variant.type) if (!libMatch) return const libPou = pous.find((pou) => pou.name === libMatch.name) @@ -872,7 +888,13 @@ export const Block = (block: BlockProps) => { handleSubmit={() => handleSubmitBlockVariableOnTextareaBlur(blockVariableValue, false)} onFocus={(e) => { e.target.select() + const { ladderFlows } = useOpenPLCStore.getState() + const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + nodeId: id, + }) if (!node || !rung) return + // Drag-lock while typing is UI state, not an edit — transient + // so focusing the input never marks the flow as modified. updateNode({ editorName: pouName, nodeId: node.id, @@ -881,10 +903,15 @@ export const Block = (block: BlockProps) => { ...node, draggable: false, }, + transient: true, }) return }} onBlur={() => { + const { ladderFlows } = useOpenPLCStore.getState() + const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + nodeId: id, + }) if (!node || !rung) return updateNode({ editorName: pouName, @@ -894,6 +921,7 @@ export const Block = (block: BlockProps) => { ...node, draggable: node.data.draggable as boolean, }, + transient: true, }) return }} @@ -924,3 +952,8 @@ export const Block = (block: BlockProps) => {
) } + +// Cast keeps the generic call signature `memo` would otherwise widen away. +const exportBlock = memo(Block) as typeof Block + +export { exportBlock as Block } diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/coil.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/coil.tsx index 4da8b8cbe..8bfd18e48 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/coil.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/coil.tsx @@ -1,5 +1,5 @@ import * as Popover from '@radix-ui/react-popover' -import { useEffect, useRef, useState } from 'react' +import { memo, useEffect, useRef, useState } from 'react' import { useDebugger } from '../../../../../middleware/shared/providers' import { useDebugCompositeKey } from '../../../../hooks/use-debug-composite-key' @@ -18,16 +18,11 @@ import type { CoilProps } from './utils/types' export type { CoilNode } from './utils/types' -export const Coil = (block: CoilProps) => { +const Coil = (block: CoilProps) => { const { selected, data, id } = block const pouName = useBoundPou() - const { - project: { - data: { pous }, - }, - ladderFlows, - ladderFlowActions: { updateNode }, - } = useOpenPLCStore() + const pous = useOpenPLCStore((state) => state.project.data.pous) + const updateNode = useOpenPLCStore((state) => state.ladderFlowActions.updateNode) const debugger_ = useDebugger() const isDebuggerVisible = useIsDebuggerVisible() @@ -149,12 +144,17 @@ export const Coil = (block: CoilProps) => { */ const handleSubmitCoilVariableOnTextareaBlur = (variableName?: string) => { const variableNameToSubmit = variableName || coilVariableValue - const { rung, node } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { rung, node } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { nodeId: id, variableName: variableNameToSubmit, }) if (!rung || !node) return + // Blur with an unchanged name is not an edit — skip the write so merely + // clicking in and out of a coil never marks the POU as modified. + if (variableNameToSubmit === (node.data as { variable?: { name?: string } }).variable?.name) return + // Persist whatever the user typed; the validation effect resolves and // type-checks it against the full project scope and drives the red state. updateNode({ @@ -211,10 +211,13 @@ export const Coil = (block: CoilProps) => { readOnly={isDebuggerVisible} onFocus={(e) => { e.target.select() - const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { nodeId: id ?? '', }) if (!node || !rung) return + // Drag-lock while typing is UI state, not an edit — transient + // so focusing the input never marks the flow as modified. updateNode({ editorName: pouName, nodeId: node.id, @@ -223,11 +226,13 @@ export const Coil = (block: CoilProps) => { ...node, draggable: false, }, + transient: true, }) return }} onBlur={() => { - const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { nodeId: id ?? '', }) if (!node || !rung) return @@ -239,6 +244,7 @@ export const Coil = (block: CoilProps) => { ...node, draggable: node.data.draggable as boolean, }, + transient: true, }) return }} @@ -326,3 +332,7 @@ export const Coil = (block: CoilProps) => {
) } + +const exportCoil = memo(Coil) + +export { exportCoil as Coil } diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/contact.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/contact.tsx index 950502495..dcc5c3df8 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/contact.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/contact.tsx @@ -1,5 +1,5 @@ import * as Popover from '@radix-ui/react-popover' -import { useEffect, useRef, useState } from 'react' +import { memo, useEffect, useRef, useState } from 'react' import { useDebugger } from '../../../../../middleware/shared/providers' import { useDebugCompositeKey } from '../../../../hooks/use-debug-composite-key' @@ -18,16 +18,11 @@ import type { ContactProps } from './utils/types' export type { ContactNode } from './utils/types' -export const Contact = (block: ContactProps) => { +const Contact = (block: ContactProps) => { const { selected, data, id } = block const pouName = useBoundPou() - const { - project: { - data: { pous }, - }, - ladderFlows, - ladderFlowActions: { updateNode }, - } = useOpenPLCStore() + const pous = useOpenPLCStore((state) => state.project.data.pous) + const updateNode = useOpenPLCStore((state) => state.ladderFlowActions.updateNode) const debugger_ = useDebugger() const isDebuggerVisible = useIsDebuggerVisible() @@ -150,12 +145,17 @@ export const Contact = (block: ContactProps) => { */ const handleSubmitContactVariableOnTextareaBlur = (variableName?: string) => { const variableNameToSubmit = variableName || contactVariableValue - const { rung, node } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { rung, node } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { nodeId: id, variableName: variableNameToSubmit, }) if (!rung || !node) return + // Blur with an unchanged name is not an edit — skip the write so merely + // clicking in and out of a contact never marks the POU as modified. + if (variableNameToSubmit === (node.data as { variable?: { name?: string } }).variable?.name) return + // Persist whatever the user typed; the validation effect resolves and // type-checks it against the full project scope and drives the red state. updateNode({ @@ -212,10 +212,13 @@ export const Contact = (block: ContactProps) => { readOnly={isDebuggerVisible} onFocus={(e) => { e.target.select() - const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { nodeId: id ?? '', }) if (!node || !rung) return + // Drag-lock while typing is UI state, not an edit — transient + // so focusing the input never marks the flow as modified. updateNode({ editorName: pouName, nodeId: node.id, @@ -224,11 +227,13 @@ export const Contact = (block: ContactProps) => { ...node, draggable: false, }, + transient: true, }) return }} onBlur={() => { - const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { node, rung } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { nodeId: id ?? '', }) if (!node || !rung) return @@ -240,6 +245,7 @@ export const Contact = (block: ContactProps) => { ...node, draggable: node.data.draggable as boolean, }, + transient: true, }) return }} @@ -327,3 +333,7 @@ export const Contact = (block: ContactProps) => {
) } + +const exportContact = memo(Contact) + +export { exportContact as Contact } diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/handle.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/handle.tsx index e007d643f..13fc00359 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/handle.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/handle.tsx @@ -3,6 +3,8 @@ import { Handle, HandleProps } from '@xyflow/react' import { cn } from '../../../../utils/cn' export type CustomHandleProps = HandleProps & { + /** ladder handles always carry an explicit id — strip the `null` xyflow ≥12.11 allows */ + id?: string glbPosition: { x: number y: number @@ -37,7 +39,7 @@ export const CustomHandle = ({ ) } -type BuildHandleProps = HandleProps & { +type BuildHandleProps = Omit & { glbX: number glbY: number relX: number @@ -51,7 +53,7 @@ type BuildHandleProps = HandleProps & { * @param relY: number - The y coordinate of the handle based on the relative position (inside the node) * @returns CustomHandleProps */ -export const buildHandle = ({ glbX, glbY, relX, relY, ...rest }: BuildHandleProps) => { +export const buildHandle = ({ glbX, glbY, relX, relY, ...rest }: BuildHandleProps): CustomHandleProps => { return { glbPosition: { x: glbX, diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/mock-node.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/mock-node.tsx index efd2cb708..c976bfb14 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/mock-node.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/mock-node.tsx @@ -1,7 +1,9 @@ +import { memo } from 'react' + import { CustomHandle } from './handle' import { MockNodeProps } from './utils/types' -export const MockNode = ({ data }: MockNodeProps) => { +const MockNode = ({ data }: MockNodeProps) => { return ( <>
@@ -13,3 +15,7 @@ export const MockNode = ({ data }: MockNodeProps) => { ) } + +const exportMockNode = memo(MockNode) + +export { exportMockNode as MockNode } diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/parallel.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/parallel.tsx index ec52da42d..2bd87fc3d 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/parallel.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/parallel.tsx @@ -1,9 +1,11 @@ +import { memo } from 'react' + import { cn } from '../../../../utils/cn' import { CustomHandle } from './handle' import { DEFAULT_PARALLEL_HEIGHT, DEFAULT_PARALLEL_WIDTH } from './utils/constants' import type { ParallelProps } from './utils/types' -export const Parallel = ({ selected, data }: ParallelProps) => { +const Parallel = ({ selected, data }: ParallelProps) => { return ( <>
{ ) } + +const exportParallel = memo(Parallel) + +export { exportParallel as Parallel } diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/placeholder.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/placeholder.tsx index 06c787b3d..d11323a2c 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/placeholder.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/placeholder.tsx @@ -1,10 +1,12 @@ +import { memo } from 'react' + import { PlaceholderNodeFilled } from '../../../../assets/icons/flow/Placeholder' import { cn } from '../../../../utils/cn' import { CustomHandle } from './handle' import { DEFAULT_PLACEHOLDER_HEIGHT, DEFAULT_PLACEHOLDER_WIDTH } from './utils/constants' import { PlaceholderProps } from './utils/types' -export const Placeholder = ({ selected, data }: PlaceholderProps) => { +const Placeholder = ({ selected, data }: PlaceholderProps) => { return ( <> { ) } + +const exportPlaceholder = memo(Placeholder) + +export { exportPlaceholder as Placeholder } diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/power-rail.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/power-rail.tsx index 52edab3a8..5a604a1c7 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/power-rail.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/power-rail.tsx @@ -1,11 +1,11 @@ import { useUpdateNodeInternals } from '@xyflow/react' -import { useEffect, useMemo } from 'react' +import { memo, useEffect, useMemo } from 'react' import { CustomHandle } from './handle' import { DEFAULT_POWER_RAIL_HEIGHT, DEFAULT_POWER_RAIL_WIDTH } from './utils/constants' import { PowerRailProps } from './utils/types' -export const PowerRail = ({ id, data }: PowerRailProps) => { +const PowerRail = ({ id, data }: PowerRailProps) => { const updateNodeInternals = useUpdateNodeInternals() // Calculate dynamic height to cover all handles (including branch handles) @@ -43,3 +43,7 @@ export const PowerRail = ({ id, data }: PowerRailProps) => { ) } + +const exportPowerRail = memo(PowerRail) + +export { exportPowerRail as PowerRail } diff --git a/src/frontend/components/_atoms/graphical-editor/ladder/variable.tsx b/src/frontend/components/_atoms/graphical-editor/ladder/variable.tsx index a71e3f0d3..9cbaf03a7 100644 --- a/src/frontend/components/_atoms/graphical-editor/ladder/variable.tsx +++ b/src/frontend/components/_atoms/graphical-editor/ladder/variable.tsx @@ -1,5 +1,5 @@ import * as Popover from '@radix-ui/react-popover' -import { useEffect, useRef, useState } from 'react' +import { memo, useEffect, useRef, useState } from 'react' import { PLCVariable } from '../../../../../middleware/shared/ports' import { useDebugger } from '../../../../../middleware/shared/providers' @@ -34,13 +34,8 @@ import { BlockNodeData, BlockVariant, LadderBlockConnectedVariables, VariableNod const VariableElement = (block: VariableProps) => { const { id, data } = block const pouName = useBoundPou() - const { - project: { - data: { pous }, - }, - ladderFlows, - ladderFlowActions: { updateNode }, - } = useOpenPLCStore() + const pous = useOpenPLCStore((state) => state.project.data.pous) + const updateNode = useOpenPLCStore((state) => state.ladderFlowActions.updateNode) const debugger_ = useDebugger() const isDebuggerVisible = useIsDebuggerVisible() const getCompositeKey = useDebugCompositeKey() @@ -165,7 +160,8 @@ const VariableElement = (block: VariableProps) => { const handleSubmitVariableValueOnTextareaBlur = (currentValue?: string) => { const variableNameToSubmit = currentValue ?? variableValue - const { pou, rung, node } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { pou, rung, node } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { nodeId: id, }) if (!pou || !rung || !node) return @@ -238,7 +234,7 @@ const VariableElement = (block: VariableProps) => { const getVariableType = (): string | undefined => { if (!data.variable || !data.variable.name) return undefined - const { pou } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { nodeId: id }) + const pou = pous.find((pou) => pou.name === pouName) if (!pou) return undefined const variable = (pou.interface?.variables ?? []).find( (v) => v.name.toLowerCase() === data.variable.name.toLowerCase(), @@ -555,4 +551,6 @@ const VariableElement = (block: VariableProps) => { ) } -export { VariableElement } +const exportVariableElement = memo(VariableElement) + +export { exportVariableElement as VariableElement } diff --git a/src/frontend/components/_atoms/react-flow/index.tsx b/src/frontend/components/_atoms/react-flow/index.tsx index ccb9f75c3..ff066c3da 100644 --- a/src/frontend/components/_atoms/react-flow/index.tsx +++ b/src/frontend/components/_atoms/react-flow/index.tsx @@ -2,7 +2,7 @@ import './style.css' import type { BackgroundProps, ControlProps, ReactFlowProps } from '@xyflow/react' import { Background, Controls, ReactFlow } from '@xyflow/react' -import { PropsWithChildren } from 'react' +import { PropsWithChildren, useMemo } from 'react' import { cn } from '../../../utils/cn' @@ -14,6 +14,8 @@ type ReactFlowPanelProps = PropsWithChildren & { viewportConfig?: ReactFlowProps } +const DEFAULT_DELETE_KEY_CODES = ['Delete', 'Backspace'] + export const ReactFlowPanel = ({ children, background, @@ -22,20 +24,27 @@ export const ReactFlowPanel = ({ controlsConfig, viewportConfig, }: ReactFlowPanelProps) => { - const getDeleteKeyCodes = () => { - if (!viewportConfig?.deleteKeyCode) return ['Delete', 'Backspace'] - return viewportConfig.deleteKeyCode - } + const deleteKeyCodes = viewportConfig?.deleteKeyCode ? viewportConfig.deleteKeyCode : DEFAULT_DELETE_KEY_CODES + + // Stable children identity — inline JSX would break FlowRenderer's memo. + const flowChildren = useMemo( + () => ( + <> + {background && } + {controls && ( + + {controlsConfig?.children} + + )} + {children} + + ), + [background, backgroundConfig, controls, controlsConfig, children], + ) return ( - - {background && } - {controls && ( - - {controlsConfig?.children} - - )} - {children} + + {flowChildren} ) } diff --git a/src/frontend/components/_atoms/tab/index.tsx b/src/frontend/components/_atoms/tab/index.tsx index d40d6473b..43affc54d 100644 --- a/src/frontend/components/_atoms/tab/index.tsx +++ b/src/frontend/components/_atoms/tab/index.tsx @@ -21,6 +21,7 @@ import { ServerIcon } from '../../../assets/icons/project/Server' import { SFCIcon } from '../../../assets/icons/project/SFC' import { STIcon } from '../../../assets/icons/project/ST' import { StructureIcon } from '../../../assets/icons/project/Structure' +import { UsersIcon } from '../../../assets/icons/project/Users' import { useOpenPLCStore } from '../../../store' import type { TabsProps } from '../../../store/slices/tabs' import { cn } from '../../../utils/cn' @@ -56,6 +57,7 @@ const TabIcons: Record = { 'ethercat-device': , 'library-manager': , 'library-manifest': , + 'user-management': , 'diff-viewer': , } @@ -87,6 +89,7 @@ const Tab = (props: ITabProps) => { | 'ethercat-device' | 'library-manager' | 'library-manifest' + | 'user-management' | 'diff-viewer' = 'il' if (fileDerivation?.type === 'data-type' || fileDerivation?.type === 'device') { @@ -123,6 +126,9 @@ const Tab = (props: ITabProps) => { if (fileDerivation?.type === 'library-manifest') { languageOrDerivation = 'library-manifest' } + if (fileDerivation?.type === 'user-management') { + languageOrDerivation = 'user-management' + } if (fileDerivation?.type === 'diff-viewer') { languageOrDerivation = 'diff-viewer' } diff --git a/src/frontend/components/_features/[workspace]/editor/device/configuration/board.tsx b/src/frontend/components/_features/[workspace]/editor/device/configuration/board.tsx index 193e09453..ca74ca98e 100644 --- a/src/frontend/components/_features/[workspace]/editor/device/configuration/board.tsx +++ b/src/frontend/components/_features/[workspace]/editor/device/configuration/board.tsx @@ -60,6 +60,7 @@ const Board = memo(function () { const setRuntimeIpAddress = useOpenPLCStore((state) => state.deviceActions.setRuntimeIpAddress) const setRuntimeConnectionStatus = useOpenPLCStore((state) => state.deviceActions.setRuntimeConnectionStatus) const setRuntimeJwtToken = useOpenPLCStore((state) => state.deviceActions.setRuntimeJwtToken) + const setRuntimeVersion = useOpenPLCStore((state) => state.deviceActions.setRuntimeVersion) const openModal = useOpenPLCStore((state) => state.modalActions.openModal) const plcStatus = useOpenPLCStore((state): RuntimeConnection['plcStatus'] => state.runtimeConnection.plcStatus) const timingStats = useOpenPLCStore((state): TimingStats | null => state.runtimeConnection.timingStats) @@ -365,6 +366,10 @@ const Board = memo(function () { return } + // Remember the runtime version so version-gated UI (e.g. User + // Management) can react to it for the lifetime of the connection. + setRuntimeVersion(result.runtimeVersion ?? null) + // Validate runtime version matches the selected board target const versionValidation = validateRuntimeVersion(deviceBoard, result.runtimeVersion) diff --git a/src/frontend/components/_features/[workspace]/editor/device/ethercat/components/cia402-axis-tab.tsx b/src/frontend/components/_features/[workspace]/editor/device/ethercat/components/cia402-axis-tab.tsx new file mode 100644 index 000000000..67536becf --- /dev/null +++ b/src/frontend/components/_features/[workspace]/editor/device/ethercat/components/cia402-axis-tab.tsx @@ -0,0 +1,183 @@ +import { InputWithRef } from '@root/frontend/components/_atoms/input' +import type { Cia402AxisConfig, ConfiguredEtherCATDevice } from '@root/middleware/shared/ports/esi-types' +import { type Cia402Role, resolveCia402Objects } from '@root/middleware/shared/utils/ethercat' +import { useMemo } from 'react' + +const inputClassName = + 'h-[26px] w-28 rounded-md border border-neutral-300 bg-white px-2 py-1 text-xs text-neutral-700 outline-none focus:border-brand-medium-dark dark:border-neutral-700 dark:bg-neutral-950 dark:text-neutral-300' + +/** Human labels for the CiA 402 object roles, in a sensible display order. */ +const ROLE_LABELS: Array<{ role: Cia402Role; label: string }> = [ + { role: 'controlWord', label: 'Control Word (0x6040)' }, + { role: 'statusWord', label: 'Status Word (0x6041)' }, + { role: 'modesOfOperation', label: 'Modes of Operation (0x6060)' }, + { role: 'modesDisplay', label: 'Modes Display (0x6061)' }, + { role: 'targetPosition', label: 'Target Position (0x607A)' }, + { role: 'positionActual', label: 'Position Actual (0x6064)' }, + { role: 'profileVelocity', label: 'Profile Velocity (0x6081)' }, + { role: 'targetVelocity', label: 'Target Velocity (0x60FF)' }, + { role: 'velocityActual', label: 'Velocity Actual (0x606C)' }, + { role: 'targetTorque', label: 'Target Torque (0x6071)' }, + { role: 'torqueActual', label: 'Torque Actual (0x6077)' }, +] + +/** Feedback signals shown in the live-values panel (drive → controller). */ +const FEEDBACK_SIGNALS: Array<{ role: Cia402Role; label: string; unit: string }> = [ + { role: 'positionActual', label: 'Actual Position', unit: 'u' }, + { role: 'velocityActual', label: 'Actual Velocity', unit: 'u/s' }, + { role: 'torqueActual', label: 'Actual Torque', unit: '' }, + { role: 'statusWord', label: 'Status Word', unit: '' }, +] + +function parseFloatInput(value: string): number | undefined { + const n = Number(value) + return Number.isFinite(n) ? n : undefined +} + +function parseIntInput(value: string, min: number): number | undefined { + const n = parseInt(value, 10) + return Number.isNaN(n) || n < min ? undefined : n +} + +export type Cia402AxisTabProps = { + device: ConfiguredEtherCATDevice + /** Merge-updates the device's CiA 402 axis config in the store. */ + onUpdate: (patch: Partial) => void +} + +/** + * SoftMotion axis (CiA 402) configuration + live-feedback screen — the OpenPLC + * analogue of the CODESYS CiA 402 device editor. Lets the user tune the + * increments↔units scaling used by SM_Drive_GenericDS402, shows how the drive's + * CiA 402 objects map to IEC located addresses, and (when a PLC is connected) + * displays real-time axis feedback. The device name is the axis name used in + * MC_*(Axis := ). + */ +export const Cia402AxisTab = ({ device, onUpdate }: Cia402AxisTabProps) => { + const cia402: Cia402AxisConfig = device.cia402 ?? { + enabled: false, + scaleNum: 1, + scaleDenom: 1, + scaleFactor: 1, + } + + const resolved = useMemo( + () => resolveCia402Objects(device.channelInfo ?? [], device.channelMappings), + [device.channelInfo, device.channelMappings], + ) + const locationByRole = useMemo(() => { + const m = new Map() + for (const o of resolved) m.set(o.role, { iecLocation: o.iecLocation, iecType: o.iecType }) + return m + }, [resolved]) + + const denom = cia402.scaleDenom === 0 ? 1 : cia402.scaleDenom + const incPerUnit = cia402.scaleFactor * (cia402.scaleNum / denom) + + return ( +
+ {/* Scaling */} +
+
+ Scaling (increments ↔ technical units) +
+
+ + + +
+ Increments per unit + + {Number.isFinite(incPerUnit) ? incPerUnit : '—'} + +
+
+
+ + {/* CiA 402 object → IEC address mapping */} +
+
CiA 402 Object Mapping
+
+ + + + + + + + + + {ROLE_LABELS.map(({ role, label }) => { + const m = locationByRole.get(role) + return ( + + + + + + ) + })} + +
ObjectIEC AddressType
{label}{m?.iecLocation ?? '—'}{m?.iecType ?? '—'}
+
+
+ + {/* Real-time feedback */} +
+
Real-time Feedback
+
+ {FEEDBACK_SIGNALS.map(({ role, label, unit }) => { + const mapped = locationByRole.has(role) + return ( +
+ {label} + + {mapped ? `— ${unit}` : 'n/a'} + +
+ ) + })} +
+

+ Live values appear here when connected to a running PLC and monitoring is active. +

+
+
+ ) +} diff --git a/src/frontend/components/_features/[workspace]/editor/device/ethercat/ethercat-device-editor.tsx b/src/frontend/components/_features/[workspace]/editor/device/ethercat/ethercat-device-editor.tsx index 2877a856e..f4e411d25 100644 --- a/src/frontend/components/_features/[workspace]/editor/device/ethercat/ethercat-device-editor.tsx +++ b/src/frontend/components/_features/[workspace]/editor/device/ethercat/ethercat-device-editor.tsx @@ -3,6 +3,7 @@ import { useDeviceConfiguration } from '@root/frontend/hooks/use-device-configur import { useOpenPLCStore } from '@root/frontend/store' import { cn } from '@root/frontend/utils/cn' import type { + Cia402AxisConfig, ConfiguredEtherCATDevice, EnrichDeviceData, ESIDeviceSummary, @@ -16,13 +17,14 @@ import { buildAddressPool } from '@root/middleware/shared/utils/iec-address' import { resolveTargetCapabilities } from '@root/middleware/shared/utils/target-capabilities' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Cia402AxisTab } from './components/cia402-axis-tab' import { ChannelMappingsSection, DeviceConfigurationForm, SdoParametersSection, } from './components/device-configuration-form' -type DeviceDetailTab = 'info' | 'configuration' | 'startup-params' | 'channel-mappings' +type DeviceDetailTab = 'info' | 'configuration' | 'startup-params' | 'channel-mappings' | 'axis' const TabItem = ({ value, label, isActive }: { value: string; label: string; isActive: boolean }) => (
) diff --git a/src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx b/src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx index 91a4b4a6f..942b6cafa 100644 --- a/src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx +++ b/src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx @@ -17,6 +17,7 @@ import type { } from '@root/middleware/shared/ports/esi-types' import type { EtherCATDevice, NetworkInterface } from '@root/middleware/shared/ports/ethercat-types' import { useEsi, useRuntime } from '@root/middleware/shared/providers/platform-context' +import { sanitizeAxisName } from '@root/middleware/shared/utils/ethercat' import { buildAddressPool } from '@root/middleware/shared/utils/iec-address' import { resolveTargetCapabilities } from '@root/middleware/shared/utils/target-capabilities' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' @@ -450,7 +451,9 @@ const EtherCATEditor = () => { for (const m of enriched.channelMappings ?? []) usedAddresses.add(m.iecLocation) } - const baseName = getShortDeviceName(bestMatch.esiDevice) + // SoftMotion drive names become axis variable names — keep them valid. + const rawName = getShortDeviceName(bestMatch.esiDevice) + const baseName = enriched.cia402?.enabled ? sanitizeAxisName(rawName) : rawName const uniqueName = generateUniqueSlaveName(baseName, takenNames) takenNames.add(uniqueName) @@ -523,7 +526,10 @@ const EtherCATEditor = () => { const nextPosition = configuredDevices.length > 0 ? Math.max(...configuredDevices.map((d) => d.position ?? 0)) + 1 : 1 - const baseName = getShortDeviceName(device) + // A SoftMotion drive's name becomes the axis variable name in generated + // code, so it must be a valid IEC identifier from the start. + const rawName = getShortDeviceName(device) + const baseName = enriched.cia402?.enabled ? sanitizeAxisName(rawName) : rawName const uniqueName = generateUniqueSlaveName(baseName, collectAllSlaveNames(project.data.remoteDevices)) const newDevice: ConfiguredEtherCATDevice = { diff --git a/src/frontend/components/_features/[workspace]/editor/diff-viewer/index.tsx b/src/frontend/components/_features/[workspace]/editor/diff-viewer/index.tsx index ed998f0b5..c34c610dc 100644 --- a/src/frontend/components/_features/[workspace]/editor/diff-viewer/index.tsx +++ b/src/frontend/components/_features/[workspace]/editor/diff-viewer/index.tsx @@ -13,7 +13,11 @@ * serialized form for files edited this session — keeping the diff live. * * The HEAD `before` map is cached in the version-control slice (`headContent`) - * and invalidated on project load / commit / in-place reload. + * and invalidated on project load / commit / in-place reload, plus pruned + * per-path on save (`recordSavedFiles`). The fetch below refires whenever the + * open path has no cached entry, so a viewer that stayed mounted across a + * commit (which empties the pending set) picks up the new HEAD on the next + * change instead of diffing against a stale snapshot. */ import { useEffect, useMemo } from 'react' @@ -58,31 +62,55 @@ export function DiffViewerEditor() { // of each diff. `null` = not yet loaded → fetched lazily below. const headContent = useOpenPLCStore((s) => s.versionControl.headContent) const setHeadContent = useOpenPLCStore((s) => s.versionControlActions.setHeadContent) + const mergeHeadContent = useOpenPLCStore((s) => s.versionControlActions.mergeHeadContent) const filePath = editor.type === 'diff-viewer' ? editor.meta.filePath : '' + // The cached snapshot serves this diff only when it has an entry for the + // open path. A missing entry means the cache predates the change being + // viewed (e.g. it was rebuilt while a commit had emptied the pending set), + // so it must be refetched — rendering it would diff against a wrong HEAD. + const headReady = headContent !== null && (!filePath || headContent[filePath] !== undefined) + // Lazily fetch the HEAD content of all pending files via the backend's // content-bearing /changes call (authoritative against the real HEAD), and - // cache the `before` map. Invalidated on load / commit / reload. + // cache the `before` map. Invalidated on load / commit / reload and pruned + // per-path on save. useEffect(() => { - if (headContent !== null || !projectId || !versionControl) return + if (headReady || !projectId || !versionControl) return let cancelled = false + // Snapshot the working-tree bytes before fetching: if `filePath` turns + // out to have no pending change, its working tree equals HEAD, so these + // bytes ARE its HEAD content. Caching them keeps `headReady` from + // refetching in a loop and gives later diffs the correct original side. + let workingTreeSnapshot = '' + if (filePath) { + try { + workingTreeSnapshot = buildAllProjectFileContents()[filePath] ?? '' + } catch { + workingTreeSnapshot = '' + } + } void (async () => { try { const { changes } = await versionControl.getChanges(projectId, undefined, true) const map: Record = {} for (const c of changes) map[c.path] = c.before ?? '' + if (filePath && map[filePath] === undefined) map[filePath] = workingTreeSnapshot if (!cancelled) setHeadContent(map) } catch { - if (!cancelled) setHeadContent({}) + // Cache an (empty) entry for the open path even on failure so + // `headReady` doesn't retry in a tight loop; the next mount or path + // change triggers a fresh attempt. + if (!cancelled) mergeHeadContent(filePath ? { [filePath]: '' } : {}) } })() return () => { cancelled = true } - }, [headContent, projectId, versionControl, setHeadContent]) + }, [headReady, filePath, projectId, versionControl, setHeadContent, mergeHeadContent]) - const original = headContent && filePath ? (headContent[filePath] ?? '') : '' + const original = headReady && headContent && filePath ? (headContent[filePath] ?? '') : '' // The working-tree side: raw loaded bytes for files untouched this session, // freshly serialized for edited ones. Recomputed when `project` changes. @@ -111,7 +139,7 @@ export function DiffViewerEditor() {

{filePath}

- {headContent !== null && ( + {headReady && (
- {headContent === null ? ( + {!headReady ? (
diff --git a/src/frontend/components/_features/[workspace]/editor/graphical/FBD/index.tsx b/src/frontend/components/_features/[workspace]/editor/graphical/FBD/index.tsx index 1ac4e5960..d8ac7033b 100644 --- a/src/frontend/components/_features/[workspace]/editor/graphical/FBD/index.tsx +++ b/src/frontend/components/_features/[workspace]/editor/graphical/FBD/index.tsx @@ -1,12 +1,14 @@ import { useEffect, useMemo } from 'react' import { useOpenPLCStore } from '../../../../../../store' -import { zodFBDFlowSchema } from '../../../../../../store/slices/fbd' +import { scheduleFlowWriteBack } from '../../../../../../store/slices/shared/flow-writeback' import { BlockNodeData } from '../../../../../_atoms/graphical-editor/fbd/block' import { BlockVariant } from '../../../../../_atoms/graphical-editor/types/block' import { FBDBody } from '../../../../../_molecules/graphical-editor/fbd' import { useBoundPou } from '../active-context' +const EMPTY_DIVERGENCES: string[] = [] + export default function FbdEditor() { // Bound POU comes from the `GraphicalEditorActiveProvider` set up // in the wrapper one level up. With multi-mount, every open FBD @@ -17,18 +19,13 @@ export default function FbdEditor() { const fbdFlows = useOpenPLCStore((state) => state.fbdFlows) const pous = useOpenPLCStore((state) => state.project.data.pous) const userLibraries = useOpenPLCStore((state) => state.libraries.user) - const fbdFlowActions = useOpenPLCStore((state) => state.fbdFlowActions) - const updatePou = useOpenPLCStore((state) => state.projectActions.updatePou) - const handleFileAndWorkspaceSavedState = useOpenPLCStore( - (state) => state.sharedWorkspaceActions.handleFileAndWorkspaceSavedState, - ) const isDebuggerVisible = useOpenPLCStore((state) => state.workspace.isDebuggerVisible) const flow = fbdFlows.find((flow) => flow.name === pouName) const flowUpdated = flow?.updated || false const nodeDivergences = useMemo(() => { - if (!flow) return [] + if (!flow) return EMPTY_DIVERGENCES const divergences = [] @@ -78,31 +75,19 @@ export default function FbdEditor() { } } - return divergences + return divergences.length > 0 ? divergences : EMPTY_DIVERGENCES }, [flow?.rung.nodes, userLibraries, pous]) /** - * Update the flow state to project JSON + * Queue the flow → project JSON write-back. The scheduler debounces it + * (edits inside the window coalesce), persists the raw flow object, and + * clears the `updated` flag; save paths flush it so a save landing inside + * the window still serializes the fresh body. Validation and the DOPE-477 + * raw-object policy live in store/slices/shared/flow-writeback.ts. */ useEffect(() => { if (!flowUpdated) return - - const flowSchema = zodFBDFlowSchema.safeParse(flow) - if (!flowSchema.success) return - - updatePou({ - name: pouName, - content: { - language: 'fbd', - value: flowSchema.data, - }, - }) - - fbdFlowActions.setFlowUpdated({ editorName: pouName, updated: false }) - - if (!isDebuggerVisible) { - handleFileAndWorkspaceSavedState(pouName) - } + scheduleFlowWriteBack(useOpenPLCStore.getState, pouName, 'fbd') }, [flowUpdated]) return ( diff --git a/src/frontend/components/_features/[workspace]/editor/graphical/active-context.tsx b/src/frontend/components/_features/[workspace]/editor/graphical/active-context.tsx index 9b716601a..cdf750de1 100644 --- a/src/frontend/components/_features/[workspace]/editor/graphical/active-context.tsx +++ b/src/frontend/components/_features/[workspace]/editor/graphical/active-context.tsx @@ -27,7 +27,7 @@ * isolated stories — under a real provider both fields are set. */ -import { createContext, type ReactNode, useContext } from 'react' +import { createContext, type ReactNode, useContext, useMemo } from 'react' import { useOpenPLCStore } from '../../../../../store' import type { EditorModel } from '../../../../../store/slices/editor' @@ -49,7 +49,8 @@ export function GraphicalEditorActiveProvider({ isActive: boolean children: ReactNode }) { - return {children} + const value = useMemo(() => ({ pouName, isActive }), [pouName, isActive]) + return {children} } /** diff --git a/src/frontend/components/_features/[workspace]/editor/graphical/elements/fbd/block/index.tsx b/src/frontend/components/_features/[workspace]/editor/graphical/elements/fbd/block/index.tsx index afc756fd9..4866ae3a2 100644 --- a/src/frontend/components/_features/[workspace]/editor/graphical/elements/fbd/block/index.tsx +++ b/src/frontend/components/_features/[workspace]/editor/graphical/elements/fbd/block/index.tsx @@ -64,17 +64,12 @@ const searchLibraryByPouName = ( const BlockElement = ({ isOpen, onClose, selectedNode }: BlockElementProps) => { const pouName = useBoundPou() const editor = useBoundEditorModel() - const { - editorActions: { updateModelVariables }, - fbdFlows, - fbdFlowActions: { setNodes, setEdges }, - project: { - data: { pous }, - }, - projectActions: { updateVariable, deleteVariable }, - libraries, - modalActions: { onOpenChange }, - } = useOpenPLCStore() + const updateModelVariables = useOpenPLCStore((state) => state.editorActions.updateModelVariables) + const { setNodes, setEdges } = useOpenPLCStore((state) => state.fbdFlowActions) + const pous = useOpenPLCStore((state) => state.project.data.pous) + const { updateVariable, deleteVariable } = useOpenPLCStore((state) => state.projectActions) + const libraries = useOpenPLCStore((state) => state.libraries) + const onOpenChange = useOpenPLCStore((state) => state.modalActions.onOpenChange) const maxInputs = 20 @@ -362,6 +357,7 @@ const BlockElement = ({ isOpen, onClose, selectedNode }: Block executionOrder: Number(formState.executionOrder), } + const { fbdFlows } = useOpenPLCStore.getState() const { rung, edges, variables } = getFBDPouVariablesRungNodeAndEdges(pouName, pous, fbdFlows, { nodeId: selectedNode.id, }) diff --git a/src/frontend/components/_features/[workspace]/editor/graphical/elements/fbd/block/library.tsx b/src/frontend/components/_features/[workspace]/editor/graphical/elements/fbd/block/library.tsx index 1a724ef0b..8019f3879 100644 --- a/src/frontend/components/_features/[workspace]/editor/graphical/elements/fbd/block/library.tsx +++ b/src/frontend/components/_features/[workspace]/editor/graphical/elements/fbd/block/library.tsx @@ -16,12 +16,9 @@ export const ModalBlockLibrary = ({ setSelectedFileKey: (string: string) => void }) => { const pouName = useBoundPou() - const { - project: { - data: { pous }, - }, - libraries: { system, user }, - } = useOpenPLCStore() + const pous = useOpenPLCStore((state) => state.project.data.pous) + const system = useOpenPLCStore((state) => state.libraries.system) + const user = useOpenPLCStore((state) => state.libraries.user) // Scope the visible system pool to bundled (canonical) + // project-enabled libraries — same gate the explorer's library // tree uses, so the picker shows exactly what the project can diff --git a/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/index.tsx b/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/index.tsx index b9c3bdc28..025fa8ab1 100644 --- a/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/index.tsx +++ b/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/index.tsx @@ -63,17 +63,12 @@ const searchLibraryByPouName = (libraries: LibraryState['libraries'], pous: PLCP const BlockElement = ({ isOpen, onClose, selectedNode }: BlockElementProps) => { const pouName = useBoundPou() const editor = useBoundEditorModel() - const { - editorActions: { updateModelVariables }, - ladderFlows, - ladderFlowActions: { setNodes, setEdges, setHandleBranches }, - project: { - data: { pous }, - }, - projectActions: { updateVariable, deleteVariable }, - libraries, - modalActions: { onOpenChange }, - } = useOpenPLCStore() + const updateModelVariables = useOpenPLCStore((state) => state.editorActions.updateModelVariables) + const { setNodes, setEdges, setHandleBranches } = useOpenPLCStore((state) => state.ladderFlowActions) + const pous = useOpenPLCStore((state) => state.project.data.pous) + const { updateVariable, deleteVariable } = useOpenPLCStore((state) => state.projectActions) + const libraries = useOpenPLCStore((state) => state.libraries) + const onOpenChange = useOpenPLCStore((state) => state.modalActions.onOpenChange) const maxInputs = 20 @@ -372,6 +367,7 @@ const BlockElement = ({ isOpen, onClose, selectedNode }: Block executionOrder: Number(formState.executionOrder), } + const { ladderFlows } = useOpenPLCStore.getState() const { rung, edges, variables } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { nodeId: selectedNode.id, }) diff --git a/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/library.tsx b/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/library.tsx index 970d4690d..2fe2c34a0 100644 --- a/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/library.tsx +++ b/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/block/library.tsx @@ -16,12 +16,9 @@ export const ModalBlockLibrary = ({ setSelectedFileKey: (string: string) => void }) => { const pouName = useBoundPou() - const { - project: { - data: { pous }, - }, - libraries: { system, user }, - } = useOpenPLCStore() + const pous = useOpenPLCStore((state) => state.project.data.pous) + const system = useOpenPLCStore((state) => state.libraries.system) + const user = useOpenPLCStore((state) => state.libraries.user) // Scope the visible system pool to bundled (canonical) + // project-enabled libraries — same gate the FBD picker and the // explorer's library tree use. diff --git a/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/coil/index.tsx b/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/coil/index.tsx index c1896cc65..cd869be87 100644 --- a/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/coil/index.tsx +++ b/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/coil/index.tsx @@ -15,14 +15,8 @@ type CoilElementProps = { const CoilElement = ({ isOpen, onClose, node }: CoilElementProps) => { const pouName = useBoundPou() - const { - ladderFlows, - project: { - data: { pous }, - }, - ladderFlowActions: { updateNode }, - modalActions: { onOpenChange }, - } = useOpenPLCStore() + const updateNode = useOpenPLCStore((state) => state.ladderFlowActions.updateNode) + const onOpenChange = useOpenPLCStore((state) => state.modalActions.onOpenChange) const [selectedModifier, setSelectedModifier] = useState(node?.data.variant as string) const coilModifiers = Object.entries(DEFAULT_COIL_TYPES).map(([label, coil]) => ({ @@ -42,7 +36,8 @@ const CoilElement = ({ isOpen, onClose, node }: CoilElementProps) => { } const handleConfirmAlteration = () => { - const { rung } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { rung } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { nodeId: node.id, }) if (!rung) return diff --git a/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/contact/index.tsx b/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/contact/index.tsx index d51129064..23979488b 100644 --- a/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/contact/index.tsx +++ b/src/frontend/components/_features/[workspace]/editor/graphical/elements/ladder/contact/index.tsx @@ -15,14 +15,8 @@ type ContactElementProps = { const ContactElement = ({ isOpen, onClose, node }: ContactElementProps) => { const pouName = useBoundPou() - const { - ladderFlows, - project: { - data: { pous }, - }, - ladderFlowActions: { updateNode }, - modalActions: { onOpenChange }, - } = useOpenPLCStore() + const updateNode = useOpenPLCStore((state) => state.ladderFlowActions.updateNode) + const onOpenChange = useOpenPLCStore((state) => state.modalActions.onOpenChange) const [selectedModifier, setSelectedModifier] = useState(node?.data.variant as string) const contactModifiers = Object.entries(DEFAULT_CONTACT_TYPES).map(([label, contact]) => ({ @@ -42,7 +36,8 @@ const ContactElement = ({ isOpen, onClose, node }: ContactElementProps) => { } const handleConfirmAlteration = () => { - const { rung } = getLadderPouVariablesRungNodeAndEdges(pouName, pous, ladderFlows, { + const { project, ladderFlows } = useOpenPLCStore.getState() + const { rung } = getLadderPouVariablesRungNodeAndEdges(pouName, project.data.pous, ladderFlows, { nodeId: node.id, }) if (!rung) return diff --git a/src/frontend/components/_features/[workspace]/editor/graphical/ladder/index.tsx b/src/frontend/components/_features/[workspace]/editor/graphical/ladder/index.tsx index cf816d9c7..a3fc5661b 100644 --- a/src/frontend/components/_features/[workspace]/editor/graphical/ladder/index.tsx +++ b/src/frontend/components/_features/[workspace]/editor/graphical/ladder/index.tsx @@ -15,14 +15,15 @@ import { import { restrictToParentElement } from '@dnd-kit/modifiers' import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable' import * as Portal from '@radix-ui/react-portal' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { createPortal } from 'react-dom' import { v4 as uuidv4 } from 'uuid' +import { usePouSnapshot } from '../../../../../../hooks/use-pou-snapshot' import { ladderSelectors } from '../../../../../../hooks/use-store-selectors' -import { openPLCStoreBase, useOpenPLCStore } from '../../../../../../store' -import { RungLadderState, zodLadderFlowSchema } from '../../../../../../store/slices/ladder' -import type { PouHistorySnapshot } from '../../../../../../store/slices/shared/types' +import { useOpenPLCStore } from '../../../../../../store' +import { RungLadderState } from '../../../../../../store/slices/ladder' +import { scheduleFlowWriteBack } from '../../../../../../store/slices/shared/flow-writeback' import { cn } from '../../../../../../utils/cn' import { BlockNode, BlockNodeData } from '../../../../../_atoms/graphical-editor/ladder/block' import { CoilNode } from '../../../../../_atoms/graphical-editor/ladder/coil' @@ -35,49 +36,92 @@ import BlockElement from '../elements/ladder/block' import CoilElement from '../elements/ladder/coil' import ContactElement from '../elements/ladder/contact' +const EMPTY_DIVERGENCES: string[] = [] + export default function LadderEditor() { // Bound POU comes from the `GraphicalEditorActiveProvider` set up // in the wrapper one level up. Mirrors `FbdEditor` — see that // file for the multi-mount rationale. const pouName = useBoundPou() - const { - ladderFlows, - ladderFlowActions, - searchNodePosition, - modals, - project: { - data: { pous }, - }, - projectActions: { updatePou }, - modalActions: { closeModal }, - sharedWorkspaceActions: { handleFileAndWorkspaceSavedState }, - snapshotActions: { pushToHistory }, - libraries: { user: userLibraries }, - workspace: { isDebuggerVisible }, - } = useOpenPLCStore() - - const captureSnapshot = useCallback( - (pouName: string): PouHistorySnapshot | null => { - const pou = pous.find((p) => p.name === pouName) - if (!pou) return null - return { - variables: pou.interface?.variables ?? [], - body: pou.body.value, - globalVariables: openPLCStoreBase.getState().project.data.configurations.resource.globalVariables, - } - }, - [pous], - ) + // Pou-scoped subscription: immer's structural sharing keeps this flow's + // identity stable when other POUs' flows (or unrelated slices) change. + const flow = useOpenPLCStore((state) => state.ladderFlows.find((f) => f.name === pouName)) + const ladderFlowActions = useOpenPLCStore((state) => state.ladderFlowActions) + const searchNodePosition = useOpenPLCStore((state) => state.searchNodePosition) + const blockElementModal = useOpenPLCStore((state) => state.modals['block-ladder-element']) + const contactElementModal = useOpenPLCStore((state) => state.modals['contact-ladder-element']) + const coilElementModal = useOpenPLCStore((state) => state.modals['coil-ladder-element']) + const pous = useOpenPLCStore((state) => state.project.data.pous) + const closeModal = useOpenPLCStore((state) => state.modalActions.closeModal) + const userLibraries = useOpenPLCStore((state) => state.libraries.user) + const isDebuggerVisible = useOpenPLCStore((state) => state.workspace.isDebuggerVisible) + + const { captureAndPush } = usePouSnapshot() const updateModelLadder = ladderSelectors.useUpdateModelLadder() - const flow = ladderFlows.find((flow) => flow.name === pouName) const rungs = flow?.rungs || [] const flowUpdated = flow?.updated || false const [activeId, setActiveId] = useState(null) const [activeItem, setActiveItem] = useState(null) - const nodeDivergences = getLibraryDivergences() + + const nodeDivergences = useMemo(() => { + if (!flow) return EMPTY_DIVERGENCES + + const divergences = [] + + for (const rung of flow.rungs) { + for (const node of rung.nodes) { + const variant = (node.data as BlockNodeData)?.variant + if (!variant) continue + + const libMatch = userLibraries.find((lib) => lib.name === variant.name && lib.type === variant.type) + if (!libMatch) continue + + const originalPou = pous.find((pou) => pou.name === libMatch.name) + if (!originalPou) continue + + const originalVariables = originalPou.interface?.variables ?? [] + const originalInOut = originalVariables.filter((variable) => + ['input', 'output', 'inOut'].includes(variable.class || ''), + ) + + const currentVariables = variant.variables.filter( + (variable) => + ['input', 'output', 'inOut'].includes(variable.class || '') && + !['OUT', 'EN', 'ENO'].includes(variable.name), + ) + + const formatVariable = (variable: { + name: string + class?: string + type: { definition: string; value: string } + }) => `${variable.name}|${variable.class}|${variable.type.definition}|${variable.type.value?.toLowerCase()}` + + if (originalPou.pouType === 'function') { + const outVariable = variant.variables.find((v) => v.name === 'OUT') + const outType = outVariable?.type?.value?.toUpperCase() + const returnType = originalPou.interface?.returnType?.toUpperCase() + if (!outType || !returnType || outType !== returnType) { + divergences.push(`${rung.id}:${node.id}`) + continue + } + } + + const currentMap = new Map(currentVariables.map((variable) => [formatVariable(variable), true])) + const hasDivergence = + originalInOut?.length !== currentVariables.length || + !originalInOut?.every((variable) => currentMap.has(formatVariable(variable))) + + if (hasDivergence) { + divergences.push(`${rung.id}:${node.id}`) + } + } + } + + return divergences.length > 0 ? divergences : EMPTY_DIVERGENCES + }, [flow?.rungs, userLibraries, pous]) const scrollableRef = useRef(null) useEffect(() => { @@ -91,27 +135,15 @@ export default function LadderEditor() { }, [searchNodePosition]) /** - * Update the flow state to project JSON. + * Queue the flow → project JSON write-back. The scheduler debounces it + * (edits inside the window coalesce), persists the raw flow object, and + * clears the `updated` flag; save paths flush it so a save landing inside + * the window still serializes the fresh body. Validation and the DOPE-477 + * raw-object policy live in store/slices/shared/flow-writeback.ts. */ useEffect(() => { if (!flowUpdated) return - - const flowSchema = zodLadderFlowSchema.safeParse(flow) - if (!flowSchema.success) return - - updatePou({ - name: pouName, - content: { - language: 'ld', - value: flowSchema.data, - }, - }) - - ladderFlowActions.setFlowUpdated({ editorName: pouName, updated: false }) - - if (!isDebuggerVisible) { - handleFileAndWorkspaceSavedState(pouName) - } + scheduleFlowWriteBack(useOpenPLCStore.getState, pouName, 'ld') }, [flowUpdated]) const getRungPos = (rungId: UniqueIdentifier) => rungs.findIndex((rung) => rung.id === rungId) @@ -127,8 +159,7 @@ export default function LadderEditor() { const handleAddNewRung = () => { if (isDebuggerVisible) return - const snapshot = captureSnapshot(pouName) - if (snapshot) pushToHistory(pouName, snapshot) + captureAndPush(pouName) const defaultViewport: [number, number] = [300, 100] @@ -187,8 +218,7 @@ export default function LadderEditor() { auxRungs.splice(destinationIndex, 0, removed) try { - const snapshot = captureSnapshot(pouName) - if (snapshot) pushToHistory(pouName, snapshot) + captureAndPush(pouName) ladderFlowActions.setRungs({ editorName: pouName, rungs: auxRungs }) } catch (error) { console.error('Failed to update rungs:', error) @@ -206,63 +236,6 @@ export default function LadderEditor() { closeModal() } - function getLibraryDivergences() { - if (!flow) return [] - - const divergences = [] - - for (const rung of flow.rungs) { - for (const node of rung.nodes) { - const variant = (node.data as BlockNodeData)?.variant - if (!variant) continue - - const libMatch = userLibraries.find((lib) => lib.name === variant.name && lib.type === variant.type) - if (!libMatch) continue - - const originalPou = pous.find((pou) => pou.name === libMatch.name) - if (!originalPou) continue - - const originalVariables = originalPou.interface?.variables ?? [] - const originalInOut = originalVariables.filter((variable) => - ['input', 'output', 'inOut'].includes(variable.class || ''), - ) - - const currentVariables = variant.variables.filter( - (variable) => - ['input', 'output', 'inOut'].includes(variable.class || '') && - !['OUT', 'EN', 'ENO'].includes(variable.name), - ) - - const formatVariable = (variable: { - name: string - class?: string - type: { definition: string; value: string } - }) => `${variable.name}|${variable.class}|${variable.type.definition}|${variable.type.value?.toLowerCase()}` - - if (originalPou.pouType === 'function') { - const outVariable = variant.variables.find((v) => v.name === 'OUT') - const outType = outVariable?.type?.value?.toUpperCase() - const returnType = originalPou.interface?.returnType?.toUpperCase() - if (!outType || !returnType || outType !== returnType) { - divergences.push(`${rung.id}:${node.id}`) - continue - } - } - - const currentMap = new Map(currentVariables.map((variable) => [formatVariable(variable), true])) - const hasDivergence = - originalInOut?.length !== currentVariables.length || - !originalInOut?.every((variable) => currentMap.has(formatVariable(variable))) - - if (hasDivergence) { - divergences.push(`${rung.id}:${node.id}`) - } - } - } - - return divergences - } - return (
@@ -299,25 +272,25 @@ export default function LadderEditor() { - {modals['block-ladder-element']?.open && ( + {blockElementModal?.open && ( } - isOpen={modals['block-ladder-element'].open} + selectedNode={blockElementModal.data as BlockNode} + isOpen={blockElementModal.open} /> )} - {modals['contact-ladder-element']?.open && ( + {contactElementModal?.open && ( )} - {modals['coil-ladder-element']?.open && ( + {coilElementModal?.open && ( )} diff --git a/src/frontend/components/_features/[workspace]/editor/user-management/index.tsx b/src/frontend/components/_features/[workspace]/editor/user-management/index.tsx new file mode 100644 index 000000000..69730f843 --- /dev/null +++ b/src/frontend/components/_features/[workspace]/editor/user-management/index.tsx @@ -0,0 +1,298 @@ +import { PencilIcon } from '@root/frontend/assets/icons/interface/Pencil' +import { PlusIcon } from '@root/frontend/assets/icons/interface/Plus' +import { RefreshIcon } from '@root/frontend/assets/icons/interface/Refresh' +import { TrashCanIcon } from '@root/frontend/assets/icons/interface/TrashCan' +import { toast } from '@root/frontend/components/_features/[app]/toast/use-toast' +import { Modal, ModalContent, ModalTitle } from '@root/frontend/components/_molecules/modal' +import { + RuntimeUserModal, + type RuntimeUserModalSubmit, +} from '@root/frontend/components/_organisms/modals/runtime-user-modal' +import { useOpenPLCStore } from '@root/frontend/store' +import type { RuntimeUser, UpdateUserParams } from '@root/middleware/shared/ports/runtime-port' +import { useRuntime } from '@root/middleware/shared/providers' +import { useCallback, useEffect, useState } from 'react' + +type EditTarget = { user: RuntimeUser; isSelf: boolean } + +const UserManagementEditor = () => { + const runtime = useRuntime() + const connectionStatus = useOpenPLCStore((s) => s.runtimeConnection.connectionStatus) + const setRuntimeConnectionStatus = useOpenPLCStore((s) => s.deviceActions.setRuntimeConnectionStatus) + const setRuntimeJwtToken = useOpenPLCStore((s) => s.deviceActions.setRuntimeJwtToken) + + const [users, setUsers] = useState([]) + const [currentUser, setCurrentUser] = useState(null) + const [loading, setLoading] = useState(true) + const [loadError, setLoadError] = useState(null) + + const [createOpen, setCreateOpen] = useState(false) + const [editTarget, setEditTarget] = useState(null) + const [deleteTarget, setDeleteTarget] = useState(null) + const [deleting, setDeleting] = useState(false) + + const isAdmin = currentUser?.role === 'admin' + + const refresh = useCallback(async () => { + setLoading(true) + setLoadError(null) + const [listResult, meResult] = await Promise.all([runtime.listUsers(), runtime.whoAmI()]) + if (!listResult.success) { + setLoadError(listResult.error || 'Failed to load users') + setUsers([]) + } else { + // Guard against a non-array payload (e.g. the runtime's existence-only + // {"msg":"Users found"} reply when the token is no longer valid), which + // would otherwise crash the table on `users.map`. + setUsers(Array.isArray(listResult.users) ? listResult.users : []) + } + if (meResult.success && meResult.user) { + setCurrentUser(meResult.user) + } + setLoading(false) + }, [runtime]) + + useEffect(() => { + // Reload whenever the screen mounts or the connection is (re)established. + if (connectionStatus === 'connected') { + void refresh() + } + }, [connectionStatus, refresh]) + + const handleCreate = async ({ username, password, role }: RuntimeUserModalSubmit): Promise => { + if (!password) return 'Password is required' + const result = await runtime.createUser({ username, password, role }) + if (!result.success) return result.error || 'Failed to create user' + toast({ title: 'User created', description: `"${username}" was created.`, variant: 'default' }) + void refresh() + return null + } + + const handleEdit = async (values: RuntimeUserModalSubmit): Promise => { + if (!editTarget) return 'No user selected' + const params: UpdateUserParams = {} + if (values.usernameChanged) params.username = values.username + if (values.passwordChanged) { + params.password = values.password + if (values.currentPassword) params.currentPassword = values.currentPassword + } + if (values.roleChanged) params.role = values.role + const changingOwnPassword = editTarget.isSelf && values.passwordChanged + const result = await runtime.updateUser(editTarget.user.id, params) + if (!result.success) return result.error || 'Failed to update user' + + if (changingOwnPassword) { + // The runtime invalidates your token when you change your own password, + // so drop the local session and force a fresh login with the new one. + await runtime.clearCredentials() + setRuntimeJwtToken(null) + setRuntimeConnectionStatus('disconnected') + toast({ + title: 'Password changed', + description: 'You have been signed out. Reconnect with your new password.', + variant: 'default', + }) + return null + } + + toast({ title: 'User updated', description: `"${values.username}" was updated.`, variant: 'default' }) + void refresh() + return null + } + + const handleDelete = async () => { + if (!deleteTarget) return + setDeleting(true) + const result = await runtime.deleteUser(deleteTarget.id) + setDeleting(false) + if (!result.success) { + toast({ title: 'Delete failed', description: result.error || 'Failed to delete user', variant: 'fail' }) + return + } + toast({ title: 'User deleted', description: `"${deleteTarget.username}" was deleted.`, variant: 'default' }) + setDeleteTarget(null) + void refresh() + } + + const canEditRow = (user: RuntimeUser) => isAdmin || user.id === currentUser?.id + const canDeleteRow = (user: RuntimeUser) => isAdmin && user.id !== currentUser?.id + + // When not connected (e.g. after changing your own password signs you out), + // show a neutral placeholder instead of the table + actions — those would + // hit the runtime unauthenticated and, worse, could render a non-array list. + if (connectionStatus !== 'connected') { + return ( +
+

User Management

+

+ You are not connected to a runtime. Connect to the runtime to manage its users. +

+
+ ) + } + + return ( +
+
+
+

User Management

+

+ Manage the accounts that can log in to this runtime. +

+
+
+ + {isAdmin && ( + + )} +
+
+ + {loading ? ( +

Loading users…

+ ) : loadError ? ( +

{loadError}

+ ) : ( +
+ + + + + + + + + + {users.map((user) => { + const isSelf = user.id === currentUser?.id + return ( + + + +
UsernameRole + Actions +
+ {user.username} + {isSelf && (you)} + {user.role} +
+ {canEditRow(user) && ( +