Skip to content

refactor(ethercat): extract concrete EthercatConfig / EtherCATMasterConfig types#737

Merged
JoaoGSP merged 2 commits into
developmentfrom
refactor/concrete-ethercat-config-types
Apr 24, 2026
Merged

refactor(ethercat): extract concrete EthercatConfig / EtherCATMasterConfig types#737
JoaoGSP merged 2 commits into
developmentfrom
refactor/concrete-ethercat-config-types

Conversation

@marconetsf
Copy link
Copy Markdown
Contributor

@marconetsf marconetsf commented Apr 21, 2026

Summary

  • Extracts the inline anonymous shape of PLCRemoteDevice.ethercatConfig into two named interfaces in the shared ports layer: EthercatConfig and EtherCATMasterConfig.
  • Consumers now reference the named types directly instead of duplicating the shape or widening to Record<string, unknown>:
    • ProjectActions.updateEthercatConfig takes an EthercatConfig (was Record<string, unknown>).
    • The slice drops the as EthercatConfig cast around masterConfig syncing and the as typeof device.ethercatConfig cast on assignment.
    • Removes the @root/backend/shared/types/PLC/open-plc import of EthercatConfig from src/frontend/store/slices/project/slice.ts — consumers now get the type transitively via the ports-declared action signature, which keeps the frontend off the backend import path.
    • The EtherCAT editor components drop the as unknown as ConfiguredEtherCATDevice[] casts from configuredDevices.

Stacking

