modbus-webserial 0.10.0 → 0.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -16
- package/dist/index.d.ts +16 -2
- package/dist/index.js +110 -36
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# modbus-webserial
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Zero-dependency library for communicating with a Modbus-RTU serial device from the browser via WebSerial.
|
|
4
4
|
|
|
5
|
+
[](https://github.com/louisfoster/awesome-web-serial#code-utilities)
|
|
5
6
|

|
|
6
7
|

|
|
7
8
|

|
|
@@ -10,14 +11,13 @@ Tiny zero-dependency library for communicating with a Modbus-RTU serial device f
|
|
|
10
11
|

|
|
11
12
|

|
|
12
13
|
|
|
13
|
-
## Install
|
|
14
|
-
|
|
15
|
-
```bash
|
|
16
|
-
npm install modbus-webserial
|
|
17
|
-
```
|
|
18
14
|
|
|
19
15
|
## Usage
|
|
20
|
-
**
|
|
16
|
+
**Try the web UI at**
|
|
17
|
+
**[modbuswebui.dev](https://modbuswebui.dev/)**
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
**Establish connection and read/write in browser**
|
|
21
21
|
```javascript
|
|
22
22
|
import { ModbusRTU } from 'modbus-webserial';
|
|
23
23
|
|
|
@@ -25,13 +25,25 @@ import { ModbusRTU } from 'modbus-webserial';
|
|
|
25
25
|
const client = await ModbusRTU.openWebSerial({ baudRate: 9600 });
|
|
26
26
|
client.setID(1);
|
|
27
27
|
|
|
28
|
-
// Read holding registers
|
|
28
|
+
// Read two holding registers from 0x0000 (0x0000 and 0x0001)
|
|
29
29
|
const { data } = await client.readHoldingRegisters(0, 2);
|
|
30
30
|
console.log('HR0=', data[0], 'HR1=', data[1]);
|
|
31
31
|
|
|
32
|
-
// Write values to holding registers 0x00 and 0x01
|
|
32
|
+
// Write values to holding registers 0x00 and 0x01 (i.e. two registers from 0x0000)
|
|
33
33
|
await client.writeRegisters(0, [0x0A, 0x0B]);
|
|
34
34
|
```
|
|
35
|
+
**You can also manually supply the port**
|
|
36
|
+
```javascript
|
|
37
|
+
const [port] = await navigator.serial.getPorts()
|
|
38
|
+
const client = await ModbusRTU.openWebSerial({ baudRate: 9600, port })
|
|
39
|
+
|
|
40
|
+
// or on connect
|
|
41
|
+
navigator.serial.addEventListener("connect", ({target: port}) => {
|
|
42
|
+
const client = await ModbusRTU.openWebSerial({ baudRate: 9600, port })
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
35
47
|
**Can also be used *without* WebSerial for building modbus frames in any environment**
|
|
36
48
|
```javascript
|
|
37
49
|
import {
|
|
@@ -42,15 +54,18 @@ import {
|
|
|
42
54
|
// Build a “Read Holding Registers” frame (ID=1, addr=0, qty=2)
|
|
43
55
|
const rawRead = buildReadHoldingRegisters(1, 0x00, 2);
|
|
44
56
|
console.log(rawRead);
|
|
45
|
-
//
|
|
57
|
+
// Uint8Array [0x01, 0x03, 0x00, 0x00, 0x00, 0x02, CRC_LO, CRC_HI]
|
|
46
58
|
|
|
47
59
|
// Build a “Write Multiple Registers” frame (ID=1, addr=0, values=[10,11])
|
|
48
60
|
const rawWrite = buildWriteRegisters(1, 0x00, [0x0A, 0x0B]);
|
|
49
61
|
console.log(rawWrite);
|
|
50
|
-
//
|
|
62
|
+
// Uint8Array [0x01, 0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00,0x0A, 0x00,0x0B, CRC_LO, CRC_HI]
|
|
51
63
|
```
|
|
52
64
|
> [!TIP]
|
|
53
65
|
> Check `src/index.ts` (or `dist/index.js`) for all exports
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
54
69
|
## Supported Functions
|
|
55
70
|
|
|
56
71
|
### Modbus Data Functions
|
|
@@ -86,6 +101,11 @@ Utility and configuration methods exposed on `ModbusRTU`:
|
|
|
86
101
|
| `getID()` | Get the current slave ID |
|
|
87
102
|
| `setTimeout(ms)` | Set transaction timeout (ms) |
|
|
88
103
|
| `getTimeout()` | Get current timeout (ms) |
|
|
104
|
+
| `getPort()` | Get 'SerialPort' instance[^1] |
|
|
105
|
+
|
|
106
|
+
[^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.
|
|
107
|
+
|
|
108
|
+
---
|
|
89
109
|
|
|
90
110
|
## Examples
|
|
91
111
|
|
|
@@ -96,6 +116,25 @@ The following demos are fully self‑contained HTML files, served via GitHub Pag
|
|
|
96
116
|
* [64‑Register Smoke Test](https://anttikotajarvi.github.io/modbus-webserial/examples/smoke-test/)
|
|
97
117
|
Automated loop testing read/write of 64 registers, coils, and discrete inputs with live counters and error logging.
|
|
98
118
|
|
|
119
|
+
* [Modbus Web-UI](https://modbuswebui.dev)
|
|
120
|
+
MB Master web app with UX fetures etc.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Behavior
|
|
125
|
+
### CRC policy (strict vs resync)
|
|
126
|
+
|
|
127
|
+
By default the transport uses **strict** CRC checking: if a received Modbus RTU frame fails CRC validation, the request fails with `CrcError`.
|
|
128
|
+
|
|
129
|
+
You can switch to a more tolerant mode that tries to recover from occasional line noise:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
const client = await ModbusRTU.openWebSerial({
|
|
133
|
+
baudRate: 9600,
|
|
134
|
+
crcPolicy: { mode: "resync", maxResyncDrops: 32 },
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
99
138
|
## Current state
|
|
100
139
|
* **v0.10**: Full modbus data-access coverage
|
|
101
140
|
* **v0.9**: Full passing tests, smoke test passed, complete README, build scripts in place
|
|
@@ -104,8 +143,4 @@ The following demos are fully self‑contained HTML files, served via GitHub Pag
|
|
|
104
143
|
|
|
105
144
|
## Roadmap
|
|
106
145
|
|
|
107
|
-
* **v1.0.0**:
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
© 2025 Antti Kotajärvi
|
|
146
|
+
* **v1.0.0**: Create and document more tests for different boards using different browsers.
|
package/dist/index.d.ts
CHANGED
|
@@ -2,10 +2,18 @@ interface WebSerialOptions {
|
|
|
2
2
|
baudRate?: number;
|
|
3
3
|
dataBits?: 7 | 8;
|
|
4
4
|
stopBits?: 1 | 2;
|
|
5
|
-
parity?:
|
|
5
|
+
parity?: "none" | "even" | "odd";
|
|
6
6
|
requestFilters?: SerialPortFilter[];
|
|
7
|
+
port?: SerialPort;
|
|
7
8
|
timeout?: number;
|
|
9
|
+
crcPolicy?: CrcPolicy;
|
|
8
10
|
}
|
|
11
|
+
type CrcPolicy = {
|
|
12
|
+
mode: "strict";
|
|
13
|
+
} | {
|
|
14
|
+
mode: "resync";
|
|
15
|
+
maxResyncDrops?: number;
|
|
16
|
+
};
|
|
9
17
|
|
|
10
18
|
/** Result payloads returned by high-level helpers */
|
|
11
19
|
interface ReadCoilResult {
|
|
@@ -61,6 +69,7 @@ declare class ModbusRTU {
|
|
|
61
69
|
getID(): number;
|
|
62
70
|
setTimeout(ms: number): void;
|
|
63
71
|
getTimeout(): number;
|
|
72
|
+
getPort(): SerialPort;
|
|
64
73
|
/** FC 01 – coils */
|
|
65
74
|
readCoils(addr: number, qty: number): Promise<ReadCoilResult>;
|
|
66
75
|
/** FC 02 – discrete inputs */
|
|
@@ -193,6 +202,11 @@ declare function crc16(buf: Uint8Array): number;
|
|
|
193
202
|
declare class CrcError extends Error {
|
|
194
203
|
constructor();
|
|
195
204
|
}
|
|
205
|
+
declare class ResyncError extends Error {
|
|
206
|
+
readonly drops: number;
|
|
207
|
+
readonly maxDrops: number;
|
|
208
|
+
constructor(drops: number, maxDrops: number);
|
|
209
|
+
}
|
|
196
210
|
declare class TimeoutError extends Error {
|
|
197
211
|
constructor();
|
|
198
212
|
}
|
|
@@ -201,4 +215,4 @@ declare class ExceptionError extends Error {
|
|
|
201
215
|
constructor(code: number);
|
|
202
216
|
}
|
|
203
217
|
|
|
204
|
-
export { CrcError, ExceptionError, type MaskWriteResult, ModbusRTU, type ReadFifoResult, type ReadRegisterResult, TimeoutError, type WebSerialOptions, type WriteFileResult, type WriteRegisterResult, buildMaskWriteRegister, buildReadCoils, buildReadDiscreteInputs, buildReadFifoQueue, buildReadFileRecord, buildReadHolding, buildReadInputRegisters, buildReadWriteMultiple, buildWriteFileRecord, buildWriteMultiple, buildWriteMultipleCoils, buildWriteSingle, buildWriteSingleCoil, crc16, parseMaskWriteRegister, parseReadCoils, parseReadDiscreteInputs, parseReadFifoQueue, parseReadFileRecord, parseReadHolding, parseReadInputRegisters, parseReadWriteMultiple, parseWriteSingle, parseWriteSingleCoil };
|
|
218
|
+
export { CrcError, ExceptionError, type MaskWriteResult, ModbusRTU, type ReadFifoResult, type ReadRegisterResult, ResyncError, TimeoutError, type WebSerialOptions, type WriteFileResult, type WriteRegisterResult, buildMaskWriteRegister, buildReadCoils, buildReadDiscreteInputs, buildReadFifoQueue, buildReadFileRecord, buildReadHolding, buildReadInputRegisters, buildReadWriteMultiple, buildWriteFileRecord, buildWriteMultiple, buildWriteMultipleCoils, buildWriteSingle, buildWriteSingleCoil, crc16, parseMaskWriteRegister, parseReadCoils, parseReadDiscreteInputs, parseReadFifoQueue, parseReadFileRecord, parseReadHolding, parseReadInputRegisters, parseReadWriteMultiple, parseWriteSingle, parseWriteSingleCoil };
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,14 @@ var CrcError = class extends Error {
|
|
|
15
15
|
super("CRC check failed");
|
|
16
16
|
}
|
|
17
17
|
};
|
|
18
|
+
var ResyncError = class extends Error {
|
|
19
|
+
constructor(drops, maxDrops) {
|
|
20
|
+
super(`Resync failed (dropped ${drops}/${maxDrops} bytes)`);
|
|
21
|
+
this.name = "ResyncError";
|
|
22
|
+
this.drops = drops;
|
|
23
|
+
this.maxDrops = maxDrops;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
18
26
|
var TimeoutError = class extends Error {
|
|
19
27
|
constructor() {
|
|
20
28
|
super("Modbus response timed out");
|
|
@@ -41,12 +49,16 @@ function crc16(buf) {
|
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
// src/transport/webserial.ts
|
|
52
|
+
var DEFAULT_CRC_POLICY = { mode: "strict" };
|
|
53
|
+
var DEFAULT_MAX_RESYNC_DROPS = 32;
|
|
44
54
|
var WebSerialTransport = class _WebSerialTransport {
|
|
45
55
|
constructor() {
|
|
46
56
|
this.timeout = 500;
|
|
47
57
|
this.rxBuf = new Uint8Array(0);
|
|
58
|
+
// rolling buffer across calls
|
|
59
|
+
this.strictCrc = true;
|
|
60
|
+
this.maxResyncDrops = DEFAULT_MAX_RESYNC_DROPS;
|
|
48
61
|
}
|
|
49
|
-
// rolling buffer across calls
|
|
50
62
|
// timeout gelpers
|
|
51
63
|
setTimeout(ms) {
|
|
52
64
|
this.timeout = ms;
|
|
@@ -54,6 +66,9 @@ var WebSerialTransport = class _WebSerialTransport {
|
|
|
54
66
|
getTimeout() {
|
|
55
67
|
return this.timeout;
|
|
56
68
|
}
|
|
69
|
+
getPort() {
|
|
70
|
+
return this.port;
|
|
71
|
+
}
|
|
57
72
|
// ----------------------------------------------------------------
|
|
58
73
|
// Factory
|
|
59
74
|
// ----------------------------------------------------------------
|
|
@@ -63,72 +78,127 @@ var WebSerialTransport = class _WebSerialTransport {
|
|
|
63
78
|
return t;
|
|
64
79
|
}
|
|
65
80
|
async init(opts) {
|
|
66
|
-
var _a, _b, _c, _d, _e, _f;
|
|
81
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
67
82
|
this.timeout = (_a = opts.timeout) != null ? _a : 500;
|
|
68
|
-
this.port =
|
|
83
|
+
this.port = (_c = opts.port) != null ? _c : await navigator.serial.requestPort({
|
|
84
|
+
filters: (_b = opts.requestFilters) != null ? _b : []
|
|
85
|
+
});
|
|
69
86
|
await this.port.open({
|
|
70
|
-
baudRate: (
|
|
71
|
-
dataBits: (
|
|
72
|
-
stopBits: (
|
|
73
|
-
parity: (
|
|
87
|
+
baudRate: (_d = opts.baudRate) != null ? _d : 9600,
|
|
88
|
+
dataBits: (_e = opts.dataBits) != null ? _e : 8,
|
|
89
|
+
stopBits: (_f = opts.stopBits) != null ? _f : 1,
|
|
90
|
+
parity: (_g = opts.parity) != null ? _g : "none"
|
|
74
91
|
});
|
|
75
92
|
this.reader = this.port.readable.getReader();
|
|
76
93
|
this.writer = this.port.writable.getWriter();
|
|
94
|
+
const policy = (_h = opts.crcPolicy) != null ? _h : DEFAULT_CRC_POLICY;
|
|
95
|
+
if (policy.mode === "strict") {
|
|
96
|
+
this.strictCrc = true;
|
|
97
|
+
this.maxResyncDrops = 0;
|
|
98
|
+
} else {
|
|
99
|
+
this.strictCrc = false;
|
|
100
|
+
this.maxResyncDrops = (_i = policy.maxResyncDrops) != null ? _i : DEFAULT_MAX_RESYNC_DROPS;
|
|
101
|
+
}
|
|
77
102
|
}
|
|
78
103
|
// ----------------------------------------------------------------
|
|
79
104
|
// transact(): Send `req` and await a response whose function-code matches the request
|
|
80
105
|
// ---------------------------------------------------------------- */
|
|
81
106
|
async transact(req) {
|
|
82
107
|
await this.writer.write(req);
|
|
108
|
+
const STRICT_CRC = this.strictCrc;
|
|
109
|
+
const MAX_RESYNC_DROPS = this.maxResyncDrops;
|
|
83
110
|
const expectedFC = req[1] & 127;
|
|
84
111
|
const deadline = Date.now() + this.timeout;
|
|
112
|
+
let State;
|
|
113
|
+
((State2) => {
|
|
114
|
+
State2[State2["TRY_EXTRACT"] = 0] = "TRY_EXTRACT";
|
|
115
|
+
State2[State2["HAVE_FRAME"] = 1] = "HAVE_FRAME";
|
|
116
|
+
State2[State2["BAD_CRC"] = 2] = "BAD_CRC";
|
|
117
|
+
State2[State2["NEED_READ"] = 3] = "NEED_READ";
|
|
118
|
+
})(State || (State = {}));
|
|
119
|
+
let state = 0 /* TRY_EXTRACT */;
|
|
120
|
+
let frame = null;
|
|
121
|
+
let discardedBytes = 0;
|
|
85
122
|
while (true) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
123
|
+
switch (state) {
|
|
124
|
+
case 0 /* TRY_EXTRACT */: {
|
|
125
|
+
frame = this.extractFrame();
|
|
126
|
+
if (!frame) {
|
|
127
|
+
state = 3 /* NEED_READ */;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
state = 1 /* HAVE_FRAME */;
|
|
131
|
+
}
|
|
132
|
+
case 1 /* HAVE_FRAME */: {
|
|
133
|
+
if (this.crcOk(frame)) {
|
|
134
|
+
const fc = frame[1] & 127;
|
|
135
|
+
if (fc === expectedFC) return frame;
|
|
136
|
+
frame = null;
|
|
137
|
+
state = 0 /* TRY_EXTRACT */;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
state = 2 /* BAD_CRC */;
|
|
141
|
+
}
|
|
142
|
+
case 2 /* BAD_CRC */: {
|
|
143
|
+
if (STRICT_CRC) throw new CrcError();
|
|
144
|
+
discardedBytes += frame ? frame.length : 0;
|
|
145
|
+
frame = null;
|
|
146
|
+
if (discardedBytes >= MAX_RESYNC_DROPS) {
|
|
147
|
+
throw new ResyncError(discardedBytes, MAX_RESYNC_DROPS);
|
|
148
|
+
}
|
|
149
|
+
if (this.rxBuf.length > 0) {
|
|
150
|
+
state = 0 /* TRY_EXTRACT */;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
state = 3 /* NEED_READ */;
|
|
154
|
+
}
|
|
155
|
+
case 3 /* NEED_READ */: {
|
|
156
|
+
if (Date.now() > deadline) throw new TimeoutError();
|
|
157
|
+
const { value } = await this.reader.read();
|
|
158
|
+
if (!value) throw new TimeoutError();
|
|
159
|
+
this.rxBuf = concat(this.rxBuf, value);
|
|
160
|
+
state = 0 /* TRY_EXTRACT */;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
default:
|
|
164
|
+
throw new Error("invalid state");
|
|
113
165
|
}
|
|
114
166
|
}
|
|
115
167
|
}
|
|
168
|
+
extractFrame() {
|
|
169
|
+
if (this.rxBuf.length < 3) return null;
|
|
170
|
+
const need = this.frameLengthIfComplete(this.rxBuf);
|
|
171
|
+
if (need === 0) return null;
|
|
172
|
+
const frame = this.rxBuf.slice(0, need);
|
|
173
|
+
this.rxBuf = this.rxBuf.slice(need);
|
|
174
|
+
return frame;
|
|
175
|
+
}
|
|
176
|
+
crcOk(frame) {
|
|
177
|
+
const crc = crc16(frame.subarray(0, frame.length - 2));
|
|
178
|
+
const crcOk = (crc & 255) === frame[frame.length - 2] && crc >> 8 === frame[frame.length - 1];
|
|
179
|
+
return crcOk;
|
|
180
|
+
}
|
|
116
181
|
// ----------------------------------------------------------------
|
|
117
182
|
// Determine expected length; return 0 if we still need more bytes
|
|
118
183
|
// ----------------------------------------------------------------
|
|
119
184
|
frameLengthIfComplete(buf) {
|
|
120
185
|
if (buf.length < 3) return 0;
|
|
186
|
+
const id = buf[0];
|
|
121
187
|
const fc = buf[1];
|
|
188
|
+
if (id === 0 || id > 247) return 0;
|
|
122
189
|
if (fc & 128) return buf.length >= 5 ? 5 : 0;
|
|
123
190
|
if (fc === 1 || fc === 2 || fc === 3 || fc === 4) {
|
|
124
|
-
const
|
|
191
|
+
const byteCount = buf[2];
|
|
192
|
+
if (byteCount > 252) {
|
|
193
|
+
return buf.length >= 8 ? 8 : 0;
|
|
194
|
+
}
|
|
195
|
+
const need = 3 + byteCount + 2;
|
|
125
196
|
return buf.length >= need ? need : 0;
|
|
126
197
|
}
|
|
127
198
|
if (fc === 5 || fc === 6 || fc === 15 || fc === 16)
|
|
128
199
|
return buf.length >= 8 ? 8 : 0;
|
|
129
200
|
return buf.length >= 8 ? 8 : 0;
|
|
130
201
|
}
|
|
131
|
-
// -- close port --
|
|
132
202
|
async close() {
|
|
133
203
|
var _a, _b, _c;
|
|
134
204
|
await ((_a = this.reader) == null ? void 0 : _a.cancel());
|
|
@@ -504,6 +574,9 @@ var ModbusRTU = class _ModbusRTU {
|
|
|
504
574
|
getTimeout() {
|
|
505
575
|
return this.transport.getTimeout();
|
|
506
576
|
}
|
|
577
|
+
getPort() {
|
|
578
|
+
return this.transport.getPort();
|
|
579
|
+
}
|
|
507
580
|
/* ========================= READ =============================== */
|
|
508
581
|
/** FC 01 – coils */
|
|
509
582
|
async readCoils(addr, qty) {
|
|
@@ -593,6 +666,7 @@ export {
|
|
|
593
666
|
CrcError,
|
|
594
667
|
ExceptionError,
|
|
595
668
|
ModbusRTU,
|
|
669
|
+
ResyncError,
|
|
596
670
|
TimeoutError,
|
|
597
671
|
buildMaskWriteRegister,
|
|
598
672
|
buildReadCoils,
|
package/package.json
CHANGED