Skip to content

refactor: make simulator modules web-compatible#644

Closed
dcoutinho1328 wants to merge 3 commits into
developmentfrom
feat/simulator-web-compatible-refactor
Closed

refactor: make simulator modules web-compatible#644
dcoutinho1328 wants to merge 3 commits into
developmentfrom
feat/simulator-web-compatible-refactor

Conversation

@dcoutinho1328
Copy link
Copy Markdown
Collaborator

@dcoutinho1328 dcoutinho1328 commented Mar 2, 2026

Summary

  • Replace all Node.js-specific APIs (Buffer, EventEmitter, fs, SerialPort) with web-standard equivalents (Uint8Array, manual listeners, queueMicrotask) in the 4 shared simulator/modbus files
  • Extract ModbusFunctionCode and ModbusDebugResponse enums into modbus-types.ts to break the dependency on Node.js net.Socket
  • Move Node.js-specific logic (file I/O, real serial port creation, bootloader delay) from shared modules into the IPC handler

Files made web-compatible (zero Node.js APIs)

  • simulator-module.tsloadAndRun() now accepts hex content string directly
  • virtual-serial-port.ts — manual listener arrays instead of EventEmitter
  • modbus-rtu-client.tsUint8Array helpers, SerialPortLike interface
  • modbus-types.ts — pure enums, no imports

Files updated (remain Node.js-specific)

  • modbus-client.ts — imports/re-exports enums from modbus-types.ts
  • ipc/main.ts — absorbs readFile, SerialPort creation, bootloader delay

Result

The 4 web-compatible files can now be copied to openplc-web with zero modifications.

Test plan

  • npm run build passes
  • npm run lint passes
  • Select "OpenPLC Simulator" board → compile → start → debugger connects → variables update → force a variable → stop cleanly
  • Select an Arduino board via RTU → compile → upload → debug → verify externalized SerialPort + bootloader delay works

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Web-compatible simulator module now available for browser environments
    • Shared Modbus PDU utilities for transport-agnostic debug operations
  • Refactor

    • Improved serial port handling with abstraction layer
    • Extracted and centralized Modbus type definitions
    • Streamlined firmware loading process
  • Tests

    • Added comprehensive unit tests for Modbus PDU utilities and byte operations

Replace all Node.js-specific APIs (Buffer, EventEmitter, fs, SerialPort)
with web-standard equivalents (Uint8Array, manual listeners, queueMicrotask)
in the 4 simulator/modbus files so they can be shared with openplc-web
verbatim. Node.js-specific logic (file I/O, real serial port creation,
bootloader delay) moved to the IPC handler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 2, 2026

Walkthrough

A significant refactor to enable web compatibility for the editor simulator by establishing platform-agnostic abstractions: extracting Modbus enums to a shared module, replacing Node.js dependencies (EventEmitter, Buffer, SerialPort) with universal JavaScript APIs (Uint8Array, queueMicrotask, listener arrays), introducing a SerialPortLike interface for dependency injection, and consolidating Modbus PDU building/parsing logic into shared utilities.

Changes

Cohort / File(s) Summary
Shared Modbus PDU Infrastructure
src/shared/modbus/modbus-pdu.ts, src/shared/modbus/modbus-pdu.spec.ts
New transport-agnostic module with Uint8Array utilities, Modbus enums, PDU builders (GetMd5, GetList, SetVariable), response parsers, and comprehensive unit tests covering byte operations, hex encoding, and edge cases.
Modbus Type Extraction
src/main/modules/modbus/modbus-types.ts, src/main/modules/modbus/modbus-client.ts
Extracts ModbusFunctionCode and ModbusDebugResponse enums to dedicated types file; modbus-client.ts re-exports these enums and refactors methods to use shared PDU builders/parsers via new wrapMbap/stripMbap helpers.
Serial Communication Refactoring
src/main/modules/simulator/virtual-serial-port.ts, src/main/modules/modbus/modbus-rtu-client.ts
VirtualSerialPort replaces EventEmitter with manual listener arrays and Uint8Array; ModbusRtuClient introduces SerialPortLike interface for dependency injection, replaces Buffer with Uint8Array utilities, and restructures to use shared PDU builders/parsers.
Simulator Module Updates
src/main/modules/simulator/simulator-module.ts
Changes loadAndRun from async file-based (hexPath: string): Promise<void> to synchronous content-based (hexContent: string): void, removing filesystem I/O.
IPC and Client Integration
src/main/modules/ipc/main.ts, src/main/modules/websocket/websocket-debug-client.ts
IPC handler loads firmware from disk and passes content string to simulator; wraps SerialPort instances for injection into ModbusRtuClient; adds ARDUINO_BOOTLOADER_DELAY_MS constant. WebSocketDebugClient refactored to use shared PDU builders/parsers via new sendCommand helper.
Documentation
docs/simulator-web-compatibility-refactor.md
Documents refactor plan for web compatibility, detailing extraction of portable logic, removal of Node.js dependencies, and alignment of simulator code between editor and web environments.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • JoaoGSP
  • Gustavohsdp

