@totemsdk/edge-modbus 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Totem SDK Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,85 @@
1
+ # @totemsdk/edge-modbus
2
+
3
+ Edge runtime adapter for Modbus — PLCs, RTUs, industrial sensors over serial/TCP.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @totemsdk/edge-modbus
9
+ ```
10
+
11
+ ## Design
12
+
13
+ This package does **not** import `modbus-serial`, `net`, or `serialport`. All network/serial behaviour is injected via `ModbusTransportPort`. You provide the transport; the package handles framing, polling, and proof generation.
14
+
15
+ ## Quick start
16
+
17
+ ```ts
18
+ import { createEdgeRuntime, createEdgeDevice, createCapabilitySet } from '@totemsdk/edge';
19
+ import { createModbusGateway, createModbusSensorBridge } from '@totemsdk/edge-modbus';
20
+ import type { ModbusTransportPort } from '@totemsdk/edge-modbus';
21
+
22
+ // 1. Implement ModbusTransportPort for your environment
23
+ const transport: ModbusTransportPort = {
24
+ async connect() { /* open TCP socket or serial port */ },
25
+ async disconnect() { /* close */ },
26
+ async sendFrame(frame) { /* send raw Modbus frame, return response */ },
27
+ onFrame(handler) { /* register inbound frame handler */ return () => {}; },
28
+ onError(handler) { /* register error handler */ return () => {}; },
29
+ };
30
+
31
+ // 2. Wire into Edge runtime
32
+ const runtime = createEdgeRuntime({
33
+ deviceId: 'plc-gateway-01',
34
+ capabilities: createCapabilitySet(['transport:modbus', 'proof:create']),
35
+ ports: { /* proof port from edge-adapters */ },
36
+ });
37
+
38
+ // 3. Create gateway
39
+ const gateway = createModbusGateway({ runtime, transport });
40
+ await gateway.start();
41
+
42
+ // 4. Read registers
43
+ const result = await gateway.readRegisters(1, 0, 10);
44
+ console.log(result.data?.values); // [123, 456, ...]
45
+
46
+ // 5. Or use the sensor bridge for automated polling + proof generation
47
+ const bridge = createModbusSensorBridge({
48
+ runtime,
49
+ transport,
50
+ gateway,
51
+ bindings: [
52
+ { sensorId: 'temp-zone-1', unitId: 1, functionCode: 3, address: 0, count: 2, intervalMs: 5000, dataType: 'temperature', unit: '°C' },
53
+ { sensorId: 'pump-status', unitId: 2, functionCode: 1, address: 0, count: 1, intervalMs: 1000, dataType: 'coil' },
54
+ ],
55
+ });
56
+ await bridge.start();
57
+ ```
58
+
59
+ ## Transport port
60
+
61
+ You must implement `ModbusTransportPort`:
62
+
63
+ | Method | Description |
64
+ |--------|-------------|
65
+ | `connect()` | Open TCP socket (port 502) or serial port |
66
+ | `disconnect()` | Close the connection |
67
+ | `sendFrame(frame)` | Send raw Modbus frame bytes, return response bytes |
68
+ | `onFrame(handler)` | Register handler for unsolicited/inbound frames |
69
+ | `onError(handler)` | Register handler for connection errors |
70
+
71
+ ## Supported function codes
72
+
73
+ | Code | Name | Gateway method |
74
+ |------|------|---------------|
75
+ | 1 | Read Coils | `readCoils(unitId, address, count)` |
76
+ | 3 | Read Holding Registers | `readRegisters(unitId, address, count)` |
77
+ | 4 | Read Input Registers | Via sensor bridge |
78
+
79
+ ## Capabilities
80
+
81
+ - `transport:modbus` — required for Modbus transport
82
+
83
+ ## License
84
+
85
+ MIT
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Modbus Edge Gateway — wires a ModbusTransportPort into an EdgeRuntime.
3
+ */
4
+ import type { EdgeRuntime, EdgeOperationResult } from '@totemsdk/edge';
5
+ import type { ModbusTransportPort } from './transport.js';
6
+ export interface ModbusGatewayConfig {
7
+ runtime: EdgeRuntime;
8
+ transport: ModbusTransportPort;
9
+ /** Map unit IDs to sensor/device identifiers. */
10
+ unitMap?: Record<number, string>;
11
+ }
12
+ export interface ModbusGateway {
13
+ start(): Promise<void>;
14
+ stop(): Promise<void>;
15
+ readonly status: 'stopped' | 'running' | 'error';
16
+ /** Read coils (function code 1). */
17
+ readCoils(unitId: number, address: number, count: number): Promise<EdgeOperationResult<{
18
+ values: boolean[];
19
+ }>>;
20
+ /** Read holding registers (function code 3). */
21
+ readRegisters(unitId: number, address: number, count: number): Promise<EdgeOperationResult<{
22
+ values: number[];
23
+ }>>;
24
+ }
25
+ export declare function createModbusGateway(config: ModbusGatewayConfig): ModbusGateway;
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ /**
3
+ * Modbus Edge Gateway — wires a ModbusTransportPort into an EdgeRuntime.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createModbusGateway = createModbusGateway;
7
+ function createModbusGateway(config) {
8
+ let status = 'stopped';
9
+ let unsubFrame;
10
+ let unsubError;
11
+ return {
12
+ get status() { return status; },
13
+ async start() {
14
+ if (status === 'running')
15
+ return;
16
+ await config.transport.connect();
17
+ unsubFrame = config.transport.onFrame((frame) => {
18
+ // Frames are dispatched by the sensor bridge
19
+ });
20
+ unsubError = config.transport.onError(() => {
21
+ status = 'error';
22
+ });
23
+ status = 'running';
24
+ },
25
+ async stop() {
26
+ unsubFrame?.();
27
+ unsubError?.();
28
+ await config.transport.disconnect();
29
+ status = 'stopped';
30
+ },
31
+ async readCoils(unitId, address, count) {
32
+ try {
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
+ }
41
+ },
42
+ async readRegisters(unitId, address, count) {
43
+ try {
44
+ const request = buildModbusFrame(unitId, 3, address, count);
45
+ const response = await config.transport.sendFrame(request);
46
+ const values = parseRegisterResponse(response, count);
47
+ return { ok: true, data: { values } };
48
+ }
49
+ catch (e) {
50
+ return { ok: false, error: String(e) };
51
+ }
52
+ },
53
+ };
54
+ }
55
+ function buildModbusFrame(unitId, functionCode, address, count) {
56
+ const buf = new Uint8Array(8);
57
+ buf[0] = 0;
58
+ buf[1] = 0; // transaction ID
59
+ buf[2] = 0;
60
+ buf[3] = 0; // protocol ID
61
+ buf[4] = 0;
62
+ buf[5] = 2; // length
63
+ buf[6] = unitId;
64
+ buf[7] = functionCode;
65
+ const addr = new Uint8Array(2);
66
+ new DataView(addr.buffer).setUint16(0, address, false);
67
+ const cnt = new Uint8Array(2);
68
+ new DataView(cnt.buffer).setUint16(0, count, false);
69
+ const result = new Uint8Array(8 + 4);
70
+ result.set(buf);
71
+ result.set(addr, 8);
72
+ result.set(cnt, 10);
73
+ return result;
74
+ }
75
+ function parseCoilResponse(response, count) {
76
+ const values = [];
77
+ for (let i = 0; i < count; i++) {
78
+ const byte = response[9 + Math.floor(i / 8)] ?? 0;
79
+ values.push(((byte >> (i % 8)) & 1) === 1);
80
+ }
81
+ return values;
82
+ }
83
+ function parseRegisterResponse(response, count) {
84
+ const values = [];
85
+ for (let i = 0; i < count; i++) {
86
+ const offset = 9 + i * 2;
87
+ values.push(new DataView(response.buffer, response.byteOffset + offset, 2).getUint16(0, false));
88
+ }
89
+ return values;
90
+ }
@@ -0,0 +1,5 @@
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';
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ 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; } });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Modbus Sensor Bridge — maps Modbus register reads to Edge proof inputs.
3
+ */
4
+ import type { EdgeRuntime } from '@totemsdk/edge';
5
+ import type { ModbusTransportPort } from './transport.js';
6
+ import type { ModbusGateway } from './gateway.js';
7
+ export interface ModbusSensorBinding {
8
+ sensorId: string;
9
+ unitId: number;
10
+ functionCode: number;
11
+ address: number;
12
+ count: number;
13
+ intervalMs: number;
14
+ dataType: 'coil' | 'register' | 'input';
15
+ unit?: string;
16
+ }
17
+ export interface ModbusSensorBridgeConfig {
18
+ runtime: EdgeRuntime;
19
+ transport: ModbusTransportPort;
20
+ gateway?: ModbusGateway;
21
+ bindings: ModbusSensorBinding[];
22
+ }
23
+ export interface ModbusSensorBridge {
24
+ start(): Promise<void>;
25
+ stop(): Promise<void>;
26
+ /** Force a single poll cycle. */
27
+ poll(): Promise<void>;
28
+ }
29
+ export declare function createModbusSensorBridge(config: ModbusSensorBridgeConfig): ModbusSensorBridge;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ /**
3
+ * Modbus Sensor Bridge — maps Modbus register reads to Edge proof inputs.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createModbusSensorBridge = createModbusSensorBridge;
7
+ function createModbusSensorBridge(config) {
8
+ const timers = [];
9
+ let running = false;
10
+ async function pollBinding(binding) {
11
+ try {
12
+ let values;
13
+ if (binding.functionCode === 1) {
14
+ const frame = buildReadFrame(binding.unitId, 1, binding.address, binding.count);
15
+ const response = await config.transport.sendFrame(frame);
16
+ values = parseCoils(response, binding.count);
17
+ }
18
+ else if (binding.functionCode === 3 || binding.functionCode === 4) {
19
+ const frame = buildReadFrame(binding.unitId, binding.functionCode, binding.address, binding.count);
20
+ const response = await config.transport.sendFrame(frame);
21
+ values = parseRegisters(response, binding.count);
22
+ }
23
+ if (config.runtime.ports.proof) {
24
+ await config.runtime.ports.proof.createProof({
25
+ subject: `sensor:${binding.sensorId}`,
26
+ claims: [{
27
+ sensorId: binding.sensorId,
28
+ unitId: binding.unitId,
29
+ address: binding.address,
30
+ values,
31
+ unit: binding.unit,
32
+ timestamp: Date.now(),
33
+ }],
34
+ });
35
+ }
36
+ }
37
+ catch {
38
+ // Poll failures are non-fatal — next cycle retries
39
+ }
40
+ }
41
+ return {
42
+ async start() {
43
+ if (running)
44
+ return;
45
+ for (const binding of config.bindings) {
46
+ await pollBinding(binding);
47
+ timers.push(setInterval(() => pollBinding(binding), binding.intervalMs));
48
+ }
49
+ running = true;
50
+ },
51
+ async stop() {
52
+ for (const t of timers)
53
+ clearInterval(t);
54
+ timers.length = 0;
55
+ running = false;
56
+ },
57
+ async poll() {
58
+ for (const binding of config.bindings) {
59
+ await pollBinding(binding);
60
+ }
61
+ },
62
+ };
63
+ }
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
+ }
@@ -0,0 +1,32 @@
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
+ /** Send a raw Modbus frame and receive the response. */
13
+ sendFrame(frame: Uint8Array): Promise<Uint8Array>;
14
+ /** Register a handler for unsolicited/inbound frames. */
15
+ onFrame(handler: (frame: Uint8Array) => void): () => void;
16
+ /** Register a handler for connection errors. */
17
+ onError(handler: (err: Error) => void): () => void;
18
+ }
19
+ export interface ModbusMessage {
20
+ /** Unit/slave ID (1-247). */
21
+ unitId: number;
22
+ /** Modbus function code (1-6, 15, 16). */
23
+ functionCode: number;
24
+ /** Starting address (0-based). */
25
+ address: number;
26
+ /** Register/coil count or value. */
27
+ value: number | number[];
28
+ /** Raw frame bytes. */
29
+ raw: Uint8Array;
30
+ /** Timestamp of receipt. */
31
+ receivedAt: number;
32
+ }
@@ -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/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@totemsdk/edge-modbus",
3
+ "version": "0.1.0",
4
+ "description": "Edge runtime adapter for Modbus — PLCs, RTUs, industrial sensors over serial/TCP",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "prepare": "tsc",
17
+ "clean": "rm -rf dist",
18
+ "test": "jest --passWithNoTests"
19
+ },
20
+ "files": ["dist", "README.md", "LICENSE"],
21
+ "dependencies": {
22
+ "@totemsdk/edge": "^0.1.3"
23
+ },
24
+ "devDependencies": {
25
+ "@types/jest": "^30.0.0",
26
+ "@types/node": "^20.0.0",
27
+ "jest": "^30.4.2",
28
+ "ts-jest": "^29.0.0",
29
+ "typescript": "^7.0.2"
30
+ },
31
+ "engines": { "node": ">=18.0.0" },
32
+ "publishConfig": { "access": "public" },
33
+ "license": "MIT",
34
+ "author": "Totem SDK",
35
+ "homepage": "https://totem.ing",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/totem-sdk/totem-sdk.git",
39
+ "directory": "packages/totem-sdk/packages/edge-modbus"
40
+ },
41
+ "keywords": ["totem", "totemsdk", "minima", "blockchain", "edge", "modbus", "transport"],
42
+ "bugs": { "url": "https://github.com/totem-sdk/totem-sdk/issues" }
43
+ }