Skip to content

fix(iec-address): close the paths where address recompaction silently no-ops (DOPE-440) - #1005

Open
JulioSergioFS wants to merge 2 commits into
developmentfrom
bugfix/DOPE-440-iec-address-recompaction-gaps
Open

fix(iec-address): close the paths where address recompaction silently no-ops (DOPE-440)#1005
JulioSergioFS wants to merge 2 commits into
developmentfrom
bugfix/DOPE-440-iec-address-recompaction-gaps

Conversation

@JulioSergioFS

@JulioSergioFS JulioSergioFS commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Pull request info

Description of the changes proposed

The scenario named in the ticket was already fixed. The central registry work
(Phases 3–6, July) made deleteIOGroup call recalculateIecAddresses(), and the test
recompacts the surviving groups into the freed slots (bug #4) has passed ever since.
The ticket predates that work and was never re-verified. What remained are the paths
where the recompaction silently no-ops, plus the ticket's own "May also affect other
plugins" clause — which turns out to be literally true for pin mapping.

  • An unresolved target is not a target without capabilities. resolveTargetCapabilities(undefined)
    answers EMPTY_CAPABILITIES; feeding that to the allocator filters out every consumer,
    so assignments comes back empty and the write-back leaves the stale addresses in place
    while reporting success. A board id fails to resolve whenever the VPP package isn't
    installed, the project was authored elsewhere, or the catalogue hasn't loaded yet. A board
    that answered is now honoured exactly as declared (the deliberate kind-deactivation on
    target switch is untouched); a board that did not resolve allocates with every producer
    active.
  • Duplicate addresses were being persisted. The provisional pool had the same conflation,
    so on a Runtime v3 or unresolved target every new group restarted at %IW0.
    modbus-tcp-remote is now always counted in its own pool. This also collapses the three
    duplicated ~25-line pool preambles into one helper.
  • Pin mapping never recompacted. removePin slides the freed slot to the end of the pin
    block; createNewPin can mint an address already held by a VPP or Modbus channel, with no
    conflict report. All three pin actions now compare the pin address signature and recalculate
    only when it moves, so alias-only and pin-number-only edits stay free.
  • Docs: new §5.1 in docs/iec-address-registry.md capturing the invariant
    "unknown target ⇒ permissive for allocation, empty for feature gating".

Summary by CodeRabbit

  • Bug Fixes

    • IEC addresses now recalculate when pins are added, removed, or change type.
    • Prevented stale addresses, stranded slots, and cross-producer allocation collisions.
    • Improved allocation for unresolved boards and boards without declared remote-I/O capabilities.
    • Modbus address pools now consistently include remote consumers.
  • Documentation

    • Added documentation covering capability-scoped allocation and unresolved-target handling.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@JulioSergioFS, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0315a71a-71e2-4348-9530-df28b6cda475

📥 Commits

Reviewing files that changed from the base of the PR and between 8ef397e and 09f4158.

📒 Files selected for processing (1)
  • src/frontend/store/__tests__/project-slice.test.ts

Walkthrough

The change adds capability-scoped IEC address allocation, permissive allocation for unresolved targets, centralized Modbus pools, and conditional address recalculation after pin layout changes. Tests cover resolved, unresolved, incapable, and pin-edit scenarios.

Changes

IEC address allocation and recalculation

Layer / File(s) Summary
Address producer capability contracts
src/middleware/shared/utils/target-capabilities/*, src/middleware/shared/utils/iec-address/address-pool.ts
AddressProducerCapabilities narrows allocation inputs to address-producing capabilities. ALL_ADDRESS_PRODUCERS_ACTIVE supports unresolved-target allocation.
Capability-aware project allocation
src/frontend/store/slices/project/slice.ts, src/frontend/store/__tests__/project-slice.test.ts, docs/iec-address-registry.md
Resolved targets use supported producer kinds. Unresolved targets use all producers. Modbus pools remain active and support clearing edited groups. Regression tests cover allocation, recompaction, and alias validation.
Pin edit recalculation
src/frontend/store/slices/device/slice.ts, src/frontend/store/__tests__/device-slice.test.ts, docs/iec-address-registry.md
Pin creation, removal, and address-changing updates trigger IEC recalculation. Alias-only and pin-number-only edits do not. Tests cover Modbus address reuse and pin-type changes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeviceSlice
  participant ProjectSlice
  participant IECAddressRegistry
  participant AddressPool
  DeviceSlice->>DeviceSlice: compare ordered pin-address layout
  DeviceSlice->>ProjectSlice: request recalculation when addresses change
  ProjectSlice->>IECAddressRegistry: rebuild project-wide registry
  IECAddressRegistry->>AddressPool: allocate with target capabilities
  AddressPool-->>IECAddressRegistry: return IEC address assignments
  IECAddressRegistry-->>ProjectSlice: update registry and allocations
Loading

Possibly related PRs

Suggested reviewers: thiagoralves, marconetsf

Poem

A rabbit checks each pin in line,
While Modbus slots regroup and shine.
Unknown boards keep producers near,
Changed pin addresses trigger cheer.
Alias edits leave the map benign.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the IEC address recompaction fix and the silently no-op paths addressed by the pull request.
Description check ✅ Passed The description clearly explains the affected paths, implementation changes, test scope, and documentation updates, although template checklist sections are absent.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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.
✨ 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 bugfix/DOPE-440-iec-address-recompaction-gaps

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.

@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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
src/frontend/store/slices/project/slice.ts (1)

265-272: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate vendorScreenData['io-mapping'] before address allocation.

deviceConfigurationSchema validates vendorScreenData only as Record<string, unknown>. Invalid entries values can make recalculateIecAddresses() throw or create malformed registry keys. Validate the nested shape with Zod or a type guard, and remove the type assertion at lines 265-272.

🤖 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/project/slice.ts` around lines 265 - 272, Update
readVppEntries to validate vendorScreenData['io-mapping'] before returning
entries, using the existing Zod validation approach or a type guard to accept
only an object with a VppMappingEntry[] entries property; return an empty array
for invalid values. Remove the current type assertion and ensure
recalculateIecAddresses receives only validated entries.

Source: Coding guidelines

🤖 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.

Inline comments:
In `@src/frontend/store/__tests__/device-slice.test.ts`:
- Around line 1792-1793: Update modbusAddresses to remove all non-null
assertions and explicitly validate each intermediate fixture value: the first
remote device, its modbusTcpConfig, ioGroups entry, and ioPoints. Throw a
diagnostic error identifying the missing fixture data when any value is absent,
then map the validated IO points to iecLocation.

---

Outside diff comments:
In `@src/frontend/store/slices/project/slice.ts`:
- Around line 265-272: Update readVppEntries to validate
vendorScreenData['io-mapping'] before returning entries, using the existing Zod
validation approach or a type guard to accept only an object with a
VppMappingEntry[] entries property; return an empty array for invalid values.
Remove the current type assertion and ensure recalculateIecAddresses receives
only validated entries.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8378ad7b-5e15-41ea-977b-4f0611120f22

📥 Commits

Reviewing files that changed from the base of the PR and between 6e7584c and 8ef397e.

📒 Files selected for processing (9)
  • docs/iec-address-registry.md
  • src/frontend/store/__tests__/device-slice.test.ts
  • src/frontend/store/__tests__/project-slice.test.ts
  • src/frontend/store/slices/device/slice.ts
  • src/frontend/store/slices/project/slice.ts
  • src/middleware/shared/utils/iec-address/address-pool.ts
  • src/middleware/shared/utils/target-capabilities/index.ts
  • src/middleware/shared/utils/target-capabilities/presets.ts
  • src/middleware/shared/utils/target-capabilities/types.ts

Comment on lines +1792 to +1793
const modbusAddresses = (store: ReturnType<typeof makeStore>) =>
store.getState().project.data.remoteDevices![0].modbusTcpConfig!.ioGroups[0].ioPoints!.map((p) => p.iecLocation)

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

file="src/frontend/store/__tests__/device-slice.test.ts"

printf '%s\n' '--- file status ---'
git ls-files -- "$file"
wc -l "$file"

printf '%s\n' '--- relevant source ---'
sed -n '1765,1805p' "$file"

printf '%s\n' '--- helper references ---'
rg -n -C 3 'modbusAddresses|remoteDevices|modbusTcpConfig|ioPoints' "$file"

printf '%s\n' '--- AST outline ---'
ast-grep outline "$file" | sed -n '1,160p'

Repository: Autonomy-Logic/openplc-editor

Length of output: 4742


🏁 Script executed:

#!/bin/bash
set -eu

file="src/frontend/store/__tests__/device-slice.test.ts"

printf '%s\n' '--- store factory and fixture setup ---'
sed -n '1,75p' "$file"
sed -n '1720,1871p' "$file"

printf '%s\n' '--- state and action definitions ---'
rg -n -C 4 'remoteDevices|interface .*Project|type .*Project|addIOGroup' src/frontend/store src -g '*.ts' -g '*.tsx' | sed -n '1,260p'

printf '%s\n' '--- read-only structural verifier ---'
python3 - <<'PY'
from pathlib import Path
import re

path = Path("src/frontend/store/__tests__/device-slice.test.ts")
text = path.read_text()
lines = text.splitlines()

helper = next(
    (i + 1 for i, line in enumerate(lines)
     if "const modbusAddresses" in line),
    None,
)
assert helper is not None, "modbusAddresses helper is missing"

helper_lines = "\n".join(lines[helper - 1:helper + 1])
assert helper_lines.count("!") == 3, helper_lines
assert text.count("modbusAddresses(store)") == 4, "unexpected helper call count"

fixture_start = next(
    i for i, line in enumerate(lines)
    if line.startswith("function seedPinsAndGroup")
)
fixture_end = next(
    i for i in range(fixture_start + 1, len(lines))
    if lines[i].startswith("    const modbusAddresses")
)
fixture = "\n".join(lines[fixture_start:fixture_end])
for required in ("remoteDevices:", "modbusTcpConfig:", "ioGroups: []", "ioPoints: []"):
    assert required in fixture, f"fixture does not contain {required}"

print(f"helper line: {helper}")
print("non-null assertions in helper: 3")
print("modbusAddresses(store) call sites: 4")
print("fixture initializes remoteDevices, modbusTcpConfig, ioGroups, and ioPoints")
PY

Repository: Autonomy-Logic/openplc-editor

Length of output: 29306


🏁 Script executed:

#!/bin/bash
set -eu

file="src/frontend/store/__tests__/device-slice.test.ts"

printf '%s\n' '--- store factory and fixture setup ---'
sed -n '1,75p' "$file"
sed -n '1720,1871p' "$file"

printf '%s\n' '--- state and action definitions ---'
rg -n -C 4 'remoteDevices|interface .*Project|type .*Project|addIOGroup' src/frontend/store src -g '*.ts' -g '*.tsx' | sed -n '1,260p'

printf '%s\n' '--- read-only structural verifier ---'
python3 - <<'PY'
from pathlib import Path

path = Path("src/frontend/store/__tests__/device-slice.test.ts")
text = path.read_text()
lines = text.splitlines()

helper = next(
    (i + 1 for i, line in enumerate(lines)
     if "const modbusAddresses" in line),
    None,
)
assert helper is not None, "modbusAddresses helper is missing"

helper_lines = "\n".join(lines[helper - 1:helper + 1])
assert helper_lines.count("!") == 3, helper_lines
assert text.count("modbusAddresses(store)") == 4, "unexpected helper call count"

fixture_start = next(
    i for i, line in enumerate(lines)
    if line.startswith("function seedPinsAndGroup")
)
fixture_end = next(
    i for i in range(fixture_start + 1, len(lines))
    if lines[i].startswith("    const modbusAddresses")
)
fixture = "\n".join(lines[fixture_start:fixture_end])
for required in ("remoteDevices:", "modbusTcpConfig:", "ioGroups: []", "ioPoints: []"):
    assert required in fixture, f"fixture does not contain {required}"

print(f"helper line: {helper}")
print("non-null assertions in helper: 3")
print("modbusAddresses(store) call sites: 4")
print("fixture initializes remoteDevices, modbusTcpConfig, ioGroups, and ioPoints")
PY

Repository: Autonomy-Logic/openplc-editor

Length of output: 32651


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact project and Modbus type declarations ---'
rg -n -C 6 'PLCProjectData|PLCRemoteDevice|ModbusIOGroup|ioPoints\??|remoteDevices\??|modbusTcpConfig\??' \
  src/backend/shared src/frontend/store/slices/project src/middleware/shared \
  -g '*.ts' | sed -n '1,320p'

printf '%s\n' '--- addIOGroup implementation ---'
rg -n -C 12 'addIOGroup\s*[:=]|function addIOGroup|addIOGroup\(' \
  src/frontend/store/slices/project -g '*.ts' | sed -n '1,260p'

printf '%s\n' '--- focused structural verifier ---'
python3 - <<'PY'
from pathlib import Path

path = Path("src/frontend/store/__tests__/device-slice.test.ts")
lines = path.read_text().splitlines()

helper_index = next(i for i, line in enumerate(lines) if "const modbusAddresses" in line)
helper = "\n".join(lines[helper_index:helper_index + 2])
assert helper.count("!") == 3
assert sum("modbusAddresses(store)" in line for line in lines) == 4

seed_index = next(i for i, line in enumerate(lines) if "function seedPinsAndGroup" in line)
helper_index = next(i for i, line in enumerate(lines) if "const modbusAddresses" in line)
fixture = "\n".join(lines[seed_index:helper_index])

required = ("remoteDevices:", "modbusTcpConfig:", "ioGroups: []", "ioPoints: []")
missing = [item for item in required if item not in fixture]
assert not missing, missing

print(f"helper source line: {helper_index + 1}")
print("non-null assertions in helper: 3")
print("helper call sites: 4")
print("fixture initializes all four expected Modbus containers")
PY

Repository: Autonomy-Logic/openplc-editor

Length of output: 32877


Remove the non-null assertions from modbusAddresses.

Narrow each intermediate value explicitly, including the first remote device, its Modbus configuration, and its IO points. Throw a diagnostic error when the fixture is incomplete.

🤖 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/__tests__/device-slice.test.ts` around lines 1792 - 1793,
Update modbusAddresses to remove all non-null assertions and explicitly validate
each intermediate fixture value: the first remote device, its modbusTcpConfig,
ioGroups entry, and ioPoints. Throw a diagnostic error identifying the missing
fixture data when any value is absent, then map the validated IO points to
iecLocation.

Source: Coding guidelines

@Gustavohsdp

Copy link
Copy Markdown
Contributor

Review — good to go after one small test cleanup

Reviewed together with the web sync PR Autonomy-Logic/openplc-web#664 — the 8 shared files are byte-identical; docs/iec-address-registry.md exists only here.

Read against the ticket (DOPE-440: "IEC addresses not recalculated after a gap … May also affect other plugins", High) rather than just the diff.

Verified: 8/8 shared files byte-identical (git hash-object); 440 tests green (device-slice, project-slice); CI 12/12 editor, 9/9 web.

The shape of this PR is right

"The scenario named in the ticket was already fixed" — and saying so, then going after the paths where the recompaction silently no-ops, is more useful than either re-fixing it or closing as cannot-reproduce. The core is one function and it reads clearly:

function allocationCapabilities(live: ProjectSliceRoot): AddressProducerCapabilities {
  const boardInfo = resolveBoardInfo(live)
  return boardInfo ? resolveTargetCapabilities(boardInfo) : ALL_ADDRESS_PRODUCERS_ACTIVE
}

The bug underneath it is worth restating because it's the kind that hides forever: resolveTargetCapabilities(undefined) answered EMPTY_CAPABILITIES, which filtered out every consumer, so assignments came back empty and the write-back left the stale addresses in place while reporting success. Silent success is the worst failure mode there is, and the reachability list (VPP not installed, project authored elsewhere, catalogue not loaded yet) is ordinary rather than exotic.

Separating "permissive for allocation" from "empty for feature gating" into two distinct presets, instead of overloading one, is what makes that fix safe to reason about.

The ticket's "may also affect other plugins" clause

That turned out literally true for pin mapping, and finding it required taking the clause seriously rather than treating it as boilerplate: removePin slid the freed slot to the end of the pin block instead of recompacting, and createNewPin could mint an address already held by a VPP or Modbus channel with no conflict report.

The guard on top of it is the detail I'd single out — all three pin actions compare the pin address signature and recalculate only when it moves, so alias-only and pin-number-only edits stay free. That's what keeps a correctness fix from turning into a performance regression on every keystroke.


🟡 1 — CodeRabbit's open finding: non-null assertions in the test helper

device-slice.test.ts:1793:

store.getState().project.data.remoteDevices![0].modbusTcpConfig!.ioGroups[0].ioPoints!.map((p) => p.iecLocation)

Why it's worth doing: CLAUDE.md is explicit ("No non-null assertions (!): handle the undefined case or narrow explicitly"), and it's the same class you fixed on request in #989 and #994 — the throwing-helper pattern from #989 (getColorsDataType()) drops straight in here. Three assertions in one line, one helper. The thread has no reply yet, so it reads as overlooked rather than declined.

Note this file is mirrored, so the fix has to land in both repos byte-identically.

🟡 2 — Document the consequence of "unknown ⇒ permissive", next to the invariant

§5.1 records "unknown target ⇒ permissive for allocation, empty for feature gating", which is the right invariant. What it doesn't record is what follows from it: a project authored while the target is unresolved allocates with every producer active, so when the VPP is later installed and the target resolves, capabilities shrink and the addresses recompact — the user sees addresses move on the first open after installing the package.

Why it's worth a sentence. For alias-bound variables that's transparent, which is what the registry exists for. For variables bound to a literal %addr the binding doesn't follow, and the amber orphan/collision glyph is what catches it. That's acceptable behaviour — and strictly better than the old symptom of stale addresses reported as success — but someone reading §5.1 to understand the model won't derive "resolving later can move addresses" from the invariant alone. One line beside it closes the loop.


Coordination

This pair and the DOPE-439 pair both touch src/frontend/store/slices/project/slice.ts in different regions (address allocation here, addIOGroup/updateIOGroup there), so expect a mechanical conflict for whichever merges second. Keep each pair's two repos merging in short sequence — the shared-surface gate only tolerates divergence while an open PR exists in the other repo.

Review assisted by Claude Code.

@thiagoralves

Copy link
Copy Markdown
Contributor

On the third bullet — forcing modbus-tcp-remote active in the provisional pool

I reviewed this PR for conceptual correctness rather than line-level style. Two of the three fixes hold up well, and I want to record that before the objection:

  • Pin edits driving the recalculation is right. I went in expecting this to be wrong, on the assumption that pin mapping is Arduino-only — it isn't any more. com.openplc.raspberry-pi's raspberry-pi device is target.type: runtime-v4 with capabilities: { pinMapping: true }, so after the merge in resolveTargetCapabilities it carries pinMapping and modbusTcpRemote and ethercat at once. Pins and Modbus really do share one address space on a shipping board, and since pin-mapping stays out of ALLOCATED_KINDS no pin address is ever reallocated — the recalculation only recompacts the producers around the moved constraints, exactly like setDeviceBoard already does. The createNewPin collision it closes is a genuine persisted duplicate.
  • The unresolved-target diagnosis is real. Empty activeKindsallocate.ts:53 filters every consumer → assignments = {}applyModbusAddresses only writes when info.address is truthy, so everything freezes and the action still returns ok(). Confirmed in code.

The one I'd like changed before merge is buildModbusProducerPool forcing modbusTcpRemote: true.

The bug is real, but the fix leaves two answers to one question

The failure mode is exactly as described: pending is created fresh per addIOGroup call and the pool drops sibling Modbus claims at address-pool.ts:227, so every group restarts at %IW0 and duplicates get persisted. No argument there.

What I don't want to ship is the resulting split: after this change the codebase holds two contradictory answers to "is modbus-tcp-remote an active producer?" — forced on in buildModbusProducerPool, still off in recalculateIecAddresses via activeKindsFromCapabilities. On a Runtime v3 target that means IO-group addresses are assigned once by the provisional pool and then never managed again: deleteIOGroup won't recompact them, gaps never close, and no conflict is reported. It heals only if the user later switches to a capable target. Of the three possible states (UI gated / allocator manages it / neither), that hybrid is the least consistent one, and the whole point of the central registry was to have exactly one authority on this.

The root cause is a UI gate, not an allocator rule

The PR names the real cause in its own comment — "the Remote Devices branch is gated on the project TYPE instead" — and then works around it. That gate is projectCaps.hasRemoteDevices (ports/types.ts:514; false for libraries, true for PLC projects) at explorer/project.tsx:455 and create-element/index.tsx:37. Nothing there consults the target.

The rest of the codebase already behaves as though that combination is unsupported:

  • board.tsx:346 computes targetCantHostRemoteIo = !targetCaps.modbusTcpRemote && !targetCaps.ethercat and warns the user they're "losing remote I/O" on switch — then never enforces it afterwards.
  • location-dropdown-options.ts:105 hides remote-device option groups from the variable location dropdown when both flags are off. So on Runtime v3 today you can create IO points that no variable can reference — which to me is the clearest signal that the missing piece is the UI gate, not the pool.

And modbusTcpRemote: false for v3 looks correct on the merits: the v3 branch of the build (compile/pipeline.ts:671+) uploads a single program.st and delivers no remote-I/O configuration at all. v3 manages its slave devices in its own web UI. So the editor genuinely cannot honour that data on v3, and flipping RUNTIME_V3_CAPABILITIES.modbusTcpRemote to true would be the wrong way out.

What I'd like instead

Gate remote-device creation on the capability — modbusTcpRemote || ethercat, the same predicate board.tsx:346 and location-dropdown-options.ts:105 already use — at the explorer branch and the create-element path. With that in place "capability off" and "screen unreachable" coincide for remote devices exactly as they already do for pin mapping, VPP and EtherCAT, the special case in buildModbusProducerPool becomes unnecessary, and capability scoping stays the single source of truth.

Note that the duplicate-address scenario the override fixes then only remains reachable on an unresolved target, where ALL_ADDRESS_PRODUCERS_ACTIVE already turns the kind on — so the override has no remaining job. The helper itself is a good consolidation and worth keeping; it's only the { ...allocationCapabilities(live), modbusTcpRemote: true } spread that should go.

Happy for the UI gate to land as a separate ticket if you'd rather keep this PR tight — in that case please drop the override here and let the v3 duplicate stand until the gate lands, rather than merging a workaround that becomes dead code a week later.

Smaller things, non-blocking

  • Narrowing buildAddressPool to AddressProducerCapabilities is a good change — it makes "not a target" representable in the type instead of by convention.
  • Please add a log line when allocation runs against an unresolved target. Permissive is the right default, but this class of bug survived months precisely because it was silent, and ALL_ADDRESS_PRODUCERS_ACTIVE is invisible from the outside too.
  • The new ?. chains in readVppEntries / resolveBoardInfo read as noise next to buildIecRegistry, which reads the same slices non-optionally a few lines away. One style or the other.
  • expect(writes()).toBe(2) as a proxy for "did it recalculate" will break on any unrelated extra setState. The address assertions in those same tests already prove the behaviour.
  • Worth deciding what happens to DOPE-440 itself: per the description its own scenario has been passing since July, so this PR is really "adjacent gaps found while verifying". Probably close the ticket as already-fixed and track these under their own key.

Longer term, the fact that a project stores only a board name is what makes the unresolved case possible at all. Persisting the four producer flags into the project file would mean an uninstalled package never loses target knowledge, and ALL_ADDRESS_PRODUCERS_ACTIVE would become a true last resort. Follow-up, not this PR.

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.

3 participants