UNPKG

modbus-connect

Version:

Modbus RTU over Web Serial and Node.js SerialPort

1,073 lines (940 loc) β€’ 72.6 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. ## Navigating through documentation - [Library Structure](#library-structure) - [Basic Usage](#basic-usage) - [Work via RS485](#work-via-rs485) - [Summary type data](#type-data) - [Main Classes and Methods](#main-classes-and-methods) - [Modbus Functions](#modbus-functions) - [Packet building & Parsing](#packet-building-and-parsing) - [Diagnostics & Error Handling](#diagnostics-and-error-handling) - [Logger](#logger) - [Utilities](#utilities) - [CRC](#crc) - [Error Handling](#error-handling) - [Polling Manager](#polling-manager) - [Slave Emulator](#slave-emulator) - [Tips for use](#tips-for-use) - [Expansion](#expansion) - [CHANGELOG](#changelog) --- <br> ## 1. πŸ“ <span id="library-structure">Library Structure</span> - **function-codes/** β€” PDU implementations for all Modbus functions (register/bit read/write, special functions). - **transport/** β€” Transport adapters (Node.js SerialPort, Web Serial API), auto-detection helpers. - **utils/** β€” Utilities: CRC, diagnostics, and helpers. - **polling-manager.js** - A tool for continuously polling a device at a specified interval - **client.js** β€” Main `ModbusClient` class for Modbus RTU devices. - **constants.js** β€” Protocol constants (function codes, errors, etc.). - **errors.js** β€” Error classes for robust exception handling, including `ModbusFlushError`. - **logger.js** β€” Event logging utilities. - **packet-builder.js** β€” ADU packet construction/parsing (with CRC). <br> ## 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. ## Intallation ```bash npm install modbus-connect ``` ## 2. πŸš€ <span id="basic-usage">Basic Usage</span> Please read [Important Note](#important-note) before use. ### Importing Modules The library provides several entry points for different functionalities: ```js // Main Modbus client import ModbusClient from 'modbus-connect/client'; // Polling manager for scheduled tasks import PollingManager from 'modbus-connect/polling-manager'; // Transport factory for creating connections import { createTransport } 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 Transports are the underlying communication layers. The library provides a factory function to simplify their creation across different environments. **Node.js Serial Port:** ```js const transport = await createTransport('node', { port: '/dev/ttyUSB0', // or 'COM3' on Windows baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none' }); ``` **Web Serial (Recommended with `port` for robust reconnection):** For reliable reconnection, especially after physical device disconnection, it's highly recommended to use a `port` function. This allows the transport to request a fresh `SerialPort` instance when needed. ```js // Function to request a SerialPort instance, typically called from a user gesture // or stored from an initial user selection. const getSerialPort = async () => { // In a real application, you might store the port object after the first user selection // and return it here, or request a new one if needed. // Example for initial request (requires user gesture): // const port = await navigator.serial.requestPort(); // Store port for future use... // return port; // Example returning a previously stored/stale port (less robust for reconnection): // return storedSerialPortInstance; // Example forcing a new request (requires user gesture, best for manual reconnection): const port = await navigator.serial.requestPort(); // Update stored reference if needed // storedSerialPortInstance = port; return port; }; const transport = await createTransport('web', { port: getSerialPort, // Recommended for robustness // OR, for simpler cases (less robust reconnection): // port: serialPortInstance, // Directly pass a SerialPort object baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none', // Optional reconnection parameters for WebSerialTransport reconnectInterval: 3000, // ms maxReconnectAttempts: 5, // Set to Infinity for continuous attempts (use with caution) maxEmptyReadsBeforeReconnect: 10 // Triggers reconnect if data stops flowing }); ``` To set the `read/write` speed parameters, it is necessary to specify parameters such as `writeTimeout` and `readTimeout` during initialization. Example: ```js const transport = await createTransport('node', { writeTimeout: 500, // your value readTimeout: 500 // your value }) ``` > If you do not specify values ​​for `readTimeout/writeTimeout` during initialization, the default parameter will be used - 1000 ms for both values ### Creating a Client ```js const client = new ModbusClient(transport, slaveId = 1, options = {}) ``` - `transport` β€” transport object (see below) - `slaveId` β€” device address (1..247) - `options` β€” `{ timeout, retryCount, retryDelay }` ### Connecting and Communicating ```js try { await client.connect(); console.log('Connected to device'); // Reading holding registers const registers = await client.readHoldingRegisters(0, 10); // Start at address 0, read 10 registers console.log('Registers:', registers); // Writing a single register await client.writeSingleRegister(5, 1234); // Write 1234 to register 5 } catch (error) { console.error('Communication error:', error.message); } finally { await client.disconnect(); } ``` ### <span id="work-via-rs485">Work via RS485</span> In order to work via RS485, you first need to connect the COM port. ```js const transport = await createTransport('node', { port: 'COM3', baudRate: 9600, dataBits: 8, stopBits: 1, parity: 'none', writeTimeout: 500, readTimeout: 500 }) ``` Then, if you have several devices connected via RS485 in series, you need to create a `ModbusClient` for each one. ```js const device_1 = new ModbusClient(transport, 38, { timeout: 1000 }); const device_2 = new ModbusClient(transport, 51, { timeout: 1000 }); ``` Then do whatever you need - read Holding/Input registers, write registers, but for each device separately. Example: ```js try { await transport.connect(); 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(); } ``` ### Hadnling Reconnection **Automatic Reconnection:** The `WebSerialTransport` and `NodeSerialTransport` now include built-in automatic reconnection logic. This handles scenarios like temporary cable glitches or device resets. You can configure this behavior using options like `reconnectInterval` and maxReconnectAttempts when creating the transport. **Manual Reconnection (Web environment)** Due to browser security policies, automatic reconnection cannot always call `navigator.serial.requestPort()` if the physical device is disconnected and reconnected. In such cases, or if automatic reconnection fails, a manual reconnection initiated by a user action is required. ```js // Example function to be called by a UI button click (user gesture) async function handleManualReconnect() { try { // Ensure any previous connection is cleanly closed if (client && transport) { try { await client.disconnect(); // This calls transport.disconnect() } catch (e) { console.warn("Error disconnecting previous connection:", e.message); } } // Create a new transport using a port that requests a port via user gesture const newTransport = await createTransport('web', { port: async () => { // This call is now valid because it's within a user gesture handler const new_port = await navigator.serial.requestPort(); // Update any stored reference // storedSerialPortInstance = port; return new_port; }, baudRate: 9600, // Use same settings as before // ... other settings maxReconnectAttempts: 0 // Disable auto-reconnect in new transport if desired }); // Create a new client const newClient = new ModbusClient(newTransport, 1); // Use correct slave ID // Connect await newClient.connect(); console.log('Manually reconnected!'); } catch (err) { console.error("Manual reconnection failed:", err.message); // Handle user cancellation or other errors } } ``` ### 🧾 <span id="type-data">Summary type data</span> | Type | Size (regs) | DataView Method | Endian / Swap | Notes | | --------------- | ----------- | -------------------- | --------------------- | ---------------------------------------------- | | `uint16` | 1 | `getUint16` | Big Endian | No changes | | `int16` | 1 | `getInt16` | Big Endian | | | `uint32` | 2 | `getUint32` | Big Endian | Standard 32-bit read | | `int32` | 2 | `getInt32` | Big Endian | | | `float` | 2 | `getFloat32` | Big Endian | IEEE 754 single precision float | | `uint32_le` | 2 | `getUint32` | Little Endian | | | `int32_le` | 2 | `getInt32` | Little Endian | | | `float_le` | 2 | `getFloat32` | Little Endian | | | `uint32_sw` | 2 | `getUint32` | Word Swap | Swap words (e.g., 0xAABBCCDD β†’ 0xCCDDAABB) | | `int32_sw` | 2 | `getInt32` | Word Swap | | | `float_sw` | 2 | `getFloat32` | Word Swap | | | `uint32_sb` | 2 | `getUint32` | Byte Swap | Swap bytes (e.g., 0xAABBCCDD β†’ 0xBBAADDCC) | | `int32_sb` | 2 | `getInt32` | Byte Swap | | | `float_sb` | 2 | `getFloat32` | Byte Swap | | | `uint32_sbw` | 2 | `getUint32` | Byte + Word Swap | Swap bytes and words (0xAABBCCDD β†’ 0xDDCCBBAA) | | `int32_sbw` | 2 | `getInt32` | Byte + Word Swap | | | `float_sbw` | 2 | `getFloat32` | Byte + Word Swap | | | `uint32_le_sw` | 2 | `getUint32` | LE + Word Swap | Little Endian with Word Swap | | `int32_le_sw` | 2 | `getInt32` | LE + Word Swap | | | `float_le_sw` | 2 | `getFloat32` | LE + Word Swap | | | `uint32_le_sb` | 2 | `getUint32` | LE + Byte Swap | Little Endian with Byte Swap | | `int32_le_sb` | 2 | `getInt32` | LE + Byte Swap | | | `float_le_sb` | 2 | `getFloat32` | LE + Byte Swap | | | `uint32_le_sbw` | 2 | `getUint32` | LE + Byte + Word Swap | Little Endian with Byte + Word Swap | | `int32_le_sbw` | 2 | `getInt32` | LE + Byte + Word Swap | | | `float_le_sbw` | 2 | `getFloat32` | LE + Byte + Word Swap | | | `uint64` | 4 | `getUint32` + BigInt | Big Endian | Combined BigInt from high and low parts | | `int64` | 4 | `getUint32` + BigInt | Big Endian | Signed BigInt | | `double` | 4 | `getFloat64` | Big Endian | IEEE 754 double precision float | | `uint64_le` | 4 | `getUint32` + BigInt | Little Endian | | | `int64_le` | 4 | `getUint32` + BigInt | Little Endian | | | `double_le` | 4 | `getFloat64` | Little Endian | | | `hex` | 1+ | β€” | β€” | Returns array of HEX strings per register | | `string` | 1+ | β€” | Big Endian (Hi β†’ Lo) | Each 16-bit register β†’ 2 ASCII chars | | `bool` | 1+ | β€” | β€” | 0 β†’ false, nonzero β†’ true | | `binary` | 1+ | β€” | β€” | Each register converted to 16 boolean bits | | `bcd` | 1+ | β€” | β€” | BCD decoding from registers | ### πŸ“Œ Expanded Usage Examples: | Example usage | Description | | ------------------- | --------------------------------------------------------------------------- | | `type: 'uint16'` | Reads registers as unsigned 16-bit integers (default no byte swapping) | | `type: 'int16'` | Reads registers as signed 16-bit integers | | `type: 'uint32'` | Reads every 2 registers as unsigned 32-bit big-endian integers | | `type: 'int32'` | Reads every 2 registers as signed 32-bit big-endian integers | | `type: 'float'` | Reads every 2 registers as 32-bit IEEE 754 floats (big-endian) | | `type: 'uint32_le'` | Reads every 2 registers as unsigned 32-bit little-endian integers | | `type: 'int32_le'` | Reads every 2 registers as signed 32-bit little-endian integers | | `type: 'float_le'` | Reads every 2 registers as 32-bit IEEE 754 floats (little-endian) | | `type: 'uint32_sw'` | Reads every 2 registers as unsigned 32-bit with word swap | | `type: 'int32_sb'` | Reads every 2 registers as signed 32-bit with byte swap | | `type: 'float_sbw'` | Reads every 2 registers as float with byte+word swap | | `type: 'hex'` | Returns an array of hex strings, e.g., `["0010", "FF0A"]` | | `type: 'string'` | Converts registers to ASCII string (each register = 2 chars) | | `type: 'bool'` | Returns an array of booleans, 0 = false, otherwise true | | `type: 'binary'` | Returns array of 16-bit boolean arrays per register (each bit separately) | | `type: 'bcd'` | Decodes BCD-encoded numbers from registers, e.g., `0x1234` β†’ `1234` | | `type: 'uint64'` | Reads 4 registers as a combined unsigned 64-bit integer (BigInt) | | `type: 'int64_le'` | Reads 4 registers as signed 64-bit little-endian integer (BigInt) | | `type: 'double'` | Reads 4 registers as 64-bit IEEE 754 double precision float (big-endian) | | `type: 'double_le'` | Reads 4 registers as 64-bit IEEE 754 double precision float (little-endian) | <br> ## 3. πŸ—οΈ <span id="main-classes-and-methods">Main Classes and Methods</span> **Methods (basic):** - `connect()` / `disconnect()` β€” open/close connection - `readHoldingRegisters(startAddress, quantity, timeout?)` β€” read holding registers - `readInputRegisters(startAddress, quantity, timeout?)` β€” read input registers - `writeSingleRegister(address, value, timeout?)` β€” write a single register - `writeMultipleRegisters(startAddress, values, timeout?)` β€” write multiple registers - `readCoils(startAddress, quantity, timeout?)` β€” read discrete outputs (coils) - `readDiscreteInputs(startAddress, quantity, timeout?)` β€” read discrete inputs - `writeSingleCoil(address, value, timeout?)` β€” write a single coil - `writeMultipleCoils(startAddress, values, timeout?)` β€” write multiple coils - `reportSlaveId(timeout?)` β€” get device identifier - `readDeviceIdentification(slaveId, categoryId, objectId)` - read device identification - `getDiagnostics()` β€” get communication statistics - `resetDiagnostics()` β€” reset statistics **Methods for SGM-130** - `writeDeviceComment(channel, comment, timeout?)` - write comment to device channel (**SGM-130 only**) - `readFileLength(fileName)` - get archive file length (**SGM-130 only**) - `openFile(fileName)` - open archive file (**SGM-130 only**) - `closeFile()` - close archive file (**SGM-130 only**) - `restartController()` - restart controller (**SGM-130 only**) - `getControllerTime(options = {})` - get current controller date/time (**SGM-130 only**) - `readDeviceComment(channel, timeout?)` β€” get device comment (**SGM-130 only**) - `setControllerTime(time, options = {})` - set current controller date/time (**SGM-130 only**) <br> ## 4. 🧩 <span id="modbus-functions">Modbus Functions</span> The `function-codes/` directory contains all standard and custom Modbus PDU builders/parsers. **Standard Functions** | HEX | Name | |:---:|------| | 0x03 | Read Holding Registers | | 0x04 | Read Input Registers | | 0x10 | Write Multiple Registers | | 0x06 | Write Single Register | | 0x01 | Read Coils | | 0x02 | Read Discrete Inputs | | 0x05 | Write Single Coil | | 0x0F | Write multiple Coils | | 0x2B | Read Device Identification | | 0x11 | Report Slave ID | **Custom Functions (SGM-130)** | HEX | Name | |:---:|------| | 0x14 | Read Device Comment | | 0x15 | Write Device Comment | | 0x52 | Read File Length | | 0x55 | Open File | | 0x57 | Close File | | 0x5C | Restart Controller | | 0x6E | Get Controller Time | | 0x6F | Set Controller Time | ### Each file exports two functions: - `build...Request(...)` β€” builds a PDU request - `parse...Response(pdu)` β€” parses the response ### Example: manual PDU building ```js const { buildReadHoldingRegistersRequest } = require('./function-codes/read-holding-registers.js'); const pdu = buildReadHoldingRegistersRequest(0, 2); ``` <br> ## 5. πŸ“¦ <span id="packet-building-and-parsing">Packet Building & Parsing</span> **packet-builder.js** - **buildPacket(slaveAddress, pdu)** β€” Adds slaveId and CRC, returns ADU - **parsePacket(packet)** β€” Verifies CRC, returns { slaveAddress, pdu } **Example:** ```js const { buildPacket, parsePacket } = require('./packet-builder.js'); const adu = buildPacket(1, pdu); const { slaveAddress, pdu: respPdu } = parsePacket(adu); ``` <br> ## 6. πŸ“Š <span id="diagnostics-and-error-handling">Diagnostics & Error Handling</span> - **diagnostics.js** β€” The `Diagnostics` class collects detailed statistics on Modbus communication, including requests, errors, response times, and data transfer volumes. - **errors.js** β€” The module defines several custom error classes for specific Modbus communication issues: - `ModbusTimeoutError` - `ModbusCRCError` - `ModbusResponseError` - `ModbusTooManyEmptyReadsError` - `ModbusExceptionError` - `ModbusFlushError` β€” Thrown when an operation is interrupted by a transport `flush()`. Diagnostics Example: ```js const stats = client.getStats(); console.log(stats); ``` Consolidated table of methods from the Diagnostics class | Method | Description | |--------|-------------| |constructor(options) | Initializes a diagnostics instance with optional settings like error thresholds and slave IDs. |reset() | Resets all statistics and counters to their initial state. Use this to start a fresh collection of data. |resetStats(metrics) | Resets a specific subset of statistics. Accepts an array of strings (e.g., ['errors', 'responseTimes']) to reset only those metrics. |destroy() | Destroys the diagnostics instance and clears resources, including pausing the logger. |recordRequest(slaveId, funcCode) | Records a new request event, incrementing the total request counter and tracking the timestamp. |recordRetry(attempts, slaveId, funcCode) | Logs retry attempts, adding to the total retry count. |recordRetrySuccess(slaveId, funcCode) | Records a successful operation after a retry. |recordFunctionCall(funcCode, slaveId) | Tracks the frequency of each Modbus function code call. |recordSuccess(responseTimeMs, slaveId, funcCode) | Logs a successful response, updating response time metrics (last, min, max, average). |recordError(error, options) | Records an error, classifying it (e.g., timeout, CRC, Modbus exception), and tracks the error message. |recordDataSent(byteLength, slaveId, funcCode) | Records the number of bytes sent in a request. |recordDataReceived(byteLength, slaveId, funcCode) | Records the number of bytes received in a response. |getStats() | Returns a comprehensive JSON object containing all collected statistics. This is the primary method for accessing all data. |printStats() | Prints a human-readable, formatted report of all statistics directly to the console via the logger. |analyze() | Analyzes the current statistics and returns an object containing warnings if any metrics exceed their predefined thresholds. |serialize() | Returns a JSON string representation of all statistics. |toTable() | Converts the statistics into an array of objects for tabular presentation. |mergeWith(other) | Combines the statistics from another Diagnostics instance into the current one. Key Properties & Metrics Tracked: - Performance Metrics: - `uptimeSeconds`: The duration since the diagnostics instance was created. - `averageResponseTime`: Average response time for successful requests. - `averageResponseTimeAll`: Average response time including both successful and failed requests. - `requestsPerSecond`: Real-time calculation of requests per second. - Request & Response Counters: - `totalRequests`: Total number of sent requests. - `successfulResponses`: Total number of successful responses. - `errorResponses`: Total number of failed responses. - `totalRetries`: Total number of retry attempts. - `totalRetrySuccesses`: Number of requests that succeeded on a retry attempt. - `totalSessions`: The number of times the diagnostics were initialized (`constructor`) or reset (`reset()`). - Error Classification: - `timeouts`: Count of Modbus timeout errors. - `crcErrors`: Count of CRC (Cyclic Redundancy Check) errors. - `modbusExceptions`: Count of responses with a Modbus exception code. - `exceptionCodeCounts`: A map showing the count for each specific Modbus exception code. - `lastErrors`: A list of the 10 most recent error messages. - `commonErrors`: A list of the top 3 most frequently occurring errors by message. - Data Transfer: - `totalDataSent`: Total bytes sent to the Modbus device. - `totalDataReceived`: Total bytes received from the Modbus device. - Timestamps: - `lastRequestTimestamp`: ISO timestamp of the last sent request. - `lastSuccessTimestamp`: ISO timestamp of the last successful response. - `lastErrorTimestamp`: ISO timestamp of the last error. The `Diagnostics` class now provides a more comprehensive set of tools for monitoring and debugging Modbus communication, offering both real-time metrics and a detailed history of errors and activity. <br> ## 7. πŸ›  <span id="logger">Logger</span> `logger.js` is a powerful logging utility designed for formatted console output in Modbus applications. It supports: - **Log levels**: `trace`, `debug`, `info`, `warn`, `error` - **Colored output**: With customizable colors for each level and exceptions - **Nested groups**: For organizing sequential operations - **Global and contextual data**: Including `slaveId`, `funcCode`, `exceptionCode`, `address`, `quantity`, `responseTime` - **Output buffering**: With configurable flush intervals and rate limiting - **Categorical loggers**: For module-specific logging - **Filtering and highlighting**: By `slaveId`, `funcCode`, or `exceptionCode` - **Statistics and debugging**: Via `summary` and `inspectBuffer` - **Real-time monitoring**: Using the `watch` feature - **Custom formatting**: For context fields like `slaveId` or `funcCode` ### πŸ“¦ Import ```js const logger = require('modbus-connect/logger'); ``` ### πŸ”Š Basic Logging Log messages at different levels with optional data: ```js logger.trace('Low-level packet details'); logger.debug('Debug message'); logger.info('Informational message', [123, 456]); logger.warn('Warning message'); logger.error('Error message', new Error('Timeout')); ``` Output example: ```bash [06:00:00][TRACE] Low-level packet details [06:00:00][INFO] Informational message [123, 456] [06:00:00][ERROR] Timeout Error: Timeout at ... ``` ### πŸ“¦ Logging with Context Pass a context object as the last argument to include Modbus-specific details: ```js logger.info('Reading registers', { slaveId: 1, funcCode: FUNCTION_CODES.READ_HOLDING_REGISTERS, address: 100, quantity: 10, responseTime: 42 }); logger.error('Modbus exception', { slaveId: 1, funcCode: FUNCTION_CODES.READ_HOLDING_REGISTERS, exceptionCode: 1 }); ``` Output: ```bash [06:00:00][INFO][S:1][F:0x03/ReadHoldingRegisters][A:100][Q:10][RT:42ms] Reading registers [06:00:00][ERROR][S:1][F:0x03/ReadHoldingRegisters][E:1/Illegal Function] Modbus exception ``` ### πŸ– Managing Log Levels Set the global log level or check the current state: ```js logger.setLevel('trace'); // 'trace' | 'debug' | 'info' | 'warn' | 'error' console.log(logger.getLevel()); // => 'trace' console.log(logger.isEnabled()); // => true ``` ### 🚫 Enabling/Disabling Logger ```js logger.disable(); // Disable all logging logger.enable(); // Re-enable logging ``` ### 🎨 Colors Disable colored output if needed: ```js logger.disableColors(); ``` ### 🧡 Log Groups Organize logs with nested groups: ```js logger.group(); logger.info('Start of session'); logger.group(); logger.debug('Nested operation'); logger.groupEnd(); logger.groupEnd(); ``` ### 🌐 Global Context Set or extend global context for all logs: ```js logger.setGlobalContext({ transport: 'TCP', slaveId: 1 }); logger.addGlobalContext({ device: 'SGM130' }); logger.setTransportType('RTU'); // Shortcut for transport ``` ### πŸ”„ Output Buffering Control buffering and flush interval: ```js logger.setBuffering(true); // Enable buffering (default, flushes every 300ms) logger.setBuffering(false); // Immediate output logger.setFlushInterval(500); // Set flush interval to 500ms logger.flush(); // Manually flush buffer ``` > *"Buffer size is capped at 1000 entries to prevent memory issues."* ### πŸ“ˆ Rate Limiting Limit log frequency to avoid console flooding: ```js logger.setRateLimit(50); // Limit to one log every 50ms (except warn/error) ``` ### πŸ“ Categorical Loggers Create named loggers for module-specific logging: ```js const transportLog = logger.createLogger('transport'); transportLog.info('Connected'); // Adds [transport] to context transportLog.setLevel('debug'); // Set level for this logger transportLog.pause(); // Temporarily disable transportLog.resume(); // Re-enable ``` ### πŸ’₯ Immediate Warn/Error Output `warn` and `error` logs are always output immediately, even with buffering enabled: ```js logger.error('Critical failure', { slaveId: 1, exceptionCode: 1 }); ``` ### πŸ” Filtering Logs Mute logs based on `slaveId`, `funcCode`, or `exceptionCode`: ```js logger.mute({ slaveId: 1, funcCode: FUNCTION_CODES.READ_COILS }); logger.info('No output', { slaveId: 1, funcCode: FUNCTION_CODES.READ_COILS }); logger.unmute({ slaveId: 1 }); ``` ### 🌟 Highlighting Logs Highlight logs matching specific conditions (e.g., errors with `exceptionCode`): ```js logger.highlight({ exceptionCode: 1 }); // Highlight Illegal Function errors logger.error('Highlighted', { slaveId: 1, funcCode: 0x03, exceptionCode: 1 }); logger.clearHighlights(); // Clear all highlights ``` >*"Highlighted logs use a red background for visibility."* ### πŸ‘€ Real-Time Monitoring Monitor logs in real-time with a callback: ```js logger.watch(log => { if (log.context.slaveId === 1) console.log('Watched:', log.level, log.args); }); logger.clearWatch(); // Stop watching ``` ### πŸ§ͺ Inspecting Buffer View the current buffer contents: ```js logger.inspectBuffer(); ``` Output: ```bash === Log Buffer Contents === [0] [06:00:00][INFO][S:1][F:0x03/ReadHoldingRegisters] Request sent [1] [06:00:00][DEBUG][S:1][F:0x03/ReadHoldingRegisters] Packet sent Buffer Size: 2/1000 ========================== ``` ### πŸ“Š Viewing Statistics Display detailed logging statistics: ```js logger.summary(); ``` Output: ```bash === Logger Summary === Trace Messages: 5 Debug Messages: 10 Info Messages: 50 Warn Messages: 3 Error Messages: 2 Total Messages: 70 By Slave ID: { "1": 50, "2": 20 } By Function Code: { "3/ReadHoldingRegisters": 40, "6/WriteSingleRegister": 20, "17/ReportSlaveId": 10 } By Exception Code: { "1/Illegal Function": 2 } Buffering: Enabled (Interval: 300ms) Rate Limit: 100ms Buffer Size: 0/1000 Current Level: info Categories: {"transport": "debug"} Filters: slaveId=[], funcCode=[], exceptionCode=[] Highlights: [{"exceptionCode": 1}] ===================== ``` ### ✍️ Custom Formatters Customize how context fields are displayed: ```js logger.setCustomFormatter('slaveId', id => `Device${id}`); logger.setCustomFormatter('funcCode', code => { const name = Object.keys(FUNCTION_CODES).find(k => FUNCTION_CODES[k] === code) || 'Unknown'; return name; }); logger.info('Test', { slaveId: 1, funcCode: FUNCTION_CODES.READ_HOLDING_REGISTERS }); ``` Output: ```bash [06:00:00][INFO][S:Device1][F:ReadHoldingRegisters] Test ``` ### πŸ–Œ Custom Log Format Configure which fields appear in the log header: ```js logger.setLogFormat(['timestamp', 'level', 'slaveId', 'funcCode']); ``` ### πŸ§ͺ Usage Example ```js const logger = require('modbus-connect/logger'); logger.setLevel('trace'); logger.setGlobalContext({ transport: 'TCP', slaveId: 1 }); logger.setLogFormat(['timestamp', 'level', 'slaveId', 'funcCode', 'exceptionCode']); logger.setCustomFormatter('slaveId', id => `Device${id}`); logger.group(); logger.info('Starting Modbus session'); const comm = logger.createLogger('comm'); comm.trace('Opening port COM3'); logger.highlight({ exceptionCode: EXCEPTION_CODES.IllegalFunction }); logger.error('Modbus exception', { slaveId: 1, funcCode: FUNCTION_CODES.READ_HOLDING_REGISTERS, exceptionCode: EXCEPTION_CODES.IllegalFunction }); logger.watch(log => console.log('Watched:', log.level, log.args)); logger.info('Response received', { responseTime: 48 }); logger.groupEnd(); logger.summary(); ``` Output: ```bash [06:00:00][INFO][S:Device1][F:0x03/ReadHoldingRegisters] Starting Modbus session [06:00:00][TRACE][S:Device1][F:0x03/ReadHoldingRegisters][comm] Opening port COM3 [06:00:00][ERROR][S:Device1][F:0x03/ReadHoldingRegisters][E:1/Illegal Function] Modbus exception [06:00:00][INFO][S:Device1][F:0x03/ReadHoldingRegisters][RT:48ms] Response received === Logger Summary === ... ``` ### πŸ“Œ Tips Logger - Use `trace` for low-level debugging (e.g., packet dumps). - Leverage `exceptionCode` in context to log Modbus errors clearly. - Use `highlight` to focus on critical issues like `Illegal Function` errors. - Monitor specific devices with `watch` or `mute` for selective logging. - Check `summary` to analyze log distribution by `slaveId`, `funcCode`, or `exceptionCode`. - Disable buffering for real-time debugging or adjust `flushInterval` for performance. - Use short log format (`setLogFormat(['timestamp', 'level'])`) for minimal output. <br> ## 8. πŸ›  <span id="utilities">Utilities</span> - **crc.js** β€” CRC implementations (Modbus, CCITT, 1-wire, DVB-S2, XModem, etc.) - **utils.js** β€” Uint8Array helpers, number conversions, hex string utilities - **diagnostics.js** - Diagnostics class collects stats (requests, errors, response times, etc.) <br> ## 9. <span id="crc">CRC</span> **All types of CRC calculations** | Name | Polynomial | Initial Value (init) | Reflection (RefIn/RefOut) | Final XOR | CRC Size | Result Byte Order | Notes | |------------------|-------------|---------------------------|---------------------------|-------------------|------------|------------------------|-----------------------------------| | **crc16Modbus** | 0x8005 (reflected 0xA001) | 0xFFFF | Yes (reflected) | None | 16 bits | Little-endian | Standard Modbus RTU CRC16 | | **crc16CcittFalse** | 0x1021 | 0xFFFF | No | None | 16 bits | Big-endian | CRC-16-CCITT-FALSE | | **crc32** | 0x04C11DB7 | 0xFFFFFFFF | Yes (reflected) | XOR 0xFFFFFFFF | 32 bits | Little-endian | Standard CRC32 | | **crc8** | 0x07 | 0x00 | No | None | 8 bits | 1 byte | CRC-8 without reflection | | **crc1** | 0x01 | 0x00 | No | None | 1 bit | 1 bit | Simple CRC-1 | | **crc8_1wire** | 0x31 (reflected 0x8C) | 0x00 | Yes (reflected) | None | 8 bits | 1 byte | CRC-8 for 1-Wire protocol | | **crc8_dvbs2** | 0xD5 | 0x00 | No | None | 8 bits | 1 byte | CRC-8 DVB-S2 | | **crc16_kermit** | 0x1021 (reflected 0x8408) | 0x0000 | Yes (reflected) | None | 16 bits | Little-endian | CRC-16 Kermit | | **crc16_xmodem** | 0x1021 | 0x0000 | No | None | 16 bits | Big-endian | CRC-16 XModem | | **crc24** | 0x864CFB | 0xB704CE | No | None | 24 bits | Big-endian (3 bytes) | CRC-24 (Bluetooth, OpenPGP) | | **crc32mpeg** | 0x04C11DB7 | 0xFFFFFFFF | No | None | 32 bits | Big-endian | CRC-32 MPEG-2 | | **crcjam** | 0x04C11DB7 | 0xFFFFFFFF | Yes (reflected) | None | 32 bits | Little-endian | CRC-32 JAM (no final XOR) | --- To use one of these options when initializing **ModbusClient**, see the example below: ```js const client = new ModbusClient( transport, // your initialize transport 0, // slave id { crcAlgorithm: 'crc16Modbus' // Selecting the type of CRC calculation } ) ``` > If you do not specify the type of CRC calculation during initialization, the default option is used - `crc16Modbus` <br> ## 10. πŸŒ€ <span id="error-handling">Error Handling</span> The library defines specific error types for different Modbus issues: - `ModbusError`: Base class for all Modbus errors. - `ModbusTimeoutError`: Raised on request timeouts. - `ModbusCRCError`: Raised on CRC checksum failures. - `ModbusResponseError`: Raised on malformed responses. - `ModbusTooManyEmptyReadsError`: Raised if the transport detects a stalled connection. - `ModbusExceptionError`: Raised for standard Modbus exception responses from devices. - `ModbusFlushError`: Raised if an operation is interrupted by a transport buffer flush. Always wrap your Modbus calls in `try...catch` block to handle these errors appropriately. ```js try { const data = await client.readHoldingRegisters(100, 1); // Invalid address } catch (err) { if (err instanceof ModbusExceptionError) { console.error(`Modbus Exception ${err.exceptionCode}: ${err.message}`); // Handle specific device errors (e.g., Illegal Data Address) } else if (err instanceof ModbusTimeoutError) { console.error('Device did not respond in time'); // Handle timeout, might trigger reconnection logic check } else if (err instanceof ModbusFlushError) { console.warn('Operation interrupted by buffer flush, likely due to reconnection'); // Task will likely be retried by PollingManager or you can retry } else { console.error('An unexpected error occurred:', err.message); } } ``` <br> ## 11. πŸŒ€ <span id="polling-manager">Polling Manager</span> `PollingManager` is a powerful utility for managing periodic asynchronous tasks. It supports retries, backoff strategies, timeouts, dynamic intervals, and lifecycle callbacks β€” ideal for polling Modbus or other real-time data sources. Improved to work seamlessly with transport `flush()` and automatic reconnection. ### πŸ“¦ Key Features - Async execution of single or multiple functions - Automatic retries with per-attempt backoff delay - Per-function timeout handling - Lifecycle control: start, stop, pause, resume, restart - Lifecycle hooks: `onStart`, `onStop`, `onData`, `onError`, `onFinish`, `onBeforeEach`, `onRetry`, `onSuccess`, `onFailure` - Dynamically adjustable polling interval per task - Full task state inspection (running, paused, etc.) - Clean-up and removal of tasks - Handles `ModbusFlushError` gracefully, resetting backoff delays - Enhanced logging and diagnostics with context-aware information - Improved queue management and performance optimization - Priority-based task execution - Conditional task execution with `shouldRun` function - Comprehensive statistics tracking - Resource-based task queuing for serial device coordination Usage example ```js const PollingManager = require('modbus-connect/polling-manager'); const pollingManager = new PollingManager({ defaultMaxRetries: 3, defaultBackoffDelay: 1000, defaultTaskTimeout: 5000, logLevel: 'debug' }); // Define a polling task pollingManager.addTask({ id: 'read-sensors', // Task name resourceId: 'COM3', // Stream name (for serial device coordination) priority: 1, // Priority: method in queue (0...Infinity) interval: 1000, // Poll every 1 second immediate: true, // Start immediately fn: [ // Multiple functions to execute () => client.readHoldingRegisters(0, 11), () => client.readInputRegisters(4, 2) ], onData: (data) => { // Handle successful results console.log('Data received:', data); }, onError: (error, index, attempt) => { // Handle errors console.log(`Error in function ${index}, attempt ${attempt}:`, error.message); }, onStart: () => console.log('Polling measure data started'), onStop: () => console.log('Polling measure data stopped'), onFinish: (success, results) => { // Called after all functions complete console.log('Task finished:', { success, results }); }, onBeforeEach: () => { // Called before each execution cycle console.log('Starting new polling cycle'); }, shouldRun: () => { // Conditional execution return document.visibilityState === 'visible'; // Only run when tab is visible }, maxRetries: 3, // Retry attempts per function backoffDelay: 300, // Base delay for exponential backoff taskTimeout: 1000 // Timeout per function call }); // Later... // pollingManager.stopTask('read-sensors'); // pollingManager.removeTask('read-sensors'); ``` >**"Resource Coordination**: If you need to perform 2 or more tasks for 1 device (for example, COM port), then `resourceId` must be the same. This ensures tasks are queued and executed sequentially, preventing concurrent access to the same device which would lead to errors." >"**Queue Management**: Tasks with the same `resourceId` are placed in a queue and executed one at a time using mutex locks. Tasks without `resourceId` run independently in their own loop." ### 🧩 Task Interface **poll.addTask(options)** Registers and starts a new polling task. ```js poll.addTask({ // Required parameters id: string, // Unique task ID (required) interval: number, // Polling interval in milliseconds (required) fn: Function | Function[], // One or multiple async functions to execute (required) // Optional parameters resourceId?: string, // Resource identifier for queue management priority?: number, // Task priority (default: 0) name?: string, // Human-readable task name immediate?: boolean, // Run immediately on add (default: false) maxRetries?: number, // Retry attempts per function (default: 3) backoffDelay?: number, // Retry delay base in ms (default: 1000) taskTimeout?: number, // Timeout per function call in ms (default: 5000) // Lifecycle callbacks onData?: Function, // Called with results on success: (results) onError?: Function, // Called on error: (error, fnIndex, attempt) onStart?: Function, // Called when the task starts onStop?: Function, // Called when the task stops onFinish?: Function, // Called when all functions complete: (success, results) onBeforeEach?: Function, // Called before each execution cycle onRetry?: Function, // Called on retry: (error, fnIndex, attempt) onSuccess?: Function, // Called on successful execution onFailure?: Function, // Called on failed execution shouldRun?: Function // Conditional execution: () => boolean }); ``` ### πŸ“Š Task Statistics Each task maintains detailed statistics for monitoring and debugging: ```js const stats = pollingManager.getTaskStats('read-sensors'); // Returns: { totalRuns: 45, // Total execution attempts totalErrors: 3, // Total errors encountered lastError: Error, // Last error object (if any) lastResult: [...], // Last successful result lastRunTime: 1234567890, // Timestamp of last execution retries: 7, // Total retry attempts successes: 42, // Successful executions failures: 3 // Failed executions } ``` ### πŸ§ͺ Additional examples #### ⏸ Pause and resume a task ```js pollingManager.pauseTask('modbus-loop'); setTimeout(() => { pollingManager.resumeTask('modbus-loop'); }, 5000); ``` #### πŸ” Restart a task ```js pollingManager.restartTask('modbus-loop'); ``` #### 🧠 Dynamically update the polling interval ```js pollingManager.setTaskInterval('modbus-loop', 2000); // now polls every 2 seconds ``` #### ❌ Remove a task ```js pollingManager.removeTask('heartbeat'); ``` #### πŸ”„ Update task configuration ```js pollingManager.updateTask('read-sensors', { interval: 2000, maxRetries: 5, backoffDelay: 500 }); ``` #### πŸ”„ Update task configuration ```js pollingManager.updateTask('read-sensors', { interval: 2000, maxRetries: 5, backoffDelay: 500 }); ``` #### πŸ“Š Monitor system performance ```js // Get detailed queue information const queueInfo = pollingManager.getQueueInfo('COM3'); console.log('Queue status:', queueInfo); // Get comprehensive system statistics const systemStats = pollingManager.getSystemStats(); console.log('System stats:', systemStats); ``` #### πŸ›  Task management methods | METHOD | DESCRIPTION | |--------|-------------| |addTask(config) | Add and start a new polling task | |startTask(id) | Start a task | |stopTask(id) | Stop a task | |pauseTask(id) | Pause execution | |resumeTask(id) | Resume execution | |restartTask(id) | Restart a task | |removeTask(id) | Remove a task | |updateTask(id, opts) | Update a task (removes and recreates) | |setTaskInterval(id, ms) | Dynamically update the task's polling interval | |clearAll() | Stops and removes all registered tasks | |restartAllTasks() | Restart all tasks | |pauseAllTasks() | Pause all tasks | |resumeAllTasks() | Resume all tasks | |startAllTasks() | Start all tasks | |stopAllTasks() | Stop all tasks | |getAllTaskStats() | Get stats for all tasks | |getQueueInfo(resourceId) | Get detailed queue information | |getSystemStats() | Get comprehensive system statistics | #### πŸ“Š Status and Checks | METHOD | DESCRIPTION | |--------|-------------| | isTaskRunning(id) | Returns true if the task is running | | isTaskPaused(id) | Returns true if the task is paused | | getTaskState(id) | Returns detailed state info: { stopped, paused, running, inProgress } | | getTaskStats(id) | Returns detailed statistics for the task | | hasTask(id) | Checks if task exists | | getTaskIds() | Returns list of all task IDs | #### πŸ”§ Configuration Options The PollingManager can be configured with various options: ```js const pollingManager = new PollingManager({ defaultMaxRetries: 3, // Default retry attempts (default: 3) defaultBackoffDelay: 1000, // Default backoff delay in ms (default: 1000) defaultTaskTimeout: 5000, // Default task timeout in ms (default: 5000) logLevel: 'info' // Logging level: trace, debug, info, warn, error (default: 'info') }); ``` #### 🧼 Cleanup ```js pollingManager.clearAll(); // Stops and removes all registered tasks, clears queues ``` #### πŸ’‘ Advanced Features **Enhanced Error Handling** The manager provides comprehensive error handling with detailed context: - Automatic retry with exponential backoff - Special handling for `ModbusFlushError` with reset backoff - Per-function error tracking - Detailed error statistics **Performance Optimizations** - Improved queue processing with processing flags to prevent duplicate execution - Mutex-based resource locking for serial device coordination - Memory-efficient task cleanup - Rate-limited logging to prevent console spam **Diagnostics and Monitoring** ```js // Get system statistics const stats = pollingManager.getSystemStats(); console.log('System Stats:', stats); // Get queue information for specific resource const queueInfo = pollingManager.getQueueInfo('COM3'); console.log('Queue Info:', queueInfo); // Monitor individual task performance const taskStats = pollingManager.getTaskStats