refactor: make simulator modules web-compatible#644
Conversation
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>
WalkthroughA 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). 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.
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
SerialPortconfig 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.
📒 Files selected for processing (7)
docs/simulator-web-compatibility-refactor.mdsrc/main/modules/ipc/main.tssrc/main/modules/modbus/modbus-client.tssrc/main/modules/modbus/modbus-rtu-client.tssrc/main/modules/modbus/modbus-types.tssrc/main/modules/simulator/simulator-module.tssrc/main/modules/simulator/virtual-serial-port.ts
| **Impact:** Behavioral parity. Same bytes, same callbacks, same order. File grows from 47 to ~85 lines. | ||
|
|
There was a problem hiding this comment.
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.
| // 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 |
There was a problem hiding this comment.
🧩 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 20Repository: Autonomy-Logic/openplc-editor
Length of output: 3597
🏁 Script executed:
# Find all usages of SerialPortLike
rg "SerialPortLike" -B 2 -A 2Repository: 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 10Repository: 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 fRepository: 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 2Repository: 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 10Repository: 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 -100Repository: 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 tsxRepository: Autonomy-Logic/openplc-editor
Length of output: 177
🏁 Script executed:
# Search for VirtualSerialPort implementation
rg "VirtualSerialPort" -B 2 -A 15Repository: Autonomy-Logic/openplc-editor
Length of output: 9070
🏁 Script executed:
# Check all TS files that reference SerialPort instantiation
rg "new SerialPort" -B 2 -A 2Repository: Autonomy-Logic/openplc-editor
Length of output: 2076
🏁 Script executed:
# Look for ipc/main.ts
find src -name "main.ts" -o -name "*ipc*" | head -20Repository: Autonomy-Logic/openplc-editor
Length of output: 221
🏁 Script executed:
# Get the full VirtualSerialPort implementation
cat -n src/main/modules/simulator/virtual-serial-port.tsRepository: 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 20Repository: 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.
| // 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.
| 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() | ||
| }) |
There was a problem hiding this comment.
🧩 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 -30Repository: 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 -10Repository: 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 2Repository: 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.tsRepository: 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 3Repository: 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 2Repository: 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 -50Repository: 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 2Repository: 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 1Repository: 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.tsRepository: 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.
…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>
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/shared/modbus/modbus-pdu.ts (2)
68-75: Consider handling edge cases inhexStringToBytes.The function assumes valid space-separated hex input. If the input contains invalid hex characters or empty parts,
parseIntreturnsNaN, which becomes0when assigned toUint8Array. 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:parseGetMd5Responsethrows on errors while other parsers return error objects.This parser uses
throwfor all error conditions, whileparseGetListResponseandparseSetVariableResponsereturnPduErrorobjects. 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
📒 Files selected for processing (5)
src/main/modules/modbus/modbus-client.tssrc/main/modules/modbus/modbus-rtu-client.tssrc/main/modules/websocket/websocket-debug-client.tssrc/shared/modbus/modbus-pdu.spec.tssrc/shared/modbus/modbus-pdu.ts
Summary
Buffer,EventEmitter,fs,SerialPort) with web-standard equivalents (Uint8Array, manual listeners,queueMicrotask) in the 4 shared simulator/modbus filesModbusFunctionCodeandModbusDebugResponseenums intomodbus-types.tsto break the dependency on Node.jsnet.SocketFiles made web-compatible (zero Node.js APIs)
simulator-module.ts—loadAndRun()now accepts hex content string directlyvirtual-serial-port.ts— manual listener arrays instead ofEventEmittermodbus-rtu-client.ts—Uint8Arrayhelpers,SerialPortLikeinterfacemodbus-types.ts— pure enums, no importsFiles updated (remain Node.js-specific)
modbus-client.ts— imports/re-exports enums frommodbus-types.tsipc/main.ts— absorbsreadFile,SerialPortcreation, bootloader delayResult
The 4 web-compatible files can now be copied to
openplc-webwith zero modifications.Test plan
npm run buildpassesnpm run lintpasses🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Refactor
Tests