UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

1,098 lines (937 loc) β€’ 50.4 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) - [Basic Usage](#basic-usage) - [Summary type data](#type-data) - [Main Classes and Methods](#main-classes-and-methods) - [Modbus Functions](#modbus-functions) - [Packet building & Parsing](#packet-building-and-parsing) - [Diagnostics & Error Handling](#diagnostics-and-error-handling) - [Logger](#logger) - [Utilities](#utilities) - [CRC](#crc) - [Error Handling](#error-handling) - [Polling Manager](#polling-manager) - [Slave Emulator](#slave-emulator) - [Tips for use](#tips-for-use) - [Expansion](#expansion) - [CHANGELOG](#changelog) --- <br> ## 1. πŸ“ <span id="library-structure">Library Structure</span> - **function-codes/** β€” PDU implementations for all Modbus functions (register/bit read/write, special functions). - **transport/** β€” Transport adapters (Node.js SerialPort, Web Serial API), auto-detection helpers. - **utils/** β€” Utilities: CRC, diagnostics, and helpers. - **polling-manager.js** - A tool for continuously polling a device at a specified interval - **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, including `ModbusFlushError`. - **logger.js** β€” Event logging utilities. - **packet-builder.js** β€” ADU packet construction/parsing (with CRC). <br> ## Features - Supports Modbus RTU over serial ports (Node.js) and Web Serial API (Browser). - Automatic reconnection mechanisms (primarily in transport layer). - Robust error handling with specific Modbus exception types. - Integrated polling manager for scheduled data acquisition. - Built-in logging with configurable levels and categories. - Diagnostic tools for monitoring communication performance. - Utility functions for CRC calculation, buffer manipulation, and data conversion. - Slave emulator for testing purposes. ## Intallation ```bash npm install modbus-connect ``` ## 2. πŸš€ <span id="basic-usage">Basic Usage</span> Please read [Important Note](#important-note) before use. ### Importing Modules The library provides several entry points for different functionalities: ```js // Main Modbus client import ModbusClient from 'modbus-connect/client'; // Polling manager for scheduled tasks import PollingManager from 'modbus-connect/polling-manager'; // Transport factory for creating connections import { createTransport } from 'modbus-connect/transport'; // Logger for diagnostics and debugging import logger from 'modbus-connect/logger'; // Slave emulator for testing import SlaveEmulator from 'modbus-connect/slave-emulator'; ``` ### Creating transports Transports are the underlying communication layers. The library provides a factory function to simplify their creation across different environments. **Node.js Serial Port:** ```js const transport = await createTransport('node', { port: '/dev/ttyUSB0', // or 'COM3' on Windows baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); ``` **Web Serial (Recommended with `port` for robust reconnection):** For reliable reconnection, especially after physical device disconnection, it's highly recommended to use a `port` function. This allows the transport to request a fresh `SerialPort` instance when needed. ```js // Function to request a SerialPort instance, typically called from a user gesture // or stored from an initial user selection. const getSerialPort = async () => { // In a real application, you might store the port object after the first user selection // and return it here, or request a new one if needed. // Example for initial request (requires user gesture): // const port = await navigator.serial.requestPort(); // Store port for future use... // return port; // Example returning a previously stored/stale port (less robust for reconnection): // return storedSerialPortInstance; // Example forcing a new request (requires user gesture, best for manual reconnection): const port = await navigator.serial.requestPort(); // Update stored reference if needed // storedSerialPortInstance = port; return port; }; const transport = await createTransport('web', { port: getSerialPort, // Recommended for robustness // OR, for simpler cases (less robust reconnection): // port: serialPortInstance, // Directly pass a SerialPort object baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none', // Optional reconnection parameters for WebSerialTransport reconnectInterval: 3000, // ms maxReconnectAttempts: 5, // Set to Infinity for continuous attempts (use with caution) maxEmptyReadsBeforeReconnect: 10 // Triggers reconnect if data stops flowing }); ``` 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', { 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 ### Creating a Client ```js const client = new ModbusClient(transport, slaveId = 1, options = {}) ``` - `transport` β€” transport object (see below) - `slaveId` β€” device address (1..247) - `options` β€” `{ timeout, retryCount, retryDelay }` ### Connecting and Communicating ```js try { await client.connect(); console.log('Connected to device'); // Reading holding registers const registers = await client.readHoldingRegisters(0, 10); // Start at address 0, read 10 registers console.log('Registers:', registers); // Writing a single register await client.writeSingleRegister(5, 1234); // Write 1234 to register 5 } catch (error) { console.error('Communication error:', error.message); } finally { await client.disconnect(); } ``` ### Hadnling Reconnection **Automatic Reconnection:** The `WebSerialTransport` and `NodeSerialTransport` now include built-in automatic reconnection logic. This handles scenarios like temporary cable glitches or device resets. You can configure this behavior using options like `reconnectInterval` and maxReconnectAttempts when creating the transport. **Manual Reconnection (Web environment)** Due to browser security policies, automatic reconnection cannot always call `navigator.serial.requestPort()` if the physical device is disconnected and reconnected. In such cases, or if automatic reconnection fails, a manual reconnection initiated by a user action is required. ```js // Example function to be called by a UI button click (user gesture) async function handleManualReconnect() { try { // Ensure any previous connection is cleanly closed if (client && transport) { try { await client.disconnect(); // This calls transport.disconnect() } catch (e) { console.warn("Error disconnecting previous connection:", e.message); } } // Create a new transport using a port that requests a port via user gesture const newTransport = await createTransport('web', { port: async () => { // This call is now valid because it's within a user gesture handler const new_port = await navigator.serial.requestPort(); // Update any stored reference // storedSerialPortInstance = port; return new_port; }, baudRate: 9600, // Use same settings as before // ... other settings maxReconnectAttempts: 0 // Disable auto-reconnect in new transport if desired }); // Create a new client const newClient = new ModbusClient(newTransport, 1); // Use correct slave ID // Connect await newClient.connect(); console.log('Manually reconnected!'); // Replace old instances // transport = newTransport; // client = newClient; // Restart polling tasks if needed // pollingManager.stopTask('read-sensors'); // pollingManager.addTask({...task, fn: () => newClient.readInputRegisters(0, 4) }); // pollingManager.startTask('read-sensors'); } catch (err) { console.error("Manual reconnection failed:", err.message); // Handle user cancellation or other errors } } // In your UI (e.g., HTML/JS framework) // <button onclick="handleManualReconnect()">Reconnect Device</button> ``` ### 🧾 <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> ## 3. πŸ—οΈ <span id="main-classes-and-methods">Main Classes and Methods</span> **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 **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> ## 4. 🧩 <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> ## 5. πŸ“¦ <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> ## 6. πŸ“Š <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`, - `ModbusFlushError` - Thrown when an operation is interrupted by a transport `flush()`. **Diagnostics example:** ```js const stats = client.getDiagnostics(); console.log(stats); client.resetDiagnostics(); ``` ### Here's a consolidated table of methods from the provided `Diagnostics` class: | Method | Description | |-----------------------------------|-----------------------------------------------------------------------------| | **constructor()** | Initializes the diagnostics object and resets statistics | | **reset()** | Resets all statistics and counters | | **recordRequest()** | Records a request event (increments counter and updates timestamp) | | **recordRetry(attempts)** | Logs retry attempts (adds to total retry count) | | **recordRetrySuccess()** | Records a successful retry operation | | **recordFunctionCall(funcCode)** | Tracks function calls by Modbus function code | | **recordSuccess(responseTimeMs)** | Logs successful responses with response time metrics | | **recordError(error, options)** | Records error details with optional code, timing and exception information | | **recordDataSent(byteLength)** | Tracks outgoing data volume | | **recordDataReceived(byteLength)**| Tracks incoming data volume | | **getStats()** | Returns comprehensive statistics object | | **serialize()** | Returns JSON-formatted statistics | | **toTable()** | Converts stats to array of {metric, value} objects | | **mergeWith(other)** | Combines statistics from another Diagnostics instance | ### Key Properties Tracked: - Request/response counters - Error classification (timeouts, CRC errors, Modbus exceptions) - Timing metrics - Data transfer volumes - Function call frequencies - Error message tracking - Session management The class provides complete instrumentation for Modbus communication monitoring with both real-time metrics and historical error tracking. <br> ## 7. πŸ›  <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> ## 8. πŸ›  <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 - **diagnostics.js** - Diagnostics class collects stats (requests, errors, response times, etc.) <br> ## 9. <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> ## 10. πŸŒ€ <span id="error-handling">Error Handling</span> The library defines specific error types for different Modbus issues: - `ModbusError`: Base class for all Modbus errors. - `ModbusTimeoutError`: Raised on request timeouts. - `ModbusCRCError`: Raised on CRC checksum failures. - `ModbusResponseError`: Raised on malformed responses. - `ModbusTooManyEmptyReadsError`: Raised if the transport detects a stalled connection. - `ModbusExceptionError`: Raised for standard Modbus exception responses from devices. - `ModbusFlushError`: Raised if an operation is interrupted by a transport buffer flush. Always wrap your Modbus calls in `try...catch` block to handle these errors appropriately. ```js try { const data = await client.readHoldingRegisters(100, 1); // Invalid address } catch (err) { if (err instanceof ModbusExceptionError) { console.error(`Modbus Exception ${err.exceptionCode}: ${err.message}`); // Handle specific device errors (e.g., Illegal Data Address) } else if (err instanceof ModbusTimeoutError) { console.error('Device did not respond in time'); // Handle timeout, might trigger reconnection logic check } else if (err instanceof ModbusFlushError) { console.warn('Operation interrupted by buffer flush, likely due to reconnection'); // Task will likely be retried by PollingManager or you can retry } else { console.error('An unexpected error occurred:', err.message); } } ``` <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. Improved to work seamlessly with transport `flush()` and automatic reconnection. ### πŸ“¦ 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 - Handles `ModbusFlushError` gracefully, resetting backoff delays ### Usage example ```js const PollingManager = require('modbus-connect/polling-manager'); const pollingManager = new PollingManager(); // Define a polling task poll.addTask({ id: 'read-sensors', // Task name resourceId: 'test', // Stream name priority: 1, // Priority: method in queue (0...Infinity) interval: 1000, fn: [ async () => return 'test' ], onData: (data) => { console.log(data) }, onError: async (error, index, attempt) => { console.log(error) }, onStart: () => console.log('Polling measure data started'), onStop: () => console.log('Polling measure data stopped'), maxRetries: 3, backoffDelay: 300, taskTimeout: 1000 }) pollingManager.startTask('read-sensors'); // Later... // pollingManager.stopTask('read-sensors'); // pollingManager.removeTask('read-sensors'); ``` >If you need to perform 2 or more tasks for 1 device (for example, COM port), then resourceId must be the same, otherwise, tasks in 2 different threads will be performed in parallel, which will lead to errors. >Frequent use of `pauseAllTasks` and `resumeAllTasks` may result in duplicate tasks in the task queue. ### 🧩 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'); ``` **🧠 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 | | clearAll() | Stops and removes all registered tasks | | restartAllTasks() | Restart all tasks | | pauseAllTasks() | Pause all tasks | | resumeAllTasks() | Resume all tasks | | startAllTasks() | Start all tasks | | stopAllTasks() | Stop all tasks | | getAllTaskStats() | Get stats for all tasks | ### πŸ“Š 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 } | | getTaskStats(id) | Detailed info: { metric's data } | 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'); } } }); ``` >Improved Flush Handling: The `PollingManager` now automatically flushes the transport buffer before each task run and intelligently handles `ModbusFlushError` during, retries, resetting the exponential backoff delay for better resoinsiviness after a flush. <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. ### Emulator 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> ## πŸ“Ž 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. - `PollingManager` handles transport `flush()` and `ModbusFlushError` internally for smotther operation. <br> ## ❗ <span id="important-note">Important Note</span> - Automatic transport reconnection is handled by the transport layer, not by the `ModbusClient` directly during retries anymore. >Automatic device reconnection for `WebSerialTransoprt` is not possible due to browser restrictions for user security reasons. <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** or **Edge** or **Opera**). - Use auto-detection methods to find port parameters and slaveId - Use the client's `getDiagnostics()` and `resetDiagnostics()` methods for diagnostics. <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> ## <span id="changelog">CHANGELOG</span> ### **1.8.5 (2025-7-29)** - Improved device response checking in `packet-builder.js` and `write-multiple-registers.js` ### **1.8.4 (2025-07-26)** - Returned the previous parameters for `web` transport in `factory.js`, now it is necessary to simply pass **port** instead of **deviceManager** - Library manual updated ### **1.8.2 (2025-07-25)** - Added task streams to `PollingManager`. This changed the usage of task creation in PollingManager, see changes in [PollingManager](#polling-manager) - Removed check for function code in device response to `writeMultipleRegisters` ### **1.8.1 (2025-07-24)** - Added `portFactory` option to `ModbusClient` and `createTransport` constructor's for web transport - Added `deviceManager` option to `ModbusClient` and `createTransport` constructor's for web transport - Library manual updated ### **1.8.0 (2025-07-24)** - Refactored Reconnection Logic: Automatic reconnection responsibility has been moved from `ModbusClient` to the transport layer (**NodeSerialTransport**, **WebSerialTransport**). This prevents conflicts and simplifies the client's error handling. - Enhanced Flush Handling: Added ModbusFlushError and implemented `flush()` methods in transports. `ModbusClient` and `PollingManager` now correctly handle this error, often resetting backoff delays for faster recovery. - Improved PollingManager: `PollingManager` now resets its exponential backoff delay after a transport flush or a `ModbusFlushError`. It also flushes the transport buffer before each task run to ensure clean communication state. - Removed Transport Restart: Removed the automatic transport restart logic from `PollingManager` as transports now manage their own reconnection. - Updated Documentation: Documentation updated to reflect the new architecture and error handling improvements. ### **1.7.4 (2025-07-23)** - Added automatic reconnection for **ModbusClient** when all retries are exhausted in `ModbusClient(options = { retryCount })` > The automatic reconnection system is currently experimental and will be improved in modern updates - Added clearing of buffer for sending and reading packets from garbage data. ### **1.7.3 (2025-07-21)** - For **Polling Manager** added buffer clearing when starting a task - For **NodeSerialPort** and **WebSerialPort** added buffer clearing when connecting and disconnecting via flush() - All Modbus functions have been