modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
1,098 lines (937 loc) β’ 50.4 kB
Markdown
# 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