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 +46 -0
- package/dist/index.d.ts +18 -5
- package/dist/index.js +284 -86
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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.
|
|
60
|
-
this.
|
|
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
|
|
82
|
-
this.
|
|
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:
|
|
88
|
-
dataBits:
|
|
89
|
-
stopBits:
|
|
90
|
-
parity:
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
state =
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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",
|