UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

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