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