serial-core 0.2.0-dev.4 → 0.2.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 CHANGED
@@ -91,6 +91,94 @@ async function sendCommand() {
91
91
  // serial.disconnect();
92
92
  ```
93
93
 
94
+ ## Handshake
95
+
96
+ The handshake feature allows you to verify the device identity and connection before considering the port as fully connected. This is useful when you need to confirm that the connected device is the expected one.
97
+
98
+ ### Basic Handshake Configuration
99
+
100
+ ```typescript
101
+ const serial = new SerialService({
102
+ path: '/dev/ttyUSB0',
103
+ baudRate: 9600,
104
+ autoConnect: true,
105
+ reconnectInterval: 5000,
106
+ handshake: {
107
+ command: 'AT\r\n', // Command to send to the device
108
+ pattern: /OK/, // Expected response pattern (string or RegExp)
109
+ timeout: 2000 // Maximum time to wait for response (ms)
110
+ }
111
+ });
112
+ ```
113
+
114
+ ### Handshake with Binary Data (Hex Comparison)
115
+
116
+ When working with devices that respond with binary data or non-printable characters, you can use the `hexPattern` option to compare responses in hexadecimal format:
117
+
118
+ ```typescript
119
+ const connectionCommand = Buffer.from([0x01, 0x02, 0x03, 0x04]);
120
+
121
+ const serial = new SerialService({
122
+ path: '/dev/ttyUSB0',
123
+ baudRate: 9600,
124
+ autoConnect: true,
125
+ reconnectInterval: 5000,
126
+ handshake: {
127
+ command: connectionCommand,
128
+ pattern: connectionCommand.toString('hex'), // Compare in hex format
129
+ timeout: 3000,
130
+ hexPattern: true // Enable hex comparison
131
+ }
132
+ });
133
+ ```
134
+
135
+ ### Handshake Configuration Options
136
+
137
+ | Property | Type | Required | Description |
138
+ |----------|------|----------|-------------|
139
+ | `command` | `string \| Buffer` | Yes | Command to send to the device upon connection. |
140
+ | `pattern` | `string \| RegExp` | Yes | Expected pattern in the device response. |
141
+ | `timeout` | `number` | Yes | Maximum time (in ms) to wait for the handshake response. |
142
+ | `hexPattern` | `boolean` | No | If `true`, received data will be converted to hex before pattern matching. Useful for binary protocols. |
143
+
144
+ ### How Handshake Works
145
+
146
+ 1. The service opens the serial port
147
+ 2. Sends the handshake `command` to the device
148
+ 3. Waits for a response that matches the `pattern`
149
+ 4. If the pattern matches within the `timeout` period, the connection is considered successful
150
+ 5. If no match or timeout occurs, the connection fails and a reconnection attempt is scheduled
151
+
152
+ ### Handshake Examples
153
+
154
+ **Text-based handshake:**
155
+ ```typescript
156
+ handshake: {
157
+ command: 'PING\n',
158
+ pattern: 'PONG',
159
+ timeout: 1000
160
+ }
161
+ ```
162
+
163
+ **RegExp pattern for version checking:**
164
+ ```typescript
165
+ handshake: {
166
+ command: 'VERSION\r\n',
167
+ pattern: /v\d+\.\d+\.\d+/, // Matches v1.2.3 format
168
+ timeout: 2000
169
+ }
170
+ ```
171
+
172
+ **Binary protocol with hex comparison:**
173
+ ```typescript
174
+ handshake: {
175
+ command: Buffer.from([0xFF, 0x01, 0x00]),
176
+ pattern: 'ff0100', // Expected response in hex
177
+ timeout: 3000,
178
+ hexPattern: true
179
+ }
180
+ ```
181
+
94
182
  ## API Reference
95
183
 
96
184
  ### `SerialService`
@@ -107,7 +195,7 @@ The main class for managing the serial connection.
107
195
  | `baudRate` | `number` | Serial baud rate (default: `9600`). |
108
196
  | `autoConnect` | `boolean` | Whether to connect automatically on instantiation. |
109
197
  | `reconnectInterval` | `number` | Time in ms to wait before retrying connection. |
110
- | `handshake?` | `object` | Optional handshake configuration to verify device identity. |
198
+ | `handshake?` | `object` | Optional handshake configuration to verify device identity. See [Handshake](#handshake) section for details. |
111
199
 
112
200
  #### Methods
113
201
 
@@ -14,28 +14,51 @@ export declare class SerialService extends EventEmitter {
14
14
  protected config: SerialConfig;
15
15
  protected intentionalDisconnect: boolean;
16
16
  protected reconnectTimer: NodeJS.Timeout | null;
17
+ /**
18
+ * Initializes the SerialService with the provided configuration.
19
+ * @param config - Configuration object for serial connection
20
+ */
17
21
  constructor(config: SerialConfig);
22
+ /**
23
+ * Gets the autoConnect configuration value.
24
+ * @returns Whether the service should automatically connect on instantiation
25
+ */
18
26
  get autoConnect(): boolean;
27
+ /**
28
+ * Gets the current connection status.
29
+ * @returns The current serial connection status
30
+ */
19
31
  get status(): SerialStatus;
20
32
  /**
21
- * Updates status and emits the corresponding event.
33
+ * Updates the service status and emits the corresponding event.
34
+ * @param newStatus - The new status to set
22
35
  */
23
36
  protected setStatus(newStatus: SerialStatus): void;
24
37
  /**
25
38
  * Starts the connection process.
26
39
  * Handles both direct connection (known path) and scanning (VID/PID).
40
+ * @throws Will emit error events if connection fails
27
41
  */
28
42
  connect(): Promise<void>;
29
43
  /**
30
- * Physically opens the port and configures listeners.
44
+ * Physically opens the serial port and configures all necessary listeners.
45
+ * @param path - The port path to open (e.g., '/dev/ttyUSB0' or 'COM3')
31
46
  */
32
47
  protected openPort(path: string): void;
33
48
  /**
34
- * Manual disconnection.
49
+ * Manually disconnects from the serial port.
50
+ * @returns A promise that resolves when the port is fully closed
35
51
  */
36
52
  disconnect(): Promise<void>;
37
53
  /**
38
- * Public method to send data. Uses internal queue.
54
+ * Sends data through the serial port using the internal queue.
55
+ * @param data - The data to send (string or Buffer)
56
+ * @param options - Optional send configuration
57
+ * @param options.alias - A command identifier to correlate with responses
58
+ * @param options.timeout - Maximum time to wait for the operation in milliseconds
59
+ * @param options.waitResponse - If true, wait for a response before processing the next item in the queue
60
+ * @returns A promise that resolves when the data has been written to the port
61
+ * @throws If the port is not connected
39
62
  */
40
63
  send(data: string | Buffer, options?: {
41
64
  alias?: string;
@@ -43,24 +66,30 @@ export declare class SerialService extends EventEmitter {
43
66
  waitResponse?: boolean;
44
67
  }): Promise<void>;
45
68
  /**
46
- * Low-level write logic, handled by QueueManager.
47
- * Handles backpressure (drain).
69
+ * Low-level write logic executed by the QueueManager.
70
+ * Handles backpressure through the 'drain' event.
71
+ * @param data - The data to write to the port
72
+ * @returns A promise that resolves when the data has been fully written
48
73
  */
49
74
  protected performWrite(data: string | Buffer): Promise<void>;
50
75
  /**
51
- * Centralized connection failure handling.
76
+ * Handles connection failures in a centralized manner.
77
+ * Emits error events and schedules reconnection attempts if not intentional.
78
+ * @param reason - The reason for the connection failure
52
79
  */
53
80
  protected handleConnectionFailure(reason: string): void;
54
81
  /**
55
- * Schedules next connection attempt with simple backoff.
82
+ * Schedules the next connection attempt with a simple backoff strategy.
56
83
  */
57
84
  protected scheduleReconnect(): void;
58
85
  /**
59
- * Resource cleanup.
86
+ * Cleans up resources and listeners.
87
+ * Unregisters the port and removes all event listeners.
60
88
  */
61
89
  protected cleanup(): void;
62
90
  /**
63
- * Executes connection handshake.
91
+ * Executes the connection handshake to verify device responsiveness.
92
+ * Sends a handshake command and waits for a pattern match in the response.
64
93
  */
65
94
  protected performHandshake(): void;
66
95
  }
@@ -14,28 +14,51 @@ export declare class SerialService extends EventEmitter {
14
14
  protected config: SerialConfig;
15
15
  protected intentionalDisconnect: boolean;
16
16
  protected reconnectTimer: NodeJS.Timeout | null;
17
+ /**
18
+ * Initializes the SerialService with the provided configuration.
19
+ * @param config - Configuration object for serial connection
20
+ */
17
21
  constructor(config: SerialConfig);
22
+ /**
23
+ * Gets the autoConnect configuration value.
24
+ * @returns Whether the service should automatically connect on instantiation
25
+ */
18
26
  get autoConnect(): boolean;
27
+ /**
28
+ * Gets the current connection status.
29
+ * @returns The current serial connection status
30
+ */
19
31
  get status(): SerialStatus;
20
32
  /**
21
- * Updates status and emits the corresponding event.
33
+ * Updates the service status and emits the corresponding event.
34
+ * @param newStatus - The new status to set
22
35
  */
23
36
  protected setStatus(newStatus: SerialStatus): void;
24
37
  /**
25
38
  * Starts the connection process.
26
39
  * Handles both direct connection (known path) and scanning (VID/PID).
40
+ * @throws Will emit error events if connection fails
27
41
  */
28
42
  connect(): Promise<void>;
29
43
  /**
30
- * Physically opens the port and configures listeners.
44
+ * Physically opens the serial port and configures all necessary listeners.
45
+ * @param path - The port path to open (e.g., '/dev/ttyUSB0' or 'COM3')
31
46
  */
32
47
  protected openPort(path: string): void;
33
48
  /**
34
- * Manual disconnection.
49
+ * Manually disconnects from the serial port.
50
+ * @returns A promise that resolves when the port is fully closed
35
51
  */
36
52
  disconnect(): Promise<void>;
37
53
  /**
38
- * Public method to send data. Uses internal queue.
54
+ * Sends data through the serial port using the internal queue.
55
+ * @param data - The data to send (string or Buffer)
56
+ * @param options - Optional send configuration
57
+ * @param options.alias - A command identifier to correlate with responses
58
+ * @param options.timeout - Maximum time to wait for the operation in milliseconds
59
+ * @param options.waitResponse - If true, wait for a response before processing the next item in the queue
60
+ * @returns A promise that resolves when the data has been written to the port
61
+ * @throws If the port is not connected
39
62
  */
40
63
  send(data: string | Buffer, options?: {
41
64
  alias?: string;
@@ -43,24 +66,30 @@ export declare class SerialService extends EventEmitter {
43
66
  waitResponse?: boolean;
44
67
  }): Promise<void>;
45
68
  /**
46
- * Low-level write logic, handled by QueueManager.
47
- * Handles backpressure (drain).
69
+ * Low-level write logic executed by the QueueManager.
70
+ * Handles backpressure through the 'drain' event.
71
+ * @param data - The data to write to the port
72
+ * @returns A promise that resolves when the data has been fully written
48
73
  */
49
74
  protected performWrite(data: string | Buffer): Promise<void>;
50
75
  /**
51
- * Centralized connection failure handling.
76
+ * Handles connection failures in a centralized manner.
77
+ * Emits error events and schedules reconnection attempts if not intentional.
78
+ * @param reason - The reason for the connection failure
52
79
  */
53
80
  protected handleConnectionFailure(reason: string): void;
54
81
  /**
55
- * Schedules next connection attempt with simple backoff.
82
+ * Schedules the next connection attempt with a simple backoff strategy.
56
83
  */
57
84
  protected scheduleReconnect(): void;
58
85
  /**
59
- * Resource cleanup.
86
+ * Cleans up resources and listeners.
87
+ * Unregisters the port and removes all event listeners.
60
88
  */
61
89
  protected cleanup(): void;
62
90
  /**
63
- * Executes connection handshake.
91
+ * Executes the connection handshake to verify device responsiveness.
92
+ * Sends a handshake command and waits for a pattern match in the response.
64
93
  */
65
94
  protected performHandshake(): void;
66
95
  }
@@ -7,18 +7,26 @@ export declare class PortRegistry {
7
7
  private static instance;
8
8
  private lockedPorts;
9
9
  private constructor();
10
+ /**
11
+ * Gets the singleton instance of the PortRegistry.
12
+ * @returns The PortRegistry singleton instance
13
+ */
10
14
  static getInstance(): PortRegistry;
11
15
  /**
12
16
  * Attempts to register (lock) exclusive use of a port.
13
- * Returns true if registered successfully, false if already occupied.
17
+ * @param path - The port path to register
18
+ * @returns True if successfully registered, false if already in use
14
19
  */
15
20
  register(path: string): boolean;
16
21
  /**
17
22
  * Releases the port so it can be used again.
23
+ * @param path - The port path to unregister
18
24
  */
19
25
  unregister(path: string): void;
20
26
  /**
21
- * Checks if a port is currently in use by the library.
27
+ * Checks if a port is currently locked by the library.
28
+ * @param path - The port path to check
29
+ * @returns True if the port is in use, false otherwise
22
30
  */
23
31
  isLocked(path: string): boolean;
24
32
  }
@@ -7,18 +7,26 @@ export declare class PortRegistry {
7
7
  private static instance;
8
8
  private lockedPorts;
9
9
  private constructor();
10
+ /**
11
+ * Gets the singleton instance of the PortRegistry.
12
+ * @returns The PortRegistry singleton instance
13
+ */
10
14
  static getInstance(): PortRegistry;
11
15
  /**
12
16
  * Attempts to register (lock) exclusive use of a port.
13
- * Returns true if registered successfully, false if already occupied.
17
+ * @param path - The port path to register
18
+ * @returns True if successfully registered, false if already in use
14
19
  */
15
20
  register(path: string): boolean;
16
21
  /**
17
22
  * Releases the port so it can be used again.
23
+ * @param path - The port path to unregister
18
24
  */
19
25
  unregister(path: string): void;
20
26
  /**
21
- * Checks if a port is currently in use by the library.
27
+ * Checks if a port is currently locked by the library.
28
+ * @param path - The port path to check
29
+ * @returns True if the port is in use, false otherwise
22
30
  */
23
31
  isLocked(path: string): boolean;
24
32
  }
@@ -4,8 +4,12 @@
4
4
  */
5
5
  export declare class PortScanner {
6
6
  /**
7
- * Search for a port matching the provided VID and/or PID.
8
- * Normalizes strings to avoid case sensitivity issues.
7
+ * Searches for a serial port matching the provided Vendor ID and/or Product ID.
8
+ * Normalizes strings to handle case sensitivity and '0x' prefixes.
9
+ * @param vendorId - The USB vendor ID to search for (optional if productId provided)
10
+ * @param productId - The USB product ID to search for (optional)
11
+ * @returns The port path if found, null otherwise
12
+ * @throws Error if neither vendorId nor productId is provided
9
13
  */
10
14
  static findPort(vendorId?: string, productId?: string): Promise<string | null>;
11
15
  }
@@ -4,8 +4,12 @@
4
4
  */
5
5
  export declare class PortScanner {
6
6
  /**
7
- * Search for a port matching the provided VID and/or PID.
8
- * Normalizes strings to avoid case sensitivity issues.
7
+ * Searches for a serial port matching the provided Vendor ID and/or Product ID.
8
+ * Normalizes strings to handle case sensitivity and '0x' prefixes.
9
+ * @param vendorId - The USB vendor ID to search for (optional if productId provided)
10
+ * @param productId - The USB product ID to search for (optional)
11
+ * @returns The port path if found, null otherwise
12
+ * @throws Error if neither vendorId nor productId is provided
9
13
  */
10
14
  static findPort(vendorId?: string, productId?: string): Promise<string | null>;
11
15
  }
@@ -11,10 +11,19 @@ export declare class QueueManager {
11
11
  private currentTimeoutTimer;
12
12
  private currentItem;
13
13
  private writeHandler;
14
+ /**
15
+ * Initializes the QueueManager with a write handler function.
16
+ * @param writeHandler - Function to execute when writing data to the port
17
+ */
14
18
  constructor(writeHandler: (data: Buffer | string) => Promise<void>);
15
19
  /**
16
- * Adds a command to the queue and returns a promise
17
- * that resolves when the command has been drained to the port.
20
+ * Adds a command to the queue and returns a promise that resolves when processed.
21
+ * @param data - The data to queue (string or Buffer)
22
+ * @param options - Optional queue item configuration
23
+ * @param options.alias - Command identifier for response correlation
24
+ * @param options.timeout - Maximum time to wait for operation/response in milliseconds
25
+ * @param options.waitResponse - If true, block queue until response is received
26
+ * @returns Promise that resolves when the command has been processed
18
27
  */
19
28
  add(data: Buffer | string, options?: {
20
29
  alias?: string;
@@ -22,22 +31,38 @@ export declare class QueueManager {
22
31
  waitResponse?: boolean;
23
32
  }): Promise<void>;
24
33
  /**
25
- * Getter for the alias of the command currently processing (or just processed).
26
- * Useful for correlating responses.
34
+ * Gets the alias of the command currently processing or just processed.
35
+ * Useful for correlating received responses with sent commands.
36
+ * @returns The alias of the current command, or undefined if none
27
37
  */
28
38
  get currentAlias(): string | undefined;
29
39
  /**
30
- * Clears the queue (useful on abrupt disconnections)
40
+ * Clears all pending items from the queue.
41
+ * Useful when handling abrupt disconnections.
42
+ * All pending items will be rejected with a disconnection error.
31
43
  */
32
44
  clear(): void;
33
45
  /**
34
- * Notification from service that data was received.
35
- * If we are waiting for a response, this signals we can move on.
46
+ * Notifies the queue that a response has been received.
47
+ * If waiting for a response, this triggers resolution of the current command
48
+ * and processes the next item in the queue.
36
49
  */
37
50
  notifyResponse(): void;
38
51
  /**
39
- * Recursive processing loop
52
+ * Processes the queue recursively, executing items one by one.
53
+ * Handles timeouts and response waiting when configured.
54
+ * @private
40
55
  */
41
56
  private process;
57
+ /**
58
+ * Handles timeout when waiting for a response to a sent command.
59
+ * @private
60
+ * @param item - The queue item that timed out
61
+ */
42
62
  private handleResponseTimeout;
63
+ /**
64
+ * Checks if the queue is idle (no pending items, not processing, not waiting for response).
65
+ * @returns True if the queue has no pending operations
66
+ */
67
+ isIdle(): boolean;
43
68
  }
@@ -11,10 +11,19 @@ export declare class QueueManager {
11
11
  private currentTimeoutTimer;
12
12
  private currentItem;
13
13
  private writeHandler;
14
+ /**
15
+ * Initializes the QueueManager with a write handler function.
16
+ * @param writeHandler - Function to execute when writing data to the port
17
+ */
14
18
  constructor(writeHandler: (data: Buffer | string) => Promise<void>);
15
19
  /**
16
- * Adds a command to the queue and returns a promise
17
- * that resolves when the command has been drained to the port.
20
+ * Adds a command to the queue and returns a promise that resolves when processed.
21
+ * @param data - The data to queue (string or Buffer)
22
+ * @param options - Optional queue item configuration
23
+ * @param options.alias - Command identifier for response correlation
24
+ * @param options.timeout - Maximum time to wait for operation/response in milliseconds
25
+ * @param options.waitResponse - If true, block queue until response is received
26
+ * @returns Promise that resolves when the command has been processed
18
27
  */
19
28
  add(data: Buffer | string, options?: {
20
29
  alias?: string;
@@ -22,22 +31,38 @@ export declare class QueueManager {
22
31
  waitResponse?: boolean;
23
32
  }): Promise<void>;
24
33
  /**
25
- * Getter for the alias of the command currently processing (or just processed).
26
- * Useful for correlating responses.
34
+ * Gets the alias of the command currently processing or just processed.
35
+ * Useful for correlating received responses with sent commands.
36
+ * @returns The alias of the current command, or undefined if none
27
37
  */
28
38
  get currentAlias(): string | undefined;
29
39
  /**
30
- * Clears the queue (useful on abrupt disconnections)
40
+ * Clears all pending items from the queue.
41
+ * Useful when handling abrupt disconnections.
42
+ * All pending items will be rejected with a disconnection error.
31
43
  */
32
44
  clear(): void;
33
45
  /**
34
- * Notification from service that data was received.
35
- * If we are waiting for a response, this signals we can move on.
46
+ * Notifies the queue that a response has been received.
47
+ * If waiting for a response, this triggers resolution of the current command
48
+ * and processes the next item in the queue.
36
49
  */
37
50
  notifyResponse(): void;
38
51
  /**
39
- * Recursive processing loop
52
+ * Processes the queue recursively, executing items one by one.
53
+ * Handles timeouts and response waiting when configured.
54
+ * @private
40
55
  */
41
56
  private process;
57
+ /**
58
+ * Handles timeout when waiting for a response to a sent command.
59
+ * @private
60
+ * @param item - The queue item that timed out
61
+ */
42
62
  private handleResponseTimeout;
63
+ /**
64
+ * Checks if the queue is idle (no pending items, not processing, not waiting for response).
65
+ * @returns True if the queue has no pending operations
66
+ */
67
+ isIdle(): boolean;
43
68
  }
package/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=s(require(`serialport`)),l=s(require(`events`)),u={DISCONNECTED:`DISCONNECTED`,SCANNING:`SCANNING`,CONNECTING:`CONNECTING`,CONNECTED:`CONNECTED`,RECONNECTING:`RECONNECTING`};var d=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()}},f=class{static async findPort(e,t){if(!e&&!t)throw Error(`VendorID or ProductID is required for automatic scanning.`);try{let n=await c.SerialPort.list(),r=n.find(n=>{let r=n.vendorId?.toLowerCase()||``,i=n.productId?.toLowerCase()||``,a=(e||``).toLowerCase().replace(`0x`,``),o=(t||``).toLowerCase().replace(`0x`,``),s=e?r.includes(a):!0,c=t?i.includes(o):!0;return s&&c});return r?r.path:null}catch(e){return console.error(`Error scanning ports:`,e),null}}},p=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)}},m=class extends l.EventEmitter{port=null;queue;_status=u.DISCONNECTED;config;intentionalDisconnect=!1;reconnectTimer=null;constructor(e){super(),this.config=e,this.queue=new d(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===u.CONNECTED||this._status===u.CONNECTING)return;this.intentionalDisconnect=!1,this.setStatus(u.SCANNING);let e=this.config.path;if(!e)try{let t=await f.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(p.getInstance().isLocked(e)){this.handleConnectionFailure(`Port ${e} occupied by another internal instance`);return}if(!p.getInstance().register(e)){this.handleConnectionFailure(`Could not lock port ${e}`);return}this.setStatus(u.CONNECTING),this.openPort(e)}openPort(e){if(this.port=new c.SerialPort({path:e,baudRate:this.config.baudRate,autoOpen:!1}),this.port.open(t=>{if(t){this.handleConnectionFailure(t.message);return}this.config.handshake?this.performHandshake():(this.setStatus(u.CONNECTED),this.emit(`connected`,{path:e,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(u.DISCONNECTED):(this.setStatus(u.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===u.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(u.RECONNECTING),this.scheduleReconnect())}scheduleReconnect(){this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.reconnectTimer=setTimeout(()=>{this.connect()},this.config.reconnectInterval)}cleanup(){this.port&&this.port.path&&p.getInstance().unregister(this.port.path),this.port&&(this.port.removeAllListeners(),this.port=null),this.config.parser&&(this.config.parser.removeAllListeners(`data`),this.config.parser.removeAllListeners(`error`)),this.intentionalDisconnect&&this.queue.clear()}performHandshake(){if(!this.port||!this.port.isOpen||!this.config.handshake)return;let{command:e,pattern:t,timeout:n}=this.config.handshake,r,i=e=>{let n=``;n=Buffer.isBuffer(e)?e.toString():typeof e==`string`?e:String(e);let r=typeof t==`string`?new RegExp(t):t;r.test(n)&&(a(),this.setStatus(u.CONNECTED),this.emit(`connected`,{path:this.port.path,baudRate:this.config.baudRate}))},a=()=>{clearTimeout(r),this.removeListener(`data`,o)},o=e=>i(e);this.on(`data`,o),r=setTimeout(()=>{a(),this.handleConnectionFailure(`Handshake timeout (pattern: ${t})`)},n),this.performWrite(e).catch(e=>{a(),this.handleConnectionFailure(`Error writing handshake: ${e.message}`)})}};function h(e){return Buffer.isBuffer(e)?e:e instanceof Uint8Array?Buffer.from(e.buffer,e.byteOffset,e.byteLength):(Array.isArray(e),Buffer.from(e))}function g(e){return e}function _(e,t=`utf8`){return e.toString(t)}function v(e){return e.toString(`ascii`)}function y(e){return[...e]}exports.PortScanner=f,exports.SerialService=m,exports.SerialStatus=u,exports.toArray=y,exports.toAscii=v,exports.toBuffer=h,exports.toString=_,exports.toUint8Array=g;
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=s(require(`serialport`)),l=s(require(`events`)),u={DISCONNECTED:`DISCONNECTED`,SCANNING:`SCANNING`,CONNECTING:`CONNECTING`,CONNECTED:`CONNECTED`,RECONNECTING:`RECONNECTING`};var d=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}},f=class{static async findPort(e,t){if(!e&&!t)throw Error(`VendorID or ProductID is required for automatic scanning.`);try{let n=await c.SerialPort.list(),r=n.find(n=>{let r=n.vendorId?.toLowerCase()||``,i=n.productId?.toLowerCase()||``,a=(e||``).toLowerCase().replace(`0x`,``),o=(t||``).toLowerCase().replace(`0x`,``),s=e?r.includes(a):!0,c=t?i.includes(o):!0;return s&&c});return r?r.path:null}catch(e){return console.error(`Error scanning ports:`,e),null}}},p=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)}},m=class extends l.EventEmitter{port=null;queue;_status=u.DISCONNECTED;config;intentionalDisconnect=!1;reconnectTimer=null;constructor(e){super(),this.config=e,this.queue=new d(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===u.CONNECTED||this._status===u.CONNECTING)return;this.intentionalDisconnect=!1,this.setStatus(u.SCANNING);let e=this.config.path;if(!e)try{let t=await f.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(p.getInstance().isLocked(e)){this.handleConnectionFailure(`Port ${e} occupied by another internal instance`);return}if(!p.getInstance().register(e)){this.handleConnectionFailure(`Could not lock port ${e}`);return}this.setStatus(u.CONNECTING),this.openPort(e)}openPort(e){if(this.port=new c.SerialPort({path:e,baudRate:this.config.baudRate,autoOpen:!1}),this.port.open(t=>{if(t){this.handleConnectionFailure(t.message);return}this.config.handshake?this.performHandshake():(this.setStatus(u.CONNECTED),this.emit(`connected`,{path:e,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(u.DISCONNECTED):(this.setStatus(u.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===u.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(u.RECONNECTING),this.scheduleReconnect())}scheduleReconnect(){this.reconnectTimer&&clearTimeout(this.reconnectTimer),this.reconnectTimer=setTimeout(()=>{this.connect()},this.config.reconnectInterval)}cleanup(){this.port&&this.port.path&&p.getInstance().unregister(this.port.path),this.port&&(this.port.removeAllListeners(),this.port=null),this.config.parser&&(this.config.parser.removeAllListeners(`data`),this.config.parser.removeAllListeners(`error`)),this.intentionalDisconnect&&this.queue.clear()}performHandshake(){if(!this.port||!this.port.isOpen||!this.config.handshake)return;let{command:e,pattern:t,timeout:n,hexPattern:r}=this.config.handshake,i,a=e=>{let n=``;if(Buffer.isBuffer(e))n=r?e.toString(`hex`):e.toString();else if(typeof e==`string`)n=r?Buffer.from(e).toString(`hex`):e;else{let t=Buffer.from(String(e));n=r?t.toString(`hex`):t.toString()}let i=typeof t==`string`?new RegExp(t):t;i.test(n)&&(o(),this.setStatus(u.CONNECTED),this.emit(`connected`,{path:this.port.path,baudRate:this.config.baudRate}))},o=()=>{clearTimeout(i),this.removeListener(`data`,s)},s=e=>a(e);this.on(`data`,s),i=setTimeout(()=>{o(),this.handleConnectionFailure(`Handshake timeout (pattern: ${t})`)},n),this.performWrite(e).catch(e=>{o(),this.handleConnectionFailure(`Error writing handshake: ${e.message}`)})}};function h(e){return Buffer.isBuffer(e)?e:e instanceof Uint8Array?Buffer.from(e.buffer,e.byteOffset,e.byteLength):(Array.isArray(e),Buffer.from(e))}function g(e){return e}function _(e,t=`utf8`){return e.toString(t)}function v(e){return e.toString(`ascii`)}function y(e){return[...e]}exports.PortScanner=f,exports.SerialService=m,exports.SerialStatus=u,exports.toArray=y,exports.toAscii=v,exports.toBuffer=h,exports.toString=_,exports.toUint8Array=g;
package/dist/index.js CHANGED
@@ -1 +1 @@
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()}},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(`data`),this.config.parser.removeAllListeners(`error`)),this.intentionalDisconnect&&this.queue.clear()}performHandshake(){if(!this.port||!this.port.isOpen||!this.config.handshake)return;let{command:e,pattern:t,timeout:r}=this.config.handshake,i,a=e=>{let r=``;r=Buffer.isBuffer(e)?e.toString():typeof e==`string`?e:String(e);let i=typeof t==`string`?new RegExp(t):t;i.test(r)&&(o(),this.setStatus(n.CONNECTED),this.emit(`connected`,{path:this.port.path,baudRate:this.config.baudRate}))},o=()=>{clearTimeout(i),this.removeListener(`data`,s)},s=e=>a(e);this.on(`data`,s),i=setTimeout(()=>{o(),this.handleConnectionFailure(`Handshake timeout (pattern: ${t})`)},r),this.performWrite(e).catch(e=>{o(),this.handleConnectionFailure(`Error writing handshake: ${e.message}`)})}};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{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(`data`),this.config.parser.removeAllListeners(`error`)),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}=this.config.handshake,a,o=e=>{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 a=typeof t==`string`?new RegExp(t):t;a.test(r)&&(s(),this.setStatus(n.CONNECTED),this.emit(`connected`,{path:this.port.path,baudRate:this.config.baudRate}))},s=()=>{clearTimeout(a),this.removeListener(`data`,c)},c=e=>o(e);this.on(`data`,c),a=setTimeout(()=>{s(),this.handleConnectionFailure(`Handshake timeout (pattern: ${t})`)},r),this.performWrite(e).catch(e=>{s(),this.handleConnectionFailure(`Error writing handshake: ${e.message}`)})}};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};
package/dist/types.d.cts CHANGED
@@ -18,6 +18,7 @@ export interface SerialConfig {
18
18
  command: string | Buffer;
19
19
  pattern: string | RegExp;
20
20
  timeout: number;
21
+ hexPattern?: boolean;
21
22
  };
22
23
  }
23
24
  export interface QueueItem {
package/dist/types.d.ts CHANGED
@@ -18,6 +18,7 @@ export interface SerialConfig {
18
18
  command: string | Buffer;
19
19
  pattern: string | RegExp;
20
20
  timeout: number;
21
+ hexPattern?: boolean;
21
22
  };
22
23
  }
23
24
  export interface QueueItem {
@@ -5,23 +5,35 @@
5
5
  /**
6
6
  * Efficiently converts various input types to Buffer.
7
7
  * If input is already a Buffer, returns it as is (zero-copy).
8
+ * @param data - The data to convert (string, number array, Uint8Array, or Buffer)
9
+ * @returns A Buffer instance
8
10
  */
9
11
  export declare function toBuffer(data: string | number[] | Uint8Array | Buffer): Buffer;
10
12
  /**
11
13
  * Converts a Buffer to Uint8Array.
12
14
  * In Node.js, Buffers ARE Uint8Arrays, so this is ideally zero-copy.
15
+ * @param buf - The buffer to convert
16
+ * @returns The buffer as a Uint8Array
13
17
  */
14
18
  export declare function toUint8Array(buf: Buffer): Uint8Array;
15
19
  /**
16
20
  * Converts a Buffer to string with optional encoding (default utf8).
17
21
  * Wrapper alias for toString().
22
+ * @param buf - The buffer to convert
23
+ * @param encoding - The character encoding to use (default: 'utf8')
24
+ * @returns The buffer contents as a string
18
25
  */
19
26
  export declare function toString(buf: Buffer, encoding?: BufferEncoding): string;
20
27
  /**
21
- * Optimally converts to ASCII.
28
+ * Converts a Buffer to ASCII string representation.
29
+ * @param buf - The buffer to convert
30
+ * @returns The buffer contents as an ASCII string
22
31
  */
23
32
  export declare function toAscii(buf: Buffer): string;
24
33
  /**
25
- * Converts a Buffer to an Array of numbers (bytes).
34
+ * Converts a Buffer to an array of byte values.
35
+ * Note: Using spread operator may be slow for very large buffers.
36
+ * @param buf - The buffer to convert
37
+ * @returns An array of numbers representing each byte
26
38
  */
27
39
  export declare function toArray(buf: Buffer): number[];
@@ -5,23 +5,35 @@
5
5
  /**
6
6
  * Efficiently converts various input types to Buffer.
7
7
  * If input is already a Buffer, returns it as is (zero-copy).
8
+ * @param data - The data to convert (string, number array, Uint8Array, or Buffer)
9
+ * @returns A Buffer instance
8
10
  */
9
11
  export declare function toBuffer(data: string | number[] | Uint8Array | Buffer): Buffer;
10
12
  /**
11
13
  * Converts a Buffer to Uint8Array.
12
14
  * In Node.js, Buffers ARE Uint8Arrays, so this is ideally zero-copy.
15
+ * @param buf - The buffer to convert
16
+ * @returns The buffer as a Uint8Array
13
17
  */
14
18
  export declare function toUint8Array(buf: Buffer): Uint8Array;
15
19
  /**
16
20
  * Converts a Buffer to string with optional encoding (default utf8).
17
21
  * Wrapper alias for toString().
22
+ * @param buf - The buffer to convert
23
+ * @param encoding - The character encoding to use (default: 'utf8')
24
+ * @returns The buffer contents as a string
18
25
  */
19
26
  export declare function toString(buf: Buffer, encoding?: BufferEncoding): string;
20
27
  /**
21
- * Optimally converts to ASCII.
28
+ * Converts a Buffer to ASCII string representation.
29
+ * @param buf - The buffer to convert
30
+ * @returns The buffer contents as an ASCII string
22
31
  */
23
32
  export declare function toAscii(buf: Buffer): string;
24
33
  /**
25
- * Converts a Buffer to an Array of numbers (bytes).
34
+ * Converts a Buffer to an array of byte values.
35
+ * Note: Using spread operator may be slow for very large buffers.
36
+ * @param buf - The buffer to convert
37
+ * @returns An array of numbers representing each byte
26
38
  */
27
39
  export declare function toArray(buf: Buffer): number[];
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "serial-core",
3
3
  "type": "module",
4
- "version": "0.2.0-dev.4",
4
+ "version": "0.2.0",
5
5
  "description": "Resilient serial communication service with automatic reconnection and queues.",
6
6
  "author": "Danidoble <danidoble@gmail.com>",
7
7
  "license": "GPL-3.0-only",