# 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)
- [Quick Start](#quick-start)
- [Summary type data](#type-data)
- [Main Classes and Methods](#main-classes-and-methods)
- [Transports](#transports)
- [Modbus Functions](#modbus-functions)
- [Packet building & Parsing](#packet-building-and-parsing)
- [Diagnostics & Error Handling](#diagnostics-and-error-handling)
- [Logger](#logger)
- [Utilities](#utilities)
- [CRC](#crc)
- [Polling Manager](#polling-manager)
- [Slave Emulator](#slave-emulator)
- [Tips for use](#tips-for-use)
- [Expansion](#expansion)
- [CHANGELOG](#changelog)
---


## 1. 📁 <span id="library-structure">Library Structure</span>
- **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.
- **function-codes/** — PDU implementations for all Modbus functions (register/bit read/write, special functions).
- **logger.js** — Event logging utilities.
- **packet-builder.js** — ADU packet construction/parsing (with CRC).
- **transport/** — Transport adapters (Node.js SerialPort, Web Serial API), auto-detection helpers.
- **utils/** — Utilities: CRC, diagnostics, and helpers.

<br><br>

## 2. 🚀 <span id="quick-start">Quick Start</span>
### Reading Holding Registers
### Node.js Example
```js
const ModbusClient = require('modbus-connect/client');
const { createTransport } = require('modbus-connect/transport')

const transport = await createTransport('node', {
    port: 'COM3',
    baudRate: 9600,
    parity: 'none',
    dataBits: 8,
    stopBits: 1
})

const client = new ModbusClient(transport, 1)

async function main() {
    await client.connect()
    const regs = await client.readHoldingRegisters(0, 2)
    console.log('Read registers:', regs)
    await client.disconnect()
}

main()
```
**Run:**
```bash
  node ./test-node.js
```

### Browser Example (Web Serial API)
**index.html**
```html
<body>
    <button id="test">Select port</button>
    <button id="connect">Connect and read 2 registers</button>
    <script type="module" src="test-web.js"></script>
</body>
```
**test-web.js**
```js
const ModbusClient = require('modbus-connect/client');
const { createTransport } = require('modbus-connect/transport');

let port

async function getSerialPort(){
    return await navigator.serial.requestPort()
}

async function selectPort(){
    try {
        port = await getSerialPort()
        console.log(port)
    } catch (err) {
        console.error(err)
    }
}

async function connectAndRead(){
    try {
        const transport = await createTransport('web', {
            port,
            baudRate: 9600,
            parity: 'none',
            dataBits: 8,
            stopBits: 1
        })

        const client = new ModbusClient(transport, 1)
        await client.connect()
        const regs = await client.readHoldingRegisters(0, 2)
        console.log('Read registers:', regs)
        await client.disconnect()
    } catch (err) {
        console.error(err)
    }
}

document.querySelector('#test').onclick = selectPort
document.querySelector('#connect').onclick = connectAndRead
```

### 🧾 <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><br>

## 3. 🏗️ <span id="main-classes-and-methods">Main Classes and Methods</span>
### ModbusClient

**Constructor:**
```js
const client = new ModbusClient(transport, slaveId = 1, options = {})
```

- `transport` — transport object (see below)
- `slaveId` —  device address (1..247)
- `options` — `{ timeout, retryCount, retryDelay }`

**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
<br>

**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><br>

## 4. 🔌 <span id="transports">Transports</span>
In order to initialize the transport, it is necessary to determine the type of the transport itself - `node` or `web` depending on the environment in which the code is executed
- Node.js:
```js
  const transport = createTransport('node', { port, baudRate, parity, stopBits, dataBits })
```
- Node TCP/IP:
```js
  const transport = createTransport('node-tcp', { host, port, readTimeout = 100, writeTimeout = 100, reconnectInterval = 3000, maxReconnectAttempts = Infinity })
```
- Web (browser):
```js
  const transport = createTransport('web', { port, baudRate, parity, stopBits, dataBits })
```
- Web TCP (browser):
```js
  const transport = createTransport('web-tcp', { host, port, readTimeout = 100, writeTimeout = 100, reconnectInterval = 3000, maxReconnectAttempts = Infinity })
```


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', {
    port: 'COM3',
    baudRate: 9600,
    parity: 'none',
    dataBits: 8,
    stopBits: 1,
    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

<br><br>

## 5. 🧩 <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><br>

## 6. 📦 <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><br>

## 7. 📊 <span id="diagnostics-and-error-handling">Diagnostics & Error Handling</span>

- **diagnostics.js** — Diagnostics class collects stats (requests, errors, response times, etc.)
- **errors.js** — Error classes:
    - `ModbusTimeoutError`
    - `ModbusCRCError`
    - `ModbusResponseError`
    - `ModbusTooManyEmptyReadsError`
    - `ModbusExceptionError`

**Diagnostics example:**
```js
const stats = client.getDiagnostics();
console.log(stats);
client.resetDiagnostics();
```

<br><br>

## 8. 🛠 <span id="logger">Logger</span>
`logger.js` is designed for formatted logging in the console with support for:

- log levels (debug, info, warn, error),
- colors,
- nested groups,
- global context,
- output buffering,
- categorical loggers,
- timings (responseTime).

### 📦 Import
```js
const logger = require('modbus-connect/logger');
```

### 🔊 Basic logging
```js
logger.debug('Debug message');
logger.info('Informational message');
logger.warn('Warning message');
logger.error('Error message');
```
**You can also pass any data:**
```js
logger.info('Received registers:', [123, 456]);
```

### 📦 Logging with context
You can pass a context object as the last argument (for example, to display responseTime, transport, etc.):
```js
logger.info('Response received', { responseTime: 42 });
```

### 🖍 Managing log levels
**Setting the global log level:**
```js
logger.setLevel('debug'); // 'debug' | 'info' | 'warn' | 'error'
```
**Checking:**
```js
logger.getLevel(); // => 'debug'
logger.isEnabled(); // => true
```

### 🚫 Enabling / disabling the logger
```js
logger.disable(); // disable logging
logger.enable(); // enable
```

### 🎨 Colors
```js
logger.disableColors(); // disable colored output
```

### 🧵 Log groups
```js
logger.group();
logger.info('Start of group');
logger.group();
logger.debug('Nested group');
logger.groupEnd();
logger.groupEnd();
```

### 🌐 Global context
```js
logger.setGlobalContext({ transport: 'NODE' });
```
> The logger can automatically detect the execution environment, i.e. Node or WEB
Or adding new fields:
```js
logger.addGlobalContext({ device: 'SGM130' });
```

### 🔄 Output buffering
```js
logger.setBuffering(true); // enable (default)
logger.setBuffering(false); // immediate output
```
> The buffer is automatically reset every 300ms or when logs accumulate.

### 📁 Categorical loggers
You can create a named logger, which automatically adds context.logger = name.
```js
const transportLog = logger.createLogger('transport');

transportLog.info('Connected'); // adds [transport] to context
transportLog.setLevel('debug'); // the level is configured separately
```

### 💥 Immediate error output
Errors are always output immediately, even with buffering enabled:
```js
logger.error('Something went wrong!');
```

### 🧪 Usage example
```js
const logger = require('modbus-connect/logger');

logger.setLevel('debug');
logger.setGlobalContext({ transport: 'NODE' });

logger.group();
logger.info('Starting Modbus session');

const comm = logger.createLogger('comm');
comm.debug('Opening port COM3');

logger.info('Response received', { responseTime: 48 });
logger.groupEnd();
```
The output will be something like this:
```bash
[2025-05-29 03:13:27.192] [modbus] [NODE] [INFO] Serial port COM3 opened
[2025-05-29 03:13:27.192] [modbus] [NODE] [INFO] Serial port COM3 opened
[2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] Transport connected
[2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] Transport connected
[2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] [responseTime: 46 ms] Response received
[2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] [ 4866, 25629 ]
[2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] Serial port COM3 closed
[2025-05-29 03:13:27.194] [modbus] [NODE] [INFO] Transport disconnected
```

### 📌 Tips
- Use named loggers for modules (`createLogger('transport')`).
- Add `responseTime` to context if you need to measure response time.
- Group logs for sequential operations.
- In CLI or Web projects, disable colors if necessary.

<br><br>

## 9. 🛠 <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

**CRC usage example:**
```js
const { crc16modbus } = require('./utils/crc.js');
const crc = crc16modbus(Uint8Array.from([1, 3]);
consoole.log(crc)
```

<br><br>

## 10. <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><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.

### 📦 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`
- Dynamically adjustable polling interval per task
- Full task state inspection (running, paused, etc.)
- Clean-up and removal of tasks

### Usage example
```js
const ModbusClient = require('modbus-connect/client');
const logger = require('modbus-connect/logger');
const PollingManager = require('modbus-connect/polling-manager');

const log = logger.createLogger('main')

const client = new ModbusClient(transport); // your initialized client
const poll = new PollingManager();

poll.addTask({
  id: 'modbus-loop',
  interval: 1000,
  immediate: true,
  fn: [
      async () => await client.readHoldingRegisters(0, 2),
      async () => await client.readHoldingRegisters(2, 2),
  ],
  onData: (hold1, hold2) => {
      log.info('Registers:', hold1, hold2 );
  },
  onError: (error, index, attempt) => {
      console.warn(`Error in fn[${index}], attempt ${attempt}: ${error.message}`);
  },
  onStart: () => console.log('Polling started'),
  onStop: () => console.log('Polling stopped'),
  maxRetries: 3,
  backoffDelay: 300,
  taskTimeout: 2000
});
```

### 🧩 Task Interface
**poll.addTask(options)**
Registers and starts a new polling task.
```ts
poll.addTask({
  id: string,                    // Unique task ID
  interval: number,              // Polling interval (ms)
  fn: Function | Function[],     // One or multiple async functions
  onData?: Function,             // Called with results array on success
  onError?: Function,            // Called on error: (error, fnIndex, attempt)
  onStart?: Function,            // Called when the task starts
  onStop?: Function,             // Called when the task stops
  immediate?: boolean,           // Run immediately on add
  maxRetries?: number,           // Retry attempts per function
  backoffDelay?: number,         // Retry delay base (ms)
  taskTimeout?: number           // Timeout per function call (ms)
});
```

### 🧪 Additional examples
**⏸ Pause and resume a task**
```js
poll.pauseTask('modbus-loop');

setTimeout(() => {
  poll.resumeTask('modbus-loop');
}, 5000);
```

**🔁 Restart a task**
```js
poll.restartTask('modbus-loop');
```

**📡 Add a task with a single function and custom interval**
```js
poll.addTask({
  id: 'heartbeat',
  interval: 5000,
  fn: async () => {
    const status = await client.reportSlaveId();
    return status;
  },
  onData: ([result]) => console.log('Device is alive:', result),
  maxRetries: 2,
  backoffDelay: 1000
});
```

**🧠 Dynamically update the polling interval**
```js
poll.setTaskInterval('modbus-loop', 2000); // now polls every 2 seconds
```

**❌ Remove a task**
```js
poll.removeTask('heartbeat');
```

### 🛠 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  |

### 📊 Status and Checks
|    Method   |  Description |
|-------------|--------------|
| isTaskRunning(id)  |  true if the task is running                                 |
| isTaskPaused(id)   |  true if the task is paused                                  |
| getTaskState(id)   |  Detailed info: { stopped, paused, loopRunning, interval }   |
| hasTask(id)        |  Checks if task exists                                       |
| getTaskIds()       |  List of all task IDs                                        |

### 🧼 Cleanup
```js
polling.clearAll(); // Stops and removes all registered tasks
```

### Tips for use Polling Manager
In order to optimize the use of polling manager, it is necessary to use Page Visibility API

**Example**
```js
// --- 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 (poll.isTaskRunning(taskId) && !poll.isTaskPaused(taskId)) {
      poll.pauseTask(taskId);
      console.log('Polling task is automatically paused');
    }
  } else {
    console.log('The user returned to the tab');
    if (poll.isTaskPaused(taskId)) {
      poll.resumeTask(taskId);
      console.log('Polling task automatically resumed');
    }
  }
});
```

<br><br>

## 12. <span id="slave-emulator">📘 SlaveEmulator</span>
### 📦 Import
```js
const SlaveEmulator = require('modbus-connect/slave-emulator')
```

### 🏗 Creating an Instance
```js
const emulator = new SlaveEmulator(1) // 1 — Modbus slave address
```

### 🔌 Connecting and Disconnecting
```js
await emulator.connect()
// ...interact with emulator...
await emulator.disconnect()
```

### ⚙️ Initializing Registers
**Method:** `addRegisters(config)`
Use this to initialize register and bit values:
```js
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**
```js
emulator.setHoldingRegister(0, 321)
const holding = emulator.readHoldingRegisters(0, 2)
console.log(holding) // [321, 456]
```

**Input Registers**
```js
emulator.setInputRegister(1, 555)
const input = emulator.readInputRegisters(1, 1)
console.log(input) // [555]
```

**Coils (boolean flags)**
```js
emulator.setCoil(2, true)
const coils = emulator.readCoils(2, 1)
console.log(coils) // [true]
```

**Discrete Inputs**
```js
emulator.setDiscreteInput(3, true)
const inputs = emulator.readDiscreteInputs(3, 1)
console.log(inputs) // [true]
```

> Data is returned in `uint16` only.

### 🚫 Exceptions
You can set exceptions for specific operations:
```js
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
}
```

### 🧪 Handling RTU Requests
**Input:** `Uint8Array` **with Modbus RTU request**
**Output:** `Uint8Array` **with response**
Example:
```js
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
```js
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       |
| `setInputRegister(addr, val)`          | Set input register         |
| `setCoil(addr, bool)`                  | Set coil bit               |
| `setDiscreteInput(addr, bool)`         | Set discrete input bit     |
| `readHoldingRegisters(start, qty)`     | Read holding registers     |
| `readInputRegisters(start, qty)`       | Read input registers       |
| `readCoils(start, qty)`                | Read coil bits             |
| `readDiscreteInputs(start, qty)`       | Read discrete input bits   |
| `setException(funcCode, addr, exCode)` | Register an exception      |
| `handleRequest(buffer)`                | Process Modbus RTU request |

### ✅ Supported Modbus RTU Function Codes
| Function Code | Description              |
| ------------- | ------------------------ |
| `0x01`        | Read Coils               |
| `0x02`        | Read Discrete Inputs     |
| `0x03`        | Read Holding Registers   |
| `0x04`        | Read Input Registers     |
| `0x05`        | Write Single Coil        |
| `0x06`        | Write Single Register    |
| `0x0F`        | Write Multiple Coils     |
| `0x10`        | Write Multiple Registers |

### Slave emulator With PollingManager
Also `SlaveEmulator` can work in conjunction with [`PollingManager`](#polling-manager). Example usage:
```js
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')

const poll = new PollingManager()
const emulator = new SlaveEmulator(1)

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',
    interval: 1000,
    immediate: true,
    fn: [
        async () => emulator.readHoldingRegisters(0, 2),
        async () => emulator.readHoldingRegisters(2, 2),
    ],
    onData: (hold1, hold2) => {
        log.info('Registers:', hold1, hold2)
    },
    onError: (error, index, attempt) => {
        console.warn(`Error in fn[${index}], attempt ${attempt}: ${error.message}`)
    },
    onStart: () => console.log('Polling started'),
    onStop: () => console.log('Polling stopped'),
    maxRetries: 3,
    backoffDelay: 300,
    taskTimeout: 2000
})

await emulator.disconnect()
```

**Output:**
```bash
Registers added: {
  holding: [ { start: 0, value: 123 }, { start: 1, value: 456 } ],
  input: [ { start: 0, value: 999 } ],
  coils: [ { start: 0, value: true } ],
  discrete: [ { start: 0, value: false } ]
}
Polling started
[2025-06-02 04:35:59.016] [modbus] [UNKNOWN] [INFO] Connecting to emulator... 
[2025-06-02 04:35:59.017] [modbus] [UNKNOWN] [INFO] Connected 
[2025-06-02 04:35:59.017] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=0, quantity=2 
[2025-06-02 04:35:59.017] [modbus] [UNKNOWN] [INFO] Disconnecting from emulator... 
[2025-06-02 04:35:59.017] [modbus] [UNKNOWN] [INFO] Disconnected 
[2025-06-02 04:35:59.018] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=2, quantity=2 
[2025-06-02 04:35:59.018] [modbus] [UNKNOWN] [INFO] Registers: [ 123, 456 ] [ 0, 0 ]
[2025-06-02 04:36:00.034] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=0, quantity=2 
[2025-06-02 04:36:00.035] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=2, quantity=2 
[2025-06-02 04:36:00.035] [modbus] [UNKNOWN] [INFO] Registers: [ 59, 456 ] [ 0, 0 ]
[2025-06-02 04:36:01.032] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=0, quantity=2 
[2025-06-02 04:36:01.033] [modbus] [UNKNOWN] [INFO] readHoldingRegisters: start=2, quantity=2 
[2025-06-02 04:36:01.034] [modbus] [UNKNOWN] [INFO] Registers: [ 36, 456 ] [ 0, 0 ]
```

- The `infinityChange()` method simulates a fluctuating register by assigning it a random value from the defined range every X milliseconds.
- All polling `fn` handlers must return Promises, even if the underlying methods are synchronous. Wrap calls in `async () => {}` as shown above.
- The polling manager supports retries, delays, hooks for error/success callbacks, and timeout protection.






### ❗ Important 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.








<br><br>

## 📎 Notes
- Each `fn[i]` is handled independently; one failing does not stop others.
- `onData(results)` is called only if all functions succeed, with `results[i]` matching `fn[i]`.
- Retries (`maxRetries`) are applied per function, with delay `delay = backoffDelay × attempt`.
- `taskTimeout` applies individually to each function call.
- `onError(error, index, attempt)` fires on each failed attempt.
- Use `getTaskState(id)` for detailed insight into task lifecycle.
- Suitable for advanced diagnostic loops, sensor polling, background watchdogs, or telemetry logging.

<br><br>

## <span id="tips-for-use">Tips for use</span>

- For Node.js, the `serialport` package is required (`npm install serialport`).
- For browser usage, HTTPS and Web Serial API support are required (**Google Chrome** и **Edge**).
- Use auto-detection methods to find port parameters and slaveId
- Use the client's `getDiagnostics()` and `resetDiagnostics()` methods for diagnostics.

<br><br>

## <span id="expansion">Expansion</span>
You can add your own Modbus functions by implementing a pair of `build...Request` and `parse...Response` functions in the `function-codes/` folder, then importing them into the ModbusClient in `modbus/client.js`

<br><br>

## <span id="changelog">CHANGELOG</span>
### **1.6.6 (2025-07-04)**
- Modbus functions have been optimized, now `response time` comes many times faster.
  > For example, previously reading a Holding register with 0 in the amount of 1 pc. took 100ms+, now the average response time is ~54ms
- Fix for reading packets, now packets are read by the specified length, depending on the function being sent. Before, packets were read by 1 byte. This allowed us to reduce the load and speed up the response time
  > If the function code that is used to send a packet from the private function _getExpectedResponseLength(pdu) is not specified for `ModbusClient`, then the packet is read by 1 byte

### **1.6.5 (2025-07-03)**
- New file structure for transports
  | Original Name              | New Location                                     |
  |----------------------------|--------------------------------------------------|
  | Web Serial Transport       | transport/web-transports/web-serialport.js       |
  | Web TCP Transport          | transport/web-transports/node-tcp-serialport.js  |
  | Node Serial Transport      | transport/node-transports/node-serialport.js     |
  | Node TCP Serial Transport  | transport/node-transports/node-tcp-serialport.js |
  > Full information on transport [here](#transports)

- Added **Web TCP Transport** and **Node TCP/IP Transport**

### **1.6.4 (2025-07-02)**
- Fixed `onData` function in **PollingManager** - now the function correctly parses the response from the Modbus RTU device.
> Previously, if the function's response to `onData()` was **null** or **undefined**, the function would stop working. Now, if the response is incorrect, the function skips a pool round, reports an error, and starts over.

### **1.6.2 (2025-06-27)**
- Fixed the `parseWriteMultipleRegistersResponse` function - the function now correctly parses the response from the Modbus RTU device.

### **1.6.1 (2025-06-25)**
- Fixed the `createTransport` function - import methods of the **WebSerial Transport**, **NodeSerial Transport**, **TcpSerialTransport** classes. Previously, there were problems with the initialization of these classes, due to which it was impossible to initialize the creation of transport and use it in the initialization of `ModbusClient`
- Removed `utils/getSerialPort.js` file because **Web Serial API** was unstable when receiving COM port data

### **1.6.0 (2025-06-02)**
- The entire library has been migrated from ESM to CommonJS
- Added **slave device emulator**, which works without physical and virtual COM port. ([see more information](#slave-emulator))
- Added TCP/IP transport([see more information](#transports))

### **1.5.7 (2025-05-30)**
- Fixed logic for `onData:` in PollingManager. Now when specifying parameters in `onData:(fn1, fn2)`, from the parameter `fn: [fn1(), fn2()]`, the result will be passed in full to each parameter of `onData: (fn1, fn2)` accordingly. Example:
  ```js
    poll.addTask({
      id: 'modbus-loop',
      interval: 1000,
      immediate: true,
      fn: [
          async () => await client.readHoldingRegisters(0, 2),
          async () => await client.readHoldingRegisters(2, 2),
      ],
      onData: (hold1, hold2) => {
          log.info('Registers:', hold1, hold2 );
      },
      onError: (error, index, attempt) => {
          console.warn(`Error in fn[${index}], attempt ${attempt}: ${error.message}`);
      },
      onStart: () => console.log('Polling started'),
      onStop: () => console.log('Polling stopped'),
      maxRetries: 3,
      backoffDelay: 300,
      taskTimeout: 2000
    });
  ```
  **Result**
  ```bash
                                                              fn: [0] (hold1)  fn: [1] (hold2)
  [2025-05-30 04:05:41.047] [modbus] [NODE] [INFO] Registers: [ 4866, 25629 ]   [ 1986, 0 ]
  ```

### **1.5.5 (2025-05-29)**
- Added logger for import via `modbus-connect/logger` ([see more information](#logger))
- Fixed a bug that caused a `Write timeout` warning immediately after disconnecting a device
- Added the ability to specify the data type when reading `Hold/Input` registers ([see more information](#type-data)):
  > If you don't specify a data type, the result will default to `uint16`
  - Exmaple without specifying the data type:
    ```js
    const regs = await client.readInputRegisters(0, 4) // default: uint16
    // ...4866, 25629, 1986, 0
    ```
  - Example with indication of data type
    ```js
    const regs = await client.readInputRegisters(0, 4, { type: 'hex' })
    // ...'1302', '641D', '07C2', '0000'
    ```

### **1.5.4 (2025-05-27)**
- Corrected usage guide in README.md

### **1.5.3 (2025-05-27)**
- Fixed the name of the `path` option on the `node` port of the transport. Now when initializing the transport, there is no need to rewrite the names of the options, they are the same for both transports
- Added initialization parameter for **ModbusClient** - `crcAlgorithm` ([see more information](#crc))