@technoculture/data-bridge 0.1.0 → 0.1.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/CMakeLists.txt +10 -2
- package/README.md +3 -3
- package/deps/include/data_bridge/config.hpp +69 -0
- package/deps/include/data_bridge/protocol/crc16.hpp +8 -0
- package/deps/include/data_bridge/protocol/crc32.hpp +19 -0
- package/deps/include/data_bridge/protocol/packet.hpp +175 -0
- package/deps/include/data_bridge/protocol/reassembler.hpp +80 -0
- package/deps/include/data_bridge/transport/factory.hpp +0 -0
- package/deps/include/data_bridge/transport/iserial_port.hpp +15 -0
- package/deps/include/data_bridge/transport/serial_port.hpp +28 -0
- package/deps/src/CMakeLists.txt +18 -0
- package/deps/src/protocol/crc16.cpp +19 -0
- package/deps/src/protocol/packet.cpp +1 -0
- package/deps/src/transport/platform/linux/linux_serial.cpp +53 -0
- package/deps/src/transport/platform/windows/windows_serial.cpp +51 -0
- package/dist/index.d.mts +41 -72
- package/dist/index.d.ts +41 -72
- package/dist/index.js +196 -102
- package/dist/index.mjs +194 -103
- package/lib/index.ts +12 -160
- package/lib/native.ts +71 -0
- package/lib/reliable.ts +234 -0
- package/lib/resilient.ts +30 -6
- package/package.json +6 -4
- package/prebuilds/darwin-arm64/Release/data_bridge_node.node +0 -0
- package/src/addon.cpp +248 -137
- package/prebuilds/darwin-arm64/.ninja_deps +0 -0
- package/prebuilds/darwin-arm64/.ninja_log +0 -6
- package/prebuilds/darwin-arm64/CMakeCache.txt +0 -398
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeCCompiler.cmake +0 -84
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeCXXCompiler.cmake +0 -104
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeDetermineCompilerABI_C.bin +0 -0
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeDetermineCompilerABI_CXX.bin +0 -0
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CMakeSystem.cmake +0 -15
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdC/CMakeCCompilerId.c +0 -905
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdC/a.out +0 -0
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdC/apple-sdk.c +0 -1
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdCXX/CMakeCXXCompilerId.cpp +0 -920
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdCXX/a.out +0 -0
- package/prebuilds/darwin-arm64/CMakeFiles/4.0.3/CompilerIdCXX/apple-sdk.cpp +0 -1
- package/prebuilds/darwin-arm64/CMakeFiles/CMakeConfigureLog.yaml +0 -531
- package/prebuilds/darwin-arm64/CMakeFiles/InstallScripts.json +0 -7
- package/prebuilds/darwin-arm64/CMakeFiles/TargetDirectories.txt +0 -3
- package/prebuilds/darwin-arm64/CMakeFiles/cmake.check_cache +0 -1
- package/prebuilds/darwin-arm64/CMakeFiles/data_bridge_node.dir/Users/satyamtiwary/Documents/Python-Things/data-bridge/src/protocol/crc16.cpp.o +0 -0
- package/prebuilds/darwin-arm64/CMakeFiles/data_bridge_node.dir/Users/satyamtiwary/Documents/Python-Things/data-bridge/src/transport/platform/linux/linux_serial.cpp.o +0 -0
- package/prebuilds/darwin-arm64/CMakeFiles/data_bridge_node.dir/src/addon.cpp.o +0 -0
- package/prebuilds/darwin-arm64/CMakeFiles/data_bridge_node.dir/src/serial_wrapper.cpp.o +0 -0
- package/prebuilds/darwin-arm64/CMakeFiles/rules.ninja +0 -64
- package/prebuilds/darwin-arm64/build.ninja +0 -192
- package/prebuilds/darwin-arm64/cmake_install.cmake +0 -61
package/dist/index.mjs
CHANGED
|
@@ -5,13 +5,188 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
5
5
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
6
|
});
|
|
7
7
|
|
|
8
|
-
// lib/
|
|
9
|
-
import { EventEmitter
|
|
8
|
+
// lib/reliable.ts
|
|
9
|
+
import { EventEmitter } from "events";
|
|
10
|
+
|
|
11
|
+
// lib/native.ts
|
|
10
12
|
import path from "path";
|
|
13
|
+
function loadAddon() {
|
|
14
|
+
const possiblePaths = [
|
|
15
|
+
`../prebuilds/${process.platform}-${process.arch}/data_bridge_node.node`,
|
|
16
|
+
"../build/Release/data_bridge_node.node"
|
|
17
|
+
];
|
|
18
|
+
for (const p of possiblePaths) {
|
|
19
|
+
try {
|
|
20
|
+
return __require(path.join(__dirname, p));
|
|
21
|
+
} catch {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
throw new Error("Failed to load native addon. Run `npm run build` first.");
|
|
26
|
+
}
|
|
27
|
+
var addon = loadAddon();
|
|
28
|
+
var SerialPort = addon.SerialPort;
|
|
29
|
+
var Packet = addon.Packet;
|
|
30
|
+
var Reassembler = addon.Reassembler;
|
|
31
|
+
|
|
32
|
+
// lib/reliable.ts
|
|
33
|
+
var ReliableDataBridge = class _ReliableDataBridge extends EventEmitter {
|
|
34
|
+
serial;
|
|
35
|
+
reassembler;
|
|
36
|
+
isOpen_ = false;
|
|
37
|
+
// Config
|
|
38
|
+
options;
|
|
39
|
+
// State
|
|
40
|
+
seqId = 0;
|
|
41
|
+
rxBuffer = Buffer.alloc(0);
|
|
42
|
+
// ARQ State
|
|
43
|
+
// We only process one send at a time (Stop-and-Wait)
|
|
44
|
+
// Map of seqId -> { resolve, reject } (though we only have one really)
|
|
45
|
+
pendingAcks = /* @__PURE__ */ new Map();
|
|
46
|
+
constructor(options = {}) {
|
|
47
|
+
super();
|
|
48
|
+
this.serial = new SerialPort();
|
|
49
|
+
this.reassembler = new Reassembler();
|
|
50
|
+
this.options = {
|
|
51
|
+
baudRate: options.baudRate ?? 115200,
|
|
52
|
+
maxRetries: options.maxRetries ?? 10,
|
|
53
|
+
ackTimeoutMs: options.ackTimeoutMs ?? 500,
|
|
54
|
+
fragmentSize: options.fragmentSize ?? 200
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
static async open(port, baudOrOptions, cb) {
|
|
58
|
+
let opts = {};
|
|
59
|
+
if (typeof baudOrOptions === "number") {
|
|
60
|
+
opts.baudRate = baudOrOptions;
|
|
61
|
+
} else if (baudOrOptions) {
|
|
62
|
+
opts = baudOrOptions;
|
|
63
|
+
}
|
|
64
|
+
const bridge = new _ReliableDataBridge(opts);
|
|
65
|
+
if (cb) bridge.on("data", cb);
|
|
66
|
+
await bridge.open(port);
|
|
67
|
+
return bridge;
|
|
68
|
+
}
|
|
69
|
+
async open(port, baud) {
|
|
70
|
+
if (this.isOpen_) return true;
|
|
71
|
+
const baudRate = baud ?? this.options.baudRate;
|
|
72
|
+
const success = this.serial.open(port, baudRate, (data) => {
|
|
73
|
+
this.onData(data);
|
|
74
|
+
});
|
|
75
|
+
if (success) {
|
|
76
|
+
this.isOpen_ = true;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
throw new Error(`Failed to open ${port}`);
|
|
80
|
+
}
|
|
81
|
+
async close() {
|
|
82
|
+
if (this.isOpen_) {
|
|
83
|
+
this.serial.close();
|
|
84
|
+
this.isOpen_ = false;
|
|
85
|
+
this.emit("close");
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
get isOpen() {
|
|
89
|
+
return this.isOpen_;
|
|
90
|
+
}
|
|
91
|
+
async send(data) {
|
|
92
|
+
if (!this.isOpen_) throw new Error("Port not open");
|
|
93
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
94
|
+
const fragments = [];
|
|
95
|
+
for (let i = 0; i < buf.length; i += this.options.fragmentSize) {
|
|
96
|
+
fragments.push(buf.subarray(i, i + this.options.fragmentSize));
|
|
97
|
+
}
|
|
98
|
+
const totalFrags = fragments.length;
|
|
99
|
+
const seq = this.seqId;
|
|
100
|
+
this.seqId = (this.seqId + 1) % 256;
|
|
101
|
+
for (let fragId = 0; fragId < totalFrags; fragId++) {
|
|
102
|
+
const payload = fragments[fragId];
|
|
103
|
+
const packet = Packet.serialize(
|
|
104
|
+
Packet.TYPE_DATA,
|
|
105
|
+
seq,
|
|
106
|
+
payload,
|
|
107
|
+
fragId,
|
|
108
|
+
totalFrags
|
|
109
|
+
);
|
|
110
|
+
await this.sendWithRetry(packet, seq, fragId);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async sendWithRetry(packet, seq, fragId) {
|
|
114
|
+
let retries = 0;
|
|
115
|
+
while (retries <= this.options.maxRetries) {
|
|
116
|
+
const ackPromise = new Promise((resolve, reject) => {
|
|
117
|
+
const handler = (ackedFragId) => {
|
|
118
|
+
if (ackedFragId === fragId) resolve();
|
|
119
|
+
};
|
|
120
|
+
this.pendingAcks.set(seq, handler);
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
if (this.pendingAcks.has(seq)) {
|
|
123
|
+
this.pendingAcks.delete(seq);
|
|
124
|
+
reject(new Error("Timeout"));
|
|
125
|
+
}
|
|
126
|
+
}, this.options.ackTimeoutMs);
|
|
127
|
+
});
|
|
128
|
+
this.serial.write(packet);
|
|
129
|
+
try {
|
|
130
|
+
await ackPromise;
|
|
131
|
+
return;
|
|
132
|
+
} catch (e) {
|
|
133
|
+
retries++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Send failed after ${this.options.maxRetries} retries`);
|
|
137
|
+
}
|
|
138
|
+
onData(chunk) {
|
|
139
|
+
this.rxBuffer = Buffer.concat([this.rxBuffer, chunk]);
|
|
140
|
+
while (true) {
|
|
141
|
+
const { frame, remaining } = Packet.deserialize(this.rxBuffer);
|
|
142
|
+
if (!frame.valid) {
|
|
143
|
+
if (remaining.length === this.rxBuffer.length) {
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
this.rxBuffer = Buffer.from(remaining);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
this.rxBuffer = Buffer.from(remaining);
|
|
150
|
+
if (frame.header.type === Packet.TYPE_ACK) {
|
|
151
|
+
const seq = frame.header.seq_id;
|
|
152
|
+
const frag = frame.header.fragment_id;
|
|
153
|
+
if (this.pendingAcks.has(seq)) {
|
|
154
|
+
this.pendingAcks.get(seq)(frag);
|
|
155
|
+
}
|
|
156
|
+
} else if (frame.header.type === Packet.TYPE_DATA) {
|
|
157
|
+
let shouldAck = false;
|
|
158
|
+
if (this.reassembler.processFragment(frame)) {
|
|
159
|
+
shouldAck = true;
|
|
160
|
+
if (this.reassembler.isComplete(frame)) {
|
|
161
|
+
try {
|
|
162
|
+
const data = this.reassembler.getData();
|
|
163
|
+
this.emit("data", data);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
console.error("Error in data emission:", e);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} else if (this.reassembler.isDuplicate(frame)) {
|
|
169
|
+
shouldAck = true;
|
|
170
|
+
}
|
|
171
|
+
if (shouldAck) {
|
|
172
|
+
const ackParams = Packet.serialize(
|
|
173
|
+
Packet.TYPE_ACK,
|
|
174
|
+
frame.header.seq_id,
|
|
175
|
+
Buffer.alloc(0),
|
|
176
|
+
frame.header.fragment_id,
|
|
177
|
+
frame.header.total_frags
|
|
178
|
+
);
|
|
179
|
+
this.serial.write(ackParams);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
};
|
|
11
185
|
|
|
12
186
|
// lib/resilient.ts
|
|
13
|
-
import { EventEmitter } from "events";
|
|
14
|
-
var ResilientDataBridge = class _ResilientDataBridge extends
|
|
187
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
188
|
+
var ResilientDataBridge = class _ResilientDataBridge extends EventEmitter2 {
|
|
189
|
+
// Use DataBridge type from index (which is reliable now)
|
|
15
190
|
bridge = null;
|
|
16
191
|
port;
|
|
17
192
|
options;
|
|
@@ -26,6 +201,9 @@ var ResilientDataBridge = class _ResilientDataBridge extends EventEmitter {
|
|
|
26
201
|
this.port = port;
|
|
27
202
|
this.options = {
|
|
28
203
|
baudRate: options.baudRate ?? 115200,
|
|
204
|
+
maxRetries: options.maxRetries ?? 10,
|
|
205
|
+
ackTimeoutMs: options.ackTimeoutMs ?? 500,
|
|
206
|
+
fragmentSize: options.fragmentSize ?? 200,
|
|
29
207
|
reconnect: options.reconnect ?? true,
|
|
30
208
|
reconnectDelay: options.reconnectDelay ?? 1e3,
|
|
31
209
|
maxReconnectDelay: options.maxReconnectDelay ?? 3e4,
|
|
@@ -45,9 +223,8 @@ var ResilientDataBridge = class _ResilientDataBridge extends EventEmitter {
|
|
|
45
223
|
}
|
|
46
224
|
async connect() {
|
|
47
225
|
try {
|
|
48
|
-
this.bridge =
|
|
49
|
-
|
|
50
|
-
});
|
|
226
|
+
this.bridge = new ReliableDataBridge(this.options);
|
|
227
|
+
await this.bridge.open(this.port);
|
|
51
228
|
this.isConnected = true;
|
|
52
229
|
this.reconnectAttempt = 0;
|
|
53
230
|
this.bridge.on("data", (data) => {
|
|
@@ -55,6 +232,11 @@ var ResilientDataBridge = class _ResilientDataBridge extends EventEmitter {
|
|
|
55
232
|
});
|
|
56
233
|
this.bridge.on("error", (err) => {
|
|
57
234
|
this.emit("error", err);
|
|
235
|
+
if (!this.bridge?.isOpen) {
|
|
236
|
+
this.handleDisconnect();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
this.bridge.on("close", () => {
|
|
58
240
|
this.handleDisconnect();
|
|
59
241
|
});
|
|
60
242
|
if (this.isReconnecting) {
|
|
@@ -110,10 +292,11 @@ var ResilientDataBridge = class _ResilientDataBridge extends EventEmitter {
|
|
|
110
292
|
*/
|
|
111
293
|
async send(data) {
|
|
112
294
|
const buffer = typeof data === "string" ? Buffer.from(data) : data;
|
|
113
|
-
if (this.isConnected && this.bridge) {
|
|
295
|
+
if (this.isConnected && this.bridge && this.bridge.isOpen) {
|
|
114
296
|
try {
|
|
115
297
|
await this.bridge.send(buffer);
|
|
116
298
|
} catch (err) {
|
|
299
|
+
this.handleDisconnect();
|
|
117
300
|
return this.queueMessage(buffer);
|
|
118
301
|
}
|
|
119
302
|
} else {
|
|
@@ -186,100 +369,8 @@ var ResilientDataBridge = class _ResilientDataBridge extends EventEmitter {
|
|
|
186
369
|
return super.emit(event, ...args);
|
|
187
370
|
}
|
|
188
371
|
};
|
|
189
|
-
|
|
190
|
-
// lib/index.ts
|
|
191
|
-
var addon;
|
|
192
|
-
function loadAddon() {
|
|
193
|
-
const possiblePaths = [
|
|
194
|
-
// Prebuilt binaries
|
|
195
|
-
`../prebuilds/${process.platform}-${process.arch}/data_bridge_node.node`,
|
|
196
|
-
// Local build
|
|
197
|
-
"../build/Release/data_bridge_node.node"
|
|
198
|
-
];
|
|
199
|
-
for (const p of possiblePaths) {
|
|
200
|
-
try {
|
|
201
|
-
return __require(path.join(__dirname, p));
|
|
202
|
-
} catch {
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
throw new Error("Failed to load native addon. Run `npm run build` first.");
|
|
207
|
-
}
|
|
208
|
-
addon = loadAddon();
|
|
209
|
-
var DataBridge = class _DataBridge extends EventEmitter2 {
|
|
210
|
-
native;
|
|
211
|
-
_isOpen = false;
|
|
212
|
-
constructor() {
|
|
213
|
-
super();
|
|
214
|
-
this.native = new addon.DataBridge();
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Open a serial port with guaranteed reliable communication.
|
|
218
|
-
*
|
|
219
|
-
* @param port - Serial port path (e.g., '/dev/ttyUSB0' or 'COM3')
|
|
220
|
-
* @param options - Configuration options
|
|
221
|
-
* @returns Promise resolving to a DataBridge instance
|
|
222
|
-
*/
|
|
223
|
-
static async open(port, options = {}) {
|
|
224
|
-
const instance = new _DataBridge();
|
|
225
|
-
const baud = options.baudRate ?? 115200;
|
|
226
|
-
const onData = (data) => {
|
|
227
|
-
instance.emit("data", data);
|
|
228
|
-
};
|
|
229
|
-
try {
|
|
230
|
-
await instance.native.open(port, baud, onData);
|
|
231
|
-
instance._isOpen = true;
|
|
232
|
-
return instance;
|
|
233
|
-
} catch (err) {
|
|
234
|
-
throw new Error(`Failed to open ${port}: ${err instanceof Error ? err.message : err}`);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Send data with guaranteed delivery.
|
|
239
|
-
*
|
|
240
|
-
* The data will be fragmented if necessary, checksummed, and
|
|
241
|
-
* retransmitted until acknowledged by the receiver.
|
|
242
|
-
*
|
|
243
|
-
* @param data - Data to send (Buffer or string)
|
|
244
|
-
* @returns Promise resolving when data is acknowledged
|
|
245
|
-
*/
|
|
246
|
-
async send(data) {
|
|
247
|
-
if (!this._isOpen) {
|
|
248
|
-
throw new Error("Port not open");
|
|
249
|
-
}
|
|
250
|
-
const buffer = typeof data === "string" ? Buffer.from(data) : data;
|
|
251
|
-
await this.native.send(buffer);
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Close the serial port.
|
|
255
|
-
*/
|
|
256
|
-
async close() {
|
|
257
|
-
if (this._isOpen) {
|
|
258
|
-
this.native.close();
|
|
259
|
-
this._isOpen = false;
|
|
260
|
-
this.emit("close");
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Check if the port is currently open.
|
|
265
|
-
*/
|
|
266
|
-
get isOpen() {
|
|
267
|
-
return this._isOpen && this.native.isOpen();
|
|
268
|
-
}
|
|
269
|
-
// Type-safe event emitter methods
|
|
270
|
-
on(event, listener) {
|
|
271
|
-
return super.on(event, listener);
|
|
272
|
-
}
|
|
273
|
-
once(event, listener) {
|
|
274
|
-
return super.once(event, listener);
|
|
275
|
-
}
|
|
276
|
-
emit(event, ...args) {
|
|
277
|
-
return super.emit(event, ...args);
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
var index_default = DataBridge;
|
|
281
372
|
export {
|
|
282
|
-
DataBridge,
|
|
283
|
-
|
|
284
|
-
|
|
373
|
+
ReliableDataBridge as DataBridge,
|
|
374
|
+
SerialPort as RawSerialPort,
|
|
375
|
+
ResilientDataBridge
|
|
285
376
|
};
|
package/lib/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
/**
|
|
2
3
|
* Data Bridge - Guaranteed Reliable Serial Communication
|
|
3
4
|
*
|
|
@@ -5,167 +6,18 @@
|
|
|
5
6
|
* Provides a clean, async-friendly API for Electron applications.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
// Load native addon - works in both ESM and CJS
|
|
12
|
-
let addon: NativeAddon;
|
|
13
|
-
|
|
14
|
-
function loadAddon (): NativeAddon {
|
|
15
|
-
const possiblePaths = [
|
|
16
|
-
// Prebuilt binaries
|
|
17
|
-
`../prebuilds/${process.platform}-${process.arch}/data_bridge_node.node`,
|
|
18
|
-
// Local build
|
|
19
|
-
'../build/Release/data_bridge_node.node',
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
for (const p of possiblePaths)
|
|
23
|
-
{
|
|
24
|
-
try
|
|
25
|
-
{
|
|
26
|
-
// Use require for native addons (works in both ESM and CJS)
|
|
27
|
-
return require(path.join(__dirname, p));
|
|
28
|
-
} catch
|
|
29
|
-
{
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
throw new Error('Failed to load native addon. Run `npm run build` first.');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
addon = loadAddon();
|
|
38
|
-
|
|
39
|
-
interface NativeAddon {
|
|
40
|
-
DataBridge: new () => NativeDataBridge;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
interface NativeDataBridge {
|
|
44
|
-
open (port: string, baud: number, callback?: (data: Buffer) => void): Promise<boolean>;
|
|
45
|
-
send (data: Buffer | string): Promise<number>;
|
|
46
|
-
close (): boolean;
|
|
47
|
-
isOpen (): boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface DataBridgeOptions {
|
|
51
|
-
baudRate?: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface DataBridgeEvents {
|
|
55
|
-
data: (data: Buffer) => void;
|
|
56
|
-
error: (error: Error) => void;
|
|
57
|
-
close: () => void;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* DataBridge provides guaranteed reliable serial communication.
|
|
62
|
-
*
|
|
63
|
-
* Features:
|
|
64
|
-
* - Automatic retransmission on packet loss
|
|
65
|
-
* - CRC32 integrity checking
|
|
66
|
-
* - COBS framing for robust delimitation
|
|
67
|
-
* - Fragmentation for large messages
|
|
68
|
-
*
|
|
69
|
-
* @example
|
|
70
|
-
* ```typescript
|
|
71
|
-
* const bridge = await DataBridge.open('/dev/ttyUSB0');
|
|
72
|
-
*
|
|
73
|
-
* bridge.on('data', (data) => {
|
|
74
|
-
* console.log('Received:', data.toString());
|
|
75
|
-
* });
|
|
76
|
-
*
|
|
77
|
-
* await bridge.send('Hello, World!');
|
|
78
|
-
* await bridge.close();
|
|
79
|
-
* ```
|
|
80
|
-
*/
|
|
81
|
-
export class DataBridge extends EventEmitter {
|
|
82
|
-
private native: NativeDataBridge;
|
|
83
|
-
private _isOpen = false;
|
|
84
|
-
|
|
85
|
-
private constructor() {
|
|
86
|
-
super();
|
|
87
|
-
this.native = new addon.DataBridge();
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Open a serial port with guaranteed reliable communication.
|
|
92
|
-
*
|
|
93
|
-
* @param port - Serial port path (e.g., '/dev/ttyUSB0' or 'COM3')
|
|
94
|
-
* @param options - Configuration options
|
|
95
|
-
* @returns Promise resolving to a DataBridge instance
|
|
96
|
-
*/
|
|
97
|
-
static async open (port: string, options: DataBridgeOptions = {}): Promise<DataBridge> {
|
|
98
|
-
const instance = new DataBridge();
|
|
99
|
-
const baud = options.baudRate ?? 115200;
|
|
100
|
-
|
|
101
|
-
const onData = (data: Buffer) => {
|
|
102
|
-
instance.emit('data', data);
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
try
|
|
106
|
-
{
|
|
107
|
-
await instance.native.open(port, baud, onData);
|
|
108
|
-
instance._isOpen = true;
|
|
109
|
-
return instance;
|
|
110
|
-
} catch (err)
|
|
111
|
-
{
|
|
112
|
-
throw new Error(`Failed to open ${port}: ${err instanceof Error ? err.message : err}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Send data with guaranteed delivery.
|
|
118
|
-
*
|
|
119
|
-
* The data will be fragmented if necessary, checksummed, and
|
|
120
|
-
* retransmitted until acknowledged by the receiver.
|
|
121
|
-
*
|
|
122
|
-
* @param data - Data to send (Buffer or string)
|
|
123
|
-
* @returns Promise resolving when data is acknowledged
|
|
124
|
-
*/
|
|
125
|
-
async send (data: Buffer | string): Promise<void> {
|
|
126
|
-
if (!this._isOpen)
|
|
127
|
-
{
|
|
128
|
-
throw new Error('Port not open');
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const buffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
132
|
-
await this.native.send(buffer);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Close the serial port.
|
|
137
|
-
*/
|
|
138
|
-
async close (): Promise<void> {
|
|
139
|
-
if (this._isOpen)
|
|
140
|
-
{
|
|
141
|
-
this.native.close();
|
|
142
|
-
this._isOpen = false;
|
|
143
|
-
this.emit('close');
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Check if the port is currently open.
|
|
149
|
-
*/
|
|
150
|
-
get isOpen (): boolean {
|
|
151
|
-
return this._isOpen && this.native.isOpen();
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Type-safe event emitter methods
|
|
155
|
-
on<K extends keyof DataBridgeEvents> (event: K, listener: DataBridgeEvents[K]): this {
|
|
156
|
-
return super.on(event, listener);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
once<K extends keyof DataBridgeEvents> (event: K, listener: DataBridgeEvents[K]): this {
|
|
160
|
-
return super.once(event, listener);
|
|
161
|
-
}
|
|
9
|
+
import { ReliableDataBridge, DataBridgeOptions } from './reliable';
|
|
10
|
+
import { SerialPort } from './native';
|
|
162
11
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
}
|
|
12
|
+
// Export the Reliable implementation as the default DataBridge
|
|
13
|
+
export { ReliableDataBridge as DataBridge };
|
|
14
|
+
export { DataBridgeOptions };
|
|
167
15
|
|
|
168
|
-
|
|
16
|
+
// Export Raw SerialPort for advanced users
|
|
17
|
+
export { SerialPort as RawSerialPort };
|
|
169
18
|
|
|
170
|
-
//
|
|
19
|
+
// Export Resilient wrapper (connection resilience)
|
|
20
|
+
// Note: ResilientDataBridge wraps DataBridge.
|
|
21
|
+
// If DataBridge is now Reliable, ResilientDataBridge will hold a ReliableDataBridge.
|
|
22
|
+
// This gives us Connection Resilience + Data Reliability.
|
|
171
23
|
export { ResilientDataBridge, ResilientOptions, ResilientEvents } from './resilient';
|
package/lib/native.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
// Load native addon
|
|
5
|
+
function loadAddon (): any {
|
|
6
|
+
const possiblePaths = [
|
|
7
|
+
`../prebuilds/${process.platform}-${process.arch}/data_bridge_node.node`,
|
|
8
|
+
'../build/Release/data_bridge_node.node',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
for (const p of possiblePaths)
|
|
12
|
+
{
|
|
13
|
+
try
|
|
14
|
+
{
|
|
15
|
+
return require(path.join(__dirname, p));
|
|
16
|
+
} catch
|
|
17
|
+
{
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
throw new Error('Failed to load native addon. Run `npm run build` first.');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const addon = loadAddon();
|
|
25
|
+
|
|
26
|
+
export interface SerialPort {
|
|
27
|
+
open (port: string, baud: number, callback: (data: Buffer) => void): boolean;
|
|
28
|
+
write (data: Buffer): number;
|
|
29
|
+
close (): boolean;
|
|
30
|
+
isOpen (): boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const SerialPort: {
|
|
34
|
+
new(): SerialPort;
|
|
35
|
+
} = addon.SerialPort;
|
|
36
|
+
|
|
37
|
+
export interface Frame {
|
|
38
|
+
valid: boolean;
|
|
39
|
+
header: {
|
|
40
|
+
type: number;
|
|
41
|
+
seq_id: number;
|
|
42
|
+
fragment_id: number;
|
|
43
|
+
total_frags: number;
|
|
44
|
+
payload_len: number;
|
|
45
|
+
crc32: number;
|
|
46
|
+
};
|
|
47
|
+
payload: Buffer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PacketHelper {
|
|
51
|
+
serialize (type: number, seq: number, payload: string | Buffer, frag_id?: number, total_frags?: number): Buffer;
|
|
52
|
+
deserialize (buffer: Buffer): { frame: Frame; remaining: Buffer; };
|
|
53
|
+
TYPE_DATA: number;
|
|
54
|
+
TYPE_ACK: number;
|
|
55
|
+
TYPE_NACK: number;
|
|
56
|
+
TYPE_SYN: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const Packet: PacketHelper = addon.Packet;
|
|
60
|
+
|
|
61
|
+
export interface Reassembler {
|
|
62
|
+
processFragment (frame: Frame): boolean;
|
|
63
|
+
isComplete (frame: Frame): boolean;
|
|
64
|
+
getData (): Buffer;
|
|
65
|
+
isDuplicate (frame: Frame): boolean;
|
|
66
|
+
getBufferedSize (): number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const Reassembler: {
|
|
70
|
+
new(): Reassembler;
|
|
71
|
+
} = addon.Reassembler;
|