@technoculture/data-bridge 0.1.1 → 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/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/lib/reliable.ts
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { SerialPort, Packet, Reassembler, Frame } from './native';
|
|
4
|
+
|
|
5
|
+
export interface DataBridgeOptions {
|
|
6
|
+
baudRate?: number;
|
|
7
|
+
maxRetries?: number;
|
|
8
|
+
ackTimeoutMs?: number;
|
|
9
|
+
fragmentSize?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* ReliableDataBridge
|
|
14
|
+
*
|
|
15
|
+
* Implements Stop-and-Wait ARQ with fragmentation/reassembly.
|
|
16
|
+
* Mirrors the Python bindings.wrapper.DataBridge logic.
|
|
17
|
+
*/
|
|
18
|
+
export class ReliableDataBridge extends EventEmitter {
|
|
19
|
+
private serial: SerialPort;
|
|
20
|
+
private reassembler: Reassembler;
|
|
21
|
+
private isOpen_ = false;
|
|
22
|
+
|
|
23
|
+
// Config
|
|
24
|
+
private options: Required<DataBridgeOptions>;
|
|
25
|
+
|
|
26
|
+
// State
|
|
27
|
+
private seqId = 0;
|
|
28
|
+
private rxBuffer = Buffer.alloc(0);
|
|
29
|
+
|
|
30
|
+
// ARQ State
|
|
31
|
+
// We only process one send at a time (Stop-and-Wait)
|
|
32
|
+
// Map of seqId -> { resolve, reject } (though we only have one really)
|
|
33
|
+
private pendingAcks = new Map<number, (fragId: number) => void>();
|
|
34
|
+
|
|
35
|
+
constructor(options: DataBridgeOptions = {}) {
|
|
36
|
+
super();
|
|
37
|
+
this.serial = new SerialPort();
|
|
38
|
+
this.reassembler = new Reassembler();
|
|
39
|
+
|
|
40
|
+
this.options = {
|
|
41
|
+
baudRate: options.baudRate ?? 115200,
|
|
42
|
+
maxRetries: options.maxRetries ?? 10,
|
|
43
|
+
ackTimeoutMs: options.ackTimeoutMs ?? 500,
|
|
44
|
+
fragmentSize: options.fragmentSize ?? 200
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
static async open (port: string, baudOrOptions?: number | DataBridgeOptions, cb?: (data: Buffer) => void): Promise<ReliableDataBridge> {
|
|
49
|
+
let opts: DataBridgeOptions = {};
|
|
50
|
+
if (typeof baudOrOptions === 'number')
|
|
51
|
+
{
|
|
52
|
+
opts.baudRate = baudOrOptions;
|
|
53
|
+
} else if (baudOrOptions)
|
|
54
|
+
{
|
|
55
|
+
opts = baudOrOptions;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const bridge = new ReliableDataBridge(opts);
|
|
59
|
+
if (cb) bridge.on('data', cb);
|
|
60
|
+
|
|
61
|
+
await bridge.open(port);
|
|
62
|
+
return bridge;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async open (port: string, baud?: number): Promise<boolean> {
|
|
66
|
+
if (this.isOpen_) return true;
|
|
67
|
+
|
|
68
|
+
// Backward compatibility: override baud from options if provided
|
|
69
|
+
const baudRate = baud ?? this.options.baudRate;
|
|
70
|
+
|
|
71
|
+
const success = this.serial.open(port, baudRate, (data) => {
|
|
72
|
+
this.onData(data);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (success)
|
|
76
|
+
{
|
|
77
|
+
this.isOpen_ = true;
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
throw new Error(`Failed to open ${port}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async close (): Promise<void> {
|
|
84
|
+
if (this.isOpen_)
|
|
85
|
+
{
|
|
86
|
+
this.serial.close();
|
|
87
|
+
this.isOpen_ = false;
|
|
88
|
+
this.emit('close');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get isOpen (): boolean {
|
|
93
|
+
return this.isOpen_;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async send (data: string | Buffer): Promise<void> {
|
|
97
|
+
if (!this.isOpen_) throw new Error("Port not open");
|
|
98
|
+
|
|
99
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
100
|
+
|
|
101
|
+
// Fragment
|
|
102
|
+
const fragments: Buffer[] = [];
|
|
103
|
+
for (let i = 0; i < buf.length; i += this.options.fragmentSize)
|
|
104
|
+
{
|
|
105
|
+
fragments.push(buf.subarray(i, i + this.options.fragmentSize));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const totalFrags = fragments.length;
|
|
109
|
+
const seq = this.seqId;
|
|
110
|
+
this.seqId = (this.seqId + 1) % 256;
|
|
111
|
+
|
|
112
|
+
for (let fragId = 0; fragId < totalFrags; fragId++)
|
|
113
|
+
{
|
|
114
|
+
const payload = fragments[fragId];
|
|
115
|
+
|
|
116
|
+
const packet = Packet.serialize(
|
|
117
|
+
Packet.TYPE_DATA,
|
|
118
|
+
seq,
|
|
119
|
+
payload,
|
|
120
|
+
fragId,
|
|
121
|
+
totalFrags
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await this.sendWithRetry(packet, seq, fragId);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async sendWithRetry (packet: Buffer, seq: number, fragId: number): Promise<void> {
|
|
129
|
+
let retries = 0;
|
|
130
|
+
|
|
131
|
+
while (retries <= this.options.maxRetries)
|
|
132
|
+
{
|
|
133
|
+
// Prepare waiter
|
|
134
|
+
const ackPromise = new Promise<void>((resolve, reject) => {
|
|
135
|
+
const handler = (ackedFragId: number) => {
|
|
136
|
+
if (ackedFragId === fragId) resolve();
|
|
137
|
+
};
|
|
138
|
+
this.pendingAcks.set(seq, handler);
|
|
139
|
+
|
|
140
|
+
// Timeout
|
|
141
|
+
setTimeout(() => {
|
|
142
|
+
if (this.pendingAcks.has(seq))
|
|
143
|
+
{
|
|
144
|
+
this.pendingAcks.delete(seq);
|
|
145
|
+
reject(new Error("Timeout"));
|
|
146
|
+
}
|
|
147
|
+
}, this.options.ackTimeoutMs);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Send
|
|
151
|
+
this.serial.write(packet);
|
|
152
|
+
|
|
153
|
+
try
|
|
154
|
+
{
|
|
155
|
+
await ackPromise;
|
|
156
|
+
return; // Success
|
|
157
|
+
} catch (e)
|
|
158
|
+
{
|
|
159
|
+
retries++;
|
|
160
|
+
// console.log(`[Reliable] Retry ${retries} for seq=${seq} frag=${fragId}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw new Error(`Send failed after ${this.options.maxRetries} retries`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private onData (chunk: Buffer) {
|
|
168
|
+
this.rxBuffer = Buffer.concat([this.rxBuffer, chunk]);
|
|
169
|
+
|
|
170
|
+
while (true)
|
|
171
|
+
{
|
|
172
|
+
const { frame, remaining } = Packet.deserialize(this.rxBuffer);
|
|
173
|
+
|
|
174
|
+
if (!frame.valid)
|
|
175
|
+
{
|
|
176
|
+
if (remaining.length === this.rxBuffer.length)
|
|
177
|
+
{
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
// Else we consumed some garbage, continue loop with remaining
|
|
181
|
+
this.rxBuffer = Buffer.from(remaining);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Valid frame
|
|
186
|
+
this.rxBuffer = Buffer.from(remaining);
|
|
187
|
+
|
|
188
|
+
if (frame.header.type === Packet.TYPE_ACK)
|
|
189
|
+
{
|
|
190
|
+
const seq = frame.header.seq_id;
|
|
191
|
+
const frag = frame.header.fragment_id;
|
|
192
|
+
|
|
193
|
+
if (this.pendingAcks.has(seq))
|
|
194
|
+
{
|
|
195
|
+
this.pendingAcks.get(seq)!(frag);
|
|
196
|
+
}
|
|
197
|
+
} else if (frame.header.type === Packet.TYPE_DATA)
|
|
198
|
+
{
|
|
199
|
+
let shouldAck = false;
|
|
200
|
+
|
|
201
|
+
if (this.reassembler.processFragment(frame))
|
|
202
|
+
{
|
|
203
|
+
shouldAck = true;
|
|
204
|
+
if (this.reassembler.isComplete(frame))
|
|
205
|
+
{
|
|
206
|
+
try
|
|
207
|
+
{
|
|
208
|
+
const data = this.reassembler.getData();
|
|
209
|
+
this.emit('data', data);
|
|
210
|
+
} catch (e)
|
|
211
|
+
{
|
|
212
|
+
console.error("Error in data emission:", e);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else if (this.reassembler.isDuplicate(frame))
|
|
216
|
+
{
|
|
217
|
+
shouldAck = true;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (shouldAck)
|
|
221
|
+
{
|
|
222
|
+
const ackParams = Packet.serialize(
|
|
223
|
+
Packet.TYPE_ACK,
|
|
224
|
+
frame.header.seq_id,
|
|
225
|
+
Buffer.alloc(0),
|
|
226
|
+
frame.header.fragment_id,
|
|
227
|
+
frame.header.total_frags
|
|
228
|
+
);
|
|
229
|
+
this.serial.write(ackParams);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
package/lib/resilient.ts
CHANGED
|
@@ -38,6 +38,7 @@ interface QueuedMessage {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export class ResilientDataBridge extends EventEmitter {
|
|
41
|
+
// Use DataBridge type from index (which is reliable now)
|
|
41
42
|
private bridge: DataBridge | null = null;
|
|
42
43
|
private port: string;
|
|
43
44
|
private options: Required<ResilientOptions>;
|
|
@@ -53,6 +54,9 @@ export class ResilientDataBridge extends EventEmitter {
|
|
|
53
54
|
this.port = port;
|
|
54
55
|
this.options = {
|
|
55
56
|
baudRate: options.baudRate ?? 115200,
|
|
57
|
+
maxRetries: options.maxRetries ?? 10,
|
|
58
|
+
ackTimeoutMs: options.ackTimeoutMs ?? 500,
|
|
59
|
+
fragmentSize: options.fragmentSize ?? 200,
|
|
56
60
|
reconnect: options.reconnect ?? true,
|
|
57
61
|
reconnectDelay: options.reconnectDelay ?? 1000,
|
|
58
62
|
maxReconnectDelay: options.maxReconnectDelay ?? 30000,
|
|
@@ -75,9 +79,11 @@ export class ResilientDataBridge extends EventEmitter {
|
|
|
75
79
|
private async connect (): Promise<void> {
|
|
76
80
|
try
|
|
77
81
|
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
// Pass full options to reliable bridge
|
|
83
|
+
this.bridge = new DataBridge(this.options);
|
|
84
|
+
|
|
85
|
+
// Open using just the port (options passed in constructor)
|
|
86
|
+
await this.bridge.open(this.port);
|
|
81
87
|
|
|
82
88
|
this.isConnected = true;
|
|
83
89
|
this.reconnectAttempt = 0;
|
|
@@ -88,8 +94,23 @@ export class ResilientDataBridge extends EventEmitter {
|
|
|
88
94
|
});
|
|
89
95
|
|
|
90
96
|
// Handle errors and detect disconnection
|
|
97
|
+
// Note: ReliableDataBridge might not emit error on single failure (it retries),
|
|
98
|
+
// but it will if retries exhausted or port error.
|
|
91
99
|
this.bridge.on('error', (err) => {
|
|
92
100
|
this.emit('error', err);
|
|
101
|
+
// If error is fatal (port closed/lost), handle disconnect
|
|
102
|
+
// ReliableDataBridge throws/emits error if max retries reached.
|
|
103
|
+
// We need to assume that might mean broken link or just timeouts?
|
|
104
|
+
// Actually SerialPort wrapper might throw/emit if underlying handle dies.
|
|
105
|
+
// For simplicity, treat any error as triggers for potential reconnection logic or check isOpen
|
|
106
|
+
if (!this.bridge?.isOpen)
|
|
107
|
+
{
|
|
108
|
+
this.handleDisconnect();
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Check if closed
|
|
113
|
+
this.bridge.on('close', () => {
|
|
93
114
|
this.handleDisconnect();
|
|
94
115
|
});
|
|
95
116
|
|
|
@@ -117,7 +138,7 @@ export class ResilientDataBridge extends EventEmitter {
|
|
|
117
138
|
if (!this.isConnected) return;
|
|
118
139
|
|
|
119
140
|
this.isConnected = false;
|
|
120
|
-
this.bridge = null;
|
|
141
|
+
this.bridge = null; // Clean up old instance
|
|
121
142
|
this.emit('disconnect');
|
|
122
143
|
|
|
123
144
|
if (this.shouldReconnect && this.options.reconnect)
|
|
@@ -166,7 +187,7 @@ export class ResilientDataBridge extends EventEmitter {
|
|
|
166
187
|
async send (data: Buffer | string): Promise<void> {
|
|
167
188
|
const buffer = typeof data === 'string' ? Buffer.from(data) : data;
|
|
168
189
|
|
|
169
|
-
if (this.isConnected && this.bridge)
|
|
190
|
+
if (this.isConnected && this.bridge && this.bridge.isOpen)
|
|
170
191
|
{
|
|
171
192
|
// Connected - send immediately
|
|
172
193
|
try
|
|
@@ -174,7 +195,9 @@ export class ResilientDataBridge extends EventEmitter {
|
|
|
174
195
|
await this.bridge.send(buffer);
|
|
175
196
|
} catch (err)
|
|
176
197
|
{
|
|
177
|
-
// Send failed
|
|
198
|
+
// Send failed (retries exhausted or port error)
|
|
199
|
+
// Queue it and trigger reconnect check
|
|
200
|
+
this.handleDisconnect(); // Force reconnect cycle logic
|
|
178
201
|
return this.queueMessage(buffer);
|
|
179
202
|
}
|
|
180
203
|
} else
|
|
@@ -274,3 +297,4 @@ export class ResilientDataBridge extends EventEmitter {
|
|
|
274
297
|
}
|
|
275
298
|
|
|
276
299
|
export default ResilientDataBridge;
|
|
300
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technoculture/data-bridge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Guaranteed reliable serial communication for Node.js and Electron",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -18,13 +18,15 @@
|
|
|
18
18
|
"build:native": "cmake-js compile",
|
|
19
19
|
"build:ts": "tsup lib/index.ts --format cjs,esm --dts",
|
|
20
20
|
"prebuild": "cmake-js compile -O prebuilds/$(node -p \"process.platform + '-' + process.arch\")",
|
|
21
|
+
"prepack": "node scripts/copy_deps.js && npm run build:ts",
|
|
21
22
|
"prepublishOnly": "npm run build:ts",
|
|
22
23
|
"test": "vitest run",
|
|
23
|
-
"clean": "cmake-js clean && rm -rf dist prebuilds"
|
|
24
|
+
"clean": "cmake-js clean && rm -rf dist prebuilds deps"
|
|
24
25
|
},
|
|
25
26
|
"files": [
|
|
26
27
|
"dist",
|
|
27
|
-
"prebuilds",
|
|
28
|
+
"prebuilds/**/*.node",
|
|
29
|
+
"deps",
|
|
28
30
|
"src",
|
|
29
31
|
"lib",
|
|
30
32
|
"CMakeLists.txt"
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
},
|
|
61
63
|
"repository": {
|
|
62
64
|
"type": "git",
|
|
63
|
-
"url": "https://github.com/
|
|
65
|
+
"url": "https://github.com/technoculture/data-bridge"
|
|
64
66
|
},
|
|
65
67
|
"keywords": [
|
|
66
68
|
"serial",
|
|
Binary file
|