serial-core 0.2.0-dev.4 → 0.2.0-dev.5

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
 
@@ -40,4 +40,9 @@ export declare class QueueManager {
40
40
  */
41
41
  private process;
42
42
  private handleResponseTimeout;
43
+ /**
44
+ * Indicates whether the queue is idle (no pending items,
45
+ * not processing, not waiting for response)
46
+ */
47
+ isIdle(): boolean;
43
48
  }
@@ -40,4 +40,9 @@ export declare class QueueManager {
40
40
  */
41
41
  private process;
42
42
  private handleResponseTimeout;
43
+ /**
44
+ * Indicates whether the queue is idle (no pending items,
45
+ * not processing, not waiting for response)
46
+ */
47
+ isIdle(): boolean;
43
48
  }
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 {
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-dev.5",
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",