@technoculture/safeserial 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.
Files changed (33) hide show
  1. package/CMakeLists.txt +66 -0
  2. package/README.md +77 -0
  3. package/deps/include/safeserial/config.hpp +69 -0
  4. package/deps/include/safeserial/protocol/crc32.hpp +19 -0
  5. package/deps/include/safeserial/protocol/packet.hpp +175 -0
  6. package/deps/include/safeserial/protocol/reassembler.hpp +80 -0
  7. package/deps/include/safeserial/resilient_bridge.hpp +106 -0
  8. package/deps/include/safeserial/safeserial.hpp +87 -0
  9. package/deps/include/safeserial/transport/factory.hpp +0 -0
  10. package/deps/include/safeserial/transport/iserial_port.hpp +15 -0
  11. package/deps/include/safeserial/transport/serial_port.hpp +28 -0
  12. package/deps/src/CMakeLists.txt +21 -0
  13. package/deps/src/protocol/packet.cpp +1 -0
  14. package/deps/src/resilient_bridge.cpp +338 -0
  15. package/deps/src/safeserial.cpp +246 -0
  16. package/deps/src/transport/platform/linux/linux_serial.cpp +53 -0
  17. package/deps/src/transport/platform/windows/windows_serial.cpp +51 -0
  18. package/dist/index.d.mts +93 -0
  19. package/dist/index.d.ts +93 -0
  20. package/dist/index.js +202 -0
  21. package/dist/index.mjs +170 -0
  22. package/lib/index.ts +23 -0
  23. package/lib/native.ts +108 -0
  24. package/lib/reliable.ts +94 -0
  25. package/lib/resilient.ts +122 -0
  26. package/package.json +78 -0
  27. package/prebuilds/darwin-arm64/Release/data_bridge_node.node +0 -0
  28. package/prebuilds/darwin-arm64/Release/safeserial_node.node +0 -0
  29. package/scripts/copy_deps.js +44 -0
  30. package/scripts/receiver.js +37 -0
  31. package/scripts/sender.js +48 -0
  32. package/src/addon.cpp +807 -0
  33. package/src/serial_wrapper.cpp +8 -0