This PR is stacked on feat/ethercat-esi-backend (#731) because development does not yet carry the EtherCAT base shape that the refactor touches. Once #731 merges into development, we rebase this branch onto the refreshed development.

The web-side companion lives in Autonomy-Logic/openplc-web#373 (based on web development, which already carries the base shape from #360). The Shared Surface Sync CI on both PRs will remain red until #731 (and its web-side mirror openplc-web#371) land.

Test plan

  • npx tsc --noEmit clean
  • npx eslint on the 5 refactor files — no new warnings/errors
  • jest project-slice suite 239/239 passing
  • After feat: EtherCAT ESI support #731 merges: rebase onto development and re-run full check

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added EtherCAT master and slave device support with configuration UI
    • Added ESI repository management for device definitions
    • Added device discovery and scanning with automatic matching
    • Added runtime diagnostics dashboard for EtherCAT monitoring
    • Added startup parameter and channel mapping editors
  • Documentation

    • Added comprehensive EtherCAT architecture guide
    • Added operational playbook for repository synchronization
  • Improvements

    • Tasks now sort by priority in generated configurations
    • System tasks protected from accidental deletion or modification

…rConfig types

Replace the inline anonymous shape of PLCRemoteDevice.ethercatConfig with two
named interfaces in the shared ports layer, so consumers can reference them
directly instead of duplicating the shape or widening to Record<string, unknown>.

Drops the `as unknown as ConfiguredEtherCATDevice[]` casts in the EtherCAT
editor components and the `as EthercatConfig` casts around masterConfig syncing
in the project slice. The updateEthercatConfig action now receives an
EthercatConfig directly rather than the Record fallback.

Stacked on feat/ethercat-esi-backend (PR #731) — the EtherCAT base shape only
lives on that branch until #731 lands in development. Mirrored in openplc-web
on development directly (that side already carries the base shape).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 21, 2026

Walkthrough

This pull request introduces comprehensive EtherCAT (Ethernet-based Fieldbus) support to the OpenPLC Editor, including ESI (EtherCAT Slave Information) XML parsing and repository management, device discovery and matching against repository entries, per-slave configuration UI, and runtime JSON generation for the firmware build pipeline.

Changes

Cohort / File(s) Summary
Documentation
.claude/skills/sync-shared-surfaces/SKILL.md, docs/ethercat-architecture.md
Added operational playbook for mirroring "shared surfaces" between repositories and comprehensive EtherCAT architectural reference covering data flow, type systems, persistence, and runtime integration.
Backend ESI Parsing & Repository
src/backend/editor/ethercat/esi-service.ts, src/backend/shared/ethercat/esi-parser-main.ts, src/backend/shared/ethercat/esi-parser.ts
Implemented ESI XML parser using fast-xml-parser, ESI repository service with v1→v2 migration support, and utility functions for type mapping, PDO-to-channel conversion, IEC location generation, and device summary computation.
Backend Device Matching & Enrichment
src/backend/shared/ethercat/device-matcher.ts, src/backend/shared/ethercat/enrich-device-data.ts, src/backend/shared/ethercat/device-config-defaults.ts, src/backend/shared/ethercat/collect-used-iec-addresses.ts
Added device-to-repository matching with quality levels (exact/partial/none), enrichment helpers for PDO persistence and channel info generation, default slave config factory, and used-address collection across devices.
Backend Configuration & Runtime
src/backend/shared/ethercat/generate-ethercat-config.ts, src/backend/shared/ethercat/ethercat-task-helpers.ts, src/backend/shared/ethercat/sdo-config-defaults.ts, src/backend/editor/ethercat/index.ts, src/backend/shared/ethercat/index.ts
Implemented EtherCAT runtime JSON configuration generator, task/cycle-time naming helpers, SDO default configuration extraction, and shared re-export modules.
Backend Compiler Integration
src/backend/editor/compiler/compiler-module.ts
Integrated EtherCAT config generation into compile flow; improved runtime v3/v4 detection; moved conf generation to occur before compileOnly early return.
Frontend EtherCAT Components - Repository & Device Management
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-upload.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-repository.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-repository-table.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/device-browser-modal.tsx
Added ESI file upload with drag-and-drop, repository table with expand/collapsible items, repository browser modal for device selection.
Frontend EtherCAT Components - Device Configuration
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/device-configuration-form.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/sdo-parameters-table.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/channel-mapping-table.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsx
Implemented device configuration form (startup checks, addressing, timeouts, watchdog, DC), SDO parameter editing table, channel-to-IEC-location mapping table, device info/config/startup/channel tabs.
Frontend EtherCAT Components - Scanning & Diagnostics
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/device-scan-table.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/discovered-device-table.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/runtime-status-panel.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/diagnostics-tab.tsx
Added scanned/discovered device tables with selection, runtime status polling with per-slave state, cycle metrics display, plugin availability detection.
Frontend EtherCAT Components - Tabs & Settings
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/devices-tab.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/scan-bus-tab.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/repository-tab.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/advanced-tab.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/global-settings-tab.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/interface-selector.tsx
Implemented main editor tabs (devices, scanning, repository, advanced settings), global master settings with interface selection, cycle time/priority/watchdog controls.
Frontend EtherCAT ESI UI
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-device-info.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-channels-table.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-parse-progress.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/components/configured-devices.tsx
Added ESI device info panel with multi-device selector, channels table with direction/type filtering, parse progress display, configured device row/list rendering with edit/delete actions.
Frontend EtherCAT Editors
src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx, src/frontend/components/_features/[workspace]/editor/device/ethercat/ethercat-device-editor.tsx
Implemented main EtherCAT bus editor with scan/discovery/config tabs and per-device editor with device-specific configuration views.
Frontend Navigation & Tree
src/frontend/components/_molecules/breadcrumbs/index.tsx, src/frontend/components/_molecules/project-tree/index.tsx, src/frontend/screens/workspace-screen.tsx, src/frontend/components/_atoms/tab/index.tsx
Added EtherCAT device breadcrumb/tree/tab/screen rendering; added ethercatDevice leaf type with rename/delete support; updated tab icon mapping.
Frontend State Management - Editor & Tabs
src/frontend/store/slices/editor/types.ts, src/frontend/store/slices/tabs/types.ts, src/frontend/store/slices/tabs/utils.ts, src/frontend/store/slices/file/types.ts
Added plc-ethercat-device editor model type, ethercat-device tab/file types, and factory functions for creating EtherCAT device editor/tab objects.
Frontend State Management - Project & Devices
src/frontend/store/slices/project/slice.ts, src/frontend/store/slices/project/types.ts
Added system task auto-creation/deletion for EtherCAT devices, task modification protection, updateEthercatConfig action, and used-address collection including EtherCAT channel mappings.
Frontend State Management - Shared Actions
src/frontend/store/slices/shared/slice.ts, src/frontend/store/slices/shared/types.ts, src/frontend/store/slices/shared/utils.ts, src/frontend/store/slices/workspace/types.ts
Added ethercatDeviceActions (delete/rename) with cascading cleanup, child device file registration on project load, plc-ethercat-device workspace tree leaf type.
Frontend Utilities & Hooks
src/frontend/hooks/use-device-configuration.ts, src/frontend/hooks/use-store-selectors.ts, src/frontend/utils/parse-resource-configuration-to-string.ts, src/frontend/utils/parse-resource-string-to-configuration.ts, src/frontend/utils/PLC/xml-generator/codesys/instances-xml.ts, src/frontend/utils/PLC/xml-generator/old-editor/instances-xml.ts, src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx, src/frontend/components/_organisms/explorer/project.tsx, src/frontend/components/_organisms/task-editor/index.tsx, src/frontend/components/_molecules/task-table/index.tsx, src/frontend/screens/workspace-screen.tsx, src/frontend/services/save-actions.ts
Added useDeviceConfiguration hook for loading full device data; extended useRemoteDeviceIOPoints to include EtherCAT mappings; sorted tasks by priority in XML/config output; protected system tasks from modification; added EtherCAT remote device editor split; extended EtherCAT device save handling.
Type System - EtherCAT Types
src/middleware/shared/ports/esi-types.ts, src/types/ethercat/index.ts
Defined comprehensive EtherCAT types: ESI vendor/device/PDO/CoE structures, channel/mapping info, repository items, device summaries, configured device shapes, matching results, and runtime discovery/status types.
Type System - Port Interfaces
src/middleware/shared/ports/esi-port.ts, src/middleware/shared/ports/runtime-port.ts, src/middleware/shared/ports/types.ts, src/middleware/shared/ports/platform-capabilities.ts, src/types/PLC/units/task.ts, src/backend/shared/types/PLC/open-plc.ts
Added EsiPort interface for repository operations; extended RuntimePort with EtherCAT discovery/status methods; added EtherCATMasterConfig and EthercatConfig types; extended PLCTask with system task flags; added hasEthercat platform capability.
Middleware Platform & Adapters
src/middleware/editor-platform.ts, src/middleware/adapters/editor/esi-adapter.ts, src/middleware/adapters/editor/runtime-adapter.ts, src/middleware/shared/providers/platform-context.tsx, src/middleware/shared/providers/types.ts
Implemented ESI adapter with project-path-aware IPC routing, extended runtime adapter with EtherCAT methods, added useEsi() hook, integrated EsiPort into platform context.
IPC Bridge
src/main/modules/ipc/main.ts, src/main/modules/ipc/renderer.ts
Added ESI service handlers (parse/save/load/delete/clear/migrate), EtherCAT runtime handlers (interfaces/status/scan/test/validate), error wrapping utilities, token-refresh logic for expired auth.
Configuration & Dependencies
package.json, src/App.tsx
Added fast-xml-parser dependency; added project-path synchronization to platform adapter.
Frontend UI State
src/frontend/components/_features/[workspace]/create-element/element-card/index.tsx
Enabled EtherCAT protocol selection in remote-device creation UI (changed disabled: true to disabled: false).
Miscellaneous
src/frontend/store/__tests__/shared-slice.test.ts
Added test for EtherCAT device cascading deletion cleanup.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

  • PR #704: Directly related—introduces identical EtherCAT/ESI additions across the same backend services, frontend components, IPC handlers, and type definitions.
  • PR #580: Related—both modify useRemoteDeviceIOPoints and remote device IO point selectors to surface device data for UI dropdowns.
  • PR #351: Related—both modify compiler-module runtime detection and conf-generation flow plus compiler integration paths.

Suggested labels

enhancement

Suggested reviewers

  • vmleroy
  • JoaoGSP
  • thiagoralves

Poem

🐰 A EtherCAT tale of discovery and config,
Where ESI files parse through XML fog,
Devices are matched, then mapped with care,
Channels aligned with addresses fair—
Your buses are now wired for the show! 🔗

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/concrete-ethercat-config-types

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/backend/editor/compiler/compiler-module.ts (1)

1431-1431: ⚠️ Potential issue | 🟠 Major

#getBoardRuntime now throws but callers don't catch — message port leaks.

#getBoardRuntime was tightened to throw when the board is missing from hals.json (Line 187-193), but both callers (compileProgram at Line 1431 and compileForDebugger at Line 2206) invoke it before entering a try/catch block. A missing-board error now rejects the outer compileProgram/compileForDebugger promise without closing _mainProcessPort or emitting an error log to the renderer — the UI will just hang. Wrap these calls so the existing error-reporting/port-close pattern runs.

🛡️ Proposed fix
-    const boardRuntime = await this.#getBoardRuntime(boardTarget) // Get the board runtime from the hals.json file
+    let boardRuntime: string
+    try {
+      boardRuntime = await this.#getBoardRuntime(boardTarget)
+    } catch (error) {
+      _mainProcessPort.postMessage({
+        logLevel: 'error',
+        message: `${getErrorMessage(error)}\nStopping compilation process.`,
+      })
+      _mainProcessPort.close()
+      return
+    }

Apply the equivalent guard around the compileForDebugger call at Line 2206.

Also applies to: 2206-2206

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/editor/compiler/compiler-module.ts` at line 1431, The call to the
private method `#getBoardRuntime` now throws when a board is missing, but
compileProgram and compileForDebugger call it outside their existing try/catch
so errors can escape and leak _mainProcessPort and fail to emit the renderer
error; update both callers (compileProgram and compileForDebugger) to wrap the
`#getBoardRuntime`(boardTarget) invocation in the same
try/catch/error-report/port-close pattern used elsewhere: catch the thrown
error, call the existing error-emission logic to the renderer and ensure
_mainProcessPort is closed (or cleaned up) before rethrowing or returning,
matching the surrounding error-handling flow so no port or UI state is left
hanging.
src/frontend/components/_molecules/project-tree/index.tsx (1)

424-447: ⚠️ Potential issue | 🟠 Major

Make EtherCAT leaf identifiers required for EtherCAT actions.

busName and deviceId are optional, so an ethercatDevice leaf can render Rename/Delete but silently do nothing when either prop is missing. Use a discriminated props union so the compiler enforces both identifiers for EtherCAT leaves.

Proposed type shape
-type IProjectTreeLeafProps = ComponentPropsWithoutRef<'li'> & {
-  leafLang:
+type ProjectTreeLeafLang =
     | 'il'
     | 'st'
     | 'python'
     | 'cpp'
@@
     | 'server'
     | 'remoteDevice'
     | 'ethercatDevice'
+
+type IProjectTreeLeafProps = ComponentPropsWithoutRef<'li'> &
+  (
+    | {
+        leafLang: Exclude<ProjectTreeLeafLang, 'ethercatDevice'>
+        leafType: WorkspaceProjectTreeLeafType
+        label?: string
+        busName?: never
+        deviceId?: never
+      }
+    | {
+        leafLang: 'ethercatDevice'
+        leafType: WorkspaceProjectTreeLeafType
+        label?: string
+        busName: string
+        deviceId: string
+      }
+  )
-  leafType: WorkspaceProjectTreeLeafType
-  label?: string
-  busName?: string
-  deviceId?: string
-}

Also applies to: 523-582, 621-664

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/components/_molecules/project-tree/index.tsx` around lines 424 -
447, The props type IProjectTreeLeafProps currently makes busName and deviceId
optional, allowing an ethercatDevice leaf (leafLang === 'ethercatDevice') to
render actions that silently no-op; change IProjectTreeLeafProps to a
discriminated union keyed on leafLang/WorkspaceProjectTreeLeafType so that when
leafLang is 'ethercatDevice' the type requires busName and deviceId, while other
variants keep them optional or absent; update any other similar leaf prop
types/usages (the other project-tree leaf definitions referenced in the review)
to the same discriminated-union pattern so the compiler enforces the identifiers
for EtherCAT actions.
src/frontend/store/slices/project/slice.ts (1)

1062-1108: ⚠️ Potential issue | 🟡 Minor

Add task name collision check before renaming system task.

deleteRemoteDevice correctly cleans up associated system tasks, and updateRemoteDeviceName updates the task name before the device rename so the find by old name still succeeds. However, the pre-check at line 1086 only guards against duplicate device names. If a non-system task already exists with the name produced by ethercatTaskName(newName) (e.g., EtherCAT_Master1), the rename will create a duplicate task name, violating the constraint enforced by createTask at line 605.

Consider adding a task name collision check in updateRemoteDeviceName before updating systemTask.name:

if (device.protocol === 'ethercat') {
  const newTaskName = ethercatTaskName(newName)
  if (slice.project.data.configurations.resource.tasks.some((t) => t.name === newTaskName)) {
    // Handle collision: skip rename or return error
    return
  }
  // ... existing logic
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/store/slices/project/slice.ts` around lines 1062 - 1108, The
rename flow in updateRemoteDeviceName must also guard against task-name
collisions: before assigning systemTask.name = ethercatTaskName(newName) (inside
the device.protocol === 'ethercat' branch), compute const newTaskName =
ethercatTaskName(newName) and check
slice.project.data.configurations.resource.tasks.some(t => t.name ===
newTaskName) (excluding the current systemTask if necessary); if a collision
exists, abort the operation (return fail('Task name already exists') or
otherwise surface an error) rather than performing the rename, ensuring you
reference updateRemoteDeviceName, ethercatTaskName, systemTask and
slice.project.data.configurations.resource.tasks to locate the logic to change.
🟠 Major comments (22)
.claude/skills/sync-shared-surfaces/SKILL.md-81-87 (1)

81-87: ⚠️ Potential issue | 🟠 Major

Compare local HEAD blobs, not origin/<branch>, for the CRLF check.

The playbook hashes the working trees being synced, but this caveat checks origin/<branch>. A clean local branch can still be ahead of origin, so this can falsely report “already in sync” and skip a real shared-surface drift.

Proposed fix
-git -C <editor> ls-tree origin/<branch> -- <path>
-git -C <web>    ls-tree origin/<branch> -- <path>
+git -C <editor> ls-tree HEAD -- <path>
+git -C <web>    ls-tree HEAD -- <path>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.claude/skills/sync-shared-surfaces/SKILL.md around lines 81 - 87, The
CRLF-check logic currently compares blob hashes against origin/<branch> using
the git ls-tree commands shown; change those checks to compare against the local
HEAD instead (replace origin/<branch> with HEAD in the git -C <editor> ls-tree
and git -C <web> ls-tree invocations) so the playbook detects local commits
ahead of origin and does not falsely report "already in sync" for
functions/steps that compute working-tree blob hashes.
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/interface-selector.tsx-161-164 (1)

161-164: ⚠️ Potential issue | 🟠 Major

Fix the ref callback to return void.

The assignment expression in a ref callback returns the assigned element, but RefCallback in React 18 requires the callback to return void. Use a block body to avoid returning the element.

Proposed fix
-                    ref={(el) => (optionRefs.current[index] = el)}
+                    ref={(el) => {
+                      optionRefs.current[index] = el
+                    }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/components/interface-selector.tsx
around lines 161 - 164, The ref callback currently returns the assigned element
(via expression arrow), which violates React 18's RefCallback signature; change
the inline ref from (el) => (optionRefs.current[index] = el) to a block-body
callback that performs the assignment and returns void so the function does not
return the element — i.e., use (el) => { optionRefs.current[index] = el; } for
the ref on the element keyed by option.value (see optionRefs and
optionRefs.current usage in the Option rendering inside interface-selector.tsx).
src/middleware/shared/providers/types.ts-36-36 (1)

36-36: ⚠️ Potential issue | 🟠 Major

Optional esi port — consumers lack undefined guards and use non-null assertions.

The optional esi?: EsiPort declaration is fine, but several consumers bypass type safety by assuming it's always present. Specifically:

  • use-device-configuration.ts (line 67): Uses esiPort!.loadDeviceFull() without checking if esiPort is defined; useEffect guards only check device and enabled, not esiPort
  • esi-upload.tsx (line 92): Uses esi!.parseAndSaveFile() without prior validation
  • esi-repository.tsx (line 35): Uses esi!.deleteRepositoryItem() without prior validation

These should either add explicit undefined checks before calling methods (e.g., if (!esiPort) return; or esiPort?.method()) or add esiPort to effect dependencies and the guard condition to properly handle cases where the adapter isn't registered (e.g., web platform).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/middleware/shared/providers/types.ts` at line 36, The optional esi?:
EsiPort is declared but callers assume it's always present; update consumers to
guard against undefined instead of using non-null assertions: in
use-device-configuration.ts ensure the effect checks esiPort (add esiPort to the
dependency array and include it in the guard) before calling
esiPort!.loadDeviceFull(), and in esi-upload.tsx and esi-repository.tsx replace
esi!.parseAndSaveFile() and esi!.deleteRepositoryItem() with safe calls (either
early return if !esiPort or use optional chaining like
esiPort?.parseAndSaveFile()) so methods are only invoked when the EsiPort
instance exists. Ensure references to the EsiPort type and the methods
loadDeviceFull, parseAndSaveFile, and deleteRepositoryItem are updated
accordingly.
src/types/PLC/units/task.ts-6-6 (1)

6-6: ⚠️ Potential issue | 🟠 Major

Fix triggering casing in the Zod schema — it diverges from all actual usage.

The schema defines triggering as z.enum(['CYCLIC', 'INTERRUPT']), but the shared port type (src/middleware/shared/ports/types.ts:59) and every test and production use throughout the codebase expect 'Cyclic' | 'Interrupt' (capitalized). Any data validated against this schema will fail because the allowed values do not match what the rest of the application produces.

Update the Zod enum to:

triggering: z.enum(['Cyclic', 'Interrupt']),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/types/PLC/units/task.ts` at line 6, The Zod schema's triggering enum uses
uppercase values that don't match the rest of the app; update the triggering
definition in the Zod schema (the triggering field using z.enum) to use
capitalized entries 'Cyclic' and 'Interrupt' instead of 'CYCLIC' and 'INTERRUPT'
so validation matches the shared port type and existing usages.
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/advanced-tab.tsx-47-103 (1)

47-103: ⚠️ Potential issue | 🟠 Major

Avoid persisting invalid numeric drafts on every keystroke.

Number(e.target.value) writes 0 for an empty field and can write NaN/out-of-range values before onBlur clamps. Keep local draft state or clamp/validate before calling onUpdateMasterConfig, so store/task sync never sees invalid master config values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/components/advanced-tab.tsx
around lines 47 - 103, The inputs (InputWithRef instances using
masterConfig.cycleTimeUs, taskPriority, watchdogTimeoutCycles and
onUpdateMasterConfig) are writing invalid numeric drafts on every keystroke
because Number(e.target.value) returns 0/NaN for empty or partial input; change
them to manage a local controlled draft state for each field (e.g.,
cycleTimeDraft, taskPriorityDraft, watchdogTimeoutDraft) that is updated
onChange and only call onUpdateMasterConfig from onBlur after parsing, clamping
(100–100000 for cycleTimeUs, 1–31 for taskPriority, 1–100 for
watchdogTimeoutCycles) and validating the value; alternatively, onChange may
update the draft as a string and onBlur perform Number(parse) + clamp before
calling onUpdateMasterConfig so store/task sync never receives transient invalid
numbers.
src/main/modules/ipc/main.ts-1539-1678 (1)

1539-1678: ⚠️ Potential issue | 🟠 Major

Validate EtherCAT runtime responses with Zod before returning them.

These handlers parse untrusted runtime responses and then cast to typed objects. A malformed plugin response can still be forwarded as valid data. Add Zod schemas for the EtherCAT status/scan/test/validate/runtime-status payloads and parse the JSON through those schemas. As per coding guidelines, **/*.{ts,tsx} should “Use Zod for schema validation across the codebase”.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/modules/ipc/main.ts` around lines 1539 - 1678, Add Zod validation
for all parsed runtime/plugin responses used by handleEtherCATScan,
handleEtherCATTest, handleEtherCATValidate, handleEtherCATGetRuntimeStatus (and
the EtherCAT status parser earlier) by defining Zod schemas (e.g.,
EtherCATStatusResponseSchema, EtherCATScanResponseSchema,
EtherCATTestResponseSchema, EtherCATValidateResponseSchema,
EtherCATRuntimeStatusResponseSchema) and using schema.parse or schema.safeParse
inside each makeRuntimeApiPostRequest response callback instead of blind JSON
casts; on validation failure throw an Error with the validation message so
makeRuntimeApiPostRequest returns a proper error. Ensure you import Zod, place
the schemas near these handlers or in a nearby types/validation section, and
replace the current inline casts (the lambda callbacks passed to
makeRuntimeApiPostRequest in handleEtherCATScan, handleEtherCATTest,
handleEtherCATValidate, handleEtherCATGetRuntimeStatus, and the initial status
parser) with schema parsing logic.
src/frontend/components/_organisms/explorer/project.tsx-330-335 (1)

330-335: ⚠️ Potential issue | 🟠 Major

Use a unique tab identity for EtherCAT child devices.

Line 332 passes child.name, but handleCreateTab resolves editors by name. Two slaves with the same device name can collide and open/reuse the wrong editor. Include device.name/child.id in the tab identity, or switch editor lookup to the unique path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/components/_organisms/explorer/project.tsx` around lines 330 -
335, The tab identity for EtherCAT child devices currently uses child.name which
can collide; update the call to handleCreateTab so the tab identity is unique
(e.g., include device.name and child.id in the name or supply a separate unique
id field) — for example, build the tab name as
`${device.name}:${child.id}:${child.name}` or use the path
`/devices/remote/${device.name}/devices/${child.id}` as the editor lookup key;
ensure the same unique identifier is used wherever tabs are resolved
(handleCreateTab and any editor-resolve logic that currently matches by name)
and keep the elementType payload (type: 'ethercat-device', busName: device.name,
deviceId: child.id) unchanged.
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/advanced-tab.tsx-1-7 (1)

1-7: ⚠️ Potential issue | 🟠 Major

Import EtherCATMasterConfig from the shared ports surface.

This frontend component imports a backend type directly, which breaks the frontend/backend boundary and conflicts with the ports refactor. Import the named config type from the shared ports module instead. As per coding guidelines, src/frontend/**/*.{ts,tsx} should “Never import backend or Electron APIs directly in frontend code; use port interfaces through dependency injection instead”.

🏗️ Proposed fix
-import type { EtherCATMasterConfig } from '@root/backend/shared/types/PLC/open-plc'
+import type { EtherCATMasterConfig } from '@root/middleware/shared/ports'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/components/advanced-tab.tsx
around lines 1 - 7, The component imports the backend type EtherCATMasterConfig
directly; update the import so the frontend consumes the shared ports surface
instead of backend APIs—replace the current import of EtherCATMasterConfig with
the equivalent named export from the shared ports module and keep
AdvancedTabProps and the onUpdateMasterConfig signature unchanged so the
component uses the ports-facing type.
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-channels-table.tsx-58-65 (1)

58-65: ⚠️ Potential issue | 🟠 Major

Pass filtered channel IDs to “select all”.

allSelected is scoped to filteredChannels, but onChannelSelectAll(!allSelected) gives the parent no way to apply the action to only the visible rows. This can select/deselect hidden channels when a direction/search filter is active.

Proposed fix
 type ESIChannelsTableProps = {
   channels: ESIChannel[]
   onChannelSelect?: (channelId: string, selected: boolean) => void
-  onChannelSelectAll?: (selected: boolean) => void
+  onChannelSelectAll?: (channelIds: string[], selected: boolean) => void
   selectedChannels?: Set<string>
   showSelection?: boolean
 }
@@
   const handleSelectAll = () => {
     if (onChannelSelectAll) {
-      onChannelSelectAll(!allSelected)
+      onChannelSelectAll(
+        filteredChannels.map((channel) => channel.id),
+        !allSelected,
+      )
     }
   }

Also applies to: 131-136

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/components/esi-channels-table.tsx
around lines 58 - 65, The select-all handler currently toggles selection state
with onChannelSelectAll(!allSelected) which doesn't limit the operation to
visible/filtered rows; change the call to pass the IDs of the visible channels
so the parent can apply the action only to those rows (e.g. call
onChannelSelectAll(!allSelected, filteredChannels.map(c => c.id)) in
handleSelectAll), and update the other similar invocation in this component (the
duplicate select-all handler around the second block) to the same signature so
both use filtered channel IDs when toggling selection.
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-upload.tsx-39-108 (1)

39-108: ⚠️ Potential issue | 🟠 Major

Guard against overlapping uploads.

processFiles can start twice before the disabled UI state takes effect; both runs append to the same captured repository, so the later completion can drop items from the earlier one. Add a synchronous ref guard around the processing loop.

Proposed fix
 const ESIUpload = ({ onFilesLoaded, repository, isLoading = false }: ESIUploadProps) => {
   const esi = useEsi()
   const [isDragging, setIsDragging] = useState(false)
@@
   })
   const fileInputRef = useRef<HTMLInputElement>(null)
+  const processingRef = useRef(false)
 
   const processFiles = useCallback(
     async (files: FileList) => {
+      if (processingRef.current) return
+      processingRef.current = true
+
       const xmlFiles = Array.from(files).filter((file) => file.name.toLowerCase().endsWith('.xml'))
 
       if (xmlFiles.length === 0) {
+        processingRef.current = false
         onFilesLoaded(repository, [{ filename: '', error: 'No XML files found. Please upload .xml ESI files.' }])
         return
       }
 
-      setParseProgress({
-        active: true,
-        currentFile: xmlFiles[0].name,
-        currentFileIndex: 0,
-        totalFiles: xmlFiles.length,
-        percentage: 0,
-      })
+      try {
+        setParseProgress({
+          active: true,
+          currentFile: xmlFiles[0].name,
+          currentFileIndex: 0,
+          totalFiles: xmlFiles.length,
+          percentage: 0,
+        })
 
-      const newItems: ESIRepositoryItemLight[] = []
-      const errors: Array<{ filename: string; error: string }> = []
+        const newItems: ESIRepositoryItemLight[] = []
+        const errors: Array<{ filename: string; error: string }> = []
 
-      const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB
+        const MAX_FILE_SIZE = 100 * 1024 * 1024 // 100MB
 
-      // Process files one at a time to avoid memory issues
-      for (let i = 0; i < xmlFiles.length; i++) {
+        // Process files one at a time to avoid memory issues
+        for (let i = 0; i < xmlFiles.length; i++) {
           const file = xmlFiles[i]
@@
-      setParseProgress({
-        active: false,
-        currentFileIndex: 0,
-        totalFiles: 0,
-        percentage: 100,
-      })
+        setParseProgress({
+          active: false,
+          currentFileIndex: 0,
+          totalFiles: 0,
+          percentage: 100,
+        })
 
-      onFilesLoaded([...repository, ...newItems], errors.length > 0 ? errors : undefined)
+        onFilesLoaded([...repository, ...newItems], errors.length > 0 ? errors : undefined)
+      } finally {
+        processingRef.current = false
+      }
     },

Also applies to: 122-146

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/components/esi-upload.tsx
around lines 39 - 108, Add a synchronous guard to prevent overlapping runs of
processFiles: create a ref (e.g., processingRef via useRef(false)), check at the
top of processFiles and return immediately if processingRef.current is true,
then set processingRef.current = true before processing and reset it to false in
a finally block; also capture the repository into a local snapshot (e.g., const
repositorySnapshot = repository) at the start and use that snapshot when calling
onFilesLoaded to avoid lost updates due to the closure over the changing
repository; reference processFiles, processingRef, repositorySnapshot,
parseProgress, esi, and onFilesLoaded when applying the change.
src/backend/shared/ethercat/generate-ethercat-config.ts-133-147 (1)

133-147: ⚠️ Potential issue | 🟠 Major

Fail invalid SDO values instead of writing zero.

A typo like "0xGG" or "abc" becomes 0, which can push an unintended startup SDO value to a slave. Treat invalid values as configuration errors so the build fails safely.

Proposed fix
-function parseNumericValue(str: string): number {
-  if (!str || str.trim() === '') return 0
+function parseNumericValue(str: string, context: string): number {
+  if (!str || str.trim() === '') {
+    throw new Error(`Missing SDO value for ${context}`)
+  }
 
   const trimmed = str.trim()
@@
   if (/^(0x|#x)/i.test(trimmed)) {
     const hexStr = trimmed.replace(/^#x/i, '0x')
     const parsed = Number(hexStr)
-    return isNaN(parsed) ? 0 : parsed
+    if (Number.isNaN(parsed)) throw new Error(`Invalid SDO value for ${context}: ${str}`)
+    return parsed
   }
 
   const parsed = Number(trimmed)
-  return isNaN(parsed) ? 0 : parsed
+  if (Number.isNaN(parsed)) throw new Error(`Invalid SDO value for ${context}: ${str}`)
+  return parsed
 }
@@
       index: entry.index,
       subindex: entry.subIndex,
-      value: parseNumericValue(entry.value),
+      value: parseNumericValue(entry.value, `${entry.index}:${entry.subIndex}`),
       data_type: entry.dataType,

Also applies to: 206-215

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/shared/ethercat/generate-ethercat-config.ts` around lines 133 -
147, The current parseNumericValue function swallows parse errors and returns 0
for invalid inputs; change it to throw a descriptive Error for any non-empty
string that cannot be parsed (including bad hex like "0xGG" or non-numeric
"abc"), while keeping the current behavior of returning 0 for
null/empty/whitespace; update the hex-path and decimal-path checks in
parseNumericValue to throw when isNaN(parsed) and include the original input in
the error message so the build fails with a clear configuration error; apply the
same change to the duplicate parsing implementation present later (the other
parseNumericValue-like block referenced around lines 206-215) to ensure
consistent failure semantics.
src/backend/shared/ethercat/generate-ethercat-config.ts-182-197 (1)

182-197: ⚠️ Potential issue | 🟠 Major

Do not emit runtime channels with empty IEC locations.

When a channel has no mapping, this currently writes iec_location: ''. That produces a runtime config entry the plugin cannot bind reliably. Filter to enabled/mapped channels or fail generation before emitting invalid config.

Proposed fix
 function buildChannels(
   channelInfo: PersistedChannelInfo[],
-  channelMappings: { channelId: string; iecLocation: string }[],
+  channelMappings: { channelId: string; iecLocation: string; enabled?: boolean }[],
 ): RuntimeChannel[] {
-  const mappingMap = new Map(channelMappings.map((m) => [m.channelId, m.iecLocation]))
+  const mappingMap = new Map(
+    channelMappings
+      .filter((m) => m.enabled !== false && m.iecLocation.trim() !== '')
+      .map((m) => [m.channelId, m.iecLocation]),
+  )
 
-  return channelInfo.map((ch, index) => ({
-    index,
-    name: ch.name,
-    type: deriveChannelType(ch.direction, ch.bitLen),
-    bit_length: ch.bitLen,
-    iec_location: mappingMap.get(ch.channelId) ?? '',
-    pdo_index: ch.pdoIndex,
-    pdo_entry_index: ch.entryIndex,
-    pdo_entry_subindex: hexToInt(ch.entrySubIndex),
-  }))
+  let runtimeIndex = 0
+  return channelInfo.flatMap((ch) => {
+    const iecLocation = mappingMap.get(ch.channelId)
+    if (!iecLocation) return []
+
+    return [
+      {
+        index: runtimeIndex++,
+        name: ch.name,
+        type: deriveChannelType(ch.direction, ch.bitLen),
+        bit_length: ch.bitLen,
+        iec_location: iecLocation,
+        pdo_index: ch.pdoIndex,
+        pdo_entry_index: ch.entryIndex,
+        pdo_entry_subindex: hexToInt(ch.entrySubIndex),
+      },
+    ]
+  })
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/shared/ethercat/generate-ethercat-config.ts` around lines 182 -
197, The buildChannels function currently emits RuntimeChannel entries with
iec_location set to an empty string when a mapping is missing; change
buildChannels to either filter out channels that lack a mapping or throw a
generation error before emitting any runtime channels with empty IEC locations.
Specifically, update the buildChannels function (and its use sites if needed) to
consult mappingMap.get(ch.channelId) and only include channels where that value
is defined/non-empty (or raise an explicit error mentioning ch.channelId and
ch.name), ensuring no RuntimeChannel is produced with iec_location == ''.
src/frontend/store/slices/shared/slice.ts-338-361 (1)

338-361: ⚠️ Potential issue | 🟠 Major

Do not key EtherCAT child state only by slave display name.

files[slave.name] and updateFile({ name: oldName, newName }) assume global name uniqueness, but EtherCAT slaves commonly share default names and can repeat across buses. That can overwrite file entries on project open and collide tabs/editor models during rename. Use a stable key that includes busName/deviceId, or enforce global uniqueness before registering/renaming.

Minimum guard if keeping name-based identity
     rename: (busName, deviceId, newName) => {
       const state = getState()
       const remoteDevice = state.project.data.remoteDevices?.find((d) => d.name === busName)
       if (!remoteDevice) return { ok: false, message: 'Bus not found' }
 
       const devices = remoteDevice.ethercatConfig?.devices ?? []
       const device = devices.find((d) => d.id === deviceId)
       if (!device) return { ok: false, message: 'EtherCAT device not found' }
 
       const oldName = device.name
+      const existingFile = state.fileActions.getFile({ name: newName }).file
+      const duplicateSibling = devices.some((d) => d.id !== deviceId && d.name === newName)
+      if (oldName !== newName && (existingFile || duplicateSibling)) {
+        return { ok: false, message: 'EtherCAT device name already exists' }
+      }
+
       const updatedDevices = devices.map((d) => (d.id === deviceId ? { ...d, name: newName } : d))

Also applies to: 633-636

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/store/slices/shared/slice.ts` around lines 338 - 361, The rename
handler currently keys per-device state by mutable display name (see rename,
getState, remoteDevice, device.id and updateFile calls), which can collide when
multiple EtherCAT slaves share the same name; change the file/tab/editor keying
to use a stable identifier that includes busName and deviceId (e.g.
composeKey(busName, deviceId)) instead of just device.name, and update all
places that read/write files/tabs/editors (including updateFile({ name: oldName,
newName }), state.editorActions.updateEditorName and
state.tabsActions.updateTabName) to accept and use that composite key (or to
accept both {key, name} so the human name still updates). Ensure newKey is
derived from busName+deviceId when renaming so save-state tracking and lookups
no longer depend on globally unique display names.
src/frontend/components/_molecules/project-tree/index.tsx-241-292 (1)

241-292: ⚠️ Potential issue | 🟠 Major

Narrow ProjectTreeExpandableLeaf to 'remoteDevice' only.

The type leafLang: IProjectTreeLeafProps['leafLang'] permits 'ethercatDevice', but the implementation only calls renameRemoteDevice and deleteRemoteDeviceRequest from remoteDeviceActions. There is no conditional logic to route 'ethercatDevice' to ethercatDeviceActions (which requires busName and deviceId parameters).

While current usage in explorer/project.tsx only passes leafLang='remoteDevice' to this component, the type signature allows incorrect usage. EtherCAT slave nodes are correctly handled by the non-expandable ProjectTreeLeaf component instead.

Restrict the type to prevent silent failures:

Type narrowing fix
 type IProjectTreeExpandableLeafProps = ComponentPropsWithoutRef<'li'> & {
-  leafLang: IProjectTreeLeafProps['leafLang']
-  leafType: WorkspaceProjectTreeLeafType
+  leafLang: 'remoteDevice'
+  leafType: Extract<WorkspaceProjectTreeLeafType, 'remote-device'>
   label?: string
   children?: ReactNode
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/components/_molecules/project-tree/index.tsx` around lines 241 -
292, The component ProjectTreeExpandableLeaf is typed to accept any
IProjectTreeLeafProps['leafLang'] (including 'ethercatDevice') but its logic
only calls remoteDeviceActions (renameRemoteDevice, deleteRequest) which will
mis-handle non-remoteDevice leaves; change the prop type for leafLang in
IProjectTreeExpandableLeafProps to the literal 'remoteDevice' so the component
can only be used for remote devices (update the type alias on
IProjectTreeExpandableLeafProps and any callers if needed), and confirm usages
(e.g., in explorer/project.tsx) pass only 'remoteDevice' to
ProjectTreeExpandableLeaf; leave other handling (ethercatDevice) to
ProjectTreeLeaf.
src/backend/shared/ethercat/device-matcher.ts-15-45 (1)

15-45: ⚠️ Potential issue | 🟠 Major

parseHexToNumber does not actually reject empty/whitespace IDs — contradicts the docstring.

The docstring (Lines 18–19) and the guard at Lines 41–45 are predicated on parseHexToNumber returning NaN for unparseable input. But Number("") and Number(" ") return 0, not NaN. If any ESI entry has an empty vendor.id, type.productCode, or type.revisionNo, it will silently coerce to 0 and falsely match a scanned device whose vendor/product/revision happens to be 0 — which is exactly the failure mode the comment claims to prevent.

🛡️ Proposed fix: explicitly reject empty/whitespace input
 function parseHexToNumber(hexString: string): number {
-  const cleaned = hexString.replace(/#x/gi, '0x')
-  return Number(cleaned)
+  const cleaned = hexString.trim().replace(/#x/gi, '0x')
+  if (cleaned === '') return NaN
+  return Number(cleaned)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/shared/ethercat/device-matcher.ts` around lines 15 - 45, The
parseHexToNumber helper currently coerces empty or whitespace strings to 0;
update parseHexToNumber so it first trims hexString and if the trimmed string is
empty return NaN, then perform the existing replacement (replace /#x/gi with
'0x') and finally return Number(cleaned); this ensures getMatchQuality's NaN
checks (esiVendorNum, esiProductNum, esiRevisionNum) correctly reject
empty/whitespace ESI IDs (refer to parseHexToNumber and getMatchQuality names to
locate the change).
src/frontend/hooks/use-device-configuration.ts-48-75 (1)

48-75: ⚠️ Potential issue | 🟠 Major

Reset the full-device load guard when the ESI ref changes.

fullDeviceLoadedRef stays true after the first successful load. If this hook instance receives another device.esiDeviceRef, the effect runs but immediately returns, leaving stale channels/CoE data from the previous slave. Track the loaded key instead of a boolean.

Proposed fix
-  const fullDeviceLoadedRef = useRef(false)
+  const loadedDeviceKeyRef = useRef<string | null>(null)
   useEffect(() => {
-    if (!enabled || !device || fullDeviceLoadedRef.current) return
+    const deviceKey = `${device.esiDeviceRef.repositoryItemId}:${device.esiDeviceRef.deviceIndex}`
+    if (!enabled || !device || loadedDeviceKeyRef.current === deviceKey) return
 
     const loadFullDevice = async () => {
       setIsLoadingChannels(true)
       setChannelLoadError(null)
@@
           const deviceChannels = pdoToChannels(result.device)
           setChannels(deviceChannels)
           setCoeObjects(result.device.coeObjects)
-          fullDeviceLoadedRef.current = true
+          loadedDeviceKeyRef.current = deviceKey

Also applies to: 103-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/hooks/use-device-configuration.ts` around lines 48 - 75,
fullDeviceLoadedRef being a boolean causes stale data when device.esiDeviceRef
changes; change the guard to track the loaded key (e.g., a string from
device.esiDeviceRef.repositoryItemId + ':' + device.esiDeviceRef.deviceIndex)
instead of true/false, compare that key at the top of the useEffect (and return
if it matches), and when a new load succeeds set fullDeviceLoadedRef.current =
key; also reset fullDeviceLoadedRef.current to null/empty when device or
device.esiDeviceRef becomes undefined so subsequent effects will reload; apply
the same key-based approach to the similar check referenced at lines 103-104
(the other useEffect/guard).
src/frontend/hooks/use-device-configuration.ts-1-3 (1)

1-3: ⚠️ Potential issue | 🟠 Major

Move these EtherCAT helpers to a frontend-safe shared surface.

This frontend hook imports parser/enrichment/defaulting helpers from @root/backend/.... Those utilities should live in a shared module that is safe for renderer/web consumption, or be accessed through the ESI port.

As per coding guidelines, "src/frontend/**/*.{ts,tsx}: Never import backend or Electron APIs directly in frontend code; use port interfaces through dependency injection instead".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/frontend/hooks/use-device-configuration.ts` around lines 1 - 3, The hook
use-device-configuration.ts currently imports backend-only helpers
(enrichDeviceData, generateDefaultChannelMappings, pdoToChannels,
extractDefaultSdoConfigurations); move these utilities into a frontend-safe
shared surface (e.g., a new src/shared/ethercat module) or expose them via the
ESI port and inject them into the hook; then update use-device-configuration.ts
to import the functions from the new shared module or receive them from the
injected port (keep the same function names: enrichDeviceData,
generateDefaultChannelMappings, pdoToChannels, extractDefaultSdoConfigurations)
so frontend code no longer imports from `@root/backend/`*.
src/frontend/components/_features/[workspace]/editor/device/ethercat/components/device-configuration-form.tsx-1-1 (1)

1-1: ⚠️ Potential issue | 🟠 Major

Keep SDO default extraction out of the frontend/backend boundary.

This frontend component imports a backend module directly. Move extractDefaultSdoConfigurations to the shared ports/shared utility surface, or expose it through the ESI port.

As per coding guidelines, "src/frontend/**/*.{ts,tsx}: Never import backend or Electron APIs directly in frontend code; use port interfaces through dependency injection instead".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/components/device-configuration-form.tsx
at line 1, The component device-configuration-form.tsx is importing a
backend-only symbol extractDefaultSdoConfigurations; remove that direct backend
import and either (A) move extractDefaultSdoConfigurations into the shared
frontend/backend utility surface (e.g., a new shared module under src/shared/...
and update imports in both frontend and backend to use it), or (B) expose the
functionality via the existing ESI/port interface and call it through the
injected port (add a method like getDefaultSdoConfigurations to the port
interface and implement it in the backend), then update
device-configuration-form.tsx to use the shared import or call the injected port
method instead of importing the backend symbol directly. Ensure you update all
references to extractDefaultSdoConfigurations accordingly and remove any direct
backend module imports from frontend files.
src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx-2-6 (1)

2-6: ⚠️ Potential issue | 🟠 Major

Move frontend imports off the backend surface.

This frontend component imports EtherCAT helpers and EtherCATMasterConfig directly from @root/backend/.... Move frontend-safe utilities/types into the shared ports layer, or expose them through injected ports, so the renderer/web bundle does not depend on backend modules.

As per coding guidelines, "src/frontend/**/*.{ts,tsx}: Never import backend or Electron APIs directly in frontend code; use port interfaces through dependency injection instead".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/index.tsx
around lines 2 - 6, This frontend file imports backend-only utilities/types
(collectUsedIecAddresses, createDefaultSlaveConfig, getBestMatchQuality,
matchDevicesToRepository, enrichDeviceData and EtherCATMasterConfig) which
breaks the rule against importing backend/Electron APIs in src/frontend; move or
expose frontend-safe equivalents through the shared ports layer or
dependency-injection: create frontend-facing port interfaces (e.g.
EthercatDevicePort) that declare the needed functions/types, implement those
ports in the backend module that currently exports these symbols, and update
this component to import only the port interface or injected instance; ensure
the port surface exposes the same function names/types so callers
(collectUsedIecAddresses, createDefaultSlaveConfig, getBestMatchQuality,
matchDevicesToRepository, enrichDeviceData, EtherCATMasterConfig) can be used
unchanged by the component while keeping backend modules out of the renderer
bundle.
src/backend/editor/ethercat/esi-service.ts-423-451 (1)

423-451: ⚠️ Potential issue | 🟠 Major

Avoid dropping legacy index entries when appending a new ESI file.

If existingIndex is v1 or a partial v2 missing devices, Line 450 returns [], and Line 451 rewrites the index with only the newly uploaded item. Gate this path on migration first, or migrate/load existing entries before appending.

Proposed guard
         const existingIndex = await this.loadRepositoryIndex(projectPath)
+        if (
+          existingIndex &&
+          (existingIndex.version !== 2 || existingIndex.items.some((i) => i.devices === undefined))
+        ) {
+          return {
+            success: false,
+            error: 'Repository index needs migration before adding new ESI files',
+          }
+        }
+
         const existingFilenames = new Set(existingIndex?.items.map((i) => i.filename) ?? [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/editor/ethercat/esi-service.ts` around lines 423 - 451, The code
is overwriting legacy/v1 or partial v2 index entries when appending a new ESI
file because loadLightItemsFromIndex() can return [] for an existingIndex that
lacks migrated device data; fix by ensuring migration or full-load before
appending: detect when existingIndex (from loadRepositoryIndex(projectPath)) is
v1 or has missing `devices` and in that case call the migration/load routine to
produce a complete light-item list (or convert existingIndex.items into
ESIRepositoryItemLight) rather than using loadLightItemsFromIndex()’s empty
result, then pass the merged array to saveRepositoryIndexV2(projectPath,
[...currentItems, item]); update logic around existingIndex,
loadLightItemsFromIndex, and saveRepositoryIndexV2 to perform the migration/load
step when necessary.
src/backend/editor/ethercat/esi-service.ts-263-275 (1)

263-275: ⚠️ Potential issue | 🟠 Major

Update the index before deleting the XML file.

Line 267 removes the XML first. If Line 275 then fails to save repository.json, the repository still references a missing XML file. Prefer persisting the filtered index first, then deleting the XML; an orphaned XML is safer than a broken index reference.

Proposed fix
-        // Delete the XML file
-        const deleteResult = await this.deleteXmlFile(projectPath, itemId)
-        if (!deleteResult.success) {
-          return deleteResult
-        }
-
-        // Update the v2 index without the deleted item
         const currentItems = await this.loadLightItemsFromIndex(projectPath)
         const updatedItems = currentItems.filter((i) => i.id !== itemId)
-        return this.saveRepositoryIndexV2(projectPath, updatedItems)
+        const saveResult = await this.saveRepositoryIndexV2(projectPath, updatedItems)
+        if (!saveResult.success) {
+          return saveResult
+        }
+
+        return this.deleteXmlFile(projectPath, itemId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/backend/editor/ethercat/esi-service.ts` around lines 263 - 275, In
deleteRepositoryItemV2 (inside withIndexLock) persist the updated index before
removing the XML: call loadLightItemsFromIndex and then
saveRepositoryIndexV2(projectPath, updatedItems) first; if that save fails
return the error immediately. Only after a successful save should you call
deleteXmlFile(projectPath, itemId). Keep existing return types
(ESIServiceResponse) and ensure errors from saveRepositoryIndexV2 or
deleteXmlFile are propagated/logged appropriately so the repository index never
references a missing XML.
src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx-398-413 (1)

398-413: ⚠️ Potential issue | 🟠 Major

Use a unique slave key for files, models, and tabs.

Identical EtherCAT slaves commonly share the same ESI name. Using device.name/newDevice.name for addFile, removeModel, and removeTab can collide and close/remove the wrong slave editor. Include position or id in the editor/file key, and keep the display label separate if needed.

Example direction
+      const uniqueDeviceName = `${device.name} @${nextPosition}`
       const newDevice: ConfiguredEtherCATDevice = {
         id: uuidv4(),
         position: nextPosition,
-        name: device.name,
+        name: uniqueDeviceName,
         esiDeviceRef: ref,
-      fileActions.addFile({ name: newDevice.name, type: 'ethercat-device', filePath: deviceName })
+      fileActions.addFile({ name: newDevice.name, type: 'ethercat-device', filePath: deviceName })

Apply the same uniqueness rule for scan-added devices before they are persisted.

Also applies to: 448-466, 471-484

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/index.tsx
around lines 398 - 413, The newDevices creation uses device.name
(bestMatch.esiDevice.name or match.device.name) for UI/file keys which can
collide for identical slaves; change the editor/file/tab keys in the code paths
that call addFile, removeModel, and removeTab to use a unique key composed of
the new device id (uuidv4()) or position (e.g., `${id}:${position}`) instead of
the display name, and keep the existing name field as the visible label; apply
this uniqueness change for the scan-added device block where
newDevices.push(...) is constructed and in the same pattern at the other
mentioned spots (the callsites that build file/model/tab keys for scan-added
devices and for functions interacting with addFile/removeModel/removeTab) so
keys are stable before persistence.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 3b4cb29c-7f43-4895-957c-0711a237fcdc

📥 Commits

Reviewing files that changed from the base of the PR and between df844ee and 90c6d9b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (83)
  • .claude/skills/sync-shared-surfaces/SKILL.md
  • docs/ethercat-architecture.md
  • package.json
  • src/App.tsx
  • src/backend/editor/compiler/compiler-module.ts
  • src/backend/editor/ethercat/esi-service.ts
  • src/backend/editor/ethercat/index.ts
  • src/backend/shared/ethercat/collect-used-iec-addresses.ts
  • src/backend/shared/ethercat/device-config-defaults.ts
  • src/backend/shared/ethercat/device-matcher.ts
  • src/backend/shared/ethercat/enrich-device-data.ts
  • src/backend/shared/ethercat/esi-parser-main.ts
  • src/backend/shared/ethercat/esi-parser.ts
  • src/backend/shared/ethercat/ethercat-task-helpers.ts
  • src/backend/shared/ethercat/generate-ethercat-config.ts
  • src/backend/shared/ethercat/index.ts
  • src/backend/shared/ethercat/sdo-config-defaults.ts
  • src/backend/shared/types/PLC/open-plc.ts
  • src/frontend/components/_atoms/tab/index.tsx
  • src/frontend/components/_features/[workspace]/create-element/element-card/index.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/advanced-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/channel-mapping-table.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/configured-device-row.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/configured-devices.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/device-browser-modal.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/device-configuration-form.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/device-detail-panel.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/device-scan-table.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/devices-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/diagnostics-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/discovered-device-table.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-channels-table.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-device-info.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-parse-progress.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-repository-table.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-repository.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/esi-upload.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/global-settings-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/interface-selector.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/repository-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/runtime-status-panel.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/scan-bus-tab.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/components/sdo-parameters-table.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/ethercat-device-editor.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx
  • src/frontend/components/_molecules/breadcrumbs/index.tsx
  • src/frontend/components/_molecules/project-tree/index.tsx
  • src/frontend/components/_molecules/task-table/index.tsx
  • src/frontend/components/_organisms/explorer/project.tsx
  • src/frontend/components/_organisms/task-editor/index.tsx
  • src/frontend/hooks/use-device-configuration.ts
  • src/frontend/hooks/use-store-selectors.ts
  • src/frontend/screens/workspace-screen.tsx
  • src/frontend/services/save-actions.ts
  • src/frontend/store/__tests__/shared-slice.test.ts
  • src/frontend/store/slices/editor/types.ts
  • src/frontend/store/slices/file/types.ts
  • src/frontend/store/slices/project/slice.ts
  • src/frontend/store/slices/project/types.ts
  • src/frontend/store/slices/shared/slice.ts
  • src/frontend/store/slices/shared/types.ts
  • src/frontend/store/slices/shared/utils.ts
  • src/frontend/store/slices/tabs/types.ts
  • src/frontend/store/slices/tabs/utils.ts
  • src/frontend/store/slices/workspace/types.ts
  • src/frontend/utils/PLC/xml-generator/codesys/instances-xml.ts
  • src/frontend/utils/PLC/xml-generator/old-editor/instances-xml.ts
  • src/frontend/utils/parse-resource-configuration-to-string.ts
  • src/frontend/utils/parse-resource-string-to-configuration.ts
  • src/main/modules/ipc/main.ts
  • src/main/modules/ipc/renderer.ts
  • src/middleware/adapters/editor/esi-adapter.ts
  • src/middleware/adapters/editor/runtime-adapter.ts
  • src/middleware/editor-platform.ts
  • src/middleware/shared/ports/esi-port.ts
  • src/middleware/shared/ports/esi-types.ts
  • src/middleware/shared/ports/platform-capabilities.ts
  • src/middleware/shared/ports/runtime-port.ts
  • src/middleware/shared/ports/types.ts
  • src/middleware/shared/providers/platform-context.tsx
  • src/middleware/shared/providers/types.ts
  • src/types/PLC/units/task.ts
  • src/types/ethercat/index.ts

@@ -0,0 +1,128 @@
import { getBestMatchQuality } from '@root/backend/shared/ethercat/device-matcher'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether getBestMatchQuality is pure and could be relocated to a frontend-safe shared module
rg -nP --type=ts -C3 'export\s+(function|const)\s+getBestMatchQuality\b'
# Find all frontend files that import from `@root/backend/`...
rg -nP "from '@root/backend/" -g 'src/frontend/**' -l

Repository: Autonomy-Logic/openplc-editor

Length of output: 1591


Frontend directly importing runtime function from @root/backend/shared/ethercat/device-matcher.

getBestMatchQuality is a runtime function imported from backend code in a frontend component, violating the architecture boundary. The function is pure and should be exposed through a port interface and consumed via dependency injection, following the guideline: "Never import backend or Electron APIs directly in frontend code; use port interfaces through dependency injection instead" and consuming ports through convenience hooks from @root/middleware/shared/providers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/components/discovered-device-table.tsx
at line 1, The component discovered-device-table.tsx imports the backend runtime
function getBestMatchQuality directly; instead create (or use) a port that
exposes a pure matching utility (e.g., DeviceMatcherPort with a
getBestMatchQuality method), register/implement that port on the backend side,
then inject/consume it in the frontend via the provided convenience hook from
`@root/middleware/shared/providers` (e.g., useDeviceMatcher or similar) and
replace direct imports of getBestMatchQuality with calls to the injected port
method inside the component; ensure the port interface matches the original
function signature so discovered-device-table.tsx can call the injected
getBestMatchQuality without importing backend code.

Comment on lines +664 to +681
// ===================== ETHERCAT DISCOVERY =====================
this.registerHandle('ethercat:get-interfaces', this.handleEtherCATGetInterfaces)
this.registerHandle('ethercat:get-status', this.handleEtherCATGetStatus)
this.registerHandle('ethercat:scan', this.handleEtherCATScan)
this.registerHandle('ethercat:test', this.handleEtherCATTest)
this.registerHandle('ethercat:validate', this.handleEtherCATValidate)
this.registerHandle('ethercat:get-runtime-status', this.handleEtherCATGetRuntimeStatus)

// ===================== ESI REPOSITORY =====================
this.registerHandle('esi:load-repository-index', this.handleESILoadRepositoryIndex)
this.registerHandle('esi:save-xml-file', this.handleESISaveXmlFile)
this.registerHandle('esi:load-xml-file', this.handleESILoadXmlFile)
this.registerHandle('esi:delete-xml-file', this.handleESIDeleteXmlFile)
this.registerHandle('esi:parse-and-save-file', this.handleESIParseAndSaveFile)
this.registerHandle('esi:clear-repository', this.handleESIClearRepository)
this.registerHandle('esi:load-device-full', this.handleESILoadDeviceFull)
this.registerHandle('esi:load-repository-light', this.handleESILoadRepositoryLight)
this.registerHandle('esi:migrate-repository', this.handleESIMigrateRepository)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check that new EtherCAT/ESI IPC channels registered in main.ts are referenced by renderer wrappers.
rg -n -C2 --glob '*.{ts,tsx}' "'(ethercat|get-interfaces|ethercat:get-status|ethercat:scan|ethercat:test|ethercat:validate|ethercat:get-runtime-status|esi:load-repository-index|esi:save-xml-file|esi:load-xml-file|esi:delete-xml-file|esi:parse-and-save-file|esi:clear-repository|esi:load-device-full|esi:load-repository-light|esi:migrate-repository)'"

Repository: Autonomy-Logic/openplc-editor

Length of output: 28244


Add missing renderer wrapper for ethercat:get-interfaces.

The IPC handler is registered in main.ts (line 665) but the renderer wrapper is missing from renderer.ts. All other channels have their async wrappers properly exposed. Add the etherCATGetInterfaces function to renderer.ts to maintain parity between main-process registration and renderer-process invocation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/modules/ipc/main.ts` around lines 664 - 681, The main process
registers the IPC channel 'ethercat:get-interfaces' via registerHandle but
renderer.ts lacks the corresponding wrapper; add an async exported function
named etherCATGetInterfaces in renderer.ts that calls the IPC invoke for
'ethercat:get-interfaces' (matching the pattern used by other wrappers like
etherCATGetStatus/etherCATScan) so renderer code can call the channel
consistently.

Copy link
Copy Markdown
Member

@JoaoGSP JoaoGSP left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@JoaoGSP JoaoGSP merged commit 67a7545 into development Apr 24, 2026
13 checks passed
@JoaoGSP JoaoGSP deleted the refactor/concrete-ethercat-config-types branch April 24, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants