Skip to content

fix(ethercat): prevent slave name collisions across masters [DOPE-281] - #754

Merged
JoaoGSP merged 6 commits into
developmentfrom
fix/ethercat-slave-name-collision
May 20, 2026
Merged

fix(ethercat): prevent slave name collisions across masters [DOPE-281]#754
JoaoGSP merged 6 commits into
developmentfrom
fix/ethercat-slave-name-collision

Conversation

@marconetsf

@marconetsf marconetsf commented May 11, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Source slave name from <Type> (short, e.g. EL1809) instead of the long <Name LcId> descriptor.
  • Auto-suffix _NN at creation when the base name already exists in any master.
  • Reject rename to a name already taken by another slave, matching the pattern used by POU / datatype / master renames.
  • Re-add the long ESI descriptor as a subtitle below the slave header now that the title shows the short form.

Linked: DOPE-281

Test plan

  • Scan Bus: add two slaves of the same model → second is auto-suffixed (EL1809, EL1809_01).
  • Repository tab: same behavior on add.
  • Cross-bus: add EL1809 on bus A and on bus B → second becomes EL1809_01.
  • Rename a slave to a name already used by another slave → operation rejected, label reverts.
  • Rename a slave to its own current name → no-op accepted.
  • Slave editor shows the long ESI descriptor as a subtitle under the short name.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • EtherCAT device editor now displays additional device identification information in the header when available.
    • Automatic generation of unique EtherCAT slave device names to prevent naming conflicts across the project.
  • Bug Fixes

    • Device rename operations now prevent name collisions with other devices on the same or different buses.

Review Change Stack

- Source slave name from <Type> (short, e.g. "EL1809") instead of the long <Name LcId> descriptor.
- Auto-suffix _NN at creation when the base collides with any existing slave in any master.
- Reject rename to a name already taken by another slave, matching the pattern used by POU/datatype/master renames.
- Re-add the long ESI descriptor as a subtitle below the slave header now that the title shows the short form.

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

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This PR introduces utility functions for deterministic EtherCAT slave name generation and collection, integrates them into store rename validation and device-add flows to enforce globally-unique names, and displays ESI-derived device identifiers in the editor header.

Changes

EtherCAT Slave Unique Naming

Layer / File(s) Summary
Name Generation and Collection Utilities
src/frontend/utils/short-device-name.ts, src/frontend/utils/unique-slave-name.ts
getShortDeviceName extracts compact device identifiers from ESI name/type via SKU-token parsing and fallback chains. collectAllSlaveNames gathers all slave names from project remotes. generateUniqueSlaveName reserves collision-free names by appending numeric suffixes.
Utility Function Tests
src/frontend/utils/__tests__/short-device-name.test.ts, src/frontend/utils/__tests__/unique-slave-name.test.ts
Tests cover SKU parsing, whitespace handling, multi-master name collection, collision detection, and numeric suffix padding and widening.
Store Rename with Uniqueness Constraint
src/frontend/store/slices/shared/slice.ts, src/frontend/store/__tests__/shared-slice.test.ts
Rename action imports collectAllSlaveNames and rejects target names that already exist across all slaves, while allowing no-op same-name renames. Tests verify conflict rejection and error handling.
Device Addition Flows with Unique Naming
src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx
Scan-add and repository-add flows collect global slave names once, derive short base names, generate collision-free unique names per device, and reserve them incrementally instead of using raw device names.
Editor Header ESI Device Name Display
src/frontend/components/_features/[workspace]/editor/device/ethercat/ethercat-device-editor.tsx
Editor header conditionally shows the resolved ESI device name as a sub-label when it differs from the configured device name.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Autonomy-Logic/openplc-editor#704: Introduces the EtherCATEditor UI component that this PR extends with collision-free slave-name generation in device-add flows.
  • Autonomy-Logic/openplc-editor#731: Extends EtherCAT rename and delete actions with stricter uniqueness validation that directly depends on the same collectAllSlaveNames utility introduced here.

Suggested labels

bug

Suggested reviewers

  • JoaoGSP
  • dcoutinho1328

Poem

