modbus-connect
v2.8.10
Published
Modbus RTU over Web Serial and Node.js SerialPort
Downloads
285
Maintainers
Keywords
Readme
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
- Installation
- Basic Usage
- Modbus Client
- Transport Controller
- Errors Types
- Polling Manager
- Slave Emulator
- Logger
- Utils
- Utils CRC
- Plugin System
- Tips for use
- Expansion
- CHANGELOG
Features
- Supports Modbus RTU over serial ports (Node.js) and Web Serial API (Browser).
- Automatic reconnection mechanisms (primarily in transport layer).
- Robust error handling with specific Modbus exception types.
- Integrated polling manager for scheduled data acquisition.
- Built-in logging with configurable levels and categories.
- Diagnostic tools for monitoring communication performance.
- Utility functions for CRC calculation, buffer manipulation, and data conversion.
- Slave emulator for testing purposes (without COM port).
- Plugin System: Extend client functionality with custom functions, data types, and CRC algorithms without modifying the library core.
Installation
npm install modbus-connectBasic Usage
Importing Modules
The library provides several entry points for different functionalities:
// 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:
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:
// 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:
await controller.addTransport('node-port-2', 'node', {
port: 'COM3',
writeTimeout: 500,
readTimeout: 500,
slaveIds: [5],
});If you do not specify values for
readTimeout/writeTimeoutduring initialization, the default parameter will be used - 1000 ms for both values
Creating a Client
const client = new ModbusClient(controller, 1, {
/* ...options */
});controller— TheTransportControllerinstance.slaveId— Device address (1..247). The controller will route requests to the correct transport.options—{ timeout, retryCount, retryDelay, plugins }
Connecting and Communicating
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.
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();
}Modbus Client
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
TransportControllerinstance, 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 (
crc16Modbusby 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:
const ModbusClient = require('modbus-connect/client');
const TransportController = require('modbus-connect/transport');Create an instance:
// 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:
await client.connect();Output (if level >= 'info'):
[04:28:57][INFO][NodeSerialTransport] Serial port COM3 openedDisconnect:
await client.disconnect();Output:
[05:53:17][INFO][NodeSerialTransport] Serial port COM3 closedLogging Controls
1. enableLogger(level = 'info')
Enables ModbusClient logging.
Example:
client.enableLogger('debug');Now all requests/errors will be logged.
2. disableLogger()
Disables (sets 'error').
Example:
client.disableLogger();3. setLoggerContext(context)
Adds a global context (e.g., { custom: 'value' }).
Example:
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.
const registers = await client.readHoldingRegisters(100, 2);
console.log(registers); // [1234, 5678] (array of numbers)Log output (if level >= 'debug'):
[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).
const floats = await client.readHoldingRegisters(200, 2, { type: 'float' });
console.log(floats); // [3.14159] (array of float)Example 3: Reading a string.
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:
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:
const response = await client.writeSingleRegister(400, 999);
console.log(response); // { address: 400, value: 999 }Log output:
[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:
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:
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:
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:
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:
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:
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:
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:
// 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:
console.log(client.diagnostics.getStats()); // { requests: 10, errors: 2, ... }Full usage example
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):
[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 closedOn error (timeout):
[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 timeoutTransport Controller
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 slaveIds via different transports, and provides load balancing and fault tolerance.
Key Features:
- Transport Management: Add, remove, connect, disconnect.
- Routing: Automatically routes requests from
ModbusClientto the correct transport based onslaveId. - Dynamic Assignment: Ability to assign new
slaveIds 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
NodeSerialTransportandWebSerialTransport. These transports useDeviceConnectionTrackerandPortConnectionTrackerto monitor the connection status of individual Modbus slaves and the physical port itself, providing detailed error types and messages.TransportControllermanages 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
const TransportController = require('modbus-connect/transport');Or in the browser:
import TransportController from 'modbus-connect/transport';Create an instance:
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 +slaveIdsarray).For 'web':{ port: SerialPort instance, ..., slaveIds: [3, 4] }(Web Serial Port instance +slaveIdsarray).slaveIds (number[], optional):An array ofslaveIds 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 assignedslaveIdsif the primary transport fails. Returns: Promise 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.
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):
[14:30:15][INFO][TransportController] Transport "com3" added {"type":"node","slaveIds":[13, 14]}Example 2: Add Web serial transport.
// 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):
[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
Example:
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
Example:
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:
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:
// 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 transportslaveId (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:
// 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:
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
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
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 to0(no read).timeout (number, optional): Timeout for reading the response, in milliseconds. Defaults to3000ms.
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:
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.isOpeninternally. 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:
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:
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 inaddTransport.
Returns: Promise<void>
Example:
// 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:
// 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
Example:
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.
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):
[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" disconnectedFor Web: Use
type: 'web'and provide theSerialPortinstance obtained vianavigator.serial.requestPort()to theaddTransportoptions. The process for setting state handlers is the same.
Errors Types
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.jsto 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):Bytes received.expected (number):Expected bytes.
19. ModbusInvalidTransactionIdError(received, expected)
Invalid transaction ID mismatch.
Parameters:
received (number):Received ID.expected (number):Expected ID.
20. ModbusUnexpectedFunctionCodeError(sent, received)
Unexpected function code in response.
Parameters:
sent (number):Sent function code.received (number):Received function code.
Connection Errors
21. ModbusConnectionRefusedError(host, port)
Connection refused by device.
Parameters:
host (string):Target host.port (number):Target port.
22. ModbusConnectionTimeoutError(host, port, timeout)
Connection timeout.
Parameters:
host (string):Target host.port (number):Target port.timeout (number):Timeout in ms.
23. ModbusNotConnectedError()
Operation attempted without connection.
24. ModbusAlreadyConnectedError()
Attempt to connect when already connected.
Buffer & Data Errors
25. ModbusBufferOverflowError(size, max)
Buffer exceeds maximum size.
Parameters:
size (number):Current size.max (number):Maximum allowed.
26. ModbusInsufficientDataError(received, required)
Not enough data received.
Parameters:
received (number):Bytes received.required (number):Bytes needed.
27. ModbusDataConversionError(data, expectedType)
Data type conversion failure.
Parameters:
data (any):Invalid data.expectedType (string):Expected type.
Gateway Errors
28. ModbusGatewayPathUnavailableError()
Gateway path unavailable (exception 0x0A).
29. ModbusGatewayTargetDeviceError()
Gateway target device failed to respond (exception 0x0B).
Polling Errors
30. PollingTaskAlreadyExistsError(id)
Polling task ID already registered.
Parameters:
id (string):Task ID.
31. PollingTaskNotFoundError(id)
Polling task ID not found.
Parameters:
id (string):Task ID.
Polling Manager
The PollingManager class is now integrated directly into the TransportController. You typically do not create instances of it manually. Instead, a separate manager is automatically created for each transport you add. This ensures that issues on one port (like timeouts) do not affect polling on other ports.
Key Features:
- Transport Isolation: Each transport has its own independent polling queue.
- Concurrency Safety: Resolves conflicts between automatic polling and manual Client requests using a shared mutex.
- No Resource ID: Tasks are simply added to a specific transport.
Dependencies:
- async-mutex for mutexes.
- Logger from ./logger for logging.
Logging levels: Disabled by default ('none'). Use the enable*Logger methods to activate.
Initialization
You do not need to instantiate this class manually. It is created automatically when you add a transport.
Pass the configuration in the 5th argument of addTransport:
const TransportController = require('modbus-connect/transport');
const controller = new TransportController();
// PollingManager is initialized internally here:
await controller.addTransport(
'my-transport',
'node',
{ port: 'COM1', slaveIds: [1] }, // Transport config
{}, // Reconnect config
{
// PollingManager config
defaultMaxRetries: 5,
defaultBackoffDelay: 2000,
defaultTaskTimeout: 10000,
logLevel: 'info',
}
);Task management methods
| METHOD | DESCRIPTION |
| --------------------------------------------- | ---------------------------------------------------------------------------------- |
| addPollingTask(transportId, opts) | Add a new polling task to a specific transport |
| removePollingTask(transportId, taskId) | Remove a task from a transport |
| updatePollingTask(transportId, taskId, opts) | Update an existing task (removes and recreates) |
| controlTask(transportId, taskId, action) | Control a specific task (start, stop, pause, resume) |
| controlPolling(transportId, action) | Control all tasks on a transport (startAll, stopAll, pauseAll, resumeAll) |
| getPollingStats(transportId) | Get stats for all tasks on a transport |
| getPollingQueueInfo(transportId) | Get detailed queue information for a transport |
Adding and managing Tasks
1. addPollingTask(transportId, options)
Adds a new task to the specified transport queue.
Parameters:
transportId (string):The ID of the transport.options (object):Task configuration.
controller.addPollingTask('my-transport', {
// Required parameters
id: string, // Unique task ID
interval: number, // Polling interval in ms
fn: Function | Function[], // Function(s) to execute
// Optional parameters
priority?: number, // Task priority (default: 0)
name?: string, // Human-readable task name
immediate?: boolean, // Run immediately (default: true)
maxRetries?: number, // Retry attempts
backoffDelay?: number, // Retry delay
taskTimeout?: number, // Timeout per function
// Callbacks (onData, onError, onStart, onStop, onFinish, etc.)
});Example 1: A simple task without a resource (independent).
// Add a task to the transport 'com3'
controller.addPollingTask('com3', {
id: 'read-voltage',
interval: 1000,
fn: () => client.readHoldingRegisters(0, 2),
onData: res => console.log('Voltage:', res),
});Output (logs if enabled; simulation):
[14:30:15][TRACE][PollingManager] Creating TaskController { id: 'sample-task', resourceId: undefined }
[14:30:15][TRACE][TaskController] TaskController trace log
[14:30:15][DEBUG][TaskController] TaskController created { id: 'sample-task', resourceId: undefined, priority: 0, interval: 2000, maxRetries: 2, backoffDelay: 1000, taskTimeout: 3000 }
[14:30:15][WARN][TaskController] TaskController warning log
[14:30:15][ERROR][TaskController] TaskController error log
[14:30:15][INFO][PollingManager] Task added successfully { id: 'sample-task', resourceId: undefined, immediate: true }
[14:30:16][INFO][TaskController] Task started
[14:30:16][DEBUG][TaskController] Executing task once
[14:30:16][DEBUG][TaskController] Transport flushed successfully (if there is transport)
[14:30:16][INFO][TaskController] Task execution completed { success: true, resultsCount: 1 }
Data obtained: [ 'Data received' ]
[14:30:18][DEBUG][TaskController] Scheduling next run (loop)
... (repeat every 2 seconds)Validation errors:
- If id is missing: Error: Task must have an
id - If a task with ID exists: Error: Polling task with id
sample-taskalready exists.
2. updatePollingTask(transportId, taskId, newOptions)
Updates an existing task by recreating it with new options.
Parameters:
- id (string): Task ID. - newOptions (object): New options (as in addTask, without id).
Example:
controller.updatePollingTask('com3', 'read-voltage', { interval: 5000 });Output:
[14:30:15][INFO][PollingManager] Updating task { id: 'sample-task', newOptions: { interval: 3000, fn: [Function] } }
[14:30:15][INFO][PollingManager] Task removed { id: 'sample-task', resourceId: undefined }
[14:30:15][INFO][PollingManager] Task added successfully { id: 'sample-task', resourceId: undefined, immediate: false }If the task does not exist: Error: Polling task with id
sample-taskdoes not exist.
3. removePollingTask(transportId, taskId)
Stops and removes the task from the transport.
Parameters:
- id (string).
Example:
controller.removePollingTask('com3', 'read-voltage');Output:
[14:30:15][INFO][TaskController] Task stopped
[14:30:15][INFO][PollingManager] Task removed { id: 'sample-task', resourceId: undefined }If it doesn't exist: a warning in the logs.
Managing Task State
1. controlTask(transportId, taskId, action)
Manages the state of a single task.
Parameters:
transportId (string)taskId (string)action (string):'start' | 'stop' | 'pause' | 'resume'
Example:
// Pause a specific task
controller.controlTask('com3', 'read-voltage', 'pause');
// Resume it later
controller.controlTask('com3', 'read-voltage', 'resume');Bulk Operations
1. controlPolling(transportId, action)
Manages the state of all tasks on a specific transport.
Parameters:
transportId (string)action (string):'startAll' | 'stopAll' | 'pauseAll' | 'resumeAll'
Example:
// Pause all polling on COM3 (e.g., before disconnecting or critical write)
controller.controlPolling('com3', 'pauseAll');
// Resume
controller.controlPolling('com3', 'resumeAll');Queues and the System
1. getPollingQueueInfo(transportId)
Returns information about the execution queue length and task states.
Example:
const info = controller.getPollingQueueInfo('com3');
console.log(info);
// { queueLength: 1, tasks: [{ id: 'task1', state: {...} }] }If the queue does not exist: null.
2. getPollingStats(transportId)
Returns detailed statistics for all tasks on the transport.
Example:
const stats = controller.getPollingStats('com3');
console.log(stats);
// { 'task1': { totalRuns: 10, totalErrors: 0, ... } }Output after enabling (with addTask): Logs from the corresponding components will become visible, as in the examples above.
Full usage example
const TransportController = require('modbus-connect/transport');
const ModbusClient = require('modbus-connect/client');
async function main() {
const controller = new TransportController();
// 1. Add transport (PollingManager created internally)
await controller.addTransport(
'com3',
'node',
{ port: 'COM3', baudRate: 9600, slaveIds: [1] },
{},
{ logLevel: 'debug' } // Enable polling logs here
);
await controller.connectAll();
const client = new ModbusClient(controller, 1);
// 2. Add task via Controller
controller.addPollingTask('com3', {
id: 'modbus-loop',
interval: 1000,
fn: () => client.readHoldingRegisters(0, 2),
onData: results => console.log('Data:', results),
onError: err => console.error('Error:', err.message),
});
// 3. Pause polling after 5 seconds
setTimeout(() => {
console.log('Pausing polling...');
controller.controlPolling('com3', 'pauseAll');
}, 5000);
// 4. Check stats
setInterval(() => {
console.log('Stats:', controller.getPollingStats('com3'));
}, 10000);
}
main();Expected output (snippet):
Polling started
[14:30:15][DEBUG][PollingManager] Creating new TaskQueue { resourceId: 'slave-1' }
[14:30:15][INFO][TaskController] Task started
[14:30:15][DEBUG][TaskQueue] Task enqueued { taskId: 'modbus-poll' }
[14:30:15][DEBUG][TaskController] Executing task once
Modbus data: { registers: [1,2,3], slaveId: 1 }
[14:30:18][DEBUG][TaskQueue] Task marked as ready { taskId: 'modbus-poll' }
... (Retry)
Modbus error: Modbus error (on error, with retry)
Stats: { totalTasks: 1, totalQueues: 1, queuedTasks: 0, tasks: { modbus-poll: { totalRuns: 5, ... } } }Slave Emulator
The SlaveEmulator class is a Modbus device emulator (slave) that simulates slave behavior in the RTU protocol. It stores the states of coils, discrete inputs, and holding/input registers in a Map (for sparse addresses), supports address/function exceptions, infinite value changes (infinityChange), and processing of full RTU frames (handleRequest). This class is designed for testing and debugging Modbus clients without real hardware.
Key Features:
- Data Storage: Map of addresses (0–65535); default values are 0/false.
- Validation: Addresses (0–65535), quantity (1–125/2000), values (0–65535 for registers, boolean for coils).
- Exceptions: setException to simulate ModbusExceptionError (by funcCode+address).
- Infinity tasks: Automatically change values by interval (random in range).
- RTU processing: handleRequest: CRC check, slaveAddr, funcCode; returns a Uint8Array response.
- Logging: Optional (loggerEnabled); uses Logger (category 'SlaveEmulator').
- Function support: Read/Write coils/registers (01, 02, 03, 04, 05, 06, 0F, 10); throws Illegal Function for others.
The class is exported as SlaveEmulator. Asynchronous for connect/disconnect.
Initialization
Include the module:
const SlaveEmulator = require('modbus-connect/slave-emulator');
const Logger = require('modbus-connect/logger');Create an instance:
const options = {
loggerEnabled: true, // Enable logging (default: false)
};
const emulator = new SlaveEmulator(1, options); // slaveAddress=1Output during initialization (if loggerEnabled): No explicit output in the constructor.
Enable\Disable logging:
emulator.enableLogger(); // Enable (if false)
emulator.disableLogger(); // DisableConnecting:
await emulator.connect();Output (logs):
[14:30:15][INFO][SlaveEmulator] Connecting to emulator...
[14:30:15][INFO][SlaveEmulator] ConnectedDisabling:
await emulator.disconnect();Output:
[14:30:15][INFO][SlaveEmulator] Disconnecting from emulator...
[14:30:15][INFO][SlaveEmulator] DisconnectedError in constructor:
const invalid = new SlaveEmulator(300); // >247Output:
Error: Slave address must be a number between 0 and 247Main Methods
1. infinityChange({ typeRegister, register, range, interval })
Starts an infinite change of a value (random in range) over an interval.
Parameters:
typeRegister (string):'Holding', 'Input', 'Coil', 'Discrete'.register (number):Address (0–65535).range (number[]):[min, max] for registers; ignored for coils (random boolean).interval (number):ms (positive).
Returns: void. Errors: Invalid params, range min>max, invalid type.
Example:
emulator.infinityChange({
typeRegister: 'Holding',
register: 100,
range: [0, 1000],
interval: 1000,
});
// Stop
emulator.stopInfinityChange({ typeRegister: 'Holding', register: 100 });Output (logs, level >= 'info'):
[14:30:15][INFO][SlaveEmulator] Infinity change started { typeRegister: 'Holding', register: 100, interval: 1000 }
[14:30:16][DEBUG][SlaveEmulator] Infinity change updated { typeRegister: 'Holding', register: 100, value: 456 }
[14:30:16][DEBUG][SlaveEmulator] Infinity change stopped { typeRegister: 'Holding', register: 100 }2. stopInfinityChange({ typeRegister, register })
Stops a task based on a key.
Parameters:
- typeRegister (string), register (number). Returns: void. Example: See above.
3. setException(functionCode, address, exceptionCode)
Sets an exception for funcCode+address.
Parameters:
functionCode (number):e.g., 3.address (number):0–65535.exceptionCode (number):e.g., 1 (Illegal Fun
