modbus-webserial 0.10.1 → 0.11.0

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 CHANGED
@@ -1,7 +1,8 @@
1
1
  # modbus-webserial
2
2
 
3
- Tiny zero-dependency library for communicating with a Modbus-RTU serial device from the browser via WebSerial.
3
+ Zero-dependency library for communicating with a Modbus-RTU serial device from the browser via WebSerial.
4
4
 
5
+ [![Mentioned in Awesome](https://awesome.re/mentioned-badge.svg)](https://github.com/louisfoster/awesome-web-serial#code-utilities)
5
6
  ![npm](https://img.shields.io/npm/v/modbus-webserial)
6
7
  ![size](https://img.shields.io/bundlephobia/minzip/modbus-webserial)
7
8
  ![dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
@@ -10,14 +11,13 @@ Tiny zero-dependency library for communicating with a Modbus-RTU serial device f
10
11
  ![types](https://img.shields.io/npm/types/modbus-webserial)
11
12
  ![esm](https://img.shields.io/badge/esm-%F0%9F%9A%80-green)
12
13
 
13
- ## Install
14
-
15
- ```bash
16
- npm install modbus-webserial
17
- ```
18
14
 
19
15
  ## Usage
20
- **Connect → read/write in browser**
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 0x00 and 0x01
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
- // Uint8Array [0x01, 0x03, 0x00, 0x00, 0x00, 0x02, CRC_LO, CRC_HI]
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
- // Uint8Array [0x01, 0x10, 0x00, 0x00, 0x00, 0x02, 0x04, 0x00,0x0A, 0x00,0x0B, CRC_LO, CRC_HI]
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,7 +116,72 @@ 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
+
138
+ ### Concurrency
139
+ The WebSerial transport is **single-flight**: it can only have one in-progress Modbus RTU transaction at a time.
140
+
141
+ 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.
142
+
143
+ 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.
144
+
145
+ #### Wrong (concurrent calls on the same client/transport):
146
+ ```js
147
+ // Two requests started without awaiting the first one.
148
+ // This will throw (single-flight).
149
+ const p1 = client.readHoldingRegisters(0x0000, 2);
150
+ const p2 = client.readHoldingRegisters(0x0010, 2);
151
+ // -> Uncaught Error: Concurrent transact() calls are not supported
152
+
153
+ await Promise.all([p1, p2]);
154
+ ```
155
+
156
+ #### Right (serialize with `await`):
157
+ ```js
158
+ await client.readHoldingRegisters(0x0000, 2);
159
+ await client.readHoldingRegisters(0x0010, 2);
160
+ ```
161
+
162
+ ### Post-timeout recovery
163
+
164
+ 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.
165
+
166
+ To reduce that risk (along with strong request/response matching), the transport supports a post-timeout “quarantine” window.
167
+
168
+ * If `postTimeoutWaitPeriod` is `0` (default), the next request is sent immediately after a timeout.
169
+ * 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.
170
+
171
+ ```js
172
+ const client = await ModbusRTU.openWebSerial({
173
+ // ...
174
+ postTimeoutWaitPeriod: 20, // ms
175
+ });
176
+ ```
177
+
178
+ Notes:
179
+ * This affects **only the request after a timeout**.
180
+ * 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.
181
+
182
+
99
183
  ## Current state
184
+ * **v0.11**: Transport rework to handle edge cases explicitly
100
185
  * **v0.10**: Full modbus data-access coverage
101
186
  * **v0.9**: Full passing tests, smoke test passed, complete README, build scripts in place
102
187
  * **Beta**: Full Modbus RTU function‑code coverage
@@ -104,8 +189,4 @@ The following demos are fully self‑contained HTML files, served via GitHub Pag
104
189
 
105
190
  ## Roadmap
106
191
 
107
- * **v1.0.0**: Finalize API, apply bug fixes, refine docs, production-ready release
108
-
109
- ---
110
-
111
- © 2025 Antti Kotajärvi
192
+ * **v1.0.0**: Create and document more tests for different boards using different browsers.
package/dist/index.d.ts CHANGED
@@ -1,11 +1,23 @@
1
- interface WebSerialOptions {
1
+ interface WebSerialOptions extends WebSerialConfig {
2
+ requestFilters?: SerialPortFilter[];
3
+ port?: SerialPort;
4
+ }
5
+ interface WebSerialConfig {
2
6
  baudRate?: number;
3
7
  dataBits?: 7 | 8;
4
8
  stopBits?: 1 | 2;
5
- parity?: 'none' | 'even' | 'odd';
6
- requestFilters?: SerialPortFilter[];
9
+ parity?: "none" | "even" | "odd";
7
10
  timeout?: number;
11
+ crcPolicy?: Required<CrcPolicy>;
12
+ postTimeoutWaitPeriod?: number;
13
+ interRequestDelay?: number;
8
14
  }
15
+ type CrcPolicy = {
16
+ mode: "strict";
17
+ } | {
18
+ mode: "resync";
19
+ maxResyncDrops?: number;
20
+ };
9
21
 
10
22
  /** Result payloads returned by high-level helpers */
11
23
  interface ReadCoilResult {
@@ -194,12 +206,26 @@ declare function crc16(buf: Uint8Array): number;
194
206
  declare class CrcError extends Error {
195
207
  constructor();
196
208
  }
209
+ declare class ResyncError extends Error {
210
+ readonly drops: number;
211
+ readonly maxDrops: number;
212
+ constructor(drops: number, maxDrops: number);
213
+ }
197
214
  declare class TimeoutError extends Error {
198
215
  constructor();
199
216
  }
217
+ declare class StreamClosedError extends Error {
218
+ constructor();
219
+ }
200
220
  declare class ExceptionError extends Error {
201
221
  code: number;
202
222
  constructor(code: number);
203
223
  }
204
224
 
205
- 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 };
225
+ declare const BUILD_INFO: {
226
+ readonly version: string;
227
+ readonly commit: string;
228
+ readonly buildTime: string;
229
+ };
230
+
231
+ export { BUILD_INFO, CrcError, ExceptionError, type MaskWriteResult, ModbusRTU, type ReadFifoResult, type ReadRegisterResult, ResyncError, StreamClosedError, 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,11 +15,24 @@ 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");
21
29
  }
22
30
  };
31
+ var StreamClosedError = class extends Error {
32
+ constructor() {
33
+ super("Modbus stream closed");
34
+ }
35
+ };
23
36
  var ExceptionError = class extends Error {
24
37
  constructor(code) {
25
38
  var _a;
@@ -40,19 +53,59 @@ function crc16(buf) {
40
53
  return crc;
41
54
  }
42
55
 
56
+ // src/core/types.ts
57
+ var FC_READ_HOLDING_REGISTERS = 3;
58
+ var FC_WRITE_SINGLE_HOLDING_REGISTER = 6;
59
+ var FC_WRITE_MULTIPLE_HOLDING_REGISTERS = 16;
60
+ var FC_READ_COILS = 1;
61
+ var FC_WRITE_SINGLE_COIL = 5;
62
+ var FC_WRITE_MULTIPLE_COILS = 15;
63
+ var FC_READ_INPUT_REGISTERS = 4;
64
+ var FC_READ_DISCRETE_INPUTS = 2;
65
+ var FC_READ_FILE_RECORD = 20;
66
+ var FC_WRITE_FILE_RECORD = 21;
67
+ var FC_MASK_WRITE_REGISTER = 22;
68
+ var FC_READ_WRITE_MULTIPLE_REGISTERS = 23;
69
+ var FC_READ_FIFO_QUEUE = 24;
70
+
43
71
  // src/transport/webserial.ts
44
- var WebSerialTransport = class _WebSerialTransport {
72
+ var DEFAULT_CRC_POLICY = { mode: "strict" };
73
+ var DEFAULT_MAX_RESYNC_DROPS = 32;
74
+ var DEFAULTS = {
75
+ baudRate: 9600,
76
+ dataBits: 8,
77
+ stopBits: 1,
78
+ parity: "none",
79
+ timeout: 500,
80
+ crcPolicy: DEFAULT_CRC_POLICY,
81
+ postTimeoutWaitPeriod: 0,
82
+ interRequestDelay: 0
83
+ };
84
+ var _WebSerialTransport = class _WebSerialTransport {
45
85
  constructor() {
46
- this.timeout = 500;
47
86
  this.rxBuf = new Uint8Array(0);
87
+ // rolling buffer across calls
88
+ this.dirtyUntil = 0;
89
+ this.inFlight = false;
90
+ this.lastTransactionEnd = null;
91
+ this.cfg = {
92
+ baudRate: DEFAULTS.baudRate,
93
+ dataBits: DEFAULTS.dataBits,
94
+ stopBits: DEFAULTS.stopBits,
95
+ parity: DEFAULTS.parity,
96
+ timeout: DEFAULTS.timeout,
97
+ postTimeoutWaitPeriod: DEFAULTS.postTimeoutWaitPeriod,
98
+ interRequestDelay: DEFAULTS.interRequestDelay
99
+ };
100
+ this.MAX_RESYNC_DROPS = DEFAULT_MAX_RESYNC_DROPS;
101
+ this.CRC_STRICT = DEFAULT_CRC_POLICY.mode === "strict";
48
102
  }
49
- // rolling buffer across calls
50
103
  // timeout gelpers
51
104
  setTimeout(ms) {
52
- this.timeout = ms;
105
+ this.cfg.timeout = ms;
53
106
  }
54
107
  getTimeout() {
55
- return this.timeout;
108
+ return this.cfg.timeout;
56
109
  }
57
110
  getPort() {
58
111
  return this.port;
@@ -66,72 +119,202 @@ var WebSerialTransport = class _WebSerialTransport {
66
119
  return t;
67
120
  }
68
121
  async init(opts) {
69
- var _a, _b, _c, _d, _e, _f;
70
- this.timeout = (_a = opts.timeout) != null ? _a : 500;
71
- this.port = await navigator.serial.requestPort({ filters: (_b = opts.requestFilters) != null ? _b : [] });
122
+ var _a, _b, _c;
123
+ Object.entries(this.cfg).forEach(([key, defaultValue]) => {
124
+ if (typeof opts[key] !== "undefined") {
125
+ this.cfg[key] = opts[key];
126
+ }
127
+ });
128
+ if (opts.crcPolicy) {
129
+ this.CRC_STRICT = opts.crcPolicy.mode === "strict";
130
+ this.MAX_RESYNC_DROPS = opts.crcPolicy.mode === "resync" ? (_a = opts.crcPolicy.maxResyncDrops) != null ? _a : DEFAULT_MAX_RESYNC_DROPS : 0;
131
+ }
132
+ this.port = (_c = opts.port) != null ? _c : await navigator.serial.requestPort({
133
+ filters: (_b = opts.requestFilters) != null ? _b : []
134
+ });
72
135
  await this.port.open({
73
- baudRate: (_c = opts.baudRate) != null ? _c : 9600,
74
- dataBits: (_d = opts.dataBits) != null ? _d : 8,
75
- stopBits: (_e = opts.stopBits) != null ? _e : 1,
76
- parity: (_f = opts.parity) != null ? _f : "none"
136
+ baudRate: this.cfg.baudRate,
137
+ dataBits: this.cfg.dataBits,
138
+ stopBits: this.cfg.stopBits,
139
+ parity: this.cfg.parity
77
140
  });
78
141
  this.reader = this.port.readable.getReader();
79
142
  this.writer = this.port.writable.getWriter();
143
+ if (typeof performance !== "undefined") {
144
+ this.now = () => performance.now();
145
+ }
80
146
  }
81
147
  // ----------------------------------------------------------------
82
148
  // transact(): Send `req` and await a response whose function-code matches the request
83
149
  // ---------------------------------------------------------------- */
84
150
  async transact(req) {
85
- await this.writer.write(req);
86
- const expectedFC = req[1] & 127;
87
- const deadline = Date.now() + this.timeout;
88
- while (true) {
89
- while (this.rxBuf.length < 3) {
90
- if (Date.now() > deadline) throw new TimeoutError();
91
- const { value } = await this.reader.read();
92
- if (!value) throw new TimeoutError();
93
- this.rxBuf = concat(this.rxBuf, value);
94
- }
95
- const need = this.frameLengthIfComplete(this.rxBuf);
96
- if (need === 0) {
97
- if (Date.now() > deadline) throw new TimeoutError();
98
- const { value } = await this.reader.read();
99
- if (!value) throw new TimeoutError();
100
- this.rxBuf = concat(this.rxBuf, value);
101
- continue;
102
- }
103
- const frame = this.rxBuf.slice(0, need);
104
- this.rxBuf = this.rxBuf.slice(need);
105
- const crc = crc16(frame.subarray(0, frame.length - 2));
106
- const crcOk = (crc & 255) === frame[frame.length - 2] && crc >> 8 === frame[frame.length - 1];
107
- if (!crcOk) {
108
- this.rxBuf = this.rxBuf.slice(1);
109
- throw new CrcError();
151
+ if (this.inFlight) {
152
+ throw new Error("Concurrent transact() calls are not supported");
153
+ }
154
+ this.inFlight = true;
155
+ try {
156
+ if (this.cfg.interRequestDelay > 0 && this.lastTransactionEnd !== null) {
157
+ const wait = this.lastTransactionEnd + this.cfg.interRequestDelay - this.now();
158
+ if (wait > 0) await sleep(wait);
110
159
  }
111
- const fc = frame[1] & 127;
112
- if (fc === expectedFC) {
113
- return frame;
114
- } else {
115
- continue;
160
+ await this.waitOutDirtyPeriod();
161
+ await this.writer.write(req);
162
+ const deadline = this.now() + this.cfg.timeout;
163
+ let State;
164
+ ((State2) => {
165
+ State2[State2["TRY_EXTRACT"] = 0] = "TRY_EXTRACT";
166
+ State2[State2["HAVE_FRAME"] = 1] = "HAVE_FRAME";
167
+ State2[State2["BAD_CRC"] = 2] = "BAD_CRC";
168
+ State2[State2["NEED_READ"] = 3] = "NEED_READ";
169
+ })(State || (State = {}));
170
+ let state = 0 /* TRY_EXTRACT */;
171
+ let frame = null;
172
+ let discardedBytes = 0;
173
+ while (true) {
174
+ switch (state) {
175
+ case 0 /* TRY_EXTRACT */: {
176
+ frame = this.extractFrame();
177
+ if (!frame) {
178
+ state = 3 /* NEED_READ */;
179
+ break;
180
+ }
181
+ state = 1 /* HAVE_FRAME */;
182
+ }
183
+ case 1 /* HAVE_FRAME */: {
184
+ if (this.crcOk(frame)) {
185
+ if (!proveRequestResponseMismatch(req, frame)) return frame;
186
+ frame = null;
187
+ state = 0 /* TRY_EXTRACT */;
188
+ continue;
189
+ }
190
+ state = 2 /* BAD_CRC */;
191
+ }
192
+ case 2 /* BAD_CRC */: {
193
+ if (this.CRC_STRICT) throw new CrcError();
194
+ discardedBytes += frame ? frame.length : 0;
195
+ frame = null;
196
+ if (discardedBytes >= this.MAX_RESYNC_DROPS) {
197
+ throw new ResyncError(discardedBytes, this.MAX_RESYNC_DROPS);
198
+ }
199
+ if (this.rxBuf.length > 0) {
200
+ state = 0 /* TRY_EXTRACT */;
201
+ continue;
202
+ }
203
+ state = 3 /* NEED_READ */;
204
+ }
205
+ case 3 /* NEED_READ */: {
206
+ const now = this.now();
207
+ const remaining = deadline - now;
208
+ if (remaining <= 0) throw new TimeoutError();
209
+ const value = await this.readWithTimeout(remaining);
210
+ this.rxBuf = concat(this.rxBuf, value);
211
+ state = 0 /* TRY_EXTRACT */;
212
+ continue;
213
+ }
214
+ default:
215
+ throw new Error("invalid state");
216
+ }
116
217
  }
218
+ } finally {
219
+ this.inFlight = false;
220
+ this.lastTransactionEnd = this.now();
117
221
  }
118
222
  }
223
+ async readOnce(maxMs) {
224
+ if (maxMs <= 0) return { kind: "timeout" };
225
+ const ms = Math.max(1, maxMs);
226
+ let timer;
227
+ try {
228
+ const result = await Promise.race([
229
+ this.reader.read(),
230
+ new Promise((resolve) => {
231
+ timer = setTimeout(() => resolve(_WebSerialTransport.TIMEOUT), ms);
232
+ })
233
+ ]);
234
+ if (result === _WebSerialTransport.TIMEOUT)
235
+ return { kind: "timeout" };
236
+ if (result.done || !result.value) return { kind: "closed" };
237
+ return { kind: "chunk", value: result.value };
238
+ } finally {
239
+ if (timer) clearTimeout(timer);
240
+ }
241
+ }
242
+ async waitOutDirtyPeriod() {
243
+ for (; ; ) {
244
+ const now = this.now();
245
+ if (now >= this.dirtyUntil) return;
246
+ const remaining = this.dirtyUntil - now;
247
+ const r = await this.readOnce(remaining);
248
+ if (r.kind !== "chunk") break;
249
+ }
250
+ this.rxBuf = new Uint8Array(0);
251
+ }
252
+ async readWithTimeout(ms) {
253
+ const r = await this.readOnce(ms);
254
+ if (r.kind === "chunk") return r.value;
255
+ if (r.kind === "timeout") {
256
+ await this.resetReaderAfterTimeout();
257
+ this.rxBuf = new Uint8Array(0);
258
+ this.dirtyUntil = this.now() + this.cfg.postTimeoutWaitPeriod;
259
+ throw new TimeoutError();
260
+ }
261
+ if (r.kind === "closed") {
262
+ throw new StreamClosedError();
263
+ }
264
+ throw new Error("invalid read result");
265
+ }
266
+ async resetReaderAfterTimeout() {
267
+ try {
268
+ await this.reader.cancel();
269
+ } catch (e) {
270
+ }
271
+ try {
272
+ this.reader.releaseLock();
273
+ } catch (e) {
274
+ }
275
+ this.reader = this.port.readable.getReader();
276
+ }
277
+ extractFrame() {
278
+ if (this.rxBuf.length < 3) return null;
279
+ const need = this.frameLengthIfComplete(this.rxBuf);
280
+ if (need === 0) return null;
281
+ const frame = this.rxBuf.slice(0, need);
282
+ this.rxBuf = this.rxBuf.slice(need);
283
+ return frame;
284
+ }
285
+ crcOk(frame) {
286
+ const crc = crc16(frame.subarray(0, frame.length - 2));
287
+ const crcOk = (crc & 255) === frame[frame.length - 2] && crc >> 8 === frame[frame.length - 1];
288
+ return crcOk;
289
+ }
290
+ now() {
291
+ return Date.now();
292
+ }
119
293
  // ----------------------------------------------------------------
120
294
  // Determine expected length; return 0 if we still need more bytes
121
295
  // ----------------------------------------------------------------
122
296
  frameLengthIfComplete(buf) {
123
297
  if (buf.length < 3) return 0;
298
+ const id = buf[0];
124
299
  const fc = buf[1];
300
+ if (id === 0 || id > 247) return 0;
125
301
  if (fc & 128) return buf.length >= 5 ? 5 : 0;
126
302
  if (fc === 1 || fc === 2 || fc === 3 || fc === 4) {
127
- const need = 3 + buf[2] + 2;
303
+ const byteCount = buf[2];
304
+ if (byteCount > 252) {
305
+ return buf.length >= 8 ? 8 : 0;
306
+ }
307
+ const need = 3 + byteCount + 2;
128
308
  return buf.length >= need ? need : 0;
129
309
  }
130
- if (fc === 5 || fc === 6 || fc === 15 || fc === 16)
310
+ if (fc === 5 || // write single coil
311
+ fc === 6 || // write single register
312
+ fc === 15 || // write multiple coils
313
+ fc === 16 || // write multiple registers
314
+ fc === 22)
131
315
  return buf.length >= 8 ? 8 : 0;
132
316
  return buf.length >= 8 ? 8 : 0;
133
317
  }
134
- // -- close port --
135
318
  async close() {
136
319
  var _a, _b, _c;
137
320
  await ((_a = this.reader) == null ? void 0 : _a.cancel());
@@ -139,27 +322,100 @@ var WebSerialTransport = class _WebSerialTransport {
139
322
  await ((_c = this.port) == null ? void 0 : _c.close());
140
323
  }
141
324
  };
325
+ _WebSerialTransport.TIMEOUT = Symbol("timeout");
326
+ var WebSerialTransport = _WebSerialTransport;
327
+ function proveRequestResponseMismatch(req, res) {
328
+ if (req.length < 2 || res.length < 2) return false;
329
+ const reqAddr = req[0];
330
+ const reqFc = req[1] & 127;
331
+ const resAddr = res[0];
332
+ const resFc = res[1];
333
+ const resBaseFc = resFc & 127;
334
+ if (resAddr !== reqAddr) return true;
335
+ if (resBaseFc !== reqFc) return true;
336
+ if (resFc & 128) {
337
+ return res.length !== 5;
338
+ }
339
+ switch (reqFc) {
340
+ case FC_READ_COILS:
341
+ case FC_READ_DISCRETE_INPUTS: {
342
+ if (req.length < 6 || res.length < 3) return false;
343
+ const quantity = u16be(req, 4);
344
+ const expectedByteCount = Math.ceil(quantity / 8);
345
+ if (res[2] !== expectedByteCount) return true;
346
+ if (res.length !== 3 + expectedByteCount + 2) return true;
347
+ return false;
348
+ }
349
+ case FC_READ_HOLDING_REGISTERS:
350
+ case FC_READ_INPUT_REGISTERS: {
351
+ if (req.length < 6 || res.length < 3) return false;
352
+ const quantity = u16be(req, 4);
353
+ const expectedByteCount = quantity * 2;
354
+ if (res[2] !== expectedByteCount) return true;
355
+ if (res.length !== 3 + expectedByteCount + 2) return true;
356
+ return false;
357
+ }
358
+ case FC_WRITE_SINGLE_COIL:
359
+ case FC_WRITE_SINGLE_HOLDING_REGISTER:
360
+ case FC_MASK_WRITE_REGISTER: {
361
+ if (req.length < 8 || res.length < 8) return false;
362
+ if (res.length !== req.length) return true;
363
+ if (!bytesEqual(req, 2, res, 2, req.length - 4)) return true;
364
+ return false;
365
+ }
366
+ case FC_WRITE_MULTIPLE_COILS:
367
+ case FC_WRITE_MULTIPLE_HOLDING_REGISTERS: {
368
+ if (req.length < 6 || res.length < 8) return false;
369
+ if (res.length !== 8) return true;
370
+ if (!bytesEqual(req, 2, res, 2, 4)) return true;
371
+ return false;
372
+ }
373
+ case FC_READ_WRITE_MULTIPLE_REGISTERS: {
374
+ if (req.length < 10 || res.length < 3) return false;
375
+ const readQuantity = u16be(req, 4);
376
+ const expectedByteCount = readQuantity * 2;
377
+ if (res[2] !== expectedByteCount) return true;
378
+ if (res.length !== 3 + expectedByteCount + 2) return true;
379
+ return false;
380
+ }
381
+ case FC_READ_FIFO_QUEUE: {
382
+ if (res.length < 6) return true;
383
+ const byteCount = u16be(res, 2);
384
+ if (byteCount < 2) return true;
385
+ if (res.length !== 4 + byteCount + 2) return true;
386
+ const fifoDataBytes = byteCount - 2;
387
+ if (fifoDataBytes % 2 !== 0) return true;
388
+ return false;
389
+ }
390
+ case FC_READ_FILE_RECORD:
391
+ case FC_WRITE_FILE_RECORD: {
392
+ if (res.length < 5) return true;
393
+ const byteCount = res[2];
394
+ if (res.length !== 3 + byteCount + 2) return true;
395
+ return false;
396
+ }
397
+ default:
398
+ return false;
399
+ }
400
+ }
142
401
  function concat(a, b) {
143
402
  const out = new Uint8Array(a.length + b.length);
144
403
  out.set(a, 0);
145
404
  out.set(b, a.length);
146
405
  return out;
147
406
  }
148
-
149
- // src/core/types.ts
150
- var FC_READ_HOLDING_REGISTERS = 3;
151
- var FC_WRITE_SINGLE_HOLDING_REGISTER = 6;
152
- var FC_WRITE_MULTIPLE_HOLDING_REGISTERS = 16;
153
- var FC_READ_COILS = 1;
154
- var FC_WRITE_SINGLE_COIL = 5;
155
- var FC_WRITE_MULTIPLE_COILS = 15;
156
- var FC_READ_INPUT_REGISTERS = 4;
157
- var FC_READ_DISCRETE_INPUTS = 2;
158
- var FC_READ_FILE_RECORD = 20;
159
- var FC_WRITE_FILE_RECORD = 21;
160
- var FC_MASK_WRITE_REGISTER = 22;
161
- var FC_READ_WRITE_MULTIPLE_REGISTERS = 23;
162
- var FC_READ_FIFO_QUEUE = 24;
407
+ function u16be(buf, offset) {
408
+ return buf[offset] << 8 | buf[offset + 1];
409
+ }
410
+ function bytesEqual(a, aStart, b, bStart, len) {
411
+ for (let i = 0; i < len; i++) {
412
+ if (a[aStart + i] !== b[bStart + i]) return false;
413
+ }
414
+ return true;
415
+ }
416
+ async function sleep(ms) {
417
+ return new Promise((resolve) => setTimeout(resolve, ms));
418
+ }
163
419
 
164
420
  // src/core/frames.ts
165
421
  function buildReadHolding(id, addr, len) {
@@ -595,10 +851,20 @@ var ModbusRTU = class _ModbusRTU {
595
851
  return { data: parseReadFifoQueue(raw), raw };
596
852
  }
597
853
  };
854
+
855
+ // src/index.ts
856
+ var BUILD_INFO = {
857
+ version: "0.11.0",
858
+ commit: "0107e92",
859
+ buildTime: "2026-03-13T07:30:43.483Z"
860
+ };
598
861
  export {
862
+ BUILD_INFO,
599
863
  CrcError,
600
864
  ExceptionError,
601
865
  ModbusRTU,
866
+ ResyncError,
867
+ StreamClosedError,
602
868
  TimeoutError,
603
869
  buildMaskWriteRegister,
604
870
  buildReadCoils,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "modbus-webserial",
3
- "version": "0.10.1",
4
- "description": "Tiny TypeScript library for speaking Modbus-RTU from the browser via Web Serial",
3
+ "version": "0.11.0",
4
+ "description": "ESM library for speaking Modbus-RTU from the browser via Web Serial",
5
5
  "type": "module",
6
6
  "author": "Antti Kotajärvi <antti.kotajarvi@hotmail.com>",
7
7
  "license": "MIT",