@mcesystems/usb-device-listener 1.0.73 β†’ 1.0.75

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.
Files changed (2) hide show
  1. package/README.md +491 -491
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,491 +1,491 @@
1
- # USB Device Listener
2
-
3
- A high-performance native Node.js addon for monitoring USB device connections and disconnections on Windows and macOS. 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 OS APIs
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
- - πŸ–₯️ **Cross-platform** - Supports both Windows and macOS
14
-
15
- ## Installation
16
-
17
- ```bash
18
- npm install
19
- ```
20
-
21
- ### Requirements
22
-
23
- - **Node.js**: v18+ (ESM support required)
24
-
25
- #### Windows
26
- - **OS**: Windows 10/11
27
- - **Build tools**: Visual Studio 2022 with C++ build tools
28
-
29
- #### macOS
30
- - **OS**: macOS 10.15 (Catalina) or later
31
- - **Build tools**: Xcode Command Line Tools (`xcode-select --install`)
32
-
33
- ## Quick Start
34
-
35
- Examples below use `@mcesystems/tool-debug-g4` for formatted logs.
36
-
37
- ```javascript
38
- import usbListener from "usb-device-listener";
39
- import {
40
- logDataObject,
41
- logErrorObject,
42
- logHeader,
43
- logNamespace,
44
- setLogLevel
45
- } from "@mcesystems/tool-debug-g4";
46
-
47
- logNamespace("usb");
48
- setLogLevel("debug");
49
-
50
- // Define configuration
51
- const config = {
52
- logicalPortMap: {
53
- // Windows format: "Port_#0005.Hub_#0002"
54
- // macOS format: "Port_#14200000"
55
- "Port_#0005.Hub_#0002": 1, // Map physical port to logical port 1
56
- "Port_#0006.Hub_#0002": 2
57
- },
58
- targetDevices: [
59
- { vid: "04E8", pid: "6860" } // Only monitor Samsung devices
60
- ],
61
- ignoredHubs: [],
62
- listenOnlyHubs: []
63
- };
64
-
65
- function handleDeviceAdd(device) {
66
- logDataObject("Device connected", {
67
- locationInfo: device.locationInfo,
68
- vid: device.vid.toString(16).toUpperCase().padStart(4, "0"),
69
- pid: device.pid.toString(16).toUpperCase().padStart(4, "0"),
70
- logicalPort: device.logicalPort ?? "<unmapped>"
71
- });
72
- }
73
-
74
- function handleDeviceRemove(device) {
75
- logDataObject("Device disconnected", {
76
- locationInfo: device.locationInfo,
77
- logicalPort: device.logicalPort ?? "<unmapped>"
78
- });
79
- }
80
-
81
- // Register event handlers
82
- usbListener.onDeviceAdd(handleDeviceAdd);
83
- usbListener.onDeviceRemove(handleDeviceRemove);
84
-
85
- // Start listening
86
- try {
87
- usbListener.startListening(config);
88
- logHeader("Listening for USB events");
89
- } catch (error) {
90
- logErrorObject(error, "Failed to start");
91
- }
92
-
93
- // Graceful shutdown
94
- process.on('SIGINT', () => {
95
- logHeader("Stopping USB listener");
96
- usbListener.stopListening();
97
- process.exit(0);
98
- });
99
- ```
100
- Example output:
101
- ```
102
- usb:info ================================================================================
103
- usb:info Listening for USB events
104
- usb:info ================================================================================
105
- usb:detail ================================================================================
106
- usb:detail *Device connected* handleDeviceAdd
107
- usb:detail ================================================================================
108
- usb:detail
109
- usb:detail # locationInfo: Port_#0005.Hub_#0002
110
- usb:detail # vid: 04E8
111
- usb:detail # pid: 6860
112
- usb:detail # logicalPort: 1
113
- usb:detail
114
- usb:detail ================================================================================
115
- ```
116
-
117
- ## API Reference
118
-
119
- ### `startListening(config)`
120
-
121
- Start monitoring USB device events.
122
-
123
- **Parameters:**
124
- - `config` (Object): Configuration object
125
- - `logicalPortMap` (Object, optional): Map physical locations to logical port numbers
126
- - Key: Location string (platform-specific format)
127
- - Value: Logical port number (integer)
128
- - `targetDevices` (Array, optional): Filter specific devices by VID/PID
129
- - Each element: `{ vid: string, pid: string }` (hex strings, e.g., "04E8")
130
- - Empty array = monitor all devices
131
- - `ignoredHubs` (Array, optional): Hub location strings to ignore
132
- - `listenOnlyHubs` (Array, optional): Only monitor these hub locations
133
-
134
- **Throws:**
135
- - `TypeError` if config is not an object
136
- - `Error` if listener is already running
137
-
138
- **Example:**
139
- ```javascript
140
- usbListener.startListening({
141
- logicalPortMap: {
142
- "Port_#0005.Hub_#0002": 1 // Windows
143
- // "Port_#14200000": 1 // macOS
144
- },
145
- targetDevices: [], // Monitor all devices
146
- ignoredHubs: ["Port_#0001.Hub_#0001"], // Ignore this hub
147
- listenOnlyHubs: [] // No restriction
148
- });
149
- ```
150
-
151
- ### `stopListening()`
152
-
153
- Stop monitoring and clean up resources. Safe to call multiple times.
154
-
155
- **Example:**
156
- ```javascript
157
- usbListener.stopListening();
158
- ```
159
-
160
- ### `updateConfig(config)`
161
-
162
- Update the listener config at runtime. When listening, subsequent device events and `listDevices()` use the new config. When not listening, only `listDevices()` uses it until the next `startListening()`. Config is fully replaced (use `getCurrentConfig()` to merge).
163
-
164
- **Parameters:**
165
- - `config` (Object): Same shape as `startListening(config)` (logicalPortMap, targetDevices, ignoredDevices, listenOnlyDevices)
166
-
167
- **Throws:**
168
- - `TypeError` if config is not an object
169
-
170
- **Example:**
171
- ```javascript
172
- // Replace config entirely
173
- usbListener.updateConfig({ logicalPortMap: newMap, targetDevices: [] });
174
-
175
- // Merge with current config
176
- usbListener.updateConfig({ ...usbListener.getCurrentConfig(), logicalPortMap: newMap });
177
- ```
178
-
179
- ### `getCurrentConfig()`
180
-
181
- Return a copy of the current config. Mutating the returned object does not affect the listener's internal config. Use with `updateConfig()` to merge partial changes.
182
-
183
- **Returns:** Shallow copy of the current config object (ListenerConfig)
184
-
185
- **Example:**
186
- ```javascript
187
- const current = usbListener.getCurrentConfig();
188
- usbListener.updateConfig({
189
- ...current,
190
- targetDevices: [...(current.targetDevices ?? []), { vid: "04E8", pid: "6860" }]
191
- });
192
- ```
193
-
194
- ### `onDeviceAdd(callback)`
195
-
196
- Register callback for device connection events.
197
-
198
- **Parameters:**
199
- - `callback` (Function): Called when device is connected
200
- - `deviceInfo` (Object):
201
- - `deviceId` (string): Platform-specific device instance ID
202
- - `vid` (number): Vendor ID (decimal)
203
- - `pid` (number): Product ID (decimal)
204
- - `locationInfo` (string): Physical port location (platform-specific format)
205
- - `logicalPort` (number|null): Mapped logical port or null
206
-
207
- **Example:**
208
- ```javascript
209
- import { logDataObject } from "@mcesystems/tool-debug-g4";
210
-
211
- usbListener.onDeviceAdd((device) => {
212
- const vidHex = device.vid.toString(16).toUpperCase().padStart(4, "0");
213
- const pidHex = device.pid.toString(16).toUpperCase().padStart(4, "0");
214
- logDataObject("Device connected", {
215
- device: `${vidHex}:${pidHex}`,
216
- port: device.logicalPort ?? "<unmapped>"
217
- });
218
- });
219
- ```
220
-
221
- ### `onDeviceRemove(callback)`
222
-
223
- Register callback for device disconnection events. Device info format same as `onDeviceAdd`.
224
-
225
- **Example:**
226
- ```javascript
227
- import { logDataObject } from "@mcesystems/tool-debug-g4";
228
-
229
- usbListener.onDeviceRemove((device) => {
230
- logDataObject("Device disconnected", {
231
- port: device.logicalPort ?? "<unmapped>"
232
- });
233
- });
234
- ```
235
-
236
- ### `listDevices()`
237
-
238
- Get list of all currently connected USB devices.
239
-
240
- **Returns:** Array of device objects (same format as callback parameter)
241
-
242
- **Example:**
243
- ```javascript
244
- import { logDataObject } from "@mcesystems/tool-debug-g4";
245
-
246
- const devices = usbListener.listDevices();
247
- devices.forEach((device) => {
248
- logDataObject("Device", {
249
- locationInfo: device.locationInfo,
250
- vid: device.vid.toString(16).toUpperCase().padStart(4, "0"),
251
- pid: device.pid.toString(16).toUpperCase().padStart(4, "0")
252
- });
253
- });
254
- ```
255
-
256
- ## Platform-Specific Details
257
-
258
- ### Physical Port Location
259
-
260
- Each platform assigns unique location strings to USB ports:
261
-
262
- #### Windows
263
- - **Format**: `Port_#XXXX.Hub_#YYYY`
264
- - **Example**: `Port_#0005.Hub_#0002`
265
- - **Source**: Windows Device Management API
266
-
267
- #### macOS
268
- - **Format**: `Port_#XXXXXXXX` (hexadecimal location ID)
269
- - **Example**: `Port_#14200000`
270
- - **Source**: IOKit locationID property (encodes bus/port path)
271
-
272
- ### Getting Location Strings
273
-
274
- Use the included `list-devices.js` utility:
275
-
276
- ```bash
277
- node list-devices.js
278
- ```
279
-
280
- **Windows output:**
281
- ```
282
- Device 1:
283
- Device ID: USB\VID_04E8&PID_6860\R58NC2971AJ
284
- VID: 0x04E8
285
- PID: 0x6860
286
- Location Info (mapping key): Port_#0005.Hub_#0002
287
-
288
- Device 2:
289
- Device ID: USB\VID_27C6&PID_6594\UID0014C59F
290
- VID: 0x27C6
291
- PID: 0x6594
292
- Location Info (mapping key): Port_#0007.Hub_#0002
293
- ```
294
-
295
- **macOS output:**
296
- ```
297
- Device 1:
298
- Device ID: USB\VID_04E8&PID_6860\Port_#14200000
299
- VID: 0x04E8
300
- PID: 0x6860
301
- Location Info (mapping key): Port_#14200000
302
-
303
- Device 2:
304
- Device ID: USB\VID_05AC&PID_8262\Port_#14100000
305
- VID: 0x05AC
306
- PID: 0x8262
307
- Location Info (mapping key): Port_#14100000
308
- ```
309
-
310
- Copy the "Location Info" values to use in your `logicalPortMap`.
311
-
312
- ### Device Filtering
313
-
314
- **By VID/PID:**
315
- ```javascript
316
- targetDevices: [
317
- { vid: "2341", pid: "0043" }, // Arduino Uno
318
- { vid: "0483", pid: "5740" } // STM32
319
- ]
320
- ```
321
-
322
- **By Hub:**
323
- ```javascript
324
- listenOnlyHubs: ["Hub_#0002"] // Only monitor this hub (Windows)
325
- // or
326
- ignoredHubs: ["Hub_#0001"] // Ignore this hub
327
- ```
328
-
329
- ## Performance & Scalability
330
-
331
- ### Handling Many Devices
332
-
333
- The listener is designed to handle multiple simultaneous device events efficiently:
334
-
335
- βœ… **Thread-safe**: Device cache protected by mutex
336
- βœ… **Non-blocking**: Runs in separate thread, doesn't block Node.js
337
- βœ… **Efficient**: Only processes filtered devices
338
- βœ… **Memory-safe**: Automatic cleanup on disconnect
339
-
340
- ### Tested Scenarios
341
-
342
- - Multiple rapid connect/disconnect cycles
343
- - Simultaneous connection of 10+ devices
344
- - Hub with many devices
345
- - Long-running processes (hours/days)
346
-
347
- ### Best Practices
348
-
349
- 1. **Use device filtering** when possible to reduce CPU usage:
350
- ```javascript
351
- targetDevices: [{ vid: "04E8", pid: "6860" }] // Better than monitoring all
352
- ```
353
-
354
- 2. **Keep callbacks fast** - offload heavy processing:
355
- ```javascript
356
- onDeviceAdd((device) => {
357
- // Good: Quick database write
358
- db.logConnection(device);
359
-
360
- // Bad: Long synchronous operation
361
- // processLargeFile(device); // Use setTimeout or worker thread instead
362
- });
363
- ```
364
-
365
- 3. **Handle errors gracefully**:
366
- ```javascript
367
- onDeviceAdd((device) => {
368
- try {
369
- processDevice(device);
370
- } catch (error) {
371
- console.error('Device processing failed:', error);
372
- }
373
- });
374
- ```
375
-
376
- 4. **Clean shutdown**:
377
- ```javascript
378
- process.on('SIGINT', () => {
379
- usbListener.stopListening();
380
- // Wait briefly for cleanup
381
- setTimeout(() => process.exit(0), 100);
382
- });
383
- ```
384
-
385
- ## Architecture
386
-
387
- ### Windows
388
-
389
- ```
390
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
391
- β”‚ Node.js Application β”‚
392
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
393
- β”‚ β”‚ JavaScript API β”‚ β”‚
394
- β”‚ β”‚ (index.js - documented) β”‚ β”‚
395
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
396
- β”‚ β”‚ β”‚
397
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
398
- β”‚ β”‚ N-API Addon (addon.cc) β”‚ β”‚
399
- β”‚ β”‚ - Converts JS ↔ C++ types β”‚ β”‚
400
- β”‚ β”‚ - ThreadSafeFunction callbacks β”‚ β”‚
401
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
402
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
403
- β”‚
404
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
405
- β”‚ USBListener (usb_listener_win.cc) β”‚
406
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
407
- β”‚ β”‚ Listener Thread (MessageLoop) β”‚ β”‚
408
- β”‚ β”‚ - Hidden message-only window β”‚ β”‚
409
- β”‚ β”‚ - RegisterDeviceNotification β”‚ β”‚
410
- β”‚ β”‚ - Receives WM_DEVICECHANGE β”‚ β”‚
411
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
412
- β”‚ β”‚ β”‚
413
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
414
- β”‚ β”‚ Windows Device Management API β”‚ β”‚
415
- β”‚ β”‚ - SetupDi* functions β”‚ β”‚
416
- β”‚ β”‚ - CM_Get_DevNode_* β”‚ β”‚
417
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
418
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
419
- ```
420
-
421
- ### macOS
422
-
423
- ```
424
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
425
- β”‚ Node.js Application β”‚
426
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
427
- β”‚ β”‚ JavaScript API β”‚ β”‚
428
- β”‚ β”‚ (index.js - documented) β”‚ β”‚
429
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
430
- β”‚ β”‚ β”‚
431
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
432
- β”‚ β”‚ N-API Addon (addon.cc) β”‚ β”‚
433
- β”‚ β”‚ - Converts JS ↔ C++ types β”‚ β”‚
434
- β”‚ β”‚ - ThreadSafeFunction callbacks β”‚ β”‚
435
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
436
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
437
- β”‚
438
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
439
- β”‚ USBListener (usb_listener_mac.cc) β”‚
440
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
441
- β”‚ β”‚ Listener Thread (CFRunLoop) β”‚ β”‚
442
- β”‚ β”‚ - IONotificationPortRef β”‚ β”‚
443
- β”‚ β”‚ - kIOFirstMatchNotification β”‚ β”‚
444
- β”‚ β”‚ - kIOTerminatedNotification β”‚ β”‚
445
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
446
- β”‚ β”‚ β”‚
447
- β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
448
- β”‚ β”‚ IOKit Framework β”‚ β”‚
449
- β”‚ β”‚ - IOServiceMatching β”‚ β”‚
450
- β”‚ β”‚ - IORegistryEntryCreateCFPropertyβ”‚ β”‚
451
- β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
452
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
453
- ```
454
-
455
- ## Troubleshooting
456
-
457
- ### Windows
458
-
459
- **Build errors:**
460
- - Ensure Visual Studio C++ build tools are installed
461
- - Run from "x64 Native Tools Command Prompt"
462
-
463
- ### macOS
464
-
465
- **Build errors:**
466
- - Install Xcode Command Line Tools: `xcode-select --install`
467
- - Ensure you have the latest Xcode version
468
-
469
- **Permission issues:**
470
- - USB access doesn't require special permissions on macOS
471
- - If using App Sandbox, ensure proper entitlements
472
-
473
- ### General
474
-
475
- **No events firing:**
476
- - Check `targetDevices` filter isn't too restrictive
477
- - Verify callbacks are registered before calling `startListening()`
478
- - Use `listDevices()` to confirm devices are visible
479
-
480
- **Incorrect location info:**
481
- - Location strings are generated by the OS
482
- - Different USB controllers may use different formats
483
- - Always use `listDevices()` to get actual location strings
484
-
485
- ## License
486
-
487
- MIT
488
-
489
- ## Contributing
490
-
491
- Issues and pull requests welcome!
1
+ # USB Device Listener
2
+
3
+ A high-performance native Node.js addon for monitoring USB device connections and disconnections on Windows and macOS. 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 OS APIs
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
+ - πŸ–₯️ **Cross-platform** - Supports both Windows and macOS
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install
19
+ ```
20
+
21
+ ### Requirements
22
+
23
+ - **Node.js**: v18+ (ESM support required)
24
+
25
+ #### Windows
26
+ - **OS**: Windows 10/11
27
+ - **Build tools**: Visual Studio 2022 with C++ build tools
28
+
29
+ #### macOS
30
+ - **OS**: macOS 10.15 (Catalina) or later
31
+ - **Build tools**: Xcode Command Line Tools (`xcode-select --install`)
32
+
33
+ ## Quick Start
34
+
35
+ Examples below use `@mcesystems/tool-debug-g4` for formatted logs.
36
+
37
+ ```javascript
38
+ import usbListener from "usb-device-listener";
39
+ import {
40
+ logDataObject,
41
+ logErrorObject,
42
+ logHeader,
43
+ logNamespace,
44
+ setLogLevel
45
+ } from "@mcesystems/tool-debug-g4";
46
+
47
+ logNamespace("usb");
48
+ setLogLevel("debug");
49
+
50
+ // Define configuration
51
+ const config = {
52
+ logicalPortMap: {
53
+ // Windows format: "Port_#0005.Hub_#0002"
54
+ // macOS format: "Port_#14200000"
55
+ "Port_#0005.Hub_#0002": 1, // Map physical port to logical port 1
56
+ "Port_#0006.Hub_#0002": 2
57
+ },
58
+ targetDevices: [
59
+ { vid: "04E8", pid: "6860" } // Only monitor Samsung devices
60
+ ],
61
+ ignoredHubs: [],
62
+ listenOnlyHubs: []
63
+ };
64
+
65
+ function handleDeviceAdd(device) {
66
+ logDataObject("Device connected", {
67
+ locationInfo: device.locationInfo,
68
+ vid: device.vid.toString(16).toUpperCase().padStart(4, "0"),
69
+ pid: device.pid.toString(16).toUpperCase().padStart(4, "0"),
70
+ logicalPort: device.logicalPort ?? "<unmapped>"
71
+ });
72
+ }
73
+
74
+ function handleDeviceRemove(device) {
75
+ logDataObject("Device disconnected", {
76
+ locationInfo: device.locationInfo,
77
+ logicalPort: device.logicalPort ?? "<unmapped>"
78
+ });
79
+ }
80
+
81
+ // Register event handlers
82
+ usbListener.onDeviceAdd(handleDeviceAdd);
83
+ usbListener.onDeviceRemove(handleDeviceRemove);
84
+
85
+ // Start listening
86
+ try {
87
+ usbListener.startListening(config);
88
+ logHeader("Listening for USB events");
89
+ } catch (error) {
90
+ logErrorObject(error, "Failed to start");
91
+ }
92
+
93
+ // Graceful shutdown
94
+ process.on('SIGINT', () => {
95
+ logHeader("Stopping USB listener");
96
+ usbListener.stopListening();
97
+ process.exit(0);
98
+ });
99
+ ```
100
+ Example output:
101
+ ```
102
+ usb:info ================================================================================
103
+ usb:info Listening for USB events
104
+ usb:info ================================================================================
105
+ usb:detail ================================================================================
106
+ usb:detail *Device connected* handleDeviceAdd
107
+ usb:detail ================================================================================
108
+ usb:detail
109
+ usb:detail # locationInfo: Port_#0005.Hub_#0002
110
+ usb:detail # vid: 04E8
111
+ usb:detail # pid: 6860
112
+ usb:detail # logicalPort: 1
113
+ usb:detail
114
+ usb:detail ================================================================================
115
+ ```
116
+
117
+ ## API Reference
118
+
119
+ ### `startListening(config)`
120
+
121
+ Start monitoring USB device events.
122
+
123
+ **Parameters:**
124
+ - `config` (Object): Configuration object
125
+ - `logicalPortMap` (Object, optional): Map physical locations to logical port numbers
126
+ - Key: Location string (platform-specific format)
127
+ - Value: Logical port number (integer)
128
+ - `targetDevices` (Array, optional): Filter specific devices by VID/PID
129
+ - Each element: `{ vid: string, pid: string }` (hex strings, e.g., "04E8")
130
+ - Empty array = monitor all devices
131
+ - `ignoredHubs` (Array, optional): Hub location strings to ignore
132
+ - `listenOnlyHubs` (Array, optional): Only monitor these hub locations
133
+
134
+ **Throws:**
135
+ - `TypeError` if config is not an object
136
+ - `Error` if listener is already running
137
+
138
+ **Example:**
139
+ ```javascript
140
+ usbListener.startListening({
141
+ logicalPortMap: {
142
+ "Port_#0005.Hub_#0002": 1 // Windows
143
+ // "Port_#14200000": 1 // macOS
144
+ },
145
+ targetDevices: [], // Monitor all devices
146
+ ignoredHubs: ["Port_#0001.Hub_#0001"], // Ignore this hub
147
+ listenOnlyHubs: [] // No restriction
148
+ });
149
+ ```
150
+
151
+ ### `stopListening()`
152
+
153
+ Stop monitoring and clean up resources. Safe to call multiple times.
154
+
155
+ **Example:**
156
+ ```javascript
157
+ usbListener.stopListening();
158
+ ```
159
+
160
+ ### `updateConfig(config)`
161
+
162
+ Update the listener config at runtime. When listening, subsequent device events and `listDevices()` use the new config. When not listening, only `listDevices()` uses it until the next `startListening()`. Config is fully replaced (use `getCurrentConfig()` to merge).
163
+
164
+ **Parameters:**
165
+ - `config` (Object): Same shape as `startListening(config)` (logicalPortMap, targetDevices, ignoredDevices, listenOnlyDevices)
166
+
167
+ **Throws:**
168
+ - `TypeError` if config is not an object
169
+
170
+ **Example:**
171
+ ```javascript
172
+ // Replace config entirely
173
+ usbListener.updateConfig({ logicalPortMap: newMap, targetDevices: [] });
174
+
175
+ // Merge with current config
176
+ usbListener.updateConfig({ ...usbListener.getCurrentConfig(), logicalPortMap: newMap });
177
+ ```
178
+
179
+ ### `getCurrentConfig()`
180
+
181
+ Return a copy of the current config. Mutating the returned object does not affect the listener's internal config. Use with `updateConfig()` to merge partial changes.
182
+
183
+ **Returns:** Shallow copy of the current config object (ListenerConfig)
184
+
185
+ **Example:**
186
+ ```javascript
187
+ const current = usbListener.getCurrentConfig();
188
+ usbListener.updateConfig({
189
+ ...current,
190
+ targetDevices: [...(current.targetDevices ?? []), { vid: "04E8", pid: "6860" }]
191
+ });
192
+ ```
193
+
194
+ ### `onDeviceAdd(callback)`
195
+
196
+ Register callback for device connection events.
197
+
198
+ **Parameters:**
199
+ - `callback` (Function): Called when device is connected
200
+ - `deviceInfo` (Object):
201
+ - `deviceId` (string): Platform-specific device instance ID
202
+ - `vid` (number): Vendor ID (decimal)
203
+ - `pid` (number): Product ID (decimal)
204
+ - `locationInfo` (string): Physical port location (platform-specific format)
205
+ - `logicalPort` (number|null): Mapped logical port or null
206
+
207
+ **Example:**
208
+ ```javascript
209
+ import { logDataObject } from "@mcesystems/tool-debug-g4";
210
+
211
+ usbListener.onDeviceAdd((device) => {
212
+ const vidHex = device.vid.toString(16).toUpperCase().padStart(4, "0");
213
+ const pidHex = device.pid.toString(16).toUpperCase().padStart(4, "0");
214
+ logDataObject("Device connected", {
215
+ device: `${vidHex}:${pidHex}`,
216
+ port: device.logicalPort ?? "<unmapped>"
217
+ });
218
+ });
219
+ ```
220
+
221
+ ### `onDeviceRemove(callback)`
222
+
223
+ Register callback for device disconnection events. Device info format same as `onDeviceAdd`.
224
+
225
+ **Example:**
226
+ ```javascript
227
+ import { logDataObject } from "@mcesystems/tool-debug-g4";
228
+
229
+ usbListener.onDeviceRemove((device) => {
230
+ logDataObject("Device disconnected", {
231
+ port: device.logicalPort ?? "<unmapped>"
232
+ });
233
+ });
234
+ ```
235
+
236
+ ### `listDevices()`
237
+
238
+ Get list of all currently connected USB devices.
239
+
240
+ **Returns:** Array of device objects (same format as callback parameter)
241
+
242
+ **Example:**
243
+ ```javascript
244
+ import { logDataObject } from "@mcesystems/tool-debug-g4";
245
+
246
+ const devices = usbListener.listDevices();
247
+ devices.forEach((device) => {
248
+ logDataObject("Device", {
249
+ locationInfo: device.locationInfo,
250
+ vid: device.vid.toString(16).toUpperCase().padStart(4, "0"),
251
+ pid: device.pid.toString(16).toUpperCase().padStart(4, "0")
252
+ });
253
+ });
254
+ ```
255
+
256
+ ## Platform-Specific Details
257
+
258
+ ### Physical Port Location
259
+
260
+ Each platform assigns unique location strings to USB ports:
261
+
262
+ #### Windows
263
+ - **Format**: `Port_#XXXX.Hub_#YYYY`
264
+ - **Example**: `Port_#0005.Hub_#0002`
265
+ - **Source**: Windows Device Management API
266
+
267
+ #### macOS
268
+ - **Format**: `Port_#XXXXXXXX` (hexadecimal location ID)
269
+ - **Example**: `Port_#14200000`
270
+ - **Source**: IOKit locationID property (encodes bus/port path)
271
+
272
+ ### Getting Location Strings
273
+
274
+ Use the included `list-devices.js` utility:
275
+
276
+ ```bash
277
+ node list-devices.js
278
+ ```
279
+
280
+ **Windows output:**
281
+ ```
282
+ Device 1:
283
+ Device ID: USB\VID_04E8&PID_6860\R58NC2971AJ
284
+ VID: 0x04E8
285
+ PID: 0x6860
286
+ Location Info (mapping key): Port_#0005.Hub_#0002
287
+
288
+ Device 2:
289
+ Device ID: USB\VID_27C6&PID_6594\UID0014C59F
290
+ VID: 0x27C6
291
+ PID: 0x6594
292
+ Location Info (mapping key): Port_#0007.Hub_#0002
293
+ ```
294
+
295
+ **macOS output:**
296
+ ```
297
+ Device 1:
298
+ Device ID: USB\VID_04E8&PID_6860\Port_#14200000
299
+ VID: 0x04E8
300
+ PID: 0x6860
301
+ Location Info (mapping key): Port_#14200000
302
+
303
+ Device 2:
304
+ Device ID: USB\VID_05AC&PID_8262\Port_#14100000
305
+ VID: 0x05AC
306
+ PID: 0x8262
307
+ Location Info (mapping key): Port_#14100000
308
+ ```
309
+
310
+ Copy the "Location Info" values to use in your `logicalPortMap`.
311
+
312
+ ### Device Filtering
313
+
314
+ **By VID/PID:**
315
+ ```javascript
316
+ targetDevices: [
317
+ { vid: "2341", pid: "0043" }, // Arduino Uno
318
+ { vid: "0483", pid: "5740" } // STM32
319
+ ]
320
+ ```
321
+
322
+ **By Hub:**
323
+ ```javascript
324
+ listenOnlyHubs: ["Hub_#0002"] // Only monitor this hub (Windows)
325
+ // or
326
+ ignoredHubs: ["Hub_#0001"] // Ignore this hub
327
+ ```
328
+
329
+ ## Performance & Scalability
330
+
331
+ ### Handling Many Devices
332
+
333
+ The listener is designed to handle multiple simultaneous device events efficiently:
334
+
335
+ βœ… **Thread-safe**: Device cache protected by mutex
336
+ βœ… **Non-blocking**: Runs in separate thread, doesn't block Node.js
337
+ βœ… **Efficient**: Only processes filtered devices
338
+ βœ… **Memory-safe**: Automatic cleanup on disconnect
339
+
340
+ ### Tested Scenarios
341
+
342
+ - Multiple rapid connect/disconnect cycles
343
+ - Simultaneous connection of 10+ devices
344
+ - Hub with many devices
345
+ - Long-running processes (hours/days)
346
+
347
+ ### Best Practices
348
+
349
+ 1. **Use device filtering** when possible to reduce CPU usage:
350
+ ```javascript
351
+ targetDevices: [{ vid: "04E8", pid: "6860" }] // Better than monitoring all
352
+ ```
353
+
354
+ 2. **Keep callbacks fast** - offload heavy processing:
355
+ ```javascript
356
+ onDeviceAdd((device) => {
357
+ // Good: Quick database write
358
+ db.logConnection(device);
359
+
360
+ // Bad: Long synchronous operation
361
+ // processLargeFile(device); // Use setTimeout or worker thread instead
362
+ });
363
+ ```
364
+
365
+ 3. **Handle errors gracefully**:
366
+ ```javascript
367
+ onDeviceAdd((device) => {
368
+ try {
369
+ processDevice(device);
370
+ } catch (error) {
371
+ console.error('Device processing failed:', error);
372
+ }
373
+ });
374
+ ```
375
+
376
+ 4. **Clean shutdown**:
377
+ ```javascript
378
+ process.on('SIGINT', () => {
379
+ usbListener.stopListening();
380
+ // Wait briefly for cleanup
381
+ setTimeout(() => process.exit(0), 100);
382
+ });
383
+ ```
384
+
385
+ ## Architecture
386
+
387
+ ### Windows
388
+
389
+ ```
390
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
391
+ β”‚ Node.js Application β”‚
392
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
393
+ β”‚ β”‚ JavaScript API β”‚ β”‚
394
+ β”‚ β”‚ (index.js - documented) β”‚ β”‚
395
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
396
+ β”‚ β”‚ β”‚
397
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
398
+ β”‚ β”‚ N-API Addon (addon.cc) β”‚ β”‚
399
+ β”‚ β”‚ - Converts JS ↔ C++ types β”‚ β”‚
400
+ β”‚ β”‚ - ThreadSafeFunction callbacks β”‚ β”‚
401
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
402
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
403
+ β”‚
404
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
405
+ β”‚ USBListener (usb_listener_win.cc) β”‚
406
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
407
+ β”‚ β”‚ Listener Thread (MessageLoop) β”‚ β”‚
408
+ β”‚ β”‚ - Hidden message-only window β”‚ β”‚
409
+ β”‚ β”‚ - RegisterDeviceNotification β”‚ β”‚
410
+ β”‚ β”‚ - Receives WM_DEVICECHANGE β”‚ β”‚
411
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
412
+ β”‚ β”‚ β”‚
413
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
414
+ β”‚ β”‚ Windows Device Management API β”‚ β”‚
415
+ β”‚ β”‚ - SetupDi* functions β”‚ β”‚
416
+ β”‚ β”‚ - CM_Get_DevNode_* β”‚ β”‚
417
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
418
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
419
+ ```
420
+
421
+ ### macOS
422
+
423
+ ```
424
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
425
+ β”‚ Node.js Application β”‚
426
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
427
+ β”‚ β”‚ JavaScript API β”‚ β”‚
428
+ β”‚ β”‚ (index.js - documented) β”‚ β”‚
429
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
430
+ β”‚ β”‚ β”‚
431
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
432
+ β”‚ β”‚ N-API Addon (addon.cc) β”‚ β”‚
433
+ β”‚ β”‚ - Converts JS ↔ C++ types β”‚ β”‚
434
+ β”‚ β”‚ - ThreadSafeFunction callbacks β”‚ β”‚
435
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
436
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
437
+ β”‚
438
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
439
+ β”‚ USBListener (usb_listener_mac.cc) β”‚
440
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
441
+ β”‚ β”‚ Listener Thread (CFRunLoop) β”‚ β”‚
442
+ β”‚ β”‚ - IONotificationPortRef β”‚ β”‚
443
+ β”‚ β”‚ - kIOFirstMatchNotification β”‚ β”‚
444
+ β”‚ β”‚ - kIOTerminatedNotification β”‚ β”‚
445
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
446
+ β”‚ β”‚ β”‚
447
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
448
+ β”‚ β”‚ IOKit Framework β”‚ β”‚
449
+ β”‚ β”‚ - IOServiceMatching β”‚ β”‚
450
+ β”‚ β”‚ - IORegistryEntryCreateCFPropertyβ”‚ β”‚
451
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
452
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
453
+ ```
454
+
455
+ ## Troubleshooting
456
+
457
+ ### Windows
458
+
459
+ **Build errors:**
460
+ - Ensure Visual Studio C++ build tools are installed
461
+ - Run from "x64 Native Tools Command Prompt"
462
+
463
+ ### macOS
464
+
465
+ **Build errors:**
466
+ - Install Xcode Command Line Tools: `xcode-select --install`
467
+ - Ensure you have the latest Xcode version
468
+
469
+ **Permission issues:**
470
+ - USB access doesn't require special permissions on macOS
471
+ - If using App Sandbox, ensure proper entitlements
472
+
473
+ ### General
474
+
475
+ **No events firing:**
476
+ - Check `targetDevices` filter isn't too restrictive
477
+ - Verify callbacks are registered before calling `startListening()`
478
+ - Use `listDevices()` to confirm devices are visible
479
+
480
+ **Incorrect location info:**
481
+ - Location strings are generated by the OS
482
+ - Different USB controllers may use different formats
483
+ - Always use `listDevices()` to get actual location strings
484
+
485
+ ## License
486
+
487
+ MIT
488
+
489
+ ## Contributing
490
+
491
+ Issues and pull requests welcome!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcesystems/usb-device-listener",
3
- "version": "1.0.73",
3
+ "version": "1.0.75",
4
4
  "description": "Native cross-platform USB device listener for Windows and macOS",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",