modbus-webserial 0.1.0-alpha.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/LICENSE +3 -0
- package/README.md +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +175 -0
- package/package.json +44 -0
package/LICENSE
ADDED
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
modbus-webserial (alpha)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
interface WebSerialOptions {
|
|
2
|
+
baudRate?: number;
|
|
3
|
+
dataBits?: 7 | 8;
|
|
4
|
+
stopBits?: 1 | 2;
|
|
5
|
+
parity?: 'none' | 'even' | 'odd';
|
|
6
|
+
requestFilters?: SerialPortFilter[];
|
|
7
|
+
timeout?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Result payloads returned by high-level helpers */
|
|
11
|
+
interface ReadRegisterResult {
|
|
12
|
+
data: number[];
|
|
13
|
+
raw: Uint8Array;
|
|
14
|
+
}
|
|
15
|
+
interface WriteRegisterResult {
|
|
16
|
+
address: number;
|
|
17
|
+
value: number;
|
|
18
|
+
raw: Uint8Array;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
declare class ModbusRTU {
|
|
22
|
+
private id;
|
|
23
|
+
private transport;
|
|
24
|
+
static openWebSerial(opts?: WebSerialOptions): Promise<ModbusRTU>;
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
isOpen(): boolean;
|
|
27
|
+
setID(id: number): void;
|
|
28
|
+
getID(): number;
|
|
29
|
+
readHoldingRegisters(addr: number, len: number): Promise<ReadRegisterResult>;
|
|
30
|
+
writeRegister(addr: number, value: number): Promise<WriteRegisterResult>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare function buildReadHolding(id: number, addr: number, len: number): Uint8Array;
|
|
34
|
+
declare function buildWriteSingle(id: number, addr: number, value: number): Uint8Array;
|
|
35
|
+
declare function parseReadHolding(resp: Uint8Array): number[];
|
|
36
|
+
declare function parseWriteSingle(resp: Uint8Array): {
|
|
37
|
+
address: number;
|
|
38
|
+
value: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** Standard Modbus CRC-16 (poly 0xA001, little-endian) */
|
|
42
|
+
declare function crc16(buf: Uint8Array): number;
|
|
43
|
+
|
|
44
|
+
declare class CrcError extends Error {
|
|
45
|
+
constructor();
|
|
46
|
+
}
|
|
47
|
+
declare class TimeoutError extends Error {
|
|
48
|
+
constructor();
|
|
49
|
+
}
|
|
50
|
+
declare class ExceptionError extends Error {
|
|
51
|
+
code: number;
|
|
52
|
+
constructor(code: number);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { CrcError, ExceptionError, ModbusRTU, type ReadRegisterResult, TimeoutError, type WebSerialOptions, type WriteRegisterResult, buildReadHolding, buildWriteSingle, crc16, parseReadHolding, parseWriteSingle };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// src/core/errors.ts
|
|
2
|
+
var CrcError = class extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super("CRC check failed");
|
|
5
|
+
}
|
|
6
|
+
};
|
|
7
|
+
var TimeoutError = class extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super("Modbus response timed out");
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var ExceptionError = class extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(code) {
|
|
15
|
+
super(`Modbus exception ${code.toString(16)}`);
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// src/transport/webserial.ts
|
|
21
|
+
var WebSerialTransport = class _WebSerialTransport {
|
|
22
|
+
port;
|
|
23
|
+
reader;
|
|
24
|
+
writer;
|
|
25
|
+
timeout = 500;
|
|
26
|
+
static async open(opts = {}) {
|
|
27
|
+
const t = new _WebSerialTransport();
|
|
28
|
+
await t.init(opts);
|
|
29
|
+
return t;
|
|
30
|
+
}
|
|
31
|
+
async init(opts) {
|
|
32
|
+
this.timeout = opts.timeout ?? 500;
|
|
33
|
+
this.port = await navigator.serial.requestPort({ filters: opts.requestFilters ?? [] });
|
|
34
|
+
await this.port.open({
|
|
35
|
+
baudRate: opts.baudRate ?? 9600,
|
|
36
|
+
dataBits: opts.dataBits ?? 8,
|
|
37
|
+
stopBits: opts.stopBits ?? 1,
|
|
38
|
+
parity: opts.parity ?? "none"
|
|
39
|
+
});
|
|
40
|
+
this.reader = this.port.readable.getReader();
|
|
41
|
+
this.writer = this.port.writable.getWriter();
|
|
42
|
+
}
|
|
43
|
+
async transact(frame) {
|
|
44
|
+
await this.writer.write(frame);
|
|
45
|
+
const timeout = new Promise(
|
|
46
|
+
(_, reject) => setTimeout(() => reject(new TimeoutError()), this.timeout)
|
|
47
|
+
);
|
|
48
|
+
const { value } = await Promise.race([this.reader.read(), timeout]);
|
|
49
|
+
if (!value) throw new TimeoutError();
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
async close() {
|
|
53
|
+
var _a, _b, _c;
|
|
54
|
+
await ((_a = this.reader) == null ? void 0 : _a.cancel());
|
|
55
|
+
await ((_b = this.writer) == null ? void 0 : _b.close());
|
|
56
|
+
await ((_c = this.port) == null ? void 0 : _c.close());
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// src/core/types.ts
|
|
61
|
+
var FC_READ_HOLDING = 3;
|
|
62
|
+
var FC_WRITE_SINGLE = 6;
|
|
63
|
+
|
|
64
|
+
// src/core/crc16.ts
|
|
65
|
+
function crc16(buf) {
|
|
66
|
+
let crc = 65535;
|
|
67
|
+
for (let b of buf) {
|
|
68
|
+
crc ^= b;
|
|
69
|
+
for (let i = 0; i < 8; i++) {
|
|
70
|
+
crc = crc >> 1 ^ (crc & 1 ? 40961 : 0);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return crc;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// src/core/frames.ts
|
|
77
|
+
function buildReadHolding(id, addr, len) {
|
|
78
|
+
const frame = new Uint8Array(8);
|
|
79
|
+
frame[0] = id;
|
|
80
|
+
frame[1] = FC_READ_HOLDING;
|
|
81
|
+
frame[2] = addr >> 8;
|
|
82
|
+
frame[3] = addr & 255;
|
|
83
|
+
frame[4] = len >> 8;
|
|
84
|
+
frame[5] = len & 255;
|
|
85
|
+
const crc = crc16(frame.subarray(0, 6));
|
|
86
|
+
frame[6] = crc & 255;
|
|
87
|
+
frame[7] = crc >> 8;
|
|
88
|
+
return frame;
|
|
89
|
+
}
|
|
90
|
+
function buildWriteSingle(id, addr, value) {
|
|
91
|
+
const frame = new Uint8Array(8);
|
|
92
|
+
frame[0] = id;
|
|
93
|
+
frame[1] = FC_WRITE_SINGLE;
|
|
94
|
+
frame[2] = addr >> 8;
|
|
95
|
+
frame[3] = addr & 255;
|
|
96
|
+
frame[4] = value >> 8;
|
|
97
|
+
frame[5] = value & 255;
|
|
98
|
+
const crc = crc16(frame.subarray(0, 6));
|
|
99
|
+
frame[6] = crc & 255;
|
|
100
|
+
frame[7] = crc >> 8;
|
|
101
|
+
return frame;
|
|
102
|
+
}
|
|
103
|
+
function parseReadHolding(resp) {
|
|
104
|
+
basicChecks(resp, FC_READ_HOLDING);
|
|
105
|
+
const byteCount = resp[2];
|
|
106
|
+
const words = [];
|
|
107
|
+
for (let i = 0; i < byteCount; i += 2) {
|
|
108
|
+
words.push(resp[3 + i] << 8 | resp[4 + i]);
|
|
109
|
+
}
|
|
110
|
+
return words;
|
|
111
|
+
}
|
|
112
|
+
function parseWriteSingle(resp) {
|
|
113
|
+
basicChecks(resp, FC_WRITE_SINGLE);
|
|
114
|
+
const addr = resp[2] << 8 | resp[3];
|
|
115
|
+
const value = resp[4] << 8 | resp[5];
|
|
116
|
+
return { address: addr, value };
|
|
117
|
+
}
|
|
118
|
+
function basicChecks(frame, expectedFC) {
|
|
119
|
+
const crcExpected = crc16(frame.subarray(0, frame.length - 2));
|
|
120
|
+
const crcGot = frame[frame.length - 2] | frame[frame.length - 1] << 8;
|
|
121
|
+
if (crcExpected !== crcGot) throw new CrcError();
|
|
122
|
+
const fc = frame[1] & 127;
|
|
123
|
+
const isException = (frame[1] & 128) !== 0;
|
|
124
|
+
if (isException) throw new ExceptionError(frame[2]);
|
|
125
|
+
if (fc !== expectedFC) throw new Error("Unexpected function code");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// src/client.ts
|
|
129
|
+
var ModbusRTU = class _ModbusRTU {
|
|
130
|
+
id = 1;
|
|
131
|
+
transport;
|
|
132
|
+
/* ---------- static factory ---------- */
|
|
133
|
+
static async openWebSerial(opts) {
|
|
134
|
+
const cli = new _ModbusRTU();
|
|
135
|
+
cli.transport = await WebSerialTransport.open(opts);
|
|
136
|
+
return cli;
|
|
137
|
+
}
|
|
138
|
+
/* ---------- housekeeping ---------- */
|
|
139
|
+
async close() {
|
|
140
|
+
await this.transport.close();
|
|
141
|
+
}
|
|
142
|
+
isOpen() {
|
|
143
|
+
return !!this.transport;
|
|
144
|
+
}
|
|
145
|
+
/* ---------- config ---------- */
|
|
146
|
+
setID(id) {
|
|
147
|
+
this.id = id;
|
|
148
|
+
}
|
|
149
|
+
getID() {
|
|
150
|
+
return this.id;
|
|
151
|
+
}
|
|
152
|
+
/* ---------- helpers (v0.1) ---------- */
|
|
153
|
+
async readHoldingRegisters(addr, len) {
|
|
154
|
+
const req = buildReadHolding(this.id, addr, len);
|
|
155
|
+
const raw = await this.transport.transact(req);
|
|
156
|
+
return { data: parseReadHolding(raw), raw };
|
|
157
|
+
}
|
|
158
|
+
async writeRegister(addr, value) {
|
|
159
|
+
const req = buildWriteSingle(this.id, addr, value);
|
|
160
|
+
const raw = await this.transport.transact(req);
|
|
161
|
+
const { address, value: v } = parseWriteSingle(raw);
|
|
162
|
+
return { address, value: v, raw };
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
export {
|
|
166
|
+
CrcError,
|
|
167
|
+
ExceptionError,
|
|
168
|
+
ModbusRTU,
|
|
169
|
+
TimeoutError,
|
|
170
|
+
buildReadHolding,
|
|
171
|
+
buildWriteSingle,
|
|
172
|
+
crc16,
|
|
173
|
+
parseReadHolding,
|
|
174
|
+
parseWriteSingle
|
|
175
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "modbus-webserial",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Tiny TypeScript library for speaking Modbus-RTU from the browser via Web Serial",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
22
|
+
"prepublishOnly": "npm run build && npm test",
|
|
23
|
+
"test": "vitest"
|
|
24
|
+
},
|
|
25
|
+
"repository": "git+https://github.com/your-gh-user/modbus-webserial.git",
|
|
26
|
+
"keywords": [
|
|
27
|
+
"modbus",
|
|
28
|
+
"rtu",
|
|
29
|
+
"webserial",
|
|
30
|
+
"browser",
|
|
31
|
+
"typescript"
|
|
32
|
+
],
|
|
33
|
+
"author": "Your Name <you@example.com>",
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=18"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/w3c-web-serial": "^1",
|
|
40
|
+
"tsup": "^8",
|
|
41
|
+
"typescript": "^5",
|
|
42
|
+
"vitest": "^1"
|
|
43
|
+
}
|
|
44
|
+
}
|