A rabbit hops through names with care, 🐰
Checking collisions here and there,
Short suffixes numbered neat—_01, _02 fare,
No two slaves clash in the author's prayer!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main objective: preventing EtherCAT slave name collisions across masters, which is the primary focus of the changeset.
Description check ✅ Passed The description covers the main changes, includes test plan items, and links to the Jira issue. While the DOD checklist is provided as a template, the author has partially filled the description with substantive content.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/ethercat-slave-name-collision

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

marconetsf and others added 3 commits May 11, 2026 10:45
The helper was misplaced under backend/shared/ethercat/, which made the
store import violate the layer rule "Store must not import from Backend
Shared". Move it to frontend/utils/ alongside next-name.ts and
ethercat-status.ts — the conventional location for pure helpers shared
between store and components.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inline a single-element array literal that exceeded the explicit-wrap
heuristic but fits within the 120-char width.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@marconetsf
marconetsf requested a review from JoaoGSP May 11, 2026 09:51

@JoaoGSP JoaoGSP left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for tackling this — the fix works end-to-end and the tests are solid. Before approving I want to flag one thing about scope:

The DOPE-281 description identifies the root cause as tabs, editor, and file slices keying by slave.name alone, and the AC reads "Two masters can hold slaves with the same name; clicking each opens its own editor with its own configuration, independently." This PR takes the inverse approach — it makes duplicate names impossible rather than making the slices handle them.

Both approaches close the user-visible bug, and this one is lower-risk. But two things still worry me:

  1. Legacy projects. A project saved before this fix can still contain duplicate slave names across masters. Opening it will hit the original bug — no migration / load-time de-dup. Is there a known set of in-the-wild projects we should check, or do we want a one-time fixup on project load?
  2. Other write paths. This guard runs in ethercatDeviceActions.rename and in the two creation paths in index.tsx. If anything else ever writes to ethercatConfig.devices[].name (paste, import, undo replay), the name-keyed-slice bug returns. Worth a comment on collectAllSlaveNames saying "all writes must funnel through here," or moving the check into a lower layer.

If the scope change is intentional, please update DOPE-281's description/AC so a future reviewer (or audit) doesn't trip on it. Otherwise happy to approve once we agree on (1).

