modbus-webserial 0.10.3 → 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
@@ -135,7 +135,53 @@ const client = await ModbusRTU.openWebSerial({
135
135
  });
136
136
  ```
137
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
+
138
183
  ## Current state
184
+ * **v0.11**: Transport rework to handle edge cases explicitly
139
185
  * **v0.10**: Full modbus data-access coverage
140
186
  * **v0.9**: Full passing tests, smoke test passed, complete README, build scripts in place
141
187
  * **Beta**: Full Modbus RTU function‑code coverage
package/dist/index.d.ts CHANGED
@@ -1,12 +1,16 @@
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
9
  parity?: "none" | "even" | "odd";
6
- requestFilters?: SerialPortFilter[];
7
- port?: SerialPort;
8
10
  timeout?: number;
9
- crcPolicy?: CrcPolicy;
11
+ crcPolicy?: Required<CrcPolicy>;
12
+ postTimeoutWaitPeriod?: number;
13
+ interRequestDelay?: number;
10
14
  }
11
15
  type CrcPolicy = {
12
16
  mode: "strict";
@@ -210,9 +214,18 @@ declare class ResyncError extends Error {
210
214
  declare class TimeoutError extends Error {
211
215
  constructor();
212
216
  }
217
+ declare class StreamClosedError extends Error {
218
+ constructor();
219
+ }
213
220
  declare class ExceptionError extends Error {
214
221
  code: number;
215
222
  constructor(code: number);
216
223
  }
217
224
 
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 };
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
@@ -28,6 +28,11 @@ var TimeoutError = class extends Error {
28
28
  super("Modbus response timed out");
29
29
  }
30
30
  };
31
+ var StreamClosedError = class extends Error {
32
+ constructor() {
33
+ super("Modbus stream closed");
34
+ }
35
+ };
31
36
  var ExceptionError = class extends Error {
32
37
  constructor(code) {
33
38
  var _a;
@@ -48,23 +53,59 @@ function crc16(buf) {
48
53
  return crc;
49
54
  }
50
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
+
51
71
  // src/transport/webserial.ts
52
72
  var DEFAULT_CRC_POLICY = { mode: "strict" };
53
73
  var DEFAULT_MAX_RESYNC_DROPS = 32;
54
- var WebSerialTransport = class _WebSerialTransport {
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 {
55
85
  constructor() {
56
- this.timeout = 500;
57
86
  this.rxBuf = new Uint8Array(0);
58
87
  // rolling buffer across calls
59
- this.strictCrc = true;
60
- this.maxResyncDrops = DEFAULT_MAX_RESYNC_DROPS;
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";
61
102
  }
62
103
  // timeout gelpers
63
104
  setTimeout(ms) {
64
- this.timeout = ms;
105
+ this.cfg.timeout = ms;
65
106
  }
66
107
  getTimeout() {
67
- return this.timeout;
108
+ return this.cfg.timeout;
68
109
  }
69
110
  getPort() {
70
111
  return this.port;
@@ -78,92 +119,160 @@ var WebSerialTransport = class _WebSerialTransport {
78
119
  return t;
79
120
  }
80
121
  async init(opts) {
81
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
82
- this.timeout = (_a = opts.timeout) != null ? _a : 500;
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
+ }
83
132
  this.port = (_c = opts.port) != null ? _c : await navigator.serial.requestPort({
84
133
  filters: (_b = opts.requestFilters) != null ? _b : []
85
134
  });
86
135
  await this.port.open({
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"
136
+ baudRate: this.cfg.baudRate,
137
+ dataBits: this.cfg.dataBits,
138
+ stopBits: this.cfg.stopBits,
139
+ parity: this.cfg.parity
91
140
  });
92
141
  this.reader = this.port.readable.getReader();
93
142
  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;
143
+ if (typeof performance !== "undefined") {
144
+ this.now = () => performance.now();
101
145
  }
102
146
  }
103
147
  // ----------------------------------------------------------------
104
148
  // transact(): Send `req` and await a response whose function-code matches the request
105
149
  // ---------------------------------------------------------------- */
106
150
  async transact(req) {
107
- await this.writer.write(req);
108
- const STRICT_CRC = this.strictCrc;
109
- const MAX_RESYNC_DROPS = this.maxResyncDrops;
110
- const expectedFC = req[1] & 127;
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;
122
- while (true) {
123
- switch (state) {
124
- case 0 /* TRY_EXTRACT */: {
125
- frame = this.extractFrame();
126
- if (!frame) {
127
- state = 3 /* NEED_READ */;
128
- break;
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);
159
+ }
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 */;
129
182
  }
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;
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 */;
139
191
  }
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);
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 */;
148
204
  }
149
- if (this.rxBuf.length > 0) {
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);
150
211
  state = 0 /* TRY_EXTRACT */;
151
212
  continue;
152
213
  }
153
- state = 3 /* NEED_READ */;
214
+ default:
215
+ throw new Error("invalid state");
154
216
  }
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");
165
217
  }
218
+ } finally {
219
+ this.inFlight = false;
220
+ this.lastTransactionEnd = this.now();
221
+ }
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) {
166
270
  }
271
+ try {
272
+ this.reader.releaseLock();
273
+ } catch (e) {
274
+ }
275
+ this.reader = this.port.readable.getReader();
167
276
  }
168
277
  extractFrame() {
169
278
  if (this.rxBuf.length < 3) return null;
@@ -178,6 +287,9 @@ var WebSerialTransport = class _WebSerialTransport {
178
287
  const crcOk = (crc & 255) === frame[frame.length - 2] && crc >> 8 === frame[frame.length - 1];
179
288
  return crcOk;
180
289
  }
290
+ now() {
291
+ return Date.now();
292
+ }
181
293
  // ----------------------------------------------------------------
182
294
  // Determine expected length; return 0 if we still need more bytes
183
295
  // ----------------------------------------------------------------
@@ -195,7 +307,11 @@ var WebSerialTransport = class _WebSerialTransport {
195
307
  const need = 3 + byteCount + 2;
196
308
  return buf.length >= need ? need : 0;
197
309
  }
198
- 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)
199
315
  return buf.length >= 8 ? 8 : 0;
200
316
  return buf.length >= 8 ? 8 : 0;
201
317
  }
@@ -206,27 +322,100 @@ var WebSerialTransport = class _WebSerialTransport {
206
322
  await ((_c = this.port) == null ? void 0 : _c.close());
207
323
  }
208
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
+ }
209
401
  function concat(a, b) {
210
402
  const out = new Uint8Array(a.length + b.length);
211
403
  out.set(a, 0);
212
404
  out.set(b, a.length);
213
405
  return out;
214
406
  }
215
-
216
- // src/core/types.ts
217
- var FC_READ_HOLDING_REGISTERS = 3;
218
- var FC_WRITE_SINGLE_HOLDING_REGISTER = 6;
219
- var FC_WRITE_MULTIPLE_HOLDING_REGISTERS = 16;
220
- var FC_READ_COILS = 1;
221
- var FC_WRITE_SINGLE_COIL = 5;
222
- var FC_WRITE_MULTIPLE_COILS = 15;
223
- var FC_READ_INPUT_REGISTERS = 4;
224
- var FC_READ_DISCRETE_INPUTS = 2;
225
- var FC_READ_FILE_RECORD = 20;
226
- var FC_WRITE_FILE_RECORD = 21;
227
- var FC_MASK_WRITE_REGISTER = 22;
228
- var FC_READ_WRITE_MULTIPLE_REGISTERS = 23;
229
- 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
+ }
230
419
 
231
420
  // src/core/frames.ts
232
421
  function buildReadHolding(id, addr, len) {
@@ -662,11 +851,20 @@ var ModbusRTU = class _ModbusRTU {
662
851
  return { data: parseReadFifoQueue(raw), raw };
663
852
  }
664
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
+ };
665
861
  export {
862
+ BUILD_INFO,
666
863
  CrcError,
667
864
  ExceptionError,
668
865
  ModbusRTU,
669
866
  ResyncError,
867
+ StreamClosedError,
670
868
  TimeoutError,
671
869
  buildMaskWriteRegister,
672
870
  buildReadCoils,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "modbus-webserial",
3
- "version": "0.10.3",
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",