serial-core 0.2.4 → 0.3.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 +653 -169
- package/dist/index.cjs +10 -1
- package/dist/index.d.cts +511 -9
- package/dist/index.d.ts +511 -9
- package/dist/index.js +10 -1
- package/llms.txt +191 -0
- package/package.json +23 -23
- package/dist/SerialService.d.cts +0 -99
- package/dist/SerialService.d.ts +0 -99
- package/dist/core/PortRegistry.d.cts +0 -32
- package/dist/core/PortRegistry.d.ts +0 -32
- package/dist/core/PortScanner.d.cts +0 -15
- package/dist/core/PortScanner.d.ts +0 -15
- package/dist/core/QueueManager.d.cts +0 -68
- package/dist/core/QueueManager.d.ts +0 -68
- package/dist/types.d.cts +0 -42
- package/dist/types.d.ts +0 -42
- package/dist/utils/buffers.d.cts +0 -39
- package/dist/utils/buffers.d.ts +0 -39
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,515 @@
|
|
|
1
|
+
import { SerialPort } from "serialport";
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
/** Connection states emitted by `SerialService` through the `status` event. */
|
|
4
|
+
export declare const SerialStatus: {
|
|
5
|
+
/** Initial state, manual stop, disabled recovery or exhausted retry budget. */
|
|
6
|
+
readonly DISCONNECTED: "DISCONNECTED";
|
|
7
|
+
/** Waiting for previous teardown or enumerating candidate devices. */
|
|
8
|
+
readonly SCANNING: "SCANNING";
|
|
9
|
+
/** Port reserved; opening or performing the optional handshake. */
|
|
10
|
+
readonly CONNECTING: "CONNECTING";
|
|
11
|
+
/** Port open and handshake, when configured, completed; `send()` is allowed. */
|
|
12
|
+
readonly CONNECTED: "CONNECTED";
|
|
13
|
+
/** A retry is scheduled; `send()` rejects during recovery. */
|
|
14
|
+
readonly RECONNECTING: "RECONNECTING";
|
|
15
|
+
};
|
|
16
|
+
/** Union of the string values in {@link SerialStatus}. */
|
|
17
|
+
export type SerialStatus = (typeof SerialStatus)[keyof typeof SerialStatus];
|
|
1
18
|
/**
|
|
2
|
-
*
|
|
19
|
+
* Options for `SerialService`. `baudRate` and at least one device selector
|
|
20
|
+
* (`path`, `vendorId`, `productId`, `serialNumber`) are required.
|
|
3
21
|
*
|
|
4
|
-
*
|
|
22
|
+
* @remarks
|
|
23
|
+
* A fixed path takes precedence over discovery selectors. Timer values must be
|
|
24
|
+
* positive, finite and no greater than 2147483647 ms. Invalid configuration throws
|
|
25
|
+
* during construction. Specify optional properties only when overriding defaults.
|
|
5
26
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
27
|
+
* @example Vendor-only autodiscovery; omit path so discovery is enabled.
|
|
28
|
+
* ```ts
|
|
29
|
+
* const config: SerialConfig = { baudRate: 9600, vendorId: '0403', autoConnect: true };
|
|
30
|
+
* ```
|
|
31
|
+
* @example Product-only autodiscovery, without a vendor selector.
|
|
32
|
+
* ```ts
|
|
33
|
+
* const config: SerialConfig = { baudRate: 115200, productId: '6001', autoConnect: true };
|
|
34
|
+
* ```
|
|
35
|
+
* @example Exact vendor/product combination with optional physical-unit identity.
|
|
36
|
+
* ```ts
|
|
37
|
+
* const config: SerialConfig = {
|
|
38
|
+
* baudRate: 115200, vendorId: '0403', productId: '6001', serialNumber: 'UNIT-1'
|
|
39
|
+
* };
|
|
40
|
+
* ```
|
|
8
41
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
42
|
+
interface SerialConfig {
|
|
43
|
+
/** Required positive finite baud rate; there is no implicit baud-rate default. */
|
|
44
|
+
baudRate: number;
|
|
45
|
+
/** Fixed OS path, such as `COM3` or `/dev/ttyUSB0`; disables discovery when set. */
|
|
46
|
+
path?: string;
|
|
47
|
+
/** Exact USB vendor ID after hex normalization, e.g. `0403` or `0x0403`. Enables vendor-only autodiscovery when path is omitted; combine productId/serialNumber to narrow matches. */
|
|
48
|
+
vendorId?: string;
|
|
49
|
+
/** Exact USB product ID after hex normalization. Enables product-only autodiscovery without vendorId when path is omitted; combine vendorId to restrict the vendor. */
|
|
50
|
+
productId?: string;
|
|
51
|
+
/** Initial retry delay in milliseconds. @defaultValue 2000 */
|
|
52
|
+
reconnectInterval?: number;
|
|
53
|
+
/** Starts connection in a microtask after construction. @defaultValue false */
|
|
54
|
+
autoConnect?: boolean;
|
|
55
|
+
/** Enables retries after connection failures or unexpected closure. @defaultValue true */
|
|
56
|
+
autoReconnect?: boolean;
|
|
57
|
+
/** Finite retry-delay multiplier, at least 1. @defaultValue 1 */
|
|
58
|
+
reconnectBackoffFactor?: number;
|
|
59
|
+
/** Retry delay ceiling, at least reconnectInterval. @defaultValue Math.max(30000, reconnectInterval) */
|
|
60
|
+
maxReconnectInterval?: number;
|
|
61
|
+
/** Non-negative integer retry budget, excluding the initial attempt, or Infinity. @defaultValue Infinity */
|
|
62
|
+
maxReconnectAttempts?: number;
|
|
63
|
+
/** Exact serial-number selector; distinguishes units with identical VID/PID. */
|
|
64
|
+
serialNumber?: string;
|
|
65
|
+
/** Physical write and drain deadline in milliseconds, independent of send options. @defaultValue 5000 */
|
|
66
|
+
writeTimeout?: number;
|
|
67
|
+
/** Response deadline in milliseconds after a write completes. @defaultValue 5000 */
|
|
68
|
+
responseTimeout?: number;
|
|
69
|
+
/** Positive integer maximum of active plus pending commands. @defaultValue 1000 */
|
|
70
|
+
maxQueueSize?: number;
|
|
71
|
+
/** Reused parser, kept writable after disconnect. Mutually exclusive with parserFactory. */
|
|
72
|
+
parser?: NodeJS.ReadWriteStream;
|
|
73
|
+
/** Fresh parser for each connection; recommended to avoid stale frame buffers across sessions. */
|
|
74
|
+
parserFactory?: () => NodeJS.ReadWriteStream;
|
|
75
|
+
/** Optional verification before CONNECTED; failure triggers the configured recovery policy. */
|
|
76
|
+
handshake?: {
|
|
77
|
+
/** Command sent directly, bypassing the normal queue, after opening the port. */
|
|
78
|
+
command: string | Buffer;
|
|
79
|
+
/** Regex or regex source tested against accumulated input; required even when callback is supplied. */
|
|
80
|
+
pattern: string | RegExp;
|
|
81
|
+
/** Positive deadline in milliseconds, including handshake command transmission. */
|
|
82
|
+
timeout: number;
|
|
83
|
+
/** Tests lowercase hexadecimal input instead of UTF-8 text; ignored with callback. @defaultValue false */
|
|
84
|
+
hexPattern?: boolean;
|
|
85
|
+
/** Synchronous per-chunk/frame validator with priority over pattern; true accepts, exceptions fail verification. */
|
|
86
|
+
callback?: ((data: Buffer | string) => boolean) | null;
|
|
87
|
+
/** Positive integer accumulated-byte ceiling for pattern matching; not applied to callback. @defaultValue 65536 */
|
|
88
|
+
maxBufferSize?: number;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** Per-command options for `SerialService.send()`; all fields are optional. */
|
|
92
|
+
interface SendOptions {
|
|
93
|
+
/** Local response-context label, not sent to the device and not a protocol request ID. */
|
|
94
|
+
alias?: string;
|
|
95
|
+
/**
|
|
96
|
+
* Positive finite milliseconds, at most 2147483647. With waitResponse, starts
|
|
97
|
+
* after writing and overrides responseTimeout; otherwise limits the queue's write wait.
|
|
98
|
+
* The service-level writeTimeout independently limits physical write/drain.
|
|
99
|
+
*/
|
|
100
|
+
timeout?: number;
|
|
101
|
+
/** Waits for a matching response as well as write/drain; otherwise resolves after drain. @defaultValue false */
|
|
102
|
+
waitResponse?: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* Synchronous response predicate, used only with waitResponse. False ignores
|
|
105
|
+
* a frame; true accepts it; exceptions reject the command. Without a predicate,
|
|
106
|
+
* the next chunk/frame is accepted. Use a parser for complete protocol frames.
|
|
107
|
+
*/
|
|
108
|
+
responseMatcher?: (data: Buffer | string) => boolean;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Metadata returned by serialport enumeration, passed through without modification.
|
|
112
|
+
* Includes path and potentially manufacturer, serialNumber, pnpId, locationId,
|
|
113
|
+
* vendorId and productId; optional field availability depends on OS/driver/device.
|
|
114
|
+
*/
|
|
115
|
+
type SerialPortInfo = Awaited<ReturnType<typeof import('serialport').SerialPort.list>>[number];
|
|
116
|
+
/** Exact AND-combined discovery filters; an empty object lists every enumerated port. */
|
|
117
|
+
interface PortFilter {
|
|
118
|
+
/** Exact OS path; enumeration still runs and the port is not opened. */
|
|
119
|
+
path?: string;
|
|
120
|
+
/** Exact USB VID after trimming, case folding and removal of 0x and leading zeros. */
|
|
121
|
+
vendorId?: string;
|
|
122
|
+
/** Exact USB PID using the same normalization as vendorId. */
|
|
123
|
+
productId?: string;
|
|
124
|
+
/** Exact, case-sensitive serial number. */
|
|
125
|
+
serialNumber?: string;
|
|
126
|
+
/** Exact, case-sensitive manufacturer name. */
|
|
127
|
+
manufacturer?: string;
|
|
128
|
+
/** Excludes reservations in this loaded library instance; does not probe other processes. @defaultValue false */
|
|
129
|
+
availableOnly?: boolean;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Typed service event listener signatures, not an EventEmitter tuple map.
|
|
133
|
+
* Listeners run synchronously and should not throw. Handle asynchronous listener
|
|
134
|
+
* rejections inside the listener; this service does not enable captureRejections.
|
|
135
|
+
*/
|
|
136
|
+
interface SerialEvents {
|
|
137
|
+
/** Emitted only when status changes. CONNECTED permits sending. */
|
|
138
|
+
status: (status: SerialStatus) => void;
|
|
139
|
+
/** Raw chunk or parsed frame, including handshake traffic; optional alias is local context, not guaranteed correlation. */
|
|
140
|
+
data: (data: Buffer | string, alias?: string) => void;
|
|
141
|
+
/** Operational error, emitted only when at least one error listener exists; missing listeners do not stop recovery. */
|
|
142
|
+
error: (err: Error) => void;
|
|
143
|
+
/** Readiness after opening, successful transmission and optional handshake. Emitted on each successful connection. */
|
|
144
|
+
connected: (info: {
|
|
145
|
+
/** Ready transport's actual OS path, which can change after USB rediscovery. */
|
|
146
|
+
path: string;
|
|
147
|
+
/** Configured connection speed in bits per second. */
|
|
148
|
+
baudRate: number;
|
|
149
|
+
}) => void;
|
|
150
|
+
/** Manual shutdown or unexpected loss; reasons currently emitted are Manual and Unexpected. */
|
|
151
|
+
disconnected: (reason: string) => void;
|
|
152
|
+
/** Scheduled retry: attempt is one-based and delay is milliseconds, not proof of an open attempt. */
|
|
153
|
+
reconnecting: (info: {
|
|
154
|
+
/** One-based scheduled retry number, excluding the initial connection attempt. */
|
|
155
|
+
attempt: number;
|
|
156
|
+
/** Capped delay in milliseconds before starting the scheduled retry. */
|
|
157
|
+
delay: number;
|
|
158
|
+
}) => void;
|
|
159
|
+
/** Retry budget exhausted; attempts excludes the initial connection attempt. Not emitted when autoReconnect is false. */
|
|
160
|
+
reconnectFailed: (info: {
|
|
161
|
+
/** Number of automatic retries consumed; excludes the initial attempt. */
|
|
162
|
+
attempts: number;
|
|
163
|
+
}) => void;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Internal bounded FIFO with response waiting and stale-callback isolation.
|
|
167
|
+
* Not exported from the package root; clients should use SerialService.send().
|
|
168
|
+
* The injected writer supplies physical-write deadlines; this class alone does
|
|
169
|
+
* not bound every write or validate its constructor options.
|
|
170
|
+
*/
|
|
171
|
+
declare class QueueManager {
|
|
172
|
+
private writeHandler;
|
|
173
|
+
private maxSize;
|
|
174
|
+
private responseTimeout;
|
|
175
|
+
private queue;
|
|
176
|
+
private active;
|
|
177
|
+
private _currentAlias?;
|
|
178
|
+
/**
|
|
179
|
+
* Creates the internal command queue.
|
|
180
|
+
* @param writeHandler - Writer that resolves after physical completion and rejects on failure.
|
|
181
|
+
* @param maxSize - Active plus pending command limit; defaults to 1000.
|
|
182
|
+
* @param responseTimeout - Default post-write response deadline in milliseconds; defaults to 5000.
|
|
183
|
+
* @remarks SerialService validates these constructor arguments before creating its queue.
|
|
184
|
+
*/
|
|
185
|
+
constructor(writeHandler: (data: Buffer | string) => Promise<void>, maxSize?: number, responseTimeout?: number);
|
|
186
|
+
/**
|
|
187
|
+
* Enqueues and starts a FIFO command, copying Buffer payloads.
|
|
188
|
+
* @param data - UTF-8 text or binary command.
|
|
189
|
+
* @param options - Alias, write/response timeout and optional response predicate.
|
|
190
|
+
* @returns Resolves without response data; rejects on capacity, invalid timeout,
|
|
191
|
+
* writer error, response timeout, predicate error or clear().
|
|
192
|
+
* @remarks A timed-out write remains active until its writer settles, preventing overlap.
|
|
193
|
+
*/
|
|
194
|
+
add(data: Buffer | string, options?: SendOptions): Promise<void>;
|
|
195
|
+
/** Active or most recently processed command alias; retained while idle and reset by clear(). */
|
|
196
|
+
get currentAlias(): string | undefined;
|
|
197
|
+
/**
|
|
198
|
+
* Rejects all unfinished commands, cancels their queue timers and resets alias/context.
|
|
199
|
+
* @param error - Shared rejection reason; defaults to Queue cleared due to disconnection.
|
|
200
|
+
* @remarks Does not cancel the injected writer. SerialService tears down the old transport;
|
|
201
|
+
* late writer callbacks cannot settle a new command. Reusing this queue directly requires
|
|
202
|
+
* ensuring the old physical writer cannot overlap a new session.
|
|
203
|
+
*/
|
|
204
|
+
clear(error?: Error): void;
|
|
205
|
+
/**
|
|
206
|
+
* Offers a received chunk/frame to the active waitResponse command.
|
|
207
|
+
* @param data - Input for responseMatcher; optional only for unfiltered response notification.
|
|
208
|
+
* @remarks Ignores unrelated/no-active responses. A predicate exception rejects the command.
|
|
209
|
+
* A matching early response is retained until writing completes; no payload is returned.
|
|
210
|
+
*/
|
|
211
|
+
notifyResponse(data?: Buffer | string): void;
|
|
212
|
+
/** Settles a still-current command and advances only after its writer has completed. */
|
|
213
|
+
private finish;
|
|
214
|
+
/** Starts at most one writer and installs the relevant queue/response deadline. */
|
|
215
|
+
private process;
|
|
216
|
+
/**
|
|
217
|
+
* Reports the absence of active and pending commands.
|
|
218
|
+
* @returns False for a timed-out command whose physical writer has not settled yet.
|
|
219
|
+
*/
|
|
220
|
+
isIdle(): boolean;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Owns one serial connection, its FIFO command queue and automatic recovery.
|
|
224
|
+
*
|
|
225
|
+
* @remarks
|
|
226
|
+
* Import from `serial-core`. Connection failures are reported through events;
|
|
227
|
+
* `connect()` starts an attempt and does not wait for readiness. Register a
|
|
228
|
+
* `connected` listener before starting. Pending commands reject on disconnect
|
|
229
|
+
* and are never replayed. Inherited EventEmitter listeners run synchronously.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```ts
|
|
233
|
+
* const serial = new SerialService({ path: '/dev/ttyUSB0', baudRate: 9600 });
|
|
234
|
+
* serial.on('error', error => console.error(error.message));
|
|
235
|
+
* serial.on('connected', () => {
|
|
236
|
+
* void serial.send('PING\n').catch(console.error);
|
|
237
|
+
* });
|
|
238
|
+
* await serial.connect();
|
|
239
|
+
* // Later: await serial.disconnect();
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
export declare class SerialService extends EventEmitter<{ [K in keyof SerialEvents]: Parameters<SerialEvents[K]>; }> {
|
|
243
|
+
/** Current transport; null while discovering or after cleanup. An open port alone does not imply handshake readiness. */
|
|
244
|
+
protected port: SerialPort | null;
|
|
245
|
+
/** Per-service FIFO; cleared on every disconnect or connection failure. Internal extension state. */
|
|
246
|
+
protected queue: QueueManager;
|
|
247
|
+
/** Current lifecycle state. Use setStatus() so transitions emit the status event. */
|
|
248
|
+
protected _status: SerialStatus;
|
|
249
|
+
/** Configuration snapshot with defaults; do not mutate during an active connection. */
|
|
250
|
+
protected config: SerialConfig;
|
|
251
|
+
/** Manual-stop flag that suppresses retries and invalidates late open callbacks. */
|
|
252
|
+
protected intentionalDisconnect: boolean;
|
|
253
|
+
/** At most one scheduled retry timer, cleared on manual stop or a new attempt. */
|
|
254
|
+
protected reconnectTimer: NodeJS.Timeout | null;
|
|
255
|
+
private generation;
|
|
256
|
+
private attempts;
|
|
257
|
+
private lockedPath;
|
|
258
|
+
private opening;
|
|
259
|
+
private closing;
|
|
260
|
+
/** Transport retained until closure succeeds, preserving its process reservation. */
|
|
261
|
+
private retiring;
|
|
262
|
+
private closeInProgress;
|
|
263
|
+
private detach;
|
|
264
|
+
private cancelHandshake;
|
|
265
|
+
/**
|
|
266
|
+
* Creates a service and validates selectors, deadlines, queue capacity and retry settings.
|
|
267
|
+
* @param config - Required baud rate plus a fixed path or discovery selector.
|
|
268
|
+
* @throws Error for invalid configuration; SyntaxError for an invalid pattern regex when no callback is used.
|
|
269
|
+
* @remarks autoConnect defaults to false. When true, connection starts in a microtask so listeners can be attached first.
|
|
270
|
+
* @example Vendor-only autodiscovery, with automatic rediscovery after disconnect.
|
|
271
|
+
* ```ts
|
|
272
|
+
* const serial = new SerialService({ baudRate: 9600, vendorId: '0403', autoConnect: true });
|
|
273
|
+
* serial.on('connected', ({ path }) => console.log('Ready:', path));
|
|
274
|
+
* serial.on('error', error => console.error(error.message));
|
|
275
|
+
* ```
|
|
276
|
+
* @example Product-only autodiscovery; omit path and vendorId.
|
|
277
|
+
* ```ts
|
|
278
|
+
* const serial = new SerialService({ baudRate: 115200, productId: '6001', autoConnect: true });
|
|
279
|
+
* ```
|
|
280
|
+
* @example Restrict discovery to both USB IDs; omit path to enable discovery.
|
|
281
|
+
* ```ts
|
|
282
|
+
* const serial = new SerialService({ baudRate: 115200, vendorId: '0403', productId: '6001' });
|
|
283
|
+
* serial.on('connected', ({ path }) => console.log(path));
|
|
284
|
+
* await serial.connect(); // Initiates discovery; connected reports readiness.
|
|
285
|
+
* ```
|
|
286
|
+
*/
|
|
287
|
+
constructor(config: SerialConfig);
|
|
288
|
+
/**
|
|
289
|
+
* Enumerates serial port metadata without opening any device.
|
|
290
|
+
* @param filter - Optional AND-combined selectors; defaults to all enumerated ports.
|
|
291
|
+
* @returns Matching metadata in driver enumeration order; an empty array means no match.
|
|
292
|
+
* @throws Rejects with the underlying OS/driver enumeration error.
|
|
293
|
+
* @remarks availableOnly checks this library's reservations, not OS-wide availability.
|
|
294
|
+
* @example `const ports = await SerialService.listPorts({ vendorId: '0403' });`
|
|
295
|
+
*/
|
|
296
|
+
static listPorts(filter?: PortFilter): Promise<SerialPortInfo[]>;
|
|
297
|
+
/**
|
|
298
|
+
* Finds all matching ports; equivalent to the static listPorts(filter).
|
|
299
|
+
* @param filter - Optional exact selectors, combined with AND.
|
|
300
|
+
* @returns Matching metadata; rejects on enumeration failure.
|
|
301
|
+
* @example `const devices = await SerialService.findPorts({ serialNumber: 'UNIT-1' });`
|
|
302
|
+
*/
|
|
303
|
+
static findPorts(filter?: PortFilter): Promise<SerialPortInfo[]>;
|
|
304
|
+
/**
|
|
305
|
+
* Instance convenience wrapper for SerialService.listPorts().
|
|
306
|
+
* @param filter - Enumeration filters; does not implicitly use this service's config.
|
|
307
|
+
* @returns Matching metadata; rejects on enumeration failure. Does not change connection state.
|
|
308
|
+
*/
|
|
309
|
+
listPorts(filter?: PortFilter): Promise<SerialPortInfo[]>;
|
|
310
|
+
/**
|
|
311
|
+
* Instance convenience wrapper for SerialService.findPorts().
|
|
312
|
+
* @param filter - Enumeration filters; does not implicitly use this service's config.
|
|
313
|
+
* @returns Matching metadata; rejects on enumeration failure. Does not reserve devices.
|
|
314
|
+
*/
|
|
315
|
+
findPorts(filter?: PortFilter): Promise<SerialPortInfo[]>;
|
|
316
|
+
/** Whether constructor-triggered connection was configured; does not report current connectivity. */
|
|
317
|
+
get autoConnect(): boolean;
|
|
318
|
+
/** Current lifecycle state; CONNECTED is required for send(), even when portIsOpen() returns true. */
|
|
319
|
+
get status(): SerialStatus;
|
|
320
|
+
/**
|
|
321
|
+
* Updates lifecycle state and emits status only when its value changes.
|
|
322
|
+
* @param status - New state; changing state alone does not open or close the transport.
|
|
323
|
+
* @remarks Protected extension hook; callers are responsible for preserving lifecycle invariants.
|
|
324
|
+
*/
|
|
325
|
+
protected setStatus(status: SerialStatus): void;
|
|
326
|
+
/**
|
|
327
|
+
* Starts a connection attempt or returns immediately if already scanning, connecting or connected.
|
|
328
|
+
* @returns Resolves after teardown/discovery and initiation of opening, not after readiness.
|
|
329
|
+
* @remarks
|
|
330
|
+
* Register connected/error listeners first. Operational failures are reported by events,
|
|
331
|
+
* with retries controlled by autoReconnect. An explicit inactive/recovery call resets the
|
|
332
|
+
* retry budget. Without a fixed path, every attempt rediscovers an unreserved match.
|
|
333
|
+
* Concurrent calls do not create duplicate attempts; they do not share a readiness promise.
|
|
334
|
+
*/
|
|
335
|
+
connect(): Promise<void>;
|
|
336
|
+
/** Runs one generation-guarded attempt without resetting the automatic retry budget. */
|
|
337
|
+
private startConnection;
|
|
338
|
+
/**
|
|
339
|
+
* Creates a transport, attaches owned listeners, opens it and starts optional verification.
|
|
340
|
+
* @param path - Already reserved OS path selected by connect().
|
|
341
|
+
* @remarks Protected implementation hook. Overrides must preserve ownership, cleanup,
|
|
342
|
+
* readiness events and late-callback guards; this is not a second public connection API.
|
|
343
|
+
*/
|
|
344
|
+
protected openPort(path: string): void;
|
|
345
|
+
/** Marks verified readiness, resets retry count and emits connected for the current transport. */
|
|
346
|
+
private markConnected;
|
|
347
|
+
/**
|
|
348
|
+
* Stops automatic recovery, invalidates pending attempts and rejects unfinished commands.
|
|
349
|
+
* @returns Resolves after pending open/close callbacks and transport closure.
|
|
350
|
+
* @throws Rejects if closure fails and the transport remains open; call disconnect() again to retry.
|
|
351
|
+
* @remarks
|
|
352
|
+
* Status becomes DISCONNECTED before physical closure finishes. OS open/close callbacks
|
|
353
|
+
* have no service deadline, so await completion during shutdown. A failed close retains
|
|
354
|
+
* its reservation. This instance can be reused with connect(); user listeners remain attached.
|
|
355
|
+
*/
|
|
356
|
+
disconnect(): Promise<void>;
|
|
357
|
+
/**
|
|
358
|
+
* Enqueues a command for FIFO transmission while CONNECTED.
|
|
359
|
+
* @param data - UTF-8 string or binary Buffer; Buffers are copied when enqueued.
|
|
360
|
+
* @param options - Alias, optional per-command timeout and response-wait settings.
|
|
361
|
+
* @returns Resolves with no payload after write/drain and, when requested, a matching response.
|
|
362
|
+
* Receive response payloads through the data event.
|
|
363
|
+
* @throws Rejects when not ready, over capacity, on invalid timeout, write/response failure,
|
|
364
|
+
* matcher exception or disconnection. A failed command may already have reached the device.
|
|
365
|
+
* @remarks No automatic replay. With waitResponse, timeout starts after write/drain;
|
|
366
|
+
* otherwise it limits writing. The physical writeTimeout always applies independently.
|
|
367
|
+
* @example `await serial.send('READ\n', { alias: 'read', waitResponse: true, timeout: 1000 });`
|
|
368
|
+
*/
|
|
369
|
+
send(data: string | Buffer, options?: SendOptions): Promise<void>;
|
|
370
|
+
/**
|
|
371
|
+
* Writes directly and waits for the transport drain callback, bypassing the FIFO.
|
|
372
|
+
* @param data - Raw command, also used by the handshake implementation.
|
|
373
|
+
* @returns Resolves after drain; rejects on closed transport, error or writeTimeout.
|
|
374
|
+
* @remarks Protected hook. Overrides must bound writes and preserve serialization;
|
|
375
|
+
* direct calls can interleave protocol traffic and are not a client API.
|
|
376
|
+
*/
|
|
377
|
+
protected performWrite(data: string | Buffer): Promise<void>;
|
|
378
|
+
/**
|
|
379
|
+
* Clears failed session resources, applies retry policy and reports an operational error.
|
|
380
|
+
* @param reason - Detail included in the emitted Connection failure error.
|
|
381
|
+
* @remarks Emits disconnected for an open/connected session; commands are rejected, not replayed.
|
|
382
|
+
*/
|
|
383
|
+
protected handleConnectionFailure(reason: string): void;
|
|
384
|
+
/** Emits the special error event only when observed, preserving recovery without a listener. */
|
|
385
|
+
private reportError;
|
|
386
|
+
/**
|
|
387
|
+
* Schedules at most one retry using capped exponential backoff.
|
|
388
|
+
* @remarks Honors intentionalDisconnect, autoReconnect and the remaining retry budget.
|
|
389
|
+
* Emits reconnecting for scheduling or reconnectFailed when the budget is exhausted.
|
|
390
|
+
* Automatic retries do not reset their own counter.
|
|
391
|
+
*/
|
|
392
|
+
protected scheduleReconnect(): void;
|
|
393
|
+
/**
|
|
394
|
+
* Cancels handshake, rejects the FIFO, detaches owned data listeners and starts teardown.
|
|
395
|
+
* @param error - Rejection reason for active/pending commands; defaults to a disconnection error.
|
|
396
|
+
* @remarks This void hook initiates asynchronous closure; disconnect() awaits the internal
|
|
397
|
+
* closing promise. It does not itself set status, schedule retries or remove client listeners.
|
|
398
|
+
*/
|
|
399
|
+
protected cleanup(error?: Error): void;
|
|
400
|
+
/** Closes or observes an already-closing transport; releases its reservation only after closure succeeds. */
|
|
401
|
+
private closeRetiringPort;
|
|
402
|
+
/**
|
|
403
|
+
* Sends the configured verification command and validates incoming data before readiness.
|
|
404
|
+
* @remarks No-op without an open port and handshake config. Regex matching accumulates
|
|
405
|
+
* bounded bytes; a callback receives individual chunks/frames. Both a match and successful
|
|
406
|
+
* transmission are required. Failure cancels listeners/timers and invokes recovery.
|
|
407
|
+
*/
|
|
408
|
+
protected performHandshake(): void;
|
|
409
|
+
/**
|
|
410
|
+
* Reports whether there are no active or pending commands.
|
|
411
|
+
* @returns True even when disconnected; this is not a connectivity or device-idle check.
|
|
412
|
+
* A rejected, still-draining write can keep the queue busy until transport completion.
|
|
413
|
+
*/
|
|
414
|
+
queueIsIdle(): boolean;
|
|
415
|
+
/**
|
|
416
|
+
* Reports the currently tracked transport's isOpen flag.
|
|
417
|
+
* @returns False when no current port exists. May be true during handshake;
|
|
418
|
+
* use status === SerialStatus.CONNECTED to decide whether send() is allowed.
|
|
419
|
+
*/
|
|
420
|
+
portIsOpen(): boolean;
|
|
421
|
+
/**
|
|
422
|
+
* Returns the currently tracked transport path, including during open/handshake.
|
|
423
|
+
* @returns The current path or null after cleanup/before creating a transport;
|
|
424
|
+
* does not return a configured or discovered path when no current transport exists.
|
|
425
|
+
*/
|
|
426
|
+
getPortPath(): string | null;
|
|
427
|
+
/**
|
|
428
|
+
* Returns local context for the active or most recently processed command.
|
|
429
|
+
* @returns Alias or null if absent/cleared. An idle queue may retain its last alias;
|
|
430
|
+
* unsolicited data may inherit it. It is not a device acknowledgement or request ID.
|
|
431
|
+
*/
|
|
432
|
+
getLastAlias(): string | null;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Optimized helpers for Buffer operations.
|
|
436
|
+
* Designed to minimize unnecessary copies and validations where possible.
|
|
437
|
+
*/
|
|
438
|
+
/**
|
|
439
|
+
* Efficiently converts various input types to Buffer.
|
|
440
|
+
* If input is already a Buffer, returns it as is (zero-copy).
|
|
441
|
+
* @param data - The data to convert (string, number array, Uint8Array, or Buffer)
|
|
442
|
+
* @returns The original Buffer, a shared-memory view for Uint8Array, or a new Buffer
|
|
443
|
+
* for strings and number arrays. Strings use UTF-8. Array conversion follows Buffer.from.
|
|
444
|
+
* @remarks Mutating a Buffer/Uint8Array view may mutate the original storage.
|
|
445
|
+
* Use Buffer.from(result) when an independent copy is required.
|
|
446
|
+
* @example `const command = toBuffer([0x01, 0x02, 0xff]);`
|
|
447
|
+
*/
|
|
448
|
+
export declare function toBuffer(data: string | number[] | Uint8Array | Buffer): Buffer;
|
|
449
|
+
/**
|
|
450
|
+
* Converts a Buffer to Uint8Array.
|
|
451
|
+
* In Node.js, Buffers ARE Uint8Arrays, so this is ideally zero-copy.
|
|
452
|
+
* @param buf - The buffer to convert
|
|
453
|
+
* @returns The exact same object, typed as Uint8Array; this does not copy memory.
|
|
454
|
+
* @example `const bytes = toUint8Array(Buffer.from([1, 2]));`
|
|
455
|
+
*/
|
|
456
|
+
export declare function toUint8Array(buf: Buffer): Uint8Array;
|
|
457
|
+
/**
|
|
458
|
+
* Converts a Buffer to string with optional encoding (default utf8).
|
|
459
|
+
* Wrapper alias for toString().
|
|
460
|
+
* @param buf - The buffer to convert
|
|
461
|
+
* @param encoding - The character encoding to use (default: 'utf8')
|
|
462
|
+
* @returns Decoded text using Node Buffer.toString semantics; decoding does not modify buf.
|
|
463
|
+
* @example `const hex = toString(Buffer.from([0xff, 0x01]), 'hex'); // 'ff01'`
|
|
464
|
+
*/
|
|
465
|
+
export declare function toString(buf: Buffer, encoding?: BufferEncoding): string;
|
|
466
|
+
/**
|
|
467
|
+
* Converts a Buffer to ASCII string representation.
|
|
468
|
+
* @param buf - The buffer to convert
|
|
469
|
+
* @returns ASCII-decoded text using Buffer.toString('ascii'), which masks the high bit.
|
|
470
|
+
* @example `const text = toAscii(Buffer.from([0x4f, 0x4b])); // 'OK'`
|
|
471
|
+
*/
|
|
472
|
+
export declare function toAscii(buf: Buffer): string;
|
|
473
|
+
/**
|
|
474
|
+
* Converts a Buffer to an array of byte values.
|
|
475
|
+
* Note: Using spread operator may be slow for very large buffers.
|
|
476
|
+
* @param buf - The buffer to convert
|
|
477
|
+
* @returns A new independent number array containing byte values 0 through 255.
|
|
478
|
+
* @example `const bytes = toArray(Buffer.from([0x00, 0xff])); // [0, 255]`
|
|
479
|
+
*/
|
|
480
|
+
export declare function toArray(buf: Buffer): number[];
|
|
481
|
+
/**
|
|
482
|
+
* Stateless discovery API exported from `serial-core`.
|
|
483
|
+
* Enumeration never opens or reserves a device. Filters are exact and AND-combined;
|
|
484
|
+
* order and optional metadata come from the OS/driver and are not sorted here.
|
|
485
|
+
*/
|
|
486
|
+
export declare class PortScanner {
|
|
487
|
+
/**
|
|
488
|
+
* Lists port metadata matching optional selectors.
|
|
489
|
+
* @param filter - Defaults to all ports. VID/PID normalize case, whitespace, 0x and leading zeros.
|
|
490
|
+
* Other string fields are exact and case-sensitive; missing metadata cannot match a supplied selector.
|
|
491
|
+
* @returns Metadata in enumeration order, or an empty array if nothing matches.
|
|
492
|
+
* @throws Rejects on serialport.list() / OS / driver errors; does not swallow or log them.
|
|
493
|
+
* @remarks availableOnly filters this loaded library's reservations, not other applications' locks.
|
|
494
|
+
* @example `const ports = await PortScanner.listPorts({ availableOnly: true });`
|
|
495
|
+
*/
|
|
496
|
+
static listPorts(filter?: PortFilter): Promise<SerialPortInfo[]>;
|
|
497
|
+
/**
|
|
498
|
+
* Finds all matching devices; equivalent to listPorts(filter).
|
|
499
|
+
* @param filter - Optional exact AND-combined metadata selectors.
|
|
500
|
+
* @returns Matching metadata; rejects on enumeration failure. Does not reserve the result.
|
|
501
|
+
* @example `const units = await PortScanner.findPorts({ vendorId: '0403', serialNumber: 'A' });`
|
|
502
|
+
*/
|
|
503
|
+
static findPorts(filter?: PortFilter): Promise<SerialPortInfo[]>;
|
|
504
|
+
/**
|
|
505
|
+
* Convenience helper returning the first matching path instead of full metadata.
|
|
506
|
+
* @param vendorId - Optional vendor ID; required when productId is absent.
|
|
507
|
+
* @param productId - Optional product ID; may be supplied without vendorId.
|
|
508
|
+
* @returns First matching OS path or null for no match; ordering is driver-dependent.
|
|
509
|
+
* @throws Rejects if both IDs are absent/empty or enumeration fails.
|
|
510
|
+
* @remarks Does not exclude reserved ports. Use findPorts({ availableOnly: true }) for that.
|
|
511
|
+
* @example `const path = await PortScanner.findPort('0x0403', '6001');`
|
|
512
|
+
*/
|
|
513
|
+
static findPort(vendorId?: string, productId?: string): Promise<string | null>;
|
|
514
|
+
}
|
|
515
|
+
export type { PortFilter, SendOptions, SerialConfig, SerialEvents, SerialPortInfo };
|
package/dist/index.js
CHANGED
|
@@ -1 +1,10 @@
|
|
|
1
|
-
import{SerialPort as e}from"serialport";import{EventEmitter as t}from"events";const n={DISCONNECTED:`DISCONNECTED`,SCANNING:`SCANNING`,CONNECTING:`CONNECTING`,CONNECTED:`CONNECTED`,RECONNECTING:`RECONNECTING`};var r=class{queue=[];isProcessing=!1;_currentAlias=void 0;waitingForResponse=!1;currentTimeoutTimer=null;currentItem=null;writeHandler;constructor(e){this.writeHandler=e}add(e,t){return new Promise((n,r)=>{this.queue.push({data:e,resolve:n,reject:r,alias:t?.alias,timeout:t?.timeout,waitResponse:t?.waitResponse}),this.process()})}get currentAlias(){return this._currentAlias}clear(){this.queue.forEach(e=>e.reject(Error(`Queue cleared due to disconnection`))),this.queue=[],this.waitingForResponse&&this.currentItem&&(this.currentTimeoutTimer&&clearTimeout(this.currentTimeoutTimer),this.currentItem.reject(Error(`Queue cleared due to disconnection`))),this.isProcessing=!1,this.waitingForResponse=!1,this.currentItem=null,this.currentTimeoutTimer=null}notifyResponse(){this.waitingForResponse&&this.currentItem&&(this.currentTimeoutTimer&&clearTimeout(this.currentTimeoutTimer),this.waitingForResponse=!1,this.currentTimeoutTimer=null,this.currentItem.resolve(),this.currentItem=null,this.isProcessing=!1,this.process())}async process(){if(this.isProcessing||this.waitingForResponse||this.queue.length===0)return;this.isProcessing=!0;let e=this.queue.shift();if(!e){this.isProcessing=!1;return}this.currentItem=e,this._currentAlias=e.alias;let t=null,n=!1;try{if(!e.waitResponse&&e.timeout&&e.timeout>0&&(t=setTimeout(()=>{n=!0,e.reject(Error(`Write timeout after ${e.timeout}ms`)),this.isProcessing=!1,this.currentItem=null,this.process()},e.timeout)),await this.writeHandler(e.data),n)return;t&&clearTimeout(t),e.waitResponse?(this.waitingForResponse=!0,e.timeout&&e.timeout>0&&(this.currentTimeoutTimer=setTimeout(()=>{this.handleResponseTimeout(e)},e.timeout)),this.isProcessing=!1):(e.resolve(),this.currentItem=null,this.isProcessing=!1,this.process())}catch(r){n||(t&&clearTimeout(t),e.reject(r instanceof Error?r:Error(String(r))),this.waitingForResponse=!1,this.currentItem=null,this.isProcessing=!1,this.process())}}handleResponseTimeout(e){this.waitingForResponse=!1,this.currentTimeoutTimer=null,this.currentItem=null,e.reject(Error(`Response timeout after ${e.timeout}ms`)),this.process()}isIdle(){return this.queue.length===0&&!this.isProcessing&&!this.waitingForResponse}},i=class{static async findPort(t,n){if(!t&&!n)throw Error(`VendorID or ProductID is required for automatic scanning.`);try{let r=await e.list(),i=r.find(e=>{let r=e.vendorId?.toLowerCase()||``,i=e.productId?.toLowerCase()||``,a=(t||``).toLowerCase().replace(`0x`,``),o=(n||``).toLowerCase().replace(`0x`,``),s=t?r.includes(a):!0,c=n?i.includes(o):!0;return s&&c});return i?i.path:null}catch(e){return console.error(`Error scanning ports:`,e),null}}},a=class e{static instance;lockedPorts=new Set;constructor(){}static getInstance(){return e.instance||=new e,e.instance}register(e){return this.lockedPorts.has(e)?!1:(this.lockedPorts.add(e),!0)}unregister(e){this.lockedPorts.delete(e)}isLocked(e){return this.lockedPorts.has(e)}},o=class extends t{port=null;queue;_status=n.DISCONNECTED;config;intentionalDisconnect=!1;reconnectTimer=null;constructor(e){super(),this.config=e,this.queue=new r(async e=>this.performWrite(e)),this.config.autoConnect&&this.connect()}get autoConnect(){return this.config.autoConnect}get status(){return this._status}setStatus(e){this._status!==e&&(this._status=e,this.emit(`status`,e))}async connect(){if(this._status===n.CONNECTED||this._status===n.CONNECTING)return;this.intentionalDisconnect=!1,this.setStatus(n.SCANNING);let e=this.config.path;if(!e)try{let t=await i.findPort(this.config.vendorId,this.config.productId);if(t)e=t;else{this.handleConnectionFailure(`Device not found in scan`);return}}catch(e){this.handleConnectionFailure(`Error scanning: ${e}`);return}if(a.getInstance().isLocked(e)){this.handleConnectionFailure(`Port ${e} occupied by another internal instance`);return}if(!a.getInstance().register(e)){this.handleConnectionFailure(`Could not lock port ${e}`);return}this.setStatus(n.CONNECTING),this.openPort(e)}openPort(t){if(this.port=new e({path:t,baudRate:this.config.baudRate,autoOpen:!1}),this.port.open(e=>{if(e){this.handleConnectionFailure(e.message);return}this.config.handshake?this.performHandshake():(this.setStatus(n.CONNECTED),this.emit(`connected`,{path:t,baudRate:this.config.baudRate}))}),this.config.parser){let e=this.port.pipe(this.config.parser);e.on(`data`,e=>{this.queue.notifyResponse(),this.emit(`data`,e,this.queue.currentAlias)}),e.on(`error`,e=>this.emit(`error`,e))}else this.port.on(`data`,e=>{this.queue.notifyResponse(),this.emit(`data`,e,this.queue.currentAlias)});this.port.on(`error`,e=>{this.emit(`error`,e)}),this.port.on(`close`,()=>{this.cleanup(),this.emit(`disconnected`,this.intentionalDisconnect?`Manual`:`Unexpected`),this.intentionalDisconnect?this.setStatus(n.DISCONNECTED):(this.setStatus(n.RECONNECTING),this.scheduleReconnect())})}async disconnect(){if(this.intentionalDisconnect=!0,this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.port&&this.port.isOpen)return new Promise(e=>{this.port?.close(()=>e())})}send(e,t){return this._status===n.CONNECTED?this.queue.add(e,t):Promise.reject(Error(`Port not connected`))}performWrite(e){return new Promise((t,n)=>{if(!this.port||!this.port.isOpen)return n(Error(`Port closed during write`));let r=!this.port.write(e,e=>{if(e)return n(e);r||t()});r&&this.port.once(`drain`,t)})}handleConnectionFailure(e){this.emit(`error`,Error(`Connection failure: ${e}`)),this.cleanup(),this.intentionalDisconnect||(this.setStatus(n.RECONNECTING),this.scheduleReconnect())}scheduleReconnect(){this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.reconnectTimer=setTimeout(()=>{this.connect()},this.config.reconnectInterval)}cleanup(){this.port&&this.port.path&&a.getInstance().unregister(this.port.path),this.port&&(this.port.removeAllListeners(),this.port=null),this.config.parser&&this.config.parser.removeAllListeners(),this.intentionalDisconnect&&this.queue.clear()}performHandshake(){if(!this.port||!this.port.isOpen||!this.config.handshake)return;let{command:e,pattern:t,timeout:r,hexPattern:i,callback:a}=this.config.handshake,o,s=e=>{if(a){let t;t=Buffer.isBuffer(e)||typeof e==`string`?e:Buffer.from(String(e)),a(t)&&(c(),this.setStatus(n.CONNECTED),this.emit(`connected`,{path:this.port.path,baudRate:this.config.baudRate}));return}let r=``;if(Buffer.isBuffer(e))r=i?e.toString(`hex`):e.toString();else if(typeof e==`string`)r=i?Buffer.from(e).toString(`hex`):e;else{let t=Buffer.from(String(e));r=i?t.toString(`hex`):t.toString()}let o=typeof t==`string`?new RegExp(t):t;o.test(r)&&(c(),this.setStatus(n.CONNECTED),this.emit(`connected`,{path:this.port.path,baudRate:this.config.baudRate}))},c=()=>{clearTimeout(o),this.removeListener(`data`,l)},l=e=>s(e);this.on(`data`,l),o=setTimeout(()=>{c(),this.handleConnectionFailure(`Handshake timeout (pattern: ${t})`)},r),this.performWrite(e).catch(e=>{c(),this.handleConnectionFailure(`Error writing handshake: ${e.message}`)})}queueIsIdle(){return this.queue.isIdle()}portIsOpen(){return this.port?.isOpen??!1}getPortPath(){return this.port?.path??null}getLastAlias(){return this.queue.currentAlias??null}};function s(e){return Buffer.isBuffer(e)?e:e instanceof Uint8Array?Buffer.from(e.buffer,e.byteOffset,e.byteLength):(Array.isArray(e),Buffer.from(e))}function c(e){return e}function l(e,t=`utf8`){return e.toString(t)}function u(e){return e.toString(`ascii`)}function d(e){return[...e]}export{i as PortScanner,o as SerialService,n as SerialStatus,d as toArray,u as toAscii,s as toBuffer,l as toString,c as toUint8Array};
|
|
1
|
+
import{SerialPort as e}from"serialport";import{EventEmitter as t}from"events";const n={DISCONNECTED:`DISCONNECTED`,SCANNING:`SCANNING`,CONNECTING:`CONNECTING`,CONNECTED:`CONNECTED`,RECONNECTING:`RECONNECTING`};var r=class{writeHandler;maxSize;responseTimeout;queue=[];active=null;_currentAlias;constructor(e,t=1e3,n=5e3){this.writeHandler=e,this.maxSize=t,this.responseTimeout=n}add(e,t={}){return t.timeout!==void 0&&(!Number.isFinite(t.timeout)||t.timeout<=0||t.timeout>2147483647)?Promise.reject(/* @__PURE__ */ Error(`timeout must be a positive finite number within the timer limit`)):this.queue.length+ +!!this.active>=this.maxSize?Promise.reject(/* @__PURE__ */ Error(`Serial queue capacity exceeded`)):new Promise((n,r)=>{this.queue.push({data:Buffer.isBuffer(e)?Buffer.from(e):e,...t,resolve:n,reject:r}),this.process()})}get currentAlias(){return this._currentAlias}clear(e=/* @__PURE__ */ Error(`Queue cleared due to disconnection`)){let t=this.queue.splice(0),n=this.active;this.active=null,this._currentAlias=void 0,n&&(clearTimeout(n.timer),n.settled||n.item.reject(e),n.settled=!0),t.forEach(t=>t.reject(e))}notifyResponse(e){let t=this.active;if(t&&t.item.waitResponse&&!t.settled)try{if(t.item.responseMatcher&&(e===void 0||!t.item.responseMatcher(e)))return;t.responded=!0,t.writing||this.finish(t)}catch(e){this.finish(t,e instanceof Error?e:Error(String(e)))}}finish(e,t){this.active!==e||e.settled||(clearTimeout(e.timer),e.settled=!0,t?e.item.reject(t):e.item.resolve(),e.writing||(this.active=null,this.process()))}process(){if(this.active||!this.queue.length)return;let e=this.queue.shift(),t={item:e,writing:!0,responded:!1,settled:!1};this.active=t,this._currentAlias=e.alias,!e.waitResponse&&e.timeout&&(t.timer=setTimeout(()=>this.finish(t,/* @__PURE__ */ Error(`Write timeout after ${e.timeout}ms`)),e.timeout)),(async()=>{try{if(await this.writeHandler(e.data),this.active!==t)return;if(t.writing=!1,t.settled)this.active=null,this.process();else if(!e.waitResponse||t.responded)this.finish(t);else{let n=e.timeout??this.responseTimeout;t.timer=setTimeout(()=>this.finish(t,/* @__PURE__ */ Error(`Response timeout after ${n}ms`)),n)}}catch(e){if(this.active!==t)return;t.writing=!1,t.settled?(this.active=null,this.process()):this.finish(t,e instanceof Error?e:Error(String(e)))}})()}isIdle(){return!this.active&&this.queue.length===0}},i=class e{static instance;lockedPorts=/* @__PURE__ */ new Set;constructor(){}static getInstance(){return e.instance||=new e,e.instance}register(e){return!this.lockedPorts.has(e)&&(this.lockedPorts.add(e),!0)}unregister(e){this.lockedPorts.delete(e)}isLocked(e){return this.lockedPorts.has(e)}};function a(e){return e.trim().toLowerCase().replace(/^0x/,``).replace(/^0+(?=.)/,``)}var o=class{static async listPorts(t={}){return(await e.list()).filter(e=>{for(let n of[`vendorId`,`productId`])if(t[n]!==void 0&&(e[n]===void 0||a(e[n])!==a(t[n])))return!1;for(let n of[`path`,`serialNumber`,`manufacturer`])if(t[n]!==void 0&&e[n]!==t[n])return!1;return!t.availableOnly||!i.getInstance().isLocked(e.path)})}static async findPorts(e={}){return this.listPorts(e)}static async findPort(e,t){if(!e&&!t)throw Error(`VendorID or ProductID is required for automatic scanning.`);return(await this.listPorts({vendorId:e,productId:t}))[0]?.path??null}};const s=e=>e instanceof Error?e:Error(String(e));var c=class a extends t{port=null;queue;_status=n.DISCONNECTED;config;intentionalDisconnect=!1;reconnectTimer=null;generation=0;attempts=0;lockedPath=null;opening=null;closing=Promise.resolve();retiring=null;closeInProgress=!1;detach=null;cancelHandshake=null;constructor(e){super(),this.config={autoConnect:!1,autoReconnect:!0,reconnectInterval:2e3,reconnectBackoffFactor:1,maxReconnectAttempts:1/0,writeTimeout:5e3,responseTimeout:5e3,maxQueueSize:1e3,...e,maxReconnectInterval:e.maxReconnectInterval??Math.max(3e4,e.reconnectInterval??2e3)};for(let e of[`baudRate`,`reconnectInterval`,`maxReconnectInterval`,`writeTimeout`,`responseTimeout`,`maxQueueSize`]){let t=this.config[e];if(!Number.isFinite(t)||t<=0)throw Error(`${e} must be a positive finite number`);if(e!==`baudRate`&&e!==`maxQueueSize`&&t>2147483647)throw Error(`${e} exceeds the timer limit`)}if(!Number.isInteger(this.config.maxQueueSize))throw Error(`maxQueueSize must be an integer`);let t=this.config.reconnectBackoffFactor;if(!Number.isFinite(t)||t<1)throw Error(`reconnectBackoffFactor must be at least 1`);let n=this.config.maxReconnectAttempts;if(n!==1/0&&(!Number.isInteger(n)||n<0))throw Error(`maxReconnectAttempts must be a non-negative integer or Infinity`);if(this.config.maxReconnectInterval<this.config.reconnectInterval)throw Error(`maxReconnectInterval must be >= reconnectInterval`);if(!e.path&&!e.vendorId&&!e.productId&&!e.serialNumber)throw Error(`A path, vendorId, productId or serialNumber is required`);if(e.parser&&e.parserFactory)throw Error(`Use parser or parserFactory, not both`);if(e.handshake){if(!Number.isFinite(e.handshake.timeout)||e.handshake.timeout<=0||e.handshake.timeout>2147483647)throw Error(`handshake.timeout must be positive, finite and within the timer limit`);if(e.handshake.maxBufferSize!==void 0&&(!Number.isInteger(e.handshake.maxBufferSize)||e.handshake.maxBufferSize<=0))throw Error(`handshake.maxBufferSize must be a positive integer`);e.handshake.callback||new RegExp(e.handshake.pattern)}this.queue=new r(e=>this.performWrite(e),this.config.maxQueueSize,this.config.responseTimeout),this.config.autoConnect&&queueMicrotask(()=>{this.intentionalDisconnect||this.connect()})}static listPorts(e={}){return o.listPorts(e)}static findPorts(e={}){return o.findPorts(e)}listPorts(e={}){return a.listPorts(e)}findPorts(e={}){return a.findPorts(e)}get autoConnect(){return this.config.autoConnect??!1}get status(){return this._status}setStatus(e){this._status!==e&&(this._status=e,this.emit(`status`,e))}async connect(){[n.SCANNING,n.CONNECTING,n.CONNECTED].some(e=>e===this._status)||(this.attempts=0),await this.startConnection()}async startConnection(){if([n.SCANNING,n.CONNECTING,n.CONNECTED].some(e=>e===this._status))return;this.intentionalDisconnect=!1,this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.reconnectTimer=null;let e=++this.generation;if(this.setStatus(n.SCANNING),await this.closing,!(e!==this.generation||this.intentionalDisconnect))try{if(this.retiring){if(this.cleanup(),await this.closing,e!==this.generation||this.intentionalDisconnect)return;if(this.retiring)throw Error(`Previous serial port could not be closed`)}let t=this.config.path;if(!t){let n=await o.findPorts({vendorId:this.config.vendorId,productId:this.config.productId,serialNumber:this.config.serialNumber,availableOnly:!0});if(e!==this.generation||this.intentionalDisconnect)return;t=n[0]?.path}if(!t)throw Error(`Device not found in scan`);if(!i.getInstance().register(t))throw Error(`Port ${t} occupied by another internal instance`);if(this.lockedPath=t,this.setStatus(n.CONNECTING),e!==this.generation||this.intentionalDisconnect)return;this.openPort(t)}catch(t){e===this.generation&&!this.intentionalDisconnect&&this.handleConnectionFailure(s(t).message)}}openPort(t){let r=new e({path:t,baudRate:this.config.baudRate,autoOpen:!1});this.port=r;let a=this.config.parserFactory?.()??this.config.parser,o=a??r,c=e=>{if(this.port!==r)return;let t=this.queue.currentAlias;this.queue.notifyResponse(e),this.emit(`data`,e,t)},l=e=>{this.port===r&&this.handleConnectionFailure(e.message)},u=()=>{this.port===r&&(this.cleanup(),this.intentionalDisconnect?this.setStatus(n.DISCONNECTED):this.scheduleReconnect(),this.emit(`disconnected`,this.intentionalDisconnect?`Manual`:`Unexpected`))};o.on(`data`,c),a&&(a.on(`error`,l),r.pipe(a,{end:!1})),r.on(`error`,l),r.on(`close`,u),this.detach=()=>{o.removeListener(`data`,c),r.removeListener(`close`,u),a&&(r.unpipe(a),a.removeListener(`error`,l))};let d;this.opening=new Promise(e=>{d=e});let f=e=>{this.port!==r||this.intentionalDisconnect?!e&&r.isOpen?this.closeRetiringPort(r,t).then(d):(i.getInstance().unregister(t),d()):(this.opening=null,d(),e?this.handleConnectionFailure(e.message):this.config.handshake?this.performHandshake():this.markConnected())};try{r.open(f)}catch(e){f(s(e))}}markConnected(){this.port&&!this.intentionalDisconnect&&(this.attempts=0,this.setStatus(n.CONNECTED),this.port&&!this.intentionalDisconnect&&this.emit(`connected`,{path:this.port.path,baudRate:this.config.baudRate}))}async disconnect(){this.intentionalDisconnect=!0,++this.generation,this.attempts=0,this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.reconnectTimer=null;let e=this.port!==null;if(this.cleanup(),this.setStatus(n.DISCONNECTED),await this.closing,this.retiring)throw Error(`Serial port could not be closed; call disconnect() again to retry`);e&&this.emit(`disconnected`,`Manual`)}send(e,t){return this._status===n.CONNECTED?this.queue.add(e,t):Promise.reject(/* @__PURE__ */ Error(`Port not connected`))}performWrite(e){let t=this.port;return t?.isOpen?new Promise((n,r)=>{let i=!1,a=(e,a=!1)=>{i||(i=!0,clearTimeout(l),t.removeListener(`close`,o),t.removeListener(`error`,c),e?r(e):n(),e&&a&&this.port===t&&this.handleConnectionFailure(e.message))},o=()=>a(/* @__PURE__ */ Error(`Port closed during write`)),c=e=>a(e),l=setTimeout(()=>{let e=/* @__PURE__ */ Error(`Write timeout after ${this.config.writeTimeout}ms`);a(e,!0)},this.config.writeTimeout);t.once(`close`,o),t.once(`error`,c);try{t.write(e,e=>{if(!i){if(e)a(e,!0);else try{t.drain(e=>a(e,!0))}catch(e){a(s(e),!0)}}})}catch(e){a(s(e),!0)}}):Promise.reject(/* @__PURE__ */ Error(`Port closed during write`))}handleConnectionFailure(e){let t=this.port?.isOpen||this._status===n.CONNECTED,r=/* @__PURE__ */ Error(`Connection failure: ${e}`);this.cleanup(r),this.intentionalDisconnect||this.scheduleReconnect(),t&&this.emit(`disconnected`,`Unexpected`),this.reportError(r)}reportError(e){this.listenerCount(`error`)&&this.emit(`error`,e)}scheduleReconnect(){if(this.intentionalDisconnect||this.reconnectTimer)return;if(!this.config.autoReconnect||this.attempts>=this.config.maxReconnectAttempts){this.setStatus(n.DISCONNECTED),this.config.autoReconnect&&this.emit(`reconnectFailed`,{attempts:this.attempts});return}let e=Math.min(this.config.maxReconnectInterval,this.config.reconnectInterval*this.config.reconnectBackoffFactor**this.attempts);++this.attempts,this.reconnectTimer=setTimeout(()=>{this.reconnectTimer=null,this.startConnection()},e),this.setStatus(n.RECONNECTING),this.intentionalDisconnect||this.emit(`reconnecting`,{attempt:this.attempts,delay:e})}cleanup(e){if(this.cancelHandshake?.(),this.cancelHandshake=null,this.queue.clear(e),this.detach?.(),this.detach=null,this.closeInProgress)return;let t=this.port??this.retiring?.port,n=this.lockedPath??this.retiring?.path;this.port=null,this.lockedPath=null,this.opening?(this.closing=this.opening,this.opening=null):t&&(t.isOpen||t.closing)?this.closing=this.closeRetiringPort(t,n??null):n&&(i.getInstance().unregister(n),this.retiring=null)}closeRetiringPort(e,t){return this.retiring={port:e,path:t},this.closeInProgress=!0,new Promise(n=>{let r=r=>{this.closeInProgress=!1,e.removeListener?.(`close`,a),e.removeListener?.(`error`,o),r&&e.isOpen||(t&&i.getInstance().unregister(t),this.retiring?.port===e&&(this.retiring=null)),n(),r&&this.reportError(r)},a=()=>r(),o=e=>r(e);try{e.closing?(e.once(`close`,a),e.once(`error`,o)):e.close(r)}catch(e){r(s(e))}})}performHandshake(){if(!this.port?.isOpen||!this.config.handshake)return;let e=this.port,{command:t,pattern:n,timeout:r,hexPattern:i,callback:a,maxBufferSize:o=65536}=this.config.handshake,c=a?null:new RegExp(n),l=Buffer.alloc(0),u=!0,d=!1,f=!1,p=()=>{u=!1,clearTimeout(_),this.removeListener(`data`,g),this.cancelHandshake=null},m=t=>{u&&(p(),this.port===e&&this.handleConnectionFailure(t))},h=()=>{u&&d&&f&&this.port===e&&(p(),this.markConnected())},g=t=>{if(u&&this.port===e)try{let e;if(a)e=a(t);else{let n=Buffer.isBuffer(t)?t:Buffer.from(t);if(l.length+n.length>o){m(`Handshake buffer limit exceeded`);return}l=Buffer.concat([l,n]),c.lastIndex=0,e=c.test(l.toString(i?`hex`:`utf8`))}e&&(f=!0,h())}catch(e){m(`Handshake validation failed: ${s(e).message}`)}},_=setTimeout(()=>m(`Handshake timeout (pattern: ${n})`),r);this.cancelHandshake=p,this.on(`data`,g),this.performWrite(t).then(()=>{d=!0,h()}).catch(e=>m(`Error writing handshake: ${s(e).message}`))}queueIsIdle(){return this.queue.isIdle()}portIsOpen(){return this.port?.isOpen??!1}getPortPath(){return this.port?.path??null}getLastAlias(){return this.queue.currentAlias??null}};function l(e){return Buffer.isBuffer(e)?e:e instanceof Uint8Array?Buffer.from(e.buffer,e.byteOffset,e.byteLength):Buffer.from(e)}function u(e){return e}function d(e,t=`utf8`){return e.toString(t)}function f(e){return e.toString(`ascii`)}function p(e){return[...e]}
|
|
2
|
+
/**
|
|
3
|
+
* @license GPL-3.0 serial-core
|
|
4
|
+
*
|
|
5
|
+
* Created by (c) Danidoble.
|
|
6
|
+
*
|
|
7
|
+
* This source code is licensed under the GPL-3.0 license found in the
|
|
8
|
+
* LICENSE file in the root directory of this source tree.
|
|
9
|
+
*/
|
|
10
|
+
export{o as PortScanner,c as SerialService,n as SerialStatus,p as toArray,f as toAscii,l as toBuffer,d as toString,u as toUint8Array};
|