@mcesystems/usb-device-listener 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # USB Device Listener
2
+
3
+ A high-performance native Node.js addon for monitoring USB device connections and disconnections on Windows. Built with N-API for stability across Node.js versions.
4
+
5
+ ## Features
6
+
7
+ - โšก **Real-time monitoring** - Immediate notification of USB device events via native Windows API
8
+ - ๐ŸŽฏ **Device filtering** - Monitor specific devices by VID/PID or hub location
9
+ - ๐Ÿ“ **Physical port mapping** - Map USB ports to logical port numbers for consistent device identification
10
+ - ๐Ÿงต **Thread-safe** - Runs in separate thread without blocking Node.js event loop
11
+ - ๐Ÿ’ช **Production-ready** - Memory-safe, handles multiple simultaneous device changes
12
+ - ๐Ÿ” **Device enumeration** - List all currently connected devices
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install
18
+ ```
19
+
20
+ ### Requirements
21
+
22
+ - **Node.js**: v18+ (ESM support required)
23
+ - **Windows**: 10/11 (uses Windows Device Management API)
24
+ - **Build tools**: Visual Studio 2022 with C++ build tools
25
+
26
+ ## Quick Start
27
+
28
+ ```javascript
29
+ import usbListener from 'usb-device-listener';
30
+
31
+ // Define configuration
32
+ const config = {
33
+ logicalPortMap: {
34
+ "Port_#0005.Hub_#0002": 1, // Map physical port to logical port 1
35
+ "Port_#0006.Hub_#0002": 2
36
+ },
37
+ targetDevices: [
38
+ { vid: "04E8", pid: "6860" } // Only monitor Samsung devices
39
+ ],
40
+ ignoredHubs: [],
41
+ listenOnlyHubs: []
42
+ };
43
+
44
+ // Register event handlers
45
+ usbListener.onDeviceAdd((device) => {
46
+ console.log('Device connected:', device.locationInfo);
47
+ console.log('VID:PID:', `${device.vid.toString(16)}:${device.pid.toString(16)}`);
48
+ console.log('Logical Port:', device.logicalPort);
49
+ });
50
+
51
+ usbListener.onDeviceRemove((device) => {
52
+ console.log('Device disconnected:', device.locationInfo);
53
+ });
54
+
55
+ // Start listening
56
+ try {
57
+ usbListener.startListening(config);
58
+ console.log('Listening for USB events...');
59
+ } catch (error) {
60
+ console.error('Failed to start:', error.message);
61
+ }
62
+
63
+ // Graceful shutdown
64
+ process.on('SIGINT', () => {
65
+ usbListener.stopListening();
66
+ process.exit(0);
67
+ });
68
+ ```
69
+
70
+ ## API Reference
71
+
72
+ ### `startListening(config)`
73
+
74
+ Start monitoring USB device events.
75
+
76
+ **Parameters:**
77
+ - `config` (Object): Configuration object
78
+ - `logicalPortMap` (Object, optional): Map physical locations to logical port numbers
79
+ - Key: Location string (e.g., "Port_#0005.Hub_#0002")
80
+ - Value: Logical port number (integer)
81
+ - `targetDevices` (Array, optional): Filter specific devices by VID/PID
82
+ - Each element: `{ vid: string, pid: string }` (hex strings, e.g., "04E8")
83
+ - Empty array = monitor all devices
84
+ - `ignoredHubs` (Array, optional): Hub location strings to ignore
85
+ - `listenOnlyHubs` (Array, optional): Only monitor these hub locations
86
+
87
+ **Throws:**
88
+ - `TypeError` if config is not an object
89
+ - `Error` if listener is already running
90
+
91
+ **Example:**
92
+ ```javascript
93
+ usbListener.startListening({
94
+ logicalPortMap: {
95
+ "Port_#0005.Hub_#0002": 1
96
+ },
97
+ targetDevices: [], // Monitor all devices
98
+ ignoredHubs: ["Port_#0001.Hub_#0001"], // Ignore this hub
99
+ listenOnlyHubs: [] // No restriction
100
+ });
101
+ ```
102
+
103
+ ### `stopListening()`
104
+
105
+ Stop monitoring and clean up resources. Safe to call multiple times.
106
+
107
+ **Example:**
108
+ ```javascript
109
+ usbListener.stopListening();
110
+ ```
111
+
112
+ ### `onDeviceAdd(callback)`
113
+
114
+ Register callback for device connection events.
115
+
116
+ **Parameters:**
117
+ - `callback` (Function): Called when device is connected
118
+ - `deviceInfo` (Object):
119
+ - `deviceId` (string): Windows device instance ID
120
+ - `vid` (number): Vendor ID (decimal)
121
+ - `pid` (number): Product ID (decimal)
122
+ - `locationInfo` (string): Physical port location
123
+ - `logicalPort` (number|null): Mapped logical port or null
124
+
125
+ **Example:**
126
+ ```javascript
127
+ usbListener.onDeviceAdd((device) => {
128
+ const vidHex = device.vid.toString(16).toUpperCase().padStart(4, '0');
129
+ const pidHex = device.pid.toString(16).toUpperCase().padStart(4, '0');
130
+ console.log(`Device ${vidHex}:${pidHex} on port ${device.logicalPort || 'unmapped'}`);
131
+ });
132
+ ```
133
+
134
+ ### `onDeviceRemove(callback)`
135
+
136
+ Register callback for device disconnection events. Device info format same as `onDeviceAdd`.
137
+
138
+ **Example:**
139
+ ```javascript
140
+ usbListener.onDeviceRemove((device) => {
141
+ console.log(`Port ${device.logicalPort} disconnected`);
142
+ });
143
+ ```
144
+
145
+ ### `listDevices()`
146
+
147
+ Get list of all currently connected USB devices.
148
+
149
+ **Returns:** Array of device objects (same format as callback parameter)
150
+
151
+ **Example:**
152
+ ```javascript
153
+ const devices = usbListener.listDevices();
154
+ devices.forEach(device => {
155
+ console.log(`${device.locationInfo}: VID=${device.vid.toString(16)}`);
156
+ });
157
+ ```
158
+
159
+ ## How It Works
160
+
161
+ ### Physical Port Location
162
+
163
+ Windows assigns a unique location string to each USB port:
164
+ - Format: `Port_#XXXX.Hub_#YYYY`
165
+ - Example: `Port_#0005.Hub_#0002`
166
+ - **Stable**: Same port always has same location string
167
+ - **Use case**: Map to logical port numbers (1, 2, 3...) for your application
168
+
169
+ ### Getting Location Strings
170
+
171
+ Use the included `list-devices.js` utility:
172
+
173
+ ```bash
174
+ node list-devices.js
175
+ ```
176
+
177
+ Output:
178
+ ```
179
+ Device 1:
180
+ Device ID: USB\VID_04E8&PID_6860\R58NC2971AJ
181
+ VID: 0x04E8
182
+ PID: 0x6860
183
+ Location Info (mapping key): Port_#0005.Hub_#0002
184
+
185
+ Device 2:
186
+ Device ID: USB\VID_27C6&PID_6594\UID0014C59F
187
+ VID: 0x27C6
188
+ PID: 0x6594
189
+ Location Info (mapping key): Port_#0007.Hub_#0002
190
+ ```
191
+
192
+ Copy the "Location Info" values to use in your `logicalPortMap`.
193
+
194
+ ### Device Filtering
195
+
196
+ **By VID/PID:**
197
+ ```javascript
198
+ targetDevices: [
199
+ { vid: "2341", pid: "0043" }, // Arduino Uno
200
+ { vid: "0483", pid: "5740" } // STM32
201
+ ]
202
+ ```
203
+
204
+ **By Hub:**
205
+ ```javascript
206
+ listenOnlyHubs: ["Hub_#0002"] // Only monitor this hub
207
+ // or
208
+ ignoredHubs: ["Hub_#0001"] // Ignore this hub
209
+ ```
210
+
211
+ ## Performance & Scalability
212
+
213
+ ### Handling Many Devices
214
+
215
+ The listener is designed to handle multiple simultaneous device events efficiently:
216
+
217
+ โœ… **Thread-safe**: Device cache protected by mutex
218
+ โœ… **Non-blocking**: Runs in separate thread, doesn't block Node.js
219
+ โœ… **Efficient**: Only processes filtered devices
220
+ โœ… **Memory-safe**: Automatic cleanup on disconnect
221
+
222
+ ### Tested Scenarios
223
+
224
+ - Multiple rapid connect/disconnect cycles
225
+ - Simultaneous connection of 10+ devices
226
+ - Hub with many devices
227
+ - Long-running processes (hours/days)
228
+
229
+ ### Best Practices
230
+
231
+ 1. **Use device filtering** when possible to reduce CPU usage:
232
+ ```javascript
233
+ targetDevices: [{ vid: "04E8", pid: "6860" }] // Better than monitoring all
234
+ ```
235
+
236
+ 2. **Keep callbacks fast** - offload heavy processing:
237
+ ```javascript
238
+ onDeviceAdd((device) => {
239
+ // Good: Quick database write
240
+ db.logConnection(device);
241
+
242
+ // Bad: Long synchronous operation
243
+ // processLargeFile(device); // Use setTimeout or worker thread instead
244
+ });
245
+ ```
246
+
247
+ 3. **Handle errors gracefully**:
248
+ ```javascript
249
+ onDeviceAdd((device) => {
250
+ try {
251
+ processDevice(device);
252
+ } catch (error) {
253
+ console.error('Device processing failed:', error);
254
+ }
255
+ });
256
+ ```
257
+
258
+ 4. **Clean shutdown**:
259
+ ```javascript
260
+ process.on('SIGINT', () => {
261
+ usbListener.stopListening();
262
+ // Wait briefly for cleanup
263
+ setTimeout(() => process.exit(0), 100);
264
+ });
265
+ ```
266
+
267
+ ## Architecture
268
+
269
+ ```
270
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
271
+ โ”‚ Node.js Application โ”‚
272
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
273
+ โ”‚ โ”‚ JavaScript API โ”‚ โ”‚
274
+ โ”‚ โ”‚ (index.js - documented) โ”‚ โ”‚
275
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
276
+ โ”‚ โ”‚ โ”‚
277
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
278
+ โ”‚ โ”‚ N-API Addon (addon.cc) โ”‚ โ”‚
279
+ โ”‚ โ”‚ - Converts JS โ†” C++ types โ”‚ โ”‚
280
+ โ”‚ โ”‚ - ThreadSafeFunction callbacks โ”‚ โ”‚
281
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
282
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
283
+ โ”‚
284
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
285
+ โ”‚ USBListener (usb_listener.cc) โ”‚
286
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
287
+ โ”‚ โ”‚ Listener Thread (MessageLoop) โ”‚ โ”‚
288
+ โ”‚ โ”‚ - Hidden message-only window โ”‚ โ”‚
289
+ โ”‚ โ”‚ - RegisterDeviceNotification โ”‚ โ”‚
290
+ โ”‚ โ”‚ - Receives WM_DEVICECHANGE โ”‚ โ”‚
291
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
292
+ โ”‚ โ”‚ โ”‚
293
+ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚
294
+ โ”‚ โ”‚ Windows Device Management API โ”‚ โ”‚
295
+ โ”‚ โ”‚ - SetupDi* functions โ”‚ โ”‚
296
+ โ”‚ โ”‚ - CM_Get_DevNode_* โ”‚ โ”‚
297
+ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚
298
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
299
+ ```
300
+
301
+ ## Troubleshooting
302
+
303
+ **Build errors:**
304
+ - Ensure Visual Studio C++ build tools are installed
305
+ - Run from "x64 Native Tools Command Prompt"
306
+
307
+ **No events firing:**
308
+ - Check `targetDevices` filter isn't too restrictive
309
+ - Verify callbacks are registered before calling `startListening()`
310
+ - Use `listDevices()` to confirm devices are visible
311
+
312
+ **Incorrect location info:**
313
+ - Location strings are generated by Windows
314
+ - Different USB controllers may use different formats
315
+ - Always use `listDevices()` to get actual location strings
316
+
317
+ ## License
318
+
319
+ MIT
320
+
321
+ ## Contributing
322
+
323
+ Issues and pull requests welcome!
@@ -0,0 +1,24 @@
1
+ import type { DeviceInfo, ListenerConfig } from "./types";
2
+ /**
3
+ * Determine if a device notification should be sent based on the configuration.
4
+ *
5
+ * Filter priority (highest to lowest):
6
+ * 1. ignoredDevices - if device matches, always return false
7
+ * 2. listenOnlyDevices - if specified, device must match at least one
8
+ * 3. targetDevices - if specified, device must match at least one
9
+ * 4. logicalPortMap - if specified, device location must be in the map
10
+ *
11
+ * @param device - The device information from the native addon
12
+ * @param config - The listener configuration
13
+ * @returns true if the device should trigger a notification, false otherwise
14
+ */
15
+ export declare function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean;
16
+ /**
17
+ * Apply logical port mapping to a device if configured
18
+ *
19
+ * @param device - The device information from the native addon
20
+ * @param config - The listener configuration
21
+ * @returns Device info with logicalPort set if mapped, otherwise unchanged
22
+ */
23
+ export declare function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo;
24
+ //# sourceMappingURL=device-filter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device-filter.d.ts","sourceRoot":"","sources":["../src/device-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAgB,MAAM,SAAS,CAAC;AA4BxE;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CA8BtF;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,cAAc,GAAG,UAAU,CAQ9F"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=example.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"example.d.ts","sourceRoot":"","sources":["../../src/examples/example.ts"],"names":[],"mappings":""}
@@ -0,0 +1,187 @@
1
+ // src/index.ts
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ // src/device-filter.ts
7
+ function toHexString(value) {
8
+ return value.toString(16).toUpperCase().padStart(4, "0");
9
+ }
10
+ function matchesDevice(device, target) {
11
+ const deviceVid = toHexString(device.vid);
12
+ const devicePid = toHexString(device.pid);
13
+ const targetVid = target.vid.toUpperCase();
14
+ const targetPid = target.pid.toUpperCase();
15
+ return deviceVid === targetVid && devicePid === targetPid;
16
+ }
17
+ function matchesAnyDevice(device, targets) {
18
+ return targets.some((target) => matchesDevice(device, target));
19
+ }
20
+ function shouldNotifyDevice(device, config2) {
21
+ if (config2.ignoredDevices && config2.ignoredDevices.length > 0) {
22
+ if (matchesAnyDevice(device, config2.ignoredDevices)) {
23
+ return false;
24
+ }
25
+ }
26
+ if (config2.listenOnlyDevices && config2.listenOnlyDevices.length > 0) {
27
+ if (!matchesAnyDevice(device, config2.listenOnlyDevices)) {
28
+ return false;
29
+ }
30
+ }
31
+ if (config2.targetDevices && config2.targetDevices.length > 0) {
32
+ if (!matchesAnyDevice(device, config2.targetDevices)) {
33
+ return false;
34
+ }
35
+ }
36
+ if (config2.logicalPortMap && Object.keys(config2.logicalPortMap).length > 0) {
37
+ if (!(device.locationInfo in config2.logicalPortMap)) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+ function applyLogicalPortMapping(device, config2) {
44
+ if (config2.logicalPortMap && device.locationInfo in config2.logicalPortMap) {
45
+ return {
46
+ ...device,
47
+ logicalPort: config2.logicalPortMap[device.locationInfo]
48
+ };
49
+ }
50
+ return device;
51
+ }
52
+
53
+ // src/index.ts
54
+ var __filename = fileURLToPath(import.meta.url);
55
+ var __dirname = dirname(__filename);
56
+ var packageRoot = dirname(__dirname);
57
+ function loadNativeAddon() {
58
+ const require2 = createRequire(import.meta.url);
59
+ const addonPath = join(packageRoot, "build", "Release", "@mcesystems+usb-device-listener.node");
60
+ try {
61
+ return require2(addonPath);
62
+ } catch {
63
+ try {
64
+ const nodeGypBuild = require2("node-gyp-build");
65
+ return nodeGypBuild(packageRoot);
66
+ } catch {
67
+ throw new Error(
68
+ `Failed to load native addon. Tried:
69
+ 1. Direct path: ${addonPath}
70
+ 2. node-gyp-build search
71
+ Make sure the addon is built with 'npm run rebuild' or 'npm run prebuild'`
72
+ );
73
+ }
74
+ }
75
+ }
76
+ var addon = loadNativeAddon();
77
+ var UsbDeviceListenerImpl = class {
78
+ config = {};
79
+ userAddCallback = null;
80
+ userRemoveCallback = null;
81
+ /**
82
+ * Start listening for USB device events
83
+ */
84
+ startListening(config2) {
85
+ if (typeof config2 !== "object" || config2 === null) {
86
+ throw new TypeError("Config must be an object");
87
+ }
88
+ this.config = config2;
89
+ addon.startListening();
90
+ }
91
+ /**
92
+ * Stop listening for USB device events
93
+ */
94
+ stopListening() {
95
+ addon.stopListening();
96
+ }
97
+ /**
98
+ * Register callback for device connection events
99
+ */
100
+ onDeviceAdd(callback) {
101
+ if (typeof callback !== "function") {
102
+ throw new TypeError("Callback must be a function");
103
+ }
104
+ this.userAddCallback = callback;
105
+ addon.onDeviceAdd((device) => {
106
+ if (shouldNotifyDevice(device, this.config)) {
107
+ const enrichedDevice = applyLogicalPortMapping(device, this.config);
108
+ this.userAddCallback?.(enrichedDevice);
109
+ }
110
+ });
111
+ }
112
+ /**
113
+ * Register callback for device disconnection events
114
+ */
115
+ onDeviceRemove(callback) {
116
+ if (typeof callback !== "function") {
117
+ throw new TypeError("Callback must be a function");
118
+ }
119
+ this.userRemoveCallback = callback;
120
+ addon.onDeviceRemove((device) => {
121
+ if (shouldNotifyDevice(device, this.config)) {
122
+ const enrichedDevice = applyLogicalPortMapping(device, this.config);
123
+ this.userRemoveCallback?.(enrichedDevice);
124
+ }
125
+ });
126
+ }
127
+ /**
128
+ * List all currently connected USB devices
129
+ */
130
+ listDevices() {
131
+ return addon.listDevices();
132
+ }
133
+ };
134
+ var usbDeviceListener = new UsbDeviceListenerImpl();
135
+ var index_default = usbDeviceListener;
136
+
137
+ // src/examples/example.ts
138
+ var config = {
139
+ logicalPortMap: {
140
+ "Port_#0005.Hub_#0002": 1,
141
+ "Port_#0006.Hub_#0002": 2
142
+ },
143
+ ignoredDevices: [],
144
+ listenOnlyDevices: []
145
+ };
146
+ index_default.onDeviceAdd((device) => {
147
+ console.log("Device connected:");
148
+ console.log(" Device ID:", device.deviceId);
149
+ console.log(` VID: 0x${device.vid.toString(16).toUpperCase().padStart(4, "0")}`);
150
+ console.log(` PID: 0x${device.pid.toString(16).toUpperCase().padStart(4, "0")}`);
151
+ console.log(" Location:", device.locationInfo);
152
+ console.log(" Logical Port:", device.logicalPort !== null ? device.logicalPort : "Not mapped");
153
+ console.log("");
154
+ });
155
+ index_default.onDeviceRemove((device) => {
156
+ console.log("Device disconnected:");
157
+ console.log(" Device ID:", device.deviceId);
158
+ console.log(` VID: 0x${device.vid.toString(16).toUpperCase().padStart(4, "0")}`);
159
+ console.log(` PID: 0x${device.pid.toString(16).toUpperCase().padStart(4, "0")}`);
160
+ console.log(" Location:", device.locationInfo);
161
+ console.log(" Logical Port:", device.logicalPort !== null ? device.logicalPort : "Not mapped");
162
+ console.log("");
163
+ });
164
+ try {
165
+ console.log("Starting USB device listener...");
166
+ console.log("Config:", JSON.stringify(config, null, 2));
167
+ console.log("");
168
+ index_default.startListening(config);
169
+ console.log("Listening for USB device events. Press Ctrl+C to stop.");
170
+ console.log("");
171
+ } catch (error) {
172
+ if (error instanceof Error) {
173
+ console.error("Error starting listener:", error.message);
174
+ }
175
+ process.exit(1);
176
+ }
177
+ process.on("SIGINT", () => {
178
+ console.log("\nStopping listener...");
179
+ index_default.stopListening();
180
+ process.exit(0);
181
+ });
182
+ process.on("SIGTERM", () => {
183
+ console.log("\nStopping listener...");
184
+ index_default.stopListening();
185
+ process.exit(0);
186
+ });
187
+ //# sourceMappingURL=example.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/index.ts", "../../src/device-filter.ts", "../../src/examples/example.ts"],
4
+ "sourcesContent": ["\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\nimport type {\n\tDeviceAddCallback,\n\tDeviceInfo,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tUsbDeviceListener,\n} from \"./types\";\n\n/**\n * Native addon interface\n * This is the raw C++ addon loaded via N-API\n */\ninterface NativeAddon {\n\tstartListening(): void;\n\tstopListening(): void;\n\tonDeviceAdd(callback: DeviceAddCallback): void;\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\n\tlistDevices(): DeviceInfo[];\n}\n\n// ESM compatibility: get __dirname equivalent\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst packageRoot = dirname(__dirname);\n\n/**\n * Load native addon\n * In development: uses node-gyp-build to find the addon\n * In production: loads directly from the known build location\n */\nfunction loadNativeAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\n\t// Try to load from the known production location first\n\tconst addonPath = join(packageRoot, \"build\", \"Release\", \"@mcesystems+usb-device-listener.node\");\n\n\ttry {\n\t\treturn require(addonPath);\n\t} catch {\n\t\t// Fallback to node-gyp-build for development (if available)\n\t\ttry {\n\t\t\tconst nodeGypBuild = require(\"node-gyp-build\");\n\t\t\treturn nodeGypBuild(packageRoot);\n\t\t} catch {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to load native addon. Tried:\\n 1. Direct path: ${addonPath}\\n 2. node-gyp-build search\\nMake sure the addon is built with 'npm run rebuild' or 'npm run prebuild'`\n\t\t\t);\n\t\t}\n\t}\n}\n\nconst addon = loadNativeAddon();\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nclass UsbDeviceListenerImpl implements UsbDeviceListener {\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\t/**\n\t * Start listening for USB device events\n\t */\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t\taddon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\taddon.stopListening();\n\t}\n\n\t/**\n\t * Register callback for device connection events\n\t */\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userAddCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\taddon.onDeviceAdd((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userAddCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Register callback for device disconnection events\n\t */\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userRemoveCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\taddon.onDeviceRemove((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * List all currently connected USB devices\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\treturn addon.listDevices();\n\t}\n}\n\n// Export singleton instance\nconst usbDeviceListener: UsbDeviceListener = new UsbDeviceListenerImpl();\n\nexport default usbDeviceListener;\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tTargetDevice,\n\tUsbDeviceListener,\n} from \"./types\";\n", "import type { DeviceInfo, ListenerConfig, TargetDevice } from \"./types\";\n\n/**\n * Convert a decimal VID/PID to uppercase hex string for comparison\n */\nfunction toHexString(value: number): string {\n\treturn value.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/**\n * Check if a device matches a target device filter by VID/PID\n */\nfunction matchesDevice(device: DeviceInfo, target: TargetDevice): boolean {\n\tconst deviceVid = toHexString(device.vid);\n\tconst devicePid = toHexString(device.pid);\n\tconst targetVid = target.vid.toUpperCase();\n\tconst targetPid = target.pid.toUpperCase();\n\n\treturn deviceVid === targetVid && devicePid === targetPid;\n}\n\n/**\n * Check if a device matches any device in a list of target devices\n */\nfunction matchesAnyDevice(device: DeviceInfo, targets: TargetDevice[]): boolean {\n\treturn targets.some((target) => matchesDevice(device, target));\n}\n\n/**\n * Determine if a device notification should be sent based on the configuration.\n *\n * Filter priority (highest to lowest):\n * 1. ignoredDevices - if device matches, always return false\n * 2. listenOnlyDevices - if specified, device must match at least one\n * 3. targetDevices - if specified, device must match at least one\n * 4. logicalPortMap - if specified, device location must be in the map\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns true if the device should trigger a notification, false otherwise\n */\nexport function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean {\n\t// Priority 1: Check ignoredDevices (highest priority - always blocks)\n\tif (config.ignoredDevices && config.ignoredDevices.length > 0) {\n\t\tif (matchesAnyDevice(device, config.ignoredDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 2: Check listenOnlyDevices (if specified, device must match)\n\tif (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.listenOnlyDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 3: Check targetDevices (if specified, device must match)\n\tif (config.targetDevices && config.targetDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.targetDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 4: Check logicalPortMap (if specified, device must be mapped)\n\tif (config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns Device info with logicalPort set if mapped, otherwise unchanged\n */\nexport function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo {\n\tif (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {\n\t\treturn {\n\t\t\t...device,\n\t\t\tlogicalPort: config.logicalPortMap[device.locationInfo],\n\t\t};\n\t}\n\treturn device;\n}\n", "import usbListener, { type DeviceInfo, type ListenerConfig } from \"../index\";\n\n// Type-safe configuration - no casting needed!\nconst config: ListenerConfig = {\n\tlogicalPortMap: {\n\t\t\"Port_#0005.Hub_#0002\": 1,\n\t\t\"Port_#0006.Hub_#0002\": 2,\n\t},\n\tignoredDevices: [],\n\tlistenOnlyDevices: [],\n};\n\n// Type-safe callback with full IntelliSense\nusbListener.onDeviceAdd((device: DeviceInfo) => {\n\tconsole.log(\"Device connected:\");\n\tconsole.log(\" Device ID:\", device.deviceId);\n\tconsole.log(` VID: 0x${device.vid.toString(16).toUpperCase().padStart(4, \"0\")}`);\n\tconsole.log(` PID: 0x${device.pid.toString(16).toUpperCase().padStart(4, \"0\")}`);\n\tconsole.log(\" Location:\", device.locationInfo);\n\tconsole.log(\" Logical Port:\", device.logicalPort !== null ? device.logicalPort : \"Not mapped\");\n\tconsole.log(\"\");\n});\n\nusbListener.onDeviceRemove((device: DeviceInfo) => {\n\tconsole.log(\"Device disconnected:\");\n\tconsole.log(\" Device ID:\", device.deviceId);\n\tconsole.log(` VID: 0x${device.vid.toString(16).toUpperCase().padStart(4, \"0\")}`);\n\tconsole.log(` PID: 0x${device.pid.toString(16).toUpperCase().padStart(4, \"0\")}`);\n\tconsole.log(\" Location:\", device.locationInfo);\n\tconsole.log(\" Logical Port:\", device.logicalPort !== null ? device.logicalPort : \"Not mapped\");\n\tconsole.log(\"\");\n});\n\ntry {\n\tconsole.log(\"Starting USB device listener...\");\n\tconsole.log(\"Config:\", JSON.stringify(config, null, 2));\n\tconsole.log(\"\");\n\n\tusbListener.startListening(config);\n\n\tconsole.log(\"Listening for USB device events. Press Ctrl+C to stop.\");\n\tconsole.log(\"\");\n} catch (error) {\n\tif (error instanceof Error) {\n\t\tconsole.error(\"Error starting listener:\", error.message);\n\t}\n\tprocess.exit(1);\n}\n\nprocess.on(\"SIGINT\", () => {\n\tconsole.log(\"\\nStopping listener...\");\n\tusbListener.stopListening();\n\tprocess.exit(0);\n});\n\nprocess.on(\"SIGTERM\", () => {\n\tconsole.log(\"\\nStopping listener...\");\n\tusbListener.stopListening();\n\tprocess.exit(0);\n});\n"],
5
+ "mappings": "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",
6
+ "names": ["config", "require", "config"]
7
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=list-devices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-devices.d.ts","sourceRoot":"","sources":["../../src/examples/list-devices.ts"],"names":[],"mappings":""}
@@ -0,0 +1,162 @@
1
+ // src/index.ts
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ // src/device-filter.ts
7
+ function toHexString(value) {
8
+ return value.toString(16).toUpperCase().padStart(4, "0");
9
+ }
10
+ function matchesDevice(device, target) {
11
+ const deviceVid = toHexString(device.vid);
12
+ const devicePid = toHexString(device.pid);
13
+ const targetVid = target.vid.toUpperCase();
14
+ const targetPid = target.pid.toUpperCase();
15
+ return deviceVid === targetVid && devicePid === targetPid;
16
+ }
17
+ function matchesAnyDevice(device, targets) {
18
+ return targets.some((target) => matchesDevice(device, target));
19
+ }
20
+ function shouldNotifyDevice(device, config) {
21
+ if (config.ignoredDevices && config.ignoredDevices.length > 0) {
22
+ if (matchesAnyDevice(device, config.ignoredDevices)) {
23
+ return false;
24
+ }
25
+ }
26
+ if (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {
27
+ if (!matchesAnyDevice(device, config.listenOnlyDevices)) {
28
+ return false;
29
+ }
30
+ }
31
+ if (config.targetDevices && config.targetDevices.length > 0) {
32
+ if (!matchesAnyDevice(device, config.targetDevices)) {
33
+ return false;
34
+ }
35
+ }
36
+ if (config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0) {
37
+ if (!(device.locationInfo in config.logicalPortMap)) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+ function applyLogicalPortMapping(device, config) {
44
+ if (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {
45
+ return {
46
+ ...device,
47
+ logicalPort: config.logicalPortMap[device.locationInfo]
48
+ };
49
+ }
50
+ return device;
51
+ }
52
+
53
+ // src/index.ts
54
+ var __filename = fileURLToPath(import.meta.url);
55
+ var __dirname = dirname(__filename);
56
+ var packageRoot = dirname(__dirname);
57
+ function loadNativeAddon() {
58
+ const require2 = createRequire(import.meta.url);
59
+ const addonPath = join(packageRoot, "build", "Release", "@mcesystems+usb-device-listener.node");
60
+ try {
61
+ return require2(addonPath);
62
+ } catch {
63
+ try {
64
+ const nodeGypBuild = require2("node-gyp-build");
65
+ return nodeGypBuild(packageRoot);
66
+ } catch {
67
+ throw new Error(
68
+ `Failed to load native addon. Tried:
69
+ 1. Direct path: ${addonPath}
70
+ 2. node-gyp-build search
71
+ Make sure the addon is built with 'npm run rebuild' or 'npm run prebuild'`
72
+ );
73
+ }
74
+ }
75
+ }
76
+ var addon = loadNativeAddon();
77
+ var UsbDeviceListenerImpl = class {
78
+ config = {};
79
+ userAddCallback = null;
80
+ userRemoveCallback = null;
81
+ /**
82
+ * Start listening for USB device events
83
+ */
84
+ startListening(config) {
85
+ if (typeof config !== "object" || config === null) {
86
+ throw new TypeError("Config must be an object");
87
+ }
88
+ this.config = config;
89
+ addon.startListening();
90
+ }
91
+ /**
92
+ * Stop listening for USB device events
93
+ */
94
+ stopListening() {
95
+ addon.stopListening();
96
+ }
97
+ /**
98
+ * Register callback for device connection events
99
+ */
100
+ onDeviceAdd(callback) {
101
+ if (typeof callback !== "function") {
102
+ throw new TypeError("Callback must be a function");
103
+ }
104
+ this.userAddCallback = callback;
105
+ addon.onDeviceAdd((device) => {
106
+ if (shouldNotifyDevice(device, this.config)) {
107
+ const enrichedDevice = applyLogicalPortMapping(device, this.config);
108
+ this.userAddCallback?.(enrichedDevice);
109
+ }
110
+ });
111
+ }
112
+ /**
113
+ * Register callback for device disconnection events
114
+ */
115
+ onDeviceRemove(callback) {
116
+ if (typeof callback !== "function") {
117
+ throw new TypeError("Callback must be a function");
118
+ }
119
+ this.userRemoveCallback = callback;
120
+ addon.onDeviceRemove((device) => {
121
+ if (shouldNotifyDevice(device, this.config)) {
122
+ const enrichedDevice = applyLogicalPortMapping(device, this.config);
123
+ this.userRemoveCallback?.(enrichedDevice);
124
+ }
125
+ });
126
+ }
127
+ /**
128
+ * List all currently connected USB devices
129
+ */
130
+ listDevices() {
131
+ return addon.listDevices();
132
+ }
133
+ };
134
+ var usbDeviceListener = new UsbDeviceListenerImpl();
135
+ var index_default = usbDeviceListener;
136
+
137
+ // src/examples/list-devices.ts
138
+ console.log("Listing all connected USB devices:\n");
139
+ try {
140
+ const devices = index_default.listDevices();
141
+ if (devices.length === 0) {
142
+ console.log("No USB devices found.");
143
+ } else {
144
+ devices.forEach((device, index) => {
145
+ console.log(`Device ${index + 1}:`);
146
+ console.log(` Device ID: ${device.deviceId}`);
147
+ console.log(` VID: 0x${device.vid.toString(16).toUpperCase().padStart(4, "0")}`);
148
+ console.log(` PID: 0x${device.pid.toString(16).toUpperCase().padStart(4, "0")}`);
149
+ console.log(` Location Info (mapping key): ${device.locationInfo}`);
150
+ console.log("");
151
+ });
152
+ console.log(`Total devices: ${devices.length}`);
153
+ console.log(
154
+ '\nTo add a device to your config, use the "Location Info" as the key in the logicalPortMap.'
155
+ );
156
+ }
157
+ } catch (error) {
158
+ if (error instanceof Error) {
159
+ console.error("Error listing devices:", error.message);
160
+ }
161
+ }
162
+ //# sourceMappingURL=list-devices.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/index.ts", "../../src/device-filter.ts", "../../src/examples/list-devices.ts"],
4
+ "sourcesContent": ["\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\nimport type {\n\tDeviceAddCallback,\n\tDeviceInfo,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tUsbDeviceListener,\n} from \"./types\";\n\n/**\n * Native addon interface\n * This is the raw C++ addon loaded via N-API\n */\ninterface NativeAddon {\n\tstartListening(): void;\n\tstopListening(): void;\n\tonDeviceAdd(callback: DeviceAddCallback): void;\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\n\tlistDevices(): DeviceInfo[];\n}\n\n// ESM compatibility: get __dirname equivalent\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst packageRoot = dirname(__dirname);\n\n/**\n * Load native addon\n * In development: uses node-gyp-build to find the addon\n * In production: loads directly from the known build location\n */\nfunction loadNativeAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\n\t// Try to load from the known production location first\n\tconst addonPath = join(packageRoot, \"build\", \"Release\", \"@mcesystems+usb-device-listener.node\");\n\n\ttry {\n\t\treturn require(addonPath);\n\t} catch {\n\t\t// Fallback to node-gyp-build for development (if available)\n\t\ttry {\n\t\t\tconst nodeGypBuild = require(\"node-gyp-build\");\n\t\t\treturn nodeGypBuild(packageRoot);\n\t\t} catch {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to load native addon. Tried:\\n 1. Direct path: ${addonPath}\\n 2. node-gyp-build search\\nMake sure the addon is built with 'npm run rebuild' or 'npm run prebuild'`\n\t\t\t);\n\t\t}\n\t}\n}\n\nconst addon = loadNativeAddon();\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nclass UsbDeviceListenerImpl implements UsbDeviceListener {\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\t/**\n\t * Start listening for USB device events\n\t */\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t\taddon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\taddon.stopListening();\n\t}\n\n\t/**\n\t * Register callback for device connection events\n\t */\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userAddCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\taddon.onDeviceAdd((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userAddCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Register callback for device disconnection events\n\t */\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userRemoveCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\taddon.onDeviceRemove((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * List all currently connected USB devices\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\treturn addon.listDevices();\n\t}\n}\n\n// Export singleton instance\nconst usbDeviceListener: UsbDeviceListener = new UsbDeviceListenerImpl();\n\nexport default usbDeviceListener;\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tTargetDevice,\n\tUsbDeviceListener,\n} from \"./types\";\n", "import type { DeviceInfo, ListenerConfig, TargetDevice } from \"./types\";\n\n/**\n * Convert a decimal VID/PID to uppercase hex string for comparison\n */\nfunction toHexString(value: number): string {\n\treturn value.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/**\n * Check if a device matches a target device filter by VID/PID\n */\nfunction matchesDevice(device: DeviceInfo, target: TargetDevice): boolean {\n\tconst deviceVid = toHexString(device.vid);\n\tconst devicePid = toHexString(device.pid);\n\tconst targetVid = target.vid.toUpperCase();\n\tconst targetPid = target.pid.toUpperCase();\n\n\treturn deviceVid === targetVid && devicePid === targetPid;\n}\n\n/**\n * Check if a device matches any device in a list of target devices\n */\nfunction matchesAnyDevice(device: DeviceInfo, targets: TargetDevice[]): boolean {\n\treturn targets.some((target) => matchesDevice(device, target));\n}\n\n/**\n * Determine if a device notification should be sent based on the configuration.\n *\n * Filter priority (highest to lowest):\n * 1. ignoredDevices - if device matches, always return false\n * 2. listenOnlyDevices - if specified, device must match at least one\n * 3. targetDevices - if specified, device must match at least one\n * 4. logicalPortMap - if specified, device location must be in the map\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns true if the device should trigger a notification, false otherwise\n */\nexport function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean {\n\t// Priority 1: Check ignoredDevices (highest priority - always blocks)\n\tif (config.ignoredDevices && config.ignoredDevices.length > 0) {\n\t\tif (matchesAnyDevice(device, config.ignoredDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 2: Check listenOnlyDevices (if specified, device must match)\n\tif (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.listenOnlyDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 3: Check targetDevices (if specified, device must match)\n\tif (config.targetDevices && config.targetDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.targetDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 4: Check logicalPortMap (if specified, device must be mapped)\n\tif (config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns Device info with logicalPort set if mapped, otherwise unchanged\n */\nexport function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo {\n\tif (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {\n\t\treturn {\n\t\t\t...device,\n\t\t\tlogicalPort: config.logicalPortMap[device.locationInfo],\n\t\t};\n\t}\n\treturn device;\n}\n", "import usbListener, { type DeviceInfo } from \"../index\";\n\nconsole.log(\"Listing all connected USB devices:\\n\");\n\ntry {\n\tconst devices: DeviceInfo[] = usbListener.listDevices();\n\n\tif (devices.length === 0) {\n\t\tconsole.log(\"No USB devices found.\");\n\t} else {\n\t\tdevices.forEach((device: DeviceInfo, index: number) => {\n\t\t\tconsole.log(`Device ${index + 1}:`);\n\t\t\tconsole.log(` Device ID: ${device.deviceId}`);\n\t\t\tconsole.log(` VID: 0x${device.vid.toString(16).toUpperCase().padStart(4, \"0\")}`);\n\t\t\tconsole.log(` PID: 0x${device.pid.toString(16).toUpperCase().padStart(4, \"0\")}`);\n\t\t\tconsole.log(` Location Info (mapping key): ${device.locationInfo}`);\n\t\t\tconsole.log(\"\");\n\t\t});\n\n\t\tconsole.log(`Total devices: ${devices.length}`);\n\t\tconsole.log(\n\t\t\t'\\nTo add a device to your config, use the \"Location Info\" as the key in the logicalPortMap.'\n\t\t);\n\t}\n} catch (error) {\n\tif (error instanceof Error) {\n\t\tconsole.error(\"Error listing devices:\", error.message);\n\t}\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["require"]
7
+ }
package/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { UsbDeviceListener } from "./types";
2
+ declare const usbDeviceListener: UsbDeviceListener;
3
+ export default usbDeviceListener;
4
+ export type { DeviceInfo, DeviceAddCallback, DeviceRemoveCallback, ListenerConfig, TargetDevice, UsbDeviceListener, } from "./types";
5
+ //# sourceMappingURL=index.d.ts.map
package/index.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAKX,iBAAiB,EACjB,MAAM,SAAS,CAAC;AAwHjB,QAAA,MAAM,iBAAiB,EAAE,iBAA+C,CAAC;AAEzE,eAAe,iBAAiB,CAAC;AACjC,YAAY,EACX,UAAU,EACV,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,YAAY,EACZ,iBAAiB,GACjB,MAAM,SAAS,CAAC"}
package/index.js ADDED
@@ -0,0 +1,139 @@
1
+ // src/index.ts
2
+ import { createRequire } from "node:module";
3
+ import { dirname, join } from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ // src/device-filter.ts
7
+ function toHexString(value) {
8
+ return value.toString(16).toUpperCase().padStart(4, "0");
9
+ }
10
+ function matchesDevice(device, target) {
11
+ const deviceVid = toHexString(device.vid);
12
+ const devicePid = toHexString(device.pid);
13
+ const targetVid = target.vid.toUpperCase();
14
+ const targetPid = target.pid.toUpperCase();
15
+ return deviceVid === targetVid && devicePid === targetPid;
16
+ }
17
+ function matchesAnyDevice(device, targets) {
18
+ return targets.some((target) => matchesDevice(device, target));
19
+ }
20
+ function shouldNotifyDevice(device, config) {
21
+ if (config.ignoredDevices && config.ignoredDevices.length > 0) {
22
+ if (matchesAnyDevice(device, config.ignoredDevices)) {
23
+ return false;
24
+ }
25
+ }
26
+ if (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {
27
+ if (!matchesAnyDevice(device, config.listenOnlyDevices)) {
28
+ return false;
29
+ }
30
+ }
31
+ if (config.targetDevices && config.targetDevices.length > 0) {
32
+ if (!matchesAnyDevice(device, config.targetDevices)) {
33
+ return false;
34
+ }
35
+ }
36
+ if (config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0) {
37
+ if (!(device.locationInfo in config.logicalPortMap)) {
38
+ return false;
39
+ }
40
+ }
41
+ return true;
42
+ }
43
+ function applyLogicalPortMapping(device, config) {
44
+ if (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {
45
+ return {
46
+ ...device,
47
+ logicalPort: config.logicalPortMap[device.locationInfo]
48
+ };
49
+ }
50
+ return device;
51
+ }
52
+
53
+ // src/index.ts
54
+ var __filename = fileURLToPath(import.meta.url);
55
+ var __dirname = dirname(__filename);
56
+ var packageRoot = dirname(__dirname);
57
+ function loadNativeAddon() {
58
+ const require2 = createRequire(import.meta.url);
59
+ const addonPath = join(packageRoot, "build", "Release", "@mcesystems+usb-device-listener.node");
60
+ try {
61
+ return require2(addonPath);
62
+ } catch {
63
+ try {
64
+ const nodeGypBuild = require2("node-gyp-build");
65
+ return nodeGypBuild(packageRoot);
66
+ } catch {
67
+ throw new Error(
68
+ `Failed to load native addon. Tried:
69
+ 1. Direct path: ${addonPath}
70
+ 2. node-gyp-build search
71
+ Make sure the addon is built with 'npm run rebuild' or 'npm run prebuild'`
72
+ );
73
+ }
74
+ }
75
+ }
76
+ var addon = loadNativeAddon();
77
+ var UsbDeviceListenerImpl = class {
78
+ config = {};
79
+ userAddCallback = null;
80
+ userRemoveCallback = null;
81
+ /**
82
+ * Start listening for USB device events
83
+ */
84
+ startListening(config) {
85
+ if (typeof config !== "object" || config === null) {
86
+ throw new TypeError("Config must be an object");
87
+ }
88
+ this.config = config;
89
+ addon.startListening();
90
+ }
91
+ /**
92
+ * Stop listening for USB device events
93
+ */
94
+ stopListening() {
95
+ addon.stopListening();
96
+ }
97
+ /**
98
+ * Register callback for device connection events
99
+ */
100
+ onDeviceAdd(callback) {
101
+ if (typeof callback !== "function") {
102
+ throw new TypeError("Callback must be a function");
103
+ }
104
+ this.userAddCallback = callback;
105
+ addon.onDeviceAdd((device) => {
106
+ if (shouldNotifyDevice(device, this.config)) {
107
+ const enrichedDevice = applyLogicalPortMapping(device, this.config);
108
+ this.userAddCallback?.(enrichedDevice);
109
+ }
110
+ });
111
+ }
112
+ /**
113
+ * Register callback for device disconnection events
114
+ */
115
+ onDeviceRemove(callback) {
116
+ if (typeof callback !== "function") {
117
+ throw new TypeError("Callback must be a function");
118
+ }
119
+ this.userRemoveCallback = callback;
120
+ addon.onDeviceRemove((device) => {
121
+ if (shouldNotifyDevice(device, this.config)) {
122
+ const enrichedDevice = applyLogicalPortMapping(device, this.config);
123
+ this.userRemoveCallback?.(enrichedDevice);
124
+ }
125
+ });
126
+ }
127
+ /**
128
+ * List all currently connected USB devices
129
+ */
130
+ listDevices() {
131
+ return addon.listDevices();
132
+ }
133
+ };
134
+ var usbDeviceListener = new UsbDeviceListenerImpl();
135
+ var index_default = usbDeviceListener;
136
+ export {
137
+ index_default as default
138
+ };
139
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../src/device-filter.ts"],
4
+ "sourcesContent": ["\nimport { createRequire } from \"node:module\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { applyLogicalPortMapping, shouldNotifyDevice } from \"./device-filter\";\nimport type {\n\tDeviceAddCallback,\n\tDeviceInfo,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tUsbDeviceListener,\n} from \"./types\";\n\n/**\n * Native addon interface\n * This is the raw C++ addon loaded via N-API\n */\ninterface NativeAddon {\n\tstartListening(): void;\n\tstopListening(): void;\n\tonDeviceAdd(callback: DeviceAddCallback): void;\n\tonDeviceRemove(callback: DeviceRemoveCallback): void;\n\tlistDevices(): DeviceInfo[];\n}\n\n// ESM compatibility: get __dirname equivalent\nconst __filename = fileURLToPath(import.meta.url);\nconst __dirname = dirname(__filename);\n\nconst packageRoot = dirname(__dirname);\n\n/**\n * Load native addon\n * In development: uses node-gyp-build to find the addon\n * In production: loads directly from the known build location\n */\nfunction loadNativeAddon(): NativeAddon {\n\tconst require = createRequire(import.meta.url);\n\n\t// Try to load from the known production location first\n\tconst addonPath = join(packageRoot, \"build\", \"Release\", \"@mcesystems+usb-device-listener.node\");\n\n\ttry {\n\t\treturn require(addonPath);\n\t} catch {\n\t\t// Fallback to node-gyp-build for development (if available)\n\t\ttry {\n\t\t\tconst nodeGypBuild = require(\"node-gyp-build\");\n\t\t\treturn nodeGypBuild(packageRoot);\n\t\t} catch {\n\t\t\tthrow new Error(\n\t\t\t\t`Failed to load native addon. Tried:\\n 1. Direct path: ${addonPath}\\n 2. node-gyp-build search\\nMake sure the addon is built with 'npm run rebuild' or 'npm run prebuild'`\n\t\t\t);\n\t\t}\n\t}\n}\n\nconst addon = loadNativeAddon();\n\n/**\n * USB Device Listener implementation\n * Provides a type-safe wrapper around the native C++ addon\n */\nclass UsbDeviceListenerImpl implements UsbDeviceListener {\n\tprivate config: ListenerConfig = {};\n\tprivate userAddCallback: DeviceAddCallback | null = null;\n\tprivate userRemoveCallback: DeviceRemoveCallback | null = null;\n\n\t/**\n\t * Start listening for USB device events\n\t */\n\tpublic startListening(config: ListenerConfig): void {\n\t\tif (typeof config !== \"object\" || config === null) {\n\t\t\tthrow new TypeError(\"Config must be an object\");\n\t\t}\n\t\tthis.config = config;\n\t\taddon.startListening();\n\t}\n\n\t/**\n\t * Stop listening for USB device events\n\t */\n\tpublic stopListening(): void {\n\t\taddon.stopListening();\n\t}\n\n\t/**\n\t * Register callback for device connection events\n\t */\n\tpublic onDeviceAdd(callback: DeviceAddCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userAddCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\taddon.onDeviceAdd((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userAddCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Register callback for device disconnection events\n\t */\n\tpublic onDeviceRemove(callback: DeviceRemoveCallback): void {\n\t\tif (typeof callback !== \"function\") {\n\t\t\tthrow new TypeError(\"Callback must be a function\");\n\t\t}\n\t\tthis.userRemoveCallback = callback;\n\n\t\t// Set up internal callback that filters devices\n\t\taddon.onDeviceRemove((device: DeviceInfo) => {\n\t\t\tif (shouldNotifyDevice(device, this.config)) {\n\t\t\t\tconst enrichedDevice = applyLogicalPortMapping(device, this.config);\n\t\t\t\tthis.userRemoveCallback?.(enrichedDevice);\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * List all currently connected USB devices\n\t */\n\tpublic listDevices(): DeviceInfo[] {\n\t\treturn addon.listDevices();\n\t}\n}\n\n// Export singleton instance\nconst usbDeviceListener: UsbDeviceListener = new UsbDeviceListenerImpl();\n\nexport default usbDeviceListener;\nexport type {\n\tDeviceInfo,\n\tDeviceAddCallback,\n\tDeviceRemoveCallback,\n\tListenerConfig,\n\tTargetDevice,\n\tUsbDeviceListener,\n} from \"./types\";\n", "import type { DeviceInfo, ListenerConfig, TargetDevice } from \"./types\";\n\n/**\n * Convert a decimal VID/PID to uppercase hex string for comparison\n */\nfunction toHexString(value: number): string {\n\treturn value.toString(16).toUpperCase().padStart(4, \"0\");\n}\n\n/**\n * Check if a device matches a target device filter by VID/PID\n */\nfunction matchesDevice(device: DeviceInfo, target: TargetDevice): boolean {\n\tconst deviceVid = toHexString(device.vid);\n\tconst devicePid = toHexString(device.pid);\n\tconst targetVid = target.vid.toUpperCase();\n\tconst targetPid = target.pid.toUpperCase();\n\n\treturn deviceVid === targetVid && devicePid === targetPid;\n}\n\n/**\n * Check if a device matches any device in a list of target devices\n */\nfunction matchesAnyDevice(device: DeviceInfo, targets: TargetDevice[]): boolean {\n\treturn targets.some((target) => matchesDevice(device, target));\n}\n\n/**\n * Determine if a device notification should be sent based on the configuration.\n *\n * Filter priority (highest to lowest):\n * 1. ignoredDevices - if device matches, always return false\n * 2. listenOnlyDevices - if specified, device must match at least one\n * 3. targetDevices - if specified, device must match at least one\n * 4. logicalPortMap - if specified, device location must be in the map\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns true if the device should trigger a notification, false otherwise\n */\nexport function shouldNotifyDevice(device: DeviceInfo, config: ListenerConfig): boolean {\n\t// Priority 1: Check ignoredDevices (highest priority - always blocks)\n\tif (config.ignoredDevices && config.ignoredDevices.length > 0) {\n\t\tif (matchesAnyDevice(device, config.ignoredDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 2: Check listenOnlyDevices (if specified, device must match)\n\tif (config.listenOnlyDevices && config.listenOnlyDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.listenOnlyDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 3: Check targetDevices (if specified, device must match)\n\tif (config.targetDevices && config.targetDevices.length > 0) {\n\t\tif (!matchesAnyDevice(device, config.targetDevices)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// Priority 4: Check logicalPortMap (if specified, device must be mapped)\n\tif (config.logicalPortMap && Object.keys(config.logicalPortMap).length > 0) {\n\t\tif (!(device.locationInfo in config.logicalPortMap)) {\n\t\t\treturn false;\n\t\t}\n\t}\n\n\treturn true;\n}\n\n/**\n * Apply logical port mapping to a device if configured\n *\n * @param device - The device information from the native addon\n * @param config - The listener configuration\n * @returns Device info with logicalPort set if mapped, otherwise unchanged\n */\nexport function applyLogicalPortMapping(device: DeviceInfo, config: ListenerConfig): DeviceInfo {\n\tif (config.logicalPortMap && device.locationInfo in config.logicalPortMap) {\n\t\treturn {\n\t\t\t...device,\n\t\t\tlogicalPort: config.logicalPortMap[device.locationInfo],\n\t\t};\n\t}\n\treturn device;\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["require"]
7
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@mcesystems/usb-device-listener",
3
+ "version": "1.0.0",
4
+ "description": "Native Windows USB device listener using PnP notifications without custom drivers",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "types": "index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./index.js",
11
+ "types": "./index.d.ts"
12
+ }
13
+ },
14
+ "keywords": [
15
+ "usb",
16
+ "device",
17
+ "listener",
18
+ "monitor",
19
+ "hotplug",
20
+ "pnp",
21
+ "windows",
22
+ "native",
23
+ "addon",
24
+ "n-api",
25
+ "typescript",
26
+ "esm",
27
+ "device-detection",
28
+ "hardware"
29
+ ],
30
+ "author": "USB Device Listener Contributors",
31
+ "license": "ISC",
32
+ "dependencies": {
33
+ "node-addon-api": "^8.2.1"
34
+ },
35
+ "files": ["*.js", "*.d.ts", "*.d.ts.map", "*.js.map", "examples", "build"],
36
+ "os": ["win32"],
37
+ "engines": {
38
+ "node": ">=20.0.0"
39
+ }
40
+ }
package/types.d.ts ADDED
@@ -0,0 +1,195 @@
1
+ /**
2
+ * USB Device Listener Type Definitions
3
+ *
4
+ * Type-safe interface for monitoring USB device connections and disconnections
5
+ */
6
+ /**
7
+ * Device information returned by the listener
8
+ */
9
+ export interface DeviceInfo {
10
+ /**
11
+ * Windows device instance ID
12
+ * Format: "USB\VID_xxxx&PID_xxxx\..."
13
+ */
14
+ deviceId: string;
15
+ /**
16
+ * USB Vendor ID (decimal)
17
+ * Use .toString(16) to convert to hex string
18
+ */
19
+ vid: number;
20
+ /**
21
+ * USB Product ID (decimal)
22
+ * Use .toString(16) to convert to hex string
23
+ */
24
+ pid: number;
25
+ /**
26
+ * Physical USB port location
27
+ * Format: "Port_#xxxx.Hub_#yyyy"
28
+ * This value is stable and can be used as a key for logical port mapping
29
+ */
30
+ locationInfo: string;
31
+ /**
32
+ * User-defined logical port number from configuration
33
+ * null if not mapped in logicalPortMap
34
+ */
35
+ logicalPort: number | null;
36
+ }
37
+ /**
38
+ * Target device filter by VID/PID
39
+ */
40
+ export interface TargetDevice {
41
+ /**
42
+ * USB Vendor ID as hex string (e.g., "04E8")
43
+ */
44
+ vid: string;
45
+ /**
46
+ * USB Product ID as hex string (e.g., "6860")
47
+ */
48
+ pid: string;
49
+ }
50
+ /**
51
+ * Configuration for USB device listener
52
+ */
53
+ export interface ListenerConfig {
54
+ /**
55
+ * Map physical port locations to logical port numbers
56
+ *
57
+ * Key: Location info string (e.g., "Port_#0005.Hub_#0002")
58
+ * Value: Logical port number (e.g., 1, 2, 3)
59
+ *
60
+ * Use listDevices() to discover location strings for your setup
61
+ *
62
+ * @example
63
+ * {
64
+ * "Port_#0005.Hub_#0002": 1,
65
+ * "Port_#0006.Hub_#0002": 2
66
+ * }
67
+ */
68
+ logicalPortMap?: Record<string, number>;
69
+ /**
70
+ * Filter to monitor only specific devices by VID/PID
71
+ *
72
+ * Empty array or undefined = monitor all USB devices
73
+ *
74
+ * @example
75
+ * [
76
+ * { vid: "04E8", pid: "6860" }, // Samsung device
77
+ * { vid: "2341", pid: "0043" } // Arduino Uno
78
+ * ]
79
+ */
80
+ targetDevices?: TargetDevice[];
81
+ /**
82
+ * Devices to ignore by VID/PID
83
+ *
84
+ * Devices matching any entry will not trigger events,
85
+ * even if they match other filters.
86
+ * This takes highest priority over all other filters.
87
+ *
88
+ * @example
89
+ * [
90
+ * { vid: "1234", pid: "5678" } // Ignore this specific device
91
+ * ]
92
+ */
93
+ ignoredDevices?: TargetDevice[];
94
+ /**
95
+ * Only monitor these specific devices by VID/PID
96
+ *
97
+ * Empty array or undefined = monitor all devices (subject to other filters)
98
+ * When set, only devices matching an entry will trigger events.
99
+ *
100
+ * @example
101
+ * [
102
+ * { vid: "04E8", pid: "6860" } // Only listen to this device
103
+ * ]
104
+ */
105
+ listenOnlyDevices?: TargetDevice[];
106
+ }
107
+ /**
108
+ * Callback type for device add events
109
+ */
110
+ export type DeviceAddCallback = (deviceInfo: DeviceInfo) => void;
111
+ /**
112
+ * Callback type for device remove events
113
+ */
114
+ export type DeviceRemoveCallback = (deviceInfo: DeviceInfo) => void;
115
+ /**
116
+ * USB Device Listener Module
117
+ */
118
+ export interface UsbDeviceListener {
119
+ /**
120
+ * Start listening for USB device events
121
+ *
122
+ * Creates a background thread that monitors Windows device change messages.
123
+ * Callbacks registered with onDeviceAdd/onDeviceRemove will be invoked
124
+ * when matching devices are connected or disconnected.
125
+ *
126
+ * @param config - Listener configuration
127
+ * @throws {TypeError} If config is not an object
128
+ * @throws {Error} If listener is already running
129
+ *
130
+ * @example
131
+ * listener.startListening({
132
+ * logicalPortMap: { "Port_#0005.Hub_#0002": 1 },
133
+ * targetDevices: [{ vid: "04E8", pid: "6860" }]
134
+ * });
135
+ */
136
+ startListening(config: ListenerConfig): void;
137
+ /**
138
+ * Stop listening for USB device events
139
+ *
140
+ * Stops the background thread and cleans up resources.
141
+ * Safe to call multiple times.
142
+ *
143
+ * @example
144
+ * listener.stopListening();
145
+ */
146
+ stopListening(): void;
147
+ /**
148
+ * Register callback for device connection events
149
+ *
150
+ * The callback will be invoked on the Node.js main thread whenever
151
+ * a matching USB device is connected.
152
+ *
153
+ * @param callback - Function to call when device is connected
154
+ * @throws {TypeError} If callback is not a function
155
+ *
156
+ * @example
157
+ * listener.onDeviceAdd((device) => {
158
+ * console.log(`Device ${device.vid.toString(16)}:${device.pid.toString(16)}`);
159
+ * console.log(`Connected to port ${device.logicalPort}`);
160
+ * });
161
+ */
162
+ onDeviceAdd(callback: DeviceAddCallback): void;
163
+ /**
164
+ * Register callback for device disconnection events
165
+ *
166
+ * The callback will be invoked on the Node.js main thread whenever
167
+ * a matching USB device is disconnected.
168
+ *
169
+ * @param callback - Function to call when device is disconnected
170
+ * @throws {TypeError} If callback is not a function
171
+ *
172
+ * @example
173
+ * listener.onDeviceRemove((device) => {
174
+ * console.log(`Port ${device.logicalPort} disconnected`);
175
+ * });
176
+ */
177
+ onDeviceRemove(callback: DeviceRemoveCallback): void;
178
+ /**
179
+ * List all currently connected USB devices
180
+ *
181
+ * Returns information about all USB devices currently connected to the system.
182
+ * Useful for discovering device locations and initial state.
183
+ *
184
+ * @returns Array of device information objects
185
+ *
186
+ * @example
187
+ * const devices = listener.listDevices();
188
+ * devices.forEach(device => {
189
+ * console.log(`Location: ${device.locationInfo}`);
190
+ * console.log(`VID:PID = ${device.vid.toString(16)}:${device.pid.toString(16)}`);
191
+ * });
192
+ */
193
+ listDevices(): DeviceInfo[];
194
+ }
195
+ //# sourceMappingURL=types.d.ts.map
package/types.d.ts.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IAEjB;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;OAGG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;;;OAIG;IACH,YAAY,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC5B;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ;;OAEG;IACH,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC9B;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAExC;;;;;;;;;;OAUG;IACH,aAAa,CAAC,EAAE,YAAY,EAAE,CAAC;IAE/B;;;;;;;;;;;OAWG;IACH,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC;IAEhC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,YAAY,EAAE,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEjE;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,UAAU,KAAK,IAAI,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;;;;;;;;;;;;OAgBG;IACH,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAAC;IAE7C;;;;;;;;OAQG;IACH,aAAa,IAAI,IAAI,CAAC;IAEtB;;;;;;;;;;;;;;OAcG;IACH,WAAW,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAE/C;;;;;;;;;;;;;OAaG;IACH,cAAc,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI,CAAC;IAErD;;;;;;;;;;;;;;OAcG;IACH,WAAW,IAAI,UAAU,EAAE,CAAC;CAC5B"}