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 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,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**: Finalize API, apply bug fixes, refine docs, production-ready release
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?: 'none' | 'even' | 'odd';
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 = await navigator.serial.requestPort({ filters: (_b = opts.requestFilters) != null ? _b : [] });
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: (_c = opts.baudRate) != null ? _c : 9600,
71
- dataBits: (_d = opts.dataBits) != null ? _d : 8,
72
- stopBits: (_e = opts.stopBits) != null ? _e : 1,
73
- parity: (_f = opts.parity) != null ? _f : "none"
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
- while (this.rxBuf.length < 3) {
87
- if (Date.now() > deadline) throw new TimeoutError();
88
- const { value } = await this.reader.read();
89
- if (!value) throw new TimeoutError();
90
- this.rxBuf = concat(this.rxBuf, value);
91
- }
92
- const need = this.frameLengthIfComplete(this.rxBuf);
93
- if (need === 0) {
94
- if (Date.now() > deadline) throw new TimeoutError();
95
- const { value } = await this.reader.read();
96
- if (!value) throw new TimeoutError();
97
- this.rxBuf = concat(this.rxBuf, value);
98
- continue;
99
- }
100
- const frame = this.rxBuf.slice(0, need);
101
- this.rxBuf = this.rxBuf.slice(need);
102
- const crc = crc16(frame.subarray(0, frame.length - 2));
103
- const crcOk = (crc & 255) === frame[frame.length - 2] && crc >> 8 === frame[frame.length - 1];
104
- if (!crcOk) {
105
- this.rxBuf = this.rxBuf.slice(1);
106
- throw new CrcError();
107
- }
108
- const fc = frame[1] & 127;
109
- if (fc === expectedFC) {
110
- return frame;
111
- } else {
112
- continue;
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 need = 3 + buf[2] + 2;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modbus-webserial",
3
- "version": "0.10.0",
3
+ "version": "0.10.3",
4
4
  "description": "Tiny TypeScript 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>",