@senxor/serial-core 1.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/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # @senxor/serial-core
2
+
3
+ Shared serial transport layer for Senxor.js. Official packages `@senxor/web-serial` and `@senxor/capacitor-serial` build on this module so the same framing, parsing, and register commands work everywhere.
4
+
5
+ Most applications should depend on a concrete transport (Web Serial or Capacitor) instead of this package. Use `@senxor/serial-core` when you need to plug Senxor into a new runtime by implementing a thin adapter around your serial API.
6
+
7
+ ## What you get
8
+
9
+ - **`SerialTransportBase`** – Wraps any port that implements **`ISerialPort`** and exposes `@senxor/core`'s transport contract (`open` / `close`, register read/write, streaming callbacks). Pass the result into `new Senxor(transport)` from `@senxor/core`.
10
+ - **`ISerialPort`**, **`SerialDeviceInfo`**, **`SerialOptions`** – The minimal port surface your adapter must provide (open/close, `write`, event-style `on` for `data`, `error`, `open`, `close`, `disconnect`).
11
+ - **`senxorPortOptions`** – Default serial settings (115200 8N1) used when opening a port for Senxor.
12
+ - **`isSenxorDevice`** – Returns whether USB vendor/product IDs match a known Senxor product (same check the official transports use when listing devices).
13
+ - **`CommandSender`** – Lower-level helper for sending framed commands and matching ACKs; mainly useful if you extend or debug serial behavior rather than typical app code.
14
+ - **`utils`** – Small helpers such as **`toHexString`**, **`decodeUint8Array`**, shared with protocol tooling.
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pnpm add @senxor/core @senxor/serial-core
20
+ ```
21
+
22
+ You still need a real serial backend (browser Web Serial, Capacitor plugin, Node serial library, etc.) that you wrap with `ISerialPort`.
23
+
24
+ ## Development (this repository)
25
+
26
+ From the monorepo root:
27
+
28
+ ```bash
29
+ pnpm install
30
+ pnpm test --filter @senxor/serial-core
31
+ pnpm build --filter @senxor/serial-core
32
+ ```
33
+
34
+ For how `Senxor` consumes a transport after you wire one up, see `packages/core/README.md` and an official transport package for a full example.
@@ -0,0 +1,116 @@
1
+ //#region src/types.d.ts
2
+ interface SerialDeviceInfo {
3
+ vendorId: number;
4
+ productId: number;
5
+ [key: string]: any;
6
+ }
7
+ interface SerialOptions {
8
+ baudRate: number;
9
+ dataBits?: number;
10
+ stopBits?: number;
11
+ [key: string]: any;
12
+ }
13
+ interface ISerialPort<TDeviceInfo extends SerialDeviceInfo = SerialDeviceInfo, TOptions extends SerialOptions = SerialOptions> {
14
+ deviceInfo: TDeviceInfo;
15
+ isOpen: boolean;
16
+ open(options?: TOptions): Promise<void>;
17
+ close(): Promise<void>;
18
+ write(data: Uint8Array): Promise<void>;
19
+ on(event: "data", listener: (data: Uint8Array) => void): () => void;
20
+ on(event: "error", listener: (error: Error) => void): () => void;
21
+ on(event: "open", listener: () => void): () => void;
22
+ on(event: "close", listener: () => void): () => void;
23
+ on(event: "disconnect", listener: () => void): () => void;
24
+ }
25
+ type SenxorCommand = {
26
+ RREG: {
27
+ regAddr: number;
28
+ };
29
+ WREG: {
30
+ regAddr: number;
31
+ regValue: number;
32
+ };
33
+ RRSE: {
34
+ regs: Record<number, number>;
35
+ };
36
+ };
37
+ type SenxorAck = {
38
+ RREG: number;
39
+ WREG: void;
40
+ RRSE: Record<number, number>;
41
+ GFRA: {
42
+ header?: Uint8Array;
43
+ frame: Uint8Array;
44
+ };
45
+ SERR: void;
46
+ };
47
+ //#endregion
48
+ //#region src/command-sender.d.ts
49
+ declare class CommandSender<TPort extends ISerialPort> {
50
+ private port;
51
+ private commandMutex;
52
+ private encoder;
53
+ private pendingCommand?;
54
+ private errorListener?;
55
+ constructor(port: TPort);
56
+ onError(listener: (error: SenxorTransportError$1) => void): void;
57
+ sendCommand<K extends keyof SenxorCommand>(type: K, command: string, timeout?: number, attempts?: number): Promise<SenxorAck[K]>;
58
+ resolveAck<K extends keyof SenxorCommand>(type: K, data: SenxorAck[K]): void;
59
+ private sendCommandWithRetry;
60
+ private sendCommandOnce;
61
+ }
62
+ //#endregion
63
+ //#region src/transports.d.ts
64
+ declare const senxorPortOptions: SerialOptions;
65
+ declare class SerialTransportBase<TPort extends ISerialPort> implements ISenxorTransport {
66
+ private port;
67
+ private parser;
68
+ private commandSender;
69
+ private dataListener?;
70
+ private errorListener?;
71
+ private openListener?;
72
+ private closeListener?;
73
+ private disconnectListener?;
74
+ constructor(port: TPort);
75
+ get isOpen(): boolean;
76
+ get deviceInfo(): SerialDeviceInfo;
77
+ open(): Promise<void>;
78
+ close(): Promise<void>;
79
+ readReg(address: number): Promise<number>;
80
+ readRegs(addresses: number[]): Promise<Record<number, number>>;
81
+ writeReg(address: number, value: number): Promise<void>;
82
+ onData(listener: (data: SenxorRawData) => void): void;
83
+ onError(listener: (error: SenxorTransportError) => void): void;
84
+ onOpen(listener: () => void): void;
85
+ onClose(listener: () => void): void;
86
+ onDisconnect(listener: () => void): void;
87
+ private setupPortEventListeners;
88
+ private handleMessage;
89
+ private handlePortDisconnect;
90
+ private handlePortClose;
91
+ private handlePortOpen;
92
+ private handleGfraAck;
93
+ private handleParserError;
94
+ private handlePortError;
95
+ private handleSerrAck;
96
+ private handleUnknownAck;
97
+ }
98
+ //#endregion
99
+ //#region src/utils.d.ts
100
+ /**
101
+ * Convert a number to a fixed-length uppercase hexadecimal string
102
+ * @param value - The number to convert (must be non-negative)
103
+ * @param length - The desired length of the hex string
104
+ * @returns The uppercase hex string padded to the specified length
105
+ * @throws Error if value is negative or produces more digits than length
106
+ */
107
+ declare const toHexString: (value: number, length: number) => string;
108
+ /**
109
+ * Decode a Uint8Array to ASCII string
110
+ * @param buffer - The Uint8Array to decode
111
+ * @returns The decoded string
112
+ */
113
+ declare const decodeUint8Array: (buffer: Uint8Array) => string;
114
+ declare const isSenxorDevice: (device: SerialDeviceInfo) => boolean;
115
+ //#endregion
116
+ export { CommandSender, ISerialPort, SenxorAck, SenxorCommand, SerialDeviceInfo, SerialOptions, SerialTransportBase, decodeUint8Array, isSenxorDevice, senxorPortOptions, toHexString };
package/dist/index.js ADDED
@@ -0,0 +1,932 @@
1
+ import { Mutex } from "async-mutex";
2
+ import { PrefixLengthParser } from "serial-packet-parser";
3
+ const SENXOR_PRODUCT_ID = {
4
+ 45058: "EVK",
5
+ 45088: "XPRO",
6
+ 37779: "XCAM"
7
+ };
8
+ //#endregion
9
+ //#region ../core/src/error.ts
10
+ var SenxorTransportError = class extends Error {
11
+ cause;
12
+ constructor(message, cause) {
13
+ super(message);
14
+ this.name = "SenxorTransportError";
15
+ this.cause = cause;
16
+ }
17
+ };
18
+ //#endregion
19
+ //#region ../core/src/fields.ts
20
+ const FIELDS = {
21
+ SW_RESET: {
22
+ name: "SW_RESET",
23
+ readable: false,
24
+ writable: true,
25
+ addr: 0,
26
+ startBit: 0,
27
+ endBit: 1,
28
+ selfReset: true
29
+ },
30
+ DMA_TIMEOUT_ENABLE: {
31
+ name: "DMA_TIMEOUT_ENABLE",
32
+ readable: true,
33
+ writable: true,
34
+ addr: 1,
35
+ startBit: 0,
36
+ endBit: 1,
37
+ selfReset: false
38
+ },
39
+ TIMEOUT_PERIOD: {
40
+ name: "TIMEOUT_PERIOD",
41
+ readable: true,
42
+ writable: true,
43
+ addr: 1,
44
+ startBit: 1,
45
+ endBit: 3,
46
+ selfReset: false
47
+ },
48
+ STOP_HOST_XFER: {
49
+ name: "STOP_HOST_XFER",
50
+ readable: true,
51
+ writable: true,
52
+ addr: 1,
53
+ startBit: 3,
54
+ endBit: 4,
55
+ selfReset: true
56
+ },
57
+ REQ_RETRANSMIT: {
58
+ name: "REQ_RETRANSMIT",
59
+ readable: true,
60
+ writable: true,
61
+ addr: 25,
62
+ startBit: 0,
63
+ endBit: 1,
64
+ selfReset: true
65
+ },
66
+ AUTO_RETRANSMIT: {
67
+ name: "AUTO_RETRANSMIT",
68
+ readable: true,
69
+ writable: true,
70
+ addr: 25,
71
+ startBit: 1,
72
+ endBit: 2,
73
+ selfReset: false
74
+ },
75
+ GET_SINGLE_FRAME: {
76
+ name: "GET_SINGLE_FRAME",
77
+ readable: true,
78
+ writable: true,
79
+ addr: 177,
80
+ startBit: 0,
81
+ endBit: 1,
82
+ selfReset: true
83
+ },
84
+ CONTINUOUS_STREAM: {
85
+ name: "CONTINUOUS_STREAM",
86
+ readable: true,
87
+ writable: true,
88
+ addr: 177,
89
+ startBit: 1,
90
+ endBit: 2,
91
+ selfReset: false
92
+ },
93
+ READOUT_MODE: {
94
+ name: "READOUT_MODE",
95
+ readable: true,
96
+ writable: true,
97
+ addr: 177,
98
+ startBit: 2,
99
+ endBit: 5,
100
+ selfReset: false
101
+ },
102
+ NO_HEADER: {
103
+ name: "NO_HEADER",
104
+ readable: true,
105
+ writable: true,
106
+ addr: 177,
107
+ startBit: 5,
108
+ endBit: 6,
109
+ selfReset: false
110
+ },
111
+ ADC_ENABLE: {
112
+ name: "ADC_ENABLE",
113
+ readable: true,
114
+ writable: true,
115
+ addr: 177,
116
+ startBit: 7,
117
+ endBit: 8,
118
+ selfReset: false
119
+ },
120
+ FW_VERSION_MAJOR: {
121
+ name: "FW_VERSION_MAJOR",
122
+ readable: true,
123
+ writable: false,
124
+ addr: 178,
125
+ startBit: 4,
126
+ endBit: 8,
127
+ selfReset: false
128
+ },
129
+ FW_VERSION_MINOR: {
130
+ name: "FW_VERSION_MINOR",
131
+ readable: true,
132
+ writable: false,
133
+ addr: 178,
134
+ startBit: 0,
135
+ endBit: 4,
136
+ selfReset: false
137
+ },
138
+ FW_VERSION_BUILD: {
139
+ name: "FW_VERSION_BUILD",
140
+ readable: true,
141
+ writable: false,
142
+ addr: 179,
143
+ startBit: 0,
144
+ endBit: 8,
145
+ selfReset: false
146
+ },
147
+ FRAME_RATE_DIVIDER: {
148
+ name: "FRAME_RATE_DIVIDER",
149
+ readable: true,
150
+ writable: true,
151
+ addr: 180,
152
+ startBit: 0,
153
+ endBit: 7,
154
+ selfReset: false
155
+ },
156
+ SLEEP_PERIOD: {
157
+ name: "SLEEP_PERIOD",
158
+ readable: true,
159
+ writable: true,
160
+ addr: 181,
161
+ startBit: 0,
162
+ endBit: 6,
163
+ selfReset: false
164
+ },
165
+ PERIOD_X100: {
166
+ name: "PERIOD_X100",
167
+ readable: true,
168
+ writable: true,
169
+ addr: 181,
170
+ startBit: 6,
171
+ endBit: 7,
172
+ selfReset: false
173
+ },
174
+ SLEEP: {
175
+ name: "SLEEP",
176
+ readable: true,
177
+ writable: true,
178
+ addr: 181,
179
+ startBit: 7,
180
+ endBit: 8,
181
+ selfReset: true
182
+ },
183
+ READOUT_TOO_SLOW: {
184
+ name: "READOUT_TOO_SLOW",
185
+ readable: true,
186
+ writable: false,
187
+ addr: 182,
188
+ startBit: 1,
189
+ endBit: 2,
190
+ selfReset: true
191
+ },
192
+ SENXOR_IF_ERROR: {
193
+ name: "SENXOR_IF_ERROR",
194
+ readable: true,
195
+ writable: false,
196
+ addr: 182,
197
+ startBit: 2,
198
+ endBit: 3,
199
+ selfReset: false
200
+ },
201
+ CAPTURE_ERROR: {
202
+ name: "CAPTURE_ERROR",
203
+ readable: true,
204
+ writable: false,
205
+ addr: 182,
206
+ startBit: 3,
207
+ endBit: 4,
208
+ selfReset: false
209
+ },
210
+ DATA_READY: {
211
+ name: "DATA_READY",
212
+ readable: true,
213
+ writable: false,
214
+ addr: 182,
215
+ startBit: 4,
216
+ endBit: 5,
217
+ selfReset: false
218
+ },
219
+ BOOTING_UP: {
220
+ name: "BOOTING_UP",
221
+ readable: true,
222
+ writable: false,
223
+ addr: 182,
224
+ startBit: 5,
225
+ endBit: 6,
226
+ selfReset: false
227
+ },
228
+ CLK_SLOW_DOWN: {
229
+ name: "CLK_SLOW_DOWN",
230
+ readable: true,
231
+ writable: true,
232
+ addr: 183,
233
+ startBit: 0,
234
+ endBit: 1,
235
+ selfReset: false
236
+ },
237
+ MODULE_GAIN: {
238
+ name: "MODULE_GAIN",
239
+ readable: true,
240
+ writable: true,
241
+ addr: 185,
242
+ startBit: 0,
243
+ endBit: 4,
244
+ selfReset: false
245
+ },
246
+ SENXOR_TYPE: {
247
+ name: "SENXOR_TYPE",
248
+ readable: true,
249
+ writable: false,
250
+ addr: 186,
251
+ startBit: 0,
252
+ endBit: 8,
253
+ selfReset: false
254
+ },
255
+ MODULE_TYPE: {
256
+ name: "MODULE_TYPE",
257
+ readable: true,
258
+ writable: false,
259
+ addr: 187,
260
+ startBit: 0,
261
+ endBit: 8,
262
+ selfReset: false
263
+ },
264
+ MCU_TYPE: {
265
+ name: "MCU_TYPE",
266
+ readable: true,
267
+ writable: false,
268
+ addr: 51,
269
+ startBit: 0,
270
+ endBit: 8,
271
+ selfReset: false
272
+ },
273
+ LUT_SOURCE: {
274
+ name: "LUT_SOURCE",
275
+ readable: true,
276
+ writable: true,
277
+ addr: 188,
278
+ startBit: 0,
279
+ endBit: 1,
280
+ selfReset: false
281
+ },
282
+ LUT_SELECTOR: {
283
+ name: "LUT_SELECTOR",
284
+ readable: true,
285
+ writable: true,
286
+ addr: 188,
287
+ startBit: 1,
288
+ endBit: 3,
289
+ selfReset: false
290
+ },
291
+ LUT_VERSION: {
292
+ name: "LUT_VERSION",
293
+ readable: true,
294
+ writable: false,
295
+ addr: 188,
296
+ startBit: 4,
297
+ endBit: 8,
298
+ selfReset: false
299
+ },
300
+ CORR_FACTOR: {
301
+ name: "CORR_FACTOR",
302
+ readable: true,
303
+ writable: true,
304
+ addr: 194,
305
+ startBit: 0,
306
+ endBit: 8,
307
+ selfReset: false
308
+ },
309
+ START_COLOFFS_CALIB: {
310
+ name: "START_COLOFFS_CALIB",
311
+ readable: true,
312
+ writable: true,
313
+ addr: 197,
314
+ startBit: 1,
315
+ endBit: 2,
316
+ selfReset: true
317
+ },
318
+ COLOFFS_CALIB_ON: {
319
+ name: "COLOFFS_CALIB_ON",
320
+ readable: true,
321
+ writable: false,
322
+ addr: 197,
323
+ startBit: 2,
324
+ endBit: 3,
325
+ selfReset: false
326
+ },
327
+ USE_SELF_CALIB: {
328
+ name: "USE_SELF_CALIB",
329
+ readable: true,
330
+ writable: true,
331
+ addr: 197,
332
+ startBit: 4,
333
+ endBit: 5,
334
+ selfReset: true
335
+ },
336
+ CALIB_SAMPLE_SIZE: {
337
+ name: "CALIB_SAMPLE_SIZE",
338
+ readable: true,
339
+ writable: true,
340
+ addr: 197,
341
+ startBit: 5,
342
+ endBit: 8,
343
+ selfReset: false
344
+ },
345
+ EMISSIVITY: {
346
+ name: "EMISSIVITY",
347
+ readable: true,
348
+ writable: true,
349
+ addr: 202,
350
+ startBit: 0,
351
+ endBit: 8,
352
+ selfReset: false
353
+ },
354
+ OFFSET: {
355
+ name: "OFFSET",
356
+ readable: true,
357
+ writable: true,
358
+ addr: 203,
359
+ startBit: 0,
360
+ endBit: 8,
361
+ selfReset: false
362
+ },
363
+ OTF: {
364
+ name: "OTF",
365
+ readable: true,
366
+ writable: true,
367
+ addr: 205,
368
+ startBit: 0,
369
+ endBit: 8,
370
+ selfReset: false
371
+ },
372
+ PRODUCTION_YEAR: {
373
+ name: "PRODUCTION_YEAR",
374
+ readable: true,
375
+ writable: false,
376
+ addr: 224,
377
+ startBit: 0,
378
+ endBit: 8,
379
+ selfReset: false
380
+ },
381
+ PRODUCTION_WEEK: {
382
+ name: "PRODUCTION_WEEK",
383
+ readable: true,
384
+ writable: false,
385
+ addr: 225,
386
+ startBit: 0,
387
+ endBit: 8,
388
+ selfReset: false
389
+ },
390
+ MANUF_LOCATION: {
391
+ name: "MANUF_LOCATION",
392
+ readable: true,
393
+ writable: false,
394
+ addr: 226,
395
+ startBit: 0,
396
+ endBit: 8,
397
+ selfReset: false
398
+ },
399
+ SERIAL_NUMBER_0: {
400
+ name: "SERIAL_NUMBER_0",
401
+ readable: true,
402
+ writable: false,
403
+ addr: 227,
404
+ startBit: 0,
405
+ endBit: 8,
406
+ selfReset: false
407
+ },
408
+ SERIAL_NUMBER_1: {
409
+ name: "SERIAL_NUMBER_1",
410
+ readable: true,
411
+ writable: false,
412
+ addr: 228,
413
+ startBit: 0,
414
+ endBit: 8,
415
+ selfReset: false
416
+ },
417
+ SERIAL_NUMBER_2: {
418
+ name: "SERIAL_NUMBER_2",
419
+ readable: true,
420
+ writable: false,
421
+ addr: 229,
422
+ startBit: 0,
423
+ endBit: 8,
424
+ selfReset: false
425
+ },
426
+ USER_FLASH_ENABLE: {
427
+ name: "USER_FLASH_ENABLE",
428
+ readable: true,
429
+ writable: true,
430
+ addr: 216,
431
+ startBit: 0,
432
+ endBit: 1,
433
+ selfReset: false
434
+ },
435
+ TEMP_UNITS: {
436
+ name: "TEMP_UNITS",
437
+ readable: true,
438
+ writable: true,
439
+ addr: 49,
440
+ startBit: 0,
441
+ endBit: 3,
442
+ selfReset: false
443
+ },
444
+ STARK_ENABLE: {
445
+ name: "STARK_ENABLE",
446
+ readable: true,
447
+ writable: true,
448
+ addr: 32,
449
+ startBit: 0,
450
+ endBit: 1,
451
+ selfReset: false
452
+ },
453
+ STARK_TYPE: {
454
+ name: "STARK_TYPE",
455
+ readable: true,
456
+ writable: true,
457
+ addr: 32,
458
+ startBit: 1,
459
+ endBit: 4,
460
+ selfReset: false
461
+ },
462
+ SPATIAL_KERNEL: {
463
+ name: "SPATIAL_KERNEL",
464
+ readable: true,
465
+ writable: true,
466
+ addr: 32,
467
+ startBit: 4,
468
+ endBit: 5,
469
+ selfReset: false
470
+ },
471
+ STARK_CUTOFF: {
472
+ name: "STARK_CUTOFF",
473
+ readable: true,
474
+ writable: true,
475
+ addr: 33,
476
+ startBit: 0,
477
+ endBit: 7,
478
+ selfReset: false
479
+ },
480
+ STARK_GRADIENT: {
481
+ name: "STARK_GRADIENT",
482
+ readable: true,
483
+ writable: true,
484
+ addr: 34,
485
+ startBit: 0,
486
+ endBit: 8,
487
+ selfReset: false
488
+ },
489
+ STARK_SCALE: {
490
+ name: "STARK_SCALE",
491
+ readable: true,
492
+ writable: true,
493
+ addr: 35,
494
+ startBit: 0,
495
+ endBit: 8,
496
+ selfReset: false
497
+ },
498
+ MMS_KXMS: {
499
+ name: "MMS_KXMS",
500
+ readable: true,
501
+ writable: true,
502
+ addr: 37,
503
+ startBit: 0,
504
+ endBit: 1,
505
+ selfReset: false
506
+ },
507
+ MMS_RA: {
508
+ name: "MMS_RA",
509
+ readable: true,
510
+ writable: true,
511
+ addr: 37,
512
+ startBit: 1,
513
+ endBit: 2,
514
+ selfReset: false
515
+ },
516
+ MEDIAN_ENABLE: {
517
+ name: "MEDIAN_ENABLE",
518
+ readable: true,
519
+ writable: true,
520
+ addr: 48,
521
+ startBit: 0,
522
+ endBit: 1,
523
+ selfReset: false
524
+ },
525
+ MEDIAN_KERNEL_SIZE: {
526
+ name: "MEDIAN_KERNEL_SIZE",
527
+ readable: true,
528
+ writable: true,
529
+ addr: 48,
530
+ startBit: 1,
531
+ endBit: 2,
532
+ selfReset: false
533
+ },
534
+ TEMPORAL_ENABLE: {
535
+ name: "TEMPORAL_ENABLE",
536
+ readable: true,
537
+ writable: true,
538
+ addr: 208,
539
+ startBit: 0,
540
+ endBit: 1,
541
+ selfReset: false
542
+ },
543
+ TEMPORAL_INIT: {
544
+ name: "TEMPORAL_INIT",
545
+ readable: true,
546
+ writable: true,
547
+ addr: 208,
548
+ startBit: 1,
549
+ endBit: 2,
550
+ selfReset: true
551
+ },
552
+ TEMPORAL_LSB: {
553
+ name: "TEMPORAL_LSB",
554
+ readable: true,
555
+ writable: true,
556
+ addr: 209,
557
+ startBit: 0,
558
+ endBit: 8,
559
+ selfReset: false
560
+ },
561
+ TEMPORAL_MSB: {
562
+ name: "TEMPORAL_MSB",
563
+ readable: true,
564
+ writable: true,
565
+ addr: 210,
566
+ startBit: 0,
567
+ endBit: 8,
568
+ selfReset: false
569
+ }
570
+ };
571
+ (() => {
572
+ const result = {};
573
+ Object.keys(FIELDS).forEach((fieldName) => {
574
+ const addr = FIELDS[fieldName].addr;
575
+ if (!result[addr]) result[addr] = [];
576
+ result[addr].push(fieldName);
577
+ });
578
+ return result;
579
+ })();
580
+ //#endregion
581
+ //#region src/command-sender.ts
582
+ var CommandSender = class {
583
+ port;
584
+ commandMutex;
585
+ encoder = new TextEncoder();
586
+ pendingCommand;
587
+ errorListener;
588
+ constructor(port) {
589
+ this.port = port;
590
+ this.commandMutex = new Mutex();
591
+ }
592
+ onError(listener) {
593
+ this.errorListener = listener;
594
+ }
595
+ async sendCommand(type, command, timeout = 2e3, attempts = 3) {
596
+ return this.commandMutex.runExclusive(async () => {
597
+ return this.sendCommandWithRetry(type, command, timeout, attempts);
598
+ });
599
+ }
600
+ resolveAck(type, data) {
601
+ if (!this.pendingCommand) {
602
+ const err = new SenxorTransportError(`Bare ACK received: ${type} without a corresponding command`);
603
+ this.errorListener?.(err);
604
+ return;
605
+ }
606
+ if (this.pendingCommand.type !== type) {
607
+ const err = new SenxorTransportError(`ACK type mismatch: expected ${this.pendingCommand.type}, received ${type}`);
608
+ this.pendingCommand.reject(err);
609
+ this.pendingCommand = void 0;
610
+ return;
611
+ }
612
+ this.pendingCommand.resolve(data);
613
+ this.pendingCommand = void 0;
614
+ }
615
+ async sendCommandWithRetry(type, command, timeout, attempts) {
616
+ let lastError;
617
+ for (let i = 0; i < attempts; i++) try {
618
+ return await this.sendCommandOnce(type, command, timeout);
619
+ } catch (error) {
620
+ lastError = error;
621
+ }
622
+ throw lastError;
623
+ }
624
+ async sendCommandOnce(type, command, timeout) {
625
+ return new Promise((resolve, reject) => {
626
+ this.pendingCommand = {
627
+ type,
628
+ resolve,
629
+ reject
630
+ };
631
+ const timeoutId = setTimeout(() => {
632
+ this.pendingCommand = void 0;
633
+ reject(new SenxorTransportError(`Command timeout: no ACK received for ${type}`));
634
+ }, timeout);
635
+ this.port.write(this.encoder.encode(command)).catch((err) => {
636
+ clearTimeout(timeoutId);
637
+ this.pendingCommand = void 0;
638
+ reject(new SenxorTransportError(`Failed to write command: ${type}`, err));
639
+ });
640
+ });
641
+ }
642
+ };
643
+ //#endregion
644
+ //#region src/utils.ts
645
+ const decoder = new TextDecoder("ascii");
646
+ /**
647
+ * Convert a number to a fixed-length uppercase hexadecimal string
648
+ * @param value - The number to convert (must be non-negative)
649
+ * @param length - The desired length of the hex string
650
+ * @returns The uppercase hex string padded to the specified length
651
+ * @throws Error if value is negative or produces more digits than length
652
+ */
653
+ const toHexString = (value, length) => {
654
+ if (value < 0) throw new Error(`Cannot convert negative value ${value} to hex string`);
655
+ const hex = value.toString(16).toUpperCase();
656
+ if (hex.length > length) throw new Error(`Value ${value} (0x${hex}) exceeds ${length} hex digits`);
657
+ return hex.padStart(length, "0");
658
+ };
659
+ /**
660
+ * Decode a Uint8Array to ASCII string
661
+ * @param buffer - The Uint8Array to decode
662
+ * @returns The decoded string
663
+ */
664
+ const decodeUint8Array = (buffer) => {
665
+ return decoder.decode(buffer);
666
+ };
667
+ const isSenxorDevice = (device) => {
668
+ return device.vendorId === 1046 && device.productId in SENXOR_PRODUCT_ID;
669
+ };
670
+ //#endregion
671
+ //#region src/parser.ts
672
+ const msgPrefix = new TextEncoder().encode(" #");
673
+ const msgLenFieldSize = 4;
674
+ const msgLenFieldStart = 4;
675
+ const msgCmdStart = msgLenFieldStart + msgLenFieldSize;
676
+ const cmdLen = 4;
677
+ const checksumIndex = -4;
678
+ const msgDataStart = msgCmdStart + cmdLen;
679
+ const parseMessageLength = (buf, start, size) => {
680
+ const hexString = decodeUint8Array(buf.slice(start, start + size));
681
+ return Number.parseInt(hexString, 16);
682
+ };
683
+ const verifyChecksum = (message, _payloadStart, _payloadLen) => {
684
+ return decodeUint8Array(message.slice(checksumIndex)) === toHexString(message.slice(msgLenFieldStart, checksumIndex).reduce((acc, val) => acc + val, 0) & 65535, 4);
685
+ };
686
+ const parseMessageBody = (message) => {
687
+ return {
688
+ cmd: decodeUint8Array(message.slice(msgCmdStart, msgCmdStart + cmdLen)),
689
+ data: message.slice(msgDataStart, checksumIndex)
690
+ };
691
+ };
692
+ const messageParserOptions = {
693
+ prefix: msgPrefix,
694
+ lengthFieldSize: msgLenFieldSize,
695
+ parseLength: parseMessageLength,
696
+ minPayloadLen: 4,
697
+ maxPayloadLen: 64 * 1024,
698
+ maxBufferSize: 64 * 1024,
699
+ verifyMessage: verifyChecksum,
700
+ onMessage: void 0,
701
+ onError: void 0,
702
+ onResync: void 0
703
+ };
704
+ const gfraDataFormat = {
705
+ 10080: {
706
+ header: void 0,
707
+ frame: {
708
+ start: 160,
709
+ end: 10080
710
+ }
711
+ },
712
+ 10240: {
713
+ header: {
714
+ start: 160,
715
+ end: 320
716
+ },
717
+ frame: {
718
+ start: 320,
719
+ end: 10240
720
+ }
721
+ },
722
+ 39360: {
723
+ header: void 0,
724
+ frame: {
725
+ start: 960,
726
+ end: 39360
727
+ }
728
+ },
729
+ 39680: {
730
+ header: {
731
+ start: 960,
732
+ end: 1280
733
+ },
734
+ frame: {
735
+ start: 1280,
736
+ end: 39680
737
+ }
738
+ },
739
+ 5200: {
740
+ header: {
741
+ start: 100,
742
+ end: 200
743
+ },
744
+ frame: {
745
+ start: 200,
746
+ end: 5200
747
+ }
748
+ }
749
+ };
750
+ const senxorAckDecoder = {
751
+ RREG(data) {
752
+ const hexString = decodeUint8Array(data);
753
+ return Number.parseInt(hexString, 16);
754
+ },
755
+ WREG(data) {},
756
+ RRSE(data) {
757
+ const regs = {};
758
+ for (let i = 0; i < data.length; i += 4) {
759
+ const regAddr = Number.parseInt(decodeUint8Array(data.slice(i, i + 2)), 16);
760
+ regs[regAddr] = Number.parseInt(decodeUint8Array(data.slice(i + 2, i + 4)), 16);
761
+ }
762
+ return regs;
763
+ },
764
+ GFRA(data) {
765
+ const length = data.length;
766
+ if (length in gfraDataFormat) {
767
+ const { header: headerSlice, frame: frameSlice } = gfraDataFormat[length];
768
+ return {
769
+ header: headerSlice ? data.slice(headerSlice.start, headerSlice.end) : void 0,
770
+ frame: data.slice(frameSlice.start, frameSlice.end)
771
+ };
772
+ }
773
+ throw new Error(`Invalid GFRA data length: ${length}`);
774
+ },
775
+ SERR(data) {}
776
+ };
777
+ const senxorAckEncoder = {
778
+ RREG(regAddr) {
779
+ return ` #000ARREG${toHexString(regAddr, 2)}XXXX`;
780
+ },
781
+ WREG(regAddr, regValue) {
782
+ return ` #000CWREG${toHexString(regAddr, 2)}${toHexString(regValue, 2)}XXXX`;
783
+ },
784
+ RRSE(addrs) {
785
+ const payload = `RRSE${addrs.map((addr) => toHexString(addr, 2)).join("")}FFXXXX`;
786
+ return ` #${toHexString(payload.length, 4)}${payload}`;
787
+ }
788
+ };
789
+ //#endregion
790
+ //#region src/transports.ts
791
+ const senxorPortOptions = {
792
+ baudRate: 115200,
793
+ bytesize: 8,
794
+ stopbits: 1
795
+ };
796
+ var SerialTransportBase = class {
797
+ port;
798
+ parser;
799
+ commandSender;
800
+ dataListener;
801
+ errorListener;
802
+ openListener;
803
+ closeListener;
804
+ disconnectListener;
805
+ constructor(port) {
806
+ this.port = port;
807
+ this.commandSender = new CommandSender(this.port);
808
+ this.parser = new PrefixLengthParser({
809
+ ...messageParserOptions,
810
+ onMessage: this.handleMessage.bind(this),
811
+ onError: this.handleParserError.bind(this)
812
+ });
813
+ this.setupPortEventListeners();
814
+ }
815
+ get isOpen() {
816
+ return this.port.isOpen;
817
+ }
818
+ get deviceInfo() {
819
+ return this.port.deviceInfo;
820
+ }
821
+ async open() {
822
+ await this.port.open(senxorPortOptions);
823
+ }
824
+ async close() {
825
+ await this.port.close();
826
+ }
827
+ async readReg(address) {
828
+ if (!this.port.isOpen) throw new SenxorTransportError("Cannot read register: port is closed");
829
+ const command = senxorAckEncoder.RREG(address);
830
+ return this.commandSender.sendCommand("RREG", command, 1e3, 2);
831
+ }
832
+ async readRegs(addresses) {
833
+ if (!this.port.isOpen) throw new SenxorTransportError("Cannot read registers: port is closed");
834
+ const commands = senxorAckEncoder.RRSE(addresses);
835
+ return this.commandSender.sendCommand("RRSE", commands, 1e3, 2);
836
+ }
837
+ async writeReg(address, value) {
838
+ if (!this.port.isOpen) throw new SenxorTransportError("Cannot write register: port is closed");
839
+ const command = senxorAckEncoder.WREG(address, value);
840
+ return this.commandSender.sendCommand("WREG", command, 1e3, 2);
841
+ }
842
+ onData(listener) {
843
+ this.dataListener = listener;
844
+ }
845
+ onError(listener) {
846
+ this.errorListener = listener;
847
+ this.commandSender.onError(listener);
848
+ }
849
+ onOpen(listener) {
850
+ this.openListener = listener;
851
+ }
852
+ onClose(listener) {
853
+ this.closeListener = listener;
854
+ }
855
+ onDisconnect(listener) {
856
+ this.disconnectListener = listener;
857
+ }
858
+ setupPortEventListeners() {
859
+ this.port.on("data", (data) => this.parser.push(data));
860
+ this.port.on("error", (error) => this.handlePortError(error));
861
+ this.port.on("open", () => this.handlePortOpen());
862
+ this.port.on("close", () => this.handlePortClose());
863
+ this.port.on("disconnect", () => this.handlePortDisconnect());
864
+ }
865
+ handleMessage(message, _totalLength) {
866
+ const { cmd: cmdStr, data } = parseMessageBody(message);
867
+ switch (cmdStr) {
868
+ case "RREG": {
869
+ const regValue = senxorAckDecoder.RREG(data);
870
+ this.commandSender.resolveAck("RREG", regValue);
871
+ break;
872
+ }
873
+ case "WREG": {
874
+ const wregResult = senxorAckDecoder.WREG(data);
875
+ this.commandSender.resolveAck("WREG", wregResult);
876
+ break;
877
+ }
878
+ case "RRSE": {
879
+ const rrseResult = senxorAckDecoder.RRSE(data);
880
+ this.commandSender.resolveAck("RRSE", rrseResult);
881
+ break;
882
+ }
883
+ case "GFRA": {
884
+ const gfraData = senxorAckDecoder.GFRA(data);
885
+ this.handleGfraAck(gfraData);
886
+ break;
887
+ }
888
+ case "SERR":
889
+ this.handleSerrAck();
890
+ break;
891
+ default:
892
+ this.handleUnknownAck(cmdStr);
893
+ break;
894
+ }
895
+ }
896
+ handlePortDisconnect() {
897
+ this.disconnectListener?.();
898
+ }
899
+ handlePortClose() {
900
+ this.closeListener?.();
901
+ }
902
+ handlePortOpen() {
903
+ this.openListener?.();
904
+ }
905
+ handleGfraAck(gfraData) {
906
+ const timestamp = Date.now();
907
+ const senxorRawData = {
908
+ header: gfraData.header,
909
+ frame: gfraData.frame,
910
+ timestamp
911
+ };
912
+ this.dataListener?.(senxorRawData);
913
+ }
914
+ handleParserError(error) {
915
+ const err = new SenxorTransportError("Parser error", error);
916
+ this.errorListener?.(err);
917
+ }
918
+ handlePortError(error) {
919
+ const err = new SenxorTransportError("Port I/O error", error);
920
+ this.errorListener?.(err);
921
+ }
922
+ handleSerrAck() {
923
+ const err = new SenxorTransportError("SERR Error: The device does not have a lens module installed");
924
+ this.errorListener?.(err);
925
+ }
926
+ handleUnknownAck(cmd) {
927
+ const err = new SenxorTransportError(`Unknown ACK: ${cmd}`);
928
+ this.errorListener?.(err);
929
+ }
930
+ };
931
+ //#endregion
932
+ export { CommandSender, SerialTransportBase, decodeUint8Array, isSenxorDevice, senxorPortOptions, toHexString };
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@senxor/serial-core",
3
+ "type": "module",
4
+ "version": "1.1.0",
5
+ "description": "Shared serial transport core for Senxor JavaScript SDK.",
6
+ "author": "Shui <zelongshui@meridianinno.com>",
7
+ "license": "Apache-2.0",
8
+ "homepage": "https://github.com/MeridianInnovation/senxor-js#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/MeridianInnovation/senxor-js.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/MeridianInnovation/senxor-js/issues"
15
+ },
16
+ "exports": {
17
+ ".": "./dist/index.js",
18
+ "./package.json": "./package.json"
19
+ },
20
+ "types": "./dist/index.d.ts",
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "devDependencies": {
25
+ "@types/node": "^25.3.5",
26
+ "@typescript/native-preview": "7.0.0-dev.20260309.1",
27
+ "bumpp": "^10.4.1",
28
+ "tsdown": "^0.21.1",
29
+ "typescript": "^5.9.3",
30
+ "vitest": "^4.0.18"
31
+ },
32
+ "dependencies": {
33
+ "async-mutex": "^0.5.0",
34
+ "serial-packet-parser": "latest"
35
+ },
36
+ "scripts": {
37
+ "build": "tsdown",
38
+ "dev": "tsdown --watch",
39
+ "test": "vitest",
40
+ "typecheck": "tsc --noEmit"
41
+ }
42
+ }