# modbus-webserial

Zero-dependency library for communicating with a Modbus-RTU serial device from the browser via WebSerial.

[![Mentioned in Awesome](https://awesome.re/mentioned-badge.svg)](https://github.com/louisfoster/awesome-web-serial#code-utilities)
![npm](https://img.shields.io/npm/v/modbus-webserial)
![size](https://img.shields.io/bundlephobia/minzip/modbus-webserial)
![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
![CI](https://img.shields.io/github/actions/workflow/status/anttikotajarvi/modbus-webserial/ci.yaml?branch=main)
[![License](https://img.shields.io/npm/l/modbus-webserial)](./LICENSE)
![types](https://img.shields.io/npm/types/modbus-webserial)
![esm](https://img.shields.io/badge/esm-%F0%9F%9A%80-green)


## Usage
**Try the web UI at**
**[modbuswebui.dev](https://modbuswebui.dev/)**


**Establish connection and read/write in browser**
```javascript
import { ModbusRTU } from 'modbus-webserial';

// Prompt the WebSerial dialog and open port
const client = await ModbusRTU.openWebSerial({ baudRate: 9600 });
client.setID(1);

// Read two holding registers from 0x0000 (0x0000 and 0x0001)
const { data } = await client.readHoldingRegisters(0, 2);
console.log('HR0=', data[0], 'HR1=', data[1]);

// Write values to holding registers 0x00 and 0x01 (i.e. two registers from 0x0000)
await client.writeRegisters(0, [0x0A, 0x0B]);
```
**You can also manually supply the port** 
```javascript
const [port] = await navigator.serial.getPorts()
const client = await ModbusRTU.openWebSerial({ baudRate: 9600, port })

// or on connect
navigator.serial.addEventListener("connect", ({target: port}) => {
   const client = await ModbusRTU.openWebSerial({ baudRate: 9600, port })
})
```

---
**Can also be used *without* WebSerial for building modbus frames in any environment**
```javascript
import {
  buildReadHoldingRegisters,
  buildWriteRegisters
} from 'modbus-webserial';

// Build a “Read Holding Registers” frame (ID=1, addr=0, qty=2)
const rawRead = buildReadHoldingRegisters(1, 0x00, 2);
console.log(rawRead);
// Uint8Array [0x01, 0x03, 0x00, 0x00, 0x00, 0x02, CRC_LO, CRC_HI]

// Build a “Write Multiple Registers” frame (ID=1, addr=0, values=[10,11])
const rawWrite = buildWriteRegisters(1, 0x00, [0x0A, 0x0B]);
console.log(rawWrite);
// Uint8Array [0x01, 0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00,0x0A, 0x00,0x0B, CRC_LO, CRC_HI]
```
> [!TIP]
> Check `src/index.ts` (or `dist/index.js`) for all exports 

---

## Supported Functions

### Modbus Data Functions

The following Modbus-RTU function calls are implemented:

| Function                          | Description                              |
| --------------------------------- | ---------------------------------------- |
| `readCoils(addr, qty)`            | FC 01 – Read coil status                 |
| `readDiscreteInputs(addr, qty)`   | FC 02 – Read discrete input status       |
| `readHoldingRegisters(addr, qty)` | FC 03 – Read holding registers           |
| `readInputRegisters(addr, qty)`   | FC 04 – Read input registers             |
| `writeCoil(addr, state)`          | FC 05 – Write single coil                |
| `writeRegister(addr, value)`      | FC 06 – Write single holding register    |
| `writeCoils(addr, states)`        | FC 15 – Write multiple coils             |
| `writeRegisters(addr, values)`    | FC 16 – Write multiple holding registers |
| `readFileRecord(file, rec, len)`  | FC 20 – Read file record (single ref)    |
| `writeFileRecord(file, rec, vals)`| FC 21 – Write file record (single ref)   |
| `maskWriteRegister(addr, and, or)`| FC 22 – Mask write register              |
| `readWriteRegisters(rAddr, rQty, wAddr, vals)` | FC 23 – Read/write multiple regs |
| `readFifoQueue(addr)`             | FC 24 – Read FIFO queue                  |
> [!CAUTION]
> Not all slave libraries support file records, FIFO queues, mask writes or read-write calls
### Auxiliary Client Methods

Utility and configuration methods exposed on `ModbusRTU`:

| Method                   | Purpose                             |
| ------------------------ | ----------------------------------- |
| `openWebSerial(options)` | Open a serial port via WebSerial    |
| `close()`                | Close the current serial connection |
| `setID(id)`              | Set the Modbus slave ID             |
| `getID()`                | Get the current slave ID            |
| `setTimeout(ms)`         | Set transaction timeout (ms)        |
| `getTimeout()`           | Get current timeout (ms)            |
| `getPort()`              | Get 'SerialPort' instance[^1]          |

[^1]: The returned `SerialPort` instance from `getPort` can be used to access properties such as `usbVendorId` and `usbProductId` for retrieving information about the connected USB device.

---

## [Examples](https://anttikotajarvi.github.io/modbus-webserial/examples/)
The following demos are fully self‑contained HTML files, served via GitHub Pages:

* [Basic Read/Write Demo](https://anttikotajarvi.github.io/modbus-webserial/examples/basic-demo/)
  Simple page to connect, read two registers, and write two registers.
* [64‑Register Smoke Test](https://anttikotajarvi.github.io/modbus-webserial/examples/smoke-test/)
  Automated loop testing read/write of 64 registers, coils, and discrete inputs with live counters and error logging.

* [Modbus Web-UI](https://modbuswebui.dev)
  MB Master web app with UX fetures etc.

--- 

## Behavior
### CRC policy (strict vs resync)

By default the transport uses **strict** CRC checking: if a received Modbus RTU frame fails CRC validation, the request fails with `CrcError`.

You can switch to a more tolerant mode that tries to recover from occasional line noise:

```js
const client = await ModbusRTU.openWebSerial({
  baudRate: 9600,
  crcPolicy: { mode: "resync", maxResyncDrops: 32 },
});
```

### Concurrency
The WebSerial transport is **single-flight**: it can only have one in-progress Modbus RTU transaction at a time.

This is intentional. Modbus RTU does not include a transaction identifier, so overlapping requests on the same serial link makes it ambiguous which response belongs to which request.

There is **no internal request queue**. To avoid abstracting away the serial “one request at a time” nature of Modbus RTU, starting a new transaction while another is in progress will throw an error immediately.

#### Wrong (concurrent calls on the same client/transport):
```js
// Two requests started without awaiting the first one.
// This will throw (single-flight).
const p1 = client.readHoldingRegisters(0x0000, 2);
const p2 = client.readHoldingRegisters(0x0010, 2); 
// -> Uncaught Error: Concurrent transact() calls are not supported

await Promise.all([p1, p2]);
```

#### Right (serialize with `await`):
```js
await client.readHoldingRegisters(0x0000, 2);
await client.readHoldingRegisters(0x0010, 2);
```

### Post-timeout recovery

When a request times out, a Modbus RTU slave may still send a **late reply** afterwards. If you immediately send a new request (that is similar enough to the old one), that late reply can be mistaken as the response for the new request.

To reduce that risk (along with strong request/response matching), the transport supports a post-timeout “quarantine” window.

* If `postTimeoutWaitPeriod` is `0` (default), the next request is sent immediately after a timeout.
* If `postTimeoutWaitPeriod` is `> 0`, the next `transact()` call will **delay sending** the request for the specified period, while **discarding** any bytes received during that window.

```js
const client = await ModbusRTU.openWebSerial({
  // ...
  postTimeoutWaitPeriod: 20, // ms
});
```

Notes:
* This affects **only the request after a timeout**.
* When you use the client sequentially (with `await`), this shows up as a small delay before the next request. It does **not** change how timeouts are thrown or how you catch them.


## Current state
* **v0.11**: Transport rework to handle edge cases explicitly
* **v0.10**: Full modbus data-access coverage
* **v0.9**: Full passing tests, smoke test passed, complete README, build scripts in place
* **Beta**: Full Modbus RTU function‑code coverage
* **Alpha**: Basic structure and layout

## Roadmap

* **v1.0.0**: Create and document more tests for different boards using different browsers.