Poem

🐰 A rabbit hops through Modbus lanes,
Converting buffers into Uint8Array chains,
With SerialPorts injected clean,
The web and editor code align—a dream serene!
Platform-agnostic abstractions now take flight,
Making simulators truly portable and light. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'refactor: make simulator modules web-compatible' directly and clearly summarizes the primary objective of the changeset: converting Node.js-specific simulator and modbus modules to be web-compatible.
Description check ✅ Passed The PR description provides a clear summary of changes, identifies affected files, explains the refactoring goals, and includes a test plan. However, it does not follow the repository's template structure with explicit sections for References, DOD checklist completion, and formal issue linkage.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/simulator-web-compatible-refactor

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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.

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: 3

🧹 Nitpick comments (1)
src/main/modules/ipc/main.ts (1)

956-968: Extract RTU port setup and bootloader wait into shared helpers.

The same SerialPort config and bootloader-delay flow appears in three places; centralizing it will reduce divergence and simplify future updates.

Also applies to: 972-974, 1132-1144, 1151-1153, 1280-1292, 1294-1294

🤖 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 956 - 968, Duplicate SerialPort
setup and bootloader-delay logic should be extracted into shared helpers:
createSerialPort(config) that returns a configured SerialPort (using path,
baudRate, autoOpen:false, dataBits:8, stopBits:1, parity:'none') and
createRtuClientWithBootloaderWait(connectionParams) (or initRtuClient) that
constructs the ModbusRtuClient with slaveId/timeout/serialPort, performs the
required bootloader wait/delay, then opens the port/client. Replace the inline
SerialPort/ModbusRtuClient blocks (the SerialPort instantiation and new
ModbusRtuClient({...}) usage) in the repeated locations with calls to these
helpers so all code uses the single shared implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/simulator-web-compatibility-refactor.md`:
- Around line 89-90: Update the docs' behavioral-parity claim to avoid asserting
strict "same callbacks, same order" until the listener-dispatch edge case in
virtual-serial-port.ts is resolved; specifically, revise the sentence in
docs/simulator-web-compatibility-refactor.md to soften it (e.g., "same callbacks
in the same order in typical cases" or include an explicit caveat noting that
callback ordering may differ due to the listener-dispatch edge case in
virtual-serial-port.ts) so the documentation no longer guarantees identical
callback ordering.

In `@src/main/modules/modbus/modbus-rtu-client.ts`:
- Around line 9-15: The SerialPortLike interface uses untyped variadic
signatures for on/once/removeListener; change them to overloaded signatures for
the three typed events ('open' -> no args, 'data' -> (data: Uint8Array), 'error'
-> (err: Error)) and then update VirtualSerialPort's public methods
(VirtualSerialPort.on, .once, .removeListener) to implement those overloads
instead of (...args: any[]), matching the internal listener arrays (the private
openListeners, dataListeners, errorListeners) so the ESLint suppressions can be
removed and callers get strict typings.
- Around line 142-148: connect() currently hangs if the injectedSerialPort is
already open and accumulates event listeners across reconnects; update connect()
(and references to this.serialPort / this.injectedSerialPort) to first check the
port's open state (e.g., this.serialPort?.isOpen) and resolve immediately if
already open, only call open() when not open, and register event handlers with
once('open', ...) and once('error', ...) (or attach handlers and remove them
after resolution/rejection) so listeners aren't permanently added on each
connect attempt; ensure both success and error paths clean up any listeners so
repeated calls to connect() don't create duplicates.

---

Nitpick comments:
In `@src/main/modules/ipc/main.ts`:
- Around line 956-968: Duplicate SerialPort setup and bootloader-delay logic
should be extracted into shared helpers: createSerialPort(config) that returns a
configured SerialPort (using path, baudRate, autoOpen:false, dataBits:8,
stopBits:1, parity:'none') and
createRtuClientWithBootloaderWait(connectionParams) (or initRtuClient) that
constructs the ModbusRtuClient with slaveId/timeout/serialPort, performs the
required bootloader wait/delay, then opens the port/client. Replace the inline
SerialPort/ModbusRtuClient blocks (the SerialPort instantiation and new
ModbusRtuClient({...}) usage) in the repeated locations with calls to these
helpers so all code uses the single shared implementation.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e414315 and 2626178.

📒 Files selected for processing (7)
  • docs/simulator-web-compatibility-refactor.md
  • src/main/modules/ipc/main.ts
  • src/main/modules/modbus/modbus-client.ts
  • src/main/modules/modbus/modbus-rtu-client.ts
  • src/main/modules/modbus/modbus-types.ts
  • src/main/modules/simulator/simulator-module.ts
  • src/main/modules/simulator/virtual-serial-port.ts

Comment on lines +89 to +90
**Impact:** Behavioral parity. Same bytes, same callbacks, same order. File grows from 47 to ~85 lines.

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 | 🟡 Minor

Behavioral-parity claim is currently too strong.

Given the current listener-dispatch edge case in virtual-serial-port.ts, “same callbacks, same order” should be softened until that is fixed.

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

In `@docs/simulator-web-compatibility-refactor.md` around lines 89 - 90, Update
the docs' behavioral-parity claim to avoid asserting strict "same callbacks,
same order" until the listener-dispatch edge case in virtual-serial-port.ts is
resolved; specifically, revise the sentence in
docs/simulator-web-compatibility-refactor.md to soften it (e.g., "same callbacks
in the same order in typical cases" or include an explicit caveat noting that
callback ordering may differ due to the listener-dispatch edge case in
virtual-serial-port.ts) so the documentation no longer guarantees identical
callback ordering.

Comment on lines +9 to +15
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event: string, listener: (...args: any[]) => void): void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
once(event: string, listener: (...args: any[]) => void): void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
removeListener(event: string, listener: (...args: any[]) => void): void
removeAllListeners(event?: string): void
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate the SerialPortLike interface
fd -t f "modbus-rtu-client.ts" --exec cat -n {} \;

Repository: Autonomy-Logic/openplc-editor

Length of output: 19298


🏁 Script executed:

# Search for SerialPortLike interface definition
rg "interface SerialPortLike" -A 20

Repository: Autonomy-Logic/openplc-editor

Length of output: 3597


🏁 Script executed:

# Find all usages of SerialPortLike
rg "SerialPortLike" -B 2 -A 2

Repository: Autonomy-Logic/openplc-editor

Length of output: 3847


🏁 Script executed:

# Find files that implement or reference SerialPortLike
rg "class.*VirtualSerialPort\|implements SerialPortLike\|extends SerialPortLike" -B 2 -A 10

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


🏁 Script executed:

# Search for all files in the modbus module
fd -t f . src/main/modules/modbus/ --type f

Repository: Autonomy-Logic/openplc-editor

Length of output: 198


🏁 Script executed:

# Check if there are any type assertions or uses of the event handlers with 'any'
rg "\.on\(|\.once\(|\.removeListener\(" src/main/modules/modbus/ -B 2 -A 2

Repository: Autonomy-Logic/openplc-editor

Length of output: 3658


🏁 Script executed:

# Search for VirtualSerialPort or other implementations
rg "VirtualSerialPort\|class.*SerialPort" --type ts --type tsx -B 1 -A 10

Repository: Autonomy-Logic/openplc-editor

Length of output: 100


🏁 Script executed:

# Check the ipc/main.ts for SerialPort usage
fd -type f "ipc" "main.ts" | xargs cat -n 2>/dev/null | head -100

Repository: Autonomy-Logic/openplc-editor

Length of output: 243


🏁 Script executed:

# Find all files that might instantiate or use SerialPort
rg "new SerialPort\|SerialPort(" -B 2 -A 2 --type ts --type tsx

Repository: Autonomy-Logic/openplc-editor

Length of output: 177


🏁 Script executed:

# Search for VirtualSerialPort implementation
rg "VirtualSerialPort" -B 2 -A 15

Repository: Autonomy-Logic/openplc-editor

Length of output: 9070


🏁 Script executed:

# Check all TS files that reference SerialPort instantiation
rg "new SerialPort" -B 2 -A 2

Repository: Autonomy-Logic/openplc-editor

Length of output: 2076


🏁 Script executed:

# Look for ipc/main.ts
find src -name "main.ts" -o -name "*ipc*" | head -20

Repository: Autonomy-Logic/openplc-editor

Length of output: 221


🏁 Script executed:

# Get the full VirtualSerialPort implementation
cat -n src/main/modules/simulator/virtual-serial-port.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 3792


🏁 Script executed:

# Check if there are any other implementations of SerialPortLike
rg "implements.*SerialPortLike\|extends.*SerialPortLike" -B 5 -A 20

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


Replace untyped event handlers with overloaded signatures in SerialPortLike interface and update VirtualSerialPort to match.

The interface currently uses (...args: any[]) across on, once, and removeListener methods, forcing downstream code to work with untyped event payloads. Actual usage is limited to three typed events: 'open' (no args), 'data' (Uint8Array), and 'error' (Error).

VirtualSerialPort already enforces these types internally through its private listener arrays (lines 14–16), but the public methods expose any[] to satisfy the current interface. Updating both to use overloaded signatures eliminates the ESLint suppressions and provides strict type safety.

Changes required

src/main/modules/modbus/modbus-rtu-client.ts (interface definition):

 export interface SerialPortLike {
   isOpen: boolean
   open(): void
   close(): void
   write(data: Uint8Array, callback?: (err?: Error | null) => void): void
   flush(callback?: (err?: Error | null) => void): void
-  // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-  on(event: string, listener: (...args: any[]) => void): void
-  // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-  once(event: string, listener: (...args: any[]) => void): void
-  // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-  removeListener(event: string, listener: (...args: any[]) => void): void
+  on(event: 'open', listener: () => void): void
+  on(event: 'data', listener: (data: Uint8Array) => void): void
+  on(event: 'error', listener: (err: Error) => void): void
+  once(event: 'open', listener: () => void): void
+  once(event: 'data', listener: (data: Uint8Array) => void): void
+  once(event: 'error', listener: (err: Error) => void): void
+  removeListener(event: 'open', listener: () => void): void
+  removeListener(event: 'data', listener: (data: Uint8Array) => void): void
+  removeListener(event: 'error', listener: (err: Error) => void): void
   removeAllListeners(event?: string): void
 }

src/main/modules/simulator/virtual-serial-port.ts (implementation): Update lines 59, 66, and 77 to use typed overloads matching the interface.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on(event: string, listener: (...args: any[]) => void): void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
once(event: string, listener: (...args: any[]) => void): void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
removeListener(event: string, listener: (...args: any[]) => void): void
removeAllListeners(event?: string): void
on(event: 'open', listener: () => void): void
on(event: 'data', listener: (data: Uint8Array) => void): void
on(event: 'error', listener: (err: Error) => void): void
once(event: 'open', listener: () => void): void
once(event: 'data', listener: (data: Uint8Array) => void): void
once(event: 'error', listener: (err: Error) => void): void
removeListener(event: 'open', listener: () => void): void
removeListener(event: 'data', listener: (data: Uint8Array) => void): void
removeListener(event: 'error', listener: (err: Error) => void): void
removeAllListeners(event?: string): void
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/modules/modbus/modbus-rtu-client.ts` around lines 9 - 15, The
SerialPortLike interface uses untyped variadic signatures for
on/once/removeListener; change them to overloaded signatures for the three typed
events ('open' -> no args, 'data' -> (data: Uint8Array), 'error' -> (err:
Error)) and then update VirtualSerialPort's public methods
(VirtualSerialPort.on, .once, .removeListener) to implement those overloads
instead of (...args: any[]), matching the internal listener arrays (the private
openListeners, dataListeners, errorListeners) so the ESLint suppressions can be
removed and callers get strict typings.

