modbus-webserial
v0.11.2
Published
ESM library for speaking Modbus-RTU from the browser via Web Serial
Downloads
1,762
Maintainers
Readme
modbus-webserial
Zero-dependency library for communicating with a Modbus-RTU serial device from the browser via WebSerial.
Usage
Try the web UI at modbuswebui.dev
Establish connection and read/write in browser
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
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
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(ordist/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
The following demos are fully self‑contained HTML files, served via GitHub Pages:
Basic Read/Write Demo Simple page to connect, read two registers, and write two registers.
64‑Register Smoke Test Automated loop testing read/write of 64 registers, coils, and discrete inputs with live counters and error logging.
Modbus Web-UI 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:
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):
// 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):
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
postTimeoutWaitPeriodis0(default), the next request is sent immediately after a timeout. - If
postTimeoutWaitPeriodis> 0, the nexttransact()call will delay sending the request for the specified period, while discarding any bytes received during that window.
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.