package/src/addon.cpp ADDED
@@ -0,0 +1,807 @@
1
+ /**
2
+ * SafeSerial Node.js Native Addon
3
+ *
4
+ * Exposes the C++ SafeSerial library to JavaScript via Node-API.
5
+ * Aligned with Python _core:
6
+ * - SerialPort: Raw async serial I/O
7
+ * - Packet: Serialization/Deserialization helpers
8
+ * - Reassembler: Fragmentation handling
9
+ */
10
+
11
+ #include <napi.h>
12
+ #include <safeserial/safeserial.hpp>
13
+ #include <safeserial/resilient_bridge.hpp>
14
+ #include <safeserial/transport/serial_port.hpp>
15
+ #include <safeserial/protocol/packet.hpp>
16
+ #include <safeserial/protocol/reassembler.hpp>
17
+ #include <memory>
18
+ #include <thread>
19
+ #include <atomic>
20
+ #include <vector>
21
+
22
+ // --- SerialPort Wrapper ---
23
+ class SerialPortWrapper : public Napi::ObjectWrap<SerialPortWrapper> {
24
+ public:
25
+ static Napi::Object Init(Napi::Env env, Napi::Object exports);
26
+ SerialPortWrapper(const Napi::CallbackInfo& info);
27
+ ~SerialPortWrapper();
28
+
29
+ private:
30
+ Napi::Value Open(const Napi::CallbackInfo& info);
31
+ Napi::Value Close(const Napi::CallbackInfo& info);
32
+ Napi::Value Write(const Napi::CallbackInfo& info);
33
+ Napi::Value IsOpen(const Napi::CallbackInfo& info);
34
+
35
+ std::unique_ptr<SerialPort> serial_;
36
+ std::atomic<bool> is_open_{false};
37
+ std::atomic<bool> should_stop_{false};
38
+ std::thread receive_thread_;
39
+ Napi::ThreadSafeFunction tsfn_;
40
+
41
+ void ReceiveLoop();
42
+ void StopReceiveLoop();
43
+ };
44
+
45
+ static uint32_t GetUInt32(const Napi::Object& obj, const char* key, uint32_t fallback) {
46
+ if (obj.Has(key) && obj.Get(key).IsNumber()) {
47
+ return obj.Get(key).As<Napi::Number>().Uint32Value();
48
+ }
49
+ return fallback;
50
+ }
51
+
52
+ static bool GetBool(const Napi::Object& obj, const char* key, bool fallback) {
53
+ if (obj.Has(key) && obj.Get(key).IsBoolean()) {
54
+ return obj.Get(key).As<Napi::Boolean>().Value();
55
+ }
56
+ return fallback;
57
+ }
58
+
59
+ Napi::Object SerialPortWrapper::Init(Napi::Env env, Napi::Object exports) {
60
+ Napi::Function func = DefineClass(env, "SerialPort", {
61
+ InstanceMethod("open", &SerialPortWrapper::Open),
62
+ InstanceMethod("close", &SerialPortWrapper::Close),
63
+ InstanceMethod("write", &SerialPortWrapper::Write),
64
+ InstanceMethod("isOpen", &SerialPortWrapper::IsOpen),
65
+ });
66
+ exports.Set("SerialPort", func);
67
+ return exports;
68
+ }
69
+
70
+ SerialPortWrapper::SerialPortWrapper(const Napi::CallbackInfo& info)
71
+ : Napi::ObjectWrap<SerialPortWrapper>(info) {
72
+ serial_ = std::make_unique<SerialPort>();
73
+ }
74
+
75
+ SerialPortWrapper::~SerialPortWrapper() {
76
+ StopReceiveLoop();
77
+ }
78
+
79
+ Napi::Value SerialPortWrapper::Open(const Napi::CallbackInfo& info) {
80
+ Napi::Env env = info.Env();
81
+ if (info.Length() < 1 || !info[0].IsString()) {
82
+ Napi::TypeError::New(env, "Port path required").ThrowAsJavaScriptException();
83
+ return env.Undefined();
84
+ }
85
+
86
+ std::string port = info[0].As<Napi::String>().Utf8Value();
87
+ int baud = 115200;
88
+ if (info.Length() >= 2 && info[1].IsNumber()) baud = info[1].As<Napi::Number>().Int32Value();
89
+
90
+ // Callback is mandatory for raw serial port to receive data
91
+ if (info.Length() < 3 || !info[2].IsFunction()) {
92
+ Napi::TypeError::New(env, "Callback required").ThrowAsJavaScriptException();
93
+ return env.Undefined();
94
+ }
95
+
96
+ if (serial_->open(port, baud)) {
97
+ is_open_ = true;
98
+
99
+ tsfn_ = Napi::ThreadSafeFunction::New(
100
+ env,
101
+ info[2].As<Napi::Function>(),
102
+ "SerialPort Receive Callback",
103
+ 0, 1
104
+ );
105
+
106
+ should_stop_ = false;
107
+ receive_thread_ = std::thread(&SerialPortWrapper::ReceiveLoop, this);
108
+
109
+ return Napi::Boolean::New(env, true);
110
+ }
111
+
112
+ return Napi::Boolean::New(env, false);
113
+ }
114
+
115
+ Napi::Value SerialPortWrapper::Close(const Napi::CallbackInfo& info) {
116
+ StopReceiveLoop();
117
+ serial_->close();
118
+ is_open_ = false;
119
+ return Napi::Boolean::New(info.Env(), true);
120
+ }
121
+
122
+ Napi::Value SerialPortWrapper::Write(const Napi::CallbackInfo& info) {
123
+ Napi::Env env = info.Env();
124
+ if (info.Length() < 1 || !info[0].IsBuffer()) {
125
+ Napi::TypeError::New(env, "Buffer required").ThrowAsJavaScriptException();
126
+ return env.Undefined();
127
+ }
128
+
129
+ Napi::Buffer<uint8_t> buf = info[0].As<Napi::Buffer<uint8_t>>();
130
+ std::vector<uint8_t> data(buf.Data(), buf.Data() + buf.Length());
131
+
132
+ // Write is synchronous in C++ implementation usually, or we can make it async if needed.
133
+ // For now, keeping it synchronous/blocking for simplicity as per Python binding.
134
+ // (Node.js event loop might block briefly, but serial write is fast or buffered)
135
+ int written = serial_->write(data);
136
+ return Napi::Number::New(env, written);
137
+ }
138
+
139
+ Napi::Value SerialPortWrapper::IsOpen(const Napi::CallbackInfo& info) {
140
+ return Napi::Boolean::New(info.Env(), is_open_.load());
141
+ }
142
+
143
+ void SerialPortWrapper::ReceiveLoop() {
144
+ uint8_t buffer[4096];
145
+ while (!should_stop_) {
146
+ // Read raw bytes
147
+ int n = serial_->read(buffer, sizeof(buffer));
148
+ if (n > 0) {
149
+ // Copy data for the callback
150
+ std::vector<uint8_t> data(buffer, buffer + n);
151
+
152
+ auto status = tsfn_.BlockingCall([data](Napi::Env env, Napi::Function callback) {
153
+ callback.Call({
154
+ Napi::Buffer<uint8_t>::Copy(env, data.data(), data.size())
155
+ });
156
+ });
157
+
158
+ if (status != napi_ok) break;
159
+ }
160
+ // Small sleep to prevent tight loop if read is non-blocking and returns 0 often
161
+ // But read should be blocking with timeout.
162
+ // Assuming implementation of serial_->read handles timeout or blocking.
163
+ // The previous implementation had a sleep, so we surely need one if non-blocking.
164
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
165
+ }
166
+ }
167
+
168
+ void SerialPortWrapper::StopReceiveLoop() {
169
+ should_stop_ = true;
170
+ if (receive_thread_.joinable()) receive_thread_.join();
171
+ if (tsfn_) tsfn_.Release();
172
+ }
173
+
174
+ // --- Packet Wrapper ---
175
+ class PacketWrapper : public Napi::ObjectWrap<PacketWrapper> {
176
+ public:
177
+ static Napi::Object Init(Napi::Env env, Napi::Object exports);
178
+ PacketWrapper(const Napi::CallbackInfo& info);
179
+
180
+ private:
181
+ static Napi::Value Serialize(const Napi::CallbackInfo& info);
182
+ static Napi::Value Deserialize(const Napi::CallbackInfo& info);
183
+ };
184
+
185
+ // --- DataBridge Wrapper ---
186
+ class DataBridgeWrapper : public Napi::ObjectWrap<DataBridgeWrapper> {
187
+ public:
188
+ static Napi::Object Init(Napi::Env env, Napi::Object exports);
189
+ DataBridgeWrapper(const Napi::CallbackInfo& info);
190
+ ~DataBridgeWrapper();
191
+
192
+ private:
193
+ Napi::Value Open(const Napi::CallbackInfo& info);
194
+ Napi::Value Close(const Napi::CallbackInfo& info);
195
+ Napi::Value Send(const Napi::CallbackInfo& info);
196
+ Napi::Value IsOpen(const Napi::CallbackInfo& info);
197
+ Napi::Value OnData(const Napi::CallbackInfo& info);
198
+
199
+ std::unique_ptr<DataBridge> bridge_;
200
+ Napi::ThreadSafeFunction tsfn_;
201
+ };
202
+
203
+ // --- ResilientDataBridge Wrapper ---
204
+ class ResilientDataBridgeWrapper : public Napi::ObjectWrap<ResilientDataBridgeWrapper> {
205
+ public:
206
+ static Napi::Object Init(Napi::Env env, Napi::Object exports);
207
+ ResilientDataBridgeWrapper(const Napi::CallbackInfo& info);
208
+ ~ResilientDataBridgeWrapper();
209
+
210
+ private:
211
+ Napi::Value Open(const Napi::CallbackInfo& info);
212
+ Napi::Value Close(const Napi::CallbackInfo& info);
213
+ Napi::Value Send(const Napi::CallbackInfo& info);
214
+ Napi::Value IsConnected(const Napi::CallbackInfo& info);
215
+ Napi::Value QueueLength(const Napi::CallbackInfo& info);
216
+
217
+ Napi::Value OnData(const Napi::CallbackInfo& info);
218
+ Napi::Value OnError(const Napi::CallbackInfo& info);
219
+ Napi::Value OnDisconnect(const Napi::CallbackInfo& info);
220
+ Napi::Value OnReconnecting(const Napi::CallbackInfo& info);
221
+ Napi::Value OnReconnected(const Napi::CallbackInfo& info);
222
+ Napi::Value OnClose(const Napi::CallbackInfo& info);
223
+
224
+ std::unique_ptr<ResilientDataBridge> bridge_;
225
+ Napi::ThreadSafeFunction data_tsfn_;
226
+ Napi::ThreadSafeFunction error_tsfn_;
227
+ Napi::ThreadSafeFunction disconnect_tsfn_;
228
+ Napi::ThreadSafeFunction reconnecting_tsfn_;
229
+ Napi::ThreadSafeFunction reconnected_tsfn_;
230
+ Napi::ThreadSafeFunction close_tsfn_;
231
+ };
232
+
233
+ Napi::Object ResilientDataBridgeWrapper::Init(Napi::Env env, Napi::Object exports) {
234
+ Napi::Function func = DefineClass(env, "ResilientDataBridge", {
235
+ InstanceMethod("open", &ResilientDataBridgeWrapper::Open),
236
+ InstanceMethod("close", &ResilientDataBridgeWrapper::Close),
237
+ InstanceMethod("send", &ResilientDataBridgeWrapper::Send),
238
+ InstanceMethod("isConnected", &ResilientDataBridgeWrapper::IsConnected),
239
+ InstanceMethod("queueLength", &ResilientDataBridgeWrapper::QueueLength),
240
+ InstanceMethod("onData", &ResilientDataBridgeWrapper::OnData),
241
+ InstanceMethod("onError", &ResilientDataBridgeWrapper::OnError),
242
+ InstanceMethod("onDisconnect", &ResilientDataBridgeWrapper::OnDisconnect),
243
+ InstanceMethod("onReconnecting", &ResilientDataBridgeWrapper::OnReconnecting),
244
+ InstanceMethod("onReconnected", &ResilientDataBridgeWrapper::OnReconnected),
245
+ InstanceMethod("onClose", &ResilientDataBridgeWrapper::OnClose),
246
+ });
247
+ exports.Set("ResilientDataBridge", func);
248
+ return exports;
249
+ }
250
+
251
+ ResilientDataBridgeWrapper::ResilientDataBridgeWrapper(const Napi::CallbackInfo& info)
252
+ : Napi::ObjectWrap<ResilientDataBridgeWrapper>(info) {
253
+ ResilientDataBridge::Options options = ResilientDataBridge::Options::Defaults();
254
+ if (info.Length() >= 1 && info[0].IsObject()) {
255
+ Napi::Object opts = info[0].As<Napi::Object>();
256
+ options.bridge.baud_rate = static_cast<int>(GetUInt32(opts, "baudRate", options.bridge.baud_rate));
257
+ options.bridge.max_retries = static_cast<uint8_t>(GetUInt32(opts, "maxRetries", options.bridge.max_retries));
258
+ options.bridge.ack_timeout_ms = static_cast<uint16_t>(GetUInt32(opts, "ackTimeoutMs", options.bridge.ack_timeout_ms));
259
+ options.bridge.fragment_size = static_cast<uint16_t>(GetUInt32(opts, "fragmentSize", options.bridge.fragment_size));
260
+ options.reconnect = GetBool(opts, "reconnect", options.reconnect);
261
+ options.reconnect_delay_ms = GetUInt32(opts, "reconnectDelay", options.reconnect_delay_ms);
262
+ options.max_reconnect_delay_ms = GetUInt32(opts, "maxReconnectDelay", options.max_reconnect_delay_ms);
263
+ options.max_queue_size = GetUInt32(opts, "maxQueueSize", static_cast<uint32_t>(options.max_queue_size));
264
+ }
265
+ bridge_ = std::make_unique<ResilientDataBridge>(options);
266
+ }
267
+
268
+ ResilientDataBridgeWrapper::~ResilientDataBridgeWrapper() {
269
+ if (bridge_) {
270
+ bridge_->set_on_data(nullptr);
271
+ bridge_->set_on_error(nullptr);
272
+ bridge_->set_on_disconnect(nullptr);
273
+ bridge_->set_on_reconnecting(nullptr);
274
+ bridge_->set_on_reconnected(nullptr);
275
+ bridge_->set_on_close(nullptr);
276
+ bridge_->close();
277
+ }
278
+ if (data_tsfn_) data_tsfn_.Release();
279
+ if (error_tsfn_) error_tsfn_.Release();
280
+ if (disconnect_tsfn_) disconnect_tsfn_.Release();
281
+ if (reconnecting_tsfn_) reconnecting_tsfn_.Release();
282
+ if (reconnected_tsfn_) reconnected_tsfn_.Release();
283
+ if (close_tsfn_) close_tsfn_.Release();
284
+ }
285
+
286
+ Napi::Value ResilientDataBridgeWrapper::Open(const Napi::CallbackInfo& info) {
287
+ Napi::Env env = info.Env();
288
+ if (info.Length() < 1 || !info[0].IsString()) {
289
+ Napi::TypeError::New(env, "Port path required").ThrowAsJavaScriptException();
290
+ return env.Undefined();
291
+ }
292
+ std::string port = info[0].As<Napi::String>().Utf8Value();
293
+ bool ok = bridge_->open(port);
294
+ return Napi::Boolean::New(env, ok);
295
+ }
296
+
297
+ Napi::Value ResilientDataBridgeWrapper::Close(const Napi::CallbackInfo& info) {
298
+ bridge_->close();
299
+ return Napi::Boolean::New(info.Env(), true);
300
+ }
301
+
302
+ Napi::Value ResilientDataBridgeWrapper::Send(const Napi::CallbackInfo& info) {
303
+ Napi::Env env = info.Env();
304
+ if (info.Length() < 1) {
305
+ Napi::TypeError::New(env, "Data required").ThrowAsJavaScriptException();
306
+ return env.Undefined();
307
+ }
308
+
309
+ std::vector<uint8_t> data;
310
+ if (info[0].IsBuffer()) {
311
+ Napi::Buffer<uint8_t> buf = info[0].As<Napi::Buffer<uint8_t>>();
312
+ data.assign(buf.Data(), buf.Data() + buf.Length());
313
+ } else {
314
+ std::string payload = info[0].ToString().Utf8Value();
315
+ data.assign(payload.begin(), payload.end());
316
+ }
317
+
318
+ try {
319
+ int written = bridge_->send(data);
320
+ return Napi::Number::New(env, written);
321
+ } catch (const std::exception& ex) {
322
+ Napi::Error::New(env, ex.what()).ThrowAsJavaScriptException();
323
+ return env.Undefined();
324
+ }
325
+ }
326
+
327
+ Napi::Value ResilientDataBridgeWrapper::IsConnected(const Napi::CallbackInfo& info) {
328
+ return Napi::Boolean::New(info.Env(), bridge_->is_connected());
329
+ }
330
+
331
+ Napi::Value ResilientDataBridgeWrapper::QueueLength(const Napi::CallbackInfo& info) {
332
+ return Napi::Number::New(info.Env(), bridge_->queue_length());
333
+ }
334
+
335
+ Napi::Value ResilientDataBridgeWrapper::OnData(const Napi::CallbackInfo& info) {
336
+ Napi::Env env = info.Env();
337
+ if (info.Length() < 1 || !info[0].IsFunction()) {
338
+ Napi::TypeError::New(env, "Callback required").ThrowAsJavaScriptException();
339
+ return env.Undefined();
340
+ }
341
+
342
+ if (data_tsfn_) {
343
+ data_tsfn_.Release();
344
+ }
345
+
346
+ data_tsfn_ = Napi::ThreadSafeFunction::New(
347
+ env,
348
+ info[0].As<Napi::Function>(),
349
+ "ResilientDataBridge Data Callback",
350
+ 0, 1);
351
+
352
+ bridge_->set_on_data([this](const std::vector<uint8_t>& data) {
353
+ auto payload = new std::vector<uint8_t>(data);
354
+ auto status = data_tsfn_.BlockingCall(payload, [](Napi::Env env, Napi::Function callback, std::vector<uint8_t>* payload) {
355
+ callback.Call({
356
+ Napi::Buffer<uint8_t>::Copy(env, payload->data(), payload->size())
357
+ });
358
+ delete payload;
359
+ });
360
+ if (status != napi_ok) {
361
+ delete payload;
362
+ }
363
+ });
364
+
365
+ return env.Undefined();
366
+ }
367
+
368
+ Napi::Value ResilientDataBridgeWrapper::OnError(const Napi::CallbackInfo& info) {
369
+ Napi::Env env = info.Env();
370
+ if (info.Length() < 1 || !info[0].IsFunction()) {
371
+ Napi::TypeError::New(env, "Callback required").ThrowAsJavaScriptException();
372
+ return env.Undefined();
373
+ }
374
+
375
+ if (error_tsfn_) {
376
+ error_tsfn_.Release();
377
+ }
378
+
379
+ error_tsfn_ = Napi::ThreadSafeFunction::New(
380
+ env,
381
+ info[0].As<Napi::Function>(),
382
+ "ResilientDataBridge Error Callback",
383
+ 0, 1);
384
+
385
+ bridge_->set_on_error([this](const std::string& message) {
386
+ auto payload = new std::string(message);
387
+ auto status = error_tsfn_.BlockingCall(payload, [](Napi::Env env, Napi::Function callback, std::string* payload) {
388
+ callback.Call({ Napi::String::New(env, *payload) });
389
+ delete payload;
390
+ });
391
+ if (status != napi_ok) {
392
+ delete payload;
393
+ }
394
+ });
395
+
396
+ return env.Undefined();
397
+ }
398
+
399
+ Napi::Value ResilientDataBridgeWrapper::OnDisconnect(const Napi::CallbackInfo& info) {
400
+ Napi::Env env = info.Env();
401
+ if (info.Length() < 1 || !info[0].IsFunction()) {
402
+ Napi::TypeError::New(env, "Callback required").ThrowAsJavaScriptException();
403
+ return env.Undefined();
404
+ }
405
+
406
+ if (disconnect_tsfn_) {
407
+ disconnect_tsfn_.Release();
408
+ }
409
+
410
+ disconnect_tsfn_ = Napi::ThreadSafeFunction::New(
411
+ env,
412
+ info[0].As<Napi::Function>(),
413
+ "ResilientDataBridge Disconnect Callback",
414
+ 0, 1);
415
+
416
+ bridge_->set_on_disconnect([this]() {
417
+ auto status = disconnect_tsfn_.BlockingCall([](Napi::Env env, Napi::Function callback) {
418
+ callback.Call({});
419
+ });
420
+ if (status != napi_ok) {
421
+ return;
422
+ }
423
+ });
424
+
425
+ return env.Undefined();
426
+ }
427
+
428
+ Napi::Value ResilientDataBridgeWrapper::OnReconnecting(const Napi::CallbackInfo& info) {
429
+ Napi::Env env = info.Env();
430
+ if (info.Length() < 1 || !info[0].IsFunction()) {
431
+ Napi::TypeError::New(env, "Callback required").ThrowAsJavaScriptException();
432
+ return env.Undefined();
433
+ }
434
+
435
+ if (reconnecting_tsfn_) {
436
+ reconnecting_tsfn_.Release();
437
+ }
438
+
439
+ reconnecting_tsfn_ = Napi::ThreadSafeFunction::New(
440
+ env,
441
+ info[0].As<Napi::Function>(),
442
+ "ResilientDataBridge Reconnecting Callback",
443
+ 0, 1);
444
+
445
+ bridge_->set_on_reconnecting([this](uint32_t attempt, uint32_t delay) {
446
+ auto payload = new std::pair<uint32_t, uint32_t>(attempt, delay);
447
+ auto status = reconnecting_tsfn_.BlockingCall(payload, [](Napi::Env env, Napi::Function callback, std::pair<uint32_t, uint32_t>* payload) {
448
+ callback.Call({
449
+ Napi::Number::New(env, payload->first),
450
+ Napi::Number::New(env, payload->second)
451
+ });
452
+ delete payload;
453
+ });
454
+ if (status != napi_ok) {
455
+ delete payload;
456
+ }
457
+ });
458
+
459
+ return env.Undefined();
460
+ }
461
+
462
+ Napi::Value ResilientDataBridgeWrapper::OnReconnected(const Napi::CallbackInfo& info) {
463
+ Napi::Env env = info.Env();
464
+ if (info.Length() < 1 || !info[0].IsFunction()) {
465
+ Napi::TypeError::New(env, "Callback required").ThrowAsJavaScriptException();
466
+ return env.Undefined();
467
+ }
468
+
469
+ if (reconnected_tsfn_) {
470
+ reconnected_tsfn_.Release();
471
+ }
472
+
473
+ reconnected_tsfn_ = Napi::ThreadSafeFunction::New(
474
+ env,
475
+ info[0].As<Napi::Function>(),
476
+ "ResilientDataBridge Reconnected Callback",
477
+ 0, 1);
478
+
479
+ bridge_->set_on_reconnected([this]() {
480
+ auto status = reconnected_tsfn_.BlockingCall([](Napi::Env env, Napi::Function callback) {
481
+ callback.Call({});
482
+ });
483
+ if (status != napi_ok) {
484
+ return;
485
+ }
486
+ });
487
+
488
+ return env.Undefined();
489
+ }
490
+
491
+ Napi::Value ResilientDataBridgeWrapper::OnClose(const Napi::CallbackInfo& info) {
492
+ Napi::Env env = info.Env();
493
+ if (info.Length() < 1 || !info[0].IsFunction()) {
494
+ Napi::TypeError::New(env, "Callback required").ThrowAsJavaScriptException();
495
+ return env.Undefined();
496
+ }
497
+
498
+ if (close_tsfn_) {
499
+ close_tsfn_.Release();
500
+ }
501
+
502
+ close_tsfn_ = Napi::ThreadSafeFunction::New(
503
+ env,
504
+ info[0].As<Napi::Function>(),
505
+ "ResilientDataBridge Close Callback",
506
+ 0, 1);
507
+
508
+ bridge_->set_on_close([this]() {
509
+ auto status = close_tsfn_.BlockingCall([](Napi::Env env, Napi::Function callback) {
510
+ callback.Call({});
511
+ });
512
+ if (status != napi_ok) {
513
+ return;
514
+ }
515
+ });
516
+
517
+ return env.Undefined();
518
+ }
519
+
520
+ Napi::Object DataBridgeWrapper::Init(Napi::Env env, Napi::Object exports) {
521
+ Napi::Function func = DefineClass(env, "DataBridge", {
522
+ InstanceMethod("open", &DataBridgeWrapper::Open),
523
+ InstanceMethod("close", &DataBridgeWrapper::Close),
524
+ InstanceMethod("send", &DataBridgeWrapper::Send),
525
+ InstanceMethod("isOpen", &DataBridgeWrapper::IsOpen),
526
+ InstanceMethod("onData", &DataBridgeWrapper::OnData),
527
+ });
528
+ exports.Set("DataBridge", func);
529
+ return exports;
530
+ }
531
+
532
+ DataBridgeWrapper::DataBridgeWrapper(const Napi::CallbackInfo& info)
533
+ : Napi::ObjectWrap<DataBridgeWrapper>(info) {
534
+ bridge_ = std::make_unique<DataBridge>();
535
+ }
536
+
537
+ DataBridgeWrapper::~DataBridgeWrapper() {
538
+ if (bridge_) {
539
+ bridge_->set_on_data(nullptr);
540
+ bridge_->close();
541
+ }
542
+ if (tsfn_) {
543
+ tsfn_.Release();
544
+ }
545
+ }
546
+
547
+ Napi::Value DataBridgeWrapper::Open(const Napi::CallbackInfo& info) {
548
+ Napi::Env env = info.Env();
549
+ if (info.Length() < 1 || !info[0].IsString()) {
550
+ Napi::TypeError::New(env, "Port path required").ThrowAsJavaScriptException();
551
+ return env.Undefined();
552
+ }
553
+
554
+ std::string port = info[0].As<Napi::String>().Utf8Value();
555
+ int baud = -1;
556
+ if (info.Length() >= 2 && info[1].IsNumber()) {
557
+ baud = info[1].As<Napi::Number>().Int32Value();
558
+ }
559
+
560
+ bool ok = bridge_->open(port, baud);
561
+ return Napi::Boolean::New(env, ok);
562
+ }
563
+
564
+ Napi::Value DataBridgeWrapper::Close(const Napi::CallbackInfo& info) {
565
+ bridge_->close();
566
+ return Napi::Boolean::New(info.Env(), true);
567
+ }
568
+
569
+ Napi::Value DataBridgeWrapper::Send(const Napi::CallbackInfo& info) {
570
+ Napi::Env env = info.Env();
571
+ if (info.Length() < 1) {
572
+ Napi::TypeError::New(env, "Data required").ThrowAsJavaScriptException();
573
+ return env.Undefined();
574
+ }
575
+
576
+ std::vector<uint8_t> data;
577
+ if (info[0].IsBuffer()) {
578
+ Napi::Buffer<uint8_t> buf = info[0].As<Napi::Buffer<uint8_t>>();
579
+ data.assign(buf.Data(), buf.Data() + buf.Length());
580
+ } else {
581
+ std::string payload = info[0].ToString().Utf8Value();
582
+ data.assign(payload.begin(), payload.end());
583
+ }
584
+
585
+ uint16_t ack_timeout_ms = 0;
586
+ uint8_t max_retries = 0;
587
+ uint16_t fragment_size = 0;
588
+ if (info.Length() >= 2 && info[1].IsNumber()) {
589
+ ack_timeout_ms = info[1].As<Napi::Number>().Uint32Value();
590
+ }
591
+ if (info.Length() >= 3 && info[2].IsNumber()) {
592
+ max_retries = static_cast<uint8_t>(info[2].As<Napi::Number>().Uint32Value());
593
+ }
594
+ if (info.Length() >= 4 && info[3].IsNumber()) {
595
+ fragment_size = info[3].As<Napi::Number>().Uint32Value();
596
+ }
597
+
598
+ try {
599
+ int written = bridge_->send(data, ack_timeout_ms, max_retries, fragment_size);
600
+ return Napi::Number::New(env, written);
601
+ } catch (const std::exception& ex) {
602
+ Napi::Error::New(env, ex.what()).ThrowAsJavaScriptException();
603
+ return env.Undefined();
604
+ }
605
+ }
606
+
607
+ Napi::Value DataBridgeWrapper::IsOpen(const Napi::CallbackInfo& info) {
608
+ return Napi::Boolean::New(info.Env(), bridge_->is_open());
609
+ }
610
+
611
+ Napi::Value DataBridgeWrapper::OnData(const Napi::CallbackInfo& info) {
612
+ Napi::Env env = info.Env();
613
+ if (info.Length() < 1 || !info[0].IsFunction()) {
614
+ Napi::TypeError::New(env, "Callback required").ThrowAsJavaScriptException();
615
+ return env.Undefined();
616
+ }
617
+
618
+ if (tsfn_) {
619
+ tsfn_.Release();
620
+ }
621
+
622
+ tsfn_ = Napi::ThreadSafeFunction::New(
623
+ env,
624
+ info[0].As<Napi::Function>(),
625
+ "DataBridge Data Callback",
626
+ 0, 1);
627
+
628
+ bridge_->set_on_data([this](const std::vector<uint8_t>& data) {
629
+ auto payload = new std::vector<uint8_t>(data);
630
+ auto status = tsfn_.BlockingCall(payload, [](Napi::Env env, Napi::Function callback, std::vector<uint8_t>* payload) {
631
+ callback.Call({
632
+ Napi::Buffer<uint8_t>::Copy(env, payload->data(), payload->size())
633
+ });
634
+ delete payload;
635
+ });
636
+
637
+ if (status != napi_ok) {
638
+ delete payload;
639
+ }
640
+ });
641
+
642
+ return env.Undefined();
643
+ }
644
+
645
+ Napi::Object PacketWrapper::Init(Napi::Env env, Napi::Object exports) {
646
+ Napi::Function func = DefineClass(env, "Packet", {
647
+ StaticMethod("serialize", &PacketWrapper::Serialize),
648
+ StaticMethod("deserialize", &PacketWrapper::Deserialize),
649
+ StaticValue("TYPE_DATA", Napi::Number::New(env, Packet::TYPE_DATA)),
650
+ StaticValue("TYPE_ACK", Napi::Number::New(env, Packet::TYPE_ACK)),
651
+ StaticValue("TYPE_NACK", Napi::Number::New(env, Packet::TYPE_NACK)),
652
+ StaticValue("TYPE_SYN", Napi::Number::New(env, Packet::TYPE_SYN)),
653
+ });
654
+ exports.Set("Packet", func);
655
+ return exports;
656
+ }
657
+
658
+ PacketWrapper::PacketWrapper(const Napi::CallbackInfo& info) : Napi::ObjectWrap<PacketWrapper>(info) {}
659
+
660
+ Napi::Value PacketWrapper::Serialize(const Napi::CallbackInfo& info) {
661
+ Napi::Env env = info.Env();
662
+ // Args: type, seq, payload, frag_id (opt), total_frags (opt)
663
+
664
+ if (info.Length() < 3) throw Napi::Error::New(env, "Args: type, seq, payload");
665
+
666
+ uint8_t type = info[0].As<Napi::Number>().Uint32Value();
667
+ uint8_t seq = info[1].As<Napi::Number>().Uint32Value();
668
+ std::string payload;
669
+
670
+ if (info[2].IsBuffer()) {
671
+ Napi::Buffer<char> buf = info[2].As<Napi::Buffer<char>>();
672
+ payload.assign(buf.Data(), buf.Length());
673
+ } else {
674
+ payload = info[2].ToString().Utf8Value();
675
+ }
676
+
677
+ uint16_t frag_id = 0;
678
+ uint16_t total_frags = 1;
679
+ if (info.Length() > 3) frag_id = info[3].As<Napi::Number>().Uint32Value();
680
+ if (info.Length() > 4) total_frags = info[4].As<Napi::Number>().Uint32Value();
681
+
682
+ auto vec = Packet::serialize(type, seq, payload, frag_id, total_frags);
683
+ return Napi::Buffer<uint8_t>::Copy(env, vec.data(), vec.size());
684
+ }
685
+
686
+ Napi::Value PacketWrapper::Deserialize(const Napi::CallbackInfo& info) {
687
+ Napi::Env env = info.Env();
688
+ if (info.Length() < 1 || !info[0].IsBuffer()) throw Napi::Error::New(env, "Buffer required");
689
+
690
+ Napi::Buffer<uint8_t> buf = info[0].As<Napi::Buffer<uint8_t>>();
691
+ std::vector<uint8_t> data(buf.Data(), buf.Data() + buf.Length());
692
+
693
+ // Packet::deserialize modifies the vector (consumes bytes)
694
+ Packet::Frame frame = Packet::deserialize(data);
695
+
696
+ Napi::Object result = Napi::Object::New(env);
697
+
698
+ // Frame object
699
+ Napi::Object frameObj = Napi::Object::New(env);
700
+ frameObj.Set("valid", frame.valid);
701
+
702
+ // Header
703
+ Napi::Object header = Napi::Object::New(env);
704
+ header.Set("type", frame.header.type);
705
+ header.Set("seq_id", frame.header.seq_id);
706
+ header.Set("fragment_id", frame.header.fragment_id);
707
+ header.Set("total_frags", frame.header.total_frags);
708
+ header.Set("payload_len", frame.header.payload_len);
709
+ header.Set("crc32", frame.header.crc32);
710
+ frameObj.Set("header", header);
711
+
712
+ // Payload
713
+ frameObj.Set("payload", Napi::Buffer<uint8_t>::Copy(env, frame.payload.data(), frame.payload.size()));
714
+
715
+ result.Set("frame", frameObj);
716
+ result.Set("remaining", Napi::Buffer<uint8_t>::Copy(env, data.data(), data.size()));
717
+
718
+ return result;
719
+ }
720
+
721
+ // --- Reassembler Wrapper ---
722
+ class ReassemblerWrapper : public Napi::ObjectWrap<ReassemblerWrapper> {
723
+ public:
724
+ static Napi::Object Init(Napi::Env env, Napi::Object exports);
725
+ ReassemblerWrapper(const Napi::CallbackInfo& info);
726
+
727
+ private:
728
+ std::unique_ptr<Reassembler> reassembler_;
729
+
730
+ Napi::Value ProcessFragment(const Napi::CallbackInfo& info);
731
+ Napi::Value IsComplete(const Napi::CallbackInfo& info);
732
+ Napi::Value GetData(const Napi::CallbackInfo& info);
733
+ Napi::Value IsDuplicate(const Napi::CallbackInfo& info);
734
+ Napi::Value GetBufferedSize(const Napi::CallbackInfo& info);
735
+ };
736
+
737
+ Napi::Object ReassemblerWrapper::Init(Napi::Env env, Napi::Object exports) {
738
+ Napi::Function func = DefineClass(env, "Reassembler", {
739
+ InstanceMethod("processFragment", &ReassemblerWrapper::ProcessFragment),
740
+ InstanceMethod("isComplete", &ReassemblerWrapper::IsComplete),
741
+ InstanceMethod("getData", &ReassemblerWrapper::GetData),
742
+ InstanceMethod("isDuplicate", &ReassemblerWrapper::IsDuplicate),
743
+ InstanceMethod("getBufferedSize", &ReassemblerWrapper::GetBufferedSize),
744
+ });
745
+ exports.Set("Reassembler", func);
746
+ return exports;
747
+ }
748
+
749
+ ReassemblerWrapper::ReassemblerWrapper(const Napi::CallbackInfo& info) : Napi::ObjectWrap<ReassemblerWrapper>(info) {
750
+ reassembler_ = std::make_unique<Reassembler>();
751
+ }
752
+
753
+ // Helper to extract C++ Frame from JS Frame object
754
+ Packet::Frame JsToFrame(Napi::Object jsFrame) {
755
+ Packet::Frame frame;
756
+ frame.valid = jsFrame.Get("valid").As<Napi::Boolean>().Value();
757
+ Napi::Object header = jsFrame.Get("header").As<Napi::Object>();
758
+ frame.header.type = header.Get("type").As<Napi::Number>().Uint32Value();
759
+ frame.header.seq_id = header.Get("seq_id").As<Napi::Number>().Uint32Value();
760
+ frame.header.fragment_id = header.Get("fragment_id").As<Napi::Number>().Uint32Value();
761
+ frame.header.total_frags = header.Get("total_frags").As<Napi::Number>().Uint32Value();
762
+
763
+ Napi::Buffer<uint8_t> pl = jsFrame.Get("payload").As<Napi::Buffer<uint8_t>>();
764
+ frame.payload.assign(pl.Data(), pl.Data() + pl.Length());
765
+ return frame;
766
+ }
767
+
768
+ Napi::Value ReassemblerWrapper::ProcessFragment(const Napi::CallbackInfo& info) {
769
+ if (info.Length() < 1 || !info[0].IsObject()) throw Napi::Error::New(info.Env(), "Frame object required");
770
+ Packet::Frame frame = JsToFrame(info[0].As<Napi::Object>());
771
+ return Napi::Boolean::New(info.Env(), reassembler_->process_fragment(frame));
772
+ }
773
+
774
+ Napi::Value ReassemblerWrapper::IsComplete(const Napi::CallbackInfo& info) {
775
+ if (info.Length() < 1 || !info[0].IsObject()) throw Napi::Error::New(info.Env(), "Frame object required");
776
+ Packet::Frame frame = JsToFrame(info[0].As<Napi::Object>());
777
+ return Napi::Boolean::New(info.Env(), reassembler_->is_complete(frame));
778
+ }
779
+
780
+ Napi::Value ReassemblerWrapper::GetData(const Napi::CallbackInfo& info) {
781
+ auto data = reassembler_->get_data();
782
+ return Napi::Buffer<uint8_t>::Copy(info.Env(), data.data(), data.size());
783
+ }
784
+
785
+ Napi::Value ReassemblerWrapper::IsDuplicate(const Napi::CallbackInfo& info) {
786
+ if (info.Length() < 1 || !info[0].IsObject()) throw Napi::Error::New(info.Env(), "Frame object required");
787
+ Packet::Frame frame = JsToFrame(info[0].As<Napi::Object>());
788
+ return Napi::Boolean::New(info.Env(), reassembler_->is_duplicate(frame));
789
+ }
790
+
791
+ Napi::Value ReassemblerWrapper::GetBufferedSize(const Napi::CallbackInfo& info) {
792
+ return Napi::Number::New(info.Env(), reassembler_->get_buffered_size());
793
+ }
794
+
795
+
796
+ // --- Init ---
797
+
798
+ Napi::Object InitAll(Napi::Env env, Napi::Object exports) {
799
+ SerialPortWrapper::Init(env, exports);
800
+ DataBridgeWrapper::Init(env, exports);
801
+ ResilientDataBridgeWrapper::Init(env, exports);
802
+ PacketWrapper::Init(env, exports);
803
+ ReassemblerWrapper::Init(env, exports);
804
+ return exports;
805
+ }
806
+
807
+ NODE_API_MODULE(safeserial_node, InitAll)