@totemsdk/edge-modbus 0.1.0 → 0.2.2
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 +202 -21
- package/NOTICE +60 -0
- package/README.md +76 -6
- package/dist/cjs/gateway.d.ts +35 -0
- package/dist/cjs/gateway.js +235 -0
- package/dist/cjs/index.d.ts +7 -0
- package/dist/cjs/index.js +9 -0
- package/dist/cjs/native-transport.d.ts +36 -0
- package/dist/cjs/native-transport.js +166 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/sensor-bridge.d.ts +30 -0
- package/dist/cjs/sensor-bridge.js +64 -0
- package/dist/cjs/transport.d.ts +38 -0
- package/dist/cjs/transport.js +8 -0
- package/dist/gateway.d.ts +14 -4
- package/dist/gateway.js +191 -49
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -7
- package/dist/native-transport.d.ts +36 -0
- package/dist/native-transport.js +129 -0
- package/dist/sensor-bridge.d.ts +2 -1
- package/dist/sensor-bridge.js +13 -40
- package/dist/transport.d.ts +7 -1
- package/dist/transport.js +1 -2
- package/package.json +54 -26
- package/sidecar.json +34 -0
package/dist/gateway.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Modbus Edge Gateway — wires a ModbusTransportPort into an EdgeRuntime.
|
|
3
|
+
*
|
|
4
|
+
* Uses a unit-prefixed PDU (no CRC or MBAP header) at the transport boundary.
|
|
5
|
+
* The binding owns TCP transaction framing or RTU CRC validation.
|
|
4
6
|
*/
|
|
5
|
-
|
|
6
|
-
exports.createModbusGateway = createModbusGateway;
|
|
7
|
-
function createModbusGateway(config) {
|
|
7
|
+
export function createModbusGateway(config) {
|
|
8
8
|
let status = 'stopped';
|
|
9
9
|
let unsubFrame;
|
|
10
10
|
let unsubError;
|
|
@@ -14,8 +14,8 @@ function createModbusGateway(config) {
|
|
|
14
14
|
if (status === 'running')
|
|
15
15
|
return;
|
|
16
16
|
await config.transport.connect();
|
|
17
|
-
unsubFrame = config.transport.onFrame((
|
|
18
|
-
//
|
|
17
|
+
unsubFrame = config.transport.onFrame((_frame) => {
|
|
18
|
+
// Unsolicited frames are not expected in Modbus request/response model
|
|
19
19
|
});
|
|
20
20
|
unsubError = config.transport.onError(() => {
|
|
21
21
|
status = 'error';
|
|
@@ -29,62 +29,204 @@ function createModbusGateway(config) {
|
|
|
29
29
|
status = 'stopped';
|
|
30
30
|
},
|
|
31
31
|
async readCoils(unitId, address, count) {
|
|
32
|
-
|
|
33
|
-
const request = buildModbusFrame(unitId, 1, address, count);
|
|
34
|
-
const response = await config.transport.sendFrame(request);
|
|
35
|
-
const values = parseCoilResponse(response, count);
|
|
36
|
-
return { ok: true, data: { values } };
|
|
37
|
-
}
|
|
38
|
-
catch (e) {
|
|
39
|
-
return { ok: false, error: String(e) };
|
|
40
|
-
}
|
|
32
|
+
return readBits(config.transport, unitId, 1, address, count);
|
|
41
33
|
},
|
|
42
|
-
async
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
34
|
+
async readDiscreteInputs(unitId, address, count) {
|
|
35
|
+
return readBits(config.transport, unitId, 2, address, count);
|
|
36
|
+
},
|
|
37
|
+
async readHoldingRegisters(unitId, address, count) {
|
|
38
|
+
return readRegisters(config.transport, unitId, 3, address, count);
|
|
39
|
+
},
|
|
40
|
+
async readInputRegisters(unitId, address, count) {
|
|
41
|
+
return readRegisters(config.transport, unitId, 4, address, count);
|
|
42
|
+
},
|
|
43
|
+
async writeSingleCoil(unitId, address, value) {
|
|
44
|
+
if (typeof value !== 'boolean')
|
|
45
|
+
return { ok: false, error: 'Coil value must be boolean', errorCode: 'MODBUS_INVALID_REQUEST' };
|
|
46
|
+
return writeSingle(config.transport, unitId, 5, address, value ? 0xFF00 : 0x0000);
|
|
47
|
+
},
|
|
48
|
+
async writeSingleRegister(unitId, address, value) {
|
|
49
|
+
return writeSingle(config.transport, unitId, 6, address, value);
|
|
50
|
+
},
|
|
51
|
+
async writeMultipleCoils(unitId, address, values) {
|
|
52
|
+
return writeMultipleCoilsImpl(config.transport, unitId, address, values);
|
|
53
|
+
},
|
|
54
|
+
async writeMultipleRegisters(unitId, address, values) {
|
|
55
|
+
return writeMultipleRegistersImpl(config.transport, unitId, address, values);
|
|
52
56
|
},
|
|
53
57
|
};
|
|
54
58
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
59
|
+
// ── Unit-prefixed PDU builders (no CRC or MBAP header) ───────────────────
|
|
60
|
+
class ModbusRequestError extends Error {
|
|
61
|
+
}
|
|
62
|
+
class ModbusExceptionError extends Error {
|
|
63
|
+
}
|
|
64
|
+
function validateRange(unitId, address, count, maximum) {
|
|
65
|
+
if (!Number.isInteger(unitId) || unitId < 1 || unitId > 247 ||
|
|
66
|
+
!Number.isInteger(address) || address < 0 || address > 0xffff ||
|
|
67
|
+
!Number.isInteger(count) || count < 1 || count > maximum || address + count > 0x10000) {
|
|
68
|
+
throw new ModbusRequestError('Invalid Modbus unit, address or quantity');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function validateRegister(value) {
|
|
72
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
|
|
73
|
+
throw new ModbusRequestError('Register value must be an unsigned 16-bit integer');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function buildReadFrame(unitId, fc, address, count) {
|
|
77
|
+
validateRange(unitId, address, count, fc <= 2 ? 2000 : 125);
|
|
78
|
+
const buf = new Uint8Array(6);
|
|
79
|
+
buf[0] = unitId;
|
|
80
|
+
buf[1] = fc;
|
|
81
|
+
new DataView(buf.buffer).setUint16(2, address, false);
|
|
82
|
+
new DataView(buf.buffer).setUint16(4, count, false);
|
|
83
|
+
return buf;
|
|
84
|
+
}
|
|
85
|
+
function buildWriteSingleFrame(unitId, fc, address, value) {
|
|
86
|
+
validateRange(unitId, address, 1, 1);
|
|
87
|
+
validateRegister(value);
|
|
88
|
+
const buf = new Uint8Array(6);
|
|
89
|
+
buf[0] = unitId;
|
|
90
|
+
buf[1] = fc;
|
|
91
|
+
new DataView(buf.buffer).setUint16(2, address, false);
|
|
92
|
+
new DataView(buf.buffer).setUint16(4, value, false);
|
|
93
|
+
return buf;
|
|
94
|
+
}
|
|
95
|
+
function buildWriteMultipleCoilsFrame(unitId, address, values) {
|
|
96
|
+
validateRange(unitId, address, values.length, 1968);
|
|
97
|
+
for (const value of values) {
|
|
98
|
+
if (typeof value !== 'boolean')
|
|
99
|
+
throw new ModbusRequestError('Coil values must be boolean');
|
|
100
|
+
}
|
|
101
|
+
const byteCount = Math.ceil(values.length / 8);
|
|
102
|
+
const buf = new Uint8Array(7 + byteCount);
|
|
103
|
+
buf[0] = unitId;
|
|
104
|
+
buf[1] = 15; // FC 15
|
|
105
|
+
new DataView(buf.buffer).setUint16(2, address, false);
|
|
106
|
+
new DataView(buf.buffer).setUint16(4, values.length, false);
|
|
107
|
+
buf[6] = byteCount;
|
|
108
|
+
for (let i = 0; i < values.length; i++) {
|
|
109
|
+
if (values[i]) {
|
|
110
|
+
buf[7 + Math.floor(i / 8)] |= (1 << (i % 8));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return buf;
|
|
114
|
+
}
|
|
115
|
+
function buildWriteMultipleRegistersFrame(unitId, address, values) {
|
|
116
|
+
validateRange(unitId, address, values.length, 123);
|
|
117
|
+
for (const value of values)
|
|
118
|
+
validateRegister(value);
|
|
119
|
+
const byteCount = values.length * 2;
|
|
120
|
+
const buf = new Uint8Array(7 + byteCount);
|
|
121
|
+
buf[0] = unitId;
|
|
122
|
+
buf[1] = 16; // FC 16
|
|
123
|
+
new DataView(buf.buffer).setUint16(2, address, false);
|
|
124
|
+
new DataView(buf.buffer).setUint16(4, values.length, false);
|
|
125
|
+
buf[6] = byteCount;
|
|
126
|
+
for (let i = 0; i < values.length; i++) {
|
|
127
|
+
new DataView(buf.buffer).setUint16(7 + i * 2, values[i], false);
|
|
128
|
+
}
|
|
129
|
+
return buf;
|
|
130
|
+
}
|
|
131
|
+
// ── Response parsers (RTU format — no MBAP header) ────────────────────────
|
|
132
|
+
function checkResponse(response, unitId, expectedFunction) {
|
|
133
|
+
if (response.length < 2 || response[0] !== unitId)
|
|
134
|
+
throw new Error('Modbus response unit/length mismatch');
|
|
135
|
+
const fc = response[1];
|
|
136
|
+
if (fc === (expectedFunction | 0x80)) {
|
|
137
|
+
if (response.length !== 3 || response[2] === 0)
|
|
138
|
+
throw new Error('Malformed Modbus exception response');
|
|
139
|
+
throw new ModbusExceptionError(`Modbus exception: function=${expectedFunction} code=${response[2]}`);
|
|
140
|
+
}
|
|
141
|
+
if (fc !== expectedFunction)
|
|
142
|
+
throw new Error('Modbus response function mismatch');
|
|
143
|
+
}
|
|
144
|
+
function checkReadLength(response, byteCount) {
|
|
145
|
+
if (response.length !== 3 + byteCount || response[2] !== byteCount) {
|
|
146
|
+
throw new Error('Modbus response byte count/length mismatch');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function checkWriteResponse(response, request) {
|
|
150
|
+
checkResponse(response, request[0], request[1]);
|
|
151
|
+
if (response.length !== 6 || response.some((byte, index) => byte !== request[index])) {
|
|
152
|
+
throw new Error('Modbus write acknowledgement mismatch');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function writeFailure(error) {
|
|
156
|
+
return { ok: false, error: String(error), errorCode: error instanceof ModbusRequestError
|
|
157
|
+
? 'MODBUS_INVALID_REQUEST' : error instanceof ModbusExceptionError
|
|
158
|
+
? 'MODBUS_EXCEPTION' : 'MODBUS_WRITE_OUTCOME_UNKNOWN' };
|
|
159
|
+
}
|
|
160
|
+
function parseBitResponse(response, count) {
|
|
161
|
+
checkReadLength(response, Math.ceil(count / 8));
|
|
76
162
|
const values = [];
|
|
77
163
|
for (let i = 0; i < count; i++) {
|
|
78
|
-
const byte = response[
|
|
164
|
+
const byte = response[3 + Math.floor(i / 8)];
|
|
79
165
|
values.push(((byte >> (i % 8)) & 1) === 1);
|
|
80
166
|
}
|
|
81
167
|
return values;
|
|
82
168
|
}
|
|
83
169
|
function parseRegisterResponse(response, count) {
|
|
170
|
+
checkReadLength(response, count * 2);
|
|
84
171
|
const values = [];
|
|
85
172
|
for (let i = 0; i < count; i++) {
|
|
86
|
-
|
|
87
|
-
values.push(new DataView(response.buffer, response.byteOffset + offset, 2).getUint16(0, false));
|
|
173
|
+
values.push(new DataView(response.buffer, response.byteOffset + 3 + i * 2, 2).getUint16(0, false));
|
|
88
174
|
}
|
|
89
175
|
return values;
|
|
90
176
|
}
|
|
177
|
+
// ── Operation implementations ─────────────────────────────────────────────
|
|
178
|
+
async function readBits(transport, unitId, fc, address, count) {
|
|
179
|
+
try {
|
|
180
|
+
const request = buildReadFrame(unitId, fc, address, count);
|
|
181
|
+
const response = await transport.sendFrame(request);
|
|
182
|
+
checkResponse(response, unitId, fc);
|
|
183
|
+
return { ok: true, data: { values: parseBitResponse(response, count) } };
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
return { ok: false, error: String(e) };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function readRegisters(transport, unitId, fc, address, count) {
|
|
190
|
+
try {
|
|
191
|
+
const request = buildReadFrame(unitId, fc, address, count);
|
|
192
|
+
const response = await transport.sendFrame(request);
|
|
193
|
+
checkResponse(response, unitId, fc);
|
|
194
|
+
return { ok: true, data: { values: parseRegisterResponse(response, count) } };
|
|
195
|
+
}
|
|
196
|
+
catch (e) {
|
|
197
|
+
return { ok: false, error: String(e) };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async function writeSingle(transport, unitId, fc, address, value) {
|
|
201
|
+
try {
|
|
202
|
+
const request = buildWriteSingleFrame(unitId, fc, address, value);
|
|
203
|
+
const response = await transport.sendFrame(request);
|
|
204
|
+
checkWriteResponse(response, request);
|
|
205
|
+
return { ok: true };
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
return writeFailure(e);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function writeMultipleCoilsImpl(transport, unitId, address, values) {
|
|
212
|
+
try {
|
|
213
|
+
const request = buildWriteMultipleCoilsFrame(unitId, address, values);
|
|
214
|
+
const response = await transport.sendFrame(request);
|
|
215
|
+
checkWriteResponse(response, request);
|
|
216
|
+
return { ok: true };
|
|
217
|
+
}
|
|
218
|
+
catch (e) {
|
|
219
|
+
return writeFailure(e);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
async function writeMultipleRegistersImpl(transport, unitId, address, values) {
|
|
223
|
+
try {
|
|
224
|
+
const request = buildWriteMultipleRegistersFrame(unitId, address, values);
|
|
225
|
+
const response = await transport.sendFrame(request);
|
|
226
|
+
checkWriteResponse(response, request);
|
|
227
|
+
return { ok: true };
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
return writeFailure(e);
|
|
231
|
+
}
|
|
232
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export { createModbusGateway } from './gateway.js';
|
|
|
3
3
|
export type { ModbusGatewayConfig, ModbusGateway } from './gateway.js';
|
|
4
4
|
export { createModbusSensorBridge } from './sensor-bridge.js';
|
|
5
5
|
export type { ModbusSensorBinding, ModbusSensorBridgeConfig, ModbusSensorBridge } from './sensor-bridge.js';
|
|
6
|
+
export { NativeModbusTransport } from './native-transport.js';
|
|
7
|
+
export type { NativeModbusConfig } from './native-transport.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
var gateway_js_1 = require("./gateway.js");
|
|
5
|
-
Object.defineProperty(exports, "createModbusGateway", { enumerable: true, get: function () { return gateway_js_1.createModbusGateway; } });
|
|
6
|
-
var sensor_bridge_js_1 = require("./sensor-bridge.js");
|
|
7
|
-
Object.defineProperty(exports, "createModbusSensorBridge", { enumerable: true, get: function () { return sensor_bridge_js_1.createModbusSensorBridge; } });
|
|
1
|
+
export { createModbusGateway } from './gateway.js';
|
|
2
|
+
export { createModbusSensorBridge } from './sensor-bridge.js';
|
|
3
|
+
export { NativeModbusTransport } from './native-transport.js';
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeModbusTransport — connects to the edge-modbus Go binary over TCP.
|
|
3
|
+
*
|
|
4
|
+
* The Go binary listens on a local TCP socket (default 127.0.0.1:15002)
|
|
5
|
+
* and accepts newline-delimited JSON requests. This class implements
|
|
6
|
+
* ModbusTransportPort by forwarding all calls to the Go process.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const transport = new NativeModbusTransport({ host: '127.0.0.1', port: 15002 });
|
|
10
|
+
* await transport.connect();
|
|
11
|
+
* const response = await transport.sendFrame(requestFrame);
|
|
12
|
+
* transport.disconnect();
|
|
13
|
+
*/
|
|
14
|
+
import type { ModbusTransportPort } from './transport.js';
|
|
15
|
+
export interface NativeModbusConfig {
|
|
16
|
+
host?: string;
|
|
17
|
+
port?: number;
|
|
18
|
+
/** Connection timeout in ms. Default 5000. */
|
|
19
|
+
connectTimeoutMs?: number;
|
|
20
|
+
/** Per-request timeout in ms. Default 10000. */
|
|
21
|
+
requestTimeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
export declare class NativeModbusTransport implements ModbusTransportPort {
|
|
24
|
+
private socket;
|
|
25
|
+
private buffer;
|
|
26
|
+
private pending;
|
|
27
|
+
private frameHandlers;
|
|
28
|
+
private errorHandlers;
|
|
29
|
+
private readonly config;
|
|
30
|
+
constructor(config?: NativeModbusConfig);
|
|
31
|
+
connect(): Promise<void>;
|
|
32
|
+
disconnect(): Promise<void>;
|
|
33
|
+
sendFrame(frame: Uint8Array): Promise<Uint8Array>;
|
|
34
|
+
onFrame(handler: (frame: Uint8Array) => void): () => void;
|
|
35
|
+
onError(handler: (err: Error) => void): () => void;
|
|
36
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NativeModbusTransport — connects to the edge-modbus Go binary over TCP.
|
|
3
|
+
*
|
|
4
|
+
* The Go binary listens on a local TCP socket (default 127.0.0.1:15002)
|
|
5
|
+
* and accepts newline-delimited JSON requests. This class implements
|
|
6
|
+
* ModbusTransportPort by forwarding all calls to the Go process.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* const transport = new NativeModbusTransport({ host: '127.0.0.1', port: 15002 });
|
|
10
|
+
* await transport.connect();
|
|
11
|
+
* const response = await transport.sendFrame(requestFrame);
|
|
12
|
+
* transport.disconnect();
|
|
13
|
+
*/
|
|
14
|
+
let requestCounter = 0;
|
|
15
|
+
export class NativeModbusTransport {
|
|
16
|
+
constructor(config = {}) {
|
|
17
|
+
this.socket = null;
|
|
18
|
+
this.buffer = '';
|
|
19
|
+
this.pending = new Map();
|
|
20
|
+
this.frameHandlers = [];
|
|
21
|
+
this.errorHandlers = [];
|
|
22
|
+
this.config = {
|
|
23
|
+
host: config.host ?? '127.0.0.1',
|
|
24
|
+
port: config.port ?? 15002,
|
|
25
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 5000,
|
|
26
|
+
requestTimeoutMs: config.requestTimeoutMs ?? 10000,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
async connect() {
|
|
30
|
+
const net = await import('net');
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const timer = setTimeout(() => {
|
|
33
|
+
this.socket?.destroy();
|
|
34
|
+
reject(new Error(`NativeModbusTransport: connect timeout (${this.config.connectTimeoutMs}ms)`));
|
|
35
|
+
}, this.config.connectTimeoutMs);
|
|
36
|
+
this.socket = net.createConnection({ host: this.config.host, port: this.config.port }, () => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
this.socket.on('data', (chunk) => {
|
|
41
|
+
this.buffer += chunk.toString('utf-8');
|
|
42
|
+
const lines = this.buffer.split('\n');
|
|
43
|
+
this.buffer = lines.pop() ?? '';
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
if (!line.trim())
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
const msg = JSON.parse(line);
|
|
49
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
50
|
+
const { resolve: res, reject: rej } = this.pending.get(msg.id);
|
|
51
|
+
this.pending.delete(msg.id);
|
|
52
|
+
if (msg.ok && msg.frame) {
|
|
53
|
+
res(hexToBytes(msg.frame));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
rej(new Error(msg.error ?? 'unknown error'));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// ignore parse errors on partial lines
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
this.socket.on('error', (err) => {
|
|
66
|
+
for (const handler of this.errorHandlers) {
|
|
67
|
+
try {
|
|
68
|
+
handler(err);
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
this.socket.on('close', () => {
|
|
74
|
+
for (const [, pending] of this.pending) {
|
|
75
|
+
pending.reject(new Error('NativeModbusTransport: connection closed'));
|
|
76
|
+
}
|
|
77
|
+
this.pending.clear();
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
async disconnect() {
|
|
82
|
+
if (this.socket) {
|
|
83
|
+
this.socket.destroy();
|
|
84
|
+
this.socket = null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async sendFrame(frame) {
|
|
88
|
+
if (!this.socket) {
|
|
89
|
+
throw new Error('NativeModbusTransport: not connected');
|
|
90
|
+
}
|
|
91
|
+
const id = `modbus-${++requestCounter}`;
|
|
92
|
+
const request = { id, frame: bytesToHex(frame) };
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
const timer = setTimeout(() => {
|
|
95
|
+
this.pending.delete(id);
|
|
96
|
+
reject(new Error(`NativeModbusTransport: request ${id} timed out (${this.config.requestTimeoutMs}ms)`));
|
|
97
|
+
}, this.config.requestTimeoutMs);
|
|
98
|
+
this.pending.set(id, {
|
|
99
|
+
resolve: (data) => { clearTimeout(timer); resolve(data); },
|
|
100
|
+
reject: (err) => { clearTimeout(timer); reject(err); },
|
|
101
|
+
});
|
|
102
|
+
this.socket.write(JSON.stringify(request) + '\n');
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
onFrame(handler) {
|
|
106
|
+
this.frameHandlers.push(handler);
|
|
107
|
+
return () => {
|
|
108
|
+
this.frameHandlers = this.frameHandlers.filter(h => h !== handler);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
onError(handler) {
|
|
112
|
+
this.errorHandlers.push(handler);
|
|
113
|
+
return () => {
|
|
114
|
+
this.errorHandlers = this.errorHandlers.filter(h => h !== handler);
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function bytesToHex(bytes) {
|
|
119
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
120
|
+
}
|
|
121
|
+
function hexToBytes(hex) {
|
|
122
|
+
if (hex.length % 2 !== 0)
|
|
123
|
+
throw new Error('Invalid hex string');
|
|
124
|
+
const arr = new Uint8Array(hex.length / 2);
|
|
125
|
+
for (let i = 0; i < arr.length; i++) {
|
|
126
|
+
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
127
|
+
}
|
|
128
|
+
return arr;
|
|
129
|
+
}
|
package/dist/sensor-bridge.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Modbus Sensor Bridge — maps Modbus register reads to Edge proof inputs.
|
|
3
|
+
*
|
|
4
|
+
* Shares gateway validation so malformed responses never become sensor proofs.
|
|
3
5
|
*/
|
|
4
6
|
import type { EdgeRuntime } from '@totemsdk/edge';
|
|
5
7
|
import type { ModbusTransportPort } from './transport.js';
|
|
@@ -23,7 +25,6 @@ export interface ModbusSensorBridgeConfig {
|
|
|
23
25
|
export interface ModbusSensorBridge {
|
|
24
26
|
start(): Promise<void>;
|
|
25
27
|
stop(): Promise<void>;
|
|
26
|
-
/** Force a single poll cycle. */
|
|
27
28
|
poll(): Promise<void>;
|
|
28
29
|
}
|
|
29
30
|
export declare function createModbusSensorBridge(config: ModbusSensorBridgeConfig): ModbusSensorBridge;
|
package/dist/sensor-bridge.js
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Modbus Sensor Bridge — maps Modbus register reads to Edge proof inputs.
|
|
3
|
+
*
|
|
4
|
+
* Shares gateway validation so malformed responses never become sensor proofs.
|
|
4
5
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function createModbusSensorBridge(config) {
|
|
6
|
+
import { createModbusGateway } from './gateway.js';
|
|
7
|
+
export function createModbusSensorBridge(config) {
|
|
8
8
|
const timers = [];
|
|
9
9
|
let running = false;
|
|
10
|
+
const gateway = config.gateway ?? createModbusGateway({ runtime: config.runtime, transport: config.transport });
|
|
10
11
|
async function pollBinding(binding) {
|
|
11
12
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const response = await config.transport.sendFrame(frame);
|
|
21
|
-
values = parseRegisters(response, binding.count);
|
|
22
|
-
}
|
|
13
|
+
const args = [binding.unitId, binding.address, binding.count];
|
|
14
|
+
const result = binding.functionCode === 1 ? await gateway.readCoils(...args)
|
|
15
|
+
: binding.functionCode === 2 ? await gateway.readDiscreteInputs(...args)
|
|
16
|
+
: binding.functionCode === 3 ? await gateway.readHoldingRegisters(...args)
|
|
17
|
+
: binding.functionCode === 4 ? await gateway.readInputRegisters(...args) : undefined;
|
|
18
|
+
if (!result?.ok || !result.data)
|
|
19
|
+
return;
|
|
20
|
+
const values = result.data.values;
|
|
23
21
|
if (config.runtime.ports.proof) {
|
|
24
22
|
await config.runtime.ports.proof.createProof({
|
|
25
23
|
subject: `sensor:${binding.sensorId}`,
|
|
@@ -61,28 +59,3 @@ function createModbusSensorBridge(config) {
|
|
|
61
59
|
},
|
|
62
60
|
};
|
|
63
61
|
}
|
|
64
|
-
function buildReadFrame(unitId, fc, address, count) {
|
|
65
|
-
const buf = new Uint8Array(12);
|
|
66
|
-
new DataView(buf.buffer).setUint16(0, 1, false); // transaction ID
|
|
67
|
-
new DataView(buf.buffer).setUint16(4, 6, false); // length
|
|
68
|
-
buf[6] = unitId;
|
|
69
|
-
buf[7] = fc;
|
|
70
|
-
new DataView(buf.buffer).setUint16(8, address, false);
|
|
71
|
-
new DataView(buf.buffer).setUint16(10, count, false);
|
|
72
|
-
return buf;
|
|
73
|
-
}
|
|
74
|
-
function parseCoils(response, count) {
|
|
75
|
-
const result = [];
|
|
76
|
-
for (let i = 0; i < count; i++) {
|
|
77
|
-
const byte = response[9 + Math.floor(i / 8)] ?? 0;
|
|
78
|
-
result.push(((byte >> (i % 8)) & 1) === 1);
|
|
79
|
-
}
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
function parseRegisters(response, count) {
|
|
83
|
-
const result = [];
|
|
84
|
-
for (let i = 0; i < count; i++) {
|
|
85
|
-
result.push(new DataView(response.buffer, response.byteOffset + 9 + i * 2, 2).getUint16(0, false));
|
|
86
|
-
}
|
|
87
|
-
return result;
|
|
88
|
-
}
|
package/dist/transport.d.ts
CHANGED
|
@@ -9,7 +9,13 @@ export interface ModbusTransportPort {
|
|
|
9
9
|
connect(): Promise<void>;
|
|
10
10
|
/** Close the connection. */
|
|
11
11
|
disconnect(): Promise<void>;
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Exchange [unitId, functionCode, ...PDU data], with no CRC or MBAP header.
|
|
14
|
+
* Reads return [unitId, functionCode, byteCount, ...data]; successful writes
|
|
15
|
+
* return [unitId, functionCode, addressHi, addressLo, valueOrCountHi, valueOrCountLo].
|
|
16
|
+
* The binding must validate wire CRC/MBAP transaction identity and must not
|
|
17
|
+
* retry writes automatically when the physical outcome is uncertain.
|
|
18
|
+
*/
|
|
13
19
|
sendFrame(frame: Uint8Array): Promise<Uint8Array>;
|
|
14
20
|
/** Register a handler for unsolicited/inbound frames. */
|
|
15
21
|
onFrame(handler: (frame: Uint8Array) => void): () => void;
|
package/dist/transport.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Modbus transport port — injected by the caller.
|
|
4
3
|
*
|
|
5
4
|
* Supports Modbus TCP (port 502) and Modbus RTU (serial).
|
|
6
5
|
* The caller provides the actual socket/serial implementation.
|
|
7
6
|
*/
|
|
8
|
-
|
|
7
|
+
export {};
|