Comment thread src/frontend/utils/unique-slave-name.ts
let candidate = base
let i = 0
while (taken.has(candidate)) {
i++

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Minor: when existing is already a Set (which is the hot path inside the scan-bus loop), this copies it on every call. Not a real cost at our scale, but existing instanceof Set ? existing : new Set(existing) is free if you want it. Optional.

Comment thread src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx Outdated
Comment thread src/frontend/store/slices/shared/slice.ts
The previous chain assumed <Type> always carried text, but the ESI
schema only requires its ProductCode/RevisionNo attributes. Vendors
emitting self-closing <Type/> were silently dropping to the long
localized <Name>, defeating the readability win of short product codes.

Introduce getShortDeviceName with a tiered fallback that validates
<Type> shape, extracts SKU-shaped tokens from <Name>, and falls back
to the canonical (productCode, revisionNo) identity when nothing
readable is available. Also clarify the unique-slave-name padStart
docstring and document the rename guard as the sole rejecting
enforcement point for slave-name uniqueness across masters.

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

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (1)
src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx (1)

473-503: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use latest store state when generating/appending repository-added devices.

After await (Line 476), Line 486 and Line 502 still use render-time snapshots (project.data.remoteDevices, configuredDevices). Rapid consecutive adds can produce duplicate names/positions or drop a previously added device.

💡 Proposed fix
   const handleAddDeviceFromBrowser = useCallback(
     async (ref: ESIDeviceRef, device: ESIDeviceSummary, repoItem: ESIRepositoryItemLight) => {
@@
-      const nextPosition =
-        configuredDevices.length > 0 ? Math.max(...configuredDevices.map((d) => d.position ?? 0)) + 1 : 1
+      const { project: latestProject } = useOpenPLCStore.getState()
+      const latestRemoteDevices = latestProject.data.remoteDevices ?? []
+      const latestMaster =
+        latestRemoteDevices.find((d) => d.name === deviceName)?.ethercatConfig?.devices ?? []
+
+      const nextPosition =
+        latestMaster.length > 0 ? Math.max(...latestMaster.map((d) => d.position ?? 0)) + 1 : 1

       const baseName = getShortDeviceName(device)
-      const uniqueName = generateUniqueSlaveName(baseName, collectAllSlaveNames(project.data.remoteDevices))
+      const uniqueName = generateUniqueSlaveName(baseName, collectAllSlaveNames(latestRemoteDevices))
@@
-      syncDevicesToStore([...configuredDevices, newDevice])
+      syncDevicesToStore([...latestMaster, newDevice])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/index.tsx
around lines 473 - 503, In handleAddDeviceFromBrowser: after the await that
loads the full device, re-read the current store state instead of using the
render-time snapshots (project.data.remoteDevices and configuredDevices) so
name/position generation and the final sync use the latest list; specifically,
call the store/getter you use elsewhere to obtain the up-to-date remoteDevices
and configuredDevices, then pass those into collectUsedIecAddresses,
collectAllSlaveNames/generateUniqueSlaveName and compute nextPosition, and
finally call syncDevicesToStore with the fresh list merged with the new device
(rather than spreading the stale configuredDevices variable).
🧹 Nitpick comments (1)
src/frontend/store/slices/shared/slice.ts (1)

9-9: ⚡ Quick win

Use path alias instead of relative import.

Replace the relative import with the @root/* path alias for consistency with project conventions.

♻️ Proposed fix
-import { collectAllSlaveNames } from '../../../utils/unique-slave-name'
+import { collectAllSlaveNames } from '`@root/frontend/utils/unique-slave-name`'

As per coding guidelines, prefer path alias @root/* for imports instead of relative paths to ./src/*.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/frontend/store/slices/shared/slice.ts` at line 9, Replace the relative
import of collectAllSlaveNames with the project path alias: locate the import
statement that currently reads "import { collectAllSlaveNames } from
'../../../utils/unique-slave-name'" in slice.ts and change it to use the `@root`
alias (e.g. import { collectAllSlaveNames } from
'`@root/utils/unique-slave-name`') so it follows the project's path-alias
convention; ensure the module specifier matches the existing alias pattern used
elsewhere in the repo and that the symbol name collectAllSlaveNames remains
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@src/frontend/components/_features/`[workspace]/editor/device/ethercat/index.tsx:
- Around line 473-503: In handleAddDeviceFromBrowser: after the await that loads
the full device, re-read the current store state instead of using the
render-time snapshots (project.data.remoteDevices and configuredDevices) so
name/position generation and the final sync use the latest list; specifically,
call the store/getter you use elsewhere to obtain the up-to-date remoteDevices
and configuredDevices, then pass those into collectUsedIecAddresses,
collectAllSlaveNames/generateUniqueSlaveName and compute nextPosition, and
finally call syncDevicesToStore with the fresh list merged with the new device
(rather than spreading the stale configuredDevices variable).

---

Nitpick comments:
In `@src/frontend/store/slices/shared/slice.ts`:
- Line 9: Replace the relative import of collectAllSlaveNames with the project
path alias: locate the import statement that currently reads "import {
collectAllSlaveNames } from '../../../utils/unique-slave-name'" in slice.ts and
change it to use the `@root` alias (e.g. import { collectAllSlaveNames } from
'`@root/utils/unique-slave-name`') so it follows the project's path-alias
convention; ensure the module specifier matches the existing alias pattern used
elsewhere in the repo and that the symbol name collectAllSlaveNames remains
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 0349893b-bf66-4316-8bfa-bce0901432de

📥 Commits

Reviewing files that changed from the base of the PR and between f06a6cd and 7679a9f.

📒 Files selected for processing (8)
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/ethercat-device-editor.tsx
  • src/frontend/components/_features/[workspace]/editor/device/ethercat/index.tsx
  • src/frontend/store/__tests__/shared-slice.test.ts
  • src/frontend/store/slices/shared/slice.ts
  • src/frontend/utils/__tests__/short-device-name.test.ts
  • src/frontend/utils/__tests__/unique-slave-name.test.ts
  • src/frontend/utils/short-device-name.ts
  • src/frontend/utils/unique-slave-name.ts

@JoaoGSP
JoaoGSP merged commit 4db9dd4 into development May 20, 2026
12 checks passed
@JoaoGSP
JoaoGSP deleted the fix/ethercat-slave-name-collision branch May 20, 2026 12:40
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