Comment on lines 142 to 148
async connect(): Promise<void> {
// If a pre-built serial port was provided (e.g. VirtualSerialPort), use it directly
if (this.injectedSerialPort) {
this.serialPort = this.injectedSerialPort
return new Promise((resolve, reject) => {
this.serialPort.on('open', () => resolve())
this.serialPort.on('error', (err: Error) => reject(err))
this.serialPort.open()
})
}

this.serialPort = this.injectedSerialPort
return new Promise((resolve, reject) => {
try {
this.serialPort = new SerialPort({
path: this.port,
baudRate: this.baudRate,
dataBits: 8,
stopBits: 1,
parity: 'none',
})

this.serialPort.on('open', () => {
setTimeout(() => {
resolve()
}, ARDUINO_BOOTLOADER_DELAY_MS)
})

this.serialPort.on('error', (error: unknown) => {
reject(error instanceof Error ? error : new Error(String(error)))
})
} catch (error) {
reject(error instanceof Error ? error : new Error(String(error)))
}
this.serialPort!.on('open', () => resolve())
this.serialPort!.on('error', (err: unknown) => reject(err instanceof Error ? err : new Error(String(err))))
this.serialPort!.open()
})
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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's read the actual file to confirm the current implementation
cat -n src/main/modules/modbus/modbus-rtu-client.ts | sed -n '140,150p'

Repository: Autonomy-Logic/openplc-editor

Length of output: 488


🏁 Script executed:

# Let's also check the full context around the connect method and the serialPort initialization
cat -n src/main/modules/modbus/modbus-rtu-client.ts | head -160 | tail -30

Repository: Autonomy-Logic/openplc-editor

Length of output: 1213


🏁 Script executed:

# Find SerialPort type definitions to understand the API
fd -t f '\.(ts|tsx)$' -x grep -l 'SerialPort\|serialPort' {} \; | head -10

Repository: Autonomy-Logic/openplc-editor

Length of output: 537


🏁 Script executed:

# Search for reconnect or multiple connect() calls
rg 'connect\(\)|reconnect' src/main/modules/modbus/ -A 2 -B 2

Repository: Autonomy-Logic/openplc-editor

Length of output: 1382


🏁 Script executed:

# Find SerialPort type definition and imports
head -50 src/main/modules/modbus/modbus-rtu-client.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 1670


🏁 Script executed:

# Search for reconnect patterns and multiple connect() calls
rg 'this\.connect\(\)' src/main/modules/modbus/ -B 3 -A 3

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


🏁 Script executed:

# Check if listeners are managed elsewhere or if there's a pattern of calling connect() multiple times
rg 'serialPort\?.on\(|serialPort\?.once\(' src/main/modules/modbus/ -B 2 -A 2

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


🏁 Script executed:

# Search more broadly for connect usage patterns
rg '\.connect\(\)' src/ -B 2 -A 2 | head -50

Repository: Autonomy-Logic/openplc-editor

Length of output: 2703


🏁 Script executed:

# Look for any reconnect logic or repeated connect calls
rg 'reconnect|retry.*connect|connect.*retry' src/main/modules/ -i -B 2 -A 2

Repository: Autonomy-Logic/openplc-editor

Length of output: 7558


🏁 Script executed:

# Check if there are multiple listener registrations in the codebase
rg '\.on\(' src/main/modules/modbus/modbus-rtu-client.ts -B 1 -A 1

Repository: Autonomy-Logic/openplc-editor

Length of output: 405


🏁 Script executed:

# Check the full modbus-rtu-client.ts to see if connect is called multiple times or if listeners persist
wc -l src/main/modules/modbus/modbus-rtu-client.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 121


connect() should handle pre-opened ports and clean up listeners.

If an injected port is already open, the method hangs waiting for an open event that never fires. Additionally, persistent on() handlers cause duplicate listeners to accumulate on reconnect attempts since connect() is called multiple times during the application's reconnection logic.

🛠️ Proposed fix
   async connect(): Promise<void> {
     this.serialPort = this.injectedSerialPort
+    if (this.serialPort.isOpen) return
+
     return new Promise((resolve, reject) => {
-      this.serialPort!.on('open', () => resolve())
-      this.serialPort!.on('error', (err: unknown) => reject(err instanceof Error ? err : new Error(String(err))))
-      this.serialPort!.open()
+      const onOpen = () => {
+        cleanup()
+        resolve()
+      }
+      const onError = (err: unknown) => {
+        cleanup()
+        reject(err instanceof Error ? err : new Error(String(err)))
+      }
+      const cleanup = () => {
+        this.serialPort?.removeListener('open', onOpen)
+        this.serialPort?.removeListener('error', onError)
+      }
+
+      this.serialPort!.once('open', onOpen)
+      this.serialPort!.once('error', onError)
+      this.serialPort!.open()
     })
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/modules/modbus/modbus-rtu-client.ts` around lines 142 - 148,
connect() currently hangs if the injectedSerialPort is already open and
accumulates event listeners across reconnects; update connect() (and references
to this.serialPort / this.injectedSerialPort) to first check the port's open
state (e.g., this.serialPort?.isOpen) and resolve immediately if already open,
only call open() when not open, and register event handlers with once('open',
...) and once('error', ...) (or attach handlers and remove them after
resolution/rejection) so listeners aren't permanently added on each connect
attempt; ensure both success and error paths clean up any listeners so repeated
calls to connect() don't create duplicates.

dcoutinho1328 and others added 2 commits March 6, 2026 21:38
…s to use it

Extract PDU building/parsing logic duplicated across ModbusTcpClient,
WebSocketDebugClient, and ModbusRtuClient into a single shared module
at src/shared/modbus/modbus-pdu.ts. Uses Uint8Array for browser
compatibility. All three clients now delegate PDU construction and
response parsing to the shared module, keeping only transport-specific
framing (MBAP header, CRC, hex encoding). Removes modbus-types.ts
(enums moved to shared module). Includes 36 unit tests with
byte-for-byte compatibility checks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.

🧹 Nitpick comments (2)
src/shared/modbus/modbus-pdu.ts (2)

68-75: Consider handling edge cases in hexStringToBytes.

The function assumes valid space-separated hex input. If the input contains invalid hex characters or empty parts, parseInt returns NaN, which becomes 0 when assigned to Uint8Array. This is acceptable for internal use with known-good input but could silently mask malformed data from external sources.

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

In `@src/shared/modbus/modbus-pdu.ts` around lines 68 - 75, The hexStringToBytes
function currently trusts space-separated input and will silently convert
invalid or empty parts into 0; update hexStringToBytes to robustly handle edge
cases by trimming the input, splitting on whitespace and filtering out empty
tokens, then for each token use parseInt(token, 16) and validate that the result
is a finite integer within 0–255; if any token is invalid (NaN or out of range)
throw a descriptive error (or return a defined failure) so malformed external
input is not silently masked — reference the hexStringToBytes function and its
loop over parts when making these changes.

147-166: parseGetMd5Response throws on errors while other parsers return error objects.

This parser uses throw for all error conditions, while parseGetListResponse and parseSetVariableResponse return PduError objects. This inconsistency means callers must handle errors differently depending on which parser they use. Consider aligning the error handling approach across all parsers for consistency.

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

In `@src/shared/modbus/modbus-pdu.ts` around lines 147 - 166, The
parseGetMd5Response function currently throws Errors on bad length, function
code mismatch and non-success status; change it to match the other parsers
(parseGetListResponse, parseSetVariableResponse) by returning PduError objects
instead of throwing. In parseGetMd5Response use readUint8 to read fc and status
and if pdu.length < 2, fc !== ModbusFunctionCode.DEBUG_GET_MD5 or status !==
ModbusDebugResponse.SUCCESS return a PduError containing a descriptive message
(include the status hex like 0x${status.toString(16)} for non-success) and only
return GetMd5Result ({ md5: string }) on success after decoding the md5 bytes.
Ensure signatures/types remain GetMd5Result | PduError (or the project’s
established union) so callers handle errors consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/shared/modbus/modbus-pdu.ts`:
- Around line 68-75: The hexStringToBytes function currently trusts
space-separated input and will silently convert invalid or empty parts into 0;
update hexStringToBytes to robustly handle edge cases by trimming the input,
splitting on whitespace and filtering out empty tokens, then for each token use
parseInt(token, 16) and validate that the result is a finite integer within
0–255; if any token is invalid (NaN or out of range) throw a descriptive error
(or return a defined failure) so malformed external input is not silently masked
— reference the hexStringToBytes function and its loop over parts when making
these changes.
- Around line 147-166: The parseGetMd5Response function currently throws Errors
on bad length, function code mismatch and non-success status; change it to match
the other parsers (parseGetListResponse, parseSetVariableResponse) by returning
PduError objects instead of throwing. In parseGetMd5Response use readUint8 to
read fc and status and if pdu.length < 2, fc !==
ModbusFunctionCode.DEBUG_GET_MD5 or status !== ModbusDebugResponse.SUCCESS
return a PduError containing a descriptive message (include the status hex like
0x${status.toString(16)} for non-success) and only return GetMd5Result ({ md5:
string }) on success after decoding the md5 bytes. Ensure signatures/types
remain GetMd5Result | PduError (or the project’s established union) so callers
handle errors consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: add1dd79-6912-4536-8b50-37b650871115

📥 Commits

Reviewing files that changed from the base of the PR and between 2626178 and d9c9726.

📒 Files selected for processing (5)
  • src/main/modules/modbus/modbus-client.ts
  • src/main/modules/modbus/modbus-rtu-client.ts
  • src/main/modules/websocket/websocket-debug-client.ts
  • src/shared/modbus/modbus-pdu.spec.ts
  • src/shared/modbus/modbus-pdu.ts

@thiagoralves thiagoralves deleted the feat/simulator-web-compatible-refactor branch April 2, 2026 23:18
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