modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
1,049 lines (890 loc) β’ 45.5 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)
- [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)
---
<br><br>
## 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, // Initialize transport
1, // Modbus address
{
timeout: 1000,
crcAlgorithm: 'crc16Modbus',
retryCount: 3, // Number of attempts to repeat the request
retryDelay: 300, // Delay between repetitions
echoEnabled: false // Enable ECHO -> True or False
}
)
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();
```
### 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><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
- **diagnostics.js** - Diagnostics class collects stats (requests, errors, response times, etc.)
<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 |
| 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');
}
}
});
```
<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.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 optimized
- Updated **Logger**
- Now the table for calculating `crc16Modbus` is created at the very beginning once
- `TCP/IP` transports removed (for revision)
### **1.6.7 (2025-07-14)**
- Updated user guide
- Added more **methods** to `PollingManager` guide
- Corrected information in point 9
- Updated information in point 7
### **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))