refactor: Unify debugger and OPC-UA variable index resolution#545
Conversation
The autocomplete component in the Ladder editor was not properly handling the Enter key when navigating to "Add variable" option with arrow keys. Root cause: A race condition occurred where the autocomplete component would unmount (due to onFocusOutside triggering closeModal) before the async useEffect chain could process the Enter key press. Solution: - Added triggerSubmit() method to autocomplete ref for synchronous submission - Updated Ladder components (variable, contact, coil) to call triggerSubmit() directly on Enter key press, bypassing the async keyPressed prop flow - This ensures the submit action completes before the blur/unmount occurs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Comprehensive documentation explaining: - Problem statement and current code duplication - Analysis of debug path formats in debug.c - Current architecture of debugger and OPC-UA systems - Detailed 6-phase refactoring plan: 1. Consolidate debug file parsing 2. Unify path building 3. Unify variable index lookup 4. Unify tree building with visitor pattern 5. Update debugger polling 6. Remove deprecated code after verification - Migration strategy with backwards compatibility - Testing and rollback plans Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
fix: Resolve keyboard navigation for Add Variable in Ladder editor
Add Slave ID (Unit ID) configuration for Modbus TCP devices to support communication through Modbus TCP gateways that forward messages to RS485 serial networks. The Slave ID is included in the Modbus TCP frame and used by gateways to address specific devices on the RS485 bus. Changes: - Add slaveId field to ModbusTcpConfigSchema (0-255, optional, default 1) - Add Slave ID input field in Remote Device editor UI - Update store slice to handle slaveId configuration - Include slave_id in generated Modbus master JSON configuration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When a project is loaded, deviceDefinitions.configuration.runtimeIpAddress was restored from the saved project, but runtimeConnection.ipAddress (ephemeral state) remained null. This caused the debugger and PLC logs polling to fail with "No runtime IP address configured" error. The fix syncs runtimeIpAddress to runtimeConnection.ipAddress when setDeviceDefinitions is called during project load. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
feat: Add Slave ID support for Modbus TCP remote devices
Consolidate the fallback index resolution logic used by both the debugger and OPC-UA systems into shared utilities. This ensures consistent behavior when resolving variable indexes for user-defined types (which can be either function blocks or structures). Key changes: - Add findDebugVariableWithFallback() to debug-variable-finder.ts - Add getIndexFromMapWithFallback() for polling map lookups - Update resolve-indices.ts to use shared fallback functions - Update debug-tree-traversal.ts to use shared fallback for FB fields - Update workspace-activity-bar to use findVariableIndexWithFallback() - Update workspace-screen.tsx polling to use shared fallback lookups - Extract debug-parser.ts with canonical parsing logic - Remove duplicate parsing logic from parse-debug-file.ts - Remove duplicate logic from debug-tree-builder.ts The unified fallback strategy (FB-style path first, then struct-style path) ensures that if one system can find a variable's index, the other will too. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. 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. Comment |
There was a problem hiding this comment.
Pull request overview
This PR consolidates debug variable index resolution logic to eliminate duplication between the debugger and OPC-UA subsystems, ensuring consistent behavior through shared utilities.
Changes:
- Extracted debug.c parsing and path resolution into canonical shared modules (
debug-parser.ts,debug-tree-traversal.ts) - Unified fallback resolution strategy for FB vs. struct paths in both debugger and OPC-UA systems
- Added Modbus TCP slave ID configuration support
- Fixed race condition in ladder editor autocomplete submission
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/debug-parser.ts | New canonical debug.c parser module |
| src/utils/debug-tree-traversal.ts | New visitor-pattern tree traversal for complex PLC types |
| src/utils/debug-variable-finder.ts | Added fallback resolution functions and path utilities |
| src/utils/opcua/resolve-indices.ts | Updated to use shared fallback resolution |
| src/renderer/utils/parse-debug-file.ts | Simplified to re-export from canonical parser |
| src/renderer/utils/debug-tree-builder.ts | Refactored to use shared traversal utilities |
| src/renderer/screens/workspace-screen.tsx | Updated polling code to use shared path resolution |
| src/renderer/components/_organisms/workspace-activity-bar/default.tsx | Updated to use shared index lookup functions |
| src/types/PLC/open-plc.ts | Added slaveId field to Modbus TCP config schema |
| src/utils/modbus/generate-modbus-master-config.ts | Added slave_id to generated Modbus config |
| src/renderer/store/slices/project/types.ts | Added slaveId to remote device config schema |
| src/renderer/store/slices/project/slice.ts | Added slaveId handling in updateRemoteDeviceConfig |
| src/renderer/store/slices/device/slice.ts | Added runtimeIpAddress sync to runtimeConnection |
| src/renderer/components/_features/[workspace]/editor/device/remote-device/index.tsx | Added Slave ID input field |
| src/renderer/components/_atoms/graphical-editor/ladder/variable.tsx | Fixed autocomplete Enter key race condition |
| src/renderer/components/_atoms/graphical-editor/ladder/contact.tsx | Fixed autocomplete Enter key race condition |
| src/renderer/components/_atoms/graphical-editor/ladder/coil.tsx | Fixed autocomplete Enter key race condition |
| src/renderer/components/_atoms/graphical-editor/autocomplete/index.tsx | Added triggerSubmit method to prevent race condition |
| src/main/modules/ipc/main.ts | Removed debug logging statement |
| docs/debugger-opcua-shared-utilities.md | Added comprehensive refactoring documentation |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (selectedVariable.positionInArray === -1) { | ||
| const addVariableOption = selectableValues.find((item) => item.type === 'add') | ||
| if (addVariableOption) { | ||
| submitAutocompletion({ variable: addVariableOption.variable }) | ||
| } else { | ||
| closeModal() | ||
| } | ||
| } else { | ||
| submitAutocompletion({ variable: selectedVariable.variable }) |
There was a problem hiding this comment.
The triggerSubmit function duplicates submission logic that exists elsewhere in the component. When selectedVariable.positionInArray !== -1, line 109 calls submitAutocompletion directly with selectedVariable.variable, but this logic should be verified against the existing submission handler to ensure consistent behavior (e.g., handling of edge cases like empty variable names).
| if (selectedVariable.positionInArray === -1) { | |
| const addVariableOption = selectableValues.find((item) => item.type === 'add') | |
| if (addVariableOption) { | |
| submitAutocompletion({ variable: addVariableOption.variable }) | |
| } else { | |
| closeModal() | |
| } | |
| } else { | |
| submitAutocompletion({ variable: selectedVariable.variable }) | |
| let variableToSubmit: { id: string; name: string } | null = null | |
| if (selectedVariable.positionInArray === -1) { | |
| const addVariableOption = selectableValues.find((item) => item.type === 'add') | |
| if (addVariableOption && addVariableOption.variable.name.trim() !== '') { | |
| variableToSubmit = addVariableOption.variable | |
| } | |
| } else if (selectedVariable.variable.name.trim() !== '') { | |
| variableToSubmit = selectedVariable.variable | |
| } | |
| if (variableToSubmit) { | |
| submitAutocompletion({ variable: variableToSubmit }) | |
| } else { | |
| closeModal() |
| }, | ||
| } | ||
| }, [selectedVariable, popoverRef, autocompleteFocus]) | ||
| }, [selectedVariable, selectableValues, popoverRef, autocompleteFocus]) |
There was a problem hiding this comment.
The useMemo dependency array includes submitAutocompletion and closeModal indirectly (they're called inside triggerSubmit), but these functions aren't listed as dependencies. If submitAutocompletion or closeModal are recreated on every render, the triggerSubmit closure will use stale references. Add submitAutocompletion and closeModal to the dependency array.
| }, [selectedVariable, selectableValues, popoverRef, autocompleteFocus]) | |
| }, [selectedVariable, selectableValues, popoverRef, autocompleteFocus, submitAutocompletion, closeModal]) |
| const module = require('@root/renderer/data/library/standard-function-blocks') as { | ||
| StandardFunctionBlocks: { pous: Array<{ name: string; type: string }> } | ||
| } | ||
| const isStandard = module.StandardFunctionBlocks.pous.some( | ||
| (pou) => pou.name.toUpperCase() === typeNameUpper && normalizeTypeString(pou.type) === 'functionblock', | ||
| ) | ||
| if (isStandard) return true |
There was a problem hiding this comment.
Using require() for a dynamic import in a try-catch block is intended to handle main process vs. renderer process differences, but the type assertion assumes a specific module structure. If the module structure changes, this will fail silently. Consider adding runtime validation of the required properties before using them.
| const module = require('@root/renderer/data/library/standard-function-blocks') as { | |
| StandardFunctionBlocks: { pous: Array<{ name: string; type: string }> } | |
| } | |
| const isStandard = module.StandardFunctionBlocks.pous.some( | |
| (pou) => pou.name.toUpperCase() === typeNameUpper && normalizeTypeString(pou.type) === 'functionblock', | |
| ) | |
| if (isStandard) return true | |
| const module: unknown = require('@root/renderer/data/library/standard-function-blocks') | |
| // Runtime validation of module structure before use | |
| if ( | |
| module && | |
| typeof module === 'object' && | |
| 'StandardFunctionBlocks' in module && | |
| module.StandardFunctionBlocks && | |
| typeof module.StandardFunctionBlocks === 'object' && | |
| 'pous' in module.StandardFunctionBlocks && | |
| Array.isArray(module.StandardFunctionBlocks.pous) | |
| ) { | |
| const isStandard = module.StandardFunctionBlocks.pous.some( | |
| (pou) => | |
| pou && | |
| typeof pou === 'object' && | |
| typeof pou.name === 'string' && | |
| typeof pou.type === 'string' && | |
| pou.name.toUpperCase() === typeNameUpper && | |
| normalizeTypeString(pou.type) === 'functionblock', | |
| ) | |
| if (isStandard) return true | |
| } |
Move closeModal and submitAutocompletion declarations before useImperativeHandle and add them to the dependency array to ensure the triggerSubmit closure always captures the latest function references. Addresses Copilot review feedback on PR #545. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
debug-variable-finder.tsdebug-parser.ts) and tree traversal utilities (debug-tree-traversal.ts)Test plan
🤖 Generated with Claude Code