@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 +323 -0
- package/build/Release/@mcesystems+usb-device-listener.node +0 -0
- package/device-filter.d.ts +24 -0
- package/device-filter.d.ts.map +1 -0
- package/examples/example.d.ts +2 -0
- package/examples/example.d.ts.map +1 -0
- package/examples/example.js +187 -0
- package/examples/example.js.map +7 -0
- package/examples/list-devices.d.ts +2 -0
- package/examples/list-devices.d.ts.map +1 -0
- package/examples/list-devices.js +162 -0
- package/examples/list-devices.js.map +7 -0
- package/index.d.ts +5 -0
- package/index.d.ts.map +1 -0
- package/index.js +139 -0
- package/index.js.map +7 -0
- package/package.json +40 -0
- package/types.d.ts +195 -0
- package/types.d.ts.map +1 -0
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!
|
|
Binary file
|
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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": ";AACA,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;;;ACE9B,SAAS,YAAY,OAAuB;AAC3C,SAAO,MAAM,SAAS,EAAE,EAAE,YAAY,EAAE,SAAS,GAAG,GAAG;AACxD;AAKA,SAAS,cAAc,QAAoB,QAA+B;AACzE,QAAM,YAAY,YAAY,OAAO,GAAG;AACxC,QAAM,YAAY,YAAY,OAAO,GAAG;AACxC,QAAM,YAAY,OAAO,IAAI,YAAY;AACzC,QAAM,YAAY,OAAO,IAAI,YAAY;AAEzC,SAAO,cAAc,aAAa,cAAc;AACjD;AAKA,SAAS,iBAAiB,QAAoB,SAAkC;AAC/E,SAAO,QAAQ,KAAK,CAAC,WAAW,cAAc,QAAQ,MAAM,CAAC;AAC9D;AAeO,SAAS,mBAAmB,QAAoB,QAAiC;AAEvF,MAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAC9D,QAAI,iBAAiB,QAAQ,OAAO,cAAc,GAAG;AACpD,aAAO;AAAA,IACR;AAAA,EACD;AAGA,MAAI,OAAO,qBAAqB,OAAO,kBAAkB,SAAS,GAAG;AACpE,QAAI,CAAC,iBAAiB,QAAQ,OAAO,iBAAiB,GAAG;AACxD,aAAO;AAAA,IACR;AAAA,EACD;AAGA,MAAI,OAAO,iBAAiB,OAAO,cAAc,SAAS,GAAG;AAC5D,QAAI,CAAC,iBAAiB,QAAQ,OAAO,aAAa,GAAG;AACpD,aAAO;AAAA,IACR;AAAA,EACD;AAGA,MAAI,OAAO,kBAAkB,OAAO,KAAK,OAAO,cAAc,EAAE,SAAS,GAAG;AAC3E,QAAI,EAAE,OAAO,gBAAgB,OAAO,iBAAiB;AACpD,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SAAO;AACR;AASO,SAAS,wBAAwB,QAAoB,QAAoC;AAC/F,MAAI,OAAO,kBAAkB,OAAO,gBAAgB,OAAO,gBAAgB;AAC1E,WAAO;AAAA,MACN,GAAG;AAAA,MACH,aAAa,OAAO,eAAe,OAAO,YAAY;AAAA,IACvD;AAAA,EACD;AACA,SAAO;AACR;;;AD9DA,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,QAAQ,UAAU;AAEpC,IAAM,cAAc,QAAQ,SAAS;AAOrC,SAAS,kBAA+B;AACvC,QAAMA,WAAU,cAAc,YAAY,GAAG;AAG7C,QAAM,YAAY,KAAK,aAAa,SAAS,WAAW,sCAAsC;AAE9F,MAAI;AACH,WAAOA,SAAQ,SAAS;AAAA,EACzB,QAAQ;AAEP,QAAI;AACH,YAAM,eAAeA,SAAQ,gBAAgB;AAC7C,aAAO,aAAa,WAAW;AAAA,IAChC,QAAQ;AACP,YAAM,IAAI;AAAA,QACT;AAAA,oBAA0D,SAAS;AAAA;AAAA;AAAA,MACpE;AAAA,IACD;AAAA,EACD;AACD;AAEA,IAAM,QAAQ,gBAAgB;AAM9B,IAAM,wBAAN,MAAyD;AAAA,EAChD,SAAyB,CAAC;AAAA,EAC1B,kBAA4C;AAAA,EAC5C,qBAAkD;AAAA;AAAA;AAAA;AAAA,EAKnD,eAAe,QAA8B;AACnD,QAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AAClD,YAAM,IAAI,UAAU,0BAA0B;AAAA,IAC/C;AACA,SAAK,SAAS;AACd,UAAM,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKO,gBAAsB;AAC5B,UAAM,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKO,YAAY,UAAmC;AACrD,QAAI,OAAO,aAAa,YAAY;AACnC,YAAM,IAAI,UAAU,6BAA6B;AAAA,IAClD;AACA,SAAK,kBAAkB;AAGvB,UAAM,YAAY,CAAC,WAAuB;AACzC,UAAI,mBAAmB,QAAQ,KAAK,MAAM,GAAG;AAC5C,cAAM,iBAAiB,wBAAwB,QAAQ,KAAK,MAAM;AAClE,aAAK,kBAAkB,cAAc;AAAA,MACtC;AAAA,IACD,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,eAAe,UAAsC;AAC3D,QAAI,OAAO,aAAa,YAAY;AACnC,YAAM,IAAI,UAAU,6BAA6B;AAAA,IAClD;AACA,SAAK,qBAAqB;AAG1B,UAAM,eAAe,CAAC,WAAuB;AAC5C,UAAI,mBAAmB,QAAQ,KAAK,MAAM,GAAG;AAC5C,cAAM,iBAAiB,wBAAwB,QAAQ,KAAK,MAAM;AAClE,aAAK,qBAAqB,cAAc;AAAA,MACzC;AAAA,IACD,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,cAA4B;AAClC,WAAO,MAAM,YAAY;AAAA,EAC1B;AACD;AAGA,IAAM,oBAAuC,IAAI,sBAAsB;AAEvE,IAAO,gBAAQ;;;AEnIf,QAAQ,IAAI,sCAAsC;AAElD,IAAI;AACH,QAAM,UAAwB,cAAY,YAAY;AAEtD,MAAI,QAAQ,WAAW,GAAG;AACzB,YAAQ,IAAI,uBAAuB;AAAA,EACpC,OAAO;AACN,YAAQ,QAAQ,CAAC,QAAoB,UAAkB;AACtD,cAAQ,IAAI,UAAU,QAAQ,CAAC,GAAG;AAClC,cAAQ,IAAI,gBAAgB,OAAO,QAAQ,EAAE;AAC7C,cAAQ,IAAI,YAAY,OAAO,IAAI,SAAS,EAAE,EAAE,YAAY,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE;AAChF,cAAQ,IAAI,YAAY,OAAO,IAAI,SAAS,EAAE,EAAE,YAAY,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE;AAChF,cAAQ,IAAI,kCAAkC,OAAO,YAAY,EAAE;AACnE,cAAQ,IAAI,EAAE;AAAA,IACf,CAAC;AAED,YAAQ,IAAI,kBAAkB,QAAQ,MAAM,EAAE;AAC9C,YAAQ;AAAA,MACP;AAAA,IACD;AAAA,EACD;AACD,SAAS,OAAO;AACf,MAAI,iBAAiB,OAAO;AAC3B,YAAQ,MAAM,0BAA0B,MAAM,OAAO;AAAA,EACtD;AACD;",
|
|
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": ";AACA,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;;;ACE9B,SAAS,YAAY,OAAuB;AAC3C,SAAO,MAAM,SAAS,EAAE,EAAE,YAAY,EAAE,SAAS,GAAG,GAAG;AACxD;AAKA,SAAS,cAAc,QAAoB,QAA+B;AACzE,QAAM,YAAY,YAAY,OAAO,GAAG;AACxC,QAAM,YAAY,YAAY,OAAO,GAAG;AACxC,QAAM,YAAY,OAAO,IAAI,YAAY;AACzC,QAAM,YAAY,OAAO,IAAI,YAAY;AAEzC,SAAO,cAAc,aAAa,cAAc;AACjD;AAKA,SAAS,iBAAiB,QAAoB,SAAkC;AAC/E,SAAO,QAAQ,KAAK,CAAC,WAAW,cAAc,QAAQ,MAAM,CAAC;AAC9D;AAeO,SAAS,mBAAmB,QAAoB,QAAiC;AAEvF,MAAI,OAAO,kBAAkB,OAAO,eAAe,SAAS,GAAG;AAC9D,QAAI,iBAAiB,QAAQ,OAAO,cAAc,GAAG;AACpD,aAAO;AAAA,IACR;AAAA,EACD;AAGA,MAAI,OAAO,qBAAqB,OAAO,kBAAkB,SAAS,GAAG;AACpE,QAAI,CAAC,iBAAiB,QAAQ,OAAO,iBAAiB,GAAG;AACxD,aAAO;AAAA,IACR;AAAA,EACD;AAGA,MAAI,OAAO,iBAAiB,OAAO,cAAc,SAAS,GAAG;AAC5D,QAAI,CAAC,iBAAiB,QAAQ,OAAO,aAAa,GAAG;AACpD,aAAO;AAAA,IACR;AAAA,EACD;AAGA,MAAI,OAAO,kBAAkB,OAAO,KAAK,OAAO,cAAc,EAAE,SAAS,GAAG;AAC3E,QAAI,EAAE,OAAO,gBAAgB,OAAO,iBAAiB;AACpD,aAAO;AAAA,IACR;AAAA,EACD;AAEA,SAAO;AACR;AASO,SAAS,wBAAwB,QAAoB,QAAoC;AAC/F,MAAI,OAAO,kBAAkB,OAAO,gBAAgB,OAAO,gBAAgB;AAC1E,WAAO;AAAA,MACN,GAAG;AAAA,MACH,aAAa,OAAO,eAAe,OAAO,YAAY;AAAA,IACvD;AAAA,EACD;AACA,SAAO;AACR;;;AD9DA,IAAM,aAAa,cAAc,YAAY,GAAG;AAChD,IAAM,YAAY,QAAQ,UAAU;AAEpC,IAAM,cAAc,QAAQ,SAAS;AAOrC,SAAS,kBAA+B;AACvC,QAAMA,WAAU,cAAc,YAAY,GAAG;AAG7C,QAAM,YAAY,KAAK,aAAa,SAAS,WAAW,sCAAsC;AAE9F,MAAI;AACH,WAAOA,SAAQ,SAAS;AAAA,EACzB,QAAQ;AAEP,QAAI;AACH,YAAM,eAAeA,SAAQ,gBAAgB;AAC7C,aAAO,aAAa,WAAW;AAAA,IAChC,QAAQ;AACP,YAAM,IAAI;AAAA,QACT;AAAA,oBAA0D,SAAS;AAAA;AAAA;AAAA,MACpE;AAAA,IACD;AAAA,EACD;AACD;AAEA,IAAM,QAAQ,gBAAgB;AAM9B,IAAM,wBAAN,MAAyD;AAAA,EAChD,SAAyB,CAAC;AAAA,EAC1B,kBAA4C;AAAA,EAC5C,qBAAkD;AAAA;AAAA;AAAA;AAAA,EAKnD,eAAe,QAA8B;AACnD,QAAI,OAAO,WAAW,YAAY,WAAW,MAAM;AAClD,YAAM,IAAI,UAAU,0BAA0B;AAAA,IAC/C;AACA,SAAK,SAAS;AACd,UAAM,eAAe;AAAA,EACtB;AAAA;AAAA;AAAA;AAAA,EAKO,gBAAsB;AAC5B,UAAM,cAAc;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKO,YAAY,UAAmC;AACrD,QAAI,OAAO,aAAa,YAAY;AACnC,YAAM,IAAI,UAAU,6BAA6B;AAAA,IAClD;AACA,SAAK,kBAAkB;AAGvB,UAAM,YAAY,CAAC,WAAuB;AACzC,UAAI,mBAAmB,QAAQ,KAAK,MAAM,GAAG;AAC5C,cAAM,iBAAiB,wBAAwB,QAAQ,KAAK,MAAM;AAClE,aAAK,kBAAkB,cAAc;AAAA,MACtC;AAAA,IACD,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,eAAe,UAAsC;AAC3D,QAAI,OAAO,aAAa,YAAY;AACnC,YAAM,IAAI,UAAU,6BAA6B;AAAA,IAClD;AACA,SAAK,qBAAqB;AAG1B,UAAM,eAAe,CAAC,WAAuB;AAC5C,UAAI,mBAAmB,QAAQ,KAAK,MAAM,GAAG;AAC5C,cAAM,iBAAiB,wBAAwB,QAAQ,KAAK,MAAM;AAClE,aAAK,qBAAqB,cAAc;AAAA,MACzC;AAAA,IACD,CAAC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKO,cAA4B;AAClC,WAAO,MAAM,YAAY;AAAA,EAC1B;AACD;AAGA,IAAM,oBAAuC,IAAI,sBAAsB;AAEvE,IAAO,gBAAQ;",
|
|
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"}
|