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
- Basic Usage
- Work via RS485
- Summary type data
- Main Classes and Methods
- Modbus Functions
- Packet building & Parsing
- Diagnostics & Error Handling
- Logger
- Utilities
- CRC
- Error Handling
- Polling Manager
- Slave Emulator
- Tips for use
- Expansion
- CHANGELOG
1. 📁 Library Structure
- 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).
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
npm install modbus-connect
2. 🚀 Basic Usage
Please read Important Note before use.
Importing Modules
The library provides several entry points for different functionalities:
// 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:
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.
// 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:
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
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
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();
}
Work via RS485
In order to work via RS485, you first need to connect the COM port.
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.
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:
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.
// 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
}
}
🧾 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) |
3. 🏗️ Main Classes and Methods
Methods (basic):
connect()
/disconnect()
— open/close connectionreadHoldingRegisters(startAddress, quantity, timeout?)
— read holding registersreadInputRegisters(startAddress, quantity, timeout?)
— read input registerswriteSingleRegister(address, value, timeout?)
— write a single registerwriteMultipleRegisters(startAddress, values, timeout?)
— write multiple registersreadCoils(startAddress, quantity, timeout?)
— read discrete outputs (coils)readDiscreteInputs(startAddress, quantity, timeout?)
— read discrete inputswriteSingleCoil(address, value, timeout?)
— write a single coilwriteMultipleCoils(startAddress, values, timeout?)
— write multiple coilsreportSlaveId(timeout?)
— get device identifierreadDeviceIdentification(slaveId, categoryId, objectId)
- read device identificationgetDiagnostics()
— get communication statisticsresetDiagnostics()
— 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)
4. 🧩 Modbus Functions
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 requestparse...Response(pdu)
— parses the response
Example: manual PDU building
const { buildReadHoldingRegistersRequest } = require('./function-codes/read-holding-registers.js');
const pdu = buildReadHoldingRegistersRequest(0, 2);
5. 📦 Packet Building & Parsing
packet-builder.js
- buildPacket(slaveAddress, pdu) — Adds slaveId and CRC, returns ADU
- parsePacket(packet) — Verifies CRC, returns { slaveAddress, pdu }
Example:
const { buildPacket, parsePacket } = require('./packet-builder.js');
const adu = buildPacket(1, pdu);
const { slaveAddress, pdu: respPdu } = parsePacket(adu);
6. 📊 Diagnostics & Error Handling
- 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 transportflush()
.
Diagnostics Example:
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.
7. 🛠 Logger
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
, orexceptionCode
- Statistics and debugging: Via
summary
andinspectBuffer
- Real-time monitoring: Using the
watch
feature - Custom formatting: For context fields like
slaveId
orfuncCode
📦 Import
const logger = require('modbus-connect/logger');
🔊 Basic Logging
Log messages at different levels with optional data:
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:
[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:
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:
[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:
logger.setLevel('trace'); // 'trace' | 'debug' | 'info' | 'warn' | 'error'
console.log(logger.getLevel()); // => 'trace'
console.log(logger.isEnabled()); // => true
🚫 Enabling/Disabling Logger
logger.disable(); // Disable all logging
logger.enable(); // Re-enable logging
🎨 Colors
Disable colored output if needed:
logger.disableColors();
🧵 Log Groups
Organize logs with nested groups:
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:
logger.setGlobalContext({ transport: 'TCP', slaveId: 1 });
logger.addGlobalContext({ device: 'SGM130' });
logger.setTransportType('RTU'); // Shortcut for transport
🔄 Output Buffering
Control buffering and flush interval:
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:
logger.setRateLimit(50); // Limit to one log every 50ms (except warn/error)
📁 Categorical Loggers
Create named loggers for module-specific logging:
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:
logger.error('Critical failure', { slaveId: 1, exceptionCode: 1 });
🔍 Filtering Logs
Mute logs based on slaveId
, funcCode
, or exceptionCode
:
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
):
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:
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:
logger.inspectBuffer();
Output:
=== 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:
logger.summary();
Output:
=== 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:
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:
[06:00:00][INFO][S:Device1][F:ReadHoldingRegisters] Test
🖌 Custom Log Format
Configure which fields appear in the log header:
logger.setLogFormat(['timestamp', 'level', 'slaveId', 'funcCode']);
🧪 Usage Example
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:
[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 likeIllegal Function
errors. - Monitor specific devices with
watch
ormute
for selective logging. - Check
summary
to analyze log distribution byslaveId
,funcCode
, orexceptionCode
. - Disable buffering for real-time debugging or adjust
flushInterval
for performance. - Use short log format (
setLogFormat(['timestamp', 'level'])
) for minimal output.
8. 🛠 Utilities
- 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.)
9. CRC
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:
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
10. 🌀 Error Handling
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.
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);
}
}
11. 🌀 Polling Manager
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
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 withoutresourceId
run independently in their own loop."
🧩 Task Interface
poll.addTask(options) Registers and starts a new polling task.
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:
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
pollingManager.pauseTask('modbus-loop');
setTimeout(() => {
pollingManager.resumeTask('modbus-loop');
}, 5000);
🔁 Restart a task
pollingManager.restartTask('modbus-loop');
🧠 Dynamically update the polling interval
pollingManager.setTaskInterval('modbus-loop', 2000); // now polls every 2 seconds
❌ Remove a task
pollingManager.removeTask('heartbeat');
🔄 Update task configuration
pollingManager.updateTask('read-sensors', {
interval: 2000,
maxRetries: 5,
backoffDelay: 500
});
🔄 Update task configuration
pollingManager.updateTask('read-sensors', {
interval: 2000,
maxRetries: 5,
backoffDelay: 500
});
📊 Monitor system performance
// 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:
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
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
// 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('read-sensors');
console.log('Task Stats:', taskStats);
Priority-based Execution Tasks can be assigned priorities for queue ordering:
pollingManager.addTask({
id: 'high-priority',
resourceId: 'COM3',
priority: 10, // Higher priority executes first
interval: 1000,
fn: () => client.readCriticalData()
});
pollingManager.addTask({
id: 'low-priority',
resourceId: 'COM3',
priority: 1, // Lower priority executes later
interval: 5000,
fn: () => client.readNonCriticalData()
});
Conditional Execution
Use shouldRun
function for conditional task execution:
pollingManager.addTask({
id: 'conditional-task',
interval: 1000,
fn: () => client.readData(),
shouldRun: () => {
// Only run when certain conditions are met
return network.isConnected() &&
device.isReady() &&
user.hasPermission();
},
onData: (data) => console.log('Conditional data:', data)
});
Tips for use Polling Manager Automatic pause/resume with Page Visibility API
// Add automatic pause/resume when tab visibility changes
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
console.log('The user switched to another tab or minimized the browser');
if (pollingManager.isTaskRunning('read-sensors') && !pollingManager.isTaskPaused('read-sensors')) {
pollingManager.pauseTask('read-sensors');
console.log('Polling task is automatically paused');
}
} else {
console.log('The user returned to the tab');
if (pollingManager.isTaskPaused('read-sensors')) {
pollingManager.resumeTask('read-sensors');
console.log('Polling task automatically resumed');
}
}
});
Network-aware polling
pollingManager.addTask({
id: 'network-aware-task',
interval: 1000,
fn: () => client.readData(),
shouldRun: () => navigator.onLine, // Only run when online
onError: (error) => {
if (!navigator.onLine) {
console.log('Network offline, pausing task');
pollingManager.pauseTask('network-aware-task');
}
}
});
// Resume when network comes back
window.addEventListener('online', () => {
pollingManager.resumeTask('network-aware-task');
});
"Improved Flush Handling: The
PollingManager
now automatically flushes the transport buffer before each task run and intelligently handlesModbusFlushError
during retries, resetting the exponential backoff delay for better responsiveness after a flush.""Enhanced Logging: Integrated with the advanced logger system for detailed monitoring and debugging capabilities with context-aware logging. Each component (manager, queues, tasks) has its own logger with detailed context information. "
"Resource Queue Management: Tasks sharing the same
resourceId
are automatically queued and executed sequentially using mutex locks, preventing concurrent access to shared resources like serial ports."
12. 📘 SlaveEmulator
📦 Import
const SlaveEmulator = require('modbus-connect/slave-emulator')
🏗 Creating an Instance
const emulator = new SlaveEmulator(1) // 1 — Modbus slave address (0-247)
🔌 Connecting and Disconnecting
await emulator.connect()
// ...interact with emulator...
await emulator.disconnect()
// Graceful cleanup
await emulator.destroy() // Stops all tasks and clears resources
⚙️ Initializing Registers
Method: addRegisters(config)
Use this to initialize register and bit values:
emulator.addRegisters({
holding: [
{ start: 0, value: 123 },
{ start: 1, value: 456 }
],
input: [
{ start: 0, value: 999 }
],
coils: [
{ start: 0, value: true }
],
discrete: [
{ start: 0, value: false }
]
})
🔄 Direct Read/Write (No RTU)
Holding Registers
emulator.setHoldingRegister(0, 321)
const holding = emulator.readHoldingRegisters(0, 2)
console.log(holding) // [321, 456]
Input Registers
emulator.setInputRegister(1, 555)
const input = emulator.readInputRegisters(1, 1)
console.log(input) // [555]
Coils (boolean flags)
emulator.setCoil(2, true)
const coils = emulator.readCoils(2, 1)
console.log(coils) // [true]
Discrete Inputs
emulator.setDiscreteInput(3, true)
const inputs = emulator.readDiscreteInputs(3, 1)
console.log(inputs) // [true]
"Data is returned in
uint16
only. All values are automatically masked to 16-bit."
🚫 Exceptions
You can set exceptions for specific operations:
emulator.setException(0x03, 1, 0x02) // Error for reading holding register 1
try {
emulator.readHoldingRegisters(1, 1)
} catch (err) {
console.log(err.message) // Exception response for function 0x03 with code 0x02
}
🔄 Dynamic Register Simulation
Simulate changing register values over time:
// Periodically change the value in holding register 0 between 30 and 65
emulator.infinityChange({
typeRegister: 'Holding', // 'Holding', 'Input', 'Coil', or 'Discrete'
register: 0, // Register address
range: [30, 65], // Value range [min, max] for registers, or boolean for coils
interval: 500 // Update interval in milliseconds
})
// Stop dynamic changes
emulator.stopInfinityChange({
typeRegister: 'Holding',
register: 0
})
🧪 Handling RTU Requests
Input: Uint8Array
with Modbus RTU request
Output: Uint8Array
with response
Example:
const request = new Uint8Array([0x01, 0x03, 0x00, 0x00, 0x00, 0x02, 0xC4, 0x0B]) // Read Holding [0,2]
const response = emulator.handleRequest(request)
console.log(Buffer.from(response).toString('hex'))
// Example output: 010304007b01c8crc_lo crc_hi
🧾 Full Example Script
const SlaveEmulator = require('modbus-connect/slave-emulator')
const logger = require('modbus-connect/logger')
const log = logger.createLogger('main')
const emulator = new SlaveEmulator(1)
await emulator.connect()
emulator.addRegisters({
holding: [{ start: 0, value: 123 }, { start: 1, value: 456 }],
input: [{ start: 0, value: 999 }],
coils: [{ start: 0, value: true }],
discrete: [{ start: 0, value: false }]
})
log.warn('Holding:', emulator.readHoldingRegisters(0, 2)) // [123, 456]
log.warn('Input:', emulator.readInputRegisters(0, 1)) // [999]
log.warn('Coils:', emulator.readCoils(0, 1)) // [true]
log.warn('Discrete:', emulator.readDiscreteInputs(0, 1)) // [false]
await emulator.disconnect()
🧰 Additional Methods
METHOD | DESCRIPTION |
---|---|
setHoldingRegister(addr, val) | Set holding register (0-65535) |
setInputRegister(addr, val) | Set input register (0-65535) |
setCoil(addr, bool) | Set coil bit (boolean) |
setDiscreteInput(addr, bool) | Set discrete input bit (boolean) |
readHoldingRegisters(start, qty) | Read holding registers (max 125) |
readInputRegisters(start, qty) | Read input registers (max 125) |
readCoils(start, qty) | Read coil bits (max 2000) |
readDiscreteInputs(start, qty) | Read discrete input bits (max 2000) |
setException(funcCode, addr, exCode) | Register an exception |
handleRequest(buffer) | Process Modbus RTU request |
infinityChange(config) | Start dynamic register simulation |
stopInfinityChange(config) | Stop dynamic register simulation |
getRegisterStats() | Get statistics about all registers |
getRegisterDump() | Get complete register dump |
getInfinityTasks() | Get list of active dynamic tasks |
clearAllRegisters() | Clear all register values |
clearExceptions() | Clear all exceptions |
clearInfinityTasks() | Stop all dynamic simulation tasks |
destroy() | Graceful shutdown with cleanup |
✅ Supported Modbus RTU Function Codes
FUNCTION CODE | DESCRIPTION | MAX QUANTITY |
---|---|---|
0x01 | Read Coils | 2000 |
0x02 | Read Discrete Inputs | 2000 |
0x03 | Read Holding Registers | 125 |
0x04 | Read Input Registers | 125 |
0x05 | Write Single Coil | 1 |
0x06 | Write Single Register | 1 |
0x0F | Write Multiple Coils | 1968 |
0x10 | Write Multiple Registers | 123 |
📊 Diagnostic Methods
// Get register statistics
const stats = emulator.getRegisterStats();
console.log(stats);
// Output: {
// coils: 5,
// discreteInputs: 3,
// holdingRegisters: 10,
// inputRegisters: 8,
// exceptions: 2,
// infinityTasks: 1
// }
// Get complete register dump
const dump = emulator.getRegisterDump();
console.log(dump);
// Output: {
// coils: { '0': true, '1': false, ... },
// discreteInputs: { '0': false, ... },
// holdingRegisters: { '0': 123, '1': 456, ... },
// inputRegisters: { '0': 999, ... }
// }
// Get active infinity tasks
const tasks = emulator.getInfinityTasks();
console.log(tasks); // ['Holding:0', 'Coil:5', ...]
🧹 Cleanup Methods
// Clear all register values
emulator.clearAllRegisters();
// Clear all exceptions
emulator.clearExceptions();
// Stop all infinity tasks
emulator.clearInfinityTasks();
// Complete cleanup (recommended)
await emulator.destroy();
Slave emulator With PollingManager
Also SlaveEmulator
can work in conjunction with (PollingManager)[##polling-manager]
. Example usage:
const SlaveEmulator = require('modbus-connect/slave-emulator')
const PollingManager = require('modbus-connect/polling-manager')
const logger = require('modbus-connect/logger')
const log = logger.createLogger('main')
log.setLevel('debug')
const poll = new PollingManager({
defaultMaxRetries: 3,
defaultBackoffDelay: 500,
defaultTaskTimeout: 2000,
logLevel: 'debug'
})
const emulator = new SlaveEmulator(1)
emulator.logger.setLevel('debug')
await emulator.connect()
// Initialize emulator register values
emulator.addRegisters({
holding: [
{ start: 0, value: 123 },
{ start: 1, value: 456 }
],
input: [
{ start: 0, value: 999 }
],
coils: [
{ start: 0, value: true }
],
discrete: [
{ start: 0, value: false }
]
})
// Periodically change the value in holding register 0 between 30 and 65
emulator.infinityChange({
typeRegister: 'Holding',
register: 0,
range: [30, 65],
interval: 500 // ms
})
// Add a polling task that reads data from the emulator every 1 second
poll.addTask({
id: 'modbus-loop',
resourceId: 'emulator-test', // Use resourceId for sequential execution
interval: 1000,
immediate: true,
fn: [
() => emulator.readHoldingRegisters(0, 2),
() => emulator.readInputRegisters(0, 1),
() => emulator.readCoils(0, 1),
() => emulator.readDiscreteInputs(0, 1)
],
onData: ([holding, input, coils, discrete]) => {
log.info('Registers updated', {
slaveId: 1,
funcCode: 0x03,
holding,
input,
coils,
discrete
});
},
onError: (error, index, attempt) => {
log.warn(`Error in fn[${index}], attempt ${attempt}`, {
slaveId: 1,
error: error.message,
funcCode: [0x01, 0x02, 0x03, 0x04][index]
});
},
onStart: () => log.info('Polling started', { slaveId: 1 }),
onStop: () => log.info('Polling stopped', { slaveId: 1 }),
maxRetries: 3,
backoffDelay: 300,
taskTimeout: 2000
});
// Later...
// await poll.stopTask('modbus-loop');
// await emulator.destroy();
Example Log Output:
[12:45:30] [INFO] [PollingManager] PollingManager initialized { config: {...} }
[12:45:30] [INFO] [Task:modbus-loop] TaskController created { id: 'modbus-loop', resourceId: 'emulator-test', interval: 1000 }
[12:45:30] [INFO] [Queue:emulator-test] TaskQueue created
[12:45:30] [INFO] [SlaveEmulator] Connecting to emulator...
[12:45:30] [INFO] [SlaveEmulator] Connected
[12:45:30] [INFO] [SlaveEmulator] Registers added successfully { coils: 1, discrete: 1, holding: 2, input: 1 }
[12:45:30] [INFO] [Task:modbus-loop] Task started
[12:45:30] [DEBUG] [Queue:emulator-test] Task enqueued { taskId: 'modbus-loop' }
[12:45:30] [DEBUG] [Queue:emulator-test] Acquiring mutex for task processing
[12:45:30] [DEBUG] [Queue:emulator-test] Processing task { taskId: 'modbus-loop' }
[12:45:30] [INFO] [Task:modbus-loop] Executing task once
[12:45:30] [INFO] [SlaveEmulator] readHoldingRegisters { startAddress: 0, quantity: 2 }
[12:45:30] [INFO] [SlaveEmulator] readInputRegisters { startAddress: 0, quantity: 1 }
[12:45:30] [INFO] [SlaveEmulator] readCoils { startAddress: 0, quantity: 1 }
[12:45:30] [INFO] [SlaveEmulator] readDiscreteInputs { startAddress: 0, quantity: 1 }
[12:45:30] [INFO] [Task:modbus-loop] Task execution completed { success: true, resultsCount: 4 }
[12:45:30] [DEBUG] [Queue:emulator-test] Task executed successfully { taskId: 'modbus-loop' }
[12:45:30] [INFO] [main] Registers updated { slaveId: 1, funcCode: 0x03, holding: [123, 456], input: [999], coils: [true], discrete: [false] }
[12:45:31] [DEBUG] [Queue:emulator-test] Task marked as ready { taskId: 'modbus-loop' }
Key Integration Features:
- Resource-based Queuing: Use
resourceId
to ensure sequential access to the emulator - Advanced Logging: Full context-aware logging with slaveId and function codes
- Error Handling: Comprehensive error handling with retry mechanisms
- Performance Monitoring: Built-in statistics and diagnostics
- Graceful Cleanup: Proper resource management with
destroy()
method
🛡️ Validation and Error Handling The emulator includes comprehensive validation:
- Address validation: All addresses must be between 0 and 65535
- Value validation: Register values must be between 0 and 65535, coil values must be boolean
- Quantity validation: Respects Modbus protocol limits for each function
- CRC validation: Automatic CRC checking for incoming requests
- Exception handling: Proper Modbus exception responses
- Input validation: Type checking for all parameters
Emulator note This emulator does not use real or virtual COM ports. It is fully virtual and designed for testing Modbus RTU logic without any physical device. It supports all standard Modbus RTU function codes with proper error handling, CRC validation, and protocol compliance. The integration with PollingManager provides enterprise-grade task management with logging, retries, and resource coordination.
📎 Notes
- Each
fn[i]
is handled