modbus-connect
Version:
Modbus RTU over Web Serial and Node.js SerialPort
1,416 lines (1,012 loc) • 109 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.
## Navigation through documentation
- [Features](#features)
- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [Modbus Client](#modbus-client)
- [Transport Controller](#transport-controller)
- [Errors Types](#errors-types)
- [Polling Manager](#polling-manager)
- [Slave Emulator](#slave-emulator)
- [Logger](#logger)
- [Utils](#utils)
- [Utils CRC](#utils-crc)
- [Plugin System](#plugin-system)
- [Tips for use](#tips-for-use)
- [Expansion](#expansion)
- [CHANGELOG](#changelog)
<br>
# <span id="features">Features</span>
- 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 (without COM port).
- **Plugin System:** Extend client functionality with custom functions, data types, and CRC algorithms without modifying the library core.
<br>
# <span id="installation">Installation</span>
```bash
npm install modbus-connect
```
<br>
# <span id="basic-usage">Basic Usage</span>
### Importing Modules
The library provides several entry points for different functionalities:
```js
// Types library
import { _type_ } from 'modbus-connect/types';
// Main Modbus client
import ModbusClient from 'modbus-connect/client';
// Transport controller for managing connections
import TransportController 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 via TransportController
The `TransportController` is the centralized way to manage one or more transport connections. It handles routing, reconnection, and assignment of slave IDs to specific transports.
**Node.js Serial Port:**
```js
await controller.addTransport(
'node-port-1',
'node',
{
port: '/dev/ttyUSB', // or 'COM' on Windows
baudRate: 19200,
slaveIds: [1, 2], // Assign these slave IDs to this transport
},
{
maxReconnectAttempts: 10, // Reconnect options
},
{
defaultInterval: 1000, // Polling options (Optional)
}
);
await controller.connectAll();
```
**Web Serial API port:**
```js
// Function to request a SerialPort instance, typically called from a user gesture
const getSerialPort = await navigator.serial.requestPort();
await controller.addTransport('web-port-1', 'web', {
port: getSerialPort,
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: 'none',
reconnectInterval: 3000,
maxReconnectAttempts: 5,
maxEmptyReadsBeforeReconnect: 10,
slaveIds: [3, 4],
});
await controller.connectAll();
```
To set the read/write speed parameters, specify writeTimeout and readTimeout during addTransport. Example:
```js
await controller.addTransport('node-port-2', 'node', {
port: 'COM3',
writeTimeout: 500,
readTimeout: 500,
slaveIds: [5],
});
```
> 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(controller, 1, {
/* ...options */
});
```
- `controller` — The `TransportController` instance.
- `slaveId` — Device address (1..247). The controller will route requests to the correct transport.
- `options` — `{ timeout, retryCount, retryDelay, plugins }`
### Connecting and Communicating
```js
try {
await client.connect();
console.log('Connected to device');
const registers = await client.readHoldingRegisters(0, 10);
console.log('Registers:', registers);
await client.writeSingleRegister(5, 1234);
} catch (error) {
console.error('Communication error:', error.message);
} finally {
await client.disconnect();
await controller.disconnectAll(); // Disconnect all managed transports
}
```
### Work via RS485
In order to work via RS485, you first need to connect the COM port.
```js
await controller.addTransport('rs485-port', 'node', {
port: 'COM3',
baudRate: 9600,
dataBits: 8,
stopBits: 1,
parity: 'none',
writeTimeout: 500,
readTimeout: 500,
slaveIds: [38, 51], // Multiple devices on the same port
});
await controller.connectAll();
const device_1 = new ModbusClient(controller, 38, { timeout: 1000 });
const device_2 = new ModbusClient(controller, 51, { timeout: 1000 });
try {
const registers_1 = await device_1.readHoldingRegisters(0, 10);
console.log('Registers 1:', registers_1);
const registers_2 = await device_2.readHoldingRegisters(0, 10);
console.log('Registers 2:', registers_2);
} catch (error) {
console.error('Communication error:', error.message);
} finally {
await device_1.disconnect();
await device_2.disconnect();
await controller.disconnectAll();
}
```
<br>
# <span id="modbus-client">Modbus Client</span>
The ModbusClient class is a client for working with Modbus devices (RTU/TCP, etc.) via the transport layer. It supports standard Modbus functions (reading/writing registers and coils), SGM130-specific functions (device comments, files, reboot, controller time), and integration with the logger from **Logger**. The client uses a **mutex** for synchronization, error retry, **diagnostics**, and **CRC checking**.
Key Features:
- **Transport:** Works **exclusively** through a `TransportController` instance, which manages routing to the underlying physical transports. Direct interaction with a transport is no longer supported.
- **Retry and Timeouts**: Automatic retry (up to retryCount), retryDelay delay, default timeout of 2000ms.
- **Logging**: Integration with Logger (default 'error' level). Context support (slaveId, funcCode).
- **Data Conversion**: Automatic conversion of registers to types (`uint16`, `float`, strin`g, etc.), with byte/word swap support.
- **Errors**: Special classes (`ModbusTimeoutError`, `ModbusCRCError`, `ModbusExceptionError`, etc.).
- **CRC**: Support for various algorithms (`crc16Modbus` by default).
- **Echo**: Optional echo check for serial (for debugging).
- **Extensible via Plugins:** Supports external plugins to add proprietary function codes, custom data types, and new CRC algorithms without modifying the library's source code.
**Dependencies:**
- async-mutex for synchronization.
- Functions from ./function-codes/\* for building/parsing PDUs.
- Logger, Diagnostics, packet-builder, utils, errors, crc.
**Logging levels:** Defaults to 'error'. Enable enableLogger() for more details.
## Initialization
Include the module:
```js
const ModbusClient = require('modbus-connect/client');
const TransportController = require('modbus-connect/transport');
```
Create an instance:
```js
// Import your plugin class
const { MyAwesomePlugin } = require('./plugins/my-awesome-plugin.js');
const controller = new TransportController();
await controller.addTransport('com-port-3', 'node', {
port: 'COM3',
baudRate: 9600,
parity: 'none',
dataBits: 8,
stopBits: 1,
slaveIds: [1],
RSMode: 'RS485', // or 'RS232'. Default is 'RS485'.
});
await controller.connectAll();
const options = {
timeout: 3000,
retryCount: 2,
retryDelay: 200,
diagnostics: true,
echoEnabled: true,
crcAlgorithm: 'crc16Modbus',
plugins: [MyAwesomePlugin], // Pass plugin classes directly in constructor
};
const client = new ModbusClient(controller, 1, options);
```
**_Initialization output (if logging is enabled):_** _No explicit output in the constructor. Logging is enabled by methods._
**Connection:**
```js
await client.connect();
```
**Output (if level >= 'info'):**
```bash
[04:28:57][INFO][NodeSerialTransport] Serial port COM3 opened
```
**Disconnect:**
```js
await client.disconnect();
```
**Output:**
```bash
[05:53:17][INFO][NodeSerialTransport] Serial port COM3 closed
```
## Logging Controls
### 1. enableLogger(level = 'info')
Enables ModbusClient logging.
**Example:**
```js
client.enableLogger('debug');
```
Now all requests/errors will be logged.
### 2. disableLogger()
Disables (sets 'error').
**Example:**
```js
client.disableLogger();
```
### 3. setLoggerContext(context)
Adds a global context (e.g., { custom: 'value' }).
**Example:**
```js
client.setLoggerContext({ env: 'test' });
```
The context is added to all logs.
## Basic Modbus methods (standard functions)
### Standard Modbus 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 |
### Summary type data
| 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) |
All methods are asynchronous and use `_sendRequest` to send with retry. They return data or a response object. The timeout is optional (uses default).
### 4. readHoldingRegisters(startAddress, quantity, options = {})
Reads holding registers (function 0x03). Converts to the type from options.type.
**Parameters:**
- `startAddress (number):` Start address (0-65535).
- `quantity (number):` Number of registers (1-125).
- `options.type (string, opt):` 'uint16', 'int16', 'uint32', 'float', 'string', 'hex', 'bool', 'bcd', etc. (see \_convertRegisters).
**Example 1: Basic reading of uint16.**
```js
const registers = await client.readHoldingRegisters(100, 2);
console.log(registers); // [1234, 5678] (array of numbers)
```
**Log output (if level >= 'debug'):**
```bash
[14:30:15][DEBUG] Attempt #1 — sending request { slaveId: 1, funcCode: 3 }
[14:30:15][DEBUG] Packet written to transport { bytes: 8, slaveId: 1, funcCode: 3 }
[14:30:15][DEBUG] Echo verified successfully { slaveId: 1, funcCode: 3 } (if echoEnabled)
[14:30:15][DEBUG] Received chunk: { bytes: 9, total: 9 }
[14:30:15][INFO] Response received { slaveId: 1, funcCode: 3, responseTime: 50 }
```
**Example 2: Reading as a float (2 registers = 1 float).**
```js
const floats = await client.readHoldingRegisters(200, 2, { type: 'float' });
console.log(floats); // [3.14159] (array of float)
```
**Example 3: Reading a string.**
```js
const str = await client.readHoldingRegisters(300, 5, { type: 'string' });
console.log(str); // 'Hello' (string)
```
**Errors:** ModbusTimeoutError, ModbusCRCError, ModbusExceptionError (with exception code).
### 5. readInputRegisters(startAddress, quantity, options = {})
Reads input registers (function 0x04). Same as readHoldingRegisters.
**Example:**
```js
const inputs = await client.readInputRegisters(50, 3, { type: 'uint32' });
console.log(inputs); // [12345678, 87654321] (2 uint32 from 4 registers)
```
**Output:** Same as readHoldingRegisters, funcCode=4.
### 6. writeSingleRegister(address, value, timeout)
Writes a single holding register (function 0x06).
**Parameters:**
- `address (number):` Address.
- `value (number):` Value (0-65535).
- `timeout (number, optional):` Timeout.
**Example:**
```js
const response = await client.writeSingleRegister(400, 999);
console.log(response); // { address: 400, value: 999 }
```
**Log output:**
```bash
[14:30:15][INFO] Response received { slaveId: 1, funcCode: 6, responseTime: 30 }
```
### 7. writeMultipleRegisters(startAddress, values, timeout)
Writes multiple holding registers (function 0x10).
**Parameters:**
- startAddress (number).
- values (number[]): Array of values.
- timeout (number, optional).
**Example:**
```js
const response = await client.writeMultipleRegisters(500, [100, 200, 300]);
console.log(response); // { startAddress: 500, quantity: 3 }
```
**Output:** funcCode=16 (0x10).
### 8. readCoils(startAddress, quantity, timeout)
Reads coils (function 0x01). Returns `{ coils: boolean[] }`.
**Example:**
```js
const { coils } = await client.readCoils(0, 8);
console.log(coils); // [true, false, true, ...]
```
**Output:** funcCode=1.
### 9. readDiscreteInputs(startAddress, quantity, timeout)
Reads discrete inputs (function 0x02). Same as readCoils.
**Example:**
```js
const { inputs } = await client.readDiscreteInputs(100, 10);
console.log(inputs); // [false, true, ...]
```
### 10 writeSingleCoil(address, value, timeout)
Writes a single coil (function 0x05). value: 0xFF00 (true) or 0x0000 (false).
**Example:**
```js
const response = await client.writeSingleCoil(10, 0xff00); // Enable
console.log(response); // { address: 10, value: 0xFF00 }
```
### 11. writeMultipleCoils(startAddress, values, timeout)
Writes multiple coils (function 0x0F). values: `boolean[]` or `number[]` (0/1).
**Example:**
```js
const response = await client.writeMultipleCoils(20, [true, false, true]);
console.log(response); // { startAddress: 20, quantity: 3 }
```
## Special Modbus Functions
### 1. reportSlaveId(timeout)
Report slave ID (function 0x11). Returns { slaveId, runStatus, ... }.
**Example:**
```js
const info = await client.reportSlaveId();
console.log(info); // { slaveId: 1, runStatus: true, ... }
```
### 2. readDeviceIdentification(timeout)
Reading identification (function 0x2B). SlaveId is temporarily reset to 0.
**Example:**
```js
const id = await client.readDeviceIdentification();
console.log(id); // { vendor: 'ABC', product: 'XYZ', ... }
```
## Internal methods (For expansion)
- `_toHex(buffer):` Buffer to a hex string. Used in logs.
- `_getExpectedResponseLength(pdu):` Expected response length for the PDU.
- `_readPacket(timeout, requestPdu):` Read a packet
- `_sendRequest(pdu, timeout, ignoreNoResponse):` Basic sending method with retry, echo, and diagnostics.
- `_convertRegisters(registers, type):` Register conversion (supports 16/32/64-bit, float, string, BCD, hex, bool, binary with swaps: \_sw, \_sb, \_le, and combinations).
**Conversion example with swap:**
```js
// In readHoldingRegisters options: { type: 'float_sw' } — word swap for float.
const swapped = await client.readHoldingRegisters(400, 2, { type: 'float_sw' });
```
## Diagnostics
The client uses Diagnostics for statistics (recordRequest, recordError, etc.). Access via client.diagnostics.
**Example:**
```js
console.log(client.diagnostics.getStats()); // { requests: 10, errors: 2, ... }
```
## Full usage example
```js
const ModbusClient = require('modbus-connect/client');
const TransportController = require('modbus-connect/transport');
async function main() {
const controller = new TransportController();
await controller.addTransport('com-port-3', 'node', {
port: 'COM3',
baudRate: 9600,
parity: 'none',
dataBits: 8,
stopBits: 1,
slaveIds: [1],
RSMode: 'RS485', // or 'RS232'. Default is 'RS485'.
});
await controller.connectAll();
const client = new ModbusClient(controller, 1, { timeout: 1000, retryCount: 1 });
client.enableLogger('info');
try {
await client.connect();
const regs = await client.readHoldingRegisters(0, 10, { type: 'uint16' });
console.log('Registers:', regs);
await client.writeSingleRegister(0, 1234);
const time = await client.getControllerTime();
console.log('Controller time:', time);
await client.disconnect();
} catch (err) {
console.error('Modbus error:', err);
} finally {
await controller.disconnectAll();
}
}
main();
```
**Expected output (snippet):**
```bash
[05:53:16][INFO][NodeSerialTransport] Serial port COM3 opened
[05:53:17][INFO] Response received { slaveId: 1, funcCode: 3, responseTime: 45 }
Registers: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[05:53:18][INFO] Response received { slaveId: 1, funcCode: 6, responseTime: 20 }
Controller time: { datetime: '2025-10-07T10:00:00Z' }
[05:53:19][INFO][NodeSerialTransport] Serial port COM3 closed
```
**On error (timeout):**
```bash
[14:30:15][WARN] Attempt #1 failed: Read timeout { responseTime: 1000, error: ModbusTimeoutError, ... }
[14:30:15][DEBUG] Retrying after delay 200ms { slaveId: 1, funcCode: 3 }
[14:30:15][ERROR] All 2 attempts exhausted { error: ModbusTimeoutError, ... }
Modbus error: Read timeout
```
<br>
# <span id="transport-controller">Transport Controller</span>
The `transport/transport-controller.js` module provides a centralized way to manage **multiple** Modbus transports (serial or TCP) depending on the environment (Node.js or Web). `TransportController` allows you to **manage connections**, **route requests** between devices with different `slaveId`s via different transports, and provides **load balancing** and **fault tolerance**.
**Key Features:**
- **Transport Management:** Add, remove, connect, disconnect.
- **Routing:** Automatically routes requests from `ModbusClient` to the correct transport based on `slaveId`.
- **Dynamic Assignment:** Ability to assign new `slaveId`s to an already connected transport.
- **Fault Tolerance:** Supports fallback transports.
- **Logging:** Integrated with the main logger.
- **Diagnostics:** Can provide transport-level statistics.
- **Device/Port State Tracking:** Internally leverages the state tracking capabilities of the underlying `NodeSerialTransport` and `WebSerialTransport`. These transports use `DeviceConnectionTracker` and `PortConnectionTracker` to monitor the connection status of individual Modbus slaves and the physical port itself, providing detailed error types and messages. `TransportController` manages these states for all managed transports. **You can subscribe to state changes by setting handlers directly on the individual transports added to the controller.**
The module exports the `TransportController` class. It maintains its own internal state for managing transports and routing.
**Dependencies:**
- `./factory.js`: For creating underlying transport instances (NodeSerialTransport, WebSerialTransport).
- `../logger.js`: For logging.
- `../types/modbus-types.js`: For type definitions.
## Initialization
Include the module
```js
const TransportController = require('modbus-connect/transport');
```
Or in the browser:
```js
import TransportController from 'modbus-connect/transport';
```
Create an instance:
```js
const controller = new TransportController();
```
Logging and diagnostics are configured internally or via the main logger.
## Main functions
### 1. `addTransport(id, type, options, reconnectOptions?, pollingConfig?)`
Asynchronously adds a new transport to the controller and initializes its internal PollingManager.
**Parameters:**
- `id (string)`: A unique identifier for this transport within the controller.
- `type (string)`: Type ('node', 'web').
- `options (object)`: Config:
- `For 'node':` `{ port: 'COM3', baudRate: 9600, ..., slaveIds: [1, 2] }` (SerialPort options + `slaveIds` array).
- `For 'web':` `{ port: SerialPort instance, ..., slaveIds: [3, 4] }` (Web Serial Port instance + `slaveIds` array).
- `slaveIds (number[], optional):` An array of `slaveId`s that this transport will handle. These are registered internally for routing.
- `RSMode (string, optional):` 'RS485' or 'RS232'. Default is 'RS485'.
- `fallbacks (string[], optional):` An array of transport IDs to use as fallbacks for the assigned `slaveIds` if the primary transport fails.
**Returns:** Promise<void>
**Errors:** Throws Error on invalid options, duplicate ID.
- `reconnectOptions (object, optional)`: `{ maxReconnectAttempts: number, reconnectInterval: number }`.
- `pollingConfig (object, optional)`: Configuration for the internal PollingManager (e.g., `{ defaultInterval: 1000, maxRetries: 3 }`).
**Example 1: Add Node.js serial transport.**
```js
async function addNodeTransport() {
try {
await controller.addTransport('com3', 'node', {
port: 'COM3', // Use path for Node
baudRate: 19200,
dataBits: 8,
stopBits: 1,
parity: 'none',
slaveIds: [13, 14], // Assign slave IDs 13 and 14 to this transport
RSMode: 'RS485', // or 'RS232'. Default is 'RS485'.
});
console.log('Transport added to controller:', 'com3');
} catch (err) {
console.error('Failed to add transport:', err.message);
}
}
addNodeTransport();
```
**Output (logs if level >= 'info'; simulation):**
```bash
[14:30:15][INFO][TransportController] Transport "com3" added {"type":"node","slaveIds":[13, 14]}
```
**Example 2: Add Web serial transport.**
```js
// In the browser, after navigator.serial.requestPort()
async function addWebTransport(port) {
try {
await controller.addTransport('webPort1', 'web', {
port, // The SerialPort instance obtained via Web Serial API
slaveIds: [15, 16], // Assign slave IDs 15 and 16 to this transport
RSMode: 'RS485', // or 'RS232'. Default is 'RS485'.
});
console.log('Transport added to controller:', 'webPort1');
} catch (err) {
console.error('Failed to add transport:', err.message);
}
}
// Simulation: const port = await navigator.serial.requestPort();
addWebTransport(port);
```
**Output (logs):**
```bash
[14:30:15][INFO][TransportController] Transport "webPort1" added {"type":"web","slaveIds":[15, 16]}
```
### 2. `removeTransport(id)`
Asynchronously removes a transport from the controller. Disconnects it first if connected.
**Parameters:**
- `id (string)`: The ID of the transport to remove.
**Returns:** Promise<void>
**Example:**
```js
async function removeTransport() {
try {
await controller.removeTransport('com3');
console.log('Transport removed from controller:', 'com3');
} catch (err) {
console.error('Failed to remove transport:', err.message);
}
}
removeTransport();
```
### 3. `connectAll()` / `connectTransport(id)`
Connects all managed transports or a specific one.
**Parameters:**
- `id (string, optional)`: The ID of the specific transport to connect.
**Returns:** Promise<void>
**Example:**
```js
async function connectAllTransports() {
try {
await controller.connectAll(); // Connect all added transports
console.log('All transports connected via controller.');
} catch (err) {
console.error('Failed to connect transports:', err.message);
}
}
connectAllTransports();
```
### 4. `listTransports()`
Returns an array of all managed transports with their details.
**Parameters:** None
**Returns:** TransportInfo[] - Array of transport info objects.
**Example:**
```js
const transports = controller.listTransports();
console.log('All transports:', transports);
```
### 5. `assignSlaveIdToTransport(transportId, slaveId)`
Dynamically assigns a `slaveId` to an already added and potentially connected transport. Useful if you discover a new device on an existing port.
**Parameters:**
- `transportId (string)`: The ID of the target transport.
- `slaveId (number)`: The Modbus slave ID to assign.
**Returns:** void
**Errors:** Throws Error if `transportId` is not found.
**Example:**
```js
// Assume 'com3' transport was added earlier and is connected
// Later, you discover a device with slaveId 122 is also on COM3
controller.assignSlaveIdToTransport('com3', 122);
console.log('Assigned slaveId 122 to transport com3');
// ModbusClient with slaveId 122 will now use the 'com3' transport.
```
### 6. `removeSlaveIdFromTransport(transportId, slaveId)`
Dynamically removes a `slaveId` from a transport's configuration. This clears the internal registry, routing maps, **and resets the internal connection tracker state** for that specific device. This method is essential if you plan to re-assign the same `slaveId` to the transport later (e.g., after a physical reconnection sequence) to avoid "already managing this ID" errors or connection state debounce issues.
**Parameters:**
- `transportId (string)`: The ID of the target transport
- `slaveId (number)`: The Modbus slave ID to remove
**Returns:** void
**Errors:** Logs a warning if `transportId` is not found or if the `slaveId` was not assigned to that transport, but does not throw an exception
**Example:**
```js
// Assume we need to reboot or physically reconnect the device with slaveId 13
// First, remove it from the controller logic
controller.removeSlaveIdFromTransport('com3', 13);
console.log('Removed slaveId 13 from transport com3');
// ... physical reconnection happens ...
// Now you can safely re-assign it
controller.assignSlaveIdToTransport('com3', 13);
```
### 7. `getTransportForSlave(slaveId)`
Gets the currently assigned transport for a specific `slaveId`. Used internally by `ModbusClient` if needed, but can be useful for direct interaction.
**Parameters:**
- `slaveId (number)`: The Modbus slave ID.
**Returns:** `Transport | null` - The assigned transport instance or null if not found.
**Example:**
```js
const assignedTransport = controller.getTransportForSlave(13);
if (assignedTransport) {
console.log('Transport for slave 13:', assignedTransport.constructor.name);
} else {
console.log('No transport assigned for slave 13');
}
```
### 8. `Device/Port State Tracking`
To track the connection state of devices or the port itself, you need to access the individual transport instance managed by the `TransportController` and set the handler on it.
**Example: Setting Device State Handler**
```js
async function addAndTrackDevice() {
await controller.addTransport('com3', 'node', {
port: 'COM3',
baudRate: 9600,
slaveIds: [1, 2],
});
await controller.connectAll();
// Get the transport instance for 'com3'
const transport = controller.getTransport('com3');
if (transport && transport.setDeviceStateHandler) {
// Set the handler to receive state updates for devices on this transport
transport.setDeviceStateHandler((slaveId, connected, error) => {
console.log(`[Transport 'com3'] Device ${slaveId} is ${connected ? 'ONLINE' : 'OFFLINE'}`);
if (error) {
console.log(`[Transport 'com3'] Device ${slaveId} Error: ${error.type}, ${error.message}`);
}
});
}
// Create clients using the controller
const client1 = new ModbusClient(controller, 1, { timeout: 2000, RSMode: 'RS485' });
await client1.connect(); // This will trigger the handler for slaveId 1
}
addAndTrackDevice();
```
**Example: Setting Port State Handler**
```js
async function addAndTrackPort() {
await controller.addTransport('com4', 'node', {
port: 'COM4',
baudRate: 115200,
slaveIds: [3],
});
// Get the transport instance for 'com4' *before* connecting if needed
const transport = controller.getTransport('com4');
if (transport && transport.setPortStateHandler) {
// Set the handler to receive state updates for the physical port
transport.setPortStateHandler((connected, slaveIds, error) => {
console.log(`[Transport 'com4'] Port is ${connected ? 'CONNECTED' : 'DISCONNECTED'}`);
console.log(`[Transport 'com4'] Affected slave IDs:`, slaveIds || []);
if (error) {
console.log(`[Transport 'com4'] Port Error: ${error.type}, ${error.message}`);
}
});
}
await controller.connectAll();
// Create clients using the controller
const client3 = new ModbusClient(controller, 3, { timeout: 2000, RSMode: 'RS485' });
await client3.connect();
}
addAndTrackPort();
```
### 9. `writeToPort(transportId, data, readLength?, timeout?)`
Allows executing a direct write operation (or any command requiring exclusive port access) on a specific transport, leveraging the `PollingManager`'s mutex to prevent conflicts with background polling tasks. This is the safest way to send a non-polling, immediate command.
**Parameters:**
- `transportId (string)`: The ID of the transport to write to.
- `data (Uint8Array)`: The data buffer to write to the port.
- `readLength (number, optional)`: The expected length of the response data (in bytes). Defaults to `0` (no read).
- `timeout (number, optional)`: Timeout for reading the response, in milliseconds. Defaults to `3000` ms.
**Returns:** `Promise<Uint8Array>` - The received data buffer or an empty buffer if `readLength` was `0`.
**Errors:** Throws Error if the transport is not found or if the underlying transport is not considered open/connected.
**Example:**
```js
async function sendDirectCommand() {
const transportId = 'com3';
const dataToSend = new Uint8Array([0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xcb, 0xfb]); // Example raw command
const expectedResponseLength = 9; // Command + 2 registers * 2 bytes/reg = 5 bytes response + header/CRC (example)
try {
console.log(`Sending direct command to transport ${transportId}...`);
// This call locks the transport's PollingManager, writes data, reads response, flushes, and releases lock.
const response = await controller.writeToPort(
transportId,
dataToSend,
expectedResponseLength,
5000 // 5 seconds timeout for this specific operation
);
console.log('Direct write successful. Response received:', response);
} catch (err) {
console.error(`Failed to write directly to transport ${transportId}:`, err.message);
}
}
sendDirectCommand();
```
> **Note on Transport State:** This method checks `info.transport.isOpen` internally. If you call this on a transport that is currently disconnecting or has an underlying error, it will likely fail, regardless of the PollingManager mutex being available. Ensure the transport is in the `'connected'` state before calling.
### 10. `getStatus(id?)`
Gets the status of a specific transport or all transports.
**Parameters:**
- `id (string, optional)`: The ID of the transport to get the status for. If not provided, returns the status of all transports.
**Returns:** TransportStatus[] - Array of transport status objects.
**Example:**
```js
const status = controller.getStatus('com3');
console.log('Transport status:', status);
```
### 11. `getActiveTransportCount()`
Returns the number of currently connected transports.
**Parameters:** None
**Returns:** number
### 12. `setLoadBalancer(strategy)`
Sets the load balancing strategy for routing requests.
**Parameters:**
- strategy (string): 'round-robin', 'sticky', 'first-available'
**Example:**
```js
controller.setLoadBalancer('round-robin');
```
### 13. `reloadTransport(id, options)`
Asynchronously reloads an existing transport with a new configuration. This is useful for changing settings like `baudRate` or even the physical `port` on the fly.
The controller will first safely disconnect the existing transport, then create a new transport instance with the provided options. If the original transport was connected, the controller will attempt to connect the new one automatically.
**Parameters:**
- `id (string)`: The unique identifier of the transport to be reloaded.
- `options (object)`: A new configuration object, identical in structure to the one used in `addTransport`.
**Returns:** `Promise<void>`
**Example:**
```js
// Initially, the transport is configured with a 9600 baudRate
await controller.addTransport('com3', 'node', {
port: 'COM3',
baudRate: 9600,
slaveIds: [1],
RSMode: 'RS485',
});
await controller.connectAll();
// ...some time later...
// Reload the same transport with a new baudRate of 19200
console.log('Reloading transport with new settings...');
await controller.reloadTransport('com3', {
port: 'COM3',
baudRate: 19200,
slaveIds: [1], // Note: You must provide all required options again
RSMode: 'RS485',
});
console.log('Transport reloaded successfully.');
```
### 14. Polling Task Management (Proxy Methods)
The `TransportController` now acts as a facade for managing polling tasks specific to each transport.
**Methods:**
- `addPollingTask(transportId, options)`: Adds a polling task to the specified transport.
- `removePollingTask(transportId, taskId)`: Removes a task.
- `updatePollingTask(transportId, taskId, options)`: Updates an existing task.
- `controlTask(transportId, taskId, action)`: Controls a specific task. Action: `'start' | 'stop' | 'pause' | 'resume'`.
- `controlPolling(transportId, action)`: Controls all tasks on the transport. Action: `'startAll' | 'stopAll' | 'pauseAll' | 'resumeAll'`.
- `getPollingStats(transportId)`: Returns statistics for all tasks on the transport.
- `executeImmediate(transportId, fn)`: Executes a function using the transport's polling mutex. This ensures the function runs atomatically, without conflicting with background polling tasks.
**Example:**
```js
// Add a periodic reading task to 'com3'
controller.addPollingTask('com3', {
id: 'read-sensors',
interval: 1000,
fn: () => client.readHoldingRegisters(0, 10),
onData: data => console.log('Data:', data),
onError: err => console.error('Error:', err.message),
});
// Execute a manual write operation safely while polling is active
await controller.executeImmediate('com3', async () => {
await client.writeSingleRegister(10, 123);
});
// Pause all polling on this transport (e.g. during maintenance)
controller.controlPolling('com3', 'pauseAll');
```
### 15. `destroy()`
Destroys the controller and disconnects all transports.
**Parameters:** None
**Returns:** Promise<void>
**Example:**
```js
await controller.destroy();
console.log('Controller destroyed');
```
## Full usage example
Integration with `ModbusClient`. Creating a controller, adding transports, setting state handlers, and using the controller in clients.
```js
const TransportController = require('modbus-connect/transport'); // Import TransportController
const ModbusClient = require('modbus-connect/client');
const Logger = require('modbus-connect/logger');
async function modbusExample() {
const logger = new Logger();
logger.enableLogger('info'); // Enable logs
const controller = new TransportController();
try {
// Add Node.js transport for slave IDs 1 and 2
await controller.addTransport('com3', 'node', {
port: 'COM3',
baudRate: 9600,
slaveIds: [1, 2],
RSMode: 'RS485',
});
// Add another Node.js transport for slave ID 3
await controller.addTransport('com4', 'node', {
port: 'COM4',
baudRate: 115200,
slaveIds: [3],
RSMode: 'RS485',
});
// Set up state tracking for each transport *after* adding but before connecting
const transport3 = controller.getTransport('com3');
if (transport3 && transport3.setDeviceStateHandler) {
transport3.setDeviceStateHandler((slaveId, connected, error) => {
console.log(`[COM3] Device ${slaveId}: ${connected ? 'ONLINE' : 'OFFLINE'}`);
if (error) console.error(`[COM3] Device ${slaveId} Error:`, error);
});
}
const transport4 = controller.getTransport('com4');
if (transport4 && transport4.setPortStateHandler) {
transport4.setPortStateHandler((connected, slaveIds, error) => {
console.log(`[COM4] Port: ${connected ? 'UP' : 'DOWN'}. Slaves affected:`, slaveIds);
if (error) console.error(`[COM4] Port Error:`, error);
});
}
// Connect all added transports
await controller.connectAll();
// Create clients, passing the controller instance and their specific slaveId
const client1 = new ModbusClient(controller, 1, { timeout: 2000, RSMode: 'RS485' }); // Uses 'com3'
const client2 = new ModbusClient(controller, 2, { timeout: 2000, RSMode: 'RS485' }); // Uses 'com3'
const client3 = new ModbusClient(controller, 3, { timeout: 2000, RSMode: 'RS485' }); // Uses 'com4'
await client1.connect();
await client2.connect();
await client3.connect();
const registers1 = await client1.readHoldingRegisters(0, 10, { type: 'uint16' });
console.log('Registers from slave 1:', registers1);
const registers2 = await client2.readHoldingRegisters(0, 10, { type: 'uint16' });
console.log('Registers from slave 2:', registers2);
const registers3 = await client3.readHoldingRegisters(0, 10, { type: 'uint16' });
console.log('Registers from slave 3:', registers3);
await client1.disconnect();
await client2.disconnect();
await client3.disconnect();
} catch (err) {
console.error('Modbus error:', err.message);
} finally {
// Disconnect all transports managed by the controller
await controller.disconnectAll();
}
}
modbusExample();
```
**Expected output (snippet):**
```bash
[14:30:15][INFO][TransportController] Transport "com3" added {"type":"node","slaveIds":[1, 2]}
[14:30:15][INFO][TransportController] Transport "com4" added {"type":"node","slaveIds":[3]}
[14:30:15][INFO][NodeSerialTransport] Serial port COM3 opened
[14:30:15][INFO][NodeSerialTransport] Serial port COM4 opened
[14:30:15][INFO] Transport connected { transport: 'NodeSerialTransport' } // For client 1
[COM3] Device 1: ONLINE // Output from device state handler
[14:30:15][INFO] Transport connected { transport: 'NodeSerialTransport' } // For client 2
[COM3] Device 2: ONLINE // Output from device state handler
[14:30:15][INFO] Transport connected { transport: 'NodeSerialTransport' } // For client 3
[COM4] Port: UP. Slaves affected: [ 3 ] // Output from port state handler
[14:30:15][INFO] Response received { slaveId: 1, funcCode: 3, responseTime: 50 }
Registers from slave 1: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[14:30:15][INFO] Response received { slaveId: 2, funcCode: 3, responseTime: 48 }
Registers from slave 2: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
[14:30:15][INFO] Response received { slaveId: 3, funcCode: 3, responseTime: 60 }
Registers from slave 3: [20, 21, 22, 23, 24, 25, 26, 27, 28, 29]
[14:30:16][INFO] Transport disconnected { transport: 'NodeSerialTransport' } // For client 1
[14:30:16][INFO] Transport disconnected { transport: 'NodeSerialTransport' } // For client 2
[14:30:16][INFO] Transport disconnected { transport: 'NodeSerialTransport' } // For client 3
[14:30:16][INFO][TransportController] Transport "com3" disconnected
[14:30:16][INFO][TransportController] Transport "com4" disconnected
```
> For Web: Use `type: 'web'` and provide the `SerialPort` instance obtained via `navigator.serial.requestPort()` to the `addTransport` options. The process for setting state handlers is the same.
<br>
# <span id="errors-types">Errors Types</span>
The errors.js module defines a hierarchy of error classes for Modbus operations. All classes inherit from the base `ModbusError (extends Error)`, allowing for easy catching in catch blocks (e.g., `catch (err) { if (err instanceof ModbusError) { ... } }`). These classes are used in **ModbusClient** (the previous module) for specific scenarios: **timeouts**, **CRC errors**, **Modbus exceptions**, etc.
**Key Features:**
- **Base Class:** ModbusError — common to all, with name = 'ModbusError'.
- **Specific Classes:** Each has a unique name and default message. ModbusExceptionError uses the EXCEPTION_CODES constants from `./constants/constants.js` to describe exceptions (e.g., 0x01 = 'Illegal Function').
- **Hierarchy:** All extend ModbusError, so instanceof **_ModbusError_** catches everything.
- **Usage:** Throw in code for custom errors or catch from the transport/client. Supports stack and message as standard Error.
- **Constants:** Depends on **_EXCEPTION_CODES_** (object { code: 'description' }).
The module exports classes. No initialization required—just import and use for throw/catch.
## Basic Error Classes
Each class has a constructor with an optional message. When throwing, the message, name, and stack (standard for Error) are displayed.
### 1. ModbusError(message)
Base class for all Modbus errors.
**Parameters:**
- `message (string, optional):` Custom message. Defaults to ''.
### 2. ModbusTimeoutError(message = 'Modbus request timed out')
Request timeout error.
### 3. ModbusCRCError(message = 'Modbus CRC check failed')
There was a CRC check error in the package.
### 4. ModbusResponseError(message = 'Invalid Modbus response')
Invalid response error (eg unexpected PDU length).
### 5. ModbusTooManyEmptyReadsError(message = 'Too many empty reads from transport')
Too many empty reads from transport (e.g., serial)
### 6. ModbusExceptionError(functionCode, exceptionCode)
Modbus exception error (response with funcCode | 0x80). Uses EXCEPTION_CODES for description.
**Parameters:**
- `functionCode (number):` Original funcCode (without 0x80).
- `exceptionCode (number):` Exception code (0x01–0xFF).
### 8. ModbusFlushError(message = 'Modbus operation interrupted by transport flush')
Error interrupting operation with transport flash (buffer clearing).
## Error Catching (General)
All classes are caught as ModbusError.
## Data Validation Errors
### 9. ModbusInvalidAddressError(address)
Invalid Modbus slave address (must be 0-247).
**Parameters:**
- `address (number):` Invalid address value.
### 10. ModbusInvalidFunctionCodeError(functionCode)
Invalid Modbus function code.
**Parameters:**
- `functionCode (number):` Invalid function code.
### 11. ModbusInvalidQuantityError(quantity, min, max)
Invalid register/coil quantity.
**Parameters:**
- `quantity (number):` Invalid quantity.
- `min (number):` Minimum allowed.
- `max (number):` Maximum allowed.
## Modbus Exception Errors
### 12. ModbusIllegalDataAddressError(address, quantity)
Modbus exception 0x02 - Illegal Data Address.
**Parameters:**
- `address (number):` Starting address.
- `quantity (number):` Quantity requested.
### 13. ModbusIllegalDataValueError(value, expected)
Modbus exception 0x03 - Illegal Data Value.
**Parameters:**
- `value (any):` Invalid value.
- `expected (string):` Expected format.
### 14. ModbusSlaveBusyError()
Modbus exception 0x04 - Slave Device Busy.
### 15. ModbusAcknowledgeError()
Modbus exception 0x05 - Acknowledge.
### 16. ModbusSlaveDeviceFailureError()
Modbus exception 0x06 - Slave Device Failure.
## Message Format Errors
### 17. ModbusMalformedFrameError(rawData)
Malformed Modbus frame received.
**Parameters:**
- `rawData (Buffer | Uint8Array):` Raw received data.
### 18. ModbusInvalidFrameLengthError(received, expected)
Invalid frame length.
**Parameters:**
- `received (number):