UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

1,045 lines (887 loc) β€’ 44 kB
# Modbus Connect (Node.js/Web Serial API) Modbus Connect is a cross-platform library for Modbus RTU communication in both Node.js and modern browsers (via the Web Serial API). It enables robust, easy interaction with industrial devices over serial ports. ## Navigating through documentation - [Library Structure](#library-structure) - [Quick Start](#quick-start) - [Summary type data](#type-data) - [Main Classes and Methods](#main-classes-and-methods) - [Transports](#transports) - [Modbus Functions](#modbus-functions) - [Packet building & Parsing](#packet-building-and-parsing) - [Diagnostics & Error Handling](#diagnostics-and-error-handling) - [Logger](#logger) - [Utilities](#utilities) - [CRC](#crc) - [Polling Manager](#polling-manager) - [Slave Emulator](#slave-emulator) - [Tips for use](#tips-for-use) - [Expansion](#expansion) - [CHANGELOG](#changelog) --- ## 1. πŸ“ <span id="library-structure">Library Structure</span> - **client.js** β€” Main ModbusClient class for Modbus RTU devices. - **constants.js** β€” Protocol constants (function codes, errors, etc.). - **errors.js** β€” Error classes for robust exception handling. - **function-codes/** β€” PDU implementations for all Modbus functions (register/bit read/write, special functions). - **logger.js** β€” Event logging utilities. - **packet-builder.js** β€” ADU packet construction/parsing (with CRC). - **transport/** β€” Transport adapters (Node.js SerialPort, Web Serial API), auto-detection helpers. - **utils/** β€” Utilities: CRC, diagnostics, and helpers. <br><br> ## 2. πŸš€ <span id="quick-start">Quick Start</span> ### Reading Holding Registers ### Node.js Example ```js const ModbusClient = require('modbus-connect/client'); const { createTransport } = require('modbus-connect/transport') const transport = await createTransport('node', { port: 'COM3', baudRate: 9600, parity: 'none', dataBits: 8, stopBits: 1 }) const client = new ModbusClient(transport, 1) async function main() { await client.connect() const regs = await client.readHoldingRegisters(0, 2) console.log('Read registers:', regs) await client.disconnect() } main() ``` **Run:** ```bash node ./test-node.js ``` ### Browser Example (Web Serial API) **index.html** ```html <body> <button id="test">Select port</button> <button id="connect">Connect and read 2 registers</button> <script type="module" src="test-web.js"></script> </body> ``` **test-web.js** ```js const ModbusClient = require('modbus-connect/client'); const { createTransport } = require('modbus-connect/transport'); let port async function getSerialPort(){ return await navigator.serial.requestPort() } async function selectPort(){ try { port = await getSerialPort() console.log(port) } catch (err) { console.error(err) } } async function connectAndRead(){ try { const transport = await createTransport('web', { port, baudRate: 9600, parity: 'none', dataBits: 8, stopBits: 1 }) const client = new ModbusClient(transport, 1) await client.connect() const regs = await client.readHoldingRegisters(0, 2) console.log('Read registers:', regs) await client.disconnect() } catch (err) { console.error(err) } } document.querySelector('#test').onclick = selectPort document.querySelector('#connect').onclick = connectAndRead ``` ### 🧾 <span id="type-data">Summary type data</span> | Type | Size (regs) | DataView Method | Endian / Swap | Notes | | --------------- | ----------- | -------------------- | --------------------- | ---------------------------------------------- | | `uint16` | 1 | `getUint16` | Big Endian | No changes | | `int16` | 1 | `getInt16` | Big Endian | | | `uint32` | 2 | `getUint32` | Big Endian | Standard 32-bit read | | `int32` | 2 | `getInt32` | Big Endian | | | `float` | 2 | `getFloat32` | Big Endian | IEEE 754 single precision float | | `uint32_le` | 2 | `getUint32` | Little Endian | | | `int32_le` | 2 | `getInt32` | Little Endian | | | `float_le` | 2 | `getFloat32` | Little Endian | | | `uint32_sw` | 2 | `getUint32` | Word Swap | Swap words (e.g., 0xAABBCCDD β†’ 0xCCDDAABB) | | `int32_sw` | 2 | `getInt32` | Word Swap | | | `float_sw` | 2 | `getFloat32` | Word Swap | | | `uint32_sb` | 2 | `getUint32` | Byte Swap | Swap bytes (e.g., 0xAABBCCDD β†’ 0xBBAADDCC) | | `int32_sb` | 2 | `getInt32` | Byte Swap | | | `float_sb` | 2 | `getFloat32` | Byte Swap | | | `uint32_sbw` | 2 | `getUint32` | Byte + Word Swap | Swap bytes and words (0xAABBCCDD β†’ 0xDDCCBBAA) | | `int32_sbw` | 2 | `getInt32` | Byte + Word Swap | | | `float_sbw` | 2 | `getFloat32` | Byte + Word Swap | | | `uint32_le_sw` | 2 | `getUint32` | LE + Word Swap | Little Endian with Word Swap | | `int32_le_sw` | 2 | `getInt32` | LE + Word Swap | | | `float_le_sw` | 2 | `getFloat32` | LE + Word Swap | | | `uint32_le_sb` | 2 | `getUint32` | LE + Byte Swap | Little Endian with Byte Swap | | `int32_le_sb` | 2 | `getInt32` | LE + Byte Swap | | | `float_le_sb` | 2 | `getFloat32` | LE + Byte Swap | | | `uint32_le_sbw` | 2 | `getUint32` | LE + Byte + Word Swap | Little Endian with Byte + Word Swap | | `int32_le_sbw` | 2 | `getInt32` | LE + Byte + Word Swap | | | `float_le_sbw` | 2 | `getFloat32` | LE + Byte + Word Swap | | | `uint64` | 4 | `getUint32` + BigInt | Big Endian | Combined BigInt from high and low parts | | `int64` | 4 | `getUint32` + BigInt | Big Endian | Signed BigInt | | `double` | 4 | `getFloat64` | Big Endian | IEEE 754 double precision float | | `uint64_le` | 4 | `getUint32` + BigInt | Little Endian | | | `int64_le` | 4 | `getUint32` + BigInt | Little Endian | | | `double_le` | 4 | `getFloat64` | Little Endian | | | `hex` | 1+ | β€” | β€” | Returns array of HEX strings per register | | `string` | 1+ | β€” | Big Endian (Hi β†’ Lo) | Each 16-bit register β†’ 2 ASCII chars | | `bool` | 1+ | β€” | β€” | 0 β†’ false, nonzero β†’ true | | `binary` | 1+ | β€” | β€” | Each register converted to 16 boolean bits | | `bcd` | 1+ | β€” | β€” | BCD decoding from registers | ### πŸ“Œ Expanded Usage Examples: | Example usage | Description | | ------------------- | --------------------------------------------------------------------------- | | `type: 'uint16'` | Reads registers as unsigned 16-bit integers (default no byte swapping) | | `type: 'int16'` | Reads registers as signed 16-bit integers | | `type: 'uint32'` | Reads every 2 registers as unsigned 32-bit big-endian integers | | `type: 'int32'` | Reads every 2 registers as signed 32-bit big-endian integers | | `type: 'float'` | Reads every 2 registers as 32-bit IEEE 754 floats (big-endian) | | `type: 'uint32_le'` | Reads every 2 registers as unsigned 32-bit little-endian integers | | `type: 'int32_le'` | Reads every 2 registers as signed 32-bit little-endian integers | | `type: 'float_le'` | Reads every 2 registers as 32-bit IEEE 754 floats (little-endian) | | `type: 'uint32_sw'` | Reads every 2 registers as unsigned 32-bit with word swap | | `type: 'int32_sb'` | Reads every 2 registers as signed 32-bit with byte swap | | `type: 'float_sbw'` | Reads every 2 registers as float with byte+word swap | | `type: 'hex'` | Returns an array of hex strings, e.g., `["0010", "FF0A"]` | | `type: 'string'` | Converts registers to ASCII string (each register = 2 chars) | | `type: 'bool'` | Returns an array of booleans, 0 = false, otherwise true | | `type: 'binary'` | Returns array of 16-bit boolean arrays per register (each bit separately) | | `type: 'bcd'` | Decodes BCD-encoded numbers from registers, e.g., `0x1234` β†’ `1234` | | `type: 'uint64'` | Reads 4 registers as a combined unsigned 64-bit integer (BigInt) | | `type: 'int64_le'` | Reads 4 registers as signed 64-bit little-endian integer (BigInt) | | `type: 'double'` | Reads 4 registers as 64-bit IEEE 754 double precision float (big-endian) | | `type: 'double_le'` | Reads 4 registers as 64-bit IEEE 754 double precision float (little-endian) | <br><br> ## 3. πŸ—οΈ <span id="main-classes-and-methods">Main Classes and Methods</span> ### ModbusClient **Constructor:** ```js const client = new ModbusClient(transport, slaveId = 1, options = {}) ``` - `transport` β€” transport object (see below) - `slaveId` β€” device address (1..247) - `options` β€” `{ timeout, retryCount, retryDelay }` **Methods (basic):** - `connect()` / `disconnect()` β€” open/close connection - `readHoldingRegisters(startAddress, quantity, timeout?)` β€” read holding registers - `readInputRegisters(startAddress, quantity, timeout?)` β€” read input registers - `writeSingleRegister(address, value, timeout?)` β€” write a single register - `writeMultipleRegisters(startAddress, values, timeout?)` β€” write multiple registers - `readCoils(startAddress, quantity, timeout?)` β€” read discrete outputs (coils) - `readDiscreteInputs(startAddress, quantity, timeout?)` β€” read discrete inputs - `writeSingleCoil(address, value, timeout?)` β€” write a single coil - `writeMultipleCoils(startAddress, values, timeout?)` β€” write multiple coils - `reportSlaveId(timeout?)` β€” get device identifier - `readDeviceIdentification(slaveId, categoryId, objectId)` - read device identification - `getDiagnostics()` β€” get communication statistics - `resetDiagnostics()` β€” reset statistics <br> **Methods for SGM-130** - `writeDeviceComment(channel, comment, timeout?)` - write comment to device channel (**SGM-130 only**) - `readFileLength(fileName)` - get archive file length (**SGM-130 only**) - `openFile(fileName)` - open archive file (**SGM-130 only**) - `closeFile()` - close archive file (**SGM-130 only**) - `restartController()` - restart controller (**SGM-130 only**) - `getControllerTime(options = {})` - get current controller date/time (**SGM-130 only**) - `readDeviceComment(channel, timeout?)` β€” get device comment (**SGM-130 only**) - `setControllerTime(time, options = {})` - set current controller date/time (**SGM-130 only**) <br><br> ## 4. πŸ”Œ <span id="transports">Transports</span> In order to initialize the transport, it is necessary to determine the type of the transport itself - `node` or `web` depending on the environment in which the code is executed - Node.js: ```js const transport = createTransport('node', { port, baudRate, parity, stopBits, dataBits }) ``` - Node TCP/IP: ```js const transport = createTransport('node-tcp', { host, port, readTimeout = 100, writeTimeout = 100, reconnectInterval = 3000, maxReconnectAttempts = Infinity }) ``` - Web (browser): ```js const transport = createTransport('web', { port, baudRate, parity, stopBits, dataBits }) ``` - Web TCP (browser): ```js const transport = createTransport('web-tcp', { host, port, readTimeout = 100, writeTimeout = 100, reconnectInterval = 3000, maxReconnectAttempts = Infinity }) ``` To set the `read/write` speed parameters, it is necessary to specify parameters such as `writeTimeout` and `readTimeout` during initialization. Example: ```js const transport = await createTransport('node', { port: 'COM3', baudRate: 9600, parity: 'none', dataBits: 8, stopBits: 1, writeTimeout: 500, // your value readTimeout: 500 // your value }) ``` > If you do not specify values ​​for `readTimeout/writeTimeout` during initialization, the default parameter will be used - 1000 ms for both values <br><br> ## 5. 🧩 <span id="modbus-functions">Modbus Functions</span> The `function-codes/` directory contains all standard and custom Modbus PDU builders/parsers. **Standard Functions** | HEX | Name | |:---:|------| | 0x03 | Read Holding Registers | | 0x04 | Read Input Registers | | 0x10 | Write Multiple Registers | | 0x06 | Write Single Register | | 0x01 | Read Coils | | 0x02 | Read Discrete Inputs | | 0x05 | Write Single Coil | | 0x0F | Write multiple Coils | | 0x2B | Read Device Identification | | 0x11 | Report Slave ID | **Custom Functions (SGM-130)** | HEX | Name | |:---:|------| | 0x14 | Read Device Comment | | 0x15 | Write Device Comment | | 0x52 | Read File Length | | 0x55 | Open File | | 0x57 | Close File | | 0x5C | Restart Controller | | 0x6E | Get Controller Time | | 0x6F | Set Controller Time | ### Each file exports two functions: - `build...Request(...)` β€” builds a PDU request - `parse...Response(pdu)` β€” parses the response ### Example: manual PDU building ```js const { buildReadHoldingRegistersRequest } = require('./function-codes/read-holding-registers.js'); const pdu = buildReadHoldingRegistersRequest(0, 2); ``` <br><br> ## 6. πŸ“¦ <span id="packet-building-and-parsing">Packet Building & Parsing</span> **packet-builder.js** - **buildPacket(slaveAddress, pdu)** β€” Adds slaveId and CRC, returns ADU - **parsePacket(packet)** β€” Verifies CRC, returns { slaveAddress, pdu } **Example:** ```js const { buildPacket, parsePacket } = require('./packet-builder.js'); const adu = buildPacket(1, pdu); const { slaveAddress, pdu: respPdu } = parsePacket(adu); ``` <br><br> ## 7. πŸ“Š <span id="diagnostics-and-error-handling">Diagnostics & Error Handling</span> - **diagnostics.js** β€” Diagnostics class collects stats (requests, errors, response times, etc.) - **errors.js** β€” Error classes: - `ModbusTimeoutError` - `ModbusCRCError` - `ModbusResponseError` - `ModbusTooManyEmptyReadsError` - `ModbusExceptionError` **Diagnostics example:** ```js const stats = client.getDiagnostics(); console.log(stats); client.resetDiagnostics(); ``` <br><br> ## 8. πŸ›  <span id="logger">Logger</span> `logger.js` is designed for formatted logging in the console with support for: - log levels (debug, info, warn, error), - colors, - nested groups, - global context, - output buffering, - categorical loggers, - timings (responseTime). ### πŸ“¦ Import ```js const logger = require('modbus-connect/logger'); ``` ### πŸ”Š Basic logging ```js logger.debug('Debug message'); logger.info('Informational message'); logger.warn('Warning message'); logger.error('Error message'); ``` **You can also pass any data:** ```js logger.info('Received registers:', [123, 456]); ``` ### πŸ“¦ Logging with context You can pass a context object as the last argument (for example, to display responseTime, transport, etc.): ```js logger.info('Response received', { responseTime: 42 }); ``` ### πŸ– Managing log levels **Setting the global log level:** ```js logger.setLevel('debug'); // 'debug' | 'info' | 'warn' | 'error' ``` **Checking:** ```js logger.getLevel(); // => 'debug' logger.isEnabled(); // => true ``` ### 🚫 Enabling / disabling the logger ```js logger.disable(); // disable logging logger.enable(); // enable ``` ### 🎨 Colors ```js logger.disableColors(); // disable colored output ``` ### 🧡 Log groups ```js logger.group(); logger.info('Start of group'); logger.group(); logger.debug('Nested group'); logger.groupEnd(); logger.groupEnd(); ``` ### 🌐 Global context ```js logger.setGlobalContext({ transport: 'NODE' }); ``` > The logger can automatically detect the execution environment, i.e. Node or WEB Or adding new fields: ```js logger.addGlobalContext({ device: 'SGM130' }); ``` ### πŸ”„ Output buffering ```js logger.setBuffering(true); // enable (default) logger.setBuffering(false); // immediate output ``` > The buffer is automatically reset every 300ms or when logs accumulate. ### πŸ“ Categorical loggers You can create a named logger, which automatically adds context.logger = name. ```js const transportLog = logger.createLogger('transport'); transportLog.info('Connected'); // adds [transport] to context transportLog.setLevel('debug'); // the level is configured separately ``` ### πŸ’₯ Immediate error output Errors are always output immediately, even with buffering enabled: ```js logger.error('Something went wrong!'); ``` ### πŸ§ͺ Usage example ```js const logger = require('modbus-connect/logger'); logger.setLevel('debug'); logger.setGlobalContext({ transport: 'NODE' }); logger.group(); logger.info('Starting Modbus session'); const comm = logger.createLogger('comm'); comm.debug('Opening port COM3'); logger.info('Response received', { responseTime: 48 }); logger.groupEnd(); ``` The output will be something like this: ```bash [2025-05-29 03:13:27.192] [modbus] [NODE] [INFO] Serial port COM3 opened [2025-05-29 03:13:27.192] [modbus] [NODE] [INFO] Serial port COM3 opened [2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] Transport connected [2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] Transport connected [2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] [responseTime: 46 ms] Response received [2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] [ 4866, 25629 ] [2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] Serial port COM3 closed [2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] Transport disconnected ``` ### πŸ“Œ Tips - Use named loggers for modules (`createLogger('transport')`). - Add `responseTime` to context if you need to measure response time. - Group logs for sequential operations. - In CLI or Web projects, disable colors if necessary. <br><br> ## 9. πŸ›  <span id="utilities">Utilities</span> - **crc.js** β€” CRC implementations (Modbus, CCITT, 1-wire, DVB-S2, XModem, etc.) - **utils.js** β€” Uint8Array helpers, number conversions, hex string utilities **CRC usage example:** ```js const { crc16modbus } = require('./utils/crc.js'); const crc = crc16modbus(Uint8Array.from([1, 3]); consoole.log(crc) ``` <br><br> ## 10. <span id="crc">CRC</span> **All types of CRC calculations** | Name | Polynomial | Initial Value (init) | Reflection (RefIn/RefOut) | Final XOR | CRC Size | Result Byte Order | Notes | |------------------|-------------|---------------------------|---------------------------|-------------------|------------|------------------------|-----------------------------------| | **crc16Modbus** | 0x8005 (reflected 0xA001) | 0xFFFF | Yes (reflected) | None | 16 bits | Little-endian | Standard Modbus RTU CRC16 | | **crc16CcittFalse** | 0x1021 | 0xFFFF | No | None | 16 bits | Big-endian | CRC-16-CCITT-FALSE | | **crc32** | 0x04C11DB7 | 0xFFFFFFFF | Yes (reflected) | XOR 0xFFFFFFFF | 32 bits | Little-endian | Standard CRC32 | | **crc8** | 0x07 | 0x00 | No | None | 8 bits | 1 byte | CRC-8 without reflection | | **crc1** | 0x01 | 0x00 | No | None | 1 bit | 1 bit | Simple CRC-1 | | **crc8_1wire** | 0x31 (reflected 0x8C) | 0x00 | Yes (reflected) | None | 8 bits | 1 byte | CRC-8 for 1-Wire protocol | | **crc8_dvbs2** | 0xD5 | 0x00 | No | None | 8 bits | 1 byte | CRC-8 DVB-S2 | | **crc16_kermit** | 0x1021 (reflected 0x8408) | 0x0000 | Yes (reflected) | None | 16 bits | Little-endian | CRC-16 Kermit | | **crc16_xmodem** | 0x1021 | 0x0000 | No | None | 16 bits | Big-endian | CRC-16 XModem | | **crc24** | 0x864CFB | 0xB704CE | No | None | 24 bits | Big-endian (3 bytes) | CRC-24 (Bluetooth, OpenPGP) | | **crc32mpeg** | 0x04C11DB7 | 0xFFFFFFFF | No | None | 32 bits | Big-endian | CRC-32 MPEG-2 | | **crcjam** | 0x04C11DB7 | 0xFFFFFFFF | Yes (reflected) | None | 32 bits | Little-endian | CRC-32 JAM (no final XOR) | --- To use one of these options when initializing **ModbusClient**, see the example below: ```js const client = new ModbusClient( transport, // your initialize transport 0, // slave id { crcAlgorithm: 'crc16Modbus' // Selecting the type of CRC calculation } ) ``` > If you do not specify the type of CRC calculation during initialization, the default option is used - `crc16Modbus` <br><br> ## 11. πŸŒ€ <span id="polling-manager">Polling Manager</span> `PollingManager` is a powerful utility for managing periodic asynchronous tasks. It supports retries, backoff strategies, timeouts, dynamic intervals, and lifecycle callbacks β€” ideal for polling Modbus or other real-time data sources. ### πŸ“¦ Key Features - Async execution of single or multiple functions - Automatic retries with per-attempt backoff delay - Per-function timeout handling - Lifecycle control: start, stop, pause, resume, restart - Lifecycle hooks: `onStart`, `onStop`, `onData`, `onError` - Dynamically adjustable polling interval per task - Full task state inspection (running, paused, etc.) - Clean-up and removal of tasks ### Usage example ```js const ModbusClient = require('modbus-connect/client'); const logger = require('modbus-connect/logger'); const PollingManager = require('modbus-connect/polling-manager'); const log = logger.createLogger('main') const client = new ModbusClient(transport); // your initialized client const poll = new PollingManager(); poll.addTask({ id: 'modbus-loop', interval: 1000, immediate: true, fn: [ async () => await client.readHoldingRegisters(0, 2), async () => await client.readHoldingRegisters(2, 2), ], onData: (hold1, hold2) => { log.info('Registers:', hold1, hold2 ); }, onError: (error, index, attempt) => { console.warn(`Error in fn[${index}], attempt ${attempt}: ${error.message}`); }, onStart: () => console.log('Polling started'), onStop: () => console.log('Polling stopped'), maxRetries: 3, backoffDelay: 300, taskTimeout: 2000 }); ``` ### 🧩 Task Interface **poll.addTask(options)** Registers and starts a new polling task. ```ts poll.addTask({ id: string, // Unique task ID interval: number, // Polling interval (ms) fn: Function | Function[], // One or multiple async functions onData?: Function, // Called with results array on success onError?: Function, // Called on error: (error, fnIndex, attempt) onStart?: Function, // Called when the task starts onStop?: Function, // Called when the task stops immediate?: boolean, // Run immediately on add maxRetries?: number, // Retry attempts per function backoffDelay?: number, // Retry delay base (ms) taskTimeout?: number // Timeout per function call (ms) }); ``` ### πŸ§ͺ Additional examples **⏸ Pause and resume a task** ```js poll.pauseTask('modbus-loop'); setTimeout(() => { poll.resumeTask('modbus-loop'); }, 5000); ``` **πŸ” Restart a task** ```js poll.restartTask('modbus-loop'); ``` **πŸ“‘ Add a task with a single function and custom interval** ```js poll.addTask({ id: 'heartbeat', interval: 5000, fn: async () => { const status = await client.reportSlaveId(); return status; }, onData: ([result]) => console.log('Device is alive:', result), maxRetries: 2, backoffDelay: 1000 }); ``` **🧠 Dynamically update the polling interval** ```js poll.setTaskInterval('modbus-loop', 2000); // now polls every 2 seconds ``` **❌ Remove a task** ```js poll.removeTask('heartbeat'); ``` ### πŸ›  Task management methods | Method | Description | |-------------|--------------| | addTask(config) | Add and start a new polling task | | startTask(id) | Start a task | | stopTask(id) | Stop a task | | pauseTask(id) | Pause execution | | resumeTask(id) | Resume execution | | restartTask(id) | Restart a task | | removeTask(id) | Remove a task | | updateTask(id, opts) | Update a task(removes and recreates) | | setTaskInterval(id, ms) | Dynamically update the task's polling interval | ### πŸ“Š Status and Checks | Method | Description | |-------------|--------------| | isTaskRunning(id) | true if the task is running | | isTaskPaused(id) | true if the task is paused | | getTaskState(id) | Detailed info: { stopped, paused, loopRunning, interval } | | hasTask(id) | Checks if task exists | | getTaskIds() | List of all task IDs | ### 🧼 Cleanup ```js polling.clearAll(); // Stops and removes all registered tasks ``` ### Tips for use Polling Manager In order to optimize the use of polling manager, it is necessary to use Page Visibility API **Example** ```js // --- Add automatic pause/resume when tab visibility changes --- document.addEventListener('visibilitychange', () => { if (document.hidden) { console.log('The user switched to another tab or minimized the browser'); if (poll.isTaskRunning(taskId) && !poll.isTaskPaused(taskId)) { poll.pauseTask(taskId); console.log('Polling task is automatically paused'); } } else { console.log('The user returned to the tab'); if (poll.isTaskPaused(taskId)) { poll.resumeTask(taskId); console.log('Polling task automatically resumed'); } } }); ``` <br><br> ## 12. <span id="slave-emulator">πŸ“˜ SlaveEmulator</span> ### πŸ“¦ Import ```js const SlaveEmulator = require('modbus-connect/slave-emulator') ``` ### πŸ— Creating an Instance ```js const emulator = new SlaveEmulator(1) // 1 β€” Modbus slave address ``` ### πŸ”Œ Connecting and Disconnecting ```js await emulator.connect() // ...interact with emulator... await emulator.disconnect() ``` ### βš™οΈ Initializing Registers **Method:** `addRegisters(config)` Use this to initialize register and bit values: ```js emulator.addRegisters({ holding: [ { start: 0, value: 123 }, { start: 1, value: 456 } ], input: [ { start: 0, value: 999 } ], coils: [ { start: 0, value: true } ], discrete: [ { start: 0, value: false } ] }) ``` ### πŸ”„ Direct Read/Write (No RTU) **Holding Registers** ```js emulator.setHoldingRegister(0, 321) const holding = emulator.readHoldingRegisters(0, 2) console.log(holding) // [321, 456] ``` **Input Registers** ```js emulator.setInputRegister(1, 555) const input = emulator.readInputRegisters(1, 1) console.log(input) // [555] ``` **Coils (boolean flags)** ```js emulator.setCoil(2, true) const coils = emulator.readCoils(2, 1) console.log(coils) // [true] ``` **Discrete Inputs** ```js emulator.setDiscreteInput(3, true) const inputs = emulator.readDiscreteInputs(3, 1) console.log(inputs) // [true] ``` > Data is returned in `uint16` only. ### 🚫 Exceptions You can set exceptions for specific operations: ```js emulator.setException(0x03, 1, 0x02) // Error for reading holding register 1 try { emulator.readHoldingRegisters(1, 1) } catch (err) { console.log(err.message) // Exception response for function 0x03 with code 0x02 } ``` ### πŸ§ͺ Handling RTU Requests **Input:** `Uint8Array` **with Modbus RTU request** **Output:** `Uint8Array` **with response** Example: ```js const request = new Uint8Array([0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B]) // Read Holding [0,2] const response = emulator.handleRequest(request) console.log(Buffer.from(response).toString('hex')) // Example output: 010304007b01c8crc_lo crc_hi ``` ### 🧾 Full Example Script ```js const SlaveEmulator = require('modbus-connect/slave-emulator') const logger = require('modbus-connect/logger') const log = logger.createLogger('main') const emulator = new SlaveEmulator(1) await emulator.connect() emulator.addRegisters({ holding: [{ start: 0, value: 123 }, { start: 1, value: 456 }], input: [{ start: 0, value: 999 }], coils: [{ start: 0, value: true }], discrete: [{ start: 0, value: false }] }) log.warn('Holding:', emulator.readHoldingRegisters(0, 2)) // [123, 456] log.warn('Input:', emulator.readInputRegisters(0, 1)) // [999] log.warn('Coils:', emulator.readCoils(0, 1)) // [true] log.warn('Discrete:', emulator.readDiscreteInputs(0, 1)) // [false] await emulator.disconnect() ``` ### 🧰 Additional Methods | Method | Description | | -------------------------------------- | -------------------------- | | `setHoldingRegister(addr, val)` | Set holding register | | `setInputRegister(addr, val)` | Set input register | | `setCoil(addr, bool)` | Set coil bit | | `setDiscreteInput(addr, bool)` | Set discrete input bit | | `readHoldingRegisters(start, qty)` | Read holding registers | | `readInputRegisters(start, qty)` | Read input registers | | `readCoils(start, qty)` | Read coil bits | | `readDiscreteInputs(start, qty)` | Read discrete input bits | | `setException(funcCode, addr, exCode)` | Register an exception | | `handleRequest(buffer)` | Process Modbus RTU request | ### βœ… Supported Modbus RTU Function Codes | Function Code | Description | | ------------- | ------------------------ | | `0x01` | Read Coils | | `0x02` | Read Discrete Inputs | | `0x03` | Read Holding Registers | | `0x04` | Read Input Registers | | `0x05` | Write Single Coil | | `0x06` | Write Single Register | | `0x0F` | Write Multiple Coils | | `0x10` | Write Multiple Registers | ### Slave emulator With PollingManager Also `SlaveEmulator` can work in conjunction with [`PollingManager`](#polling-manager). Example usage: ```js const SlaveEmulator = require('modbus-connect/slave-emulator') const PollingManager = require('modbus-connect/polling-manager') const logger = require('modbus-connect/logger') const log = logger.createLogger('main') const poll = new PollingManager() const emulator = new SlaveEmulator(1) await emulator.connect() // Initialize emulator register values emulator.addRegisters({ holding: [ { start: 0, value: 123 }, { start: 1, value: 456 } ], input: [ { start: 0, value: 999 } ], coils: [ { start: 0, value: true } ], discrete: [ { start: 0, value: false } ] }) // Periodically change the value in holding register 0 between 30 and 65 emulator.infinityChange({ typeRegister: 'Holding', register: 0, range: [30, 65], interval: 500 // ms }) // Add a polling task that reads data from the emulator every 1 second poll.addTask({ id: 'modbus-loop', interval: 1000, immediate: true, fn: [ async () => emulator.readHoldingRegisters(0, 2), async () => emulator.readHoldingRegisters(2, 2), ], onData: (hold1, hold2) => { log.info('Registers:', hold1, hold2) }, onError: (error, index, attempt) => { console.warn(`Error in fn[${index}], attempt ${attempt}: ${error.message}`) }, onStart: () => console.log('Polling started'), onStop: () => console.log('Polling stopped'), maxRetries: 3, backoffDelay: 300, taskTimeout: 2000 }) await emulator.disconnect() ``` **Output:** ```bash Registers added: { holding: [ { start: 0, value: 123 }, { start: 1, value: 456 } ], input: [ { start: 0, value: 999 } ], coils: [ { start: 0, value: true } ], discrete: [ { start: 0, value: false } ] } Polling started [2025-06-02 04:35:59.016] [modbus] [UNKNOWN] [INFO] Connecting to emulator... [2025-06-02 04:35:59.017] [modbus] [UNKNOWN] [INFO] Connected [2025-06-02 04:35:59.017] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=0, quantity=2 [2025-06-02 04:35:59.017] [modbus] [UNKNOWN] [INFO] Disconnecting from emulator... [2025-06-02 04:35:59.017] [modbus] [UNKNOWN] [INFO] Disconnected [2025-06-02 04:35:59.018] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=2, quantity=2 [2025-06-02 04:35:59.018] [modbus] [UNKNOWN] [INFO] Registers: [ 123, 456 ] [ 0, 0 ] [2025-06-02 04:36:00.034] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=0, quantity=2 [2025-06-02 04:36:00.035] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=2, quantity=2 [2025-06-02 04:36:00.035] [modbus] [UNKNOWN] [INFO] Registers: [ 59, 456 ] [ 0, 0 ] [2025-06-02 04:36:01.032] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=0, quantity=2 [2025-06-02 04:36:01.033] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=2, quantity=2 [2025-06-02 04:36:01.034] [modbus] [UNKNOWN] [INFO] Registers: [ 36, 456 ] [ 0, 0 ] ``` - The `infinityChange()` method simulates a fluctuating register by assigning it a random value from the defined range every X milliseconds. - All polling `fn` handlers must return Promises, even if the underlying methods are synchronous. Wrap calls in `async () => {}` as shown above. - The polling manager supports retries, delays, hooks for error/success callbacks, and timeout protection. ### ❗ Important Note This emulator does not use real or virtual COM ports. It is fully virtual and designed for testing Modbus RTU logic without any physical device. <br><br> ## πŸ“Ž Notes - Each `fn[i]` is handled independently; one failing does not stop others. - `onData(results)` is called only if all functions succeed, with `results[i]` matching `fn[i]`. - Retries (`maxRetries`) are applied per function, with delay `delay = backoffDelay Γ— attempt`. - `taskTimeout` applies individually to each function call. - `onError(error, index, attempt)` fires on each failed attempt. - Use `getTaskState(id)` for detailed insight into task lifecycle. - Suitable for advanced diagnostic loops, sensor polling, background watchdogs, or telemetry logging. <br><br> ## <span id="tips-for-use">Tips for use</span> - For Node.js, the `serialport` package is required (`npm install serialport`). - For browser usage, HTTPS and Web Serial API support are required (**Google Chrome** ΠΈ **Edge**). - Use auto-detection methods to find port parameters and slaveId - Use the client's `getDiagnostics()` and `resetDiagnostics()` methods for diagnostics. <br><br> ## <span id="expansion">Expansion</span> You can add your own Modbus functions by implementing a pair of `build...Request` and `parse...Response` functions in the `function-codes/` folder, then importing them into the ModbusClient in `modbus/client.js` <br><br> ## <span id="changelog">CHANGELOG</span> ### **1.6.6 (2025-07-04)** - Modbus functions have been optimized, now `response time` comes many times faster. > For example, previously reading a Holding register with 0 in the amount of 1 pc. took 100ms+, now the average response time is ~54ms - Fix for reading packets, now packets are read by the specified length, depending on the function being sent. Before, packets were read by 1 byte. This allowed us to reduce the load and speed up the response time > If the function code that is used to send a packet from the private function _getExpectedResponseLength(pdu) is not specified for `ModbusClient`, then the packet is read by 1 byte ### **1.6.5 (2025-07-03)** - New file structure for transports | Original Name | New Location | |----------------------------|--------------------------------------------------| | Web Serial Transport | transport/web-transports/web-serialport.js | | Web TCP Transport | transport/web-transports/node-tcp-serialport.js | | Node Serial Transport | transport/node-transports/node-serialport.js | | Node TCP Serial Transport | transport/node-transports/node-tcp-serialport.js | > Full information on transport [here](#transports) - Added **Web TCP Transport** and **Node TCP/IP Transport** ### **1.6.4 (2025-07-02)** - Fixed `onData` function in **PollingManager** - now the function correctly parses the response from the Modbus RTU device. > Previously, if the function's response to `onData()` was **null** or **undefined**, the function would stop working. Now, if the response is incorrect, the function skips a pool round, reports an error, and starts over. ### **1.6.2 (2025-06-27)** - Fixed the `parseWriteMultipleRegistersResponse` function - the function now correctly parses the response from the Modbus RTU device. ### **1.6.1 (2025-06-25)** - Fixed the `createTransport` function - import methods of the **WebSerial Transport**, **NodeSerial Transport**, **TcpSerialTransport** classes. Previously, there were problems with the initialization of these classes, due to which it was impossible to initialize the creation of transport and use it in the initialization of `ModbusClient` - Removed `utils/getSerialPort.js` file because **Web Serial API** was unstable when receiving COM port data ### **1.6.0 (2025-06-02)** - The entire library has been migrated from ESM to CommonJS - Added **slave device emulator**, which works without physical and virtual COM port. ([see more information](#slave-emulator)) - Added TCP/IP transport([see more information](#transports)) ### **1.5.7 (2025-05-30)** - Fixed logic for `onData:` in PollingManager. Now when specifying parameters in `onData:(fn1, fn2)`, from the parameter `fn: [fn1(), fn2()]`, the result will be passed in full to each parameter of `onData: (fn1, fn2)` accordingly. Example: ```js poll.addTask({ id: 'modbus-loop', interval: 1000, immediate: true, fn: [ async () => await client.readHoldingRegisters(0, 2), async () => await client.readHoldingRegisters(2, 2), ], onData: (hold1, hold2) => { log.info('Registers:', hold1, hold2 ); }, onError: (error, index, attempt) => { console.warn(`Error in fn[${index}], attempt ${attempt}: ${error.message}`); }, onStart: () => console.log('Polling started'), onStop: () => console.log('Polling stopped'), maxRetries: 3, backoffDelay: 300, taskTimeout: 2000 }); ``` **Result** ```bash fn: [0] (hold1) fn: [1] (hold2) [2025-05-30 04:05:41.047] [modbus] [NODE] [INFO] Registers: [ 4866, 25629 ] [ 1986, 0 ] ``` ### **1.5.5 (2025-05-29)** - Added logger for import via `modbus-connect/logger` ([see more information](#logger)) - Fixed a bug that caused a `Write timeout` warning immediately after disconnecting a device - Added the ability to specify the data type when reading `Hold/Input` registers ([see more information](#type-data)): > If you don't specify a data type, the result will default to `uint16` - Exmaple without specifying the data type: ```js const regs = await client.readInputRegisters(0, 4) // default: uint16 // ...4866, 25629, 1986, 0 ``` - Example with indication of data type ```js const regs = await client.readInputRegisters(0, 4, { type: 'hex' }) // ...'1302', '641D', '07C2', '0000' ``` ### **1.5.4 (2025-05-27)** - Corrected usage guide in README.md ### **1.5.3 (2025-05-27)** - Fixed the name of the `path` option on the `node` port of the transport. Now when initializing the transport, there is no need to rewrite the names of the options, they are the same for both transports - Added initialization parameter for **ModbusClient** - `crcAlgorithm` ([see more information](#crc))