@totemsdk/edge-modbus 0.2.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 +2 -3
- package/dist/gateway.js +71 -30
- package/dist/index.js +3 -9
- package/dist/native-transport.js +1 -5
- package/dist/sensor-bridge.d.ts +1 -1
- package/dist/sensor-bridge.js +12 -41
- package/dist/transport.d.ts +7 -1
- package/dist/transport.js +1 -2
- package/package.json +25 -15
- package/sidecar.json +34 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { ModbusTransportPort, ModbusMessage } from './transport.js';
|
|
2
|
+
export { createModbusGateway } from './gateway.js';
|
|
3
|
+
export type { ModbusGatewayConfig, ModbusGateway } from './gateway.js';
|
|
4
|
+
export { createModbusSensorBridge } from './sensor-bridge.js';
|
|
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';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.NativeModbusTransport = exports.createModbusSensorBridge = exports.createModbusGateway = void 0;
|
|
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; } });
|
|
8
|
+
var native_transport_js_1 = require("./native-transport.js");
|
|
9
|
+
Object.defineProperty(exports, "NativeModbusTransport", { enumerable: true, get: function () { return native_transport_js_1.NativeModbusTransport; } });
|
|
@@ -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,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* NativeModbusTransport — connects to the edge-modbus Go binary over TCP.
|
|
4
|
+
*
|
|
5
|
+
* The Go binary listens on a local TCP socket (default 127.0.0.1:15002)
|
|
6
|
+
* and accepts newline-delimited JSON requests. This class implements
|
|
7
|
+
* ModbusTransportPort by forwarding all calls to the Go process.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const transport = new NativeModbusTransport({ host: '127.0.0.1', port: 15002 });
|
|
11
|
+
* await transport.connect();
|
|
12
|
+
* const response = await transport.sendFrame(requestFrame);
|
|
13
|
+
* transport.disconnect();
|
|
14
|
+
*/
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.NativeModbusTransport = void 0;
|
|
50
|
+
let requestCounter = 0;
|
|
51
|
+
class NativeModbusTransport {
|
|
52
|
+
constructor(config = {}) {
|
|
53
|
+
this.socket = null;
|
|
54
|
+
this.buffer = '';
|
|
55
|
+
this.pending = new Map();
|
|
56
|
+
this.frameHandlers = [];
|
|
57
|
+
this.errorHandlers = [];
|
|
58
|
+
this.config = {
|
|
59
|
+
host: config.host ?? '127.0.0.1',
|
|
60
|
+
port: config.port ?? 15002,
|
|
61
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 5000,
|
|
62
|
+
requestTimeoutMs: config.requestTimeoutMs ?? 10000,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async connect() {
|
|
66
|
+
const net = await Promise.resolve().then(() => __importStar(require('net')));
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
this.socket?.destroy();
|
|
70
|
+
reject(new Error(`NativeModbusTransport: connect timeout (${this.config.connectTimeoutMs}ms)`));
|
|
71
|
+
}, this.config.connectTimeoutMs);
|
|
72
|
+
this.socket = net.createConnection({ host: this.config.host, port: this.config.port }, () => {
|
|
73
|
+
clearTimeout(timer);
|
|
74
|
+
resolve();
|
|
75
|
+
});
|
|
76
|
+
this.socket.on('data', (chunk) => {
|
|
77
|
+
this.buffer += chunk.toString('utf-8');
|
|
78
|
+
const lines = this.buffer.split('\n');
|
|
79
|
+
this.buffer = lines.pop() ?? '';
|
|
80
|
+
for (const line of lines) {
|
|
81
|
+
if (!line.trim())
|
|
82
|
+
continue;
|
|
83
|
+
try {
|
|
84
|
+
const msg = JSON.parse(line);
|
|
85
|
+
if (msg.id && this.pending.has(msg.id)) {
|
|
86
|
+
const { resolve: res, reject: rej } = this.pending.get(msg.id);
|
|
87
|
+
this.pending.delete(msg.id);
|
|
88
|
+
if (msg.ok && msg.frame) {
|
|
89
|
+
res(hexToBytes(msg.frame));
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
rej(new Error(msg.error ?? 'unknown error'));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// ignore parse errors on partial lines
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
this.socket.on('error', (err) => {
|
|
102
|
+
for (const handler of this.errorHandlers) {
|
|
103
|
+
try {
|
|
104
|
+
handler(err);
|
|
105
|
+
}
|
|
106
|
+
catch { }
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
this.socket.on('close', () => {
|
|
110
|
+
for (const [, pending] of this.pending) {
|
|
111
|
+
pending.reject(new Error('NativeModbusTransport: connection closed'));
|
|
112
|
+
}
|
|
113
|
+
this.pending.clear();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async disconnect() {
|
|
118
|
+
if (this.socket) {
|
|
119
|
+
this.socket.destroy();
|
|
120
|
+
this.socket = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async sendFrame(frame) {
|
|
124
|
+
if (!this.socket) {
|
|
125
|
+
throw new Error('NativeModbusTransport: not connected');
|
|
126
|
+
}
|
|
127
|
+
const id = `modbus-${++requestCounter}`;
|
|
128
|
+
const request = { id, frame: bytesToHex(frame) };
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
const timer = setTimeout(() => {
|
|
131
|
+
this.pending.delete(id);
|
|
132
|
+
reject(new Error(`NativeModbusTransport: request ${id} timed out (${this.config.requestTimeoutMs}ms)`));
|
|
133
|
+
}, this.config.requestTimeoutMs);
|
|
134
|
+
this.pending.set(id, {
|
|
135
|
+
resolve: (data) => { clearTimeout(timer); resolve(data); },
|
|
136
|
+
reject: (err) => { clearTimeout(timer); reject(err); },
|
|
137
|
+
});
|
|
138
|
+
this.socket.write(JSON.stringify(request) + '\n');
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
onFrame(handler) {
|
|
142
|
+
this.frameHandlers.push(handler);
|
|
143
|
+
return () => {
|
|
144
|
+
this.frameHandlers = this.frameHandlers.filter(h => h !== handler);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
onError(handler) {
|
|
148
|
+
this.errorHandlers.push(handler);
|
|
149
|
+
return () => {
|
|
150
|
+
this.errorHandlers = this.errorHandlers.filter(h => h !== handler);
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
exports.NativeModbusTransport = NativeModbusTransport;
|
|
155
|
+
function bytesToHex(bytes) {
|
|
156
|
+
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
157
|
+
}
|
|
158
|
+
function hexToBytes(hex) {
|
|
159
|
+
if (hex.length % 2 !== 0)
|
|
160
|
+
throw new Error('Invalid hex string');
|
|
161
|
+
const arr = new Uint8Array(hex.length / 2);
|
|
162
|
+
for (let i = 0; i < arr.length; i++) {
|
|
163
|
+
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
164
|
+
}
|
|
165
|
+
return arr;
|
|
166
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modbus Sensor Bridge — maps Modbus register reads to Edge proof inputs.
|
|
3
|
+
*
|
|
4
|
+
* Shares gateway validation so malformed responses never become sensor proofs.
|
|
5
|
+
*/
|
|
6
|
+
import type { EdgeRuntime } from '@totemsdk/edge';
|
|
7
|
+
import type { ModbusTransportPort } from './transport.js';
|
|
8
|
+
import type { ModbusGateway } from './gateway.js';
|
|
9
|
+
export interface ModbusSensorBinding {
|
|
10
|
+
sensorId: string;
|
|
11
|
+
unitId: number;
|
|
12
|
+
functionCode: number;
|
|
13
|
+
address: number;
|
|
14
|
+
count: number;
|
|
15
|
+
intervalMs: number;
|
|
16
|
+
dataType: 'coil' | 'register' | 'input';
|
|
17
|
+
unit?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ModbusSensorBridgeConfig {
|
|
20
|
+
runtime: EdgeRuntime;
|
|
21
|
+
transport: ModbusTransportPort;
|
|
22
|
+
gateway?: ModbusGateway;
|
|
23
|
+
bindings: ModbusSensorBinding[];
|
|
24
|
+
}
|
|
25
|
+
export interface ModbusSensorBridge {
|
|
26
|
+
start(): Promise<void>;
|
|
27
|
+
stop(): Promise<void>;
|
|
28
|
+
poll(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export declare function createModbusSensorBridge(config: ModbusSensorBridgeConfig): ModbusSensorBridge;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Modbus Sensor Bridge — maps Modbus register reads to Edge proof inputs.
|
|
4
|
+
*
|
|
5
|
+
* Shares gateway validation so malformed responses never become sensor proofs.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createModbusSensorBridge = createModbusSensorBridge;
|
|
9
|
+
const gateway_js_1 = require("./gateway.js");
|
|
10
|
+
function createModbusSensorBridge(config) {
|
|
11
|
+
const timers = [];
|
|
12
|
+
let running = false;
|
|
13
|
+
const gateway = config.gateway ?? (0, gateway_js_1.createModbusGateway)({ runtime: config.runtime, transport: config.transport });
|
|
14
|
+
async function pollBinding(binding) {
|
|
15
|
+
try {
|
|
16
|
+
const args = [binding.unitId, binding.address, binding.count];
|
|
17
|
+
const result = binding.functionCode === 1 ? await gateway.readCoils(...args)
|
|
18
|
+
: binding.functionCode === 2 ? await gateway.readDiscreteInputs(...args)
|
|
19
|
+
: binding.functionCode === 3 ? await gateway.readHoldingRegisters(...args)
|
|
20
|
+
: binding.functionCode === 4 ? await gateway.readInputRegisters(...args) : undefined;
|
|
21
|
+
if (!result?.ok || !result.data)
|
|
22
|
+
return;
|
|
23
|
+
const values = result.data.values;
|
|
24
|
+
if (config.runtime.ports.proof) {
|
|
25
|
+
await config.runtime.ports.proof.createProof({
|
|
26
|
+
subject: `sensor:${binding.sensorId}`,
|
|
27
|
+
claims: [{
|
|
28
|
+
sensorId: binding.sensorId,
|
|
29
|
+
unitId: binding.unitId,
|
|
30
|
+
address: binding.address,
|
|
31
|
+
values,
|
|
32
|
+
unit: binding.unit,
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
}],
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Poll failures are non-fatal — next cycle retries
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
async start() {
|
|
44
|
+
if (running)
|
|
45
|
+
return;
|
|
46
|
+
for (const binding of config.bindings) {
|
|
47
|
+
await pollBinding(binding);
|
|
48
|
+
timers.push(setInterval(() => pollBinding(binding), binding.intervalMs));
|
|
49
|
+
}
|
|
50
|
+
running = true;
|
|
51
|
+
},
|
|
52
|
+
async stop() {
|
|
53
|
+
for (const t of timers)
|
|
54
|
+
clearInterval(t);
|
|
55
|
+
timers.length = 0;
|
|
56
|
+
running = false;
|
|
57
|
+
},
|
|
58
|
+
async poll() {
|
|
59
|
+
for (const binding of config.bindings) {
|
|
60
|
+
await pollBinding(binding);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modbus transport port — injected by the caller.
|
|
3
|
+
*
|
|
4
|
+
* Supports Modbus TCP (port 502) and Modbus RTU (serial).
|
|
5
|
+
* The caller provides the actual socket/serial implementation.
|
|
6
|
+
*/
|
|
7
|
+
export interface ModbusTransportPort {
|
|
8
|
+
/** Open the connection. */
|
|
9
|
+
connect(): Promise<void>;
|
|
10
|
+
/** Close the connection. */
|
|
11
|
+
disconnect(): Promise<void>;
|
|
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
|
+
*/
|
|
19
|
+
sendFrame(frame: Uint8Array): Promise<Uint8Array>;
|
|
20
|
+
/** Register a handler for unsolicited/inbound frames. */
|
|
21
|
+
onFrame(handler: (frame: Uint8Array) => void): () => void;
|
|
22
|
+
/** Register a handler for connection errors. */
|
|
23
|
+
onError(handler: (err: Error) => void): () => void;
|
|
24
|
+
}
|
|
25
|
+
export interface ModbusMessage {
|
|
26
|
+
/** Unit/slave ID (1-247). */
|
|
27
|
+
unitId: number;
|
|
28
|
+
/** Modbus function code (1-6, 15, 16). */
|
|
29
|
+
functionCode: number;
|
|
30
|
+
/** Starting address (0-based). */
|
|
31
|
+
address: number;
|
|
32
|
+
/** Register/coil count or value. */
|
|
33
|
+
value: number | number[];
|
|
34
|
+
/** Raw frame bytes. */
|
|
35
|
+
raw: Uint8Array;
|
|
36
|
+
/** Timestamp of receipt. */
|
|
37
|
+
receivedAt: number;
|
|
38
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Modbus transport port — injected by the caller.
|
|
4
|
+
*
|
|
5
|
+
* Supports Modbus TCP (port 502) and Modbus RTU (serial).
|
|
6
|
+
* The caller provides the actual socket/serial implementation.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
package/dist/gateway.d.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Modbus Edge Gateway — wires a ModbusTransportPort into an EdgeRuntime.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* The
|
|
6
|
-
* is correct for RTU but would misparse TCP frames.
|
|
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.
|
|
7
6
|
*/
|
|
8
7
|
import type { EdgeRuntime, EdgeOperationResult } from '@totemsdk/edge';
|
|
9
8
|
import type { ModbusTransportPort } from './transport.js';
|
package/dist/gateway.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Modbus Edge Gateway — wires a ModbusTransportPort into an EdgeRuntime.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* The
|
|
7
|
-
* is correct for RTU but would misparse TCP frames.
|
|
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.
|
|
8
6
|
*/
|
|
9
|
-
|
|
10
|
-
exports.createModbusGateway = createModbusGateway;
|
|
11
|
-
function createModbusGateway(config) {
|
|
7
|
+
export function createModbusGateway(config) {
|
|
12
8
|
let status = 'stopped';
|
|
13
9
|
let unsubFrame;
|
|
14
10
|
let unsubError;
|
|
@@ -45,6 +41,8 @@ function createModbusGateway(config) {
|
|
|
45
41
|
return readRegisters(config.transport, unitId, 4, address, count);
|
|
46
42
|
},
|
|
47
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' };
|
|
48
46
|
return writeSingle(config.transport, unitId, 5, address, value ? 0xFF00 : 0x0000);
|
|
49
47
|
},
|
|
50
48
|
async writeSingleRegister(unitId, address, value) {
|
|
@@ -58,29 +56,48 @@ function createModbusGateway(config) {
|
|
|
58
56
|
},
|
|
59
57
|
};
|
|
60
58
|
}
|
|
61
|
-
// ──
|
|
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
|
+
}
|
|
62
76
|
function buildReadFrame(unitId, fc, address, count) {
|
|
63
|
-
|
|
77
|
+
validateRange(unitId, address, count, fc <= 2 ? 2000 : 125);
|
|
78
|
+
const buf = new Uint8Array(6);
|
|
64
79
|
buf[0] = unitId;
|
|
65
80
|
buf[1] = fc;
|
|
66
81
|
new DataView(buf.buffer).setUint16(2, address, false);
|
|
67
82
|
new DataView(buf.buffer).setUint16(4, count, false);
|
|
68
|
-
// CRC placeholder (bytes 6-7) — Go library handles CRC internally
|
|
69
|
-
buf[6] = 0;
|
|
70
|
-
buf[7] = 0;
|
|
71
83
|
return buf;
|
|
72
84
|
}
|
|
73
85
|
function buildWriteSingleFrame(unitId, fc, address, value) {
|
|
74
|
-
|
|
86
|
+
validateRange(unitId, address, 1, 1);
|
|
87
|
+
validateRegister(value);
|
|
88
|
+
const buf = new Uint8Array(6);
|
|
75
89
|
buf[0] = unitId;
|
|
76
90
|
buf[1] = fc;
|
|
77
91
|
new DataView(buf.buffer).setUint16(2, address, false);
|
|
78
92
|
new DataView(buf.buffer).setUint16(4, value, false);
|
|
79
|
-
buf[6] = 0;
|
|
80
|
-
buf[7] = 0;
|
|
81
93
|
return buf;
|
|
82
94
|
}
|
|
83
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
|
+
}
|
|
84
101
|
const byteCount = Math.ceil(values.length / 8);
|
|
85
102
|
const buf = new Uint8Array(7 + byteCount);
|
|
86
103
|
buf[0] = unitId;
|
|
@@ -96,6 +113,9 @@ function buildWriteMultipleCoilsFrame(unitId, address, values) {
|
|
|
96
113
|
return buf;
|
|
97
114
|
}
|
|
98
115
|
function buildWriteMultipleRegistersFrame(unitId, address, values) {
|
|
116
|
+
validateRange(unitId, address, values.length, 123);
|
|
117
|
+
for (const value of values)
|
|
118
|
+
validateRegister(value);
|
|
99
119
|
const byteCount = values.length * 2;
|
|
100
120
|
const buf = new Uint8Array(7 + byteCount);
|
|
101
121
|
buf[0] = unitId;
|
|
@@ -109,26 +129,45 @@ function buildWriteMultipleRegistersFrame(unitId, address, values) {
|
|
|
109
129
|
return buf;
|
|
110
130
|
}
|
|
111
131
|
// ── Response parsers (RTU format — no MBAP header) ────────────────────────
|
|
112
|
-
function
|
|
113
|
-
if (response.length < 2)
|
|
114
|
-
|
|
132
|
+
function checkResponse(response, unitId, expectedFunction) {
|
|
133
|
+
if (response.length < 2 || response[0] !== unitId)
|
|
134
|
+
throw new Error('Modbus response unit/length mismatch');
|
|
115
135
|
const fc = response[1];
|
|
116
|
-
if (fc
|
|
117
|
-
|
|
118
|
-
|
|
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]}`);
|
|
119
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' };
|
|
120
159
|
}
|
|
121
160
|
function parseBitResponse(response, count) {
|
|
122
|
-
|
|
161
|
+
checkReadLength(response, Math.ceil(count / 8));
|
|
123
162
|
const values = [];
|
|
124
163
|
for (let i = 0; i < count; i++) {
|
|
125
|
-
const byte = response[3 + Math.floor(i / 8)]
|
|
164
|
+
const byte = response[3 + Math.floor(i / 8)];
|
|
126
165
|
values.push(((byte >> (i % 8)) & 1) === 1);
|
|
127
166
|
}
|
|
128
167
|
return values;
|
|
129
168
|
}
|
|
130
169
|
function parseRegisterResponse(response, count) {
|
|
131
|
-
|
|
170
|
+
checkReadLength(response, count * 2);
|
|
132
171
|
const values = [];
|
|
133
172
|
for (let i = 0; i < count; i++) {
|
|
134
173
|
values.push(new DataView(response.buffer, response.byteOffset + 3 + i * 2, 2).getUint16(0, false));
|
|
@@ -140,6 +179,7 @@ async function readBits(transport, unitId, fc, address, count) {
|
|
|
140
179
|
try {
|
|
141
180
|
const request = buildReadFrame(unitId, fc, address, count);
|
|
142
181
|
const response = await transport.sendFrame(request);
|
|
182
|
+
checkResponse(response, unitId, fc);
|
|
143
183
|
return { ok: true, data: { values: parseBitResponse(response, count) } };
|
|
144
184
|
}
|
|
145
185
|
catch (e) {
|
|
@@ -150,6 +190,7 @@ async function readRegisters(transport, unitId, fc, address, count) {
|
|
|
150
190
|
try {
|
|
151
191
|
const request = buildReadFrame(unitId, fc, address, count);
|
|
152
192
|
const response = await transport.sendFrame(request);
|
|
193
|
+
checkResponse(response, unitId, fc);
|
|
153
194
|
return { ok: true, data: { values: parseRegisterResponse(response, count) } };
|
|
154
195
|
}
|
|
155
196
|
catch (e) {
|
|
@@ -160,32 +201,32 @@ async function writeSingle(transport, unitId, fc, address, value) {
|
|
|
160
201
|
try {
|
|
161
202
|
const request = buildWriteSingleFrame(unitId, fc, address, value);
|
|
162
203
|
const response = await transport.sendFrame(request);
|
|
163
|
-
|
|
204
|
+
checkWriteResponse(response, request);
|
|
164
205
|
return { ok: true };
|
|
165
206
|
}
|
|
166
207
|
catch (e) {
|
|
167
|
-
return
|
|
208
|
+
return writeFailure(e);
|
|
168
209
|
}
|
|
169
210
|
}
|
|
170
211
|
async function writeMultipleCoilsImpl(transport, unitId, address, values) {
|
|
171
212
|
try {
|
|
172
213
|
const request = buildWriteMultipleCoilsFrame(unitId, address, values);
|
|
173
214
|
const response = await transport.sendFrame(request);
|
|
174
|
-
|
|
215
|
+
checkWriteResponse(response, request);
|
|
175
216
|
return { ok: true };
|
|
176
217
|
}
|
|
177
218
|
catch (e) {
|
|
178
|
-
return
|
|
219
|
+
return writeFailure(e);
|
|
179
220
|
}
|
|
180
221
|
}
|
|
181
222
|
async function writeMultipleRegistersImpl(transport, unitId, address, values) {
|
|
182
223
|
try {
|
|
183
224
|
const request = buildWriteMultipleRegistersFrame(unitId, address, values);
|
|
184
225
|
const response = await transport.sendFrame(request);
|
|
185
|
-
|
|
226
|
+
checkWriteResponse(response, request);
|
|
186
227
|
return { ok: true };
|
|
187
228
|
}
|
|
188
229
|
catch (e) {
|
|
189
|
-
return
|
|
230
|
+
return writeFailure(e);
|
|
190
231
|
}
|
|
191
232
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,9 +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; } });
|
|
8
|
-
var native_transport_js_1 = require("./native-transport.js");
|
|
9
|
-
Object.defineProperty(exports, "NativeModbusTransport", { enumerable: true, get: function () { return native_transport_js_1.NativeModbusTransport; } });
|
|
1
|
+
export { createModbusGateway } from './gateway.js';
|
|
2
|
+
export { createModbusSensorBridge } from './sensor-bridge.js';
|
|
3
|
+
export { NativeModbusTransport } from './native-transport.js';
|
package/dist/native-transport.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* NativeModbusTransport — connects to the edge-modbus Go binary over TCP.
|
|
4
3
|
*
|
|
@@ -12,10 +11,8 @@
|
|
|
12
11
|
* const response = await transport.sendFrame(requestFrame);
|
|
13
12
|
* transport.disconnect();
|
|
14
13
|
*/
|
|
15
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.NativeModbusTransport = void 0;
|
|
17
14
|
let requestCounter = 0;
|
|
18
|
-
class NativeModbusTransport {
|
|
15
|
+
export class NativeModbusTransport {
|
|
19
16
|
constructor(config = {}) {
|
|
20
17
|
this.socket = null;
|
|
21
18
|
this.buffer = '';
|
|
@@ -118,7 +115,6 @@ class NativeModbusTransport {
|
|
|
118
115
|
};
|
|
119
116
|
}
|
|
120
117
|
}
|
|
121
|
-
exports.NativeModbusTransport = NativeModbusTransport;
|
|
122
118
|
function bytesToHex(bytes) {
|
|
123
119
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
124
120
|
}
|
package/dist/sensor-bridge.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Modbus Sensor Bridge — maps Modbus register reads to Edge proof inputs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Shares gateway validation so malformed responses never become sensor proofs.
|
|
5
5
|
*/
|
|
6
6
|
import type { EdgeRuntime } from '@totemsdk/edge';
|
|
7
7
|
import type { ModbusTransportPort } from './transport.js';
|