npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

modbus-connect

v2.8.10

Published

Modbus RTU over Web Serial and Node.js SerialPort

Downloads

285

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

  • 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-connect

Basic 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/writeTimeout during initialization, the default parameter will be used - 1000 ms for both values

Creating a Client

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

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 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:

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 opened

Disconnect:

await client.disconnect();

Output:

[05:53:17][INFO][NodeSerialTransport] Serial port COM3 closed

Logging 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., 0x12341234         | | 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 closed

On 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 timeout

Transport 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 ModbusClient to the correct transport based on slaveId.
  • 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 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

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 + slaveIds array).
    • For 'web': { port: SerialPort instance, ..., slaveIds: [3, 4] } (Web Serial Port instance + slaveIds array).
    • slaveIds (number[], optional): An array of slaveIds 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 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 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:

// 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 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:

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:

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 in addTransport.

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" 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.

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.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): 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-task already 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-task does 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=1

Output during initialization (if loggerEnabled): No explicit output in the constructor.

Enable\Disable logging:

emulator.enableLogger(); // Enable (if false)
emulator.disableLogger(); // Disable

Connecting:

await emulator.connect();

Output (logs):

[14:30:15][INFO][SlaveEmulator] Connecting to emulator...
[14:30:15][INFO][SlaveEmulator] Connected

Disabling:

await emulator.disconnect();

Output:

[14:30:15][INFO][SlaveEmulator] Disconnecting from emulator...
[14:30:15][INFO][SlaveEmulator] Disconnected

Error in constructor:

const invalid = new SlaveEmulator(300); // >247

Output:

Error: Slave address must be a number between 0 and 247

Main 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